222 lines
9.3 KiB
Bash
222 lines
9.3 KiB
Bash
#!/usr/bin/env bash
|
|
set -eo pipefail
|
|
|
|
ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
|
|
TACHYON_LIB="$ROOT_DIR/tachyon/files/usr/lib"
|
|
SNAPSHOT_UC="$TACHYON_LIB/service/snapshot.uc"
|
|
TACHYON_BIN="$ROOT_DIR/tachyon/files/usr/bin/tachyon"
|
|
WORK_DIR="$(mktemp -d)"
|
|
|
|
cleanup() {
|
|
rm -rf "$WORK_DIR"
|
|
}
|
|
trap cleanup EXIT
|
|
|
|
fail() {
|
|
printf 'FAIL: %s\n' "$1" >&2
|
|
exit 1
|
|
}
|
|
|
|
[ -r "$SNAPSHOT_UC" ] || fail "service/snapshot.uc must own config snapshot logic"
|
|
[ -r "$TACHYON_BIN" ] || fail "tachyon entrypoint is missing"
|
|
|
|
grep -Fq 'snapshot_list: [ "service/snapshot.uc", "snapshot-list", 0 ]' "$TACHYON_BIN" ||
|
|
fail "tachyon entrypoint must dispatch snapshot_list to service/snapshot.uc"
|
|
grep -Fq 'snapshot_save: [ "service/snapshot.uc", "snapshot-save", 1 ]' "$TACHYON_BIN" ||
|
|
fail "tachyon entrypoint must dispatch snapshot_save to service/snapshot.uc"
|
|
grep -Fq 'snapshot_restore: [ "service/snapshot.uc", "snapshot-restore", 1 ]' "$TACHYON_BIN" ||
|
|
fail "tachyon entrypoint must dispatch snapshot_restore to service/snapshot.uc"
|
|
grep -Fq 'snapshot_delete: [ "service/snapshot.uc", "snapshot-delete", 1 ]' "$TACHYON_BIN" ||
|
|
fail "tachyon entrypoint must dispatch snapshot_delete to service/snapshot.uc"
|
|
grep -Fq 'snapshot_save <name>' "$TACHYON_BIN" ||
|
|
fail "tachyon help must document snapshot_save"
|
|
|
|
mkdir -p "$WORK_DIR/lib"
|
|
mkdir -p "$WORK_DIR/config"
|
|
mkdir -p "$WORK_DIR/tachyon"
|
|
mkdir -p "$WORK_DIR/snapshots"
|
|
|
|
printf '%s\n' \
|
|
"config settings 'settings'" \
|
|
" option dns_type 'udp'" \
|
|
" option dns '8.8.8.8'" >"$WORK_DIR/config/tachyon"
|
|
printf '%s\n' 'snap-data' >"$WORK_DIR/tachyon/data"
|
|
|
|
cat >"$WORK_DIR/fake-bin" <<'SH'
|
|
#!/usr/bin/env sh
|
|
printf '%s\n' "$*" >> "$TACHYON_SNAPSHOT_BIN_LOG"
|
|
exit 0
|
|
SH
|
|
chmod 0755 "$WORK_DIR/fake-bin"
|
|
cat >"$WORK_DIR/fake-validate" <<'SH'
|
|
#!/usr/bin/env sh
|
|
exit 0
|
|
SH
|
|
chmod 0755 "$WORK_DIR/fake-validate"
|
|
cat >"$WORK_DIR/fake-validate-fail" <<'SH'
|
|
#!/usr/bin/env sh
|
|
exit 1
|
|
SH
|
|
chmod 0755 "$WORK_DIR/fake-validate-fail"
|
|
|
|
: >"$WORK_DIR/bin.log"
|
|
|
|
run_uc() {
|
|
local mode="$1"
|
|
shift
|
|
TACHYON_LIB="$WORK_DIR/lib" \
|
|
TACHYON_BIN="$WORK_DIR/fake-bin" \
|
|
TACHYON_CONFIG_NAME="tachyon" \
|
|
TACHYON_CONFIG_PATH="$WORK_DIR/config/tachyon" \
|
|
TACHYON_PERSISTENT_DIR="$WORK_DIR/tachyon" \
|
|
TACHYON_CONFIG_ROOT="$WORK_DIR" \
|
|
TACHYON_SNAPSHOTS_DIR="$WORK_DIR/snapshots" \
|
|
TACHYON_SNAPSHOT_LIMIT="${SNAPSHOT_LIMIT:-10}" \
|
|
TACHYON_SNAPSHOT_VALIDATE_BIN="${VALIDATE_BIN:-$WORK_DIR/fake-validate}" \
|
|
TACHYON_SNAPSHOT_BIN_LOG="$WORK_DIR/bin.log" \
|
|
ucode -L "$WORK_DIR/lib" -L "$TACHYON_LIB" "$SNAPSHOT_UC" "$mode" "$@"
|
|
}
|
|
|
|
# ---- snapshot-save: creates a tar.gz with the Telegram backup member layout
|
|
|
|
run_uc snapshot-save good >"$WORK_DIR/result.json"
|
|
grep -Fq '"success": true' "$WORK_DIR/result.json" ||
|
|
fail "snapshot-save must report success"
|
|
SAVED_FILE="$(sed -n 's/.*"file": "\([^"]*\)".*/\1/p' "$WORK_DIR/result.json")"
|
|
[ -n "$SAVED_FILE" ] || fail "snapshot-save must report the created file"
|
|
[[ "$SAVED_FILE" =~ ^[A-Za-z0-9_\-]+-[0-9]{8}-[0-9]{6}\.tar\.gz$ ]] ||
|
|
fail "snapshot file name must be <name>-<stamp>.tar.gz: $SAVED_FILE"
|
|
[ -f "$WORK_DIR/snapshots/$SAVED_FILE" ] ||
|
|
fail "snapshot archive must exist on disk"
|
|
# busybox-based images may lack stat(1); ucode's fs module is always present.
|
|
[ "$(ucode -e "let fs = require('fs'); printf('%o', fs.stat('$WORK_DIR/snapshots/$SAVED_FILE').mode & 511);")" = "600" ] ||
|
|
fail "snapshot archive must be chmod 600"
|
|
grep -Fxq 'config/tachyon' <(tar -tzf "$WORK_DIR/snapshots/$SAVED_FILE") ||
|
|
fail "snapshot must contain config/tachyon member"
|
|
grep -Fxq 'tachyon/data' <(tar -tzf "$WORK_DIR/snapshots/$SAVED_FILE") ||
|
|
fail "snapshot must contain tachyon/data member"
|
|
|
|
# ---- snapshot-save with a second snapshot and list ordering
|
|
|
|
run_uc snapshot-save good >/dev/null
|
|
run_uc snapshot-list >"$WORK_DIR/result.json"
|
|
grep -Fq '"success": true' "$WORK_DIR/result.json" ||
|
|
fail "snapshot-list must report success"
|
|
grep -Fq '"size": ' "$WORK_DIR/result.json" ||
|
|
fail "snapshot-list must include the archive size"
|
|
grep -Fq 'good-' "$WORK_DIR/result.json" ||
|
|
fail "snapshot-list must include saved snapshot names"
|
|
|
|
# ---- rotation: only SNAPSHOT_LIMIT archives are kept
|
|
|
|
SNAPSHOT_LIMIT=2 run_uc snapshot-save a >/dev/null
|
|
sleep 1
|
|
SNAPSHOT_LIMIT=2 run_uc snapshot-save b >/dev/null
|
|
sleep 1
|
|
SNAPSHOT_LIMIT=2 run_uc snapshot-save c >/dev/null
|
|
run_uc snapshot-list >"$WORK_DIR/result.json"
|
|
grep -Fq 'c-' "$WORK_DIR/result.json" || fail "rotation must keep the newest snapshot"
|
|
grep -Fq 'b-' "$WORK_DIR/result.json" || fail "rotation must keep the second newest snapshot"
|
|
if grep -Fq 'a-' "$WORK_DIR/result.json"; then
|
|
fail "rotation must remove the oldest snapshot"
|
|
fi
|
|
|
|
# ---- snapshot-restore: replaces config and data, chmod 600, restarts the service
|
|
|
|
run_uc snapshot-save good >"$WORK_DIR/result.json"
|
|
SAVED_FILE="$(sed -n 's/.*"file": "\([^"]*\)".*/\1/p' "$WORK_DIR/result.json")"
|
|
printf '%s\n' \
|
|
"config settings 'settings'" \
|
|
" option dns_type 'udp'" \
|
|
" option dns '1.1.1.1'" >"$WORK_DIR/config/tachyon"
|
|
printf '%s\n' 'changed-data' >"$WORK_DIR/tachyon/data"
|
|
run_uc snapshot-restore "$SAVED_FILE" >"$WORK_DIR/result.json"
|
|
grep -Fq '"success": true' "$WORK_DIR/result.json" ||
|
|
fail "snapshot-restore must report success"
|
|
for _ in $(seq 1 30); do
|
|
grep -Fxq 'restart' "$WORK_DIR/bin.log" 2>/dev/null && break
|
|
sleep 0.1
|
|
done
|
|
grep -Fxq 'restart' "$WORK_DIR/bin.log" ||
|
|
fail "snapshot-restore must restart the service through the backend entrypoint"
|
|
grep -Fq "option dns '8.8.8.8'" "$WORK_DIR/config/tachyon" ||
|
|
fail "snapshot-restore must replace the config with the snapshot content"
|
|
[ "$(ucode -e "let fs = require('fs'); printf('%o', fs.stat('$WORK_DIR/config/tachyon').mode & 511);")" = "600" ] ||
|
|
fail "restored config must be chmod 600"
|
|
[ "$(cat "$WORK_DIR/tachyon/data")" = 'snap-data' ] ||
|
|
fail "snapshot-restore must restore the persistent data"
|
|
|
|
# ---- restore rollback when validation fails
|
|
|
|
printf '%s\n' \
|
|
"config settings 'settings'" \
|
|
" option dns_type 'udp'" \
|
|
" option dns '9.9.9.9'" >"$WORK_DIR/config/tachyon"
|
|
VALIDATE_BIN="$WORK_DIR/fake-validate-fail" run_uc snapshot-restore "$SAVED_FILE" >"$WORK_DIR/result.json" &&
|
|
fail "snapshot-restore must fail when validation fails"
|
|
grep -Fq '"success": false' "$WORK_DIR/result.json" ||
|
|
fail "failed restore must report failure"
|
|
grep -Fq 'rolled back' "$WORK_DIR/result.json" ||
|
|
fail "failed restore must report the rollback"
|
|
grep -Fq "option dns '9.9.9.9'" "$WORK_DIR/config/tachyon" ||
|
|
fail "failed restore must keep the current config untouched"
|
|
[ "$(cat "$WORK_DIR/tachyon/data")" = 'snap-data' ] ||
|
|
fail "failed restore must not touch the persistent data"
|
|
|
|
# ---- unsafe archives are rejected
|
|
|
|
mkdir -p "$WORK_DIR/evil"
|
|
printf '%s\n' 'x' >"$WORK_DIR/evil/x"
|
|
tar -czf "$WORK_DIR/snapshots/evil-20240101-010101.tar.gz" -C "$WORK_DIR" evil/x
|
|
run_uc snapshot-restore "evil-20240101-010101.tar.gz" >"$WORK_DIR/result.json" &&
|
|
fail "snapshot-restore must reject archives with unexpected members"
|
|
grep -Fq '"success": false' "$WORK_DIR/result.json" || fail "unsafe archive must fail"
|
|
grep -Fq 'unsafe archive' "$WORK_DIR/result.json" ||
|
|
fail "unsafe archive failure must name the reason"
|
|
|
|
# ---- data-only snapshots restore data without touching the config
|
|
|
|
mv "$WORK_DIR/tachyon" "$WORK_DIR/tachyon.hold"
|
|
mkdir -p "$WORK_DIR/tachyon"
|
|
printf '%s\n' 'only-data' >"$WORK_DIR/tachyon/only"
|
|
tar -czf "$WORK_DIR/snapshots/only-20240101-010101.tar.gz" -C "$WORK_DIR" tachyon
|
|
rm -rf "$WORK_DIR/tachyon"
|
|
mv "$WORK_DIR/tachyon.hold" "$WORK_DIR/tachyon"
|
|
run_uc snapshot-restore "only-20240101-010101.tar.gz" >"$WORK_DIR/result.json"
|
|
grep -Fq '"success": true' "$WORK_DIR/result.json" ||
|
|
fail "data-only snapshot restore must report success"
|
|
[ "$(cat "$WORK_DIR/tachyon/only")" = 'only-data' ] ||
|
|
fail "data-only snapshot must restore the persistent data"
|
|
grep -Fq "option dns '9.9.9.9'" "$WORK_DIR/config/tachyon" ||
|
|
fail "data-only snapshot must not touch the config"
|
|
|
|
# ---- snapshot-delete
|
|
|
|
run_uc snapshot-delete "only-20240101-010101.tar.gz" >"$WORK_DIR/result.json"
|
|
grep -Fq '"success": true' "$WORK_DIR/result.json" ||
|
|
fail "snapshot-delete must report success"
|
|
[ ! -e "$WORK_DIR/snapshots/only-20240101-010101.tar.gz" ] ||
|
|
fail "snapshot-delete must remove the archive"
|
|
run_uc snapshot-delete "only-20240101-010101.tar.gz" >"$WORK_DIR/result.json" &&
|
|
fail "snapshot-delete must fail for a missing snapshot"
|
|
grep -Fq 'snapshot not found' "$WORK_DIR/result.json" ||
|
|
fail "missing snapshot delete must say snapshot not found"
|
|
run_uc snapshot-delete "evil" >"$WORK_DIR/result.json" &&
|
|
fail "snapshot-delete must reject invalid file names"
|
|
grep -Fq 'invalid snapshot name' "$WORK_DIR/result.json" ||
|
|
fail "invalid file name delete must be rejected"
|
|
|
|
# ---- argument validation
|
|
|
|
run_uc snapshot-save >"$WORK_DIR/result.json" && fail "snapshot-save without a name must fail"
|
|
grep -Fq 'snapshot name required' "$WORK_DIR/result.json" ||
|
|
fail "snapshot-save without a name must explain itself"
|
|
run_uc snapshot-save "./" >"$WORK_DIR/result.json" && fail "unsafe name must be rejected"
|
|
grep -Fq 'invalid snapshot name' "$WORK_DIR/result.json" ||
|
|
fail "unsafe name must be rejected as invalid"
|
|
run_uc snapshot-restore "evil" >"$WORK_DIR/result.json" && fail "restore must reject invalid files"
|
|
grep -Fq 'invalid snapshot name' "$WORK_DIR/result.json" ||
|
|
fail "restore of an invalid file name must be rejected"
|
|
|
|
printf 'snapshot contract checks passed\n'
|