platform

package
v1.786.82 Latest Latest
Warning

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

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

Documentation

Overview

applylive.go — the ONE version-monotonic deploy mechanic shared by the image-source path (deployImage) and the git build reconciler.

Both paths must write the operator Service CR (applyService) AND advance the app's live pointer (FinalizeLive). FinalizeLive is already an atomic, monotonic CAS so the recorded live version can never regress. The remaining gap (RED LOW-1) is the CR write itself: two concurrent deploys of the SAME app could interleave so that an OLDER deploy's applyService lands AFTER a NEWER one already went live, leaving the live Service CR image lagging the recorded live version. applyLive closes that gap by running supersede-check → applyService → FinalizeLive as one per-app-serialized critical section: an older deploy that loses the race is superseded and never writes its (older) CR.

console.go — the flat, top-level console aggregates that the Hanzo Cloud Console's Environments / Pipelines / Builds / Releases pages render. They are NOT a new data model: every row is DERIVED from the SAME per-org project / application / deployment / build records the /v1/platform surface already owns (platform.go, deploy.go, store.go). One store, one deploy mechanic, four read-only projections:

  • environment → a deploy target: the distinct Application.Environment values across the org's apps, each aggregating the apps that target it.
  • pipeline → an app's build/deploy configuration + its latest run (one pipeline per application).
  • build → a REAL arcd BuildKit build record (platform_builds); the git build step of a deploy. Never fabricated — real records or an honest empty.
  • release → a deployment that was actually applied to the cluster (status deploying|live): a released image tag on an app/environment.

All four are GET-only. A pipeline/build/release is CREATED only through the ONE existing write path — POST /v1/platform/projects/{p}/apps (create app) and .../deploy (trigger a build+deploy) — so there is exactly one way to make them, never a duplicate trigger here. Environments are a scope derived from apps, not a standalone record, so there is nothing to create/delete independently either.

Every handler is org-scoped through the SAME validated-principal gate as the rest of the platform surface (s.tenant → requires c.User(); org is the only tenancy key). The response is a `{ "<plural>": [...] }` object — the exact shape the console FE normalizers read (r.environments / r.pipelines / r.builds / r.releases).

deploy.go — the deploy lifecycle for /v1/platform: build (arcd BuildKit) + deploy (operator Service CR) + start/stop + deployment history/logs.

Two source kinds, ONE deploy mechanic (write the operator Service CR into tenant-<org>; the operator reconciles):

  • source=image — no build. The CR is applied immediately with the requested image tag; the operator rolls it. This is the fully end-to-end path.
  • source=git — a build is required. An in-cluster BuildKit Job is launched (client-go, the arcd model) to build the repo and push the per-tenant image; the deployment lands "building". The build watcher that flips "building"→"live" by applying the CR with the built image is phase 2 (paas-in-cloud.md §4) — until then the git path stops honestly at "building" with the real Job reference, never a fabricated "live".

Every handler is org-scoped (s.tenant) and every cluster write targets tenant-<org> derived from the validated org — never a request value.

domains.go — customer-self-serve domain management for /v1/platform apps: the default host, org-subtree hosts, and VERIFIED BYO custom domains (`yourco.com`), all rendered into the app's operator Service CR ingress so the operator materializes the Ingress + cert-manager TLS. Two host classes, one render source (the app's DomainsJSON):

  • default / org-subtree host (`*.<org>.<sitesHost>`) — STRUCTURAL: the org owns its whole subtree by construction, so these are active the moment they are added; no ownership proof is possible or needed.
  • BYO custom host (arbitrary `yourco.com`) — needs an OWNERSHIP proof before it can be rendered, or a tenant could claim a host it does not control (the RED hijack boundary). The customer publishes a per-claim TXT token at `_hanzo-challenge.<host>`; only someone who controls that zone's DNS can, so a matching token proves control (the DNS-01 model). Until verified, the claim is a pending row that is NEVER in the ingress. Global uniqueness (one org per host) is the `platform_domains.host` PRIMARY KEY.

Every handler is org-scoped through s.tenant and mutates only tenant-<org>.

k8s.go — the cluster-facing half of /v1/platform: the ONE deploy path.

A user application is deployed by writing an operator hanzo.ai/v1 `Service` CR into the caller's OWN tenant namespace; the Hanzo operator reconciles it into a Deployment + Service + Ingress (+ HPA/PDB) on DOKS. cloud never reimplements a deployer — it writes one CR, exactly like clients/paassvc patches system CRs, but here every object lives in `tenant-<org>` where the org is the gateway-minted, IAM-VALIDATED tenant (c.Org()), never a value from the request body or path. That derivation is the whole cross-tenant isolation boundary: a caller cannot name another org's namespace because the namespace is not an input.

