Architecture
This page describes the operator’s components, where they live, and the constraints a change has to preserve. For using the operator rather than changing it, start from Operations.
One rule generates most of the rest: the controller never touches a database, and the process that touches a database never holds a Kubernetes API credential. Almost every boundary below is that rule being enforced somewhere.
What a deployment looks like
Section titled “What a deployment looks like”Before the components: the shape a platform owner has to plan for, and where to look when something is wrong. Each row is argued for further down the page.
| Question | Answer |
|---|---|
| How many installations per cluster? | Exactly one. The admission configurations are a fixed singleton, so a second release is refused rather than sharing them. |
| What does the manager watch? | Every namespace. It is a cluster-scoped controller over namespaced resources. |
| How is it made available? | Replicas within that one release. One is the active reconciler through a leader-election Lease; every ready replica serves the webhooks. |
| What contends with what? | Every resource addressing one database, whatever namespace it is in. They take turns through a Lease in one configurable coordination namespace. |
| What touches a database? | Only a Job, one per operation, in the resource’s own namespace. The manager never opens a database connection. |
| What can call the Kubernetes API? | Only the control plane. Operation Pods disable automatic API-token mounting. Their explicit Pod-bound token authenticates result delivery only; its audience must not be accepted by the Kubernetes API. See Plan results stay out of logs. |
| What stops SQL nobody approved? | For each family, a named enforcement point: Execution guarantees. |
| Where do I look when an Apply is uncertain? | The resource’s own status, and A migration run nobody accounted for for the family that cannot resolve it alone. |
The failure domains that follow from those answers are not the same size. Losing a manager replica costs a reconciler and no availability. Losing the release costs reconciliation for the whole cluster, while every database keeps whatever the last Apply left it holding. Losing a database costs the resources bound to it and nothing else: the operator’s own state lives in Kubernetes.
Words this page uses
Section titled “Words this page uses”Five of them are this project’s rather than Kubernetes’s, and the diagram below uses all five. Each links to the section that goes into it.
| Term | What it is | Where it lives |
|---|---|---|
| Operation claim | One unit of work a controller has decided to do – its type, a deterministic id, the inputs it was computed from, and the Job it will dispatch. Persisted before the Job exists, so a restart finds the claim rather than a Job nobody expected. | status.activeOperation; One operation, end to end |
| Result frame | The structured result document: protocol version, operation identity, child exit code and operation-specific output. By default, the receiver persists it in PtahResultRecord objects before acknowledgment, and a background consumer validates it for the controller. Only legacy delivery writes the frame to stdout. |
internal/runner, runner.ProtocolVersion; One operation, end to end |
| Realm | The set of resources that can mutate one physical database, whatever URL each of them uses to reach it. | Concurrency and coordination |
| Post-Apply verification | Reading the database back after a mutation to confirm what it did, as distinct from what the run claimed. | The schema lifecycle, The migration lifecycle |
| Protected table | A table a verification policy refuses to let a plan change, by name. Refusal happens where the plan is computed, so no SQL is produced for it. | protectedTables; Declarative reference data |
Four identities, and what invalidates each
Section titled “Four identities, and what invalidates each”They are easy to read as interchangeable evidence and they are not. Each answers a different question, and each is invalidated by a different event.
| Identity | Answers | Invalidated by |
|---|---|---|
| Coordination identity | Which realm a resource claims – derived from spec.target.engine and either the namespace with spec.target.coordinationKey or the PtahRealm in spec.target.realmRef, and from nothing a credential carries |
Editing any of those fields, which moves the resource to another realm |
| Target identity | The effective engine, host and port, database, username, and execution-relevant connection parameters derived from the Pod’s resolved database URL | Changing a bound value. Password rotation alone leaves it unchanged; replacing a backend behind an unchanged route is outside this identity |
| Lease epoch | Which uninterrupted interval of realm ownership a claim holds | The Lease lapsing and being acquired again, which loses continuity and discards any result produced across the change |
| Execution binding epoch | Which set of execution components a plan was computed under – controller-state version, Ptah version, executor image, runner protocol. The manager’s image and revision and the runner image are recorded on plans and Jobs and are not in it | Any one of those four changing, which retires the plans computed under the old set. A manager release that changes none of them keeps the epoch. A schema names what the retired epoch left behind – its plan and the Job its claim dispatched – in status.pendingBindingRetirement until both are tidied, and rotates no further until then (Retire an execution binding) |
Coordination identity selects the shared lock. Target identity binds the route and SQL session context supplied to the runner; it does not identify a physical database instance. The Lease epoch tracks continuous lock ownership, while the execution binding tracks the components that may run an approved plan.
The shape of the system
Section titled “The shape of the system”flowchart LR
subgraph cp["Control plane"]
manager["manager<br/>reconcilers + webhooks"]
receiver["result receiver<br/>in manager"]
rotator["ptah-cert-rotator"]
crd["ptah-crd-manager<br/>Helm hooks"]
end
subgraph api["Kubernetes API"]
kinds["PtahSchema, PtahMigration<br/>plans, approvals"]
results["PtahResultRecord"]
guards["typed policies<br/>+ webhooks"]
leases["Leases"]
end
subgraph dp["Data plane, one Job per operation"]
runner["ptah-runner<br/>bounds output"]
ptah["Ptah executor"]
end
registry[("OCI registry")]
database[("PostgreSQL / MySQL")]
manager -->|"claims, then creates"| dp
manager --> kinds
kinds --> guards
manager --> leases
runner --> ptah
ptah --> registry
ptah --> database
dp -->|"result over TLS"| receiver
receiver -->|"persist before acknowledgment"| results
results -->|"background read"| manager
rotator --> guards
crd --> kinds
classDef plane fill:#eef,stroke:#88a
class cp,api,dp plane
The manager decides and records. It creates Jobs and reads persisted results through bounded background consumers. It holds no database credential and has no permission to read the Secret that carries one.
Five programs ship from cmd/:
| Program | What it is |
|---|---|
manager |
The reconcilers for both resource families, plus the admission webhook server |
ptah-runner |
Runs inside every operation Pod: validates inputs, bounds output, redacts credentials, frames the result |
ptah-cert-rotator |
Issues and replaces the webhook serving certificates |
ptah-crd-manager |
The Helm hook and the runtime verifier: the reconcile that checks the stored state, stops the previous runtime when the manager image changes and updates the CRDs, on install, upgrade and rollback alike, and the init container that holds a manager until its CRDs and admission singleton match |
kubectl-ptah |
A read-only plugin that reconstructs a published plan for a person to read |
What the manager keeps in memory
Section titled “What the manager keeps in memory”A watch is a cache, and a cache holds whole objects. The controllers register watches for both resource families, their approvals, migration run acknowledgments, realms and the Jobs they own. They also watch ConfigMaps across every namespace, because a changed verification policy has to wake the resources bound to it.
That last watch is the one whose size nobody chooses. It would otherwise hold every application ConfigMap in the cluster and the ConfigMaps each Apply mounts its plan through, up to 8 MiB of SQL per applied plan; forty applied plans is 320 MiB of cached payload against a manager whose default limit is 256 MiB. So the cache empties a ConfigMap as it stores it, keeping the metadata that names it and dropping its data, its binary data and the managed fields that describe them.
Nothing reads those emptied objects. Every ConfigMap this operator acts on – a verification policy, an Apply’s plan projection – is read straight from the API server, because each is a decision a cache may not be current enough to make, and the manager’s client routes ConfigMap reads there as well so that a read added later cannot quietly start seeing an emptied object or build a second cache holding what the first one dropped. The plan chunks themselves are not watched at all, and the client routes their reads to the API server for the same reason: a cached read of one would start a cache of every chunk in the cluster.
The two resource families
Section titled “The two resource families”PtahSchema answers “what should the database look like?”. It observes, plans
against what it found, and converges — including the reference rows a
declaration names.
PtahMigration answers “which ordered versions have run?”. It reads the
recorded history, selects the pending sequence, and executes it in order.
Each family has three kinds at its core: the resource a person writes, the immutable plan the controller publishes, and the immutable approval a person creates to authorize exactly that plan.
| Family | Desired state | Published plan | Decision |
|---|---|---|---|
| Declared schema | PtahSchema |
PtahSchemaPlan |
PtahSchemaApproval |
| Versioned migrations | PtahMigration |
PtahMigrationPlan |
PtahMigrationApproval |
A schema plan’s SQL lives in a seventh kind, PtahSchemaPlanChunk: up to
sixteen immutable chunks the plan owns and binds by UID, size and digest, so
reading a plan takes one RBAC rule on that kind. The migration family has no
such kind, because a migration plan carries no SQL: it names the files and
their checksums, and the Apply reads them from the verified artifact. See
Immutable bindings.
The migration family has an eighth kind for its second decision.
PtahMigrationRunAcknowledgment is how a person settles a run the migration
recorded in status.unresolvedRun: an Apply whose effect on the database
nobody established. Admission stamps who created it, the controller takes it
only for the run it names, and the migration’s status.resolvedRun names that
person. It replaces a hand-written status update, which needed the manager’s
own authority and recorded nobody; the chart now refuses a write to the status
subresource of every operator kind from anyone but the manager. See
A migration run nobody accounted for.
A ninth kind belongs to neither family. PtahRealm is cluster-scoped and
written by an administrator: it names a database that resources in more than
one namespace manage, and lists the namespaces allowed to claim it. The manager
reads it and never writes it. See
Concurrency and coordination.
PtahResultRecord stores immutable intent, payload, and completion records for
durable runner result delivery. Results up to 256 KiB commit atomically in one
intent containing the exact bytes, digest, and operation binding. Larger results
use separate chunks and a completion record. Existing chunked publications retain
their layout when retried. Publication checks the complete payload before
acknowledgment; a consumer checks the binding, digest, and any chunk identities before
using it. The dedicated resource permits result reads without access to database
Secrets. Its payloads are confidential and must be excluded from ordinary reader
and author roles. Delivery is enabled by default. The receiver authenticates
the exact operation and Job/Pod, persists the complete result, and only then
acknowledges it. Bounded background consumers load results independently of
reconcile workers and Lease renewal. Repeated delivery returns the same receipt
without executing SQL again. Diagnostic logs are never a correctness fallback.
A leader-only collector preserves active and recovery-pinned records and waits
at least one hour after retirement before deleting eligible publications.
The API admission guard checks current recovery pins, source identity, Job
absence, retention age, and dependency order for every deletion. The collector
sends UID- and resource-version-bound requests; a denied or unavailable guard
retains the evidence.
Backup and recovery includes these
records and the receiver trust and rotation state.
The manager reconciles the two desired-state kinds, watches the two approval kinds, the run acknowledgments and the realms, and watches the verification-policy ConfigMaps resources point at, so an edited policy is noticed rather than waited out. A realm’s grant is read before every claim: a withdrawn grant refuses the claimant on the pass the change starts, and a new one lifts a standing refusal at that refusal’s re-check, within a minute. It does not watch the plan kinds or the chunks: it writes them, and a plan it wrote tells it nothing it did not already know.
The two families are separate kinds rather than modes of one, because their evidence differs: a schema plan binds an observed state and a desired state, and a migration plan binds a version sequence, per-migration checksums and the history it was computed against. One kind carrying both would make every field conditional on which controller wrote it, and would make approval and status ambiguous.
What they genuinely share is the mechanism, not the model: OCI tag resolution
and digest pinning (internal/ocireference), registry authentication and
custom transport trust, artifact verification and type enforcement
(internal/policy), Job construction (one workload.Builder, with a single
Pod skeleton that owns the hardening and the annotation envelope), result framing
(internal/runner), coordination realms and Leases (internal/targetlock),
and the route identity guard.
Where the rest of it is
Section titled “Where the rest of it is”This page is the shape. Each contract has one home, and the page that owns it is the one to change when the contract changes.
| Page | Owns |
|---|---|
| Reconciliation | Both lifecycles, phase by phase: what each operation is for, what blocks, what suspends |
| Execution and coordination | One operation from claim to evidence, the durable claims, and how two resources share a database |
| Plans and approvals | What a plan binds, how its bytes are stored, what a decision authorizes, and declared rows |
| Credentials and admission | Which process holds which credential, and what the admission contract refuses |
| Mutation lifecycle | The obligations a mutating operation carries, and where each family enforces them |
| Release lifecycle | Install, upgrade, rollback and uninstall, and the certificate handoffs inside them |
| Execution guarantees | Each promise mapped to the enforcement point in each family, and the test that proves it |
Where the code lives
Section titled “Where the code lives”| Concept | Package |
|---|---|
| Reconcilers for both families | internal/controller |
| What each operation type is, the dispatch sequence, what retiring a claim releases | internal/mutationlifecycle |
| Whether a Job is the one its claim built | internal/jobclaim |
| Bounded Job and Pod construction | internal/workload |
| Result framing, redaction, input validation | internal/runner |
| Machine-readable data-plane contracts | internal/dataplane |
| Canonical content identities | internal/fingerprint |
| Plan publication into immutable chunks, and an Apply’s projection of them | internal/planstore |
| Plan byte-size contract | internal/plancontract |
| Migration plan derivation and naming | internal/migrationplan |
| Approval and run-acknowledgment admission | internal/admission |
| The manager’s own write guard | internal/controllerwrite |
| Pod admission snapshots | internal/podintent |
| Verification policy binding | internal/policy |
| OCI reference parsing and pinning | internal/ocireference |
| Database realm Leases | internal/targetlock |
| Durable manager-state contract | internal/controllerstate |
| Certificate lifecycle | internal/certrotation |
| CRD, RBAC and release lifecycle | internal/crdupgrade |
Reading a stored plan back, for kubectl ptah and other programs |
pkg/planread |
Read-only views behind kubectl ptah |
internal/planview, internal/schemaview, internal/migrationview |
| Bounded-cardinality metrics | internal/telemetry |
| What the manager caches from a watch | internal/managercache |
How this is proven
Section titled “How this is proven”Unit tests measure this code. The envtest suites under test/envtest measure
the API contract against a real kube-apiserver and etcd: the CRDs’ schemas and
CEL rules, every ValidatingAdmissionPolicy the chart installs together with its
binding, and the manager’s webhooks behind the chart’s own webhook
configurations. Each policy is held to a request it refuses, the write it must
admit from the identity that makes it, and a mutation of its binding, its match
or the release values it carries under which those rows have to fail. They do not measure
what Kubernetes would do: envtest runs no built-in controllers, so nothing there
reconciles a Deployment into Pods or garbage-collects by owner reference.
What proves the claims on this page is the acceptance matrix, one job per
supported Kubernetes minor and suite, against kind with a real registry,
PostgreSQL and MySQL. support/e2e-suites.json is the single place the suites
and their phases are written down. make acceptance-coverage lists each
supported minor and suite, its phases, resource families, engine inputs and
scenarios, and the CI job that supplies its evidence. The strict
Kubernetes support gate is what says they passed,
and it fails on a skipped job, an incomplete matrix or a missing result rather
than reporting green.