Documentation
¶
Overview ¶
Package platform is Hanzo PaaS: deploy containers to your own tenant namespace — builds, releases, environments, logs, custom domains.
It is the per-org container platform at /v1/platform — projects, applications, builds, deploys, environments, releases, logs and verified custom domains, each app reconciled into the caller's own tenant-<org> Kubernetes namespace.
Relationship to the sibling subsystems:
- fleet.go (/v1/platform/fleet) — the ADMIN fleet drift board: observes + deploys SYSTEM Service CRs across the platform namespaces, SuperAdmin only. It answers "what is the fleet running, and roll a tag."
- apps/projects (/v1/sites) — per-org STATIC sites (S3 hosting).
- apps/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 handlers below run natively on zip, so the binary keeps ONE router and every route stays behind the SanitizeIdentity trust boundary. That router is the ONE source of the published contract (openapi/ projects it). The Goa design module beside this package (apps/platform/design — its own go.mod, not built or tested here) is a SECOND source and has already drifted: it emits 15 operations against the 30 this package registers. Do not read it as the contract, and do not regenerate against it.
Index ¶
- func IsSemverTag(tag string) bool
- func Mount(app cloud.Router, deps cloud.Deps) error
- func Shutdown() error
- type AppView
- type Application
- type Build
- type Deployment
- type Domain
- type Drift
- type DriftFlag
- type DriftKind
- type DriftSeverity
- type EnvVarJSON
- type Observed
- type ProjectStore
- type ReleaseState
- type RunningApp
- type Store
- func (s *Store) AdvanceComputeMeter(ctx context.Context, id string, prev, now int64) (bool, error)
- func (s *Store) AllApplications(ctx context.Context) ([]Application, error)
- func (s *Store) Close() error
- func (s *Store) CreateApplication(ctx context.Context, a Application) error
- func (s *Store) CreateDomain(ctx context.Context, d Domain) error
- func (s *Store) DeleteApplication(ctx context.Context, org, projectID, slug string) (Application, bool, error)
- func (s *Store) DeleteDomain(ctx context.Context, org, appID, host string) (bool, error)
- func (s *Store) DeleteProjectApps(ctx context.Context, org, project string) ([]Application, error)
- func (s *Store) FinalizeLive(ctx context.Context, d Deployment, imageTag, namespace string, now int64) (bool, error)
- func (s *Store) GetApplication(ctx context.Context, org, projectID, slug string) (Application, error)
- func (s *Store) GetApplicationByID(ctx context.Context, org, id string) (Application, error)
- func (s *Store) GetBuild(ctx context.Context, org, id string) (Build, error)
- func (s *Store) GetDeployment(ctx context.Context, org, appID, id string) (Deployment, error)
- func (s *Store) GetDomain(ctx context.Context, org, appID, host string) (Domain, error)
- func (s *Store) InsertBuild(ctx context.Context, b Build) error
- func (s *Store) InsertDeployment(ctx context.Context, d Deployment) error
- func (s *Store) ListAllApplications(ctx context.Context, org string) ([]Application, error)
- func (s *Store) ListApplications(ctx context.Context, org, projectID string) ([]Application, error)
- func (s *Store) ListBuildingDeployments(ctx context.Context) ([]Deployment, error)
- func (s *Store) ListBuildsByOrg(ctx context.Context, org string) ([]Build, error)
- func (s *Store) ListDeployments(ctx context.Context, org, appID string) ([]Deployment, error)
- func (s *Store) ListDeploymentsByOrg(ctx context.Context, org string) ([]Deployment, error)
- func (s *Store) ListDomainsByApp(ctx context.Context, org, appID string) ([]Domain, error)
- func (s *Store) LookupDomain(ctx context.Context, host string) (Domain, bool, error)
- func (s *Store) MarkDomainVerified(ctx context.Context, org, appID, host string, now int64) (bool, error)
- func (s *Store) NextVersion(ctx context.Context, appID string) (int, error)
- func (s *Store) RunningApps(ctx context.Context) ([]RunningApp, error)
- func (s *Store) StampComputeMeter(ctx context.Context, org, id string, now int64) error
- func (s *Store) UpdateApplication(ctx context.Context, a Application) error
- func (s *Store) UpdateBuild(ctx context.Context, b Build) error
- func (s *Store) UpdateDeployment(ctx context.Context, d Deployment) error
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"`
Role string `json:"role"` // operator spec.role (sql|kv|generic|ingress|…) or "" — the one declared class field
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 console renders the same shape the Dokploy board did.
type Application ¶
type Application struct {
ID string
Org string
ProjectID string // the IAM project NAME (owner=org,name) this app lives under
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
MinScale int // container-serverless autoscaling floor (0 ⇒ no HPA, fixed Replicas). Set by /v1/run.
MaxScale int // container-serverless autoscaling ceiling (0 ⇒ no HPA). Set by /v1/run.
StorageGB int // persistent volume size in GiB (0 ⇒ stateless, no volume at all)
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 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 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 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 (fleet.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).
type ProjectStore ¶
type ProjectStore interface {
List(ctx context.Context, org string) ([]*model.Project, error)
// Get returns nil (no error) when the project does not exist — IAM's convention.
Get(ctx context.Context, org, name string) (*model.Project, error)
Exists(ctx context.Context, org, name string) (bool, error)
}
ProjectStore is platform's org-scoped view of the IAM-owned project lifecycle. Every method is scoped to org (the validated owner) and keyed by the project name — there is no platform-minted project id; the IAM identity is (org,name), and that name is the app-scope key AND the operator CR `part-of` label. The value type is IAM's canonical *model.Project, so there is exactly ONE project model across the binary. ProjectStore is READ-ONLY on purpose. A project is created and deleted at /v1/iam/projects, so platform has no Create or Delete to call — the constraint lives in the type rather than in a convention someone can forget. What platform needs of a project is only ever: does it exist, and what is under it.
type ReleaseState ¶ added in v1.801.350
type ReleaseState struct {
// ID is the build id returned by the 202.
ID string `json:"id"`
// Image is the tag the release publishes on success.
Image string `json:"image"`
// Version is that tag without the leading "v".
Version string `json:"version"`
// SHA is the commit the release pinned.
SHA string `json:"sha"`
// Status is "releasing", "released" or "failed".
Status string `json:"status"`
// Reached is the last pipeline step completed: built, smoked, tagged, pinned.
Reached string `json:"reached,omitempty"`
// Error is why it stopped, when it failed.
Error string `json:"error,omitempty"`
// StartedAt / EndedAt are unix seconds.
StartedAt int64 `json:"startedAt"`
EndedAt int64 `json:"endedAt,omitempty"`
}
ReleaseState is what a release id can be asked about. A 202 hands back an id, so the id has to mean something after the request returns — otherwise a release that dies in the detached pipeline is indistinguishable from one still running.
func ReleaseByID ¶ added in v1.801.350
func ReleaseByID(id string) (ReleaseState, bool)
ReleaseByID returns a recorded release. found=false once it has aged out.
func Releases ¶ added in v1.801.350
func Releases() []ReleaseState
Releases returns the recorded releases, newest first.
type RunningApp ¶
type RunningApp struct {
App Application
MeteredAt int64
}
RunningApp is a live application paired with its compute watermark — the input to the periodic compute meter (computemeter.go). MeteredAt is the unix second through which this app's live compute has already been billed.
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 projects/provisioning.
func (*Store) AdvanceComputeMeter ¶
AdvanceComputeMeter moves an app's compute watermark from prev to now, but ONLY if it still equals prev (compare-and-set). Returns true when THIS call advanced it — the caller then, and only then, meters the (now−prev) span. The CAS makes a double-tick idempotent: a second tick sees the already-advanced watermark, the UPDATE matches 0 rows, and no second debit is emitted. Single-writer already serializes ticks; the CAS makes the never-double-charge property hold regardless.
func (*Store) AllApplications ¶
func (s *Store) AllApplications(ctx context.Context) ([]Application, error)
AllApplications returns every app across ALL orgs — the orphan reaper's sweep set (orphans.go). Unscoped by org for the same reason RunningApps is: the reaper runs cluster-wide, then acts on each row against that row's OWN org. Every other read in this file carries `org=?`; this one is deliberate and is used only by a cluster-wide sweep that never serves a request.
func (*Store) CreateApplication ¶
func (s *Store) CreateApplication(ctx context.Context, a Application) error
func (*Store) CreateDomain ¶
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) 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 ¶
DeleteDomain releases a custom domain claim, org+app scoped. Reports whether a row was removed.
func (*Store) DeleteProjectApps ¶
DeleteProjectApps removes every application/deployment/build/domain under a project (keyed by the IAM project NAME) in ONE transaction, all scoped to org, and returns the removed apps so the caller can tear down their operator CRs. The project row itself lives in IAM (see projects.go) — deleting it is the caller's separate ProjectStore.Delete; this wipes only platform's app tree.
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 ¶
GetApplicationByID resolves an app by (org,id) for deployment/build lookups.
func (*Store) GetDeployment ¶
func (*Store) GetDomain ¶
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) InsertDeployment ¶
func (s *Store) InsertDeployment(ctx context.Context, d Deployment) error
func (*Store) ListAllApplications ¶
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 (*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 ¶
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 (*Store) ListDeploymentsByOrg ¶
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 ¶
ListDomainsByApp returns every custom domain claimed for an app, org-scoped.
func (*Store) LookupDomain ¶
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 (*Store) RunningApps ¶
func (s *Store) RunningApps(ctx context.Context) ([]RunningApp, error)
RunningApps returns every app currently `live` across ALL orgs, paired with its compute watermark — the single-writer compute meter's sweep set. It is the running-deployment analogue of the build reconciler's ListBuildingDeployments: unscoped by org on purpose (the meter runs cluster-wide on the writer pod, then meters each app to its OWN org). Only `live` apps run in-cluster and consume billable compute — a `stopped`/`building`/`draft` app has no running footprint.
func (*Store) StampComputeMeter ¶
StampComputeMeter unconditionally sets an app's compute watermark to now, scoped to org. It is called on a resume (stop→start): the meter then charges only the new live span, never the stopped gap the app just came back from. FinalizeLive stamps the same watermark for the deploy→live path; together they guarantee the meter never bills time an app was not `live`.
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) UpdateDeployment ¶
func (s *Store) UpdateDeployment(ctx context.Context, d Deployment) error