From eee5eb98488b7e63c829889130722cd0ebdf418b Mon Sep 17 00:00:00 2001 From: IvanHunters Date: Thu, 21 May 2026 12:14:55 +0500 Subject: [PATCH] fix(platform): migrate ephemeralStorage to diskSize via pre-upgrade hook Add pre-upgrade migration 41 that walks all kuberneteses.apps.cozystack.io Application CRs and renames nodeGroups[*].ephemeralStorage to diskSize, preserving the user's value. Without this migration, clusters upgraded after PR #2454 either fail to reconcile (hard fail blocks all Flux operations) or silently lose the user's disk size setting (default 20Gi replaces whatever was configured). The migration is idempotent and best-effort: a failed patch is logged and leaves the version stamp at 41 for retry on next upgrade. Update the chart-side guard error message to direct operators to the migration job logs when the field appears post-upgrade (regression detector). Bump migrations.targetVersion 41 -> 42. Assisted-By: Claude Signed-off-by: Myasnikov Daniil (cherry picked from commit ed1bb53d2c33588b018b2642e3ac1fd4709fa844) --- packages/apps/kubernetes/README.md | 2 +- .../apps/kubernetes/templates/cluster.yaml | 2 +- .../apps/kubernetes/tests/cluster_test.yaml | 6 +- .../platform/images/migrations/migrations/41 | 113 ++++++++++++++++++ packages/core/platform/values.yaml | 2 +- 5 files changed, 119 insertions(+), 6 deletions(-) create mode 100755 packages/core/platform/images/migrations/migrations/41 diff --git a/packages/apps/kubernetes/README.md b/packages/apps/kubernetes/README.md index 2fca15ae4..7f12d56f7 100644 --- a/packages/apps/kubernetes/README.md +++ b/packages/apps/kubernetes/README.md @@ -85,7 +85,7 @@ See the reference for components utilized in this service: ## Breaking Changes -- **`ephemeralStorage` renamed to `diskSize`**: The `nodeGroups[name].ephemeralStorage` field has been renamed to `nodeGroups[name].diskSize` to better reflect its purpose (persistent disk for kubelet and containerd data). There is no backward-compatibility fallback; users MUST update their configurations to use `diskSize` instead of `ephemeralStorage`. If `ephemeralStorage` is still present in values, Helm template rendering will fail with an error directing you to use `diskSize`. When upgrading the CRD directly (bypassing Helm), the unrecognized field is silently dropped and kubelet storage reverts to the default 20Gi. Existing VMs will be automatically rolling-updated via CAPI when the new values are applied. State persists across same-VM reboots (virt-launcher restart, guest reboot, node failure); VM replacement by CAPI (e.g. nodeGroup field change, MachineHealthCheck remediation) provisions a fresh PVC. +- **`ephemeralStorage` renamed to `diskSize`** (v1.4): The `nodeGroups[name].ephemeralStorage` field has been renamed to `nodeGroups[name].diskSize` to better reflect its purpose (persistent disk for kubelet and containerd data). Existing clusters are migrated transparently by platform migration 41 during the pre-upgrade hook — no manual action is required. Newly written values should use `diskSize`. Existing VMs will be automatically rolling-updated via CAPI when the new values are applied. State persists across same-VM reboots (virt-launcher restart, guest reboot, node failure); VM replacement by CAPI (e.g. nodeGroup field change, MachineHealthCheck remediation) provisions a fresh PVC. ## Parameters diff --git a/packages/apps/kubernetes/templates/cluster.yaml b/packages/apps/kubernetes/templates/cluster.yaml index 152c1b4dc..a4508a600 100644 --- a/packages/apps/kubernetes/templates/cluster.yaml +++ b/packages/apps/kubernetes/templates/cluster.yaml @@ -39,7 +39,7 @@ spec: cdi.kubevirt.io/storage.usePopulator: "false" spec: {{- if hasKey .group "ephemeralStorage" }} - {{- fail (printf "nodeGroup %q: ephemeralStorage is no longer supported. Rename it to diskSize. See README.md for migration instructions." .groupName) }} + {{- fail (printf "nodeGroup %q: ephemeralStorage is no longer supported and should have been automatically migrated to diskSize by platform migration 41. If you see this error after upgrading, the migration did not run — check the cozystack-migration-hook Job logs in cozy-system." .groupName) }} {{- end }} source: blank: {} diff --git a/packages/apps/kubernetes/tests/cluster_test.yaml b/packages/apps/kubernetes/tests/cluster_test.yaml index 80f366e03..88fc71180 100644 --- a/packages/apps/kubernetes/tests/cluster_test.yaml +++ b/packages/apps/kubernetes/tests/cluster_test.yaml @@ -351,7 +351,7 @@ tests: nodeGroups.md0.ephemeralStorage: 50Gi asserts: - failedTemplate: - errorMessage: 'nodeGroup "md0": ephemeralStorage is no longer supported. Rename it to diskSize. See README.md for migration instructions.' + errorMessage: 'nodeGroup "md0": ephemeralStorage is no longer supported and should have been automatically migrated to diskSize by platform migration 41. If you see this error after upgrading, the migration did not run — check the cozystack-migration-hook Job logs in cozy-system.' - it: fails when ephemeralStorage is set to empty string release: @@ -361,7 +361,7 @@ tests: nodeGroups.md0.ephemeralStorage: "" asserts: - failedTemplate: - errorMessage: 'nodeGroup "md0": ephemeralStorage is no longer supported. Rename it to diskSize. See README.md for migration instructions.' + errorMessage: 'nodeGroup "md0": ephemeralStorage is no longer supported and should have been automatically migrated to diskSize by platform migration 41. If you see this error after upgrading, the migration did not run — check the cozystack-migration-hook Job logs in cozy-system.' - it: fails when both ephemeralStorage and diskSize are set release: @@ -372,7 +372,7 @@ tests: nodeGroups.md0.diskSize: 50Gi asserts: - failedTemplate: - errorMessage: 'nodeGroup "md0": ephemeralStorage is no longer supported. Rename it to diskSize. See README.md for migration instructions.' + errorMessage: 'nodeGroup "md0": ephemeralStorage is no longer supported and should have been automatically migrated to diskSize by platform migration 41. If you see this error after upgrading, the migration did not run — check the cozystack-migration-hook Job logs in cozy-system.' ############################################### # KubeadmConfigTemplate — files block # diff --git a/packages/core/platform/images/migrations/migrations/41 b/packages/core/platform/images/migrations/migrations/41 new file mode 100755 index 000000000..5791d6ed0 --- /dev/null +++ b/packages/core/platform/images/migrations/migrations/41 @@ -0,0 +1,113 @@ +#!/bin/bash +# Migration 41 --> 42 +# Rename nodeGroups[*].ephemeralStorage to nodeGroups[*].diskSize in every +# kuberneteses.apps.cozystack.io Application CR. +# +# PR #2454 renamed the field in the kubernetes app chart with no fallback. +# Without this migration, upgraded clusters either fail to reconcile (hard fail +# behavior) or silently lose their disk size (silent-drop behavior). +# +# Idempotent: re-running on already-migrated values is a no-op +# (has("ephemeralStorage") is false on the post-migration shape). +# Best-effort: a failed patch is logged and leaves the version stamp at 41 so +# the migration retries on the next platform upgrade. + +set -euo pipefail + +DRY_RUN="${MIGRATION_DRY_RUN:-0}" +FAILURES=0 +PATCHED=0 +SKIPPED=0 + +# Emit ephemeralStorage: null rather than dropping the key, because JSON merge +# patch only deletes a field when an explicit null is present in the patch +# document. A patch that simply omits a key leaves the existing key intact. +JQ_TRANSFORM=' + walk( + if type == "object" and has("nodeGroups") and (.nodeGroups | type) == "object" then + .nodeGroups |= with_entries( + .value |= ( + if type == "object" and has("ephemeralStorage") then + (if has("diskSize") and (.diskSize // "") != "" then . + else .diskSize = .ephemeralStorage end) + | .ephemeralStorage = null + else . end + ) + ) + else . end + ) +' + +patch_object() { + local kind="$1" ns="$2" name="$3" selector="$4" + local obj current new patch + + obj=$(kubectl get "$kind" --namespace "$ns" "$name" --output json 2>/dev/null || true) + if [ -z "$obj" ]; then SKIPPED=$((SKIPPED + 1)); return 0; fi + + current=$(printf '%s' "$obj" | jq -c "$selector // null") + if [ "$current" = "null" ]; then SKIPPED=$((SKIPPED + 1)); return 0; fi + + new=$(printf '%s' "$current" | jq -c "$JQ_TRANSFORM") + if [ "$current" = "$new" ]; then SKIPPED=$((SKIPPED + 1)); return 0; fi + + patch=$(jq --null-input --argjson val "$new" --arg path "${selector#.}" ' + def to_path: split("."); + {} | setpath($path | to_path; $val) + ') + + if [ "$DRY_RUN" = "1" ]; then + echo "[dry-run] would patch $kind $ns/$name ($selector)" + diff <(printf '%s' "$current" | jq --sort-keys .) \ + <(printf '%s' "$new" | jq --sort-keys .) || true + PATCHED=$((PATCHED + 1)) + return 0 + fi + + local kubectl_err + if kubectl_err=$(printf '%s' "$patch" | kubectl patch "$kind" --namespace "$ns" "$name" \ + --type merge --patch-file /dev/stdin 2>&1 >/dev/null); then + echo "Patched $kind $ns/$name ($selector)" + PATCHED=$((PATCHED + 1)) + else + echo "WARN: failed to patch $kind $ns/$name ($selector): ${kubectl_err:-unknown error}" >&2 + FAILURES=$((FAILURES + 1)) + fi +} + +list_objects() { + local kind="$1" out + if ! out=$(kubectl get "$kind" --all-namespaces \ + --output 'jsonpath={range .items[*]}{.metadata.namespace}{"\t"}{.metadata.name}{"\n"}{end}' \ + 2>&1); then + case "$out" in + *"the server doesn't have a resource type"*|*"no matches for kind"*|*"NotFound"*) return 0 ;; + esac + echo "ERROR: kubectl get $kind failed: $out" >&2 + return 1 + fi + printf '%s' "$out" +} + +echo "==> Migrating kuberneteses.apps.cozystack.io" +items=$(list_objects kuberneteses.apps.cozystack.io) || exit 1 +while IFS=$'\t' read -r ns name; do + [ -z "$ns" ] && continue + patch_object kuberneteses.apps.cozystack.io "$ns" "$name" .spec +done <<<"$items" + +echo "==> Summary: patched=$PATCHED skipped=$SKIPPED failures=$FAILURES" | tee /dev/stderr + +if [ "$DRY_RUN" = "1" ]; then + echo "Dry-run complete; version stamp not advanced" + exit 0 +fi + +if [ "$FAILURES" -gt 0 ]; then + echo "WARN: $FAILURES patch(es) failed; leaving CURRENT_VERSION at 41 for retry on next upgrade." + exit 0 +fi + +kubectl create configmap --namespace cozy-system cozystack-version \ + --from-literal version=42 --dry-run=client --output yaml \ + | kubectl apply --filename - diff --git a/packages/core/platform/values.yaml b/packages/core/platform/values.yaml index 28992a8b2..a8f05d02a 100644 --- a/packages/core/platform/values.yaml +++ b/packages/core/platform/values.yaml @@ -6,7 +6,7 @@ sourceRef: migrations: enabled: false image: ghcr.io/cozystack/cozystack/platform-migrations:v1.4.0-rc.2@sha256:17390197a9b11d76a8dd1b24c6d1512adb7d3226b28b5bcba8551b14b1ec4be0 - targetVersion: 40 + targetVersion: 42 # Bundle deployment configuration bundles: system: