API compatibility
The API group is operator.ptah.run and its only version is v1alpha1. Alpha
means here what it means in Kubernetes: there is no compatibility guarantee. A
change that breaks objects you have already stored can be made before v1, and
this page does not promise otherwise.
What the project does hold to is that such a change is a decision somebody makes rather than something that slips in. Each rule below is enforced by a check, and the check is named, so what you are reading is the code rather than an intention.
What cannot change by accident
Section titled “What cannot change by accident”These are refused when the generated CRDs are compared against the previous
commit, by make verify-crd-schema-history:
| Change | Why it is refused |
|---|---|
| A field that goes from optional to required, or a new field that arrives already required, without a default | A stored object that never set the field fails the requirement on its next write. A default rescues this: structural defaulting fills an absent field from its schema default before anything validates it, on every decode, so a field that carries one is exempt. |
| An enum that lost a value | Every stored object holding the removed value stops validating, and there is no migration for it. |
| A numeric or length bound that tightened — a minimum that rose, a maximum that fell, a bound that newly appeared, or one that turned exclusive at the same value | A stored value the old bound allowed and the new one refuses fails on the same next write. |
| A pattern that changed, in either direction | Refused conservatively: proving a changed regular expression only ever widens is its own project, so any change is treated as tightening unless a declared break says otherwise. |
A new or changed x-kubernetes-validations rule on a field that already existed |
The rule runs against the stored value the first time anything writes the object, whether or not the write touches that field. |
A changed x-kubernetes-list-type or x-kubernetes-map-type |
Both are enforced against whatever the field already holds, not just against what a write changes: a list that was fine as atomic can hold entries a set or map list-type refuses. |
| A default that appeared, changed, or was taken away | It changes what a stored object reads back as, on objects nobody edited. |
A field the new schema no longer declares is not refused. Structural
pruning drops a value for any property the current schema does not carry when
it decodes a stored object, required or not, so removing a field — and
whatever required-ness it had — reaches no object already in etcd. An earlier
version of this check refused a removed required field anyway, on a claim
about a decode order that pruning code in k8s.io/apiextensions-apiserver
did not bear out; two changes that tightened validation the other way,
stokaro/ptah-operator#470
and #482, went
unrefused because the check was watching the harmless direction.
The comparison is against the storage version of each kind, because that is the schema an object in etcd is read back through. A kind the baseline does not carry is new and has nothing stored to break.
Breaking any of them is still possible: it means editing that check, which
puts the consequence in front of whoever makes the change and into the diff of
whoever reviews it. The edit is a declared break in
hack/crdschemahistory/compatibility.go: it names the schema version that
makes the break, why, and every refusal it excuses, word for word. It excuses
nothing in any other version, and it is refused itself if it names a refusal
the change does not produce. That is the whole of it before v1 — not that
the API will not move under you, but that it will not move quietly.
What must move when the schema moves
Section titled “What must move when the schema moves”Every shipped CRD carries operator.ptah.run/crd-schema-version and a digest
of its normalized spec. The same verifier requires the version to strictly
increase when the specs change, and to stay exactly equal when they do
not. A schema that changed without the version moving is refused, and so is a
version bumped without a change.
So the annotation is a fact about the bytes rather than a release number, and every shipped kind carries the same one.
Contract counters
Section titled “Contract counters”The schema version is one of several numbers that version something a release stores in a cluster or speaks to another component. Each is written down in code and held there by a check:
| Counter | Where it is declared | What it versions |
|---|---|---|
| CRD schema version | CRD_SCHEMA_VERSION in the Makefile and CurrentCRDSchemaVersion in internal/crdupgrade, stamped on every CRD as operator.ptah.run/crd-schema-version |
The generated schemas, held to the previous commit as described above. |
| Controller-state version | CONTROLLER_STATE_VERSION in the Makefile and controllerstate.CurrentVersion, stamped on every CRD and both webhook configurations as operator.ptah.run/controller-state-version |
The durable status a manager can read; see the controller-state contract. |
| Plan contract | fingerprint.CurrentPlanContractVersion for a plan computed from an OCI artifact and fingerprint.SourcePlanContractVersion for one computed from a ConfigMap or a Git commit, which the PtahSchemaPlan spec.contractVersion enum and the plan write guard repeat |
What a schema plan’s fingerprint binds, and so what an approval names. A manager writes both and reads both; one that knows only version 1 refuses a version 2 plan. |
| Migration plan contract | migrationplan.ContractVersion and migrationplan.SourceContractVersion, the same two numbers, which the PtahMigrationPlan spec.contractVersion bound repeats |
The same for a migration plan. |
| Realm digest contract | coordinationContractVersion in internal/fingerprint |
How a coordination key or a PtahRealm becomes the digest that names a database. |
| Runner protocol | runner.ProtocolVersion, recorded in support/runner-protocol.json and in the edge row of support/ptah.json |
What the runner accepts, enforces and returns. hack/verifyrunnerprotocol holds it to the runner’s source. |
| Drift vocabulary | dataplane.DriftFindingVocabularyVersion |
The closed set of drift finding categories, and the form of the kind a feature category may carry. |
| Admission contract | CurrentAdmissionContractVersion in internal/crdupgrade, stamped on both webhook configurations as operator.ptah.run/admission-contract-version |
The admission configuration every release serves. |
| Certificate staging format | certrotation.StagingFormat, the format key of the rotator’s staging Secret |
The record a certificate rotation resumes from after a restart. |
| Admission snapshot format | podintent.SnapshotVersion, which the admissionSnapshot.version enum in an operation’s status repeats |
The Pod admission rules an operation’s snapshot was taken under. |
Every counter is 1 in the 0.2.0 development tree. They previously counted
commits, and no release could hold version 32 of anything, so they were all
reset to 1 during release preparation. The schema-history check compares each
change with the commit before it, and would read 32 to 1 as a rollback. That
restart is recorded in hack/crdschemahistory/restart.go: it leaves only the schemas it
names, by version and by the digest of every CRD, it arrives at version 1 and
at nothing else, and it excuses only the stored-object transitions it lists.
Any other move below the baseline is refused as a rollback, and every change
after it is held to the rules above.
From the first release on, a counter moves only when something a tagged release
could have stored or spoken changes. A change that leaves every stored and
spoken form as it was keeps its number: the runner protocol records such a change in
support/runner-protocol.json with its reason instead of moving. A new value in
a result frame that an earlier manager refuses as malformed keeps the number too,
because that refusal is what a version mismatch would give. The CRD
schema version is the strictest of them, because its check compares every
change with the commit before it: any change to a generated schema moves it.
What the operator refuses at runtime
Section titled “What the operator refuses at runtime”The durable state and the release order carry versions of their own, and both are fences rather than documentation.
Durable state. operator.ptah.run/controller-state-version says which
status a manager can interpret. A manager reads state stamped at its own
version or below and refuses anything above, at startup and before every CRD
update. Which version carries which state, and what each refusal looks like,
is the controller-state contract.
Chart and image pairing. The reconcile hook refuses a chart whose
controller-state version its own manager image does not compile, before it
reads or changes anything. That is what stops a --reuse-values upgrade that
keeps an old image under a new chart, or an old chart paired with a new image.
helm rollback renders nothing: it runs the CRD hook of the release it
returns to, and that hook refuses stored state newer than that release reads.
Upgrading is allowed to move forward and rolling back below state that has
already been written is not.
The set of kinds cannot change quietly
Section titled “The set of kinds cannot change quietly”The generated set is the schema family and the migration family, each with its plan and its approval. The schema-history verifier refuses a set that added or removed a kind, so one cannot appear or disappear without that refusal being addressed deliberately.
Go packages for other programs
Section titled “Go packages for other programs”Two packages outside internal/ are there for programs that read what the
operator stored. pkg/planread reads a schema plan back through the store’s
own checks; kubectl ptah plan and kubectl ptah approve read through it, so
there is one reader. pkg/planread/planreadtest holds the stored states such a
reader has to answer, each with the answer planread gives, so a program that
wraps the reader can run the same cases. A test refuses an exported declaration
in either package that names an internal type.
Their Go API stands where v1alpha1 stands: no compatibility promise. Pin the
module version and read what a release changed before you move to it.
What a display read of a stored plan covers
Section titled “What a display read of a stored plan covers”A stored plan outlives an upgrade, so a reader meets documents from a Ptah newer
than the one it was built for. Ptah adds a field at the same plan contract and
format version: v0.14.0 added four to a statement and v0.19.0 added scope to
the plan, and each refused every such plan in every reader until the reader
shipped again.
From 0.2.10, planread.LoadForDisplay promises this about a document, for as
long as the build reads its plan contract and its format_version: a field the
build does not declare, on the plan, in scope, on a statement or on a declared
row set, does not stop the read. The plan comes back as planread.Load would
return it without that field, after every check on the stored objects and on the
fields the build declares. The field’s name comes back in UnreadFields, as
annotations or statements[].inputs. Its value comes back nowhere. The read
returns a DisplayPlan, which has no document, because the stored bytes hold the
value. The text format of kubectl ptah plan reads this way.
The promise stops at the field’s name. It does not cover another plan contract or
format_version, a declared field whose type changed, or a field that takes the
place of a declared one, such as a statement with native and no sql. Those are
refused with ErrUnsupported. A key that is not an identifier of at most 128
bytes, and a document with more than 64 unread fields, are refused too. It says
nothing about what an unread field does: a plan shown this way can run differently
from what its statements alone suggest. So a DisplayPlan has no SQL to hand a
program, and kubectl ptah plan -o sql and -o json refuse such a plan.
planread.Load makes no such promise and refuses any field the build does not
declare, as the manager and the runner do. That strictness is what keeps a
statement’s inputs, which name an environment variable Ptah reads at Apply, and
a plan’s annotations out of an Apply Pod. An approval is a decision about the
whole document, so kubectl ptah approve reads through Load. A program that
offers an approval must read the plan through Load too, and must not offer one
when Load refuses. A DisplayPlan cannot stand in for that read: it carries no
UID for an approval to bind and no function that takes a Plan takes it.
PlanFile keeps scope declared while any stored plan can carry it, because a
field that goes back to undeclared stops every Load of such a plan.
Before v1
Section titled “Before v1”There are no deprecations. A field that has to go, goes, in a release that
says so. Nothing is kept working for a window first, because a window is a
compatibility promise and v1alpha1 does not make one.
There is no dated transition to v1. Until there is, the only way to stay
safe is to pin the chart version you deploy and read what a release changed
before you move to it. An upgrade can require you to edit your resources, and
Release notes is where that is said, one entry per
version.