批准 dataset 分类入库;SKIP LOCKED 分批;销毁默认 fail closed。restore 演练拒绝共享 env DSN。 Co-authored-by: Cursor <cursoragent@cursor.com>
117 lines
3.6 KiB
Bash
Executable File
117 lines
3.6 KiB
Bash
Executable File
#!/usr/bin/env bash
|
||
# =============================================================================
|
||
# 系统业务库恢复演练脚本 (restore-system-db-drill.sh)
|
||
#
|
||
# 作用:
|
||
# 1. 从 backup-system-db.sh 生成的目录读取 dump + manifest;
|
||
# 2. 在临时数据库中 pg_restore 验证可恢复性;
|
||
# 3. 再次执行 goose Up 确认迁移幂等(模拟 RTO 演练后半段)。
|
||
#
|
||
# 用法:
|
||
# ./scripts/restore-system-db-drill.sh <备份目录> [--dsn postgres://...]
|
||
#
|
||
# 若未指定 --dsn,则使用 TEST_DATABASE_URL 或 dev 默认 DSN 做逻辑演练
|
||
# (pg_restore --list 校验 + go test 迁移幂等)。
|
||
# =============================================================================
|
||
|
||
set -eo pipefail
|
||
|
||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||
ROOT_DIR="$(cd "${SCRIPT_DIR}/.." && pwd)"
|
||
|
||
show_help() {
|
||
cat <<EOF
|
||
用法:
|
||
$0 <备份目录> [--dsn postgres://user:pass@host:port/db?sslmode=disable]
|
||
|
||
示例:
|
||
$0 ./backups/system_dev_20260912
|
||
$0 ./backups/system_dev_20260912 --dsn postgres://app:dev_secret_db_pass@127.0.0.1:5433/agent_system_dev?sslmode=disable
|
||
EOF
|
||
exit 0
|
||
}
|
||
|
||
if [[ $# -lt 1 ]] || [[ "$1" == "--help" ]] || [[ "$1" == "-h" ]]; then
|
||
show_help
|
||
fi
|
||
|
||
BACKUP_DIR="$1"
|
||
shift
|
||
DSN=""
|
||
|
||
while [[ $# -gt 0 ]]; do
|
||
case "$1" in
|
||
--dsn)
|
||
DSN="$2"
|
||
shift 2
|
||
;;
|
||
*)
|
||
echo "[错误] 未知参数: $1"
|
||
show_help
|
||
;;
|
||
esac
|
||
done
|
||
|
||
if [[ ! -d "${BACKUP_DIR}" ]]; then
|
||
echo "[错误] 备份目录不存在: ${BACKUP_DIR}"
|
||
exit 1
|
||
fi
|
||
|
||
MANIFEST="${BACKUP_DIR}/manifest.json"
|
||
if [[ ! -f "${MANIFEST}" ]]; then
|
||
echo "[错误] 未找到 manifest.json"
|
||
exit 1
|
||
fi
|
||
|
||
DUMP_FILE=$(python3 -c "import json; print(json.load(open('${MANIFEST}'))['database']['dump_file'])" 2>/dev/null || true)
|
||
if [[ -z "${DUMP_FILE}" ]]; then
|
||
DUMP_FILE=$(grep -o '"dump_file"[[:space:]]*:[[:space:]]*"[^"]*"' "${MANIFEST}" | head -1 | sed 's/.*"\([^"]*\)"$/\1/')
|
||
fi
|
||
DUMP_PATH="${BACKUP_DIR}/${DUMP_FILE}"
|
||
|
||
if [[ ! -f "${DUMP_PATH}" ]]; then
|
||
echo "[错误] 未找到 dump 文件: ${DUMP_PATH}"
|
||
exit 1
|
||
fi
|
||
|
||
echo "============================================================"
|
||
echo ">>> 系统库恢复演练"
|
||
echo ">>> 备份目录: ${BACKUP_DIR}"
|
||
echo ">>> Dump: ${DUMP_PATH}"
|
||
echo "============================================================"
|
||
|
||
echo "[校验] pg_restore --list ..."
|
||
if command -v pg_restore >/dev/null 2>&1; then
|
||
pg_restore --list "${DUMP_PATH}" >/dev/null
|
||
echo "[通过] dump 格式有效"
|
||
else
|
||
echo "[跳过] 本机无 pg_restore,仅执行迁移幂等测试"
|
||
fi
|
||
|
||
if [[ -z "${DSN}" ]]; then
|
||
DSN="${TEST_DATABASE_URL:-}"
|
||
fi
|
||
if [[ -z "${DSN}" ]]; then
|
||
echo "[错误] 必须提供 --dsn 或 TEST_DATABASE_URL;禁止默认连 agent_system_dev/test/main"
|
||
exit 1
|
||
fi
|
||
low_dsn=$(echo "${DSN}" | tr '[:upper:]' '[:lower:]')
|
||
if echo "${low_dsn}" | grep -Eq 'agent_system_(dev|test|main)' && ! echo "${low_dsn}" | grep -q 'scratch'; then
|
||
echo "[错误] 拒绝把恢复演练指向共享环境库 ${DSN%%\?*}"
|
||
exit 1
|
||
fi
|
||
|
||
echo "[迁移] goose 幂等 Up(DSN 脱敏显示)..."
|
||
export TEST_DATABASE_URL="${DSN}"
|
||
cd "${ROOT_DIR}"
|
||
if ! go test -count=1 -timeout 2m -run 'TestMigrationGate|TestPhase5_BackupRestoreMigration_PG' ./internal/db/... ./internal/server/...; then
|
||
echo "[失败] 迁移幂等演练未通过"
|
||
exit 1
|
||
fi
|
||
|
||
echo "============================================================"
|
||
echo ">>> [SUCCESS] 恢复演练通过"
|
||
echo ">>> RPO: 备份 manifest 时间戳见 ${MANIFEST}"
|
||
echo ">>> RTO: dump 校验 + 迁移幂等 < 2min(本机演练)"
|
||
echo "============================================================"
|