From c7290f3521876529f12ba4eb24b3b9e83c62fdcd Mon Sep 17 00:00:00 2001 From: Andrei Kvapil Date: Tue, 24 Feb 2026 23:05:03 +0100 Subject: [PATCH 1/7] fix(platform): make migration 26 helm secret deletion robust Migration 26 silently skipped namespace processing when kubectl queries failed, leaving helm release secrets intact. This caused helm to diff old vs new chart manifests during upgrade, deleting VLogs/CNPG resources and their PVCs. - Remove silent error suppression (2>/dev/null || true) from namespace discovery and HR suspend commands - Add fallback secret deletion by name pattern when label selector does not match - Add verification that all helm release secrets are deleted Co-Authored-By: Claude Signed-off-by: Andrei Kvapil --- .../platform/images/migrations/migrations/26 | 40 +++++++++++++++++-- 1 file changed, 36 insertions(+), 4 deletions(-) diff --git a/packages/core/platform/images/migrations/migrations/26 b/packages/core/platform/images/migrations/migrations/26 index 5f385b6e..c60af4d4 100755 --- a/packages/core/platform/images/migrations/migrations/26 +++ b/packages/core/platform/images/migrations/migrations/26 @@ -2,6 +2,7 @@ # Migration 26 --> 27 # Migrate monitoring resources from extra/monitoring to system/monitoring # This migration re-labels resources so they become owned by monitoring-system HelmRelease +# and deletes old helm release secrets so that helm does not diff old vs new chart manifests. set -euo pipefail @@ -35,10 +36,39 @@ relabel_resources() { done } +# Delete all helm release secrets for a given release name in a namespace. +# Uses both label selector and name-pattern matching to ensure complete cleanup. +delete_helm_secrets() { + local ns="$1" + local release="$2" + + # Primary: delete by label selector + kubectl delete secrets -n "$ns" -l "name=${release},owner=helm" --ignore-not-found + + # Fallback: find and delete by name pattern (in case labels were modified) + local remaining + remaining=$(kubectl get secrets -n "$ns" -o name | { grep "^secret/sh\.helm\.release\.v1\.${release}\." || true; }) + if [ -n "$remaining" ]; then + echo " Found secrets not matched by label selector, deleting by name..." + echo "$remaining" | while IFS= read -r secret; do + echo " Deleting $secret" + kubectl delete -n "$ns" "$secret" --ignore-not-found + done + fi + + # Verify all secrets are gone + remaining=$(kubectl get secrets -n "$ns" -o name | { grep "^secret/sh\.helm\.release\.v1\.${release}\." || true; }) + if [ -n "$remaining" ]; then + echo " ERROR: Failed to delete helm release secrets:" + echo "$remaining" + return 1 + fi +} + # Find all tenant namespaces with monitoring HelmRelease echo "Finding tenant namespaces with monitoring HelmRelease..." NAMESPACES=$(kubectl get hr --all-namespaces -l apps.cozystack.io/application.kind=Monitoring \ - -o jsonpath='{range .items[*]}{.metadata.namespace}{"\n"}{end}' 2>/dev/null | sort -u || true) + -o jsonpath='{range .items[*]}{.metadata.namespace}{"\n"}{end}' | sort -u) if [ -z "$NAMESPACES" ]; then echo "No monitoring HelmReleases found in tenant namespaces, skipping migration" @@ -66,7 +96,7 @@ for ns in $NAMESPACES; do # Step 1: Suspend the HelmRelease echo "" echo "Step 1: Suspending HelmRelease monitoring..." - kubectl patch hr -n "$ns" monitoring --type=merge -p '{"spec":{"suspend":true}}' 2>/dev/null || true + kubectl patch hr -n "$ns" monitoring --type=merge -p '{"spec":{"suspend":true}}' # Wait a moment for reconciliation to stop sleep 2 @@ -74,7 +104,7 @@ for ns in $NAMESPACES; do # Step 2: Delete helm secrets for the monitoring release echo "" echo "Step 2: Deleting helm secrets for monitoring release..." - kubectl delete secrets -n "$ns" -l name=monitoring,owner=helm --ignore-not-found + delete_helm_secrets "$ns" "monitoring" # Step 3: Relabel resources to be owned by monitoring-system echo "" @@ -121,7 +151,9 @@ for ns in $NAMESPACES; do echo "Processing Cozystack resources..." relabel_resources "$ns" "workloadmonitors.cozystack.io" - # Step 4: Delete the suspended HelmRelease (Flux won't delete resources when HR is suspended) + # Step 4: Delete the suspended HelmRelease + # Helm secrets are already gone, so flux finalizer will find no release to uninstall + # and will simply remove the finalizer without deleting any resources. echo "" echo "Step 4: Deleting suspended HelmRelease monitoring..." kubectl delete hr -n "$ns" monitoring --ignore-not-found From 880b99f3f77be9cb398ab01152f7b5246574af7d Mon Sep 17 00:00:00 2001 From: Andrei Kvapil Date: Tue, 24 Feb 2026 23:34:34 +0100 Subject: [PATCH 2/7] fix(platform): wrap grep in migrations 28 and 29 to prevent pipefail exits grep returns exit code 1 when no lines match. With set -euo pipefail, this kills the script when all secrets are helm-release secrets or when no matching resources exist. Wrap grep calls with { ... || true; }. Also fix reconcile annotation in migration 29 to use RFC3339 timestamp format instead of Unix epoch, which Flux v2 expects. Co-Authored-By: Claude Signed-off-by: Andrei Kvapil --- packages/core/platform/images/migrations/migrations/28 | 8 ++++---- packages/core/platform/images/migrations/migrations/29 | 6 +++--- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/packages/core/platform/images/migrations/migrations/28 b/packages/core/platform/images/migrations/migrations/28 index 259d5fd8..1d3de565 100755 --- a/packages/core/platform/images/migrations/migrations/28 +++ b/packages/core/platform/images/migrations/migrations/28 @@ -348,7 +348,7 @@ PVCEOF # --- 3g: Clone Secrets --- echo " --- Clone Secrets ---" for secret in $(kubectl -n "$NAMESPACE" get secret -o name 2>/dev/null \ - | grep "secret/${OLD_NAME}" | grep -v "sh.helm.release"); do + | { grep "secret/${OLD_NAME}" || true; } | { grep -v "sh.helm.release" || true; }); do old_secret_name="${secret#secret/}" new_secret_name="${NEW_NAME}${old_secret_name#${OLD_NAME}}" clone_resource "$NAMESPACE" "secret" "$old_secret_name" "$new_secret_name" "$OLD_NAME" "$NEW_NAME" @@ -357,7 +357,7 @@ PVCEOF # --- 3h: Clone ConfigMaps --- echo " --- Clone ConfigMaps ---" for cm in $(kubectl -n "$NAMESPACE" get configmap -o name 2>/dev/null \ - | grep "configmap/${OLD_NAME}"); do + | { grep "configmap/${OLD_NAME}" || true; }); do old_cm_name="${cm#configmap/}" new_cm_name="${NEW_NAME}${old_cm_name#${OLD_NAME}}" clone_resource "$NAMESPACE" "configmap" "$old_cm_name" "$new_cm_name" "$OLD_NAME" "$NEW_NAME" @@ -468,13 +468,13 @@ PVCEOF fi for secret in $(kubectl -n "$NAMESPACE" get secret -o name 2>/dev/null \ - | grep "secret/${OLD_NAME}" | grep -v "sh.helm.release"); do + | { grep "secret/${OLD_NAME}" || true; } | { grep -v "sh.helm.release" || true; }); do old_secret_name="${secret#secret/}" delete_resource "$NAMESPACE" "secret" "$old_secret_name" done for cm in $(kubectl -n "$NAMESPACE" get configmap -o name 2>/dev/null \ - | grep "configmap/${OLD_NAME}"); do + | { grep "configmap/${OLD_NAME}" || true; }); do old_cm_name="${cm#configmap/}" delete_resource "$NAMESPACE" "configmap" "$old_cm_name" done diff --git a/packages/core/platform/images/migrations/migrations/29 b/packages/core/platform/images/migrations/migrations/29 index a76071a5..1ec33cda 100755 --- a/packages/core/platform/images/migrations/migrations/29 +++ b/packages/core/platform/images/migrations/migrations/29 @@ -315,7 +315,7 @@ PVCEOF # --- 2i: Clone Secrets --- echo " --- Clone Secrets ---" kubectl -n "$NAMESPACE" get secret -o name 2>/dev/null \ - | grep "secret/${OLD_NAME}" | grep -v "sh.helm.release" | grep -v "values" \ + | { grep "secret/${OLD_NAME}" || true; } | { grep -v "sh.helm.release" || true; } | { grep -v "values" || true; } \ | while IFS= read -r secret; do old_secret_name="${secret#secret/}" suffix="${old_secret_name#${OLD_NAME}}" @@ -542,7 +542,7 @@ SVCEOF # --- 2q: Delete old resources --- echo " --- Delete old resources ---" kubectl -n "$NAMESPACE" get secret -o name 2>/dev/null \ - | grep "secret/${OLD_NAME}" | grep -v "sh.helm.release" | grep -v "values" \ + | { grep "secret/${OLD_NAME}" || true; } | { grep -v "sh.helm.release" || true; } | { grep -v "values" || true; } \ | while IFS= read -r secret; do old_secret_name="${secret#secret/}" delete_resource "$NAMESPACE" "secret" "$old_secret_name" @@ -705,7 +705,7 @@ for entry in "${INSTANCES[@]}"; do # Force immediate reconciliation echo " [TRIGGER] Reconcile ${ns}/hr/${disk_name}" kubectl -n "$ns" annotate hr "$disk_name" --overwrite \ - "reconcile.fluxcd.io/requestedAt=$(date +%s)" 2>/dev/null || true + "reconcile.fluxcd.io/requestedAt=$(date -u +'%Y-%m-%dT%H:%M:%SZ')" 2>/dev/null || true fi done From a9adda5e880791f7ef6093f8d79898ee7774dc58 Mon Sep 17 00:00:00 2001 From: Andrei Kvapil Date: Tue, 24 Feb 2026 23:34:40 +0100 Subject: [PATCH 3/7] fix(platform): make migration 27 skip missing CRDs and add secret cleanup fallback Migration 27 failed with set -e when Piraeus CRDs did not exist on clusters without linstor. Add existence check before annotating CRDs. Also add name-pattern fallback for helm secret deletion, consistent with migration 26. Co-Authored-By: Claude Signed-off-by: Andrei Kvapil --- .../platform/images/migrations/migrations/27 | 18 ++++++++++++++++-- 1 file changed, 16 insertions(+), 2 deletions(-) diff --git a/packages/core/platform/images/migrations/migrations/27 b/packages/core/platform/images/migrations/migrations/27 index 3e8719e9..5708ebbb 100755 --- a/packages/core/platform/images/migrations/migrations/27 +++ b/packages/core/platform/images/migrations/migrations/27 @@ -5,10 +5,24 @@ set -euo pipefail # Migrate Piraeus CRDs to piraeus-operator-crds Helm release for crd in linstorclusters.piraeus.io linstornodeconnections.piraeus.io linstorsatelliteconfigurations.piraeus.io linstorsatellites.piraeus.io; do - kubectl annotate crd "$crd" meta.helm.sh/release-namespace=cozy-linstor meta.helm.sh/release-name=piraeus-operator-crds --overwrite - kubectl label crd "$crd" app.kubernetes.io/managed-by=Helm helm.toolkit.fluxcd.io/namespace=cozy-linstor helm.toolkit.fluxcd.io/name=piraeus-operator-crds --overwrite + if kubectl get crd "$crd" >/dev/null 2>&1; then + echo " Relabeling CRD $crd" + kubectl annotate crd "$crd" meta.helm.sh/release-namespace=cozy-linstor meta.helm.sh/release-name=piraeus-operator-crds --overwrite + kubectl label crd "$crd" app.kubernetes.io/managed-by=Helm helm.toolkit.fluxcd.io/namespace=cozy-linstor helm.toolkit.fluxcd.io/name=piraeus-operator-crds --overwrite + else + echo " CRD $crd not found, skipping" + fi done + +# Delete old piraeus-operator helm secrets (by label and by name pattern) kubectl delete secret -n cozy-linstor -l name=piraeus-operator,owner=helm --ignore-not-found +remaining=$(kubectl get secrets -n cozy-linstor -o name 2>/dev/null | { grep "^secret/sh\.helm\.release\.v1\.piraeus-operator\." || true; }) +if [ -n "$remaining" ]; then + echo " Deleting remaining piraeus-operator helm secrets by name..." + echo "$remaining" | while IFS= read -r secret; do + kubectl delete -n cozy-linstor "$secret" --ignore-not-found + done +fi # Stamp version kubectl create configmap -n cozy-system cozystack-version \ From 7871d425dd4ec5f6f1e73a2f2bc0d1c0bd276d9b Mon Sep 17 00:00:00 2001 From: Andrei Kvapil Date: Tue, 24 Feb 2026 23:34:47 +0100 Subject: [PATCH 4/7] fix(etcd): increase HelmRelease timeout to 30m for cert rotation The post-upgrade hook deletes TLS certificates and etcd pods to trigger cert-manager regeneration. With 3 replicas and startup probes allowing up to 25 minutes per pod, the previous 10m timeout was insufficient. Co-Authored-By: Claude Signed-off-by: Andrei Kvapil --- packages/apps/tenant/templates/etcd.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/apps/tenant/templates/etcd.yaml b/packages/apps/tenant/templates/etcd.yaml index 97b39205..9a122da2 100644 --- a/packages/apps/tenant/templates/etcd.yaml +++ b/packages/apps/tenant/templates/etcd.yaml @@ -18,7 +18,7 @@ spec: name: cozystack-etcd-application-default-etcd namespace: cozy-system interval: 5m - timeout: 10m + timeout: 30m install: remediation: retries: -1 From da597225d152975ce0dc721432464f5120fa9c8d Mon Sep 17 00:00:00 2001 From: Andrei Kvapil Date: Tue, 24 Feb 2026 23:34:53 +0100 Subject: [PATCH 5/7] fix(platform): add missing field mappings in migrate-to-version-1.0.sh MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add ConfigMap fields that were not converted to Package values: - bundle-disable → bundles.disabledPackages - bundle-enable → bundles.enabledPackages - expose-ingress → publishing.ingressName - expose-services → publishing.exposedServices Remove incorrect bundles.system.type field that is not part of the Package values schema. Co-Authored-By: Claude Signed-off-by: Andrei Kvapil --- hack/migrate-to-version-1.0.sh | 32 +++++++++++++++++++++++++++----- 1 file changed, 27 insertions(+), 5 deletions(-) diff --git a/hack/migrate-to-version-1.0.sh b/hack/migrate-to-version-1.0.sh index 5a7bc381..516816d1 100755 --- a/hack/migrate-to-version-1.0.sh +++ b/hack/migrate-to-version-1.0.sh @@ -52,6 +52,10 @@ OIDC_ENABLED=$(echo "$COZYSTACK_CM" | jq -r '.data["oidc-enabled"] // "false"') KEYCLOAK_REDIRECTS=$(echo "$COZYSTACK_CM" | jq -r '.data["extra-keycloak-redirect-uri-for-dashboard"] // ""' ) TELEMETRY_ENABLED=$(echo "$COZYSTACK_CM" | jq -r '.data["telemetry-enabled"] // "true"') BUNDLE_NAME=$(echo "$COZYSTACK_CM" | jq -r '.data["bundle-name"] // "paas-full"') +BUNDLE_DISABLE=$(echo "$COZYSTACK_CM" | jq -r '.data["bundle-disable"] // ""') +BUNDLE_ENABLE=$(echo "$COZYSTACK_CM" | jq -r '.data["bundle-enable"] // ""') +EXPOSE_INGRESS=$(echo "$COZYSTACK_CM" | jq -r '.data["expose-ingress"] // "tenant-root"') +EXPOSE_SERVICES=$(echo "$COZYSTACK_CM" | jq -r '.data["expose-services"] // ""') # Certificate issuer configuration (old undocumented field: clusterissuer) OLD_CLUSTER_ISSUER=$(echo "$COZYSTACK_CM" | jq -r '.data["clusterissuer"] // ""') @@ -99,19 +103,35 @@ else EXTERNAL_IPS=$(echo "$EXTERNAL_IPS" | sed 's/,/\n/g' | awk 'BEGIN{print}{print " - "$0}') fi +# Convert comma-separated lists to YAML arrays +if [ -z "$BUNDLE_DISABLE" ]; then + DISABLED_PACKAGES="[]" +else + DISABLED_PACKAGES=$(echo "$BUNDLE_DISABLE" | sed 's/,/\n/g' | awk 'BEGIN{print}{print " - "$0}') +fi + +if [ -z "$BUNDLE_ENABLE" ]; then + ENABLED_PACKAGES="[]" +else + ENABLED_PACKAGES=$(echo "$BUNDLE_ENABLE" | sed 's/,/\n/g' | awk 'BEGIN{print}{print " - "$0}') +fi + +if [ -z "$EXPOSE_SERVICES" ]; then + EXPOSED_SERVICES_YAML="[]" +else + EXPOSED_SERVICES_YAML=$(echo "$EXPOSE_SERVICES" | sed 's/,/\n/g' | awk 'BEGIN{print}{print " - "$0}') +fi + # Determine bundle type case "$BUNDLE_NAME" in paas-full|distro-full) SYSTEM_ENABLED="true" - SYSTEM_TYPE="full" ;; paas-hosted|distro-hosted) SYSTEM_ENABLED="false" - SYSTEM_TYPE="hosted" ;; *) SYSTEM_ENABLED="false" - SYSTEM_TYPE="hosted" ;; esac @@ -142,7 +162,6 @@ echo " API Server Endpoint: $API_SERVER_ENDPOINT" echo " OIDC Enabled: $OIDC_ENABLED" echo " Bundle Name: $BUNDLE_NAME" echo " System Enabled: $SYSTEM_ENABLED" -echo " System Type: $SYSTEM_TYPE" echo " Certificate Solver: ${SOLVER:-http01 (default)}" echo " Issuer Name: ${ISSUER_NAME:-letsencrypt-prod (default)}" echo "" @@ -162,13 +181,14 @@ spec: bundles: system: enabled: $SYSTEM_ENABLED - type: "$SYSTEM_TYPE" iaas: enabled: true paas: enabled: true naas: enabled: true + disabledPackages: $DISABLED_PACKAGES + enabledPackages: $ENABLED_PACKAGES networking: clusterDomain: "$CLUSTER_DOMAIN" podCIDR: "$POD_CIDR" @@ -177,6 +197,8 @@ spec: joinCIDR: "$JOIN_CIDR" publishing: host: "$ROOT_HOST" + ingressName: "$EXPOSE_INGRESS" + exposedServices: $EXPOSED_SERVICES_YAML apiServerEndpoint: "$API_SERVER_ENDPOINT" externalIPs: $EXTERNAL_IPS ${CERTIFICATES_SECTION} From 948346ef6d655af2e58c63f4172be7646c126e06 Mon Sep 17 00:00:00 2001 From: Andrei Kvapil Date: Tue, 24 Feb 2026 23:49:43 +0100 Subject: [PATCH 6/7] fix(platform): use original cozystack.io/ui label in migration 26 and simplify migration script Migration 26 was using apps.cozystack.io/application.kind=Monitoring label which is added by migration 22 and may not be present on v0.41.1 clusters. Switch to cozystack.io/ui=true (guaranteed on old HRs) with field-selector for exact name match. Also remove redundant bundle enabled flags from migrate-to-version-1.0.sh since the variant already determines them via its values file. Co-Authored-By: Claude Signed-off-by: Andrei Kvapil --- hack/migrate-to-version-1.0.sh | 22 ------------------- .../platform/images/migrations/migrations/26 | 2 +- 2 files changed, 1 insertion(+), 23 deletions(-) diff --git a/hack/migrate-to-version-1.0.sh b/hack/migrate-to-version-1.0.sh index 516816d1..7cc0cf73 100755 --- a/hack/migrate-to-version-1.0.sh +++ b/hack/migrate-to-version-1.0.sh @@ -122,19 +122,6 @@ else EXPOSED_SERVICES_YAML=$(echo "$EXPOSE_SERVICES" | sed 's/,/\n/g' | awk 'BEGIN{print}{print " - "$0}') fi -# Determine bundle type -case "$BUNDLE_NAME" in - paas-full|distro-full) - SYSTEM_ENABLED="true" - ;; - paas-hosted|distro-hosted) - SYSTEM_ENABLED="false" - ;; - *) - SYSTEM_ENABLED="false" - ;; -esac - # Update bundle naming BUNDLE_NAME=$(echo "$BUNDLE_NAME" | sed 's/paas/isp/') @@ -161,7 +148,6 @@ echo " Root Host: $ROOT_HOST" echo " API Server Endpoint: $API_SERVER_ENDPOINT" echo " OIDC Enabled: $OIDC_ENABLED" echo " Bundle Name: $BUNDLE_NAME" -echo " System Enabled: $SYSTEM_ENABLED" echo " Certificate Solver: ${SOLVER:-http01 (default)}" echo " Issuer Name: ${ISSUER_NAME:-letsencrypt-prod (default)}" echo "" @@ -179,14 +165,6 @@ spec: platform: values: bundles: - system: - enabled: $SYSTEM_ENABLED - iaas: - enabled: true - paas: - enabled: true - naas: - enabled: true disabledPackages: $DISABLED_PACKAGES enabledPackages: $ENABLED_PACKAGES networking: diff --git a/packages/core/platform/images/migrations/migrations/26 b/packages/core/platform/images/migrations/migrations/26 index c60af4d4..96fb9851 100755 --- a/packages/core/platform/images/migrations/migrations/26 +++ b/packages/core/platform/images/migrations/migrations/26 @@ -67,7 +67,7 @@ delete_helm_secrets() { # Find all tenant namespaces with monitoring HelmRelease echo "Finding tenant namespaces with monitoring HelmRelease..." -NAMESPACES=$(kubectl get hr --all-namespaces -l apps.cozystack.io/application.kind=Monitoring \ +NAMESPACES=$(kubectl get hr --all-namespaces -l cozystack.io/ui=true --field-selector=metadata.name=monitoring \ -o jsonpath='{range .items[*]}{.metadata.namespace}{"\n"}{end}' | sort -u) if [ -z "$NAMESPACES" ]; then From cfb5914cdd8ade6867b5f765463751ba9b4d9bc3 Mon Sep 17 00:00:00 2001 From: Andrei Kvapil Date: Wed, 25 Feb 2026 00:30:54 +0100 Subject: [PATCH 7/7] fix(platform): remove protection-webhook handling from migration 29 The protection-webhook is not part of the cozystack platform and should not be managed by the migration script. Old services are now deleted directly instead of being batched through the webhook disable/enable cycle. Co-Authored-By: Claude Signed-off-by: Andrei Kvapil --- .../platform/images/migrations/migrations/29 | 81 +++---------------- 1 file changed, 12 insertions(+), 69 deletions(-) diff --git a/packages/core/platform/images/migrations/migrations/29 b/packages/core/platform/images/migrations/migrations/29 index 1ec33cda..05ba7de0 100755 --- a/packages/core/platform/images/migrations/migrations/29 +++ b/packages/core/platform/images/migrations/migrations/29 @@ -9,8 +9,6 @@ set -euo pipefail OLD_PREFIX="virtual-machine" NEW_DISK_PREFIX="vm-disk" NEW_INSTANCE_PREFIX="vm-instance" -PROTECTION_WEBHOOK_NAME="protection-webhook" -PROTECTION_WEBHOOK_NS="protection-webhook" CDI_APISERVER_NS="cozy-kubevirt-cdi" CDI_APISERVER_DEPLOY="cdi-apiserver" CDI_VALIDATING_WEBHOOKS="cdi-api-datavolume-validate cdi-api-dataimportcron-validate cdi-api-populator-validate cdi-api-validate" @@ -88,7 +86,6 @@ echo " Total: ${#INSTANCES[@]} instance(s)" # STEP 2: Migrate each instance # ============================================================ ALL_PV_NAMES=() -ALL_PROTECTED_RESOURCES=() for entry in "${INSTANCES[@]}"; do NAMESPACE="${entry%%/*}" @@ -564,71 +561,17 @@ SVCEOF delete_resource "$NAMESPACE" "secret" "$VALUES_SECRET" fi - # Collect protected resources for batch deletion + # Delete old service (if exists) if resource_exists "$NAMESPACE" "svc" "$OLD_NAME"; then - ALL_PROTECTED_RESOURCES+=("${NAMESPACE}:svc/${OLD_NAME}") + delete_resource "$NAMESPACE" "svc" "$OLD_NAME" fi done # ============================================================ -# STEP 3: Delete protected resources (Services) +# STEP 3: Restore PV reclaim policies # ============================================================ echo "" -echo "--- Step 3: Delete protected resources ---" - -if [ ${#ALL_PROTECTED_RESOURCES[@]} -gt 0 ]; then - WEBHOOK_EXISTS=false - if kubectl -n "$PROTECTION_WEBHOOK_NS" get deploy "$PROTECTION_WEBHOOK_NAME" --no-headers 2>/dev/null | grep -q .; then - WEBHOOK_EXISTS=true - fi - - if [ "$WEBHOOK_EXISTS" = "true" ]; then - echo " --- Temporarily disabling protection-webhook ---" - - WEBHOOK_REPLICAS=$(kubectl -n "$PROTECTION_WEBHOOK_NS" get deploy "$PROTECTION_WEBHOOK_NAME" \ - -o jsonpath='{.spec.replicas}' 2>/dev/null || echo "1") - - echo " [SCALE] ${PROTECTION_WEBHOOK_NAME} -> 0 (was ${WEBHOOK_REPLICAS})" - kubectl -n "$PROTECTION_WEBHOOK_NS" scale deploy "$PROTECTION_WEBHOOK_NAME" --replicas=0 - - echo " [PATCH] Set failurePolicy=Ignore on ValidatingWebhookConfiguration/${PROTECTION_WEBHOOK_NAME}" - kubectl get validatingwebhookconfiguration "$PROTECTION_WEBHOOK_NAME" -o json | \ - jq '.webhooks[].failurePolicy = "Ignore"' | \ - kubectl apply -f - 2>/dev/null || true - - echo " Waiting for webhook pods to terminate..." - kubectl -n "$PROTECTION_WEBHOOK_NS" wait --for=delete pod \ - -l app.kubernetes.io/name=protection-webhook --timeout=60s 2>/dev/null || true - sleep 3 - fi - - for entry in "${ALL_PROTECTED_RESOURCES[@]}"; do - ns="${entry%%:*}" - res="${entry#*:}" - echo " [DELETE] ${ns}/${res}" - kubectl -n "$ns" delete "$res" --wait=false 2>/dev/null || true - done - - if [ "$WEBHOOK_EXISTS" = "true" ]; then - echo " [PATCH] Set failurePolicy=Fail on ValidatingWebhookConfiguration/${PROTECTION_WEBHOOK_NAME}" - kubectl get validatingwebhookconfiguration "$PROTECTION_WEBHOOK_NAME" -o json | \ - jq '.webhooks[].failurePolicy = "Fail"' | \ - kubectl apply -f - 2>/dev/null || true - - echo " [SCALE] ${PROTECTION_WEBHOOK_NAME} -> ${WEBHOOK_REPLICAS}" - kubectl -n "$PROTECTION_WEBHOOK_NS" scale deploy "$PROTECTION_WEBHOOK_NAME" \ - --replicas="$WEBHOOK_REPLICAS" - echo " --- protection-webhook restored ---" - fi -else - echo " [SKIP] No protected resources to delete" -fi - -# ============================================================ -# STEP 4: Restore PV reclaim policies -# ============================================================ -echo "" -echo "--- Step 4: Restore PV reclaim policies ---" +echo "--- Step 3: Restore PV reclaim policies ---" for pv_name in "${ALL_PV_NAMES[@]}"; do if [ -n "$pv_name" ]; then current_policy=$(kubectl get pv "$pv_name" \ @@ -643,7 +586,7 @@ for pv_name in "${ALL_PV_NAMES[@]}"; do done # ============================================================ -# STEP 5: Temporarily disable CDI datavolume webhooks +# STEP 4: Temporarily disable CDI datavolume webhooks # ============================================================ # CDI's datavolume-validate webhook rejects DataVolume creation when a PVC # with the same name already exists. We must disable it so that vm-disk @@ -652,7 +595,7 @@ done # cdi-apiserver (which serves the webhooks), then delete webhook configs. # Both are restored after vm-disk HRs reconcile. echo "" -echo "--- Step 5: Temporarily disable CDI webhooks ---" +echo "--- Step 4: Temporarily disable CDI webhooks ---" CDI_OPERATOR_REPLICAS=$(kubectl -n "$CDI_APISERVER_NS" get deploy cdi-operator \ -o jsonpath='{.spec.replicas}' 2>/dev/null || echo "1") @@ -685,10 +628,10 @@ done sleep 2 # ============================================================ -# STEP 6: Unsuspend vm-disk HelmReleases first +# STEP 5: Unsuspend vm-disk HelmReleases first # ============================================================ echo "" -echo "--- Step 6: Unsuspend vm-disk HelmReleases ---" +echo "--- Step 5: Unsuspend vm-disk HelmReleases ---" for entry in "${INSTANCES[@]}"; do ns="${entry%%/*}" instance="${entry#*/}" @@ -729,12 +672,12 @@ for entry in "${INSTANCES[@]}"; do done # ============================================================ -# STEP 7: Restore CDI webhooks +# STEP 6: Restore CDI webhooks # ============================================================ # Scale cdi-operator and cdi-apiserver back up. # cdi-apiserver will recreate webhook configurations automatically on start. echo "" -echo "--- Step 7: Restore CDI webhooks ---" +echo "--- Step 6: Restore CDI webhooks ---" echo " [SCALE] cdi-operator -> ${CDI_OPERATOR_REPLICAS}" kubectl -n "$CDI_APISERVER_NS" scale deploy cdi-operator \ @@ -749,10 +692,10 @@ kubectl -n "$CDI_APISERVER_NS" rollout status deploy "$CDI_APISERVER_DEPLOY" --t echo " --- CDI webhooks restored ---" # ============================================================ -# STEP 8: Unsuspend vm-instance HelmReleases +# STEP 7: Unsuspend vm-instance HelmReleases # ============================================================ echo "" -echo "--- Step 8: Unsuspend vm-instance HelmReleases ---" +echo "--- Step 7: Unsuspend vm-instance HelmReleases ---" for entry in "${INSTANCES[@]}"; do ns="${entry%%/*}" instance="${entry#*/}"