Files
proxmox-scripts/tests/test_runner_cleanup.sh
l.kirchner e292a5e614
CI / Shell-Lint (bash -n, source-check, Validierungs-Tests) (pull_request) Successful in 3s
test(runner): the cleaner's list file stays inside the test
2026-10-11 01:19:02 +02:00

205 lines
10 KiB
Bash

#!/usr/bin/env bash
# Tests for install/runner-cleanup.sh, the runner's daily cleaner. Runs without
# Docker and without root: docker and stat are stand-ins on PATH that answer
# from tables, and act-runner's private /tmp is a directory under a temp root.
# stat is stood in because a ctime cannot be set back, and the cleaner goes by
# ctime; find and rm are real, under the temp root only. The cleaner's own
# list file (mktemp) goes to a directory of the test too, not to /tmp.
# Aufruf: bash tests/test_runner_cleanup.sh
set -u
REPO_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
CLEANUP="$REPO_ROOT/install/runner-cleanup.sh"
PASS=0
FAIL=0
ok() { PASS=$((PASS + 1)); echo "ok - $1"; }
nok() { FAIL=$((FAIL + 1)); echo "NOT OK - $1"; }
# Setup must not half-work: a stand-in that is not executable would let the
# real docker on PATH answer.
die() { echo "setup failed: $1" >&2; exit 1; }
T="$(mktemp -d)" || die mktemp
trap 'rm -rf "$T"' EXIT
mkdir -p "$T/conf" "$T/bin" "$T/own" || die mkdir
# patchmgr's file, as runner-install.sh lays it down.
cat >"$T/conf/patchmgr.conf" <<'CONF_EOF' || die conf
# l.kirchner/patchmgr (D64)
PROJECT=patchmgr
CONTAINER_NAME_PATTERN='^patchmgr-testdb-'
TMP_PATTERN='patchmgr-counter-check-*'
CONF_EOF
# docker: by the label org.luki-net.project=patchmgr it knows four containers
# -- "old" (created and started two days ago), "new" (one hour), "stray"
# (two days, labelled but named like something else) and "restarted"
# (created two days ago, started again an hour ago). An unlabelled container
# never comes back from a filter by label. rm of an id listed in
# $T/rm-fails fails.
OLD="$(date -u -d '2 days ago' +%FT%TZ)"
NEW="$(date -u -d '1 hour ago' +%FT%TZ)"
cat >"$T/bin/docker" <<DOCKER_EOF || die docker
#!/usr/bin/env bash
echo "\$*" >> "$T/calls"
case "\$1" in
ps) [ "\$4" = "label=org.luki-net.project=patchmgr" ] && printf 'old\nnew\nstray\nrestarted\n' ;;
inspect)
case "\$4" in
old) echo "/patchmgr-testdb-1-x $OLD $OLD" ;;
new) echo "/patchmgr-testdb-2-x $NEW $NEW" ;;
stray) echo "/other-db $OLD $OLD" ;;
restarted) echo "/patchmgr-testdb-3-x $OLD $NEW" ;;
esac ;;
rm) grep -qx "\$4" "$T/rm-fails" 2>/dev/null && exit 1 ;;
esac
exit 0
DOCKER_EOF
# stat -c %Z: the ctime from $T/ctimes by base name, "now" for anything else.
cat >"$T/bin/stat" <<STAT_EOF || die stat
#!/usr/bin/env bash
[ "\$1 \$2" = "-c %Z" ] || exit 2
b=\$(basename "\$4")
t=\$(awk -v b="\$b" '\$1 == b { print \$2 }' "$T/ctimes")
echo "\${t:-\$(date +%s)}"
STAT_EOF
chmod +x "$T/bin/docker" "$T/bin/stat" || die chmod
[ -x "$T/bin/docker" ] && [ -x "$T/bin/stat" ] || die "stand-ins not executable"
TWO_DAYS_AGO=$(($(date +%s) - 2 * 86400))
# act-runner's PrivateTmp: patchmgr's old and new copy, a copy with an old
# mtime but a fresh ctime (cp -a of an old tree), and old entries that are
# not patchmgr's.
fresh_tmp() {
rm -rf "$T/root"
R="$T/root/systemd-private-x-act-runner.service-y/tmp"
mkdir -p "$R/patchmgr-counter-check-old" "$R/patchmgr-counter-check-new" "$R/patchmgr-counter-check-copied" \
"$R/counter-check-old" "$R/go-build-old" "$R/patchmgrx-old" || die tmp
touch -d '2 days ago' "$R/patchmgr-counter-check-copied" || die touch
printf '%s %s\n' patchmgr-counter-check-old "$TWO_DAYS_AGO" counter-check-old "$TWO_DAYS_AGO" \
go-build-old "$TWO_DAYS_AGO" patchmgrx-old "$TWO_DAYS_AGO" >"$T/ctimes"
: >"$T/calls"
rm -f "$T/rm-fails"
}
cleanup() { PATH="$T/bin:$PATH" CONF_DIR="$T/conf" TMP_ROOT="$T/root" TMPDIR="$T/own" bash "$CLEANUP"; }
removed_any() { grep -q '^rm' "$T/calls"; }
untouched() { [ -d "$R/counter-check-old" ] && [ -d "$R/go-build-old" ] && [ -d "$R/patchmgrx-old" ] && [ -d "$R/patchmgr-counter-check-old" ]; }
# ── DRY_RUN: empty or 0 removes, anything else only lists ─────────────────────
for v in 1 true yes; do
fresh_tmp
out="$(DRY_RUN=$v cleanup 2>&1)"
if removed_any || [ ! -d "$R/patchmgr-counter-check-old" ]; then
nok "DRY_RUN=$v removed something"
else
ok "DRY_RUN=$v removes nothing"
fi
done
if grep -q 'container patchmgr-testdb-1-x' <<<"$out"; then ok "a dry run lists what it would remove"; else nok "a dry run did not list: $out"; fi
if grep -q 'removed for patchmgr: none (DRY_RUN' <<<"$out"; then ok "a dry run says it removed nothing"; else nok "a dry run's last line: $out"; fi
for v in 0 ''; do
fresh_tmp
DRY_RUN=$v cleanup >/dev/null 2>&1
if removed_any; then ok "DRY_RUN='$v' removes"; else nok "DRY_RUN='$v' removed nothing"; fi
done
# ── The real run: only patchmgr's, by label and name, older than 4 h ─────────
fresh_tmp
if cleanup >/dev/null 2>&1; then ok "a clean run exits 0"; else nok "a clean run exited non-zero"; fi
if grep -qx 'rm -f -v old' "$T/calls"; then ok "the old container goes, with its volumes (rm -f -v)"; else nok "the old container was not removed with -v: $(grep '^rm' "$T/calls")"; fi
if grep -q '^rm .*new' "$T/calls"; then nok "a container younger than 4 h was removed"; else ok "a container younger than 4 h stays"; fi
if grep -q '^rm .*restarted' "$T/calls"; then nok "a container started again an hour ago was removed"; else ok "a container counts from its last start, not its creation"; fi
if grep -q '^rm .*stray' "$T/calls"; then nok "a labelled container with another name was removed"; else ok "the label alone is not enough: the name must match too"; fi
if grep -q -- '--filter label=org.luki-net.project=patchmgr' "$T/calls"; then ok "containers are found by the project label"; else nok "docker was not asked by label: $(cat "$T/calls")"; fi
if grep -q -- '--filter name=' "$T/calls"; then nok "docker was asked by name, not by label"; else ok "no filter by name alone"; fi
if [ ! -e "$R/patchmgr-counter-check-old" ]; then ok "patchmgr's old copy goes"; else nok "patchmgr's old copy is still there"; fi
if [ -d "$R/patchmgr-counter-check-new" ]; then ok "a copy younger than 4 h stays"; else nok "the new copy was removed"; fi
if [ -d "$R/patchmgr-counter-check-copied" ]; then ok "a fresh copy with an old mtime stays: the age is the ctime"; else nok "a copy with an old mtime but a fresh ctime was removed"; fi
if [ -d "$R/counter-check-old" ] && [ -d "$R/go-build-old" ] && [ -d "$R/patchmgrx-old" ]; then ok "entries without patchmgr's prefix stay"; else nok "an entry without the prefix was removed"; fi
if grep -q -E '^(system|volume|image|container) prune' "$T/calls"; then nok "docker was asked to prune"; else ok "no docker prune"; fi
# ── A failure says so in the exit code, and the other projects still run ─────
fresh_tmp
echo old >"$T/rm-fails"
printf "PROJECT=other\nTMP_PATTERN='other-*'\n" >"$T/conf/zz-other.conf"
mkdir -p "$R/other-old"
echo "other-old $TWO_DAYS_AGO" >>"$T/ctimes"
cleanup >"$T/out" 2>&1
rc=$?
rm -f "$T/conf/zz-other.conf"
if [ "$rc" -eq 1 ] && grep -q 'docker rm patchmgr-testdb-1-x failed' "$T/out"; then ok "a failed docker rm makes the run exit 1"; else nok "a failed docker rm: exit $rc: $(cat "$T/out")"; fi
if [ ! -e "$R/other-old" ] && [ ! -e "$R/patchmgr-counter-check-old" ]; then ok "the rest still runs after one failure"; else nok "a failure stopped the rest"; fi
# ── A file cannot claim another project's things, or everything ──────────────
refused() { # $1: file content, $2: name for the test
fresh_tmp
mv "$T/conf/patchmgr.conf" "$T/patchmgr.conf.keep"
printf '%s\n' "$1" >"$T/conf/bad.conf"
out="$(cleanup 2>&1)"
rc=$?
rm -f "$T/conf/bad.conf"
mv "$T/patchmgr.conf.keep" "$T/conf/patchmgr.conf"
if removed_any || ! untouched; then
nok "$2 removed something"
elif [ "$rc" -ne 0 ] && grep -q 'skipped' <<<"$out"; then
ok "$2 is refused"
else
nok "$2 was not refused (exit $rc): $out"
fi
}
refused "PROJECT=patchmgr
CONTAINER_NAME_PATTERN='^.*'" "CONTAINER_NAME_PATTERN='^.*'"
refused "PROJECT=patchmgr
CONTAINER_NAME_PATTERN='^other-'" "CONTAINER_NAME_PATTERN='^other-'"
refused "PROJECT=patchmgr
CONTAINER_NAME_PATTERN='^patchmgr-testdb-|.*'" "an alternation after the prefix"
refused "PROJECT=patchmgr
CONTAINER_NAME_PATTERN='^patchmgr-{0}.*'" "a quantifier that makes the hyphen optional"
refused "PROJECT=patchmgr
CONTAINER_NAME_PATTERN='^patchmgr-testdb-['" "a pattern that is not a regex"
refused "PROJECT=patchmgr
TMP_PATTERN='patchmgr-\$(id)'" "a quoted \$(...) in TMP_PATTERN"
refused "PROJECT=patchmgr
TMP_PATTERN='patchmgr-[a-z]*'" "brackets in TMP_PATTERN"
refused "PROJECT=patchmgr
TMP_PATTERN='?*'" "TMP_PATTERN='?*'"
refused "PROJECT=patchmgr
TMP_PATTERN='*'" "TMP_PATTERN='*'"
refused "PROJECT=patchmgr
TMP_PATTERN='other-*'" "TMP_PATTERN='other-*'"
refused "PROJECT=patchmgr
TMP_PATTERN='patchmgr-x/y'" "TMP_PATTERN with a directory"
refused "PROJECT='Patch Mgr'
TMP_PATTERN='*'" "a PROJECT that is not a name"
refused "PROJECT=patchmgr-old
TMP_PATTERN='patchmgr-old-*'" "a PROJECT with a hyphen (its prefix would overlap patchmgr's)"
refused "PROJECT=patchmgr
MAX_AGE_HOURS=1
TMP_PATTERN='patchmgr-*'" "a file that sets MAX_AGE_HOURS"
refused "PROJECT=patchmgr\$(touch $T/ran)
TMP_PATTERN='patchmgr-*'" "a file with a command in it"
if [ -e "$T/ran" ]; then nok "a command in a file was run"; else ok "a file is read, not run"; fi
# ── A project claimed by two files is claimed by neither ─────────────────────
fresh_tmp
printf "PROJECT=patchmgr\nTMP_PATTERN='patchmgr-*'\n" >"$T/conf/copy.conf"
out="$(cleanup 2>&1)"
rc=$?
rm -f "$T/conf/copy.conf"
if [ "$rc" -eq 1 ] && ! removed_any && untouched && [ "$(grep -c 'claimed by 2 files' <<<"$out")" -eq 2 ]; then ok "a project in two files is refused in both"; else nok "a project in two files (exit $rc): $out"; fi
# ── The age cannot go below the job timeout of 3 h ───────────────────────────
for age in 0 3 abc 04 1000 4611686018427387904; do
if MAX_AGE_HOURS="$age" cleanup >/dev/null 2>&1; then nok "MAX_AGE_HOURS=$age was accepted"; else ok "MAX_AGE_HOURS=$age is refused"; fi
done
fresh_tmp
if MAX_AGE_HOURS=4 cleanup >/dev/null 2>&1; then ok "MAX_AGE_HOURS=4 is accepted"; else nok "MAX_AGE_HOURS=4 was refused"; fi
# ── The cleaner's list files stay in the test and are removed ────────────────
if [ -z "$(ls -A "$T/own")" ]; then ok "no list file is left behind"; else nok "list files left behind: $(ls -A "$T/own")"; fi
echo
echo "pass: $PASS, fail: $FAIL"
[ "$FAIL" -eq 0 ]