From c2f1bb2eac7845cfabd7d6ecfe400400b1498d46 Mon Sep 17 00:00:00 2001 From: Myasnikov Daniil Date: Tue, 26 May 2026 00:22:09 +0500 Subject: [PATCH] feat(platform): make unprotect strips no-delete label for teardown MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Address review feedback from @lexfrei on packages/core/platform/templates/ deletion-protection.yaml: with this PR every protected object — CRDs, ClusterIssuers, cozy-system and tenant-root Namespaces, cozystack-version ConfigMap, cozystack-packages Repository, tenant-root HelmRelease, LinstorCluster, plus the VAP and Binding themselves — carries the no-delete label. `helm uninstall cozy-platform` issues DELETE on each, and DELETE is denied for every one of them, so the chart effectively cannot be uninstalled without manual `kubectl label` against each object. Disaster recovery and dev-cluster reset both regress as a result. Add packages/core/platform/hack/unprotect.sh: enumerates the guarded set locally (per-kind, not via a single all-resources sweep — some kinds may not exist on every install, e.g. linstorcluster on a non-LINSTOR cluster, and kubectl errors on unknown kinds). For cluster-scoped kinds, lists objects with the label and labels them off one at a time. For namespaced kinds, lists (namespace, name) pairs via jsonpath and labels each in the right namespace. Idempotent: a label that is already absent is a no-op. Wire `make unprotect` in packages/core/platform/Makefile so the operator runs it before `helm uninstall`. The Makefile target carries a comment noting this is teardown / disaster-recovery only — after it runs, the cluster has no guardrail until the next `helm upgrade` re-stamps the label. Verified with a stub kubectl in PATH that the script issues the right get/label calls for both cluster-scoped (e.g. `kubectl label customresourcedefinition/packages.cozystack.io platform.cozystack.io/ no-delete-`) and namespaced kinds (e.g. `kubectl label --namespace tenant-root helmrelease tenant-root platform.cozystack.io/no-delete-`). The template-header reference at templates/deletion-protection.yaml that forward-links to `make unprotect` was added in the preceding L3 commit; the recipe it points at now exists. Assisted-By: Claude Signed-off-by: Myasnikov Daniil --- packages/core/platform/Makefile | 8 +++ packages/core/platform/hack/unprotect.sh | 70 ++++++++++++++++++++++++ 2 files changed, 78 insertions(+) create mode 100755 packages/core/platform/hack/unprotect.sh diff --git a/packages/core/platform/Makefile b/packages/core/platform/Makefile index 662c03e5f..a8e30dce8 100644 --- a/packages/core/platform/Makefile +++ b/packages/core/platform/Makefile @@ -20,6 +20,14 @@ diff: test: helm unittest . +# Strip the platform.cozystack.io/no-delete label from every guarded object so +# `helm uninstall cozy-platform` (and ad-hoc deletes of protected resources) +# is no longer denied by the deletion-protection VAP. After this runs, the +# cluster has no guardrail until the next `helm upgrade` re-stamps the label. +# Intended for teardown / disaster-recovery, not routine use. +unprotect: + hack/unprotect.sh + image: image-migrations image-migrations: diff --git a/packages/core/platform/hack/unprotect.sh b/packages/core/platform/hack/unprotect.sh new file mode 100755 index 000000000..5b9f9ef39 --- /dev/null +++ b/packages/core/platform/hack/unprotect.sh @@ -0,0 +1,70 @@ +#!/bin/sh +# Strip the platform.cozystack.io/no-delete=true label from every object that +# carries it, so `helm uninstall cozy-platform` (and ad-hoc `kubectl delete` +# on a protected resource) is no longer denied by the deletion-protection +# ValidatingAdmissionPolicy. +# +# Intended for chart teardown and disaster-recovery — NOT a routine operation. +# After this runs, the cluster has no guardrail until the next `helm upgrade` +# re-stamps the label. +# +# Idempotent: a label that is already absent is a no-op. +# +# The set of guarded kinds is enumerated locally rather than discovered +# dynamically because some of the kinds may not exist in every cluster +# (e.g. linstorcluster on a non-LINSTOR install) and kubectl returns an +# error for unknown resource types. The per-kind get is best-effort. + +set -eu + +LABEL="platform.cozystack.io/no-delete" + +# Cluster-scoped guarded kinds. CRDs and Namespaces sit here, plus the +# VAP/Binding pair (which is also self-labeled) and LinstorCluster. +CLUSTER_KINDS=" +customresourcedefinition +clusterissuer +namespace +validatingadmissionpolicy +validatingadmissionpolicybinding +linstorcluster +" + +# Namespaced guarded kinds. ConfigMap, Repository/OCIRepository, HelmRelease. +# Use --all-namespaces with -o jsonpath so we get back (ns, name) pairs and +# can pass each to a per-object `kubectl label`. +NS_KINDS=" +configmap +ocirepository +helmrelease +" + +echo "Removing $LABEL label from all guarded objects..." + +for kind in $CLUSTER_KINDS; do + names=$(kubectl get "$kind" --selector="$LABEL=true" --output=name 2>/dev/null || true) + if [ -n "$names" ]; then + # One kubectl per resource — `kubectl label` takes the label op as the + # last argument, so xargs's trailing-args model would put it in the + # wrong position. The protected set is small (<20 objects) so the + # extra round-trips are negligible. + printf '%s\n' "$names" | while read -r ref; do + [ -z "$ref" ] && continue + kubectl label "$ref" "$LABEL-" + done + fi +done + +for kind in $NS_KINDS; do + pairs=$(kubectl get "$kind" --all-namespaces --selector="$LABEL=true" \ + --output=jsonpath='{range .items[*]}{.metadata.namespace}{" "}{.metadata.name}{"\n"}{end}' \ + 2>/dev/null || true) + if [ -n "$pairs" ]; then + printf '%s\n' "$pairs" | while read -r ns name; do + [ -z "$name" ] && continue + kubectl label --namespace "$ns" "$kind" "$name" "$LABEL-" + done + fi +done + +echo "Done. helm uninstall is now unblocked."