Documentation
¶
Overview ¶
actions.go — the two GitOps write actions.
POST /v1/deploy/{name}/rollback — pin the CR image tag to a prior clean semver.
It REUSES the P1 release seam (cloud.OnServiceRelease → clients/paas
releaseService), so the clean-semver gate + idempotent spec.image patch live
in exactly ONE place; the operator reconciles the rollout.
POST /v1/deploy/{name}/sync — request an operator reconcile now by touching the
CR (an annotation bump the operator's watch observes). Today the CR is the
desired source, so sync = nudge-reconcile; when git.hanzo.ai is the source it
becomes apply-desired-from-git, same endpoint.
applications.go — GET /v1/deploy/applications: the fleet list. Each operator Service CR is one Application row: its declared image version, the running version observed from the live Deployment, the reconciled health, and the sync verdict (declared == running ⇒ Synced, else OutOfSync). The console renders this as the ArgoCD application list.
dashboard.go — the ArgoCD-UI-compatible projection API at /v1/deploy/api/*, fed the App-CR projection (projection.go). NO argocd api-server, NO repo-server, NO redis, NO stored Application/AppProject CRD — every response is synthesized from our operator App CRs. The FRONTEND is NOT here: the monochrome dashboard ships as the `hanzoai/spa`-based `cd-ui` App CR served at cd.hanzo.ai/ (base-href /); this plane is only the same-origin API it calls (no /api/, no inner /v1):
GET /v1/deploy/settings → AuthSettings (auth disabled; IAM gates at the edge)
GET /v1/deploy/session/userinfo → {loggedIn:true,...}
GET /v1/deploy/version → VersionMessage
GET /v1/deploy/account/can-i/* → {"value":"yes"}
GET /v1/deploy/applications → ApplicationList (projected)
GET /v1/deploy/applications/{name} → Application (projected)
GET /v1/deploy/applications/{name}/resource-tree → ApplicationTree
POST /v1/deploy/applications/{name}/{sync,rollback} → request App-CR reconcile
SECURITY — TENANT-SCOPED reads, SuperAdmin-only writes, fail-closed (scope.go): the READ projections (applications list/detail/resource-tree, clusters, projects, stream) resolve the request's scope (resolveScope) — a SuperAdmin sees the whole fleet, a validated org member sees ONLY its own org's apps (hanzo.ai/org label, tenant-<org> namespace), anyone else 403s. The WRITE actions (sync/rollback) and the argocd bootstrap (settings/version/can-i) stay SuperAdmin-only (guard). The argocd UI's own auth is disabled because IAM owns identity at the edge (the SPA is public static assets, the data is scoped). AppProject → IAM/Org (no argocd RBAC): projects are REFLECTED read-only from the IAM-owned (org,name) Project resource.
Package gitops mounts the native GitOps control plane at /v1/deploy — the ArgoCD-grade deploy dashboard for the operator-managed fleet, made native to the cloud binary and parallel to /v1/git (the native git server).
Each operator hanzo.ai/v1 App CR IS a GitOps Application: the desired state declared for one workload, which the Hanzo operator reconciles into a Deployment + Service + Ingress (+ HPA/PDB/Pods). This plane OBSERVES that reconciliation the way ArgoCD observes a synced Application —
GET /v1/deploy/applications — the fleet list: name, declared version,
health, sync, per app.
GET /v1/deploy/{name}/tree — the owned-resource tree (ownerRef edges)
with per-node health + sync.
GET /v1/deploy/{name}/resource/{ref} — one node's live manifest + a
desired-vs-live diff.
GET /v1/deploy/{name}/logs — the app's current pod logs.
POST /v1/deploy/{name}/rollback — pin the CR image tag to a prior semver
(the operator reconciles the rollout).
POST /v1/deploy/{name}/sync — request an operator reconcile now.
SECURITY — the projection READS are TENANT-SCOPED and the WRITES stay SuperAdmin- only, all fail-closed on the SAME identity boundary the rest of cloud trusts (resolveScope, scope.go — validated principal + injective provisioning.SanitizeOrg + the c.IsAdmin() SuperAdmin predicate): a SuperAdmin sees/mutates the whole fleet, a validated org member sees ONLY its own org's apps (hanzo.ai/org label), and the reconcile writes (sync/rollback) remain SuperAdmin-only. Secret objects are never surfaced (no node, no manifest) so the tree can never leak materialized env. The user-facing per-org PaaS is /v1/platform; this is the platform-operator console the admin dashboard consumes, now also serving a read-only per-org reflection.
GitOps note (the follow-on seam): today the CR is the desired-state source and a rollback/rollout PATCHES it directly (P1's RegisterServiceReleaser), so deploys work now. The end-state is true GitOps on OUR native git — RegisterPushBuilder commits the CR image-tag change to the manifest repo on git.hanzo.ai (github.com/hanzoai/git) and this engine syncs that repo → cluster with self-heal. The desired-vs-live diff below is already structured for that: it reads a desired source that is "cluster last-applied" now and becomes the git.hanzo.ai manifest later, with no shape change. See deployDesiredTODO.
detail.go — the three per-app DETAIL projections the ArgoCD SPA's application view calls (and 404-toasts when absent): sync windows, revision metadata, and the LIVE resource-tree stream. All THREE are TENANT-SCOPED exactly like dashApp/dashResourceTree — resolveScope + findNamespace decide visibility, so a normal org reads only its OWN apps' detail (a cross-tenant name is a clean 404, no existence oracle), a SuperAdmin reads the whole fleet, and an unvalidated caller fails closed. There is ONE scoping path (scope.go); nothing here forks it.
engine.go embeds the argo gitops-engine (github.com/hanzoai/deploy/ gitops-engine, the fork's independently-importable submodule) in-process, so the cloud binary reconciles git → cluster the way the retired argocd application-controller did — three-way merge (server-side apply), scoped prune, drift-correction, health, sync status — with NO separate argocd process and NO redis. It is the write/reconcile half of /v1/deploy; the existing routes are the read/visualize half.
The apply-set is scoped by a tracking LABEL (deploy.hanzo.ai/instance): the isManaged predicate that drives pruning returns true ONLY for live objects carrying THIS instance's label, so a prune can never delete an App CR (or any object) this plane did not create. This is the prune-safety boundary for the 60+ live App CRs — the exact property universe-crs enforced with prune:false, kept here and made explicit.
Enablement is opt-in and fail-safe (DEPLOY_ENGINE_ENABLED, default off): the first deploy of this binary is inert for the reconcile path, so it ships dark and is turned on deliberately after the shadow proof — mirroring the operator's gate discipline and the argocd shadow-then-flip cutover.
engine_mount.go wires the embedded gitops-engine (engine.go) into the /v1/deploy surface: a SuperAdmin-gated, one-shot reconcile endpoint that renders the configured git source and syncs it → cluster. This is the write half of /v1/deploy that replaces the retired universe-crs Application — the operator still renders each App CR into workloads (the domain half).
Fail-safe: the whole path is gated by DEPLOY_ENGINE_ENABLED (default off), so the first deploy of this binary is inert and the engine is turned on deliberately after the shadow proof.
health.go — per-resource health, the ArgoCD `pkg/health` vocabulary made native (Healthy / Progressing / Degraded / Suspended / Missing / Unknown). It is a PURE function over one live object, so the list + tree derive an honest per-node health with no cluster round-trip beyond the object already read.
P2b swaps the internals for github.com/argoproj/gitops-engine pkg/health (health.GetResourceHealth) for exact ArgoCD parity; the CODES emitted here are already those strings, so the wire contract the console consumes does not change.
login.go — the sign-in round trip for the deploy plane at cd.hanzo.ai.
THE PROBLEM. Every /v1/deploy route is SuperAdmin-gated on c.IsAdmin(), which SanitizeIdentity mints ONLY from a validated IAM principal whose org IS the reserved admin org. The dashboard SPA is served at cd.hanzo.ai/ and calls this plane same-origin — but the IAM session cookie is minted host-only on hanzo.id, so a session established at hanzo.id or admin.hanzo.ai is never presented to cd.hanzo.ai. With no sign-in of its own the whole surface 403s and there is no way in. This file IS the way in.
GET /v1/deploy/login — start: redirect into IAM's authorize endpoint GET /v1/deploy/callback — finish: exchange the code, mint the session cookie GET /v1/deploy/logout — clear the session cookie
WHAT IT MINTS — NOT A SECOND SESSION MECHANISM. The callback stores the IAM access-token JWT in the `__Host-hanzo_iam_token` cookie: the FIRST name in cloud's cookieTokenNames, which SanitizeIdentity already reads, independently verifies (signature/issuer/audience/expiry against the IAM JWKS) and turns into the same principal a Bearer would. So this adds exactly one thing — a way to PUT the token in the browser for this host. The gate, the validation, and the SuperAdmin predicate are untouched; a forged cookie is still just an invalid JWT, and a forged X-User-IsAdmin header is still stripped on ingress.
MINT ONLY WHAT THIS DEPLOYMENT WILL ACCEPT. The callback runs the exchanged token through cloud's OWN validator (cloud.NewTokenValidator — the same JWKS, issuer set and audience allowlist the boundary uses) BEFORE writing the cookie, and makes the admin-org decision on those VERIFIED claims. This is not defence in depth against IAM; it is the thing that makes a misconfiguration fail FAST and LOUD. The audience allowlist is env-overridable (jwtAudiencesFromEnv REPLACES the baked default), so a deployment whose CLOUD_JWT_AUDIENCES / GATEWAY_ALLOWED_AUDIENCES omits this console's client_id would otherwise mint a cookie the boundary refuses on the very next request — 403 → document-bounce to sign-in → IAM session still live → instant code → mint → 403, looping until the browser gives up. Validating here turns that infinite loop into one clear error naming the real reason.
PUBLIC CLIENT, PKCE. The deploy plane holds no client secret: it drives IAM's authorization-code flow with PKCE S256 (RFC 7636), which IAM accepts with an empty client_secret when the code carries a challenge. A secret is still sent when one is configured, for a deployment that registers a confidential client.
CSRF. The `state` is a fresh 256-bit nonce echoed into a short-lived, HttpOnly, Secure, SameSite=Lax cookie alongside the PKCE verifier and the return path. The callback accepts a code ONLY when the returned state equals the cookie's nonce (constant time), so a login-CSRF — an attacker completing THEIR authorization in the victim's browser — is refused. The cookie is the only store, so the flow survives any replica handling the callback.
logs.go — GET /v1/deploy/{name}/logs: the app's current pod logs, streamed from the newest running pod via the typed CoreV1 GetLogs subresource. The operator labels the workload it renders for an App CR with app.kubernetes.io/instance=<name>, so that selects the app's pods; the most-recently-started pod is read (the current rollout). Optional ?container= selects a container; ?tail= bounds the lines. Never fabricates output — an unreachable cluster or absent pod yields an honest 200 with an empty tail + the reason, not invented logs.
projection.go — the App-CR → ArgoCD `Application` READ PROJECTION.
The CTO decision (have-both): serve the full ArgoCD React UI, but feed it a projection of our operator `App` CRs shaped as ArgoCD `Application`s. There is NO stored Application/AppProject CRD — each App CR IS projected on the fly, its resource tree + health synthesized from the SAME readers the native /v1/deploy routes use (listAppCRs/getAppCR/buildTree/resourceHealth), with no repo-server and no redis. App CRs stay the single source of truth; the Application shape exists only at this API layer.
These `argo*` types are the MINIMAL ArgoCD v1alpha1 JSON the React app renders (list + detail + tree). Distinct from the native `Application` (applications.go) which backs the native /v1/deploy/applications surface — this backs the ArgoCD-UI-compatible /v1/deploy/api/v1/* surface.
resource.go — GET /v1/deploy/{name}/resource/{ref}: one tree node's live manifest plus a desired-vs-live diff.
{ref} is the canonical "group:kind:namespace:name" token the tree emits on each node, so the console round-trips it back verbatim. The kind must be in the closed registry (kindGVR) and the namespace a platform namespace, and the object must belong to {name}'s tree (it IS the CR, or carries an ownerRef/label tying it to the app) — so the endpoint can never be steered at an arbitrary cluster object. Secrets are not in the registry, so their manifests are never returned.
desiredSource: today "last-applied" (the object's kubectl last-applied-config annotation) or "none". When git.hanzo.ai becomes the manifest source of truth (RegisterPushBuilder → commit → engine sync), desiredSource becomes "git" with the SAME diff shape. P2b replaces the field-strip diff with gitops-engine pkg/diff (three-way) for exact ArgoCD parity.
scope.go — the TENANT SCOPE of a /v1/deploy request, and the IAM-owned project reflection.
This plane was SuperAdmin-only: it listed App CRs across ALL platform namespaces, hard-coded spec.project = "default", and read no org/project labels. It is now TENANT-AWARE and IAM-MAPPED. Every operator App CR already carries the tenant + project labels (clients/platform serviceCR + the fleet crs/*.yaml stamp them); this plane READS them and scopes each list/detail read to the caller's org, while a SuperAdmin keeps the whole-fleet view.
The tenant boundary is the SAME one the rest of cloud trusts (clients/platform .tenant / clients/s3.tenant): the gateway-minted, IAM-validated identity headers (c.IsAdmin/c.Org), the injective provisioning.SanitizeOrg normalizer, and the principal.Validated gate. There is NO third slug rule — resolveScope keys the SAME (org → tenant-<org>) boundary the PaaS writes into.
Projects are owned by Hanzo IAM (hanzo.id), the ONE source of truth for the org-scoped (Owner,Name) Project resource. This plane REFLECTS them read-only via the clean iam's in-process project store (github.com/hanzoai/iam/pkg/store over the embedded IAM's orm.DB, no HTTP hop) — mirroring clients/platform/projects.go — and never persists a CD-side project row. The retired Casdoor iam-v1 object store is GONE.
sites.go — projects the static-plane SITES into the fleet list alongside the pod-backed services. A site has no App CR and no Deployment: it is a `staticFiles` Middleware (its S3 origin, `spec.staticFiles.root: s3://cdn/<slug>`) plus an IngressRoute (its public host), served straight from S3 by the ingress. Without this, GET /v1/deploy/applications showed only the ~72 App CRs and every site — including cd.hanzo.ai itself — was invisible. Each site becomes one Application row with Role:"site", so the CD dashboard renders the WHOLE delivery surface.
A site is desired-state-in-sync by construction: it is served from exactly the S3 prefix its Middleware declares, so there is no declared-vs-running image drift to report (Version == RunningVersion == "static" ⇒ Synced). Health is whether the site is actually reachable: a staticFiles Middleware with a matching IngressRoute host is Healthy; one defined but never routed is Missing.
stream.go — GET /v1/deploy/stream/applications: the ArgoCD applications watch as Server-Sent Events. The applications view opens this the moment it loads and keeps it open for live fleet updates; a 404 here makes the SPA error-toast.
The stream emits one ADDED event per current App CR — the SAME projection dashAppList serves (listAppCRs + runningVersions + projectApp: one source, one projection) — then watches the App CRs and forwards ADDED/MODIFIED/DELETED as they occur, holding the connection open with periodic keep-alives. Every watch + goroutine it starts is bound to the request and torn down on return, so a client disconnect leaks nothing. Read-only and TENANT-SCOPED (resolveScope: SuperAdmin streams the whole fleet, a validated org member only its own org's apps); it fails closed (403 unauthorized, 503 when no cluster client is configured) and degrades to keep-alive only (the initial state still renders) if the watch verb is not granted.
tree.go — GET /v1/deploy/{name}/tree: the owned-resource tree for one Application, the ArgoCD ApplicationTree shape (a FLAT node list with parentRefs edges; the console renders the DAG). The root is the Service CR; depth-1 nodes are the operator-owned Deployment/Service/Ingress/HPA/PDB/ConfigMap; depth-2 are the Deployment's ReplicaSets and their Pods. Ownership is by ownerReferences.uid with a name-equals-app fallback (some operator-rendered children are named after the CR). Secret objects are never included — the tree cannot leak env.
P2b swaps buildTree's cluster walk for github.com/argoproj/gitops-engine pkg/cache (ClusterCache.GetManagedLiveObjs / hierarchy) for a watch-backed tree; the Node shape the console consumes does not change.
Index ¶
Constants ¶
const ( SyncSynced = "synced" SyncOutOfSync = "out-of-sync" SyncUnknown = "unknown" )
Sync codes — the ArgoCD sync vocabulary, lowercased for the wire.
const ( HealthHealthy = "healthy" HealthProgressing = "progressing" HealthDegraded = "degraded" HealthSuspended = "suspended" HealthMissing = "missing" HealthUnknown = "unknown" )
Health codes — the ArgoCD health vocabulary, lowercased for the wire.
Variables ¶
This section is empty.
Functions ¶
Types ¶
type Application ¶
type Application struct {
Name string `json:"name"`
Namespace string `json:"namespace"`
Env string `json:"env"` // main|test|dev
Role string `json:"role,omitempty"` // spec.role after the kind collapse (App)
Repository string `json:"repository"`
Version string `json:"version"` // declared: spec.image.tag
RunningVersion string `json:"runningVersion"` // observed from the Deployment
Health string `json:"health"` // healthy|progressing|degraded|suspended|missing|unknown
HealthMessage string `json:"healthMessage,omitempty"`
Sync string `json:"sync"` // synced|out-of-sync|unknown
Phase string `json:"phase,omitempty"`
Endpoints []string `json:"endpoints"`
}
Application is one fleet row. Shapes the exact fields the console list consumes.
type Node ¶
type Node struct {
ResourceRef
UID string `json:"uid,omitempty"`
CreatedAt string `json:"createdAt,omitempty"`
Health string `json:"health"`
HealthMessage string `json:"healthMessage,omitempty"`
Sync string `json:"sync,omitempty"`
Version string `json:"version,omitempty"` // image tag for a workload node
ParentRefs []ResourceRef `json:"parentRefs,omitempty"`
}
Node is one resource in the tree: its ref, ownerRef parents, and derived health/sync (+ image version for a workload). ArgoCD ResourceNode shape.
type PruneFuse ¶ added in v1.801.79
type PruneFuse struct {
MaxDeletions int // absolute cap on objects pruned in one reconcile
MaxRatio float64 // cap as a fraction of the managed set (0..1)
}
PruneFuse bounds how much a single reconcile may delete — the circuit breaker against a silent empty/partial render sweeping the fleet. Both limits are checked; either one trips the fuse. Zero disables that check.
type ResourceRef ¶
type ResourceRef struct {
Group string `json:"group"`
Version string `json:"version"`
Kind string `json:"kind"`
Namespace string `json:"namespace"`
Name string `json:"name"`
Ref string `json:"ref"`
}
ResourceRef identifies one node — the round-trip token the resource endpoint parses. Ref is the canonical "group:kind:namespace:name" string.