Builds (git-source apps) launch an in-cluster BuildKit Job (the arcd model, buildkit-job.ts) via client-go — no GitHub builders. When the cluster / CI prerequisites are absent the subsystem fails CLOSED with the real reason (never status-theater), matching paassvc.

logs.go — REAL deployment logs for /v1/platform (the deploy.go phase-2 gap).

deploymentLogs previously returned only the recorded status timeline + a Job reference, ending with a "(live BuildKit Job logs stream in phase 2)" note. This closes that gap: it streams the ACTUAL pod logs from the cluster — the BuildKit Job's pod for a git build, and the running app's pod for a deployed app — via the typed CoreV1 client's Pods().GetLogs subresource. Everything stays operator- consistent and org-scoped: the app's pods are read from tenant-<org> (the same isolation namespace the Service CR lives in), the build pod from the central build namespace by the deterministic job-name label. It NEVER fabricates output — an unreachable cluster or an absent pod yields the honest recorded timeline instead, exactly as the rest of the subsystem degrades.

Package platform is the Hanzo Cloud PaaS control plane: the per-org, user-facing Platform-as-a-Service, mounted natively in the unified cloud binary at /v1/platform (HIP-0106). It is the Go port of the standalone Dokploy (platform.hanzo.ai) tRPC backend — the culmination of "one binary ships all of Hanzo Cloud."

Relationship to the sibling subsystems:

  • clients/paassvc (/v1/paas) — the ADMIN fleet drift board: observes + deploys SYSTEM Service CRs across the platform namespaces, global-admin only. It answers "what is the fleet running, and roll a tag."
  • clients/projectsvc (/v1/projects) — per-org STATIC sites (S3 hosting).
  • clients/platform (/v1/platform) — THIS: per-org CONTAINER apps. Users create projects + applications, build them (arcd BuildKit) and deploy them (operator hanzo.ai/v1 Service CR into their OWN tenant-<org> namespace).

All three share the ONE deploy mechanic — write an operator CR, let the operator reconcile — but /v1/platform is per-tenant: every route is scoped to the gateway-minted, IAM-VALIDATED X-Org-Id (c.Org()); the deploy namespace is DERIVED from that org (tenant-<org>), never taken from the request. A tenant can never read, build, or deploy into another org's namespace. That is the red-team bar and it is structural: cross-tenant identifiers are simply not inputs to any handler.

The API is designed-first in Goa (clients/platform/design; `goa gen` emits the OpenAPI 3 contract at clients/platform/design/gen/http/openapi3.*). The runtime handlers below implement that contract natively on zip so the binary keeps ONE router and stays behind the SanitizeIdentity trust boundary.

