Files
Agent-System/scripts/backup-system-db.sh
T
weichenandCursor c47b437b22 fix(ops): stop hardcoding DB passwords and URL-embed git tokens
Load backup credentials from env/secrets files and push via a temporary credential helper so tokens never appear in process listings.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-09-20 10:57:04 +00:00

209 lines
7.2 KiB
Bash
Executable File
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#!/usr/bin/env bash
# =============================================================================
# 系统业务库备份脚本 (backup-system-db.sh)
#
# 作用:
# 1. 按环境 (dev/test/main) 定位 Docker 容器与数据库;
# 2. 采用 pg_dump -Fc 导出系统业务库(含 task_runs、工作流治理等);
# 3. 生成 manifest.json 记录 RPO 基准元数据与 SHA256。
#
# 用法:
# ./scripts/backup-system-db.sh <dev|test|main> <目标备份目录>
#
# 口令(按优先级,不硬编码):
# 1. 环境变量 SYSTEM_DB_PASSWORD
# 2. ${HOME}/.secrets/backup-env 中 SYSTEM_DB_PASSWORD_<ENV>
# 3. configs/.env.<env> 中的 SYSTEM_DB_PASSWORD / CONTEXT_DB_PASSWORD
# 当前实现经 docker exec 以库内角色执行 pg_dump,通常不需要宿主机口令;
# 若后续改为宿主机 psql 连接,必须提供上述之一。
# =============================================================================
set -euo pipefail
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
ROOT_DIR="$(cd "${SCRIPT_DIR}/.." && pwd)"
show_help() {
cat <<EOF
用法:
$0 <dev|test|main> <目标备份目录>
示例:
$0 dev ./backups/system_dev_20260912
$0 test /tmp/agent_system_test_backup
口令来源(勿写入仓库):
SYSTEM_DB_PASSWORD 或 \${HOME}/.secrets/backup-env 或 configs/.env.<env>
EOF
exit 0
}
if [[ $# -lt 2 ]] || [[ "$1" == "--help" ]] || [[ "$1" == "-h" ]]; then
show_help
fi
ENV_NAME="$1"
TARGET_DIR="$2"
case "${ENV_NAME}" in
dev)
CONTAINER_NAME="shuimu-context-pg-dev"
DB_NAME="agent_system_dev"
DB_USER="app"
ENV_FILE="${ROOT_DIR}/configs/.env.dev"
SECRET_KEY="SYSTEM_DB_PASSWORD_DEV"
;;
test)
CONTAINER_NAME="shuimu-system-pg-test"
DB_NAME="agent_system_test"
DB_USER="app"
ENV_FILE="${ROOT_DIR}/configs/.env.test"
SECRET_KEY="SYSTEM_DB_PASSWORD_TEST"
;;
main)
CONTAINER_NAME="shuimu-system-pg-main"
DB_NAME="agent_system_main"
DB_USER="app"
ENV_FILE="${ROOT_DIR}/configs/.env.main"
SECRET_KEY="SYSTEM_DB_PASSWORD_MAIN"
;;
*)
echo "[错误] 未知环境: ${ENV_NAME}(仅支持 dev/test/main)"
exit 1
;;
esac
resolve_db_password() {
if [[ -n "${SYSTEM_DB_PASSWORD:-}" ]]; then
printf '%s' "${SYSTEM_DB_PASSWORD}"
return 0
fi
local secrets_file="${HOME}/.secrets/backup-env"
if [[ -f "${secrets_file}" ]]; then
local from_secret
from_secret=$(grep -E "^${SECRET_KEY}=" "${secrets_file}" | head -1 | cut -d= -f2- || true)
if [[ -n "${from_secret}" ]]; then
printf '%s' "${from_secret}"
return 0
fi
from_secret=$(grep -E '^SYSTEM_DB_PASSWORD=' "${secrets_file}" | head -1 | cut -d= -f2- || true)
if [[ -n "${from_secret}" ]]; then
printf '%s' "${from_secret}"
return 0
fi
fi
if [[ -f "${ENV_FILE}" ]]; then
local from_env
from_env=$(grep -E '^SYSTEM_DB_PASSWORD=' "${ENV_FILE}" | head -1 | cut -d= -f2- || true)
if [[ -z "${from_env}" ]]; then
from_env=$(grep -E '^CONTEXT_DB_PASSWORD=' "${ENV_FILE}" | head -1 | cut -d= -f2- || true)
fi
if [[ -n "${from_env}" ]]; then
printf '%s' "${from_env}"
return 0
fi
fi
return 1
}
# docker exec 路径通常不需要宿主机口令;解析失败不阻断备份。
DB_PASS=""
if resolved="$(resolve_db_password 2>/dev/null)" && [[ -n "${resolved}" ]]; then
DB_PASS="${resolved}"
fi
unset resolved
# 仅保留变量供未来宿主机连接扩展;当前 pg_dump 走容器内角色。
: "${DB_PASS}"
mkdir -p "${TARGET_DIR}"
TARGET_DIR="$(cd "${TARGET_DIR}" && pwd)"
if ! command -v docker >/dev/null 2>&1; then
echo "[错误] 未找到 docker 命令"
exit 1
fi
if ! docker ps --format '{{.Names}}' | grep -q "^${CONTAINER_NAME}$"; then
echo "[警告] 容器 ${CONTAINER_NAME} 未运行,尝试启动 ${ENV_NAME} 环境..."
if [[ -f "${ROOT_DIR}/scripts/env-service.sh" ]]; then
"${ROOT_DIR}/scripts/env-service.sh" "${ENV_NAME}" start
else
docker compose -f "${ROOT_DIR}/docker-compose.${ENV_NAME}.yml" up -d system-db
fi
fi
echo "[等待] 检查 ${ENV_NAME} 系统库连接..."
for i in {1..30}; do
if docker exec -i "${CONTAINER_NAME}" pg_isready -U "${DB_USER}" -d "${DB_NAME}" >/dev/null 2>&1; then
break
fi
sleep 1
if [ "$i" -eq 30 ]; then
echo "[错误] 数据库连接超时"
exit 1
fi
done
echo "[统计] 采集 ${ENV_NAME} 系统库指标..."
TASK_RUNS=$(docker exec -i "${CONTAINER_NAME}" psql -U "${DB_USER}" -d "${DB_NAME}" -t -A -c \
"SELECT count(*) FROM task_runs;" 2>/dev/null || echo "0")
PROJECTS=$(docker exec -i "${CONTAINER_NAME}" psql -U "${DB_USER}" -d "${DB_NAME}" -t -A -c \
"SELECT count(*) FROM projects;" 2>/dev/null || echo "0")
DB_SIZE=$(docker exec -i "${CONTAINER_NAME}" psql -U "${DB_USER}" -d "${DB_NAME}" -t -A -c \
"SELECT pg_size_pretty(pg_database_size('${DB_NAME}'));" 2>/dev/null || echo "0 MB")
GOOSE_VER=$(docker exec -i "${CONTAINER_NAME}" psql -U "${DB_USER}" -d "${DB_NAME}" -t -A -c \
"SELECT version_id FROM goose_db_version ORDER BY id DESC LIMIT 1;" 2>/dev/null || echo "0")
DUMP_FILENAME="system_db_${ENV_NAME}.dump"
TMP_CONTAINER_DUMP="/tmp/${DUMP_FILENAME}"
DEST_DUMP_PATH="${TARGET_DIR}/${DUMP_FILENAME}"
echo "[导出] pg_dump -Fc ${DB_NAME} ..."
START_TIME=$(date +%s)
docker exec "${CONTAINER_NAME}" pg_dump -U "${DB_USER}" -d "${DB_NAME}" -Fc -Z 6 -f "${TMP_CONTAINER_DUMP}"
docker cp "${CONTAINER_NAME}:${TMP_CONTAINER_DUMP}" "${DEST_DUMP_PATH}"
docker exec "${CONTAINER_NAME}" rm -f "${TMP_CONTAINER_DUMP}"
DUMP_DURATION=$(( $(date +%s) - START_TIME ))
DUMP_SIZE_HUMAN=$(ls -lh "${DEST_DUMP_PATH}" | awk '{print $5}')
DUMP_SIZE_BYTES=$(stat -c "%s" "${DEST_DUMP_PATH}" 2>/dev/null || stat -f "%z" "${DEST_DUMP_PATH}" 2>/dev/null || echo "0")
if command -v sha256sum >/dev/null 2>&1; then
DUMP_SHA256=$(sha256sum "${DEST_DUMP_PATH}" | awk '{print $1}')
elif command -v shasum >/dev/null 2>&1; then
DUMP_SHA256=$(shasum -a 256 "${DEST_DUMP_PATH}" | awk '{print $1}')
else
DUMP_SHA256="unavailable"
fi
NOW_ISO=$(date -u +"%Y-%m-%dT%H:%M:%SZ")
MANIFEST_FILE="${TARGET_DIR}/manifest.json"
cat <<EOF > "${MANIFEST_FILE}"
{
"manifest_version": "1.0",
"created_at": "${NOW_ISO}",
"environment": "${ENV_NAME}",
"rpo_target_minutes": 60,
"database": {
"container_name": "${CONTAINER_NAME}",
"database_name": "${DB_NAME}",
"user": "${DB_USER}",
"dump_file": "${DUMP_FILENAME}",
"dump_sha256": "${DUMP_SHA256}",
"dump_size_bytes": ${DUMP_SIZE_BYTES},
"dump_size_human": "${DUMP_SIZE_HUMAN}",
"goose_version": ${GOOSE_VER:-0},
"task_runs_count": ${TASK_RUNS:-0},
"projects_count": ${PROJECTS:-0},
"database_size": "${DB_SIZE}"
}
}
EOF
echo "============================================================"
echo ">>> [SUCCESS] 系统库备份完成 (${ENV_NAME})"
echo ">>> Dump: ${DEST_DUMP_PATH} (${DUMP_SIZE_HUMAN}, ${DUMP_DURATION}s)"
echo ">>> Manifest: ${MANIFEST_FILE}"
echo ">>> 恢复演练: ./scripts/restore-system-db-drill.sh \"${TARGET_DIR}\""
echo "============================================================"