Documentation
¶
Overview ¶
Package paassvc 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 speaks to the SAME operator surface the standalone platform's deploy-executor drove: the `hanzo.ai/v1` `services` CustomResource. Two responsibilities, both a straight port of the Node platform:
GET /v1/paas/apps — the fleet drift board (inventory.ts): list every
operator Service 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 service row by CR name.
POST /v1/paas/apps/:app/deploy— deploy a new image tag by merge-patching the
Service CR's `.spec.image` (deploy-executor.ts).
The operator reconciles the rollout; cloud never
reimplements a deployer.
GET /v1/paas/health — real k8s reachability + Service CRD presence.
SECURITY — every route is GLOBAL-ADMIN ONLY, fail-closed, gated on the SAME predicate the rest of cloud uses: c.IsAdmin() (true only for a JWT-validated principal whose org is the admin org, matching the gateway's admin-guard — see clients/admin). Unlike clients/ml (per-tenant namespaces), the PaaS control plane reads and mutates SYSTEM Service CRs across the whole fleet, so it is admin-only: a tenant must never patch another org's — or a platform — service. The user-facing PaaS view lives in console2; users never call this 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.
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 console2 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).