games-development-ai/deploy/verify-mysql-dump-restore.sh

131 lines
6.0 KiB
Bash
Executable File
Raw Permalink 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
# 在无网络临时 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"