mirror of
https://github.com/cozystack/cozystack.git
synced 2026-10-23 03:27:50 +09:00
cert-manager runs with enableCertificateOwnerRef: false, so the <release>-talos-ca and <release>-talos-tls-cert Secrets carry no ownerReference to their Certificates and outlive the release, keeping the CA private key of a cluster that no longer exists. A new cluster created under the same name then adopts that stale CA instead of issuing its own trust root. The <release>-oidc-bootstrap Job is a hook whose only delete policy is before-hook-creation. Hooks are not part of the release manifest, so uninstall never removes it. The pre-delete hook deletes the two Certificates before their Secrets, because deleting a Secret while its Certificate exists makes cert-manager re-issue it. It runs after the TenantControlPlane is gone, since the control plane's CSR signer mounts the CA. The Role grants each delete by resourceName only. Assisted-by: LLM Signed-off-by: Ray Low <crossrt@gmail.com>
430 lines
16 KiB
Bash
430 lines
16 KiB
Bash
#!/usr/bin/env bats
|
|
# Behavioural tests for the pre-delete cleanup hook of packages/apps/kubernetes:
|
|
# the script is extracted from the rendered Job and executed against a stub
|
|
# kubectl. The helm-unittest cases in
|
|
# packages/apps/kubernetes/tests/delete_hook_test.yaml match its source TEXT,
|
|
# which cannot say whether the closing message is reachable once a backstop has
|
|
# swallowed a failure.
|
|
#
|
|
# Run from the repo root through `make bats-unit-tests`; relative paths resolve
|
|
# against that cwd. The file also remains compatible with the legacy
|
|
# `hack/cozytest.sh` translator, so each @test builds and removes its fixture
|
|
# inline instead of relying on setup()/teardown(). No EXIT
|
|
# traps: a test that dies inside one prints neither `not ok` nor a reason, and
|
|
# the suite reads green. The removal is therefore reached only on a passing run;
|
|
# a failing one leaves its temp directory behind, which is what you want when
|
|
# reading the rendered script and the captured output afterwards.
|
|
|
|
load test_helper
|
|
|
|
CHART=packages/apps/kubernetes
|
|
|
|
# The stub is driven by two independent lists of globs, matched against the
|
|
# joined arguments, and keeping them separate is the point: the script under
|
|
# test distinguishes "this object is not there" from "I could not ask", so a
|
|
# fixture that spelled both as one failing call could not tell whether an
|
|
# assertion passed for the right reason.
|
|
#
|
|
# $KUBECTL_FAIL -- these invocations exit non-zero: the apiserver refused,
|
|
# timed out, or was unreachable.
|
|
# $KUBECTL_PRESENT -- these `get ... -o name` invocations print an object.
|
|
# Anything not listed exits 0 with no output, which is
|
|
# what `--ignore-not-found` does for an absent object.
|
|
|
|
# The ordinary end state after Step 4 waited: the TenantControlPlane is gone and
|
|
# the datastore Secret is still there for Step 4b to clear.
|
|
PRESENT_SECRET_ONLY='*get secret*'
|
|
|
|
# Step 4's delete timed out with the TenantControlPlane still alive, so Step 4b
|
|
# must decline to touch a finalizer Kamaji may still own.
|
|
FAIL_TCP_DELETE='*delete tenantcontrolplanes*'
|
|
PRESENT_TCP_AND_SECRET='*get tenantcontrolplanes*
|
|
*get secret*'
|
|
|
|
# The Secret is there, and the patch that would clear its finalizer is rejected.
|
|
FAIL_SECRET_PATCH='*patch secret*'
|
|
|
|
# Step 2's bounded delete times out, so the run takes the force-clear route --
|
|
# and every force-clear then succeeds. A changed route is not residue: nothing
|
|
# survives this run, so it must still be reported as a clean one.
|
|
FAIL_HR_DELETE='*delete helmreleases*'
|
|
|
|
# ... and the re-list driving that force-clear fails too. The loop body never
|
|
# runs, so no patch is attempted and no per-object backstop can fire.
|
|
FAIL_HR_DELETE_AND_LIST='*delete helmreleases*
|
|
*get helmreleases*'
|
|
|
|
# Each of Step 4b's two probes, failing on its own. Neither says the object is
|
|
# absent -- they say the script could not find out, which is a third state.
|
|
FAIL_TCP_PROBE='*get tenantcontrolplanes*'
|
|
FAIL_SECRET_PROBE='*get secret*'
|
|
|
|
# Every failure at once. The two HelmRelease globs are distinguished by the
|
|
# patch body: Step 1 suspends (no "finalizers" in its payload) and must keep
|
|
# working, because it is not backstopped and `set -e` would end the run there.
|
|
FAIL_ALL='*delete helmreleases*
|
|
*patch helmrelease*finalizers*
|
|
*delete tenantcontrolplanes*
|
|
*get tenantcontrolplanes*
|
|
*patch secret*'
|
|
|
|
# Extract the pre-delete Job's script from the rendered chart into $1. The
|
|
# non-empty check must be an early `return 1`: hack/cozytest.sh rewrites every
|
|
# line matching ^}$ into `return 0` plus `}`, helpers included, so a check left
|
|
# in last position has its status discarded.
|
|
#
|
|
# helm's stderr is kept rather than discarded, because it carries the reason a
|
|
# render failed and without it this returns 1 with nothing to read. Only the
|
|
# per-render notice about the charts/cozy-lib symlink is dropped, and only when
|
|
# there is a failure to report.
|
|
render_script() {
|
|
helm template test-k8s "$CHART" \
|
|
--namespace tenant-test \
|
|
--values "$CHART/tests/values-ci.yaml" \
|
|
--show-only templates/delete.yaml 2>"$1.err" |
|
|
yq 'select(.kind == "Job") | .spec.template.spec.containers[0].command[2]' - > "$1"
|
|
if [ ! -s "$1" ]; then
|
|
echo "FAIL: the chart rendered no pre-delete Job script" >&2
|
|
grep -v 'found symbolic link in path' "$1.err" >&2 || true
|
|
return 1
|
|
fi
|
|
}
|
|
|
|
# A stub kubectl for $1/kubectl. `get ... -o name` answers for the three object
|
|
# kinds the script probes; the HelmRelease list returns TWO children so that a
|
|
# per-object failure in the force-clear loop hits the same backstop label twice
|
|
# and the once-per-step property has something to be true about.
|
|
#
|
|
# The closing brace of the helper inside the heredoc is indented on purpose:
|
|
# hack/cozytest.sh rewrites every line matching ^}$ anywhere in this file, the
|
|
# heredoc included, and a `return 0` spliced into the stub would make every
|
|
# injected failure disappear.
|
|
make_kubectl() {
|
|
mkdir -p "$1"
|
|
cat > "$1/kubectl" <<'STUB'
|
|
#!/bin/sh
|
|
args="$*"
|
|
echo "$args" >> "${KUBECTL_LOG:-/dev/null}"
|
|
|
|
matches() {
|
|
[ -n "$1" ] || return 1
|
|
oldifs=$IFS
|
|
IFS='
|
|
'
|
|
for pat in $1; do
|
|
case "$args" in
|
|
$pat) IFS=$oldifs; return 0 ;;
|
|
esac
|
|
done
|
|
IFS=$oldifs
|
|
return 1
|
|
}
|
|
|
|
if matches "${KUBECTL_FAIL:-}"; then
|
|
echo "stub kubectl: injected failure: $args" >&2
|
|
exit 1
|
|
fi
|
|
|
|
case "$args" in
|
|
*"get helmreleases"*"-o name"*)
|
|
echo "helmrelease.helm.toolkit.fluxcd.io/cilium"
|
|
echo "helmrelease.helm.toolkit.fluxcd.io/coredns"
|
|
;;
|
|
*"get tenantcontrolplanes"*"-o name"*)
|
|
matches "${KUBECTL_PRESENT:-}" && echo "tenantcontrolplane.kamaji.clastix.io/test-k8s"
|
|
;;
|
|
*"get secret"*"-o name"*)
|
|
matches "${KUBECTL_PRESENT:-}" && echo "secret/test-k8s-datastore-config"
|
|
;;
|
|
esac
|
|
exit 0
|
|
STUB
|
|
chmod 0755 "$1/kubectl"
|
|
}
|
|
|
|
# Fixture: $tmp holds the rendered script and the stub kubectl.
|
|
setup_case() {
|
|
tmp=$(mktemp -d) || return 1
|
|
render_script "$tmp/s.sh" || return 1
|
|
make_kubectl "$tmp/bin" || return 1
|
|
}
|
|
|
|
# Run the hook with the injected failures in $1 and the existing objects in $2,
|
|
# leaving merged output in $tmp/out and the exit status in $rc. The status must
|
|
# not abort the test: it is itself one of the things under test. KUBECONFIG is
|
|
# neutered so that a stub that stopped being found on PATH cannot reach a real
|
|
# cluster.
|
|
run_hook() {
|
|
rc=0
|
|
# Truncated per call, not appended to: the "stub was reached" check below is
|
|
# vacuous on a second run against a log the first run already filled.
|
|
: > "$tmp/kubectl.log"
|
|
KUBECONFIG=/dev/null \
|
|
KUBECTL_LOG="$tmp/kubectl.log" \
|
|
KUBECTL_FAIL="$1" \
|
|
KUBECTL_PRESENT="$2" \
|
|
PATH="$tmp/bin:$PATH" sh "$tmp/s.sh" > "$tmp/out" 2>&1 || rc=$?
|
|
# The stub has to have been reached, or the assertions below measured
|
|
# something other than this script driving kubectl.
|
|
[ -s "$tmp/kubectl.log" ] || return 1
|
|
}
|
|
|
|
@test "claims success only when nothing was left behind" {
|
|
setup_case
|
|
run_hook "" "$PRESENT_SECRET_ONLY"
|
|
|
|
[ "$rc" = 0 ]
|
|
grep -qF 'Cleanup completed successfully' "$tmp/out"
|
|
# The clean run must exercise the Step 4b patch rather than skip it, or the
|
|
# success claim is about a script that did almost nothing.
|
|
grep -qF 'Removing finalizers from secret/test-k8s-datastore-config' "$tmp/out"
|
|
if grep -qF 'residue left by' "$tmp/out"; then
|
|
echo "FAIL: a clean run reported residue"
|
|
cat "$tmp/out"
|
|
return 1
|
|
fi
|
|
if grep -qF 'WARNING' "$tmp/out"; then
|
|
echo "FAIL: a clean run emitted a warning"
|
|
cat "$tmp/out"
|
|
return 1
|
|
fi
|
|
|
|
rm -rf "$tmp"
|
|
}
|
|
|
|
@test "a rejected finalizer patch withdraws the success claim and names the step" {
|
|
setup_case
|
|
run_hook "$FAIL_SECRET_PATCH" "$PRESENT_SECRET_ONLY"
|
|
|
|
[ "$rc" = 0 ]
|
|
# The defect this pins: the closing line used to assert success even here.
|
|
if grep -qF 'Cleanup completed successfully' "$tmp/out"; then
|
|
echo "FAIL: cleanup claimed success while the datastore-secret finalizer was left in place"
|
|
cat "$tmp/out"
|
|
return 1
|
|
fi
|
|
grep -qF 'Cleanup incomplete; residue left by: step 4b (datastore-secret finalizer)' "$tmp/out"
|
|
# And only that step: a summary that names every backstop unconditionally
|
|
# tells an operator no more than the old unconditional success line did.
|
|
if grep -qF 'step 2 (' "$tmp/out"; then
|
|
echo "FAIL: a step 2 backstop was reported without firing"
|
|
cat "$tmp/out"
|
|
return 1
|
|
fi
|
|
|
|
rm -rf "$tmp"
|
|
}
|
|
|
|
@test "a fallback route that succeeded is reported as a clean run" {
|
|
# Step 2's bounded delete times out and every force-clear that follows works,
|
|
# which is the ordinary outcome for a tenant with no working nodes -- the case
|
|
# the wait is bounded for in the first place. Nothing survives the run, so
|
|
# calling it incomplete would send an operator hunting for objects that are
|
|
# not there: the same class of untrue closing line this file exists to stop,
|
|
# only pointing the other way.
|
|
setup_case
|
|
run_hook "$FAIL_HR_DELETE" "$PRESENT_SECRET_ONLY"
|
|
|
|
[ "$rc" = 0 ]
|
|
# The route really was taken, or this passes for a run that never fell back.
|
|
grep -qF 'Force-clearing finalizers on helmrelease.helm.toolkit.fluxcd.io/cilium' "$tmp/out"
|
|
if grep -qF 'residue left by' "$tmp/out"; then
|
|
echo "FAIL: a fallback that left nothing behind was reported as incomplete"
|
|
cat "$tmp/out"
|
|
return 1
|
|
fi
|
|
grep -qF 'Cleanup completed successfully' "$tmp/out"
|
|
|
|
rm -rf "$tmp"
|
|
}
|
|
|
|
@test "an enumeration that failed is reported, not taken for an empty one" {
|
|
setup_case
|
|
run_hook "$FAIL_HR_DELETE_AND_LIST" "$PRESENT_SECRET_ONLY"
|
|
|
|
[ "$rc" = 0 ]
|
|
if grep -qF 'Cleanup completed successfully' "$tmp/out"; then
|
|
echo "FAIL: cleanup claimed success after it could not list what it had to force-clear"
|
|
cat "$tmp/out"
|
|
return 1
|
|
fi
|
|
grep -qF 'Cleanup incomplete; residue left by: step 2 (child HelmRelease enumeration)' "$tmp/out"
|
|
# And not the per-object label: that one means a patch was attempted and
|
|
# rejected. Here the loop never ran, so claiming it would invent a failure.
|
|
if grep -qF 'step 2 (child HelmRelease finalizers)' "$tmp/out"; then
|
|
echo "FAIL: a per-object patch failure was reported although the loop never ran"
|
|
cat "$tmp/out"
|
|
return 1
|
|
fi
|
|
|
|
rm -rf "$tmp"
|
|
}
|
|
|
|
@test "a TenantControlPlane outliving Step 4 is reported even though no command failed" {
|
|
# Step 4 times out with the TCP still alive, so Step 4b declines to strip a
|
|
# finalizer Kamaji may still own. Every kubectl call in Step 4b succeeds, so
|
|
# nothing is caught by a `||` -- and the namespace is still wedged. This is
|
|
# the one residue the script reports from a branch rather than from a failure.
|
|
setup_case
|
|
run_hook "$FAIL_TCP_DELETE" "$PRESENT_TCP_AND_SECRET"
|
|
|
|
[ "$rc" = 0 ]
|
|
if grep -qF 'Cleanup completed successfully' "$tmp/out"; then
|
|
echo "FAIL: cleanup claimed success with the datastore-secret finalizer left in place"
|
|
cat "$tmp/out"
|
|
return 1
|
|
fi
|
|
# The label names the TenantControlPlane, not the Secret. Both branches of
|
|
# Step 4b leave the finalizer in place, but only the other one wants it
|
|
# cleared by hand; here it is Kamaji's, and a summary pointing at the Secret
|
|
# would send the reader to strip a finalizer its owner is still using.
|
|
grep -qF 'Cleanup incomplete; residue left by: step 4b (TenantControlPlane still deleting)' "$tmp/out"
|
|
if grep -qF 'step 4b (datastore-secret finalizer)' "$tmp/out"; then
|
|
echo "FAIL: the summary blamed the Secret for a TenantControlPlane that is still deleting"
|
|
cat "$tmp/out"
|
|
return 1
|
|
fi
|
|
grep -qF 'is still present, so Kamaji may still own' "$tmp/out"
|
|
|
|
rm -rf "$tmp"
|
|
}
|
|
|
|
@test "a TenantControlPlane probe that failed is not read as absent" {
|
|
# The probe is the gate that stops the finalizer being stripped from an owner
|
|
# that still exists, so reading its failure as "confirmed gone" defeats it in
|
|
# the one direction that does damage. The script must neither strip nor claim
|
|
# the run was clean.
|
|
setup_case
|
|
run_hook "$FAIL_TCP_PROBE" ""
|
|
|
|
[ "$rc" = 0 ]
|
|
if grep -qF 'Cleanup completed successfully' "$tmp/out"; then
|
|
echo "FAIL: cleanup claimed success without establishing whether the TenantControlPlane was gone"
|
|
cat "$tmp/out"
|
|
return 1
|
|
fi
|
|
grep -qF 'Cleanup incomplete; residue left by: step 4b (datastore-secret state unknown)' "$tmp/out"
|
|
# And it must NOT have gone on to patch: that is the damage the gate prevents.
|
|
if grep -qF 'Removing finalizers from secret/' "$tmp/out"; then
|
|
echo "FAIL: the finalizer was stripped although the TenantControlPlane state was unknown"
|
|
cat "$tmp/out"
|
|
return 1
|
|
fi
|
|
|
|
rm -rf "$tmp"
|
|
}
|
|
|
|
@test "a Secret probe that failed is not read as absent" {
|
|
# The TCP is confirmed gone, so Step 4b is entitled to act -- but it cannot
|
|
# find out whether the Secret is there. "not present; nothing to do" would be
|
|
# an assertion the script never checked.
|
|
setup_case
|
|
run_hook "$FAIL_SECRET_PROBE" ""
|
|
|
|
[ "$rc" = 0 ]
|
|
if grep -qF 'Cleanup completed successfully' "$tmp/out"; then
|
|
echo "FAIL: cleanup claimed success without establishing whether the Secret was still there"
|
|
cat "$tmp/out"
|
|
return 1
|
|
fi
|
|
if grep -qF 'not present; nothing to do' "$tmp/out"; then
|
|
echo "FAIL: a failed probe was reported as the Secret being absent"
|
|
cat "$tmp/out"
|
|
return 1
|
|
fi
|
|
grep -qF 'Cleanup incomplete; residue left by: step 4b (datastore-secret state unknown)' "$tmp/out"
|
|
|
|
rm -rf "$tmp"
|
|
}
|
|
|
|
@test "every step that left residue is named in the closing message" {
|
|
setup_case
|
|
run_hook "$FAIL_ALL" ""
|
|
|
|
[ "$rc" = 0 ]
|
|
if grep -qF 'Cleanup completed successfully' "$tmp/out"; then
|
|
echo "FAIL: cleanup claimed success with every remedy having failed"
|
|
cat "$tmp/out"
|
|
return 1
|
|
fi
|
|
# Read the summary as one line: asserting the labels against the whole output
|
|
# would pass on the individual WARNINGs, which were already there. Two labels,
|
|
# not five: the timeouts that changed route left nothing of their own, and
|
|
# what survived them is what the per-object failures below record.
|
|
summary=$(grep -F 'residue left by:' "$tmp/out")
|
|
for label in \
|
|
'step 2 (child HelmRelease finalizers)' \
|
|
'step 4b (datastore-secret state unknown)'; do
|
|
case "$summary" in
|
|
*"$label"*) ;;
|
|
*)
|
|
echo "FAIL: the closing message did not name: $label"
|
|
echo "$summary"
|
|
return 1
|
|
;;
|
|
esac
|
|
done
|
|
# Once each. The force-clear loop fails on both child HelmReleases, and a
|
|
# summary that listed its step twice would read as two separate failures.
|
|
dupes=$(printf '%s\n' "$summary" | tr ',' '\n' |
|
|
grep -cF 'step 2 (child HelmRelease finalizers)' || true)
|
|
if [ "$dupes" != 1 ]; then
|
|
echo "FAIL: the step 2 finalizer backstop is named $dupes times, expected once"
|
|
echo "$summary"
|
|
return 1
|
|
fi
|
|
|
|
rm -rf "$tmp"
|
|
}
|
|
|
|
@test "a step with no backstop ends the run instead of claiming success" {
|
|
# The closing message is honest only while the steps that are NOT backstopped
|
|
# still end the run under `set -e`. Nothing but `set -e` enforces that, so a
|
|
# `|| true` appended to one of them later would reinstate the exact defect
|
|
# this file pins: a cleanup that did not happen, reported as success. Step 3
|
|
# stands in for Steps 3, 5, 6, 7 and the patch inside Step 1. Step 1's
|
|
# enumeration is NOT among them: it carries `|| true` on purpose, because
|
|
# Step 2 deletes by label rather than from that list, so a failure there
|
|
# leaves nothing behind to report.
|
|
setup_case
|
|
run_hook '*delete kamajicontrolplanes*' "$PRESENT_SECRET_ONLY"
|
|
|
|
if [ "$rc" = 0 ]; then
|
|
echo "FAIL: a failed Step 3 did not end the run"
|
|
cat "$tmp/out"
|
|
return 1
|
|
fi
|
|
if grep -qF 'Cleanup completed successfully' "$tmp/out"; then
|
|
echo "FAIL: cleanup claimed success after Step 3 failed"
|
|
cat "$tmp/out"
|
|
return 1
|
|
fi
|
|
|
|
rm -rf "$tmp"
|
|
}
|
|
|
|
@test "exits 0 whether cleanup was clean or every remedy failed" {
|
|
# Pinned on its own because it is the constraint the honesty fix must not buy
|
|
# its honesty with. This is a pre-delete hook: a non-zero exit aborts the Helm
|
|
# uninstall before the release is removed, so the release stays in storage and
|
|
# the same teardown is retried forever. Reporting failure through the exit
|
|
# code is what the closing message exists to avoid.
|
|
setup_case
|
|
|
|
run_hook "" "$PRESENT_SECRET_ONLY"
|
|
if [ "$rc" != 0 ]; then
|
|
echo "FAIL: a clean run exited $rc"
|
|
cat "$tmp/out"
|
|
return 1
|
|
fi
|
|
|
|
run_hook "$FAIL_ALL" ""
|
|
if [ "$rc" != 0 ]; then
|
|
echo "FAIL: an all-failures run exited $rc, which would wedge the release"
|
|
cat "$tmp/out"
|
|
return 1
|
|
fi
|
|
|
|
rm -rf "$tmp"
|
|
}
|