mirror of
https://github.com/xCyanGrizzly/DragonsStash.git
synced 2026-09-21 05:21:43 +00:00
220 lines
7.2 KiB
Bash
220 lines
7.2 KiB
Bash
#!/usr/bin/env bash
|
|
set -Eeuo pipefail
|
|
|
|
SCRIPT_DIR="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd -P)"
|
|
PROJECT_ROOT="$(cd -- "$SCRIPT_DIR/../.." && pwd -P)"
|
|
RESTORE_SCRIPT="$PROJECT_ROOT/scripts/backup/restore.sh"
|
|
TMP_ROOT="$(mktemp -d)"
|
|
|
|
cleanup() {
|
|
rm -rf -- "$TMP_ROOT"
|
|
}
|
|
trap cleanup EXIT
|
|
|
|
fail() {
|
|
printf 'ASSERTION FAILED: %s\n' "$*" >&2
|
|
exit 1
|
|
}
|
|
|
|
setup_fake_environment() {
|
|
local fake_bin="$TMP_ROOT/bin"
|
|
|
|
mkdir -p -- "$fake_bin" "$TMP_ROOT/mount" "$TMP_ROOT/staging"
|
|
printf 'not-secret\n' > "$TMP_ROOT/restic-password"
|
|
|
|
cat > "$fake_bin/docker" <<'EOF'
|
|
#!/usr/bin/env bash
|
|
set -Eeuo pipefail
|
|
|
|
printf '%s\t' "$@" >> "$DRAGONS_STASH_DOCKER_CALLS"
|
|
printf '\n' >> "$DRAGONS_STASH_DOCKER_CALLS"
|
|
|
|
target=""
|
|
previous=""
|
|
safety_output_host=""
|
|
safety_archive=""
|
|
for argument in "$@"; do
|
|
if [[ "$previous" == "--target" ]]; then
|
|
target="$argument"
|
|
fi
|
|
if [[ "$previous" == "-v" && "$argument" == *":/safety-output" ]]; then
|
|
safety_output_host="${argument%:/safety-output}"
|
|
fi
|
|
if [[ "$argument" == /safety-output/pre-restore-*.tar ]]; then
|
|
safety_archive="${argument#/safety-output/}"
|
|
fi
|
|
previous="$argument"
|
|
done
|
|
|
|
if [[ "${1:-}" == "compose" && "${2:-}" == "config" && "${3:-}" == "--format" && "${4:-}" == "json" ]]; then
|
|
printf '{\n "name": "dragonsstash"\n}\n'
|
|
exit 0
|
|
fi
|
|
|
|
if [[ "${1:-}" == "compose" && "${2:-}" == "--profile" && "${3:-}" == "full" && "${4:-}" == "ps" && "${5:-}" == "--status" && "${6:-}" == "running" && "${7:-}" == "-q" ]]; then
|
|
case " ${RESTORE_ASSERT_RUNNING_SERVICES:-app worker bot} " in
|
|
*" ${8:-} "*) printf '%s-container\n' "${8:-}" ;;
|
|
esac
|
|
exit 0
|
|
fi
|
|
|
|
if [[ "${1:-}" == "volume" && "${2:-}" == "ls" ]]; then
|
|
if [[ " $* " == *"com.docker.compose.volume=tdlib_state"* ]]; then
|
|
printf 'dragonsstash_tdlib_state\n'
|
|
elif [[ " $* " == *"com.docker.compose.volume=tdlib_bot_state"* ]]; then
|
|
printf 'dragonsstash_tdlib_bot_state\n'
|
|
fi
|
|
exit 0
|
|
fi
|
|
|
|
if [[ " $* " == *" exec -T db pg_dump "* ]]; then
|
|
printf 'custom dump\n'
|
|
exit 0
|
|
fi
|
|
|
|
if [[ " $* " == *" backup restore "* ]]; then
|
|
if [[ "$target" != /staging/* ]]; then
|
|
printf 'Unexpected restore target: %s\n' "$target" >&2
|
|
exit 1
|
|
fi
|
|
|
|
host_target="$BACKUP_STAGING_PATH/${target#/staging/}"
|
|
mkdir -p \
|
|
"$host_target/staging/backup-legacy/manifest" \
|
|
"$host_target/data/tdlib-worker" \
|
|
"$host_target/data/tdlib-bot"
|
|
printf 'custom dump\n' > "$host_target/staging/backup-legacy/database.dump"
|
|
printf '{}\n' > "$host_target/staging/backup-legacy/manifest/backup-manifest.json"
|
|
|
|
if [[ "${RESTORE_ASSERT_CREATE_UNEXPECTED:-0}" == "1" ]]; then
|
|
mkdir -p "$host_target/data/uploads" "$host_target/data/tmp-zips" "$host_target/data/postgres"
|
|
fi
|
|
fi
|
|
|
|
if [[ -n "$safety_output_host" && -n "$safety_archive" ]]; then
|
|
printf 'archive\n' > "$safety_output_host/$safety_archive"
|
|
fi
|
|
|
|
exit 0
|
|
EOF
|
|
chmod +x "$fake_bin/docker"
|
|
|
|
cat > "$fake_bin/mountpoint" <<'EOF'
|
|
#!/usr/bin/env bash
|
|
set -Eeuo pipefail
|
|
exit 0
|
|
EOF
|
|
chmod +x "$fake_bin/mountpoint"
|
|
|
|
cat > "$fake_bin/curl" <<'EOF'
|
|
#!/usr/bin/env bash
|
|
set -Eeuo pipefail
|
|
printf '%s\t' "$@" >> "$DRAGONS_STASH_CURL_CALLS"
|
|
printf '\n' >> "$DRAGONS_STASH_CURL_CALLS"
|
|
exit 0
|
|
EOF
|
|
chmod +x "$fake_bin/curl"
|
|
|
|
export PATH="$fake_bin:$PATH"
|
|
export BACKUP_MOUNT_PATH="$TMP_ROOT/mount"
|
|
export BACKUP_STAGING_PATH="$TMP_ROOT/staging"
|
|
export BACKUP_RESTIC_PASSWORD_FILE="$TMP_ROOT/restic-password"
|
|
export BACKUP_REPOSITORY="/backup/restic"
|
|
}
|
|
|
|
run_restore_to_staging() {
|
|
local name="$1"
|
|
shift
|
|
|
|
DRAGONS_STASH_DOCKER_CALLS="$TMP_ROOT/docker-calls-$name.log" \
|
|
"$@" "$RESTORE_SCRIPT" restore-to-staging snapshot-scope "$TMP_ROOT/staging/$name"
|
|
}
|
|
|
|
run_restore_live() {
|
|
local name="$1"
|
|
shift
|
|
|
|
DRAGONS_STASH_DOCKER_CALLS="$TMP_ROOT/docker-calls-$name.log" \
|
|
DRAGONS_STASH_CURL_CALLS="$TMP_ROOT/curl-calls-$name.log" \
|
|
"$@" "$RESTORE_SCRIPT" restore-live snapshot-scope --confirm-replace-live-data
|
|
}
|
|
|
|
assert_restore_call_is_filtered() {
|
|
local calls="$1"
|
|
local restore_call
|
|
|
|
restore_call="$(grep $'backup\trestore\tsnapshot-scope' "$calls" || true)"
|
|
[[ -n "$restore_call" ]] || fail "restore command was not invoked"
|
|
[[ "$restore_call" == *$'--include\t/staging/backup-*/database.dump'* ]] || fail "database dump include filter missing"
|
|
[[ "$restore_call" == *$'--include\t/staging/backup-*/manifest\t'* ]] || fail "manifest include filter missing"
|
|
[[ "$restore_call" == *$'--include\t/staging/backup-*/manifest/**'* ]] || fail "manifest subtree include filter missing"
|
|
[[ "$restore_call" == *$'--include\t/data/tdlib-worker\t'* ]] || fail "worker TDLib root include filter missing"
|
|
[[ "$restore_call" == *$'--include\t/data/tdlib-worker/**'* ]] || fail "worker TDLib subtree include filter missing"
|
|
[[ "$restore_call" == *$'--include\t/data/tdlib-bot\t'* ]] || fail "bot TDLib root include filter missing"
|
|
[[ "$restore_call" == *$'--include\t/data/tdlib-bot/**'* ]] || fail "bot TDLib subtree include filter missing"
|
|
[[ "$restore_call" != *"/data/uploads"* ]] || fail "restore includes uploads path"
|
|
}
|
|
|
|
assert_restore_uses_include_filters() {
|
|
local calls="$TMP_ROOT/docker-calls-safe.log"
|
|
|
|
setup_fake_environment
|
|
run_restore_to_staging safe bash >/dev/null
|
|
|
|
assert_restore_call_is_filtered "$calls"
|
|
}
|
|
|
|
assert_live_restore_uses_include_filters() {
|
|
local calls="$TMP_ROOT/docker-calls-live.log"
|
|
|
|
setup_fake_environment
|
|
run_restore_live live bash >/dev/null
|
|
|
|
assert_restore_call_is_filtered "$calls"
|
|
}
|
|
|
|
assert_live_restore_restarts_only_previously_running_services() {
|
|
local calls="$TMP_ROOT/docker-calls-live-running-subset.log"
|
|
local stop_call
|
|
local start_call
|
|
|
|
setup_fake_environment
|
|
RESTORE_ASSERT_RUNNING_SERVICES="app worker" run_restore_live live-running-subset bash >/dev/null
|
|
|
|
stop_call="$(grep $'compose\t--profile\tfull\tstop' "$calls" || true)"
|
|
start_call="$(grep $'compose\t--profile\tfull\tup\t-d' "$calls" || true)"
|
|
|
|
[[ "$stop_call" == $'compose\t--profile\tfull\tstop\tapp\tworker\t' ]] \
|
|
|| fail "restore-live stopped services other than the running app/worker subset: $stop_call"
|
|
[[ "$start_call" == $'compose\t--profile\tfull\tup\t-d\tapp\tworker\t' ]] \
|
|
|| fail "restore-live started services other than the running app/worker subset: $start_call"
|
|
}
|
|
|
|
assert_live_restore_skips_health_when_app_was_not_running() {
|
|
local calls="$TMP_ROOT/curl-calls-live-without-app.log"
|
|
|
|
setup_fake_environment
|
|
RESTORE_ASSERT_RUNNING_SERVICES="worker" run_restore_live live-without-app bash >/dev/null
|
|
|
|
[[ ! -s "$calls" ]] || fail "restore-live waited for app health even though app was not previously running"
|
|
}
|
|
|
|
assert_unexpected_volume_content_is_rejected() {
|
|
local output="$TMP_ROOT/unexpected-output.log"
|
|
|
|
setup_fake_environment
|
|
if RESTORE_ASSERT_CREATE_UNEXPECTED=1 run_restore_to_staging unexpected bash >"$output" 2>&1; then
|
|
fail "restore accepted unexpected restored data volume content"
|
|
fi
|
|
grep -Eq 'Unexpected restored data volume content|Refusing restore' "$output" \
|
|
|| fail "restore rejected unexpected content without an explicit guard message"
|
|
}
|
|
|
|
assert_restore_uses_include_filters
|
|
assert_live_restore_uses_include_filters
|
|
assert_live_restore_restarts_only_previously_running_services
|
|
assert_live_restore_skips_health_when_app_was_not_running
|
|
assert_unexpected_volume_content_is_rejected
|
|
|
|
printf 'restore-path assertions passed\n'
|