hack/redis-backup-strategy.bats
Rama main · réplica de lectura del repositorio original.
#!/usr/bin/env bats
# Behavioural tests for the shipped Redis backup/restore strategy script,
# extracted from the rendered cozy-default-redis Strategy CR and EXECUTED with
# stubbed redis-cli / curl. helm-unittest (tests/strategy_redis_test.yaml) pins
# the chart's render-time decisions (volumes, env, deadline); the script's own
# behaviour — which mode reaches which command — is a contract between the
# running Pod, Redis and S3, so it is exercised here rather than string-matched.
#
# The load-bearing case is cleanup: a deleted Backup renders MODE=cleanup, and
# it must delete the object and NEVER reach the restore path's FLUSHALL against
# the live master. Blanking the object key or breaking the rfs-redis- sentinel
# prefix is caught the same way — by what the stubs are actually called with.
#
# Run from the repo root with make bats-unit-tests. To retain compatibility
# with cozytest, assertions precede inline cleanup: its translator would
# replace a final assertion's status with return 0 at a lone closing brace.
#
# The rendered script is run with `bash`, not `sh`: it opens `set -euo
# pipefail`, which the production image's busybox ash supports but the CI
# runner's /bin/sh (dash) rejects at line 1. bash matches ash's pipefail
# semantics; same reason hack/migration-50-etcd-adopt.bats runs its script
# with bash.
load test_helper
CHART=packages/system/backupstrategy-controller
# Render the shipped strategy's in-Pod script into $1, substitute the
# controller-render placeholders (Mode, Release.Name, ObjectKey, Release.Prefix)
# with the given values ($2 mode, $3 app, $4 object key, $6 chart prefix,
# redis- by default), and rebind every absolute path it touches into $5 so it
# runs unprivileged. An empty render is an early failure, as is a placeholder
# left unsubstituted, and no real /etc or /tmp path may survive the rebinding —
# these tests EXECUTE the script.
render_script() {
out=$1; mode=$2; app=$3; key=$4; sandbox=$5; prefix=${6:-redis-}
mkdir -p "$sandbox/redis-ca" "$sandbox/s3-ca" "$sandbox/restore"
helm template t "$CHART" -n cozy-backup-controller \
--set backupStorage.bucketNameOverride=test-bucket 2>/dev/null \
| yq eval 'select(.kind == "Redis") | .spec.template.spec.containers[0].args[0]' - \
| sed -e "s#{{ .Mode }}#${mode}#g" \
-e "s#{{ .Release.Prefix | default \"redis-\" }}#${prefix}#g" \
-e "s#{{ .Release.Name }}#${app}#g" \
-e "s#{{ .ObjectKey }}#${key}#g" \
-e "s#/etc/redis-ca#${sandbox}/redis-ca#g" \
-e "s#/etc/s3-ca#${sandbox}/s3-ca#g" \
-e "s#/tmp/restore#${sandbox}/restore#g" \
-e "s#/tmp/dump.rdb#${sandbox}/dump.rdb#g" > "$out"
[ -s "$out" ] || return 1
if grep -q '{{' "$out"; then
echo "rendered script still holds a controller placeholder" >&2
return 1
fi
# The exact absolute paths the rebinding above replaces; a survivor means the
# script grew a new one and the test would touch the real filesystem. Matched
# literally (not a bare /tmp/, which the sandbox itself lives under in CI).
if grep -qE '/etc/redis-ca|/etc/s3-ca|/tmp/dump\.rdb|/tmp/restore' "$out"; then
echo "rendered script still references a real /etc or /tmp path after rebinding" >&2
return 1
fi
}
# redis-cli / curl stubs that log every invocation, put first on PATH. The
# sentinel query answers with a master addr so backup/restore proceed; nothing
# actually talks to Redis or S3.
make_stubs() {
bin=$1; rclog=$2; curllog=$3
mkdir -p "$bin"
cat > "$bin/redis-cli" <<STUB
#!/bin/sh
echo "\$*" >> "$rclog"
case "\$*" in
*"get-master-addr-by-name"*) printf '127.0.0.1\n6379\n' ;;
*"INFO persistence"*) printf 'aof_rewrite_in_progress:0\naof_last_bgrewrite_status:ok\n' ;;
esac
exit 0
STUB
cat > "$bin/curl" <<STUB
#!/bin/sh
echo "\$*" >> "$curllog"
# Cleanup asks for the HTTP status with -w; a 204 is a successful delete.
case "\$*" in *"-w %{http_code}"*|*"-w"*) echo 204 ;; esac
exit 0
STUB
cat > "$bin/redis-server" <<'STUB'
#!/bin/sh
exit 0
STUB
chmod 0755 "$bin/redis-cli" "$bin/curl" "$bin/redis-server"
}
s3_env() {
export S3_ENDPOINT=s3.example.com S3_BUCKET=bkt S3_REGION=us-east-1
export AWS_ACCESS_KEY_ID=ak AWS_SECRET_ACCESS_KEY=sk S3_INSECURE_SKIP_VERIFY=false
}
@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" cleanup app key "$tmp/sb" 2>/dev/null; then
echo "FAIL: render_script reported success with no chart"
false
fi
}
@test "cleanup deletes the object and never FLUSHALLs the live master" {
tmp=$(mktemp -d)
make_stubs "$tmp/bin" "$tmp/rc.log" "$tmp/curl.log"
render_script "$tmp/s.sh" cleanup myapp "tenant-x/myapp/bkp-42.rdb" "$tmp/sb"
s3_env
PATH="$tmp/bin:$PATH" bash "$tmp/s.sh"
# A DELETE against exactly this backup's object.
grep -q -- '-X DELETE' "$tmp/curl.log"
grep -q 'tenant-x/myapp/bkp-42.rdb' "$tmp/curl.log"
# The destructive branch must be unreachable in cleanup: no FLUSHALL, no
# --pipe, and in fact no Redis contact at all. This is the assertion the
# round's cleanup arm exists to hold.
if grep -qi 'FLUSHALL' "$tmp/rc.log"; then echo "FAIL: cleanup must not flush the master"; false; fi
if grep -q -- '--pipe' "$tmp/rc.log"; then echo "FAIL: cleanup must not pipe a restore into the master"; false; fi
[ ! -s "$tmp/rc.log" ]
}
@test "backup resolves rfs-redis-<app> and uploads under the recorded object key" {
tmp=$(mktemp -d)
make_stubs "$tmp/bin" "$tmp/rc.log" "$tmp/curl.log"
render_script "$tmp/s.sh" backup myapp "tenant-x/myapp/bkp-42.rdb" "$tmp/sb"
s3_env
PATH="$tmp/bin:$PATH" bash "$tmp/s.sh"
# Sentinel host carries the rfs-redis- prefix owned by the redis chart.
grep -q -- '-h rfs-redis-myapp -p 26379' "$tmp/rc.log"
grep -q 'get-master-addr-by-name mymaster' "$tmp/rc.log"
# The RDB is uploaded to exactly the recorded object key, so a Plan keeps one
# object per run.
grep -q -- '-X PUT' "$tmp/curl.log"
grep -q 'tenant-x/myapp/bkp-42.rdb' "$tmp/curl.log"
# Backup must never wipe the source.
if grep -qi 'FLUSHALL' "$tmp/rc.log"; then echo "FAIL: backup must not flush the source"; false; fi
}
@test "a Valkey backup resolves rfs-valkey-<app>, the valkey chart's sentinel" {
tmp=$(mktemp -d)
make_stubs "$tmp/bin" "$tmp/rc.log" "$tmp/curl.log"
render_script "$tmp/s.sh" backup myapp "tenant-x/myapp/bkp-42.rdb" "$tmp/sb" valkey-
s3_env
PATH="$tmp/bin:$PATH" bash "$tmp/s.sh"
grep -q -- '-h rfs-valkey-myapp -p 26379' "$tmp/rc.log"
grep -q -- '-X PUT' "$tmp/curl.log"
if grep -q 'rfs-redis-' "$tmp/rc.log"; then echo "FAIL: a Valkey backup must not query the redis sentinel"; false; fi
}
@test "an unrecognised mode exits non-zero without touching the master" {
tmp=$(mktemp -d)
make_stubs "$tmp/bin" "$tmp/rc.log" "$tmp/curl.log"
render_script "$tmp/s.sh" bogus myapp "tenant-x/myapp/bkp-42.rdb" "$tmp/sb"
s3_env
# No bats `run` helper under hack/cozytest.sh, so capture the status directly.
if PATH="$tmp/bin:$PATH" bash "$tmp/s.sh" 2>/dev/null; then
echo "FAIL: an unrecognised mode must exit non-zero"
false
fi
# The unknown mode must fail closed, never reaching the restore FLUSHALL.
if grep -qi 'FLUSHALL' "$tmp/rc.log"; then echo "FAIL: an unrecognised mode must not reach the restore flush"; false; fi
[ -f "$tmp/rc.log" ]
}