feat(platform): make unprotect strips no-delete label for teardown

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 <noreply@anthropic.com>
Signed-off-by: Myasnikov Daniil <myasnikovdaniil2001@gmail.com>
This commit is contained in:
Myasnikov Daniil committed 2026-05-26 15:12:53 +05:00
1 parent 91aae36da3
commit c2f1bb2eac
2 files changed
+78

No files matched your search

+8
View File
@@ -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:
+70
View File
@@ -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."