Skip to content

Commit 6782761

Browse files
test(platform): e2e check that no-delete VAP denies + bypass works
Address review feedback from @lllamnyp on packages/core/platform/templates/ deletion-protection.yaml: the VAP contract had no integration coverage. A unit test that greps the rendered manifest is the wrong shape — the contract is "the api-server denies a labeled DELETE with the documented message" and that needs a real apiserver in the loop. Add a post-install bats test to hack/e2e-install-cozystack.bats that runs once cozystack is installed on the e2e cluster: 1. Skips on clusters that pre-date Kubernetes 1.30 (no VAP API). 2. Asserts the cozystack-version ConfigMap actually carries platform.cozystack.io/no-delete=true — precondition, so the deny assertion below would not misreport on a regressed binding. 3. Asserts `kubectl delete configmap cozystack-version -n cozy-system` exits non-zero AND its error message contains the documented "Deletion blocked: ... platform.cozystack.io/no-delete=true" substring AND the `--namespace` bypass hint. 4. Confirms the ConfigMap survives the rejected delete. 5. Removes the label, deletes (must succeed), recreates the ConfigMap with the original data.version and the label so the cluster ends the test in the same state it started. This single test catches every regression the PR is meant to prevent: capability gate inverted, binding objectSelector mistyped, validationActions flipped Deny→Warn, expression flipped false→true, label-key drift between the binding and the manifests, and the bypass docs missing --namespace. Assisted-By: Claude <noreply@anthropic.com> Signed-off-by: Myasnikov Daniil <myasnikovdaniil2001@gmail.com>
1 parent 23c8dab commit 6782761

1 file changed

Lines changed: 76 additions & 0 deletions

File tree

hack/e2e-install-cozystack.bats

Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -274,3 +274,79 @@ EOF
274274
-o jsonpath='{range .items[*].spec.limits[*]}{.default.cpu}{" "}{.default.memory}{" "}{.defaultRequest.cpu}{" "}{.defaultRequest.memory}{"\n"}{end}' \
275275
| grep -qx '250m 128Mi 25m 128Mi'
276276
}
277+
278+
@test "Deletion-protection VAP denies delete on labeled cozystack-version ConfigMap" {
279+
# Locks down the contract delivered by packages/core/platform/templates/
280+
# deletion-protection.yaml: a DELETE on any object carrying
281+
# platform.cozystack.io/no-delete=true is rejected by the
282+
# ValidatingAdmissionPolicy with the documented message, and the bypass
283+
# path (remove the label, then delete) succeeds.
284+
#
285+
# This covers every regression the PR is meant to prevent in a single
286+
# pass: capability gate inverted, binding objectSelector mistyped,
287+
# validationActions flipped Deny→Warn, expression flipped false→true,
288+
# label-key drift between the binding and the manifests.
289+
290+
# Preflight: VAP requires Kubernetes 1.30+. Skip on older clusters so
291+
# the suite stays green where the capability gate intentionally elides
292+
# the policy. Detect by attempting to fetch the policy by name; if the
293+
# API is present, the resource will be retrievable, otherwise kubectl
294+
# exits non-zero on an unknown resource type.
295+
if ! kubectl api-resources --api-group=admissionregistration.k8s.io \
296+
2>/dev/null | grep -qw validatingadmissionpolicies; then
297+
skip "ValidatingAdmissionPolicy API not available on this cluster"
298+
fi
299+
kubectl get validatingadmissionpolicy cozystack-no-delete-guardrail
300+
301+
# The cozystack-version ConfigMap is created with the no-delete label
302+
# baked in by the chart (templates/cozystack-version.yaml) and is
303+
# backfilled by the migration on upgrades. Asserting the label is the
304+
# precondition for the deny check below: if the label is gone the deny
305+
# will not fire and the test would misreport as a pass on a regressed
306+
# binding.
307+
kubectl get configmap cozystack-version -n cozy-system \
308+
-o jsonpath='{.metadata.labels.platform\.cozystack\.io/no-delete}' \
309+
| grep -qx 'true'
310+
311+
# The actual deny check. Capture both stdout+stderr and exit code so a
312+
# network/auth failure does not silently look like a deny success.
313+
local output rc
314+
output=$(kubectl delete configmap cozystack-version -n cozy-system 2>&1) \
315+
&& rc=0 || rc=$?
316+
echo "kubectl delete exit=$rc, output=$output"
317+
# Delete MUST have failed: success means the VAP regressed.
318+
[ "$rc" -ne 0 ]
319+
# Assert the user-facing deny message is the one this PR ships — guards
320+
# against expression flipped to "true" or message reworded away from the
321+
# documented bypass. The CEL message is on one line in the api-server
322+
# response, so grep for the literal substring with the --namespace flag.
323+
echo "$output" | grep -q 'Deletion blocked: object carries platform.cozystack.io/no-delete=true'
324+
echo "$output" | grep -q -- '--namespace'
325+
326+
# And confirm the ConfigMap is still there — a partial deny that races
327+
# tombstone creation would also be a regression.
328+
kubectl get configmap cozystack-version -n cozy-system >/dev/null
329+
330+
# Bypass path: remove the label, delete must succeed. Re-stamp the label
331+
# afterward so the cluster ends the test in the same state it started.
332+
kubectl label configmap cozystack-version -n cozy-system \
333+
platform.cozystack.io/no-delete- --overwrite
334+
# Stash the data so we can reconstruct after delete.
335+
local version
336+
version=$(kubectl get configmap cozystack-version -n cozy-system \
337+
-o jsonpath='{.data.version}')
338+
kubectl delete configmap cozystack-version -n cozy-system
339+
! kubectl get configmap cozystack-version -n cozy-system 2>/dev/null
340+
# Reconstruct: declarative apply matches the migration / template shape.
341+
cat <<EOF | kubectl apply -f -
342+
apiVersion: v1
343+
kind: ConfigMap
344+
metadata:
345+
name: cozystack-version
346+
namespace: cozy-system
347+
labels:
348+
platform.cozystack.io/no-delete: "true"
349+
data:
350+
version: "${version}"
351+
EOF
352+
}

0 commit comments

Comments
 (0)