Ctrl K
ComunidadGuías…

hack/app-cleanup-hooks_test.bats

Rama main · réplica de lectura del repositorio original.

Archivo original ↗Preguntar a KiBot
#!/usr/bin/env bats
# Behavioural tests for the two post-delete cleanup hooks that reclaim storage
# and ACME objects when an app release is uninstalled:
#
#   packages/apps/qdrant/templates/hooks/cleanup-pvc.yaml
#   packages/apps/harbor/templates/hooks/cleanup.yaml
#
# The scripts are extracted from the rendered charts and RUN against a fake
# kubectl, because the property under test is the container's exit code and no
# amount of matching against the manifest text establishes it. The helm-unittest
# suites in packages/apps/*/tests/ match the source text instead.
#
# What is pinned here:
#   * a delete that failed makes the hook exit non-zero. Without that, Helm
#     records the hook as succeeded and the hook-succeeded delete policy removes
#     the Job together with the pod that held the only account of the failure;
#   * one failed delete does not skip the deletes that follow it;
#   * an object that is already gone stays a success, so a re-run over an
#     already-clean namespace exits 0;
#   * a listing that FAILED is distinguishable from one that returned nothing.
#
# 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
# trap: docs/agents/e2e-testing.md bans them in hack/*.bats, and under the bats
# binary a test that installs one and then fails prints no TAP line at all.

load test_helper

QDRANT_CHART=packages/apps/qdrant
HARBOR_CHART=packages/apps/harbor

# render_hook <chart> <release> <namespace> <job-name> <out> [extra helm args…]
# Extract the named Job's shell script from the rendered chart into <out>. 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-command position has its status discarded.
render_hook() {
  chart=$1 release=$2 namespace=$3 job=$4 out=$5
  shift 5
  helm template "$release" "$chart" --namespace "$namespace" "$@" \
    | yq eval "select(.kind == \"Job\" and .metadata.name == \"$job\") | .spec.template.spec.containers[0].command[2]" - > "$out"
  [ -s "$out" ] || return 1
}

# write_fake_kubectl <dir>
# Emit a fake `kubectl` that logs every call to $KLOG and models the three
# kubectl behaviours these hooks depend on:
#   * a listing prints the fixture held in $PVC_LIST / $CHALLENGE_LIST /
#     $SOLVER_LIST and exits 0 — or, when its kind is named in $FAIL_GET, prints
#     an error and exits 1 having printed nothing, which is what makes "the
#     listing failed" and "the listing was empty" different events;
#   * a delete whose kind is named in $FAIL_DELETE exits 1;
#   * a delete of an object absent from $EXISTING is a NotFound: exit 0 under
#     --ignore-not-found, exit 1 without it. Dropping that flag from a hook
#     therefore turns the already-gone cases red rather than passing silently.
# A label-selector delete matches whatever is present and always exits 0, as
# kubectl's does. No line below is a bare column-0 `}`, so cozytest.sh's parser
# leaves the fixture intact.
write_fake_kubectl() {
  cat > "$1/kubectl" <<'KEOF'
#!/bin/sh
echo "$*" >> "$KLOG"
verb=$1
kind=${2:-}

# The by-name deletes put the object name last. Flags and the value that
# follows -n / -l are skipped, so what survives is the trailing positional.
obj=""
skip=0
for a in "$@"; do
  if [ "$skip" = 1 ]; then skip=0; continue; fi
  case "$a" in
    -n|-l) skip=1 ;;
    -*) ;;
    *) obj=$a ;;
  esac
done

if [ "$verb" = get ]; then
  case " ${FAIL_GET:-} " in
    *" $kind "*) echo "error: the server could not list $kind" >&2; exit 1 ;;
  esac
  case "$kind" in
    pvc) printf '%s' "${PVC_LIST:-}" ;;
    challenge) printf '%s' "${CHALLENGE_LIST:-}" ;;
    ingress) printf '%s' "${SOLVER_LIST:-}" ;;
    *) echo "fake kubectl: unexpected get kind $kind" >&2; exit 64 ;;
  esac
  exit 0
fi

if [ "$verb" != delete ]; then
  echo "fake kubectl: unexpected verb $verb" >&2
  exit 64
fi

# qdrant deletes through xargs as `delete -n NS --ignore-not-found <name>…`, so
# there is no kind argument on that call; it is always a PVC delete.
case "$kind" in -*) kind=pvc ;; esac

case " ${FAIL_DELETE:-} " in
  *" $kind "*) echo "Error from server: failed to delete $kind $obj" >&2; exit 1 ;;
esac
case "$*" in *" -l "*) exit 0 ;; esac
case " ${EXISTING:-} " in
  *" $obj "*) exit 0 ;;
esac
case "$*" in
  *--ignore-not-found*) exit 0 ;;
esac
echo "Error from server (NotFound): $obj not found" >&2
exit 1
KEOF
  chmod 0755 "$1/kubectl"
}

@test "a render that produces no script fails instead of passing as a no-op" {
  # Guards the guard: every test below runs whatever render_hook produced, so an
  # empty render would satisfy the exit-0 assertions without exercising a line
  # of the hook. This also fails if the emptiness check is ever moved into
  # last-command position, where cozytest.sh's `return 0` rewrite swallows it.
  tmp=$(mktemp -d)
  if render_hook "$tmp/no-such-chart" r ns r-cleanup "$tmp/hook.sh" 2>/dev/null; then
    echo "FAIL: render_hook reported success with no chart to render"
    false
  fi
  if render_hook "$QDRANT_CHART" qdrant-test tenant-test no-such-job "$tmp/hook.sh"; then
    echo "FAIL: render_hook reported success for a Job name the chart never emits"
    false
  fi
  rm -rf "$tmp"
}

@test "qdrant hook exits 0 and deletes exactly the release's data PVCs" {
  tmp=$(mktemp -d)
  write_fake_kubectl "$tmp"
  render_hook "$QDRANT_CHART" qdrant-test tenant-test qdrant-test-qdrant-cleanup "$tmp/hook.sh"

  export KLOG="$tmp/calls"
  # Two of this release's data PVCs, one belonging to the sibling release
  # "qdrant-test-2", and one unrelated claim.
  export PVC_LIST='persistentvolumeclaim/qdrant-storage-qdrant-test-0
persistentvolumeclaim/qdrant-storage-qdrant-test-1
persistentvolumeclaim/qdrant-storage-qdrant-test-2-0
persistentvolumeclaim/db-qdrant-test-0'
  export EXISTING='persistentvolumeclaim/qdrant-storage-qdrant-test-0 persistentvolumeclaim/qdrant-storage-qdrant-test-1'

  rc=0
  PATH="$tmp:$PATH" sh "$tmp/hook.sh" > "$tmp/out" 2> "$tmp/err" || rc=$?
  if [ "$rc" -ne 0 ]; then
    echo "FAIL: hook exited $rc when every delete succeeded"
    cat "$tmp/out" "$tmp/err"
    false
  fi
  if ! grep -q 'PVC cleanup complete' "$tmp/out"; then
    echo "FAIL: hook did not report completion"
    cat "$tmp/out" "$tmp/err"
    false
  fi
  if ! grep -q 'delete .*qdrant-storage-qdrant-test-0' "$tmp/calls"; then
    echo "FAIL: the release's data PVCs were never deleted"
    cat "$tmp/calls"
    false
  fi
  if grep -q 'qdrant-storage-qdrant-test-2-0' "$tmp/calls"; then
    echo "FAIL: the sibling release's PVC was included in the delete"
    cat "$tmp/calls"
    false
  fi
  rm -rf "$tmp"
}

