Annotate storageClass as immutable across every stateful app whose chart exposes one — clickhouse, foundationdb, harbor, http-cache, kafka (Kafka + ZooKeeper), kubernetes (top-level only), mariadb, mongodb, nats, openbao, opensearch, postgres, qdrant, rabbitmq, redis, vm-disk. Without coverage across the catalogue the UI would render storageClass read-only on some apps and editable on others — a worse mental model than uniform behaviour. The annotation lands at the schema layer (CEL XValidation rule 'self == oldSelf' on values.schema.json / types.go / embedded openAPISchema in cozyrds/<app>.yaml). The cozystack aggregated apiserver does not yet evaluate CEL on application-backed resources; that is tracked at cozystack/cozystack#2657. The UI (cozystack/cozystack-ui#6) consumes the marker today. The PVC-pinning argument is identical for every chart: Kubernetes fixes a PVC's storageClassName at creation time, so editing this field on an existing resource never migrated data. The schema annotation makes that contract explicit. kubernetes.nodeGroups[name].storageClass is intentionally NOT annotated: the field is optional and undefaulted, so a strict 'self == oldSelf' rule would block any future attempt to set it on an existing node group. A richer ratcheting form is needed there — see docs/storage-immutability.md. A refactor in pkg/registry/apps/application/rest.go extracts the inline schema-build path into buildSpecSchema so it can be unit-tested. Three regression tests in rest_spec_schema_test.go pin the contract that x-kubernetes-validations survives the v1->internal->structural conversion (loaded from the actual MariaDB ApplicationDefinition fixture on disk, not a hand-edited mock). Each per-app README gains a one-line blockquote pointing to a new docs/storage-immutability.md that explains the contract and which consumers enforce it today. Requires cozystack/cozyvalues-gen with @immutable support — bump the pre-commit pin in .github/workflows/pre-commit.yml once the release exists. Assisted-By: Claude <noreply@anthropic.com> Signed-off-by: Aleksei Sviridkin <f@lex.la>
2.6 KiB
storageClass is immutable after creation
Every stateful Cozystack application that exposes a storageClass parameter declares it immutable in the chart schema (x-kubernetes-validations: [{rule: "self == oldSelf"}]). This document explains the contract and where it is enforced.
Why
storageClass binds the StatefulSet's PVC template. Kubernetes fixes PersistentVolumeClaim.spec.storageClassName at PVC creation time — editing volumeClaimTemplates[].spec.storageClassName on an existing StatefulSet does not retroactively migrate any data. A user who edits storageClass on an existing resource may believe their data is being moved to the new class; it is not. Locking the field at the schema layer makes that contract explicit.
What enforces it
| Consumer | Behavior |
|---|---|
| Cozystack UI (cozystack/cozystack-ui#6) | Reads the CEL rule from the chart's openAPISchema and renders storageClass as a disabled, helper-text-annotated field on edit forms. Save-time overlay reinstates the original value before PUT. |
kubectl edit / kubectl patch against the cozystack aggregated apiserver |
Currently accepted — the apiserver does not yet evaluate CEL rules embedded in ApplicationDefinition.openAPISchema. The change passes through but does not propagate to existing PVCs (see "Why" above), so no data corruption is possible. Apiserver enforcement is tracked in cozystack/cozystack#2657. |
kubectl edit / kubectl patch against native CRDs |
Enforced today by the standard apiextensions apiserver. |
Apps covered
The following charts annotate at least one storageClass field as immutable:
clickhouse,foundationdb,harbor,http-cache,kafka(Kafka + ZooKeeper),kubernetes(top-level only — see below),mariadb,mongodb,nats,openbao,opensearch,postgres,qdrant,rabbitmq,redis,vm-disk.
The per-node-group kubernetes.nodeGroups[name].storageClass field is intentionally NOT annotated immutable: it is optional and undefaulted, so a strict self == oldSelf rule would block any future attempt to set it on an existing node group. If a richer ratcheting form (!has(oldSelf.storageClass) || self.storageClass == oldSelf.storageClass) becomes available, the annotation should be added back.
If you add a new stateful chart that exposes storageClass, annotate it the same way:
## @param {string} storageClass - StorageClass used to store the data.
## @immutable
storageClass: ""
For fields declared inside a @typedef, place ## @immutable on the line below the ## @field it should attach to.