Files
cozystack/docs/storage-immutability.md
Alexey Artamonov e00c8f4a30 fix(kafka): address KRaft-migration review — broker pool adoption, hook safety, tests
Addresses the branch review on the ZK->KRaft migration path.

CRITICAL — broker node pool adoption. Strimzi derives broker/PVC names as
<cluster>-<pool>-<id>; a pre-node-pool ZooKeeper cluster has brokers
<cluster>-kafka-N, so the adopting broker pool must be named exactly "kafka"
or the operator provisions fresh empty brokers and orphans the tenant's data.
A new kafka.brokerPoolName helper resolves the broker pool to "kafka" for an
existing (migrating) cluster and keeps the release-scoped "<release>-broker"
for fresh installs, so multiple Kafka instances still coexist in one namespace.
The migration hook creates the pool as "kafka" and fails closed if a "kafka"
pool already belongs to another cluster (node pool names are namespace-unique,
so in-place migration of co-namespaced ZK clusters is unsupported). Wired
through kafkanodepools.yaml, workloadmonitor.yaml and the hook.

MAJOR — migration robustness.
- Raise the HelmRelease timeout: the migration Job can run tens of minutes but
  the release timeout defaults to 10m, so helm-controller aborted and thrashed
  the pre-upgrade hook. Add release.cozystack.io/helm-install-timeout=45m on the
  kafka ApplicationDefinition, above the Job's activeDeadlineSeconds (2400s).
- Idempotent migration: do not re-issue kraft=migration once the cluster is past
  the ZooKeeper phase (an invalid backward transition Strimzi rejects on retry).
- Fail closed on the decision gates: distinguish a genuine NotFound (skip) from
  any other kubectl error (abort), and never treat an empty kafkaMetadataState on
  an existing CR as "migrated".

MINOR — version downgrade documented as unsupported; integer (not lexical)
compare in the version gate; deleteClaim/PVC-retention and the one-per-namespace
migration limitation documented in the README; docs/storage-immutability.md
updated for KRaft.

Tests — helm-unittest coverage for the broker pool name, the hook guards and the
idempotent/fail-closed logic; a bats check for the helm-install-timeout budget
ordering; and a Chainsaw ZK->KRaft migration suite that seeds a ZooKeeper
cluster, drives the migration, and asserts the original broker PVC survives (not
recreated) and the cluster reaches KRaft. The migration sequence and the
Chainsaw assertions were validated against a live cluster.

Signed-off-by: Alexey Artamonov <aleksei.artamonov@aenix.io>
2026-08-26 18:25:58 +03:00

2.7 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 (packages/system/dashboard/images/console) 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 broker + KRaft controller), kubernetes, kubernetes-nodes, mariadb, mongodb, nats, openbao, opensearch, postgres, qdrant, rabbitmq, redis, vm-disk.

The kubernetes-nodes chart annotates both its per-pool storageClass (the worker node system-disk class, defaulted replicated) and its cluster field immutable. storageClass binds the same PVC-template contract as every other chart in the table, so the plain self == oldSelf rule applies for the reason in "Why" above. cluster is immutable for a different reason: it wires the pool's CAPI objects to one parent cluster kubernetes-<cluster>, and repointing a live pool at another cluster would orphan its running worker VMs rather than migrate them.

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.