@test "qdrant hook exits 0 when the data PVCs are already gone" {
  # The idempotent re-run: uninstalling twice, or uninstalling a release whose
  # storage was reclaimed by hand, must not be reported as a failed cleanup.
  # Covered in both shapes it takes — nothing matched the selector at all, and
  # the delete itself came back NotFound.
  tmp=$(mktemp -d)
  write_fake_kubectl "$tmp"
  render_hook "$QDRANT_CHART" qdrant-test tenant-test qdrant-test-qdrant-cleanup "$tmp/hook.sh"

  export KLOG="$tmp/calls"
  export PVC_LIST=''
  export EXISTING=''

  rc=0
  PATH="$tmp:$PATH" sh "$tmp/hook.sh" > "$tmp/out" 2> "$tmp/err" || rc=$?
  if [ "$rc" -ne 0 ]; then
    echo "FAIL: hook exited $rc with no matching PVCs in the namespace"
    cat "$tmp/out" "$tmp/err"
    false
  fi
  if ! grep -q 'nothing to delete' "$tmp/out"; then
    echo "FAIL: hook did not report an empty namespace"
    cat "$tmp/out"
    false
  fi

  # Same release, but the PVCs are listed and disappear before the delete lands.
  : > "$tmp/calls"
  export PVC_LIST='persistentvolumeclaim/qdrant-storage-qdrant-test-0'
  rc=0
  PATH="$tmp:$PATH" sh "$tmp/hook.sh" > "$tmp/out2" 2> "$tmp/err2" || rc=$?
  if [ "$rc" -ne 0 ]; then
    echo "FAIL: hook exited $rc on a NotFound delete, which --ignore-not-found makes a success"
    cat "$tmp/out2" "$tmp/err2"
    false
  fi
  if ! grep -q 'delete .*qdrant-storage-qdrant-test-0' "$tmp/calls"; then
    echo "FAIL: the delete was never attempted, so the NotFound path was not exercised"
    cat "$tmp/calls"
    false
  fi
  rm -rf "$tmp"
}

@test "qdrant hook exits non-zero and names the PVCs when the delete fails" {
  tmp=$(mktemp -d)
  write_fake_kubectl "$tmp"
  render_hook "$QDRANT_CHART" qdrant-test tenant-test qdrant-test-qdrant-cleanup "$tmp/hook.sh"

  export KLOG="$tmp/calls"
  export PVC_LIST='persistentvolumeclaim/qdrant-storage-qdrant-test-0'
  export EXISTING="$PVC_LIST"
  export FAIL_DELETE='pvc'

  rc=0
  PATH="$tmp:$PATH" sh "$tmp/hook.sh" > "$tmp/out" 2> "$tmp/err" || rc=$?
  if [ "$rc" -eq 0 ]; then
    echo "FAIL: hook exited 0 although the PVC delete failed"
    cat "$tmp/out" "$tmp/err"
    false
  fi
  # The message must name what was left behind, not merely that something was.
  # Anchored on the hook's own `ERROR: ` prefix: the fake kubectl names the
  # object in its own diagnostic too, so an unanchored match is satisfied by
  # the fake and stays green even when the hook stops naming anything.
  if ! grep -q '^ERROR: .*qdrant-storage-qdrant-test-0' "$tmp/err"; then
    echo "FAIL: the failure was not attributed to a named PVC"
    cat "$tmp/err"
    false
  fi
  if grep -q 'PVC cleanup complete' "$tmp/out"; then
    echo "FAIL: hook reported completion after a failed delete"
    cat "$tmp/out"
    false
  fi
  rm -rf "$tmp"
}

