Approve a plan
With apply set to OnApproval, nothing runs until a person approves that exact plan.
What it shows. What an approval is bound to, and how much of that binding you never type.
Where it starts. An empty database and a namespace with the lab's Secrets. The schema is read from a ConfigMap, the delivery a team uses when its manifests already carry the files; Apply a schema shows the same resource reading an artifact.
A replay of a verified run, not a live cluster. 12 checks held while it was recorded. Play types it out in the frame below; the transcript is the same session, already there.
# The resource reads its schema from a ConfigMap, and asks for a person -- apply OnApproval.$ cat demo/manifests/manual-approval/storefront.yaml# The storefront schema, read from a ConfigMap, and applied only once a person# has approved the exact plan.## Nothing in it is a credential or names a namespace: kubectl apply -n puts it# in yours. It names a ConfigMap holding the SQL and a Secret holding the# database URL, and the operator reads both from that namespace.apiVersion: operator.ptah.run/v1alpha1kind: PtahSchemametadata: name: storefrontspec: target: engine: PostgreSQL # Every resource that can reach this database uses this key, so two of # them cannot plan against it at once. coordinationKey: demo/storefront/primary urlFrom: name: demo-database key: url desired: configMap: name: storefront-v1 items: - key: schema.sql path: schema.sql policy: # Nothing runs until a person approves the plan. apply: OnApproval allowDestructive: false driftSeverity: all interval: 1m execution: activeDeadlineSeconds: 300 failureRetryInterval: 10s connectTimeout: 30s # Put the SQL in the ConfigMap it names, and apply it.$ kubectl -n "$NAMESPACE" create configmap storefront-v1 --from-file=schema.sql=demo/schemas/v1.sqlkubectl -n "$NAMESPACE" apply -f demo/manifests/manual-approval/storefront.yamlconfigmap/storefront-v1 createdptahschema.operator.ptah.run/storefront created # The operator plans, and stops. Wait until it does.$ kubectl ptah wait schema/storefront -n "$NAMESPACE" --for approvalptahschema/storefront in namespace demo reached approval. Phase: AwaitingApproval Generation: 1, observed by the operator Database: last read at <time> Plan: ptah-plan-44f2a9565324d384d873dc64, fingerprint sha256:44f2a9565324d384d873dc646ec0169218f8d93bdf82708b2b2171c04d7a81c0kubectl ptah approve schema/storefront -n demo reviews it. # It has a plan and it is waiting, and the condition says what it is waiting for.$ kubectl ptah schema storefront -n "$NAMESPACE"Schema: demo/storefrontPhase: AwaitingApprovalReady: False, AwaitingApproval: Plan is waiting for approvalPolicy: apply OnApproval, allowDestructive false, driftSeverity allSource: ConfigMap storefront-v1Snapshot: ptah-src-675290a01840eb1c6a64d05eContent: sha256:5a00a478af9f3c4a52e37aff33e574f580bcb6b94eb68860feb483b509b09667Observed: <time>Drift: 1 in 1 categories, highest safe tables_added 1 safe Plan ptah-plan-44f2a9565324d384d873dc64, 1 statement, stored <time> not approved ApprovalRequired: True, Waiting: Create an approval bound to the current plan fingerprint Approvals: none Conditions: TYPE STATUS REASON SINCE MESSAGE EngineSupported True SupportedEngine <time> Database engine "PostgreSQL" is supported Suspended False Active <time> Reconciliation is active ReconciliationFailed False Succeeded <time> The latest operation completed ArtifactResolved True SnapshotTaken <time> ConfigMap storefront-v1 was copied into snapshot ptah-src-675290a01840eb1c6a64d05e Ready False AwaitingApproval <time> Plan is waiting for approval DatabaseReachable True Observed <time> Database schema was observed DriftDetected True ScopedChanges <time> Managed scope requires 1 schema statements; highest severity safe InSync False ScopedChanges <time> The authoritative managed scope differs from the verified artifact ApprovalRequired True Waiting <time> Create an approval bound to the current plan fingerprint PlanReady True Published <time> Exact plan bytes are stored in immutable chunks kubectl ptah plan storefront -n demo prints the SQL this plan holds.kubectl events -n demo --for ptahschema/storefront prints its Events. # Nothing was created. This is what waiting looks like from the database.$ kubectl -n "$NAMESPACE" exec deploy/demo-psql -- psql -c "\dt"Did not find any relations. # Approve it. It shows the exact plan, asks, and creates the approval bound to it.$ kubectl ptah approve schema/storefront -n "$NAMESPACE" --name storefront-v1 --waitPtahSchema: demo/storefront, UID <uid>Plan: ptah-plan-44f2a9565324d384d873dc64, UID <uid>Fingerprint: sha256:44f2a9565324d384d873dc646ec0169218f8d93bdf82708b2b2171c04d7a81c0 Schema: demo/storefrontPlan: current (ptah-plan-44f2a9565324d384d873dc64)Fingerprint: sha256:44f2a9565324d384d873dc646ec0169218f8d93bdf82708b2b2171c04d7a81c0Content digest: sha256:87ad80131250ebde009044c23ea01867aaffc87376ed2da7f1512e0cbb2bfffeDialect: postgresStatements: 1Destructive: falsePrivileges: noneStored: <time> Statement 1 of 1: safe (does not remove data or tighten constraints)CREATE TABLE "customers" ( "id" bigint PRIMARY KEY NOT NULL, "email" text NOT NULL); Approve plan ptah-plan-44f2a9565324d384d873dc64 of schema/storefront in demo? Type yes to approve: yesApproved: PtahSchemaApproval demo/storefront-v1, recorded by admission as kubernetes-admin at <time>.It binds plan ptah-plan-44f2a9565324d384d873dc64, fingerprint sha256:44f2a9565324d384d873dc646ec0169218f8d93bdf82708b2b2171c04d7a81c0.Waiting up to 5m0s for ptahschema/storefront in namespace demo to reach applied.ptahschema/storefront in namespace demo reached applied. Phase: InSync Generation: 1, observed by the operator Database: last read at <time> Applied: plan ptah-plan-44f2a9565324d384d873dc64, fingerprint sha256:44f2a9565324d384d873dc646ec0169218f8d93bdf82708b2b2171c04d7a81c0, at <time>kubectl ptah schema storefront -n demo shows where it stands. # With the decision recorded, the plan ran and convergence was proved.$ kubectl -n "$NAMESPACE" exec deploy/demo-psql -- psql -c "\dt" List of relations Schema | Name | Type | Owner--------+-----------+-------+--------------- public | customers | table | ptah_external(1 row) # Who approved, when, which plan, and what became of it. Used once, and already stale.$ kubectl ptah schema storefront -n "$NAMESPACE"Schema: demo/storefrontPhase: InSyncReady: True, InSync: Schema is in syncPolicy: apply OnApproval, allowDestructive false, driftSeverity allSource: ConfigMap storefront-v1Snapshot: ptah-src-675290a01840eb1c6a64d05eContent: sha256:5a00a478af9f3c4a52e37aff33e574f580bcb6b94eb68860feb483b509b09667Observed: <time>Drift: noneApplied: ptah-plan-44f2a9565324d384d873dc64 at <time> No plan is published. Approvals, newest first: storefront-v1 approved by kubernetes-admin at <time> plan ptah-plan-44f2a9565324d384d873dc64, the applied plan TYPE STATUS REASON SINCE MESSAGE Accepted False PlanNoLongerCurrent <time> The approved plan no longer matches the current database state Stale True PlanNoLongerCurrent <time> The approved plan no longer matches the current database state Consumed True DispatchCommitted <time> The exact plan was committed to one Apply Job dispatch Conditions: TYPE STATUS REASON SINCE MESSAGE EngineSupported True SupportedEngine <time> Database engine "PostgreSQL" is supported Suspended False Active <time> Reconciliation is active ReconciliationFailed False Succeeded <time> The latest operation completed ArtifactResolved True SnapshotTaken <time> ConfigMap storefront-v1 was copied into snapshot ptah-src-675290a01840eb1c6a64d05e Ready True InSync <time> Schema is in sync DatabaseReachable True Observed <time> Database schema was observed DriftDetected False ScopedConverged <time> The authoritative managed scope has no changes InSync True ScopedConverged <time> A stable scoped plan proves convergence ApprovalRequired False Satisfied <time> Current plan approval requirements are satisfied PlanReady False NoChanges <time> No executable plan is required Applying False JobCompleted <time> Apply Job completed; convergence observation is pending kubectl events -n demo --for ptahschema/storefront prints its Events.
What was checked
Every step states what has to hold before its output may be published. A condition is read as its type, status and reason on the live object, never as a phrase in a message.
| Step | Claim | Held |
|---|---|---|
| 1 | exits 0, prints "apply: OnApproval", prints "name: storefront-v1" | yes |
| 2 | exits 0, prints "configmap/storefront-v1 created", prints "storefront created" | yes |
| 3 | exits 0, prints "reached approval" | yes |
| 3 | ptahschema/storefront: ApprovalRequired=True (Waiting) | yes |
| 4 | exits 0, prints "AwaitingApproval", prints "not approved", prints "ApprovalRequired: True, Waiting" | yes |
| 5 | exits 0, reports "Did not find any relations" | yes |
| 6 | exits 0, prints "CREATE TABLE \"customers\"", prints "safe", prints "Fingerprint:", prints "Type yes to approve: yes", prints "Approved: PtahSchemaApproval", prints "reached applied" | yes |
| 6 | ptahschemaapproval/storefront-v1: spec.approver.username is set, spec.approvedAt is set, spec.mutationRequestUID is set | yes |
| 7 | exits 0, prints "customers" | yes |
| 7 | ptahschema/storefront: InSync=True (ScopedConverged) | yes |
| 8 | exits 0, prints "storefront-v1", prints "approved by", prints "the applied plan", prints "Consumed", prints "DispatchCommitted" | yes |
| 8 | ptahschemaapproval/storefront-v1: Consumed=True (DispatchCommitted) | yes |
What it ran against
- Kubernetes1.37.0
- Operatore41a04412bb1
- Ptahv0.18.0
- Executorsha256:9f8797986124
- Recorded2026-11-06
- Scenarios ate41a04412bb1
What the commands read
A session is repeated, not admired, so every name in it is one your own environment can carry. There is nothing else: no helper of ours stands between a command and the cluster, and the recorder refuses a step that reads a name this list does not hold. The commands arekubectl, ptah and kubectl ptah, each of which you install once; the last one is the operator’s own client. They run from the root of a clone of this repository, which holds the files they name: the SQL under demo/schemas, demo/migrations,demo/migrations-checkpoint and demo/mysql, the reference data underdemo/reference, and the manifests under demo/manifests.
- KUBECONFIGthe cluster the commands run against
- NAMESPACEthe namespace the resources and their database credentials live in
- OPERATOR_NAMESPACEwhere the chart installed the operator
- CONTROLLERthe controller Deployment’s name, from that release
- EXECUTOR_IMAGEthe executor image the chart runs, its
executorImagevalue - REGISTRY_IN_CLUSTERthe registry address a Pod in the cluster resolves
- PTAH_OCI_REGISTRYthe registry address the push goes to
- PTAH_OCI_USERNAMEthe account that push uses, read by
ptahitself - PTAH_OCI_PASSWORDits password, which no command prints
The last two are how a CI job hands ptah a registry credential. On a workstation,ptah oci login stores it once instead, reading the password from a prompt or from standard input rather than from the command line.
The registry has two addresses, the way a registry in an ordinary deployment does: a push goes to PTAH_OCI_REGISTRY, and the resource names REGISTRY_IN_CLUSTER with the same repository and digest. The registry Secret a resource names says which address its credential is for, and the operator sends it to no other.
The lab itself appears in one command, demo/bin/lab versions in Stand up the lab, which says what the lab is made of. What a run starts from is its preparation, which is not recorded; Where it starts above says what that is and which run ends there. Rundemo/acceptance/reproduce.sh to watch Approve a plan repeated from a directory where the lab’s scripts do not exist, by an account that may not create a Job, with the plan approved by a second account that may create neither a schema nor a Job, and then the same schema published with ptah schema push and moved to its next revision withkubectl ptah set-source.
The same changes, as edits
Each kubectl ptah command that changes a resource changes one field of it, so the same change is an ordinary edit, with kubectl edit ptahschema storefront -n "$NAMESPACE"or in a manifest you apply; the runs that keep their resources as files underdemo/manifests make their changes that way. What the commands add is checking: set-source takes only a reference pinned by digest, changes nothing while an Apply may be running, and changes nothing another writer changed since it read the resource.
| Command | The line it changes |
|---|---|
kubectl ptah suspend | spec.suspend: true |
kubectl ptah resume | spec.suspend: false |
kubectl ptah set-source --oci | spec.desired.ociRef of a schema, spec.artifact.ociRef of a migration |
kubectl ptah refresh | the annotation operator.ptah.run/read-requested-at, set to the time of the request |
An approval is not one of them. It is an object of its own, created once by the person who decided: kubectl ptah approve --dry-run client prints it as YAML for a repository, to be created with kubectl create, and never edited afterwards.
The scenario is demo/scenarios/manual-approval.yaml, and make demo replays the whole set against a lab of your own. All recorded runs.