# Reproduce the operator example

Verified on September 21, 2026 against a disposable four-node kind cluster on
Docker context `remote-dev-container`:

| Component | Tested version |
| --- | --- |
| Kubernetes | 1.37.0 |
| kind | 0.33.0 |
| Helm | 4.2.0 |
| Operator and kubectl plugin | commit `6d72944c477c851a19d2662eb345ea313c921dd0` |
| Ptah publishing CLI | released 0.7.0 binary |
| Ptah Job executor | build of release commit `127aa247772027f9ac8f5b16107552a7e1b541ed` |
| PostgreSQL | 17.11 |

`measured/runtime.json` records the image digests. The operator has no published
release at the time of verification. The executor was built from the Ptah
release source; it is not the same image bytes as the published release image.

## Prepare the environment

Use the operator source at the commit above. Its
[installation guide](https://operator.ptah.run/edge/start/install/) defines the
chart's required manager, runner, and executor image pins. The example assumes
the chart is installed and its controller is available. This is a schema-change
walkthrough, not a substitute for installing and configuring the operator.

For the recorded lab, the operator image was built using that source tree's
`test/e2e/Dockerfile.operator`, target `operator`, with `REVISION` set to the
commit above. The same image supplies the runner. The executor used the
operator repository's `test/e2e/Dockerfile.ptah` against an archive of the
specified Ptah commit. Both were published into the lab registry and installed
through the packaged chart with digest references. The chart package SHA-256
was `ea4367c7b6f77214eb1ca6d1c2866f1b8276dc90eae21c1c5b07248ad95bf7a0`.

Build the read-only client from that operator checkout and put the resulting
binary on your `PATH`:

```console
go build -o kubectl-ptah ./cmd/kubectl-ptah
```

Use an application namespace watched by the operator. `NAMESPACE` names it.
The same namespace must contain:

- `demo-database`, a Secret whose `url` key points to an empty PostgreSQL
  database.
- `demo-registry`, a Secret with `username`, `password`, and `registry` keys.
  The last key must exactly match the authority in `desired.ociRef`. For this
  HTTP-only disposable registry, it also contains `allowPlainHTTP: "true"`.
- An image pull Secret for the execution images, if their registry is private.

Supply actual credentials through your normal secret-management mechanism;
none are included in these files. For an HTTPS registry omit the plain-HTTP
settings. The publishing client must be able to reach the registry, and the
operator's Jobs must be able to reach both the registry and PostgreSQL.

Create the immutable artifact policy:

```console
kubectl -n "$NAMESPACE" apply -f verification-policy.yaml
```

This policy checks the artifact type only. It is not a publisher-signature
policy. No signature-verification claim is made by this example.

## Publish and plan

Run from this directory. `REGISTRY` is the publishing client's registry
authority; configure `PTAH_OCI_REGISTRY`, `PTAH_OCI_USERNAME`, and
`PTAH_OCI_PASSWORD` in the publishing environment, or use the Docker credential
store. The recorded client connected through an SSH forward. Jobs used a
cluster Service for the same registry.

```console
ptah schema push "oci://$REGISTRY/schemas/blog-shipments:20260923-v1" --schema-file schema.sql --dialect postgres --plain-http
```

A repeated run needs a new tag. Copy the returned digest into `shipments.yaml`
and replace its registry authority with the address your Jobs can reach.
The committed reference points to the disposable registry used for this run;
it will not exist in another cluster. Preserve the digest when selecting the
same content through another authority for the same registry.

```console
kubectl -n "$NAMESPACE" apply -f shipments.yaml
kubectl -n "$NAMESPACE" wait ptahschema/shipments --for=condition=ApprovalRequired=True --timeout=180s
kubectl ptah plan shipments --current -n "$NAMESPACE" -o sql
kubectl -n "$NAMESPACE" get ptahschema shipments -o yaml
```

The observed phase was `AwaitingApproval`. The SQL plan created the public
schema if needed, then created `shipments`. A direct PostgreSQL query confirmed
`to_regclass('public.shipments') IS NULL` before approval.

## Bind the approval to this run

Edit `approval.json` with the schema's `metadata.uid` and the current
`status.plan.name`, `uid`, and `fingerprint`. The committed identifiers are
recorded evidence, not reusable IDs for a new cluster. Inspect the plan before
creating the approval.

`wrong-approval.json` is the negative test: after replacing its schema and plan
IDs with the current ones, leave its all-zero fingerprint unchanged. Creation
must fail. In this run the webhook rejected it and the table remained absent.

```console
kubectl -n "$NAMESPACE" create -f wrong-approval.json
kubectl -n "$NAMESPACE" create -f approval.json
kubectl -n "$NAMESPACE" wait ptahschema/shipments --for=condition=InSync=True --timeout=180s
kubectl ptah schema shipments -n "$NAMESPACE"
kubectl ptah plan shipments --applied -n "$NAMESPACE" -o sql
```

The valid approval was consumed at dispatch. The observed state reached
`InSync=True`, reason `ScopedConverged`, with `observedGeneration` equal to the
resource generation. The recorded fresh observation at `10:40:49Z` followed the
apply completion at `10:40:05Z`. Both selected the digest returned by the push.

The measured run used the cluster administrator identity. A restricted reviewer
needs permission to create approvals and read all current plan chunks; see
[approval access](https://operator.ptah.run/edge/use/approvals/). Creating the
desired-state resource and approving a plan are separate permissions.

## Verify the table

Run `application-query.sql` with a PostgreSQL client connected to this same
database. It inserts shipment 1 for order 42 and returns `status = queued`.
In the lab, `kubectl exec deploy/demo-psql -- psql` provided the client with its
connection settings from the existing database Secret.

`measured/` contains captured resource states, commands, plan SQL, rejection,
and database output. `expected/` holds the excerpts shown in the article.
Trailing whitespace is removed from captured text, and the local checkout path
is replaced with `/examples/`; `verified.json` records the original hashes
where output was normalized. No credential values are stored here.

The example verifies one initial schema change, rejection of one invalid
approval, and a fresh observation after applying the accepted plan. It does
not test high availability, upgrades, disaster recovery, or a production RBAC
separation. Remove only your disposable resources after reproducing it.
