Skip to content

Retire a used approval

An approval describes one plan against one observed state. Applying it moves that state.

What it shows. Why a signature cannot authorize twice, and what the operator does with one that no longer fits.

Where it starts. Where Approve a plan stops to ask: storefront, from demo/manifests/manual-approval, reading schemas/v1.sql from the ConfigMap storefront-v1 under apply OnApproval, with its first plan waiting for a decision.

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.

sh · stale-approval
# A plan is waiting for a decision, and nothing has approved it.$ 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-0853a3f1f49d15e81b5eb193Content:              sha256:5a00a478af9f3c4a52e37aff33e574f580bcb6b94eb68860feb483b509b09667Observed:             <time>Drift:                1 in 1 categories, highest safe  tables_added                            1  safe Plan ptah-plan-c65bda3c3858a3f5116892fc, 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-0853a3f1f49d15e81b5eb193  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. # Approve it. The approval is bound to that schema, that plan and that fingerprint.$ kubectl ptah approve schema/storefront -n "$NAMESPACE" --name storefront-v1 --waitPtahSchema:    demo/storefront, UID <uid>Plan:          ptah-plan-c65bda3c3858a3f5116892fc, UID <uid>Fingerprint:   sha256:c65bda3c3858a3f5116892fc3472df606c2ffca3c1e9414e793de6f3162a5b7b Schema:         demo/storefrontPlan:           current (ptah-plan-c65bda3c3858a3f5116892fc)Fingerprint:    sha256:c65bda3c3858a3f5116892fc3472df606c2ffca3c1e9414e793de6f3162a5b7bContent 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-c65bda3c3858a3f5116892fc 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-c65bda3c3858a3f5116892fc, fingerprint sha256:c65bda3c3858a3f5116892fc3472df606c2ffca3c1e9414e793de6f3162a5b7b.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-c65bda3c3858a3f5116892fc, fingerprint sha256:c65bda3c3858a3f5116892fc3472df606c2ffca3c1e9414e793de6f3162a5b7b, at <time>kubectl ptah schema storefront -n demo shows where it stands. # It was the one dispatched, and it is already stale. It described the database before.$ 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-0853a3f1f49d15e81b5eb193Content:              sha256:5a00a478af9f3c4a52e37aff33e574f580bcb6b94eb68860feb483b509b09667Observed:             <time>Drift:                noneApplied:              ptah-plan-c65bda3c3858a3f5116892fc at <time> No plan is published. Approvals, newest first:  storefront-v1    approved by kubernetes-admin at <time>    plan ptah-plan-c65bda3c3858a3f5116892fc, 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-0853a3f1f49d15e81b5eb193  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. # So it cannot authorize a second time. Change the desired state and see.$ kubectl -n "$NAMESPACE" create configmap storefront-v2 --from-file=schema.sql=demo/schemas/v2.sqldiff -u demo/manifests/manual-approval/storefront.yaml demo/manifests/stale-approval/storefront-v2.yamlkubectl -n "$NAMESPACE" apply -f demo/manifests/stale-approval/storefront-v2.yamlconfigmap/storefront-v2 created--- demo/manifests/manual-approval/storefront.yaml	2026-11-02 14:04:31.814354479 +0000+++ demo/manifests/stale-approval/storefront-v2.yaml	2026-11-02 14:04:31.814636968 +0000@@ -19,7 +19,7 @@       key: url   desired:     configMap:-      name: storefront-v1+      name: storefront-v2       items:         - key: schema.sql           path: schema.sqlptahschema.operator.ptah.run/storefront configured # The operator plans again, and stops again.$ kubectl ptah wait schema/storefront -n "$NAMESPACE" --for approvalptahschema/storefront in namespace demo reached approval.  Phase:      AwaitingApproval  Generation: 2, observed by the operator  Database:   last read at <time>  Plan:       ptah-plan-a6087a8cbf98748dc8ebc92a, fingerprint sha256:a6087a8cbf98748dc8ebc92a95c044d9fec6dd2c4a6cac956d704f2eadbb4b58kubectl ptah approve schema/storefront -n demo reviews it. # A new plan, waiting again. The old approval names a plan that is not the current one.$ 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-v2Snapshot:             ptah-src-19e3c83561cee6d23c5a7712Content:              sha256:f35b375d5e1e3c775bbe4c2ff96230befaac0c7030adf057fc8566bf5626b029Observed:             <time>Drift:                2 in 2 categories, highest warning  columns_added                           1  warning  tables_added                            1  safe Plan ptah-plan-a6087a8cbf98748dc8ebc92a, 3 statements, stored <time>  not approved  ApprovalRequired: True, Waiting: Create an approval bound to the current plan fingerprint Approvals, newest first:  storefront-v1    approved by kubernetes-admin at <time>    plan ptah-plan-c65bda3c3858a3f5116892fc, not the current 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-v2 was copied into snapshot ptah-src-19e3c83561cee6d23c5a7712  Ready                 False   AwaitingApproval  <time>                Plan is waiting for approval  DatabaseReachable     True    Observed          <time>                Database schema was observed  DriftDetected         True    ScopedChanges     <time>                Managed scope requires 3 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  Applying              False   JobCompleted      <time>                Apply Job completed; convergence observation is pending (written for generation 1) kubectl ptah plan storefront -n demo prints the SQL this plan holds.kubectl events -n demo --for ptahschema/storefront prints its Events. # And the change nobody approved has not run.$ kubectl -n "$NAMESPACE" exec deploy/demo-psql -- psql -c "\d customers"             Table "public.customers" Column |  Type  | Collation | Nullable | Default--------+--------+-----------+----------+--------- id     | bigint |           | not null | email  | text   |           | not null |Indexes:    "customers_pkey" PRIMARY KEY, btree (id)

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.

StepClaimHeld
1exits 0, prints "AwaitingApproval", prints "not approved"yes
1ptahschema/storefront: ApprovalRequired=True (Waiting)yes
2exits 0, prints "Fingerprint:", prints "Type yes to approve: yes", prints "Approved", prints "reached applied"yes
2ptahschema/storefront: InSync=True (ScopedConverged)yes
3exits 0, prints "storefront-v1", prints "Consumed", prints "DispatchCommitted", prints "Stale", prints "PlanNoLongerCurrent"yes
3ptahschemaapproval/storefront-v1: Stale=True (PlanNoLongerCurrent)yes
4exits 0, prints "configmap/storefront-v2 created", prints "+ name: storefront-v2", prints "storefront configured"yes
5exits 0, prints "reached approval"yes
5ptahschema/storefront: ApprovalRequired=True (Waiting)yes
6exits 0, prints "AwaitingApproval", prints "not approved", prints "storefront-v1", prints "not the current plan"yes
6ptahschemaapproval/storefront-v1: Stale=True (PlanNoLongerCurrent)yes
7exits 0, never prints "signed_up_at"yes

What it ran against

  • Kubernetes1.37.0
  • Operator42522be3fd5a
  • Ptahv0.17.0
  • Executorsha256:181e27972091
  • Recorded2026-11-02
  • Scenarios at42522be3fd5a

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 executorImage value
  • 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 ptah itself
  • 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.

CommandThe line it changes
kubectl ptah suspendspec.suspend: true
kubectl ptah resumespec.suspend: false
kubectl ptah set-source --ocispec.desired.ociRef of a schema, spec.artifact.ociRef of a migration
kubectl ptah refreshthe 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/stale-approval.yaml, and make demo replays the whole set against a lab of your own. All recorded runs.