131 lines
6.0 KiB
Bash
Executable File
131 lines
6.0 KiB
Bash
Executable File
#!/usr/bin/env bash
|
||
# 在无网络临时 MySQL 容器中恢复 staging dump,并与存储审计逐表对账。
|
||
# 本脚本永不连接或回灌原 staging 数据库;任何失败都销毁临时容器并阻止激活。
|
||
set -euo pipefail
|
||
|
||
DUMP_FILE="${DUMP_FILE:-}"
|
||
EXPECTED_AUDIT_FILE="${EXPECTED_AUDIT_FILE:-}"
|
||
MYSQL_IMAGE="${MYSQL_IMAGE:-mysql:8.4.8}"
|
||
DOCKER_BIN="${DOCKER_BIN:-docker}"
|
||
RESTORE_VERIFY_TIMEOUT_SEC="${RESTORE_VERIFY_TIMEOUT_SEC:-120}"
|
||
RESTORE_VERIFY_DRY_RUN="${RESTORE_VERIFY_DRY_RUN:-0}"
|
||
VERIFY_CONTAINER_NAME="${VERIFY_CONTAINER_NAME:-gda-mysql-restore-verify-$$}"
|
||
|
||
if [ -z "$DUMP_FILE" ] || [ ! -s "$DUMP_FILE" ]; then
|
||
printf 'RESTORE_VERIFY_FAIL DUMP_FILE 不存在或为空。\n' >&2
|
||
exit 2
|
||
fi
|
||
if [ -z "$EXPECTED_AUDIT_FILE" ] || [ ! -s "$EXPECTED_AUDIT_FILE" ]; then
|
||
printf 'RESTORE_VERIFY_FAIL EXPECTED_AUDIT_FILE 不存在或为空。\n' >&2
|
||
exit 2
|
||
fi
|
||
if [[ ! "$RESTORE_VERIFY_TIMEOUT_SEC" =~ ^[0-9]+$ ]] || [ "$RESTORE_VERIFY_TIMEOUT_SEC" -lt 10 ]; then
|
||
printf 'RESTORE_VERIFY_FAIL timeout 必须是至少 10 秒的整数。\n' >&2
|
||
exit 2
|
||
fi
|
||
gzip -t "$DUMP_FILE"
|
||
|
||
value_of() {
|
||
local key="$1" file="$2"
|
||
awk -F= -v wanted="$key" '$1 == wanted {print substr($0, length($1) + 2); found=1} END {exit !found}' "$file"
|
||
}
|
||
|
||
compare_count() {
|
||
local key="$1" actual_file="$2" expected actual
|
||
expected="$(value_of "$key" "$EXPECTED_AUDIT_FILE")" || {
|
||
printf 'RESTORE_VERIFY_FAIL 审计缺字段:%s\n' "$key" >&2
|
||
exit 3
|
||
}
|
||
actual="$(value_of "$key" "$actual_file")" || {
|
||
printf 'RESTORE_VERIFY_FAIL 恢复库缺字段:%s\n' "$key" >&2
|
||
exit 3
|
||
}
|
||
[ "$expected" = "$actual" ] || {
|
||
printf 'RESTORE_VERIFY_FAIL 行数不一致 %s expected=%s actual=%s\n' "$key" "$expected" "$actual" >&2
|
||
exit 3
|
||
}
|
||
}
|
||
|
||
if [ "$RESTORE_VERIFY_DRY_RUN" = "1" ]; then
|
||
# dry-run 只验证输入可读和审计契约,绝不签发真实恢复通过。
|
||
[ "$(value_of schema "$EXPECTED_AUDIT_FILE")" = "staging-storage-audit/1" ] || {
|
||
printf 'RESTORE_VERIFY_FAIL dry-run 审计 schema 非法。\n' >&2
|
||
exit 3
|
||
}
|
||
[ "$(value_of object_storage_status "$EXPECTED_AUDIT_FILE")" = "DB_ONLY_NA" ] || {
|
||
printf 'RESTORE_VERIFY_FAIL dry-run 只接受 DB_ONLY_NA 审计。\n' >&2
|
||
exit 3
|
||
}
|
||
for key in game_version_rows game_version_package_url_nonempty \
|
||
game_runtime_package_rows game_runtime_package_url_nonempty game_runtime_package_payload_rows \
|
||
game_source_project_rows game_source_project_url_nonempty game_source_project_payload_rows \
|
||
game_aigc_task_rows game_aigc_result_package_url_nonempty infra_file_rows; do
|
||
value="$(value_of "$key" "$EXPECTED_AUDIT_FILE")" || {
|
||
printf 'RESTORE_VERIFY_FAIL dry-run 审计缺字段:%s\n' "$key" >&2
|
||
exit 3
|
||
}
|
||
[[ "$value" =~ ^[0-9]+$ ]] || exit 3
|
||
done
|
||
printf 'RESTORE_VERIFY_DRY_RUN_PASS(未启动容器,不可作为激活证据)\n'
|
||
exit 0
|
||
fi
|
||
|
||
password="$(od -An -N24 -tx1 /dev/urandom | tr -d ' \n')"
|
||
actual_file="$(mktemp)"
|
||
container_started=0
|
||
cleanup() {
|
||
rm -f "$actual_file"
|
||
if [ "$container_started" -eq 1 ]; then
|
||
"$DOCKER_BIN" stop -t 1 "$VERIFY_CONTAINER_NAME" >/dev/null 2>&1 || true
|
||
fi
|
||
}
|
||
trap cleanup EXIT
|
||
|
||
"$DOCKER_BIN" run -d --rm --network none --name "$VERIFY_CONTAINER_NAME" \
|
||
-e "MYSQL_ROOT_PASSWORD=$password" "$MYSQL_IMAGE" >/dev/null
|
||
container_started=1
|
||
|
||
deadline=$((SECONDS + RESTORE_VERIFY_TIMEOUT_SEC))
|
||
# `mysqladmin ping` 在鉴权失败时也可能返回 0,只能证明端口活,不能证明官方镜像已完成 root 口令初始化。
|
||
# 用真实鉴权查询作为就绪门,避免在临时初始化服务器阶段过早灌入 dump。
|
||
until "$DOCKER_BIN" exec -e "MYSQL_PWD=$password" "$VERIFY_CONTAINER_NAME" \
|
||
mysql -uroot -N -B -e 'SELECT 1' >/dev/null 2>&1; do
|
||
[ "$SECONDS" -lt "$deadline" ] || {
|
||
printf 'RESTORE_VERIFY_FAIL 临时 MySQL 未在时限内就绪。\n' >&2
|
||
exit 4
|
||
}
|
||
sleep 1
|
||
done
|
||
|
||
gzip -dc "$DUMP_FILE" | "$DOCKER_BIN" exec -i -e "MYSQL_PWD=$password" \
|
||
"$VERIFY_CONTAINER_NAME" mysql -uroot
|
||
|
||
"$DOCKER_BIN" exec -i -e "MYSQL_PWD=$password" "$VERIFY_CONTAINER_NAME" \
|
||
mysql -uroot -N -B 'ruoyi-vue-pro' >"$actual_file" <<'SQL'
|
||
SELECT CONCAT('flyway_failed_rows=', COUNT(*)) FROM flyway_schema_history WHERE success<>1;
|
||
SELECT CONCAT('game_version_rows=', COUNT(*)) FROM game_version WHERE deleted=0;
|
||
SELECT CONCAT('game_version_package_url_nonempty=', COALESCE(SUM(package_url<>''),0)) FROM game_version WHERE deleted=0;
|
||
SELECT CONCAT('game_runtime_package_rows=', COUNT(*)) FROM game_runtime_package WHERE deleted=0;
|
||
SELECT CONCAT('game_runtime_package_url_nonempty=', COALESCE(SUM(package_url<>''),0)) FROM game_runtime_package WHERE deleted=0;
|
||
SELECT CONCAT('game_runtime_package_payload_rows=', COALESCE(SUM(package_json IS NOT NULL AND CHAR_LENGTH(package_json)>0),0)) FROM game_runtime_package WHERE deleted=0;
|
||
SELECT CONCAT('game_source_project_rows=', COUNT(*)) FROM game_source_project WHERE deleted=0;
|
||
SELECT CONCAT('game_source_project_url_nonempty=', COALESCE(SUM(source_url<>''),0)) FROM game_source_project WHERE deleted=0;
|
||
SELECT CONCAT('game_source_project_payload_rows=', COALESCE(SUM(source_json IS NOT NULL AND CHAR_LENGTH(source_json)>0),0)) FROM game_source_project WHERE deleted=0;
|
||
SELECT CONCAT('game_aigc_task_rows=', COUNT(*)) FROM game_aigc_task WHERE deleted=0;
|
||
SELECT CONCAT('game_aigc_result_package_url_nonempty=', COALESCE(SUM(result_package_url<>''),0)) FROM game_aigc_task WHERE deleted=0;
|
||
SELECT CONCAT('infra_file_rows=', COUNT(*)) FROM infra_file WHERE deleted=0;
|
||
SQL
|
||
|
||
[ "$(value_of flyway_failed_rows "$actual_file")" = "0" ] || {
|
||
printf 'RESTORE_VERIFY_FAIL 恢复库存在失败的 Flyway 记录。\n' >&2
|
||
exit 5
|
||
}
|
||
for key in game_version_rows game_version_package_url_nonempty \
|
||
game_runtime_package_rows game_runtime_package_url_nonempty game_runtime_package_payload_rows \
|
||
game_source_project_rows game_source_project_url_nonempty game_source_project_payload_rows \
|
||
game_aigc_task_rows game_aigc_result_package_url_nonempty infra_file_rows; do
|
||
compare_count "$key" "$actual_file"
|
||
done
|
||
|
||
printf 'RESTORE_VERIFY_PASS container=%s(临时容器将立即销毁)\n' "$VERIFY_CONTAINER_NAME"
|