@test "qdrant hook exits non-zero when the PVC listing fails instead of reporting nothing to delete" {
  # A failed listing used to reach the filter as an empty string, so the hook
  # announced an empty namespace and exited 0 without having looked.
  tmp=$(mktemp -d)
  write_fake_kubectl "$tmp"
  render_hook "$QDRANT_CHART" qdrant-test tenant-test qdrant-test-qdrant-cleanup "$tmp/hook.sh"

  export KLOG="$tmp/calls"
  export PVC_LIST=''
  export EXISTING=''
  export FAIL_GET='pvc'

  rc=0
  PATH="$tmp:$PATH" sh "$tmp/hook.sh" > "$tmp/out" 2> "$tmp/err" || rc=$?
  if [ "$rc" -eq 0 ]; then
    echo "FAIL: hook exited 0 although it never managed to list the PVCs"
    cat "$tmp/out" "$tmp/err"
    false
  fi
  if grep -q 'nothing to delete' "$tmp/out"; then
    echo "FAIL: a failed listing was reported as an empty namespace"
    cat "$tmp/out"
    false
  fi
  if ! grep -q 'failed to list PVCs' "$tmp/err"; then
    echo "FAIL: the failed listing was not named"
    cat "$tmp/err"
    false
  fi
  rm -rf "$tmp"
}

@test "harbor hook exits 0 and sweeps PVCs, Certificate and ACME solvers" {
  tmp=$(mktemp -d)
  write_fake_kubectl "$tmp"
  render_hook "$HARBOR_CHART" harbor-test tenant-test harbor-test-cleanup "$tmp/hook.sh" \
    --set '_namespace.host=example.org' --set '_cluster.solver=http01'

  export KLOG="$tmp/calls"
  export CHALLENGE_LIST='harbor-test-cert-1 '
  export SOLVER_LIST='cm-acme-http-solver-abcde '
  export EXISTING='harbor-test-ingress-tls harbor-test-cert-1 cm-acme-http-solver-abcde'

  rc=0
  PATH="$tmp:$PATH" sh "$tmp/hook.sh" > "$tmp/out" 2> "$tmp/err" || rc=$?
  if [ "$rc" -ne 0 ]; then
    echo "FAIL: hook exited $rc when every delete succeeded"
    cat "$tmp/out" "$tmp/err"
    false
  fi
  if ! grep -q 'Cleanup complete' "$tmp/out"; then
    echo "FAIL: hook did not report completion"
    cat "$tmp/out" "$tmp/err"
    false
  fi
  for expected in 'delete pvc' 'delete certificate' 'delete challenge' 'delete ingress'; do
    if ! grep -q "$expected" "$tmp/calls"; then
      echo "FAIL: the hook never issued: $expected"
      cat "$tmp/calls"
      false
    fi
  done
  rm -rf "$tmp"
}

@test "harbor hook exits 0 when every object is already gone" {
  # No in-flight ACME challenge and no solver Ingress is the ordinary case, and
  # the Certificate is usually collected with the release before the hook runs.
  # All of it must stay a success.
  tmp=$(mktemp -d)
  write_fake_kubectl "$tmp"
  render_hook "$HARBOR_CHART" harbor-test tenant-test harbor-test-cleanup "$tmp/hook.sh" \
    --set '_namespace.host=example.org' --set '_cluster.solver=http01'

  export KLOG="$tmp/calls"
  export CHALLENGE_LIST=''
  export SOLVER_LIST=''
  export EXISTING=''

  rc=0
  PATH="$tmp:$PATH" sh "$tmp/hook.sh" > "$tmp/out" 2> "$tmp/err" || rc=$?
  if [ "$rc" -ne 0 ]; then
    echo "FAIL: hook exited $rc on an already-clean namespace"
    cat "$tmp/out" "$tmp/err"
    false
  fi
  # The Certificate delete is the one that actually reaches a NotFound here, so
  # it has to have been attempted for this test to mean anything.
  if ! grep -q 'delete certificate .*harbor-test-ingress-tls' "$tmp/calls"; then
    echo "FAIL: the Certificate delete was never attempted"
    cat "$tmp/calls"
    false
  fi
  rm -rf "$tmp"
}

