Skip to content

Collect the CRD redesign's property renames and migrate the first four kinds - #503

Merged
noctarius merged 6 commits into
mainfrom
worktree-crd-property-renames
Sep 11, 2026
Merged

noctarius merged 6 commits into
mainfrom
worktree-crd-property-renames

Conversation

@noctarius

Copy link
Copy Markdown
Collaborator

The ten designs under operator/docs/designs/crd-redesign each carry a "Migration from the Registered API" section, and between them they rename a substantial number of properties on CRDs that are already registered and in use. This collects those renames into one inventory and starts executing them.

Each design states its own renames beside the kind they belong to, which is the right place to decide them and the wrong place to execute them: they share one mechanism, one deprecation window, and one set of upgrade risks, and doing them kind by kind means writing that mechanism ten times.

The inventory

operator/docs/designs/crd-redesign/design-property-renames.md is the collected list, verified against operator/api/v1alpha1 rather than taken from the designs alone. Two rows do not survive that check and are recorded as corrections:

  • replicateenableReplication is named by design-crd-model.md §9.6 and design-storagepool.md §11, but StorageClassParameters has no replicate field. It is a target-state addition, not a rename.
  • design-storagebackup.md §13 puts spec.clusterName on "all four" backup kinds. BackupImport has sourceClusterName/targetClusterName instead — and the kind is retired anyway.

The document classifies each row by how it breaks, which matters more than the count: a renamed spec field is silently ignored, a renamed status field costs nothing, a renamed enum value fails loudly at admission, and a renamed toggle that also inverts is the case where a mechanical rename produces the opposite behavior.

The mechanism

A second version with a conversion webhook. v1alpha2 is the storage version and the shape every controller reads; v1alpha1 stays served and deprecated and converts into it. No reconciler has to know an older spelling exists, and a kubectl or Argo CD apply of a v1alpha1 manifest keeps working unchanged.

§3.2 records what that costs rather than glossing it — most importantly that a conversion webhook which is down makes its kinds unreadable, not merely unconverted, and that this is most likely during an upgrade. What contains it is that the webhook is served by the operator's existing manager.

Three pieces of infrastructure carry it:

  • CA bundle injection under both TLS providers. cert-controller's rotator gains CRDConversion entries; the cert-manager provisioner gains an equivalent CRD patch it did not have.
  • conversion_service.go corrects the conversion webhook's service namespace at runtime. The chart's crds/ directory is not templated, so a CRD shipped for the default install names a service that does not exist in any other namespace — and an unreachable conversion webhook fails every read of its kind.
  • hack/apply-conversion-webhook.sh writes the spec.conversion stanza back after each controller-gen run, driven by config/crd/converted-kinds.txt. That file is the single source of truth the Go registration is tested against: a kind served without being trusted, or trusted without being served, breaks in ways two disagreeing lists would not otherwise reveal.

Kinds converted

Kind Rows
ControlPlane spec.imagespec.source.managed.image; phase ReadyAvailable
StorageBackup spec.clusterNamespec.clusterRef
StorageClusterOps spec.nodeRollingRestartspec.rollingRestart; its status twin; the action enum recased
StorageNodeOps storageNodeRefnodeRef; drainremove; targetWorkerNode/newSsdPciespec.migrate; the action enum recased

Every conversion is tested in both directions. A conversion that maps going up and copies going down is a bug a one-way test cannot see, and it corrupts on the first kubectl get -o yaml | kubectl apply -f -.

A defect the tests caught

The untyped action constants in internal/utils/constants.go are deleted rather than updated. Once the enum values were recased they became actively dangerous: an untyped string constant compares against the named type without complaint, so a stale "remove" tested against StorageNodeOpsActionRemove compiles and is never equal. That defect reached the working tree — a drain stopped setting its Validating sub-phase — and a unit test caught it. Removing the shape that allows it is the fix.

StorageNodeSet keeps one version, deliberately

A kind the redesign retires does not earn a second one (§3.6). The consequence is that StorageNode.spec.storageNodeSetRef cannot be converted: the cluster's name lives in the set rather than in the node, and a conversion runs on every read, has to be a pure function of what it was handed, and cannot go and fetch it.

So v1alpha2 will carry all three fields — storageNodeSetRef deprecated, clusterRef and nodeSet optional — and the controller fills the new two as it reconciles. The workload reparents on the same schedule rather than at start-up: a fleet-wide rewrite of owner references during an upgrade, with the garbage collector watching, deletes running storage nodes if it is wrong.

Not in this PR

StoragePool, StorageNode, and StorageCluster are not converted yet — they carry the regroupings, the inverting toggles, and the BackupSpec removals that need the round-trip annotation stash. BackupPolicyStorageBackupPolicy, BackupRestoreStorageBackupOps, and VolumeMigrationPersistentVolumeOps are kind renames, so a conversion webhook structurally cannot carry them; their property renames land with the successor kinds.

The key renames of §2.6 — the StorageClass qos_* parameter keys, the simplyblock.io/ annotation prefix, the cluster finalizer, and the FDB event reasons — are not properties, so no conversion reaches them. They keep the both-spellings treatment stated there.

Verification

make test, make lint (0 issues), the house-style quality gate (9/9), and codespell all pass.

🤖 Generated with Claude Code

Copilot AI lite review requested due to automatic review settings September 8, 2026 16:08

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Changes recommended

There are RBAC least-privilege concerns around cluster-wide CRD write permissions and a Kubernetes module version skew in go.mod that should be resolved before merge.

Once you've addressed the issues Copilot identified, you can request another Copilot review.

Pull request overview

This PR introduces CRD conversion-webhook infrastructure and a new v1alpha2 storage version to safely migrate the first set of CRD property renames (and selected enum recasing) without requiring reconcilers to handle legacy spellings.

Changes:

  • Adds conversion-webhook wiring, CA bundle injection for CRD conversion, and runtime correction of the conversion service namespace for non-default installs.
  • Introduces storage.simplyblock.io/v1alpha2 types for ControlPlane, StorageBackup, StorageClusterOps, and StorageNodeOps, plus bidirectional conversion logic and round-trip tests.
  • Updates controllers and unit tests to read/own v1alpha2 resources and ensures CRD regeneration preserves spec.conversion via a post-controller-gen step.
File summaries
File Description
operator/Makefile Runs a post-generation script to restore spec.conversion after controller-gen.
operator/internal/webhook/simplyblock_rebalancer_injector_test.go Extends test scheme to include CRD types needed for conversion CA injection scenarios.
operator/internal/webhook/conversion.go Central registry of converted kinds and conversion webhook registration wiring.
operator/internal/webhook/conversion_test.go Tests consistency between Go registration, manifest list, and CA injection/namespace correction behavior.
operator/internal/webhook/conversion_service.go Patches conversion webhook service namespace on converted CRDs at runtime.
operator/internal/webhook/certmanager.go Injects conversion webhook CA bundle into CRDs in cert-manager TLS mode.
operator/internal/webhook/certmanager_test.go Refactors test fixture constants and aligns wording.
operator/internal/webhook/cert.go Extends self-signed rotator targets to include CRD conversion CA injection; wires namespace correction runnable.
operator/internal/utils/constants.go Removes untyped action string constants in favor of typed API enums.
operator/internal/controller/test_helpers_test.go Ensures fake-client schemes register both v1alpha1 and v1alpha2 APIs for mixed-version tests.
operator/internal/controller/storagenodeops_controller_unit_test.go Updates unit tests to use v1alpha2 StorageNodeOps types and enums.
operator/internal/controller/storagenode_controller.go Updates delete/remove-ops flow to create/read/own v1alpha2 StorageNodeOps.
operator/internal/controller/storagenode_controller_unit_test.go Updates unit tests for StorageNode deletion/remove ops to v1alpha2 ops objects.
operator/internal/controller/storageclusterops_noderollingrestart.go Migrates rolling-restart state handling to v1alpha2 status/spec field names.
operator/internal/controller/storageclusterops_controller.go Migrates StorageClusterOps reconciliation to v1alpha2 type and enum constants.
operator/internal/controller/storageclusterops_controller_unit_test.go Updates StorageClusterOps controller unit tests to v1alpha2 types/enums.
operator/internal/controller/storagebackupsync_controller.go Migrates StorageBackup syncing/import to v1alpha2 (e.g., clusterRef).
operator/internal/controller/storagebackupsync_controller_unit_test.go Updates sync controller tests to use v1alpha2 backups.
operator/internal/controller/storagebackup_controller.go Migrates StorageBackup reconciler to operate on v1alpha2.
operator/internal/controller/backuprestore_controller.go Reads StorageBackup as v1alpha2 during restore reconciliation.
operator/internal/controller/backuprestore_controller_test.go Updates restore tests to construct v1alpha2 StorageBackup objects.
operator/internal/controller/backupimport_controller.go Creates/patches v1alpha2 StorageBackup from BackupImport.
operator/hack/apply-conversion-webhook.sh Applies spec.conversion webhook stanza into generated CRD bases based on a canonical list.
operator/go.mod Adds a direct dependency on k8s.io/apiextensions-apiserver for CRD type usage.
operator/config/rbac/role.yaml Grants CRD read/patch/update permissions required for conversion CA injection and service reference correction.
operator/config/crd/converted-kinds.txt Canonical list of CRDs that must have conversion enabled and receive CA injection.
operator/config/crd/bases/storage.simplyblock.io_storageclusterops.yaml Adds v1alpha2 version and conversion webhook stanza for StorageClusterOps.
operator/config/crd/bases/storage.simplyblock.io_storagebackups.yaml Adds v1alpha2 version and conversion webhook stanza for StorageBackup.
operator/config/crd/bases/storage.simplyblock.io_controlplanes.yaml Adds v1alpha2 version and conversion webhook stanza for ControlPlane.
operator/cmd/main.go Registers apiextensions + v1alpha2 schemes and registers conversion webhooks after TLS readiness.
operator/api/v1alpha2/storagenodeops_types.go Adds v1alpha2 StorageNodeOps API with renamed/regrouped fields and typed enums.
operator/api/v1alpha2/storageclusterops_types.go Adds v1alpha2 StorageClusterOps API with renamed fields and typed enums.
operator/api/v1alpha2/storagebackup_types.go Adds v1alpha2 StorageBackup API with clusterRef rename.
operator/api/v1alpha2/groupversion_info.go Declares v1alpha2 group-version and scheme wiring documentation.
operator/api/v1alpha2/controlplane_types.go Adds v1alpha2 ControlPlane API with image regrouping and phase rename.
operator/api/v1alpha1/storagenodeops_conversion.go Implements v1alpha1↔hub conversions for StorageNodeOps field renames and enum recasing.
operator/api/v1alpha1/storagenodeops_conversion_test.go Adds bidirectional and round-trip conversion tests for StorageNodeOps.
operator/api/v1alpha1/storageclusterops_conversion.go Implements v1alpha1↔hub conversions for StorageClusterOps field renames and enum recasing.
operator/api/v1alpha1/storageclusterops_conversion_test.go Adds bidirectional and round-trip conversion tests for StorageClusterOps.
operator/api/v1alpha1/storagebackup_conversion.go Implements v1alpha1↔hub conversions for StorageBackup clusterNameclusterRef.
operator/api/v1alpha1/storagebackup_conversion_test.go Adds rename and round-trip conversion tests for StorageBackup.
operator/api/v1alpha1/conversion_helpers_test.go Adds shared conversion test helpers for enum bidirectional checks.
operator/api/v1alpha1/controlplane_conversion.go Implements v1alpha1↔hub conversions for ControlPlane image regrouping and phase rename.
operator/api/v1alpha1/controlplane_conversion_test.go Adds bidirectional and round-trip conversion tests for ControlPlane.
helm-charts/charts/simplyblock-operator/templates/roles/manager_role.yaml Charts RBAC updated to include CRD read/patch/update permissions.
helm-charts/charts/simplyblock-operator/crds/storage.simplyblock.io_storageclusterops.yaml Ships CRD with v1alpha2 version and conversion webhook stanza.
helm-charts/charts/simplyblock-operator/crds/storage.simplyblock.io_controlplanes.yaml Ships CRD with v1alpha2 version and conversion webhook stanza.
Review details

Files not reviewed (1)

  • operator/api/v1alpha2/zz_generated.deepcopy.go: Generated file
  • Files reviewed: 52/53 changed files
  • Comments generated: 3
  • Review effort level: Lite

💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

Comment thread operator/config/rbac/role.yaml
Comment thread operator/go.mod
@noctarius
noctarius force-pushed the worktree-crd-property-renames branch 3 times, most recently from 8e728f6 to 293a42b Compare September 8, 2026 16:44
@noctarius
noctarius requested a lite review from Copilot September 8, 2026 16:48

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Changes recommended

The conversion Service namespace correction runnable is one-shot and does not retry after missing CRDs, which can leave converted kinds unreadable depending on apply/start ordering.

Once you've addressed the issues Copilot identified, you can request another Copilot review.

Review details

Files not reviewed (1)

  • operator/api/v1alpha2/zz_generated.deepcopy.go: Generated file

Suppressed comments (1)

operator/internal/controller/backuprestore_controller_test.go:478

  • This v1alpha2 StorageBackup test object uses the v1alpha1 BackupPhase constant for Status.Phase. Prefer the v1alpha2 phase constants here to keep the test’s intent and version consistent.
  • Files reviewed: 52/55 changed files
  • Comments generated: 3
  • Review effort level: Lite

Comment thread operator/internal/webhook/conversion_service.go Outdated
Comment thread operator/internal/controller/storagenode_controller_unit_test.go
Comment thread operator/internal/controller/backuprestore_controller_test.go
@noctarius
noctarius force-pushed the worktree-crd-property-renames branch 2 times, most recently from 2847956 to 6e9ad8e Compare September 8, 2026 18:49
@noctarius
noctarius force-pushed the main branch 2 times, most recently from 60dceb7 to fbaabe4 Compare September 9, 2026 10:21
@noctarius
noctarius force-pushed the worktree-crd-property-renames branch 2 times, most recently from cd2947a to 096867e Compare September 11, 2026 07:22
noctarius and others added 6 commits September 11, 2026 12:01
The ten designs under operator/docs/designs/crd-redesign each carry a
"Migration from the Registered API" section, and between them they rename a
substantial number of properties on CRDs that are already registered. Each
design states its own renames beside the kind they belong to, which is the
right place to decide them and the wrong place to execute them: they share one
mechanism, one deprecation window, and one set of upgrade risks.

design-property-renames.md is the collected inventory, verified against
operator/api/v1alpha1 rather than taken from the designs alone. Two rows in the
designs do not survive that check and are recorded as corrections: `replicate`
-> `enableReplication` names a field StorageClassParameters does not have, and
`BackupImport` carries sourceClusterName/targetClusterName rather than the
clusterName its design attributes to "all four" backup kinds.

The mechanism is a second version with a conversion webhook. v1alpha2 is the
storage version and the shape every controller reads; v1alpha1 stays served and
deprecated and converts into it, so no reconciler has to know an older spelling
exists, and a kubectl or Argo apply of a v1alpha1 manifest keeps working
unchanged. §3.2 of the document records what that costs, including that a
conversion webhook which is down makes its kinds unreadable rather than merely
unconverted.

Three pieces of infrastructure carry it:

  - The CA bundle reaches the CRDs under both TLS providers: cert-controller's
    rotator gains CRDConversion entries, and the cert-manager provisioner gains
    an equivalent CRD patch it did not have.
  - conversion_service.go corrects the conversion webhook's service namespace
    at runtime. The chart's crds/ directory is not templated, so a CRD shipped
    for the default install names a service that does not exist in any other
    namespace, and an unreachable conversion webhook fails every read of its
    kind.
  - hack/apply-conversion-webhook.sh writes the spec.conversion stanza back
    after each controller-gen run, driven by config/crd/converted-kinds.txt.
    That file is the single source of truth the Go registration is tested
    against, because a kind served without being trusted, or trusted without
    being served, breaks in ways the two lists disagreeing would not otherwise
    reveal.

Four kinds are converted: ControlPlane (the image regrouping and the Ready to
Available phase), StorageBackup (clusterName to clusterRef), StorageClusterOps
(the rolling-restart spec and status blocks, and the action enum), and
StorageNodeOps (nodeRef, remove, the migrate regrouping, and the action enum).
Each has a conversion tested in both directions, because a conversion that maps
going up and copies going down corrupts on the first
`kubectl get -o yaml | kubectl apply -f -`.

The untyped action constants in internal/utils/constants.go are deleted rather
than updated. Once the enum values were recased they became actively dangerous:
an untyped string constant compares against the named type without complaint,
so a stale "remove" tested against StorageNodeOpsActionRemove compiles and is
never equal. That defect reached the working tree and a unit test caught it,
which is the reason for removing the shape that allows it.

StorageNodeSet keeps one version deliberately (§3.6). A kind the redesign
retires does not earn a second one, and the consequence is that
StorageNode.spec.storageNodeSetRef cannot be converted: the cluster's name
lives in the set rather than in the node, and a conversion runs on every read
and cannot go and fetch it. v1alpha2 will carry all three fields and the
controller will fill the new two as it reconciles, which is also how the
workload reparents — one node at a time rather than a fleet-wide rewrite of
owner references during an upgrade.

StoragePool, StorageNode, and StorageCluster are not converted yet.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The operator both serves the conversion webhook and reads the kinds it
converts, and as written that was a bootstrap deadlock rather than a race.

A controller-runtime manager starts its HTTP servers, then its webhook servers,
then syncs its caches, and only then runs everything else. The first two orders
are deliberate and the manager says so: probes and webhooks come first because a
cache sync over a converted kind lists it at v1alpha2, which makes the API
server convert every stored v1alpha1 object, which calls this operator's
webhook. What the manager cannot order is anything that is not one of those
servers, and the CA injection was a leader-elected Runnable — so it ran after
the sync that needed it. The list failed, the cache never synced, the manager
exited, and the injection that would have fixed it never ran. Retrying inside
the operator could not help, because the retry sat on the far side of the
failing sync.

The serving certificate and the CA bundle are now provisioned before the manager
is constructed, through a direct client rather than the manager's. Secret and
CustomResourceDefinition are core and apiextensions kinds that no conversion
webhook stands in front of, so the bootstrap can always make progress whatever
state the converted kinds are in. The conversion webhook is registered
synchronously for the same reason: deferring it to a goroutine forfeited the
ordering guarantee controller-runtime provides.

Rotation is unchanged. cert-controller's rotator and the cert-manager
provisioner keep running under the manager and re-inject whenever the material
changes; the bootstrap only guarantees the first pass has already happened, and
it writes the self-signed material under the key names the rotator reads so that
the rotator adopts it rather than issuing a second CA beside it.

Reusing existing material matters as much as creating it. An operator that
issued a fresh CA on every start would invalidate the bundle its CRDs already
carry, opening a window on every restart in which the API server rejects the
webhook it was just told to trust. The bootstrap adopts what is stored whenever
it is valid for the service's DNS name and not near expiry.

The alternative considered was shipping the CRDs with conversion strategy None
and raising it to Webhook once the operator serves. That removes the cycle and
replaces it with something worse: under None the API server answers a v1alpha2
read of a stored v1alpha1 object by relabeling the apiVersion and pruning every
field the new schema does not know, so a reader sees fields silently missing and
a controller that writes in that window persists the pruned object. A loud,
self-correcting startup failure beats a quiet data-losing one. §3.7 of
design-property-renames.md records the decision.

The CA-bundle and service-namespace injection is now one function rather than
three copies of it, since the bootstrap, the cert-manager provisioner, and the
standing correction that survives a CRD re-apply all need exactly the same
write.

This does not make a Helm upgrade work. The chart ships these CRDs in crds/,
which Helm applies on install and never on upgrade, so an existing release keeps
v1alpha1-only CRDs and the operator finds no v1alpha2 to watch. That is a
packaging change affecting every CRD in the chart and is left to its own commit.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Adding a version and moving storage in the same release makes the upgrade
one-way, and that is the cost this reverses.

With v1alpha2 as the storage version, objects start being written to etcd in a
shape the previous operator cannot read: converting them back down needs exactly
the conversion webhook that only the new operator serves. Rolling the image back
after a bad release therefore leaves the converted kinds unreadable, and
flipping storage back does not recover it, because the objects already in etcd
would then need converting in the other direction by a webhook that is no longer
running.

It also breaks the rollout itself. Every write needs conversion from the moment
the CRDs land, which is before the operator carrying the converter has finished
deploying, so the running operator stops being able to update status on those
kinds and `helm upgrade` fails applying the chart's own ControlPlane resource.

Leaving storage on v1alpha1 costs nothing in this release. The conversion
webhook is still doing real work — the operator reads v1alpha2 and etcd holds
v1alpha1, so every read converts — and the renames are fully in effect. What
changes is only which direction the conversion runs and whether the release can
be undone.

The flip is the next release rather than a distant cleanup, because it gates
everything after it: while v1alpha1 is the storage version, v1alpha2 cannot
carry information v1alpha1 cannot express, so the redesign's additive work —
observedGeneration, conditions, the step machines, spec.abort — has to wait for
it. §3.8 records the sequence.

That constraint is why hub_roundtrip_test.go exists. The storage direction is
now hub to spoke and back, which is not the trip the per-kind tests cover: a
field only the hub can express survives spoke to hub to spoke and is lost going
the other way. The four kinds here are renames and regroupings only, so nothing
is lost, and the tests assert it rather than assuming it. The one normalization
that does occur, an empty managed block on ControlPlane becoming an absent
source, is asserted deliberately: v1alpha1 states the image as one optional
string and cannot express the difference between an absent block and an empty
one, and an empty block configures nothing.

These tests do not yet catch a v1alpha2-only field added later, because they use
hand-written fixtures a new field would not appear in. A reflection-based round
trip would, and is worth adding before any additive work begins.

Also corrects the failure this branch's bootstrap fixes. It was described as the
manager exiting and the operator crash-looping. It does neither: the cache sync
blocks until the process is cancelled rather than giving up, and the health
probes are served by the HTTP servers that start before it, so the pod reports
Ready and keeps reporting Ready while reconciling nothing. There is no restart
to notice and no CrashLoopBackOff to find, which makes it considerably harder to
attribute than a crash.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
design-api-upgrade.md §6.1 settles where the conversion webhook runs, and this
branch had it in the wrong place. A CRD whose conversion strategy is Webhook
cannot be read at all while the webhook is unreachable, and the objects an
administrator reads in order to diagnose a failed operator are simplyblock
custom resources. Serving conversion from inside the operator makes them
unreadable exactly when they are most needed.

The split also dissolves a problem this branch had gone to some trouble to solve
rather than avoid. A manager syncs its caches before it runs anything that is
not an HTTP or webhook server, so an operator that both serves conversion and
watches the converted kinds has to establish the webhook's trust before its own
caches sync. The pre-start bootstrap that does so stays, because the webhook
server's certwatcher still fails on a missing certificate file, but the deadlock
it was written for is gone: this process runs no controllers, syncs no caches,
and reads no simplyblock custom resource, so nothing it does can wait on the
conversion it provides.

cmd/conversion-webhook is the second entry point, built into the operator image
by a second go build rather than into an image of its own (§29.3), so the
conversion code and the API types it converts between are versioned together
with the operator that reads them.

Its Service, Secret, and certificate directory are its own. Sharing the
operator's would reintroduce the coupling in a quieter form: the operator's
rotator pre-creates and owns webhook-server-cert, so a webhook waiting on that
Secret waits on the operator having started.

config/conversion-webhook carries the workload, and is applied by the upgrade
tool rather than by the chart or config/default. The webhook exists for the span
of an API migration (§28), and a Deployment the chart owned would outlive the
reason for it. Two replicas, because this is in the read path for the converting
kinds and a single replica being rescheduled makes them unreadable for the
duration.

The operator keeps its admission webhooks and loses everything conversion:
the /convert registration, the pre-start bootstrap, the CRDConversion entries in
its rotator, and the apiextensions grants, which move to a role bound only to
the webhook's ServiceAccount. The converting CRDs' clientConfig now names the
conversion service rather than the operator's.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The storage version is not a property of a release, and treating it as one made
a fresh install depend on a conversion webhook it has no use for.

A cluster installed today has no v1alpha1 objects and no v1alpha1 clients: the
four manifests that spoke it are this repository's own. Storing v1alpha2 from
the first write therefore means nothing is ever converted, the webhook is inert,
and it is not deployed at all. The manifests here ship that shape, and the
chart's ControlPlane is authored at v1alpha2 to match — a chart writing
v1alpha1 would be the one client forcing conversion on a cluster where nothing
serves it, and its spec.image would be pruned rather than converted, since the
apiVersion decides which schema the body is read against.

An upgrade takes the other value, and the upgrade tool writes it. A server-side
apply overwrites the live storage version, so applying these manifests unchanged
to an existing cluster would move storage before anything could convert: every
write would need a webhook still rolling out, the running operator would stop
being able to update status, and the release would be irreversible, because
objects written as v1alpha2 cannot be read by an operator that does not serve
conversion. Holding storage at v1alpha1 on apply is a positive act rather than
an omission, and design-api-upgrade.md §24 owns the flip and the rewrite that
follows it.

This reverses the previous commit's placement while keeping its reasoning, which
was right about the danger and wrong about where the fix belongs: the upgrade
path needs storage held back, and the fresh path does not, so the decision
belongs to whoever is applying rather than to the marker. §3.8 records it.

Two defects in the conversion webhook's manifests go with it, both of which
would have failed on a cluster and neither of which any test here reaches. The
ClusterRoleBinding named a ServiceAccount subject with no namespace, which is
invalid in a cluster-scoped binding and would have granted nothing, leaving the
webhook unable to inject its own CA. And the Deployment carried kustomize's
controller:latest placeholder, since this directory is not built through
config/default and had no image transformer of its own.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The upgrade tool applies the CRDs itself and embeds them in its binary, so that
the definitions a run installs are the ones the conversion code in the same
binary was built against (design-api-upgrade.md §11). Its copy is regenerated by
crd-embed, which had not run since the four converting kinds gained a v1alpha2.

The ordering in the manifests target matters and is the only judgement here.
apply-conversion-webhook.sh runs before crd-embed, so the copy the tool carries
is the one with the conversion stanza in it. Reversed, the tool would install
CRDs serving two versions with no conversion strategy, which is the shape that
answers a v1alpha2 read of a stored v1alpha1 object by pruning every field the
new schema does not know.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@noctarius
noctarius force-pushed the worktree-crd-property-renames branch from 096867e to f63c655 Compare September 11, 2026 10:03
@noctarius
noctarius merged commit 3d3d8f4 into main Sep 11, 2026
22 checks passed
@noctarius
noctarius deleted the worktree-crd-property-renames branch September 11, 2026 12:47
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants