Documentation
¶
Overview ¶
Package paas mounts the native, in-process Hanzo PaaS control plane at /v1/paas/*: the "one and only one way to deploy" made native to the cloud binary. It is the Go port of the standalone Dokploy-based platform's build→deploy lifecycle (pkg/platform/src/services/ci/deploy-executor.ts + services/apps/inventory.ts + db/schema/apps-drift.ts), collapsed into an in-process cloud subsystem exactly like clients/ml is the k8s bridge for the Kubeflow CRDs.
The deploy mechanism is the SAME one the operator already reconciles: a merge-patch of the operator `Service` CR's `.spec.image`. No second deployer is invented; the Hanzo operator owns the rollout. This module only observes the declared/running/latest tags per service (the drift board) and flips the one CR field a deploy changes — the identical contract the Node deploy-executor implemented, now native.
drift.go is the PURE half: it derives the drift verdict for one observed service row and performs no IO. It is a faithful port of `pkg/platform/src/db/schema/apps-drift.ts` so the two implementations can never disagree about what "drift" means (one way to compute drift, period). The cluster reader (paas.go) owns observing the tags; this file only interprets them.
paas.go — the cluster-facing half of the native Hanzo PaaS control plane.
It mounts /v1/paas/* on the unified cloud binary and reads the operator's `hanzo.ai/v1` `App` CustomResource — the one workload kind the fleet runs on:
GET /v1/paas/apps — the fleet drift board (inventory.ts): list every
operator App CR across the platform namespaces,
read declared vs running tag + health from the CR
(+ its status), and attach the drift verdict
(drift.go / apps-drift.ts).
GET /v1/paas/apps/:app — one app row by CR name.
POST /v1/paas/apps/:app/deploy— zero-downtime ROLLING RESTART of the app's
Deployment (the `kubectl rollout restart`
mechanism): re-pulls the DECLARED image, recreates
pods gracefully. It never changes the declared
TAG (that stays a git commit, the one thing Hanzo
CD's selfHeal reconciles), so there is no drift to
revert. This is `hanzo deploy`.
GET /v1/paas/health — real k8s reachability + App CRD presence.
SECURITY — every route is authorized off ONE IAM identity, exactly like the /v1/runner build path (clients/platform/runner.go): the guard admits a validated principal (principal.Validated) who is a SuperAdmin OR an OrgAdmin, and each handler then CONFINES a non-super caller to its own org's platform namespaces (scopedNamespaces, keyed on principal.Org — never a client header). A SuperAdmin observes/acts on the whole fleet; an OrgAdmin only on the namespaces its org owns; a plain member or an unauthenticated caller is refused 403. So a tenant admin can never observe — or restart — another org's, or a platform, app, and the platform operator drives the board off a plain `hanzo login` with no shared token. The user-facing per-app PaaS view still lives in console; this is the CLI/operator surface.
k8s client: built in-process from the in-cluster service account (rest.InClusterConfig) with a KUBECONFIG fallback for local/dev — the identical construction clients/ml uses. When no kubeconfig is resolvable the subsystem mounts anyway and every endpoint fails closed (503 + the real init error; the health route reports "degraded"), never status-theater.
release.go — the first-party release seam. build.go's RegisterServiceReleaser is the inversion that lets a build-completion path (clients/platform/release.go, or any package-cloud caller) request a rollout with no cloud⇄paas import cycle.
The App CRs in the platform namespaces are declared in universe git (infra/k8s/operator/crs/) and reconciled by Hanzo CD with selfHeal, so a direct spec.image patch is reverted on the next sync. releaseService therefore refuses and names the one way to roll a tag: commit it to the manifest. The clean-semver gate (splitReleaseImage) still validates the request so a caller gets an honest, specific error.
Index ¶
Constants ¶
This section is empty.
Variables ¶
This section is empty.
Functions ¶
func IsSemverTag ¶
IsSemverTag reports whether tag is a strict `vX.Y.Z` semver tag.
Types ¶
type AppView ¶
type AppView struct {
ID string `json:"id"` // <org>/<app>/<env>, e.g. hanzoai/iam/main
Org string `json:"org"` // image namespace, e.g. hanzoai
App string `json:"app"` // service / CR name, e.g. iam
Env string `json:"env"` // main|test|dev
Repo string `json:"repo"` // owner/repo, e.g. hanzoai/iam
Registry string `json:"registry"`
DeclaredTag string `json:"declaredTag"`
RunningTag string `json:"runningTag"`
LatestTag string `json:"latestTag"`
Health string `json:"health"` // green|yellow|red|"" (unknown)
Phase string `json:"phase"` // operator status.phase (Running/…)
Cluster string `json:"cluster"`
Namespace string `json:"namespace"`
Endpoints []string `json:"endpoints"`
Drift Drift `json:"drift"`
}
AppView is one service row on the drift board: the observed tags + topology + the derived drift verdict. It is the Go analogue of the platform's `AppView` (apps-api.ts) so console renders the same shape the Dokploy board did.
type Drift ¶
type Drift struct {
Severity DriftSeverity `json:"severity"`
Flags []DriftFlag `json:"flags"`
}
Drift is the drift verdict for one observed service row: the ordered flags plus the rolled-up severity (apps-drift.ts `Drift`).
func ComputeDrift ¶
ComputeDrift is the full drift verdict (flags + rolled-up severity) for one observed service row (apps-drift.ts `computeDrift`). Flags is always non-nil so the JSON encodes `[]`, never `null`.
type DriftFlag ¶
type DriftFlag struct {
Kind DriftKind `json:"kind"`
Severity DriftSeverity `json:"severity"`
Message string `json:"message"`
}
DriftFlag is a single drift finding: its kind, severity, and a human-readable reason (apps-drift.ts `DriftFlag`).
func ComputeDriftFlags ¶
ComputeDriftFlags derives the drift flags for one observed service row, exactly per the platform contract (apps-drift.ts `computeDriftFlags`).
Detection rules (each independent; a row may trip several):
- floating-declared — DeclaredTag is set but not vX.Y.Z. The reconciler refuses non-semver declarations, so this is hard drift. (When the declaration itself is floating, comparing it against LatestTag for "stale" is meaningless, so stale is suppressed in that case.)
- floating-running — RunningTag is set but not vX.Y.Z: the cluster is running a floating image. Hard drift.
- stale — DeclaredTag and LatestTag are both known semver and differ: a newer release exists that is not yet declared.
- un-rolled — DeclaredTag and RunningTag are both known and differ: the declaration has not reached the cluster yet.
- no-release — a DeclaredTag exists but no GH Release was found (ReleaseURL "").
- zero-assets — a GH Release exists (ReleaseURL set) but ReleaseAssets == 0.
Tags are compared verbatim (the reader stores reality un-normalized); no ordering is assumed beyond equality — matching the contract.
type DriftKind ¶
type DriftKind string
DriftKind enumerates the kinds of drift from the platform contract (apps-drift.ts `DriftKind`). Each value is independent — one service row can carry several at once (e.g. a floating running tag with a zero-asset release).
const ( // DriftStale — declared ≠ latest: a newer release exists but is not declared. (yellow) DriftStale DriftKind = "stale" // DriftUnrolled — running ≠ declared: the cluster has not rolled to the declared tag. (yellow) DriftUnrolled DriftKind = "un-rolled" // DriftFloatingDeclared — declaredTag is not strict semver; the reconciler would refuse it. (red) DriftFloatingDeclared DriftKind = "floating-declared" // DriftFloatingRunning — runningTag is not strict semver; policy violation on the cluster. (red) DriftFloatingRunning DriftKind = "floating-running" // DriftNoRelease — no GH Release found for the declared tag. (red) DriftNoRelease DriftKind = "no-release" // DriftZeroAssets — GH Release exists but shipped 0 assets. (red) DriftZeroAssets DriftKind = "zero-assets" )
type DriftSeverity ¶
type DriftSeverity string
DriftSeverity is the aggregate drift severity. "ok" = no flags; otherwise the max over flags.
const ( SeverityOK DriftSeverity = "ok" SeverityYellow DriftSeverity = "yellow" SeverityRed DriftSeverity = "red" )
func DriftSeverityOf ¶
func DriftSeverityOf(flags []DriftFlag) DriftSeverity
DriftSeverityOf rolls a list of flags up to a single severity (red > yellow > ok). Mirrors apps-drift.ts `driftSeverity`.
type Observed ¶
type Observed struct {
DeclaredTag string // what SHOULD run — spec.image.tag on the operator Service CR
RunningTag string // what ACTUALLY runs — observed from the CR status / Deployment
LatestTag string // newest released tag (GH release reader; empty until wired)
ReleaseURL string // GH Release URL for DeclaredTag (empty ⇒ no-release)
ReleaseAssets int // asset count on the GH Release (0 ⇒ zero-assets)
}
Observed is the minimal set of already-observed tag fields the drift derivation reads — mirrors the `Pick<App, …>` the TS `computeDrift` accepts. The reader (paas.go) fills these from the cluster; the release fields are populated by the GH-release reader (a follow-up), so today they are the honest zero value (ReleaseURL == "" ⇒ no-release, exactly like the un-populated TS columns).