@test "harbor hook reports a failed delete, names the object and still runs the later sweeps" {
  tmp=$(mktemp -d)
  write_fake_kubectl "$tmp"
  render_hook "$HARBOR_CHART" harbor-test tenant-test harbor-test-cleanup "$tmp/hook.sh" \
    --set '_namespace.host=example.org' --set '_cluster.solver=http01'

  export KLOG="$tmp/calls"
  export CHALLENGE_LIST='harbor-test-cert-1 '
  export SOLVER_LIST='cm-acme-http-solver-abcde '
  export EXISTING='harbor-test-ingress-tls harbor-test-cert-1 cm-acme-http-solver-abcde'
  export FAIL_DELETE='pvc'

  rc=0
  PATH="$tmp:$PATH" sh "$tmp/hook.sh" > "$tmp/out" 2> "$tmp/err" || rc=$?
  if [ "$rc" -eq 0 ]; then
    echo "FAIL: hook exited 0 although the PVC delete failed"
    cat "$tmp/out" "$tmp/err"
    false
  fi
  if ! grep -q '^ERROR: .*release=harbor-test-system' "$tmp/err"; then
    echo "FAIL: the failure was not attributed to the objects it left behind"
    cat "$tmp/err"
    false
  fi
  # The first failure must not cost the release its Certificate and solver
  # cleanup: those run to completion and the exit code is decided at the end.
  for expected in 'delete certificate' 'delete challenge' 'delete ingress'; do
    if ! grep -q "$expected" "$tmp/calls"; then
      echo "FAIL: the sweep stopped at the first failure, skipping: $expected"
      cat "$tmp/calls"
      false
    fi
  done
  if grep -q 'Cleanup complete' "$tmp/out"; then
    echo "FAIL: hook reported completion after a failed delete"
    cat "$tmp/out"
    false
  fi
  rm -rf "$tmp"
}

@test "harbor hook reports every ACME delete that failed, each by name" {
  # The PVC sweep is only one of harbor's failure paths; the Certificate and the
  # two per-object ACME deletes are three more, and #3566 names each of them
  # separately. Covered together, with the PVC delete deliberately succeeding,
  # so this test fails if ANY of the three stops reporting — reverting one of
  # them to `|| echo "WARNING …"` is the exact defect being fixed.
  tmp=$(mktemp -d)
  write_fake_kubectl "$tmp"
  render_hook "$HARBOR_CHART" harbor-test tenant-test harbor-test-cleanup "$tmp/hook.sh" \
    --set '_namespace.host=example.org' --set '_cluster.solver=http01'

  export KLOG="$tmp/calls"
  export CHALLENGE_LIST='harbor-test-cert-1 '
  export SOLVER_LIST='cm-acme-http-solver-abcde '
  export EXISTING='harbor-test-ingress-tls harbor-test-cert-1 cm-acme-http-solver-abcde'
  export FAIL_DELETE='certificate challenge ingress'

  rc=0
  PATH="$tmp:$PATH" sh "$tmp/hook.sh" > "$tmp/out" 2> "$tmp/err" || rc=$?
  if [ "$rc" -eq 0 ]; then
    echo "FAIL: hook exited 0 although three ACME deletes failed"
    cat "$tmp/out" "$tmp/err"
    false
  fi
  # Anchored on the hook's own ERROR: prefix — the fake kubectl names the object
  # in its own diagnostic too, so an unanchored match would be satisfied by the
  # fixture and stay green with the hook reporting nothing.
  for object in 'Certificate tenant-test/harbor-test-ingress-tls' \
                'Challenge tenant-test/harbor-test-cert-1' \
                'solver Ingress tenant-test/cm-acme-http-solver-abcde'; do
    if ! grep -q "^ERROR: .*$object" "$tmp/err"; then
      echo "FAIL: the hook did not report the failed delete of: $object"
      cat "$tmp/err"
      false
    fi
  done
  if grep -q 'Cleanup complete' "$tmp/out"; then
    echo "FAIL: hook reported completion after three failed deletes"
    cat "$tmp/out"
    false
  fi
  rm -rf "$tmp"
}

