Files
cozystack/hack/altinity-backup-strategy.bats
T
Myasnikov Daniil d5559bb934 test(backups): remove the Altinity strategy tests' scratch directories
Every test in hack/altinity-backup-strategy.bats made a directory with
mktemp -d and never removed it, so each passing run of the file left
nine directories in TMPDIR, and they pile up on any machine that keeps
it between runs.

Each test now removes its directory as its last statement. A failed
assertion aborts the body before that line, so a failing test keeps
its stub log and rendered script for inspection.

Assisted-by: LLM
Signed-off-by: Myasnikov Daniil <myasnikovdaniil2001@gmail.com>
2026-11-05 07:56:39 +00:00

219 lines
9.8 KiB
Bash

#!/usr/bin/env bats
# Behavioural tests for the shipped Altinity (ClickHouse) backup/restore
# strategy script, extracted from the rendered cozy-default-altinity Strategy
# CR and EXECUTED against a stubbed clickhouse-backup HTTP API, with the real
# jq doing the archive selection. Which archive a restore reads back is
# decided by that selection over list/remote, so it is exercised against
# several archives rather than string-matched.
#
# The script runs with `bash`, not `sh`, for the reason given in
# hack/redis-backup-strategy.bats: it opens `set -euo pipefail`, which the
# production image's busybox ash supports and the CI runner's dash rejects.
load test_helper
CHART=packages/system/backupstrategy-controller
# Render the shipped strategy's in-Pod script into $1 and substitute the
# controller-render placeholders the way the driver fills them: $2 mode, $3
# app (release and source application), $4 the archive name recorded on the
# Backup (empty for a Backup that records none), $5 the Backup name, $6 its
# takenAt in the archive-name layout. An empty render fails early, as does a
# placeholder left unsubstituted.
render_script() {
out=$1; mode=$2; app=$3; archive=$4; backup=$5; takenat=$6
helm template t "$CHART" -n cozy-backup-controller \
--set backupStorage.bucketNameOverride=test-bucket 2>/dev/null \
| yq eval 'select(.kind == "Altinity") | .spec.template.spec.containers[0].args[0]' - \
| sed -e "s#{{ .Mode }}#${mode}#g" \
-e "s#{{ .Release.Name }}#${app}#g" \
-e "s#{{ .Release.Namespace }}#tenant-x#g" \
-e "s#{{ .Backup.ApplicationRef.Name }}#${app}#g" \
-e "s#{{ .Backup.Name }}#${backup}#g" \
-e "s#{{ .ArchiveName | default \"\" }}#${archive}#g" \
-e "s#{{ .Backup.TakenAt | default \"\" }}#${takenat}#g" > "$out"
[ -s "$out" ] || return 1
if grep -q '{{' "$out"; then
echo "rendered script still holds a controller placeholder" >&2
return 1
fi
}
# Stub the clickhouse-backup API behind curl, first on PATH. list/remote
# serves $state/remote; actions serves $state/actions plus one success row per
# operation the script has issued, and after its first read swaps in
# $state/actions.next when a test seeds one (an operation that finishes while
# the script waits). Every request is logged to $state/curl.log. sleep is
# stubbed to fail after a few calls, so a script that never sees its
# operation finish fails the test instead of hanging it.
make_stubs() {
bin=$1; state=$2
mkdir -p "$bin"
: > "$state/remote"
: > "$state/actions"
cat > "$bin/curl" <<STUB
#!/usr/bin/env bash
set -eu
method=GET; url=""
for a in "\$@"; do
case "\$a" in POST) method=POST ;; http://*) url="\$a" ;; esac
done
echo "\$method \$url" >> "$state/curl.log"
case "\$method \$url" in
"GET "*/backup/status) echo '{}' ;;
"GET "*/backup/list/remote) cat "$state/remote" ;;
"GET "*/backup/actions)
cat "$state/actions"
if [ -f "$state/actions.done" ]; then cat "$state/actions.done"; fi
if [ -f "$state/actions.next" ]; then mv "$state/actions.next" "$state/actions"; fi
;;
"POST "*/backup/create_remote*)
name=\${url#*name=}; name=\${name%%&*}
printf '{"command":"create_remote --resume %s","status":"success","start":"2099-01-01 00:00:00"}\n' "\$name" >> "$state/actions.done"
;;
"POST "*/backup/restore_remote/*)
printf '{"command":"restore_remote %s","status":"success","start":"2099-01-01 00:00:00"}\n' "\${url##*/}" >> "$state/actions.done"
;;
*) echo "unexpected request: \$method \$url" >&2; exit 22 ;;
esac
STUB
cat > "$bin/sleep" <<STUB
#!/usr/bin/env bash
n=\$(cat "$state/sleeps" 2>/dev/null || echo 0)
echo \$((n + 1)) > "$state/sleeps"
if [ "\$n" -ge 3 ]; then echo "sleep budget spent: the operation never finished" >&2; exit 1; fi
STUB
chmod 0755 "$bin/curl" "$bin/sleep"
export API_USERNAME=u API_PASSWORD=p
}
# One list/remote row per argument, "name" or "name|desc"; created mirrors
# the name's timestamp, which is the order the pre-fix selection sorted by.
remote_rows() {
for row in "$@"; do
name=${row%%|*}; desc="tar, regular"
if [ "$row" != "$name" ]; then desc=${row#*|}; fi
ts=${name##*-}
printf '{"name":"%s","created":"%s","location":"remote","required":"","desc":"%s"}\n' "$name" "$ts" "$desc"
done
}
@test "an empty render fails the fixture instead of passing as a no-op" {
tmp=$(mktemp -d)
CHART=$tmp/no-such-chart
if render_script "$tmp/s.sh" restore myapp "" bkp 20260304T050622Z 2>/dev/null; then
echo "FAIL: render_script reported success with no chart"
false
fi
rm -rf "$tmp"
}
@test "restore reads back the archive the Backup recorded, not the newest one" {
tmp=$(mktemp -d)
make_stubs "$tmp/bin" "$tmp"
render_script "$tmp/s.sh" restore myapp clickhouse-myapp-20260304T050607Z bkp-a 20260304T050622Z
remote_rows clickhouse-myapp-20260304T040000Z clickhouse-myapp-20260304T050607Z \
clickhouse-myapp-20260304T060000Z clickhouse-myapp-20260304T070000Z > "$tmp/remote"
PATH="$tmp/bin:$PATH" bash "$tmp/s.sh"
grep -qxF 'POST http://chi-clickhouse-myapp-clickhouse-0-0.tenant-x.svc:7171/backup/restore_remote/clickhouse-myapp-20260304T050607Z' "$tmp/curl.log"
[ "$(grep -c '/backup/restore_remote/' "$tmp/curl.log")" -eq 1 ]
rm -rf "$tmp"
}
@test "restore fails naming the recorded archive when remote storage lacks it" {
tmp=$(mktemp -d)
make_stubs "$tmp/bin" "$tmp"
render_script "$tmp/s.sh" restore myapp clickhouse-myapp-20260304T050607Z bkp-a 20260304T050622Z
remote_rows clickhouse-myapp-20260304T040000Z clickhouse-myapp-20260304T060000Z > "$tmp/remote"
out=""
if out=$(PATH="$tmp/bin:$PATH" bash "$tmp/s.sh" 2>&1); then
echo "FAIL: restore succeeded without the recorded archive: $out"
false
fi
printf '%s\n' "$out" | grep -qF 'archive clickhouse-myapp-20260304T050607Z recorded on Backup bkp-a is not in remote storage'
if grep -qF '/backup/restore_remote/' "$tmp/curl.log"; then
echo "FAIL: restored another archive: $(cat "$tmp/curl.log")"
false
fi
rm -rf "$tmp"
}
@test "restore of a Backup without a recorded archive takes the newest one stamped by its takenAt, and warns" {
tmp=$(mktemp -d)
make_stubs "$tmp/bin" "$tmp"
render_script "$tmp/s.sh" restore myapp "" bkp-a 20260304T050800Z
# Each decoy sorts after the expected archive and is ruled out by one
# filter: broken, not a bare timestamp after the prefix, or stamped after
# takenAt.
remote_rows clickhouse-myapp-20260304T040000Z clickhouse-myapp-20260304T050607Z \
"clickhouse-myapp-20260304T050700Z|broken (can't stat metadata.json)" \
clickhouse-myapp-20260304T050710Z-copy \
clickhouse-myapp-20260304T060000Z > "$tmp/remote"
out=$(PATH="$tmp/bin:$PATH" bash "$tmp/s.sh" 2>&1)
grep -qxF 'POST http://chi-clickhouse-myapp-clickhouse-0-0.tenant-x.svc:7171/backup/restore_remote/clickhouse-myapp-20260304T050607Z' "$tmp/curl.log"
[ "$(grep -c '/backup/restore_remote/' "$tmp/curl.log")" -eq 1 ]
printf '%s\n' "$out" | grep -qF 'WARNING: Backup bkp-a records no archive name'
rm -rf "$tmp"
}
@test "restore of a Backup without a recorded archive fails when no archive is stamped by its takenAt" {
tmp=$(mktemp -d)
make_stubs "$tmp/bin" "$tmp"
render_script "$tmp/s.sh" restore myapp "" bkp-a 20260304T050800Z
remote_rows clickhouse-myapp-20260304T060000Z clickhouse-myapp-20260304T070000Z > "$tmp/remote"
out=""
if out=$(PATH="$tmp/bin:$PATH" bash "$tmp/s.sh" 2>&1); then
echo "FAIL: restore succeeded with every archive newer than takenAt: $out"
false
fi
printf '%s\n' "$out" | grep -qF 'no archive of myapp stamped no later than 20260304T050800Z'
if grep -qF '/backup/restore_remote/' "$tmp/curl.log"; then
echo "FAIL: restored an archive newer than takenAt: $(cat "$tmp/curl.log")"
false
fi
rm -rf "$tmp"
}
@test "backup writes the archive under the name the controller chose" {
tmp=$(mktemp -d)
make_stubs "$tmp/bin" "$tmp"
render_script "$tmp/s.sh" backup myapp clickhouse-myapp-20260304T050607Z "" ""
PATH="$tmp/bin:$PATH" bash "$tmp/s.sh"
grep -qxF 'POST http://chi-clickhouse-myapp-clickhouse-0-0.tenant-x.svc:7171/backup/create_remote?name=clickhouse-myapp-20260304T050607Z&resume=true' "$tmp/curl.log"
rm -rf "$tmp"
}
@test "a retried backup waits on the attempt the sidecar is still running" {
tmp=$(mktemp -d)
make_stubs "$tmp/bin" "$tmp"
render_script "$tmp/s.sh" backup myapp clickhouse-myapp-20260304T050607Z "" ""
printf '{"command":"create_remote --resume clickhouse-myapp-20260304T050607Z","status":"in progress","start":"2026-03-04 05:06:08"}\n' > "$tmp/actions"
printf '{"command":"create_remote --resume clickhouse-myapp-20260304T050607Z","status":"success","start":"2026-03-04 05:06:08"}\n' > "$tmp/actions.next"
PATH="$tmp/bin:$PATH" bash "$tmp/s.sh"
# The sidecar answers a second operation with 423 while one runs.
if grep -qF 'POST ' "$tmp/curl.log"; then
echo "FAIL: started another operation over the running one: $(cat "$tmp/curl.log")"
false
fi
rm -rf "$tmp"
}
@test "a retried backup after a failed attempt resumes the same archive" {
tmp=$(mktemp -d)
make_stubs "$tmp/bin" "$tmp"
render_script "$tmp/s.sh" backup myapp clickhouse-myapp-20260304T050607Z "" ""
printf '{"command":"create_remote clickhouse-myapp-20260304T050607Z","status":"error","start":"2026-03-04 05:06:08","error":"S3 PutObject 403"}\n' > "$tmp/actions"
PATH="$tmp/bin:$PATH" bash "$tmp/s.sh"
grep -qxF 'POST http://chi-clickhouse-myapp-clickhouse-0-0.tenant-x.svc:7171/backup/create_remote?name=clickhouse-myapp-20260304T050607Z&resume=true' "$tmp/curl.log"
rm -rf "$tmp"
}
@test "backup falls back to a timestamped name when the controller supplies none" {
tmp=$(mktemp -d)
make_stubs "$tmp/bin" "$tmp"
render_script "$tmp/s.sh" backup myapp "" "" ""
PATH="$tmp/bin:$PATH" bash "$tmp/s.sh"
grep -qE '^POST http://[^ ]*/backup/create_remote\?name=clickhouse-myapp-[0-9]{8}T[0-9]{6}Z&resume=true$' "$tmp/curl.log"
rm -rf "$tmp"
}