Skip to content

Stand up the lab

A four-node kind cluster, the chart from a package, and every image pinned by digest.

What it shows. What the other runs are against, and how to tell an install that finished from one that started.

Where it starts. The lab make demo-up builds, with nothing of the runs in its namespace. This run is the setup the others start from, and its checks are installation evidence rather than lessons.

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

sh · lab-setup
# The lab is one cluster, built from the versions this repository declares.$ demo/bin/lab versionskubernetes  1.37.0kind        v0.33.0operator    42522be3fd5a85b371eeb3f4e3a1ac5af9dc3ab7executor    e2e-registry.demo.svc.cluster.local:5000/ptah-executor@sha256:181e279720916ed455bf9fe8f1620b8e0d71dd37cc127b3ae662317697c92548ptah        v0.17.0 # An install that finished, not one that started. The rollout is complete.$ kubectl -n "$OPERATOR_NAMESPACE" rollout status deployment "$CONTROLLER" --timeout=3mdeployment "ptah-operator" successfully rolled out # Two replicas, one leader, and every image a digest, so one exact build runs.$ kubectl -n "$OPERATOR_NAMESPACE" get deployment -o wideNAME                                                           READY   UP-TO-DATE   AVAILABLE   AGE   CONTAINERS            IMAGES                                                                                                                                                                       SELECTORptah-operator-cert-rotator                                     1/1     1            1           14m   certificate-rotator   e2e-registry.demo.svc.cluster.local:5000/ptah-operator@sha256:50572f5de8a20f1b4cd155f0dbad266634f3bb54fedffb5af6bf88ffaf667a43   app.kubernetes.io/component=certificate-rotation,app.kubernetes.io/instance=ptah,app.kubernetes.io/name=ptah-operatorptah-operator                                                  2/2     2            2           14m   manager               e2e-registry.demo.svc.cluster.local:5000/ptah-operator@sha256:50572f5de8a20f1b4cd155f0dbad266634f3bb54fedffb5af6bf88ffaf667a43   app.kubernetes.io/component=controller,app.kubernetes.io/instance=ptah,app.kubernetes.io/name=ptah-operator # The kinds the operator serves. A schema, its plan, and the approval that lets it run.$ kubectl api-resources --api-group=operator.ptah.runNAME                              SHORTNAMES     APIVERSION                   NAMESPACED   KINDptahmigrationapprovals            ptahmapprove   operator.ptah.run/v1alpha1   true         PtahMigrationApprovalptahmigrationplans                ptahmplan      operator.ptah.run/v1alpha1   true         PtahMigrationPlanptahmigrationrunacknowledgments   ptahmack       operator.ptah.run/v1alpha1   true         PtahMigrationRunAcknowledgmentptahmigrations                    ptahm          operator.ptah.run/v1alpha1   true         PtahMigrationptahrealms                        ptahr          operator.ptah.run/v1alpha1   false        PtahRealmptahresultrecords                 ptahresult     operator.ptah.run/v1alpha1   true         PtahResultRecordptahschemaapprovals               ptahapprove    operator.ptah.run/v1alpha1   true         PtahSchemaApprovalptahschemaplanchunks              ptahchunk      operator.ptah.run/v1alpha1   true         PtahSchemaPlanChunkptahschemaplans                   ptahplan       operator.ptah.run/v1alpha1   true         PtahSchemaPlanptahschemas                       ptahs          operator.ptah.run/v1alpha1   true         PtahSchema # Each of those is served once its CRD is Established.$ kubectl wait --for=condition=Established --timeout=1m \  crd/ptahschemas.operator.ptah.run crd/ptahschemaplans.operator.ptah.run crd/ptahschemaapprovals.operator.ptah.runcustomresourcedefinition.apiextensions.k8s.io/ptahschemas.operator.ptah.run condition metcustomresourcedefinition.apiextensions.k8s.io/ptahschemaplans.operator.ptah.run condition metcustomresourcedefinition.apiextensions.k8s.io/ptahschemaapprovals.operator.ptah.run condition met # The namespace the runs work in. Credentials are Secrets; the policy cannot be edited.$ kubectl -n "$NAMESPACE" get secret,configmapNAME                          TYPE                             DATA   AGEsecret/demo-database          Opaque                           6      13msecret/demo-mysql-database    Opaque                           5      13msecret/demo-registry          Opaque                           4      13msecret/demo-registry-pull     kubernetes.io/dockerconfigjson   1      13msecret/demo-shadow-database   Opaque                           2      13m NAME                                           DATA   AGEconfigmap/demo-migration-verification-policy   1      13mconfigmap/demo-verification-policy             1      13mconfigmap/e2e-verification-policy              1      14mconfigmap/kube-root-ca.crt                     1      14m # One registry, two addresses. You push to the first; Pods in the cluster pull from the second.$ echo "push to:   ${PTAH_OCI_REGISTRY:?is not set}"echo "pull from: ${REGISTRY_IN_CLUSTER:?is not set}"push to:   127.0.0.1:23100pull from: e2e-registry.demo.svc.cluster.local:5000

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 "kubernetes", prints "kind", prints "operator", prints "executor", prints "ptah"yes
2exits 0, prints "successfully rolled out"yes
2deployment/$CONTROLLER: Available=True (MinimumReplicasAvailable)yes
3exits 0, prints "ptah", prints "@sha256:"yes
4exits 0, prints "ptahschemas", prints "ptahschemaplans", prints "ptahschemaapprovals", prints "ptahmigrations"yes
5exits 0, prints "ptahschemas.operator.ptah.run condition met", prints "ptahschemaapprovals.operator.ptah.run condition met"yes
6exits 0, prints "demo-database", prints "demo-registry", prints "demo-verification-policy", never prints "password"yes
7exits 0, prints "push to: ", prints "pull from: "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/lab-setup.yaml, and make demo replays the whole set against a lab of your own. All recorded runs.