Skip to content
PtahOperator

Apply a schema

Publish a schema as an artifact, point the operator at it, and watch it converge.

What it shows. What the operator does between a desired state and a database, and how it says it is done.

A replay of a verified run, not a live cluster. 13 checks held while it was recorded. Play types it out in the frame below; the transcript is the same session, already there.

sh · first-apply
# The desired state is SQL. This is the whole of it.$ cat demo/schemas/v1.sqlCREATE TABLE customers (  id bigint NOT NULL PRIMARY KEY,  email text NOT NULL); # The lab tells you where its registry is and what it generated for you.$ eval "$(demo/bin/lab credentials)"export PTAH_OCI_USERNAME PTAH_OCI_PASSWORD PTAH_OCI_REGISTRYecho "registry: $PTAH_OCI_REGISTRY"registry: 127.0.0.1:24793 # Publish the schema. This is ptah's own command, run as you or CI would.$ eval "$(demo/bin/lab credentials)"export PTAH_OCI_USERNAME PTAH_OCI_PASSWORD PTAH_OCI_REGISTRYptah schema push "oci://$PTAH_OCI_REGISTRY/schemas/demo:v1-$$" \  --schema-file demo/schemas/v1.sql --dialect postgres --plain-http \  | tee /dev/stderr | sed -n 's/^Digest: //p' > demo/.lab/digestPushed schema as oci://127.0.0.1:24793/schemas/demo:v1-26729Digest: sha256:3f784583308b8fb02d2fac3fe06e8cf21fd2adf66bc52e2f6879aec25b11df6cVersion:Tags: [v1-26729] # The resource names the artifact by digest, and the credentials by Secret.$ APPLY=Always demo/bin/lab manifest storefront "$(cat demo/.lab/digest)" | tee demo/.lab/storefront.yaml# The desired state of one database, as a Kubernetes resource.## Nothing here is a credential. The database URL and the registry password are# named, and read by the operator from Secrets this namespace holds; what the# resource carries is which artifact, which database, and what the operator is# allowed to do about the difference.apiVersion: operator.ptah.run/v1alpha1kind: PtahSchemametadata:  name: storefront  namespace: demospec:  target:    engine: PostgreSQL    # Every resource that can reach this physical database, through any alias    # or credential, has to use this exact key. It is how two of them are kept    # from planning against each other.    coordinationKey: demo/storefront/primary    urlFrom:      name: demo-database      key: url  desired:    # A digest, not a tag: what was reviewed is what runs.    ociRef: oci://e2e-registry.demo.svc.cluster.local:5000/schemas/demo@sha256:3f784583308b8fb02d2fac3fe06e8cf21fd2adf66bc52e2f6879aec25b11df6c    registryAuthFrom:      name: demo-registry      mode: Environment      usernameKey: username      passwordKey: password      registryKey: registry    verificationPolicyFrom:      name: demo-verification-policy      key: policy.yaml    transport:      plainHTTP: true  policy:    apply: Always    allowDestructive: false    driftSeverity: all  interval: 1m  suspend: false  execution:    activeDeadlineSeconds: 300    failureRetryInterval: 10s    connectTimeout: 30s # Apply it. Nothing else is typed after this; the operator does the rest.$ kubectl apply -f demo/.lab/storefront.yamlptahschema.operator.ptah.run/storefront created # Resolve, verify, observe, plan, apply, and prove. Each is a condition.$ kubectl -n "$NAMESPACE" get ptahschema storefront -o json \  | jq -r '.status.conditions[] | "\(.type)\t\(.status)\t\(.reason)"' | column -t -s "$(printf '\t')"EngineSupported       True   SupportedEngineReady                 True   InSyncReconciliationFailed  False  SucceededArtifactResolved      True   DigestPinnedArtifactVerified      True   PolicySatisfiedDatabaseReachable     True   ObservedDriftDetected         False  ScopedConvergedInSync                True   ScopedConvergedApprovalRequired      False  SatisfiedPlanReady             False  NoChangesApplying              False  JobCompletedSuspended             False  Active # Applying says False, reason JobCompleted. A finished Job is not a converged database.$ kubectl -n "$NAMESPACE" get ptahschema storefront -o json \  | jq -r '.status.conditions[] | select(.type == "Applying" or .type == "InSync") | "\(.type): \(.message)"'InSync: A stable scoped plan proves convergenceApplying: Apply Job completed; convergence observation is pending # The plan it derived, and the SQL that plan carried.$ kubectl -n "$NAMESPACE" get ptahschemaplanNAME                                 SCHEMA       FINGERPRINT                                                               STATEMENTS   DESTRUCTIVE   AGEptah-plan-8231b2ce17ff7360ab2536de   storefront   sha256:8231b2ce17ff7360ab2536de2a04972e2e7998d2a7489ce6ecf79e51009b8989   1            false         21s # What it ran is stored, and one read-only command prints it.$ kubectl ptah plan storefront --applied -n "$NAMESPACE" -o sql-- POSTGRES TABLE: customers --CREATE TABLE "customers" (  "id" bigint PRIMARY KEY NOT NULL,  "email" text NOT NULL); # And the database has it.$ 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 "CREATE TABLE customers"yes
2exits 0, prints "registry: ", never prints "PTAH_OCI_PASSWORD"yes
3exits 0, reports "Pushed schema as oci://", reports "Digest: sha256:"yes
4exits 0, prints "kind: PtahSchema", prints "ociRef: oci://", prints "urlFrom", never prints "password:"yes
5exits 0, prints "created"yes
6ptahschema/storefront: InSync=True (ScopedConverged)yes
6exits 0, prints "InSync", prints "ScopedConverged", prints "JobCompleted"yes
6ptahschema/storefront: Ready=True (InSync), status.phase=InSync, status.applied.artifactDigest is setyes
7exits 0, prints "Applying:", prints "InSync:"yes
7ptahschema/storefront: Applying=False (JobCompleted)yes
8exits 0, prints "storefront"yes
9exits 0, prints "CREATE TABLE \"customers\""yes
10exits 0, prints "customers_pkey"yes

What it ran against

  • Kubernetes1.37.0
  • Operator8c7fe187707f
  • Ptahv0.6.0
  • Executorsha256:91ec0a6fa27f
  • Recorded2026-09-17
  • Scenarios at8c7fe187707f

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 read-only client.

  • KUBECONFIGthe cluster the commands run against
  • NAMESPACEthe namespace the schema and its database credentials live in
  • OPERATOR_NAMESPACEwhere the chart installed the operator
  • CONTROLLERthe controller Deployment’s name, from that release
  • 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
  • PTAH_OCI_PASSWORDits password, which no command prints

A session also uses demo/bin/lab in two places, and both are the lab handing over what it generated rather than doing the demonstration’s work:lab credentials prints the three registry values above, and lab manifestfills the published template with values the step states. Rundemo/acceptance/reproduce.sh to watch the same scenario repeated from a directory where neither exists, by an account that may not create a Job.

The scenario is demo/scenarios/first-apply.yaml, and make demo replays the whole set against a lab of your own. All recorded runs.