deploy

package
v1.801.150 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Jul 21, 2026 License: Apache-2.0 Imports: 44 Imported by: 0

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 in-process object store (embedded IAM, no HTTP hop) — mirroring clients/platform/projects.go — and never persists a CD-side project row.

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

View Source
const (
	SyncSynced    = "synced"
	SyncOutOfSync = "out-of-sync"
	SyncUnknown   = "unknown"
)

Sync codes — the ArgoCD sync vocabulary, lowercased for the wire.

View Source
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

func Mount

func Mount(app *zip.App, deps cloud.Deps) error

Mount wires /v1/deploy/* onto app. Every handler gates on c.IsAdmin() first.

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.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL