Collect the CRD redesign's property renames and migrate the first four kinds - #503
Conversation
There was a problem hiding this comment.
🟡 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/v1alpha2types forControlPlane,StorageBackup,StorageClusterOps, andStorageNodeOps, plus bidirectional conversion logic and round-trip tests. - Updates controllers and unit tests to read/own
v1alpha2resources and ensures CRD regeneration preservesspec.conversionvia a post-controller-genstep.
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 clusterName↔clusterRef. |
| 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.
8e728f6 to
293a42b
Compare
There was a problem hiding this comment.
🟡 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
2847956 to
6e9ad8e
Compare
60dceb7 to
fbaabe4
Compare
cd2947a to
096867e
Compare
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>
096867e to
f63c655
Compare
The ten designs under
operator/docs/designs/crd-redesigneach 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.mdis the collected list, verified againstoperator/api/v1alpha1rather than taken from the designs alone. Two rows do not survive that check and are recorded as corrections:replicate→enableReplicationis named bydesign-crd-model.md§9.6 anddesign-storagepool.md§11, butStorageClassParametershas noreplicatefield. It is a target-state addition, not a rename.design-storagebackup.md§13 putsspec.clusterNameon "all four" backup kinds.BackupImporthassourceClusterName/targetClusterNameinstead — 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.
v1alpha2is the storage version and the shape every controller reads;v1alpha1stays served and deprecated and converts into it. No reconciler has to know an older spelling exists, and akubectlor Argo CD apply of av1alpha1manifest 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:
CRDConversionentries; the cert-manager provisioner gains an equivalent CRD patch it did not have.conversion_service.gocorrects the conversion webhook's service namespace at runtime. The chart'scrds/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.shwrites thespec.conversionstanza back after eachcontroller-genrun, driven byconfig/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
ControlPlanespec.image→spec.source.managed.image; phaseReady→AvailableStorageBackupspec.clusterName→spec.clusterRefStorageClusterOpsspec.nodeRollingRestart→spec.rollingRestart; its status twin; the action enum recasedStorageNodeOpsstorageNodeRef→nodeRef;drain→remove;targetWorkerNode/newSsdPcie→spec.migrate; the action enum recasedEvery 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.goare 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 againstStorageNodeOpsActionRemovecompiles and is never equal. That defect reached the working tree — a drain stopped setting itsValidatingsub-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.storageNodeSetRefcannot 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
v1alpha2will carry all three fields —storageNodeSetRefdeprecated,clusterRefandnodeSetoptional — 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, andStorageClusterare not converted yet — they carry the regroupings, the inverting toggles, and theBackupSpecremovals that need the round-trip annotation stash.BackupPolicy→StorageBackupPolicy,BackupRestore→StorageBackupOps, andVolumeMigration→PersistentVolumeOpsare 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
StorageClassqos_*parameter keys, thesimplyblock.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