deploy

package
v1.801.307 Latest Latest
Warning

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

Go to latest
Published: Jul 28, 2026 License: Apache-2.0 Imports: 47 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/*, 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.

gitops.go — GET /v1/deploy/gitops: the CD plane's OWN state, read from Hanzo CD's Application CRs (apps.hanzo.ai/v1alpha1, k8s.CDApplications).

This is deliberately NOT another workload board. Every other read in this package projects operator App CRs — one row per workload, its declared vs running image tag. A CD Application is the layer ABOVE that: the git source CD polls, the commit it last applied, and the deploys it has performed. The two disagree in exactly the case an operator most needs to see — main carries a new image pin, CD has not applied that commit yet, so every App CR still declares the old tag and the drift board is legitimately "Synced" while the deploy has not landed. Reading the App CRs can never surface that; only the Application's applied revision can.

`history` IS the recent-delivery feed: Hanzo CD records each applied revision with its start/finish, so the deploy log needs no second store and cannot drift from what actually happened.

SuperAdmin-gated (guard) like the rest of the dashboard writes and bootstrap: the CD plane is fleet infrastructure with no tenant dimension. READ-ONLY — the operator view observes CD, it never drives it (sync policy here is `automated` with selfHeal, so the plane reconciles itself; the actionable verb an operator has is the per-app reconcile already served by dashSync).

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 + trusted issuer set 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. A deployment whose trusted-issuer set omits this token's issuer 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/* surface (dashboard.go; no /api/, no inner /v1).

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

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 cloud.Router, 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 GitOpsApp

type GitOpsApp struct {
	Name           string           `json:"name"`
	Namespace      string           `json:"namespace"`
	Project        string           `json:"project,omitempty"`
	RepoURL        string           `json:"repoURL,omitempty"`
	Path           string           `json:"path,omitempty"`
	TargetRevision string           `json:"targetRevision,omitempty"`
	Revision       string           `json:"revision,omitempty"` // the commit last applied
	Sync           string           `json:"sync"`               // Synced|OutOfSync|Unknown
	Health         string           `json:"health"`             // Healthy|Degraded|Progressing|…
	ReconciledAt   string           `json:"reconciledAt,omitempty"`
	Automated      bool             `json:"automated"`
	SelfHeal       bool             `json:"selfHeal"`
	Resources      int              `json:"resources"`
	Operation      *GitOpsOperation `json:"operation,omitempty"`
	History        []GitOpsDeploy   `json:"history"`
}

GitOpsApp is one CD Application: what it tracks, what it has applied, and how that went.

type GitOpsDeploy

type GitOpsDeploy struct {
	ID         int64  `json:"id"`
	Revision   string `json:"revision"`
	StartedAt  string `json:"startedAt,omitempty"`
	DeployedAt string `json:"deployedAt,omitempty"`
	Automated  bool   `json:"automated"`
}

GitOpsDeploy is one revision Hanzo CD actually applied.

type GitOpsOperation

type GitOpsOperation struct {
	Phase      string `json:"phase"`
	Message    string `json:"message,omitempty"`
	StartedAt  string `json:"startedAt,omitempty"`
	FinishedAt string `json:"finishedAt,omitempty"`
	Revision   string `json:"revision,omitempty"`
}

GitOpsOperation is the LAST sync operation and how it ended — the honest answer to "did the most recent attempt succeed", which the sync verdict alone does not give (an Application is "Synced" to whatever revision it managed to apply).

type GitOpsPlane

type GitOpsPlane struct {
	Installed    bool        `json:"installed"`
	Reason       string      `json:"reason,omitempty"`
	Applications []GitOpsApp `json:"applications"`
}

GitOpsPlane is the reply. `installed` is false — with an empty list and a reason — when the CD CRD is not served in this cluster. That is a FACT about the cluster, not a failure of this request, so the caller can say "no CD plane here" instead of rendering an error it cannot act on. A genuine transport or RBAC failure still errors (k8sErr).

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

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