@test "harbor hook reports a failed solver Ingress listing, the second of its two loops" {
  # The Challenge listing is covered below; this is the other loop. Both have
  # the same shape and each needs its own case, because a revert of one is
  # invisible to a test that only exercises the other.
  tmp=$(mktemp -d)
  write_fake_kubectl "$tmp"
  render_hook "$HARBOR_CHART" harbor-test tenant-test harbor-test-cleanup "$tmp/hook.sh" \
    --set '_namespace.host=example.org' --set '_cluster.solver=http01'

  export KLOG="$tmp/calls"
  export CHALLENGE_LIST='harbor-test-cert-1 '
  export SOLVER_LIST=''
  export EXISTING='harbor-test-ingress-tls harbor-test-cert-1'
  export FAIL_GET='ingress'

  rc=0
  PATH="$tmp:$PATH" sh "$tmp/hook.sh" > "$tmp/out" 2> "$tmp/err" || rc=$?
  if [ "$rc" -eq 0 ]; then
    echo "FAIL: hook exited 0 although it never managed to list the solver Ingresses"
    cat "$tmp/out" "$tmp/err"
    false
  fi
  if ! grep -q '^ERROR: .*failed to list solver Ingresses' "$tmp/err"; then
    echo "FAIL: the failed solver Ingress listing was not reported by the hook"
    cat "$tmp/err"
    false
  fi
  # The Challenge sweep runs before it and must have completed normally: this
  # failure is counted, not fatal.
  if ! grep -q 'delete challenge .*harbor-test-cert-1' "$tmp/calls"; then
    echo "FAIL: the Challenge sweep preceding the failed listing did not run"
    cat "$tmp/calls"
    false
  fi
  if grep -q 'Cleanup complete' "$tmp/out"; then
    echo "FAIL: hook reported completion after a failed listing"
    cat "$tmp/out"
    false
  fi
  rm -rf "$tmp"
}

@test "harbor hook exits non-zero when a listing fails instead of sweeping nothing" {
  # Iterating $(kubectl get …) directly cannot tell a failed listing from an
  # empty one: both leave the loop body unexecuted, so the sweep reported
  # success without having looked.
  tmp=$(mktemp -d)
  write_fake_kubectl "$tmp"
  render_hook "$HARBOR_CHART" harbor-test tenant-test harbor-test-cleanup "$tmp/hook.sh" \
    --set '_namespace.host=example.org' --set '_cluster.solver=http01'

  export KLOG="$tmp/calls"
  export CHALLENGE_LIST=''
  export SOLVER_LIST='cm-acme-http-solver-abcde '
  export EXISTING='harbor-test-ingress-tls cm-acme-http-solver-abcde'
  export FAIL_GET='challenge'

  rc=0
  PATH="$tmp:$PATH" sh "$tmp/hook.sh" > "$tmp/out" 2> "$tmp/err" || rc=$?
  if [ "$rc" -eq 0 ]; then
    echo "FAIL: hook exited 0 although it never managed to list the Challenges"
    cat "$tmp/out" "$tmp/err"
    false
  fi
  if ! grep -q 'failed to list Challenges' "$tmp/err"; then
    echo "FAIL: the failed listing was not named"
    cat "$tmp/err"
    false
  fi
  # A failed listing is counted, not fatal: the solver Ingress sweep after it
  # still runs and still deletes what it finds.
  if ! grep -q 'delete ingress .*cm-acme-http-solver-abcde' "$tmp/calls"; then
    echo "FAIL: the failed listing aborted the solver Ingress sweep that follows it"
    cat "$tmp/calls"
    false
  fi
  if grep -q 'Cleanup complete' "$tmp/out"; then
    echo "FAIL: hook reported completion after a failed listing"
    cat "$tmp/out"
    false
  fi
  rm -rf "$tmp"
}