reconcile.go — the git build→deploy handoff (formerly deploy.go's "phase 2").

deployGit launches an in-cluster BuildKit Job and lands the deployment "building"; it does NOT block the HTTP request waiting for the build. This reconciler is the ONE owner of what happens next: it periodically scans every deployment still "building", checks its Job, and on success applies the operator Service CR with the built image — the SAME applyService the image-source path uses — flipping build→succeeded, deployment→deploying, app→live. The operator then reconciles the rollout.

Why a reconciler and not a per-deploy goroutine: state lives in the store, not in memory, so a cloud restart mid-build RESUMES cleanly (the next tick re-reads "building" rows). It is idempotent (applyService is create-or-update; a row is advanced off "building" once handled) and org-scoped (every write targets tenant-<row.Org>, derived from the row, never a request value).

secrets.go — KMS-sealed secret env vars for /v1/platform apps (closes the createApp/setEnv 501). A user env var flagged `secret:true` is NEVER stored in platform.db and NEVER rendered inline into the operator Service CR. Instead the ONE secret invariant holds end-to-end: a secret value lives ONLY in KMS and the operator-materialized k8s Secret; cloud's control-plane DB holds a masked placeholder and cloud never handles the plaintext at deploy time.

The path a secret takes:

  1. SEAL — cloud seals the value into its embedded KMS (deps.KMS, AES-256-GCM envelope, clients/kms) at the ORG-SCOPED coordinate /orgs/<org>/platform/ <app>/<KEY> (kmsSecretRef). Plaintext is sealed before it touches disk, never logged, never echoed back over the API (toAppView masks it). If KMS is unavailable the write FAILS CLOSED — plaintext never lands in the DB.
  2. PROVISION — on app-create/deploy, cloud ensures the tenant's PER-TENANT KMS credential exists (ensureTenantKMSAuth): a machine identity whose token carries owner=<org>, projected into tenant-<org> as the creds Secret the CR references. It is the ONE privileged step; when unavailable it degrades to an honest "pending" (never a shared reader), so no cross-tenant identity is ever created.
  3. DECLARE — cloud writes a canonical KMSSecret CR (secrets.lux.network/ v1alpha1) into tenant-<org> pointing the operator at /v1/kms/orgs/<org>/ secrets/platform/<app> (projectSlug=<org>, explicit keys). The operator logs in with the per-tenant creds (/v1/kms/auth/login → IAM), reads each key back from cloud's KMS, and materializes the managed Secret `<app>-env`. Cloud's org-scope guard admits the read ONLY for owner==<org>, so a tenant can read ONLY its own scope. Best-effort: a missing CRD/RBAC/credential degrades to a "pending" status, never a failed deploy.
  4. MOUNT — the Service CR renders each secret env as `valueFrom.secretKeyRef` → that Secret (optional, so the pod boots even before the sync lands); the hanzo operator mounts it into the Deployment env. The pod reads the secret from the k8s Secret — cloud is never in the plaintext path at runtime.

Everything is org-scoped: the KMS coordinate, the auth identity, the KMSSecret CR, and the managed Secret all live under the VALIDATED tenant, never a request value — the same cross-tenant boundary as the rest of /v1/platform, enforced at cloud's ONE auth boundary (SanitizeIdentity + the kmssvc org-scope guard).

validate.go — the boundary validators + resource-bound policy for /v1/platform.

Three concerns, one home (DRY), because all three feed the SAME privileged cluster path (a BuildKit Job + operator Service CR in tenant-<org>):

  1. build-input validation — repo.url / dockerfile / git-ref are the ONLY free-form strings that flow into the in-cluster build. They are validated here so they can never be misread as a separate buildctl argument, a shell token, a path escape, or a git-context fragment break. Combined with the exec-form argv in launchBuildJob (no `sh -c`), this closes OS command injection AND the `--output`/`--opt` override class (CRIT-1).
  2. replica bounds — clampReplicas caps a tenant's requested replica count so a single app cannot request an unbounded Deployment (part of MED-3).
  3. namespace resource bounds — a ResourceQuota + LimitRange are applied to every tenant namespace so a tenant's TOTAL footprint is capped in the cluster regardless of what slipped past the per-object clamp (defense in depth for MED-3). Concurrent build fan-out is capped in k8s.go using resourceLimits.maxConcurrentBuilds below.

Every bound is a hard-coded safe default, operator-overridable via env — the values are resolved ONCE at k8sClient construction so a request path never re-reads the environment and the tests are deterministic.

Index

Constants

This section is empty.

Variables

This section is empty.

Functions

func Mount

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

Mount wires the /v1/platform surface onto app per HIP-0106.

func Shutdown

func Shutdown() error

Shutdown closes the platform store. Idempotent. Mirrors the projectsvc/paassvc Shutdown contract so the serve layer releases subsystem resources uniformly.

Types

type Application

type Application struct {
	ID            string
	Org           string
	ProjectID     string
	Slug          string
	Name          string
	Description   string
	Environment   string
	Source        string // git | image
	RepoURL       string
	RepoBranch    string
	RepoProvider  string
	ImageRepo     string
	ImageTag      string
	BuildType     string
	Dockerfile    string
	Port          int
	Replicas      int
	EnvJSON       string
	DomainsJSON   string
	Status        string
	Namespace     string
	CurrentDeploy string
	CreatedAt     int64
	UpdatedAt     int64
}

Application is a deployable unit under a project (Dokploy: application). It deploys as an operator hanzo.ai/v1 Service CR in the tenant-<org> namespace. EnvJSON/DomainsJSON hold JSON-encoded []EnvVar / []string; secrets are never stored here (secret env is rejected at the boundary until KMS sealing lands).

type Build

type Build struct {
	ID            string
	Org           string
	ApplicationID string
	DeploymentID  string
	Status        string
	Image         string
	JobName       string
	LogsRef       string
	CreatedAt     int64
	UpdatedAt     int64
}

Build is one arcd (in-cluster BuildKit) build record (Dokploy fork: build_job).

type Deployment

type Deployment struct {
	ID            string
	Org           string
	ApplicationID string
	Version       int
	Status        string
	Source        string
	Commit        string
	Image         string
	BuildID       string
	Message       string
	CreatedAt     int64
	UpdatedAt     int64
}

Deployment is one immutable build+deploy attempt for an application, versioned monotonically per app (Dokploy: deployment).

type Domain

type Domain struct {
	Host       string
	Org        string
	ProjectID  string
	AppID      string
	AppSlug    string
	Status     string // pending | verified
	Token      string
	CreatedAt  int64
	VerifiedAt int64
}

Domain is a BYO custom (arbitrary-host) domain a tenant has claimed for an application — `yourco.com` / `app.yourco.com`. The org's own hanzo.app subtree hosts and the app's default host are STRUCTURAL (they live in the app's DomainsJSON and are validated by suffix), so they are NOT rows here; this table exists for the two things a custom host needs that a subtree host does not:

  • GLOBAL UNIQUENESS — Host is the PRIMARY KEY, so exactly one org can ever claim `yourco.com` (the site_hosts model). A second org's claim collides.
  • an OWNERSHIP-VERIFICATION lifecycle — Status pending → verified, gated on a DNS challenge Token the customer publishes at `_hanzo-challenge.<host>`.

A custom host is rendered into the app's operator ingress (added to DomainsJSON) ONLY once its row is `verified` — an unverified claim never reaches the CR.

type EnvVarJSON

type EnvVarJSON struct {
	Key    string `json:"key"`
	Value  string `json:"value"`
	Secret bool   `json:"secret"`
}

EnvVarJSON is the JSON shape of one application env var as stored/served.

type Project

type Project struct {
	ID          string
	Org         string
	Slug        string
	Name        string
	Description string
	CreatedAt   int64
	UpdatedAt   int64
}

Project is the org-scoped root of the deploy tree (Dokploy: project).

type Store

type Store struct {
	// contains filtered or unexported fields
}

Store is the platform metadata database. ONE SQLite file ({DataDir}/platform.db) holds every org's records; tenancy is the org column. MaxOpenConns(1) serializes writes against the file lock without busy retries, matching projectsvc/provisioning.

func (*Store) Close

func (s *Store) Close() error

Close closes the underlying database. Idempotent-safe via the caller.

func (*Store) CreateApplication

func (s *Store) CreateApplication(ctx context.Context, a Application) error

func (*Store) CreateDomain

func (s *Store) CreateDomain(ctx context.Context, d Domain) error

CreateDomain claims a custom host for an app. Host is the PRIMARY KEY, so a second claim of the SAME host (by any org, any app) collides → errConflict. This is the global-uniqueness boundary (two orgs can never both claim `yourco.com`).

func (*Store) CreateProject

func (s *Store) CreateProject(ctx context.Context, p Project) error

func (*Store) DeleteApplication

func (s *Store) DeleteApplication(ctx context.Context, org, projectID, slug string) (Application, bool, error)

DeleteApplication removes an app plus its deployments/builds, scoped to org.

func (*Store) DeleteDomain

func (s *Store) DeleteDomain(ctx context.Context, org, appID, host string) (bool, error)

DeleteDomain releases a custom domain claim, org+app scoped. Reports whether a row was removed.

func (*Store) DeleteProject

func (s *Store) DeleteProject(ctx context.Context, org, slug string) (Project, []Application, bool, error)

DeleteProject removes a project plus every application/deployment/build under it, in one transaction, all scoped to org. Reports whether a project was deleted and returns the removed apps so the caller can tear down their CRs.

func (*Store) FinalizeLive

func (s *Store) FinalizeLive(ctx context.Context, d Deployment, imageTag, namespace string, now int64) (bool, error)

FinalizeLive advances an application to `live` at deployment d — atomically and MONOTONICALLY. It is the ONE way the platform marks an app live, shared by the synchronous image deploy (deploy.go) and the async git build reconciler (reconcile.go). The app is moved to this deployment ONLY when d's version is at least the version of the app's currently-live deployment: the guard is a single conditional UPDATE (SQLite serializes it under MaxOpenConns(1)), so an OLDER version whose write races in LATE can never overwrite a NEWER one already live — no read-then-write TOCTOU. Returns whether the app advanced (false ⇒ a newer version is already live, i.e. this deployment was superseded, or the app row is gone). Every predicate is org-scoped.

func (*Store) GetApplication

func (s *Store) GetApplication(ctx context.Context, org, projectID, slug string) (Application, error)

GetApplication resolves an app by (org, project_id, slug) — the org is ALWAYS in the predicate so a caller can never read another tenant's app.

func (*Store) GetApplicationByID

func (s *Store) GetApplicationByID(ctx context.Context, org, id string) (Application, error)

GetApplicationByID resolves an app by (org,id) for deployment/build lookups.

func (*Store) GetBuild

func (s *Store) GetBuild(ctx context.Context, org, id string) (Build, error)

func (*Store) GetDeployment

func (s *Store) GetDeployment(ctx context.Context, org, appID, id string) (Deployment, error)

func (*Store) GetDomain

func (s *Store) GetDomain(ctx context.Context, org, appID, host string) (Domain, error)

GetDomain resolves a custom domain scoped to (org, app_id, host) — the tenant path used by verify/delete. Org is always in the predicate so a caller can never read another tenant's domain row.

func (*Store) GetProject

func (s *Store) GetProject(ctx context.Context, org, slug string) (Project, error)

func (*Store) GetProjectByID

func (s *Store) GetProjectByID(ctx context.Context, org, id string) (Project, error)

GetProjectByID resolves a project by (org,id) — used to re-verify tenancy when an application row references its project.

func (*Store) InsertBuild

func (s *Store) InsertBuild(ctx context.Context, b Build) error

func (*Store) InsertDeployment

func (s *Store) InsertDeployment(ctx context.Context, d Deployment) error

func (*Store) ListAllApplications

func (s *Store) ListAllApplications(ctx context.Context, org string) ([]Application, error)

ListAllApplications returns every application under org across ALL its projects, newest-updated first. It is the org-wide input to the console aggregates (environments/pipelines/builds/releases in console.go). Org is the ONLY predicate — the SAME tenancy boundary as every other query — so it can never surface another tenant's apps.

func (*Store) ListApplications

func (s *Store) ListApplications(ctx context.Context, org, projectID string) ([]Application, error)

func (*Store) ListBuildingDeployments

func (s *Store) ListBuildingDeployments(ctx context.Context) ([]Deployment, error)

ListBuildingDeployments returns every deployment still in the "building" state across ALL orgs, oldest first. It is the input to the build reconciler (reconcile.go), which owns the git build→deploy handoff. Because the query is keyed on status (not org), the reconciler resumes in-flight builds after a cloud restart — the goroutine is stateless; the store IS the state. Every write the reconciler then makes is still org-scoped (tenant-<row.Org>).

func (*Store) ListBuildsByOrg

func (s *Store) ListBuildsByOrg(ctx context.Context, org string) ([]Build, error)

ListBuildsByOrg returns every build record for org across ALL apps, newest-created first. Org-wide input to the console builds aggregate (console.go); org is the only tenancy predicate. These are REAL BuildKit build records — the aggregate never fabricates a build that did not run.

func (*Store) ListDeployments

func (s *Store) ListDeployments(ctx context.Context, org, appID string) ([]Deployment, error)

func (*Store) ListDeploymentsByOrg

func (s *Store) ListDeploymentsByOrg(ctx context.Context, org string) ([]Deployment, error)

ListDeploymentsByOrg returns every deployment for org across ALL apps, newest-created first. Org-wide input to the console releases/pipelines aggregates (console.go); org is the only tenancy predicate.

func (*Store) ListDomainsByApp

func (s *Store) ListDomainsByApp(ctx context.Context, org, appID string) ([]Domain, error)

ListDomainsByApp returns every custom domain claimed for an app, org-scoped.

func (*Store) ListProjects

func (s *Store) ListProjects(ctx context.Context, org string) ([]Project, error)

func (*Store) LookupDomain

func (s *Store) LookupDomain(ctx context.Context, host string) (Domain, bool, error)

LookupDomain resolves a host GLOBALLY (across every org) — the uniqueness probe. It is the ONLY store read not scoped to a caller's org, and exists solely so the add-domain handler can answer "is this host already claimed, and by whom" to decide a 409. The caller MUST NOT echo a foreign row's details back to a tenant (it reveals only that the host is taken, never by whom).

func (*Store) MarkDomainVerified

func (s *Store) MarkDomainVerified(ctx context.Context, org, appID, host string, now int64) (bool, error)

MarkDomainVerified flips a pending custom domain to verified, org+app scoped. Reports whether a row advanced (false ⇒ no such pending row for this tenant).

func (*Store) NextVersion

func (s *Store) NextVersion(ctx context.Context, appID string) (int, error)

func (*Store) UpdateApplication

func (s *Store) UpdateApplication(ctx context.Context, a Application) error

UpdateApplication overwrites the mutable fields of an app; org+project+slug+id are immutable and form the tenancy/identity key.

func (*Store) UpdateBuild

func (s *Store) UpdateBuild(ctx context.Context, b Build) error

func (*Store) UpdateDeployment

func (s *Store) UpdateDeployment(ctx context.Context, d Deployment) error

func (*Store) UpdateProject

func (s *Store) UpdateProject(ctx context.Context, p Project) error

Jump to

Keyboard shortcuts

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