989 lines
52 KiB
Bash
Executable File
989 lines
52 KiB
Bash
Executable File
#!/usr/bin/env bash
|
||
# staging RC 脚本回归:只用假 SSH 与本地假 HTTP 服务,不连接远端、不启动 systemd。
|
||
set -euo pipefail
|
||
|
||
ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)"
|
||
ACTIVATE="$ROOT_DIR/deploy/activate-staging-rc.sh"
|
||
PREPARE="$ROOT_DIR/deploy/prepare-staging-rc.sh"
|
||
SMOKE="$ROOT_DIR/deploy/smoke-test.sh"
|
||
STORAGE_AUDIT="$ROOT_DIR/deploy/generate-staging-storage-audit.sh"
|
||
RESTORE_VERIFY="$ROOT_DIR/deploy/verify-mysql-dump-restore.sh"
|
||
GENERATION_GATE_REF="sha256:$(printf '0%.0s' $(seq 1 64)):r1/$(printf '0%.0s' $(seq 1 64)).attestation"
|
||
GENERATION_GATE_RUN_ID="r1-test-run-20260723"
|
||
GENERATION_ATTESTATION_EXPECTED_ISSUER="attacker-controlled-issuer"
|
||
GENERATION_ATTESTATION_TRUSTED_PUBLIC_KEY_FILE="/tmp/attacker-controlled-public-key.pem"
|
||
FIXED_GENERATION_ATTESTATION_ISSUER="r1-mini-infra"
|
||
FIXED_GENERATION_ATTESTATION_PUBLIC_KEY_SHA256="e3cc17cd86805b053f61450b5c2c6e4e168bb01dbf1e83840124290d1196a304"
|
||
REPOSITORY_GENERATION_ATTESTATION_PUBLIC_KEY="$ROOT_DIR/deploy/trust/r1-attestation-ed25519.pub"
|
||
TMP_DIR="$(mktemp -d)"
|
||
SERVER_PID=""
|
||
|
||
export GENERATION_GATE_RUN_ID GENERATION_ATTESTATION_EXPECTED_ISSUER
|
||
export GENERATION_ATTESTATION_TRUSTED_PUBLIC_KEY_FILE
|
||
|
||
cleanup() {
|
||
if [ -n "$SERVER_PID" ]; then
|
||
kill "$SERVER_PID" 2>/dev/null || true
|
||
wait "$SERVER_PID" 2>/dev/null || true
|
||
fi
|
||
rm -rf "$TMP_DIR"
|
||
}
|
||
trap cleanup EXIT
|
||
|
||
fail() {
|
||
printf 'TEST_FAIL %s\n' "$*" >&2
|
||
exit 1
|
||
}
|
||
|
||
# 执行 prepare 内嵌的真实 Surefire XML 校验器,覆盖全跳过拒绝与部分跳过放行边界。
|
||
test_surefire_report_guard() {
|
||
local surefire_verifier="$TMP_DIR/verify-security-java-reports.py"
|
||
local surefire_fixture="$TMP_DIR/surefire-fixture"
|
||
local surefire_report_dir="$surefire_fixture/game-cloud/security/target/surefire-reports"
|
||
|
||
awk '
|
||
/^import pathlib,sys,xml.etree.ElementTree as ET$/ { capture=1 }
|
||
capture && /^PY$/ { exit }
|
||
capture { print }
|
||
' "$PREPARE" >"$surefire_verifier"
|
||
rg -q 'surefire-reports/TEST-\*\.xml' "$surefire_verifier" || fail "无法提取 prepare 的 Surefire XML 校验器"
|
||
|
||
mkdir -p "$surefire_report_dir" "$surefire_fixture/evidence"
|
||
cat >"$surefire_report_dir/TEST-com.example.AllSkippedSecurityTest.xml" <<'XML'
|
||
<?xml version="1.0" encoding="UTF-8"?>
|
||
<testsuite name="com.example.AllSkippedSecurityTest" tests="1" failures="0" errors="0" skipped="1"/>
|
||
XML
|
||
if (cd "$surefire_fixture" && python3 "$surefire_verifier" \
|
||
"$surefire_fixture/evidence/security-java-tests-summary.tsv" AllSkippedSecurityTest) \
|
||
>"$TMP_DIR/surefire-all-skipped.log" 2>&1; then
|
||
fail "Surefire 校验器错误放行 tests=1、skipped=1 的全跳过测试类"
|
||
fi
|
||
rg -q 'executed=0' "$TMP_DIR/surefire-all-skipped.log" || fail "全跳过拒绝原因未记录实际执行数"
|
||
|
||
# 边界正例:同类存在一个 skipped,但仍至少执行一个测试时必须允许并产出摘要。
|
||
cat >"$surefire_report_dir/TEST-com.example.AllSkippedSecurityTest.xml" <<'XML'
|
||
<?xml version="1.0" encoding="UTF-8"?>
|
||
<testsuite name="com.example.AllSkippedSecurityTest" tests="2" failures="0" errors="0" skipped="1"/>
|
||
XML
|
||
(cd "$surefire_fixture" && python3 "$surefire_verifier" \
|
||
"$surefire_fixture/evidence/security-java-tests-summary.tsv" AllSkippedSecurityTest)
|
||
rg -q $'^AllSkippedSecurityTest\\t2\\t0\\t0\\t1$' \
|
||
"$surefire_fixture/evidence/security-java-tests-summary.tsv" || fail "部分跳过但有执行的测试类未生成正确摘要"
|
||
}
|
||
|
||
if [ "${STAGING_RC_TEST_CASE:-}" = "surefire-report-guard" ]; then
|
||
test_surefire_report_guard
|
||
printf 'TEST_PASS Surefire XML 全跳过拒绝与部分跳过边界通过\n'
|
||
exit 0
|
||
fi
|
||
|
||
bash -n "$ACTIVATE" "$PREPARE" "$SMOKE" "$STORAGE_AUDIT" "$RESTORE_VERIFY"
|
||
rg -q 'docker exec -i .*MYSQL_CONTAINER' "$STORAGE_AUDIT" || fail "存储审计未把 SQL stdin 接入 MySQL 容器"
|
||
|
||
# 假 SSH 只捕获参数和 stdin 中的远端脚本;不执行任何远端命令。
|
||
cat >"$TMP_DIR/fake-ssh" <<'SH'
|
||
#!/usr/bin/env bash
|
||
set -euo pipefail
|
||
printf '%s\n' "$@" >"$RC_TEST_TMP/ssh-args.txt"
|
||
cat >"$RC_TEST_TMP/remote-script.sh"
|
||
SH
|
||
chmod +x "$TMP_DIR/fake-ssh"
|
||
|
||
export RC_TEST_TMP="$TMP_DIR"
|
||
RC_COMMIT="0000000000000000000000000000000000000000" \
|
||
GENERATION_GATE_EVIDENCE_REF="$GENERATION_GATE_REF" \
|
||
BOOTSTRAP_ADMIN_TOKEN_FILE=/root/game-staging/credentials/bootstrap-admin.token \
|
||
SSH_BIN="$TMP_DIR/fake-ssh" \
|
||
bash "$ACTIVATE" >"$TMP_DIR/activate-wrapper.log"
|
||
|
||
rg -q '^ProxyCommand=none$' "$TMP_DIR/ssh-args.txt" || fail "SSH 未显式禁用代理"
|
||
rg -q '^root@100\.64\.0\.7$' "$TMP_DIR/ssh-args.txt" || fail "SSH 默认目标不是 mini-desktop 直连地址"
|
||
bash -n "$TMP_DIR/remote-script.sh"
|
||
rg -q 'pre-migration-ruoyi-vue-pro\.sql\.gz' "$TMP_DIR/remote-script.sh" || fail "缺迁移前数据库快照"
|
||
rg -q 'generate-staging-storage-audit\.sh' "$TMP_DIR/remote-script.sh" || fail "缺实时存储审计"
|
||
rg -q 'verify-mysql-dump-restore\.sh' "$TMP_DIR/remote-script.sh" || fail "缺隔离恢复验证"
|
||
rg -q 'failure_handler' "$TMP_DIR/remote-script.sh" || fail "缺失败回滚处理"
|
||
rg -q 'start_stack.*48080 4173 4174 8300 9501 true' "$TMP_DIR/remote-script.sh" || fail "缺正式五端口启动"
|
||
rg -q '^install_maintenance_gate()' "$TMP_DIR/remote-script.sh" || fail "缺正式端口维护门"
|
||
rg -q '^release_maintenance_gate()' "$TMP_DIR/remote-script.sh" || fail "缺维护门解除函数"
|
||
rg -q '^verify_stack()' "$TMP_DIR/remote-script.sh" || fail "缺五服务逐项复核函数"
|
||
rg -Fq 'INPUT 1 ! -i lo -j "$maintenance_chain"' "$TMP_DIR/remote-script.sh" || \
|
||
fail "维护门没有在 INPUT 首位封锁全部非 loopback 入口"
|
||
rg -q 'maintenance_ports="48080,4173,4174,8300,9501"' "$TMP_DIR/remote-script.sh" || \
|
||
fail "维护门未覆盖全部五个正式端口"
|
||
rg -q 'mv -Tf.*active_pointer' "$TMP_DIR/remote-script.sh" || fail "active 指针不是原子替换"
|
||
rg -Fq 'spring.cloud.nacos.discovery.enabled=${discovery_enabled}' "$TMP_DIR/remote-script.sh" || \
|
||
fail "backend launcher 没有显式控制 Nacos discovery"
|
||
rg -q 'start_candidate_probe.*14173 14174 18300 19501' "$TMP_DIR/remote-script.sh" || \
|
||
fail "候选非数据面探针未启动"
|
||
rg -q 'verify_generation_gate_evidence' "$TMP_DIR/remote-script.sh" || fail "缺生成 R1 证据校验"
|
||
! rg -q 'git .*cat-file.*expected_commit' "$TMP_DIR/remote-script.sh" || fail "R1 attestation 仍要求属于被证明 commit,存在自引用"
|
||
rg -q 'attestation_root' "$TMP_DIR/remote-script.sh" || fail "缺独立 R1 attestation 根目录"
|
||
rg -q 'openssl pkeyutl -verify' "$TMP_DIR/remote-script.sh" || fail "R1 attestation 缺 detached signature 校验"
|
||
rg -q 'expected_generation_gate_run_id' "$TMP_DIR/remote-script.sh" || fail "R1 attestation 未绑定独立 run-id"
|
||
rg -q 'expected_attestation_issuer' "$TMP_DIR/remote-script.sh" || fail "R1 attestation 未绑定 issuer"
|
||
rg -q 'trusted_attestation_public_key_file' "$TMP_DIR/remote-script.sh" || fail "R1 attestation 未绑定信任公钥"
|
||
rg -Fq 'expected_attestation_issuer="r1-mini-infra"' "$TMP_DIR/remote-script.sh" || fail "R1 issuer 未固定"
|
||
rg -Fq 'trusted_attestation_public_key_file="/root/game-staging/trust/r1-attestation-ed25519.pub"' \
|
||
"$TMP_DIR/remote-script.sh" || fail "R1 公钥路径未固定"
|
||
rg -Fq "trusted_attestation_public_key_sha256=\"$FIXED_GENERATION_ATTESTATION_PUBLIC_KEY_SHA256\"" \
|
||
"$TMP_DIR/remote-script.sh" || fail "R1 公钥摘要未固定"
|
||
! rg -q 'attacker-controlled-(issuer|public-key)' "$TMP_DIR/ssh-args.txt" || fail "R1 信任根仍由调用参数传入"
|
||
rg -q 'flock -n' "$TMP_DIR/remote-script.sh" || fail "activation 缺单实例部署锁"
|
||
rg -q 'candidate_backend=SKIPPED_NO_ISOLATED_DATA_PLANE' "$TMP_DIR/remote-script.sh" || \
|
||
fail "候选探活没有明确跳过共享数据面 backend"
|
||
rg -q 'run_flyway_migrations' "$TMP_DIR/remote-script.sh" || fail "迁移未拆成独立受控步骤"
|
||
rg -q 'restore_database_snapshot' "$TMP_DIR/remote-script.sh" || fail "迁移后失败缺数据库快照补偿"
|
||
rg -q 'FIRST_RC_FULL_ROLLBACK=UNAVAILABLE' "$TMP_DIR/remote-script.sh" || fail "首个 RC 未明确禁止宣称完整回滚"
|
||
rg -q 'manage-dogfood-reviewers\.py' "$TMP_DIR/remote-script.sh" || fail "激活未调用审核账号轮换工具"
|
||
rg -q 'provision-dogfood-with-nacos-transaction\.sh' "$TMP_DIR/remote-script.sh" || \
|
||
fail "激活未在 admin 禁用前执行账号预置事务"
|
||
rg -q 'ADMIN_TOKEN_FILE="\$bootstrap_admin_token_file"' "$TMP_DIR/remote-script.sh" || \
|
||
fail "激活账号预置未使用 bootstrap token 文件"
|
||
rg -q 'PROVISION_PASS accounts=40 creators=10 role_mismatches=0 claimed_accounts=40 duplicate_claims=0' \
|
||
"$TMP_DIR/remote-script.sh" || fail "激活未核对 40 人账号终验"
|
||
rg -q '请先运行受控 new-api import' "$TMP_DIR/remote-script.sh" || \
|
||
fail "额度池不足没有给出受控 new-api import 指引"
|
||
rg -q -- '--ack ROTATE_DOGFOOD_REVIEWERS' "$TMP_DIR/remote-script.sh" || fail "审核账号轮换 ACK 不精确"
|
||
rg -q 'reviewer-runtime-verification\.json' "$TMP_DIR/remote-script.sh" || fail "缺 reviewer/readonly 登录与旧 token 失效运行态证据"
|
||
rg -Fq 'if status != 401 or body.get("code") != 401:' "$TMP_DIR/remote-script.sh" || \
|
||
fail "bootstrap token 失效验收未严格要求 HTTP 401 且业务 code=401,非 401 可能被误判为失效"
|
||
rg -q 'rm -f.*bootstrap_admin_token_file' "$TMP_DIR/remote-script.sh" || fail "轮换成功后未删除 bootstrap token 文件"
|
||
! rg -q 'admin123' "$ACTIVATE" || fail "激活脚本写入了默认公开密码"
|
||
for required_marker in actor_prompt_eval judge_prompt_eval game_content_gold live_prompt full_gate fresh_25 historical_11 production_shadow_20 runtime_eligible; do
|
||
rg -q "$required_marker" "$TMP_DIR/remote-script.sh" || fail "生成门未核对 R1 字段:$required_marker"
|
||
done
|
||
gate_line="$(rg -n 'verify_generation_gate_evidence' "$TMP_DIR/remote-script.sh" | tail -1 | cut -d: -f1)"
|
||
snapshot_line="$(rg -n '迁移前数据库快照' "$TMP_DIR/remote-script.sh" | tail -1 | cut -d: -f1)"
|
||
[ "$gate_line" -lt "$snapshot_line" ] || fail "生成门校验没有发生在数据库快照/迁移前"
|
||
candidate_probe_line="$(rg -n '启动不接共享数据面的候选探针' "$TMP_DIR/remote-script.sh" | tail -1 | cut -d: -f1)"
|
||
maintenance_install_line="$(rg -n '^install_maintenance_gate$' "$TMP_DIR/remote-script.sh" | tail -1 | cut -d: -f1)"
|
||
stop_old_line="$(rg -n '^stop_stack .*48080 4173 4174 8300 9501$' "$TMP_DIR/remote-script.sh" | tail -1 | cut -d: -f1)"
|
||
restore_verify_line="$(rg -n '在隔离临时 MySQL 容器中验证快照可恢复' "$TMP_DIR/remote-script.sh" | tail -1 | cut -d: -f1)"
|
||
migration_line="$(rg -n '^run_flyway_migrations$' "$TMP_DIR/remote-script.sh" | tail -1 | cut -d: -f1)"
|
||
[ "$candidate_probe_line" -lt "$stop_old_line" ] || fail "候选非数据面探针没有在停止旧栈前完成"
|
||
[ "$maintenance_install_line" -lt "$stop_old_line" ] || fail "维护门没有在严格停栈前安装"
|
||
[ "$stop_old_line" -lt "$snapshot_line" ] || fail "迁移快照前旧栈仍可能写入,失败补偿会丢数据"
|
||
[ "$snapshot_line" -lt "$restore_verify_line" ] || fail "隔离恢复验证没有使用停写后的最终快照"
|
||
[ "$restore_verify_line" -lt "$migration_line" ] || fail "未先完成隔离恢复验证就执行正式迁移"
|
||
reviewer_line="$(rg -n 'manage-dogfood-reviewers\.py' "$TMP_DIR/remote-script.sh" | tail -1 | cut -d: -f1)"
|
||
provision_line="$(rg -n 'provision-dogfood-with-nacos-transaction\.sh' "$TMP_DIR/remote-script.sh" | tail -1 | cut -d: -f1)"
|
||
provision_pass_line="$(rg -n 'PROVISION_PASS accounts=40 creators=10 role_mismatches=0 claimed_accounts=40 duplicate_claims=0' "$TMP_DIR/remote-script.sh" | tail -1 | cut -d: -f1)"
|
||
runtime_verify_line="$(rg -n '狗粮审核账号轮换与运行态验证 PASS' "$TMP_DIR/remote-script.sh" | tail -1 | cut -d: -f1)"
|
||
stable_smoke_line="$(rg -n 'stable-smoke\.log' "$TMP_DIR/remote-script.sh" | tail -1 | cut -d: -f1)"
|
||
active_switch_line="$(rg -n '正式栈全绿,原子切换 active 指针' "$TMP_DIR/remote-script.sh" | tail -1 | cut -d: -f1)"
|
||
stable_start_line="$(rg -n '^start_stack .*48080 4173 4174 8300 9501 true$' "$TMP_DIR/remote-script.sh" | tail -1 | cut -d: -f1)"
|
||
maintenance_release_line="$(rg -n '^release_maintenance_gate$' "$TMP_DIR/remote-script.sh" | tail -1 | cut -d: -f1)"
|
||
[ "$maintenance_install_line" -lt "$stable_start_line" ] || fail "正式栈启动前未封锁外部入口"
|
||
[ "$maintenance_install_line" -lt "$snapshot_line" ] || fail "停写快照前未封锁外部入口"
|
||
[ "$provision_line" -lt "$provision_pass_line" ] || fail "账号预置后未核对终验"
|
||
[ "$provision_pass_line" -lt "$reviewer_line" ] || fail "admin 禁用发生在账号预置恢复前"
|
||
[ "$reviewer_line" -lt "$runtime_verify_line" ] || fail "审核账号轮换后未做运行态验证"
|
||
[ "$runtime_verify_line" -lt "$stable_smoke_line" ] || fail "审核账号运行态验证没有位于写 smoke 前"
|
||
[ "$stable_smoke_line" -lt "$active_switch_line" ] || fail "active 指针在 smoke 前切换"
|
||
[ "$active_switch_line" -lt "$maintenance_release_line" ] || fail "事务提交前提前解除外部入口封锁"
|
||
token_delete_line="$(rg -n '^delete_bootstrap_token$' "$TMP_DIR/remote-script.sh" | tail -1 | cut -d: -f1)"
|
||
[ "$active_switch_line" -lt "$token_delete_line" ] || fail "bootstrap token 未在 smoke+active 后最后删除"
|
||
failure_handler_body="$(sed -n '/^failure_handler()/,/^}/p' "$TMP_DIR/remote-script.sh")"
|
||
! printf '%s\n' "$failure_handler_body" | rg -q '^ delete_bootstrap_token' || \
|
||
fail "激活失败路径删除了补偿后重试所需的 bootstrap token"
|
||
printf '%s\n' "$failure_handler_body" | rg -q 'cleanup_dogfood_provision_runtime' || \
|
||
fail "激活失败补偿未定向清理本次限流桶与 OAuth token 缓存"
|
||
printf '%s\n' "$failure_handler_body" | rg -q 'verify_stack.*rollback' || \
|
||
fail "回滚后未逐项复核五服务"
|
||
printf '%s\n' "$failure_handler_body" | rg -q 'release_maintenance_gate' || \
|
||
fail "旧栈完整恢复后未解除维护门"
|
||
rg -q '^verify_active_pointer()' "$TMP_DIR/remote-script.sh" || \
|
||
fail "active 指针恢复后缺 realpath/commit 复核函数"
|
||
pointer_restore_line="$(printf '%s\n' "$failure_handler_body" | rg -n 'restore_active_pointer.*previous' | cut -d: -f1)"
|
||
pointer_verify_line="$(printf '%s\n' "$failure_handler_body" | rg -n 'verify_active_pointer.*previous' | cut -d: -f1)"
|
||
rollback_release_line="$(printf '%s\n' "$failure_handler_body" | rg -n 'release_maintenance_gate' | cut -d: -f1)"
|
||
[ -n "$pointer_restore_line" ] && [ -n "$pointer_verify_line" ] && [ -n "$rollback_release_line" ] \
|
||
&& [ "$pointer_restore_line" -lt "$pointer_verify_line" ] \
|
||
&& [ "$pointer_verify_line" -lt "$rollback_release_line" ] || \
|
||
fail "失败回滚未按 active 恢复→realpath/commit 复核→最后解除维护门排序"
|
||
printf '%s\n' "$failure_handler_body" | rg -q 'rollback_rc=' || \
|
||
fail "失败回滚证据未记录总 rollback_rc"
|
||
! printf '%s\n' "$failure_handler_body" | rg -q 'exit 72' || \
|
||
fail "运行态 cleanup 失败仍直接 exit 72,跳过最终证据落盘"
|
||
cleanup_rc_line="$(printf '%s\n' "$failure_handler_body" | rg -n 'rc=72' | cut -d: -f1)"
|
||
failure_status_line="$(printf '%s\n' "$failure_handler_body" | rg -n "printf 'failed commit=" | cut -d: -f1)"
|
||
failure_chmod_line="$(printf '%s\n' "$failure_handler_body" | rg -n 'chmod -R go-rwx' | cut -d: -f1)"
|
||
failure_exit_line="$(printf '%s\n' "$failure_handler_body" | rg -n 'exit "\$rc"' | cut -d: -f1)"
|
||
[ "$cleanup_rc_line" -lt "$failure_status_line" ] \
|
||
&& [ "$failure_status_line" -lt "$failure_chmod_line" ] \
|
||
&& [ "$failure_chmod_line" -lt "$failure_exit_line" ] || \
|
||
fail "cleanup rc=72 后未按 RC_STATUS→chmod→统一 exit 顺序完整落证"
|
||
cleanup_function_body="$(sed -n '/^cleanup_dogfood_provision_runtime()/,/^}/p' "$TMP_DIR/remote-script.sh")"
|
||
printf '%s\n' "$cleanup_function_body" | rg -q -- '--cleanup' || fail "激活补偿未进入定向清理模式"
|
||
printf '%s\n' "$cleanup_function_body" | rg -q 'account_provision_cleanup_manifest.*account_provision_run_id' || \
|
||
fail "激活补偿未用本次 run-id 约束清理清单"
|
||
start_stack_body="$(sed -n '/^start_stack()/,/^}/p' "$TMP_DIR/remote-script.sh")"
|
||
if printf '%s\n' "$start_stack_body" \
|
||
| rg '^ (run_unit|wait_port|wait_json_health|wait_url) ' \
|
||
| rg -v '\|\| return 1$' >/dev/null; then
|
||
fail "start_stack 存在依赖 errexit 的启动或探活步骤"
|
||
fi
|
||
verify_stack_body="$(sed -n '/^verify_stack()/,/^}/p' "$TMP_DIR/remote-script.sh")"
|
||
[ "$(printf '%s\n' "$verify_stack_body" | rg -c 'systemctl is-active --quiet')" -eq 5 ] || \
|
||
fail "回滚复核没有逐项检查五个 systemd 服务"
|
||
|
||
# 用有状态假 iptables 执行维护门真实函数,验证链清理失败时独立护栏仍封锁入口。
|
||
cat >"$TMP_DIR/fake-iptables" <<'SH'
|
||
#!/usr/bin/env bash
|
||
set -euo pipefail
|
||
state="${RC_TEST_TMP:?}/iptables-state"
|
||
mkdir -p "$state"
|
||
printf '%s\n' "$*" >>"$state/calls.log"
|
||
case "$*" in
|
||
'-w 5 -nL GDA_STAGING_MAINT') [ -f "$state/chain" ] ;;
|
||
'-w 5 -N GDA_STAGING_MAINT') touch "$state/chain" ;;
|
||
'-w 5 -C GDA_STAGING_MAINT -p tcp -m multiport --dports 48080,4173,4174,8300,9501 -j REJECT')
|
||
[ -f "$state/reject-rule" ] ;;
|
||
'-w 5 -A GDA_STAGING_MAINT -p tcp -m multiport --dports 48080,4173,4174,8300,9501 -j REJECT')
|
||
touch "$state/reject-rule" ;;
|
||
'-w 5 -C INPUT ! -i lo -j GDA_STAGING_MAINT') [ -f "$state/input-jump" ] ;;
|
||
'-w 5 -I INPUT 1 ! -i lo -j GDA_STAGING_MAINT') touch "$state/input-jump" ;;
|
||
'-w 5 -D INPUT ! -i lo -j GDA_STAGING_MAINT') rm -f "$state/input-jump" ;;
|
||
'-w 5 -nL GDA_STAGING_RELEASE_GUARD') [ -f "$state/release-guard-chain" ] ;;
|
||
'-w 5 -N GDA_STAGING_RELEASE_GUARD') touch "$state/release-guard-chain" ;;
|
||
'-w 5 -C GDA_STAGING_RELEASE_GUARD -p tcp -m multiport --dports 48080,4173,4174,8300,9501 -j REJECT')
|
||
[ -f "$state/release-guard-reject" ] ;;
|
||
'-w 5 -A GDA_STAGING_RELEASE_GUARD -p tcp -m multiport --dports 48080,4173,4174,8300,9501 -j REJECT')
|
||
touch "$state/release-guard-reject" ;;
|
||
'-w 5 -C INPUT ! -i lo -j GDA_STAGING_RELEASE_GUARD') [ -f "$state/release-guard-jump" ] ;;
|
||
'-w 5 -I INPUT 1 ! -i lo -j GDA_STAGING_RELEASE_GUARD') touch "$state/release-guard-jump" ;;
|
||
'-w 5 -D INPUT ! -i lo -j GDA_STAGING_RELEASE_GUARD') rm -f "$state/release-guard-jump" ;;
|
||
'-w 5 -F GDA_STAGING_MAINT')
|
||
[ ! -f "$state/fail-F" ] || exit 91
|
||
rm -f "$state/reject-rule" ;;
|
||
'-w 5 -X GDA_STAGING_MAINT')
|
||
[ ! -f "$state/fail-X" ] || exit 92
|
||
rm -f "$state/chain" ;;
|
||
*) printf '未知 fake iptables 调用:%s\n' "$*" >&2; exit 96 ;;
|
||
esac
|
||
SH
|
||
chmod +x "$TMP_DIR/fake-iptables"
|
||
{
|
||
printf '%s\n' 'set -euo pipefail'
|
||
printf 'IPTABLES_BIN=%q\n' "$TMP_DIR/fake-iptables"
|
||
printf 'evidence=%q\n' "$TMP_DIR/maintenance-evidence"
|
||
printf '%s\n' 'maintenance_chain=GDA_STAGING_MAINT' \
|
||
'maintenance_release_guard_chain=GDA_STAGING_RELEASE_GUARD' \
|
||
'maintenance_ports=48080,4173,4174,8300,9501' 'maintenance_gate_active=0'
|
||
printf '%s\n' 'log_step() { :; }'
|
||
sed -n '/^maintenance_rule_present()/,/^}/p' "$TMP_DIR/remote-script.sh"
|
||
sed -n '/^maintenance_release_guard_present()/,/^}/p' "$TMP_DIR/remote-script.sh"
|
||
sed -n '/^install_maintenance_gate()/,/^}/p' "$TMP_DIR/remote-script.sh"
|
||
sed -n '/^release_maintenance_gate()/,/^}/p' "$TMP_DIR/remote-script.sh"
|
||
printf '%s\n' 'mkdir -p "$evidence"' 'install_maintenance_gate' \
|
||
'[ "$maintenance_gate_active" -eq 1 ]' 'maintenance_rule_present' \
|
||
'release_maintenance_gate' '[ "$maintenance_gate_active" -eq 0 ]'
|
||
} >"$TMP_DIR/maintenance-harness.sh"
|
||
bash "$TMP_DIR/maintenance-harness.sh"
|
||
[ ! -e "$TMP_DIR/iptables-state/chain" ] \
|
||
&& [ ! -e "$TMP_DIR/iptables-state/reject-rule" ] \
|
||
&& [ ! -e "$TMP_DIR/iptables-state/input-jump" ] \
|
||
&& [ ! -e "$TMP_DIR/iptables-state/release-guard-jump" ] \
|
||
&& [ -e "$TMP_DIR/iptables-state/release-guard-chain" ] \
|
||
&& [ -e "$TMP_DIR/iptables-state/release-guard-reject" ] || fail "维护门解除后的 guard 状态非法"
|
||
rg -q '^maintenance_gate=OPENING ' "$TMP_DIR/maintenance-evidence/maintenance-gate.status" || \
|
||
fail "维护门未留下最终开放提交前证据"
|
||
tail -1 "$TMP_DIR/iptables-state/calls.log" | rg -Fq \
|
||
-- '-D INPUT ! -i lo -j GDA_STAGING_RELEASE_GUARD' || \
|
||
fail "最终开放不是最后一条 iptables 命令"
|
||
|
||
for failed_operation in F X; do
|
||
rm -rf "$TMP_DIR/iptables-state" "$TMP_DIR/maintenance-evidence"
|
||
{
|
||
sed -n '1,/^mkdir -p "\$evidence"/p' "$TMP_DIR/maintenance-harness.sh"
|
||
printf '%s\n' 'install_maintenance_gate'
|
||
printf 'touch %q\n' "$TMP_DIR/iptables-state/fail-$failed_operation"
|
||
printf '%s\n' 'set +e' 'release_maintenance_gate' 'release_rc=$?' 'set -e' \
|
||
'[ "$release_rc" -ne 0 ]' '[ "$maintenance_gate_active" -eq 1 ]' \
|
||
'maintenance_release_guard_present'
|
||
} >"$TMP_DIR/maintenance-fail-$failed_operation.sh"
|
||
bash "$TMP_DIR/maintenance-fail-$failed_operation.sh" || fail "-$failed_operation 失败后维护门未保持封锁"
|
||
done
|
||
|
||
# 执行真实 stop_stack:服务停止失败、unit 残留或端口残留均必须阻止快照继续。
|
||
stop_stack_body="$(sed -n '/^stop_stack()/,/^}/p' "$TMP_DIR/remote-script.sh")"
|
||
{
|
||
printf '%s\n' 'set -euo pipefail' 'state="${RC_TEST_TMP:?}/stop-stack-state"' 'mkdir -p "$state"'
|
||
printf '%s\n' 'log_step() { :; }' 'unit_name() { printf "%s-%s" "$1" "$2"; }'
|
||
cat <<'SH'
|
||
systemctl() {
|
||
local action="$1"
|
||
shift
|
||
if [ "${1:-}" = "--quiet" ]; then shift; fi
|
||
local unit="${1:-}"
|
||
case "$action" in
|
||
is-active) [ -f "$state/unit-$unit" ] ;;
|
||
stop)
|
||
[ "$unit" != "${STOP_FAIL_UNIT:-}" ] || return 93
|
||
rm -f "$state/unit-$unit"
|
||
;;
|
||
*) return 94 ;;
|
||
esac
|
||
}
|
||
port_listening() {
|
||
[ -f "$state/port-$1" ]
|
||
}
|
||
SH
|
||
printf '%s\n' "$stop_stack_body"
|
||
cat <<'SH'
|
||
for service in backend studio admin cheap-agent cheap-worker; do
|
||
touch "$state/unit-gda-staging-$service"
|
||
done
|
||
stop_stack gda-staging 48080 4173 4174 8300 9501
|
||
for service in backend studio admin cheap-agent cheap-worker; do
|
||
[ ! -e "$state/unit-gda-staging-$service" ]
|
||
done
|
||
|
||
touch "$state/unit-gda-staging-backend"
|
||
set +e
|
||
STOP_FAIL_UNIT=gda-staging-backend stop_stack gda-staging 48080 4173 4174 8300 9501
|
||
stop_rc=$?
|
||
set -e
|
||
[ "$stop_rc" -ne 0 ]
|
||
[ -e "$state/unit-gda-staging-backend" ]
|
||
|
||
rm -f "$state"/unit-*
|
||
touch "$state/port-48080"
|
||
set +e
|
||
stop_stack gda-staging 48080 4173 4174 8300 9501
|
||
port_rc=$?
|
||
set -e
|
||
[ "$port_rc" -ne 0 ]
|
||
SH
|
||
} >"$TMP_DIR/stop-stack-harness.sh"
|
||
bash "$TMP_DIR/stop-stack-harness.sh" || fail "stop_stack 未严格传播服务或端口残留"
|
||
|
||
# 在关闭 errexit 的回滚环境中执行真实 start_stack,首个启动失败必须立即返回且不得继续探活。
|
||
{
|
||
printf '%s\n' 'set -uo pipefail' 'calls="${RC_TEST_TMP:?}/start-stack-calls.log"'
|
||
printf '%s\n' 'write_launchers() { printf "/tmp/launchers"; }' \
|
||
'unit_name() { printf "%s-%s" "$1" "$2"; }' \
|
||
'run_unit() { printf "run_unit %s\n" "$1" >>"$calls"; return 1; }' \
|
||
'wait_port() { printf "unexpected wait_port\n" >>"$calls"; return 0; }' \
|
||
'wait_json_health() { printf "unexpected wait_json_health\n" >>"$calls"; return 0; }' \
|
||
'wait_url() { printf "unexpected wait_url\n" >>"$calls"; return 0; }'
|
||
printf '%s\n' "$start_stack_body"
|
||
printf '%s\n' 'set +e' \
|
||
'start_stack /tmp/release rollback test-prefix 48080 4173 4174 8300 9501 true' \
|
||
'rc=$?' 'set -e' '[ "$rc" -eq 1 ]' \
|
||
'[ "$(wc -l <"$calls" | tr -d " ")" -eq 1 ]' \
|
||
'! rg -q "^unexpected " "$calls"'
|
||
} >"$TMP_DIR/start-stack-harness.sh"
|
||
bash "$TMP_DIR/start-stack-harness.sh"
|
||
|
||
# 执行失败回滚状态机:即使新 active 尚未完成标记,也必须恢复并复核旧指针后才开放入口。
|
||
{
|
||
cat <<'SH'
|
||
#!/usr/bin/env bash
|
||
set -uo pipefail
|
||
case_name="${1:?}"
|
||
state="${RC_TEST_TMP:?}/rollback-state-$case_name"
|
||
calls="$state/calls.log"
|
||
evidence="$state/evidence"
|
||
release="$state/release"
|
||
mkdir -p "$evidence" "$release"
|
||
commit=0000000000000000000000000000000000000000
|
||
candidate_prefix=gda-candidate
|
||
stable_prefix=gda-staging
|
||
switch_started=1
|
||
pointer_changed=0
|
||
previous_was_active=1
|
||
previous_valid=1
|
||
previous=/root/game-staging/releases/1111111111111111111111111111111111111111
|
||
database_restore_required=1
|
||
database_restore_rc=not-run
|
||
runtime_cleanup_rc=not-run
|
||
bootstrap_token_validated=0
|
||
maintenance_gate_active=1
|
||
run_id=test-run
|
||
|
||
log_step() { printf 'log:%s\n' "$*" >>"$calls"; }
|
||
capture_journals() { :; }
|
||
stop_stack() { printf 'stop:%s\n' "$1" >>"$calls"; }
|
||
cleanup_dogfood_provision_runtime() { return 0; }
|
||
restore_database_snapshot() { printf 'database\n' >>"$calls"; return 0; }
|
||
start_stack() { printf 'start-old-stack\n' >>"$calls"; return 0; }
|
||
verify_stack() { printf 'verify-old-stack\n' >>"$calls"; return 0; }
|
||
restore_active_pointer() {
|
||
printf 'restore-active\n' >>"$calls"
|
||
[ "$case_name" != pointer-fail ] || return 88
|
||
}
|
||
verify_active_pointer() { printf 'verify-active\n' >>"$calls"; return 0; }
|
||
release_maintenance_gate() { printf 'release-maintenance\n' >>"$calls"; maintenance_gate_active=0; }
|
||
SH
|
||
printf '%s\n' "$failure_handler_body"
|
||
printf '%s\n' 'false' 'failure_handler'
|
||
} >"$TMP_DIR/rollback-state-harness.sh"
|
||
chmod +x "$TMP_DIR/rollback-state-harness.sh"
|
||
|
||
set +e
|
||
bash "$TMP_DIR/rollback-state-harness.sh" success
|
||
rollback_success_rc=$?
|
||
set -e
|
||
[ "$rollback_success_rc" -eq 1 ] || fail "完整回滚没有保留原始激活失败码"
|
||
success_calls="$TMP_DIR/rollback-state-success/calls.log"
|
||
database_line="$(rg -n '^database$' "$success_calls" | cut -d: -f1 || true)"
|
||
service_line="$(rg -n '^verify-old-stack$' "$success_calls" | cut -d: -f1 || true)"
|
||
restore_pointer_line="$(rg -n '^restore-active$' "$success_calls" | cut -d: -f1 || true)"
|
||
verify_pointer_line="$(rg -n '^verify-active$' "$success_calls" | cut -d: -f1 || true)"
|
||
release_line="$(rg -n '^release-maintenance$' "$success_calls" | cut -d: -f1 || true)"
|
||
[ -n "$database_line" ] && [ -n "$service_line" ] && [ -n "$restore_pointer_line" ] \
|
||
&& [ -n "$verify_pointer_line" ] && [ -n "$release_line" ] \
|
||
&& [ "$database_line" -lt "$service_line" ] \
|
||
&& [ "$service_line" -lt "$restore_pointer_line" ] \
|
||
&& [ "$restore_pointer_line" -lt "$verify_pointer_line" ] \
|
||
&& [ "$verify_pointer_line" -lt "$release_line" ] || \
|
||
fail "完整回滚未按数据库→旧栈→active 恢复复核→维护门释放执行"
|
||
rg -q '^rollback_rc=0$' "$TMP_DIR/rollback-state-success/evidence/rollback-result.txt" || \
|
||
fail "完整回滚证据未记录 rollback_rc=0"
|
||
|
||
set +e
|
||
bash "$TMP_DIR/rollback-state-harness.sh" pointer-fail
|
||
rollback_pointer_fail_rc=$?
|
||
set -e
|
||
[ "$rollback_pointer_fail_rc" -eq 73 ] || fail "active 指针恢复失败未返回统一回滚失败码 73"
|
||
pointer_fail_calls="$TMP_DIR/rollback-state-pointer-fail/calls.log"
|
||
! rg -q '^release-maintenance$' "$pointer_fail_calls" || fail "active 指针失败后仍开放正式入口"
|
||
rg -q '^rollback_pointer_rc=88$' "$TMP_DIR/rollback-state-pointer-fail/evidence/rollback-result.txt" || \
|
||
fail "active 指针失败证据未记录原始非零返回码"
|
||
rg -q '^rollback_rc=88$' "$TMP_DIR/rollback-state-pointer-fail/evidence/rollback-result.txt" || \
|
||
fail "active 指针失败未计入总 rollback_rc"
|
||
|
||
# 执行真实 active 指针函数,验证目标 realpath 与 RC 目录名、Git HEAD 三者必须一致。
|
||
pointer_functions="$TMP_DIR/active-pointer-functions.sh"
|
||
{
|
||
printf '%s\n' 'set -euo pipefail'
|
||
sed -n '/^restore_active_pointer()/,/^}/p' "$TMP_DIR/remote-script.sh"
|
||
sed -n '/^verify_active_pointer()/,/^}/p' "$TMP_DIR/remote-script.sh"
|
||
} >"$pointer_functions"
|
||
pointer_root="$TMP_DIR/pointer-releases"
|
||
pointer_seed="$TMP_DIR/pointer-seed"
|
||
mkdir -p "$pointer_root" "$pointer_seed"
|
||
git -C "$pointer_seed" init -q
|
||
git -C "$pointer_seed" config user.name staging-rc-test
|
||
git -C "$pointer_seed" config user.email staging-rc-test@example.invalid
|
||
printf 'v1\n' >"$pointer_seed/artifact.txt"
|
||
git -C "$pointer_seed" add artifact.txt
|
||
git -C "$pointer_seed" commit -q -m v1
|
||
pointer_commit="$(git -C "$pointer_seed" rev-parse HEAD)"
|
||
pointer_release="$pointer_root/$pointer_commit"
|
||
mv "$pointer_seed" "$pointer_release"
|
||
{
|
||
printf 'root=%q\n' "$pointer_root"
|
||
printf 'active_pointer=%q\n' "$pointer_root/active"
|
||
printf '%s\n' 'run_id=pointer-test' 'log_step() { :; }' \
|
||
'mv() { if [ "${1:-}" = -Tf ]; then command mv -f "$2" "$3"; else command mv "$@"; fi; }'
|
||
printf 'source %q\n' "$pointer_functions"
|
||
printf 'restore_active_pointer %q\n' "$pointer_release"
|
||
printf 'verify_active_pointer %q\n' "$pointer_release"
|
||
} >"$TMP_DIR/active-pointer-pass.sh"
|
||
bash "$TMP_DIR/active-pointer-pass.sh" || fail "active 指针 realpath/commit 正例失败"
|
||
[ "$(readlink -f "$pointer_root/active")" = "$(realpath "$pointer_release")" ] || \
|
||
fail "active 指针未恢复到旧 RC 的 canonical 路径"
|
||
|
||
printf 'v2\n' >>"$pointer_release/artifact.txt"
|
||
git -C "$pointer_release" add artifact.txt
|
||
git -C "$pointer_release" commit -q -m v2
|
||
if bash -c "root=$(printf '%q' "$pointer_root"); active_pointer=$(printf '%q' "$pointer_root/active"); \
|
||
run_id=pointer-test; log_step() { :; }; source $(printf '%q' "$pointer_functions"); \
|
||
verify_active_pointer $(printf '%q' "$pointer_release")"; then
|
||
fail "active RC 的 Git HEAD 漂移后仍通过 commit 复核"
|
||
fi
|
||
|
||
if rg -q '/root/games-development-ai' "$TMP_DIR/remote-script.sh"; then
|
||
fail "激活脚本引用了远端脏旧仓"
|
||
fi
|
||
|
||
# 生成门引用是激活必填项,且必须在 SSH 前拒绝缺失或不安全路径。
|
||
: >"$TMP_DIR/ssh-called"
|
||
cat >"$TMP_DIR/reject-ssh" <<'SH'
|
||
#!/usr/bin/env bash
|
||
printf 'called\n' >>"$RC_TEST_TMP/ssh-called"
|
||
exit 99
|
||
SH
|
||
chmod +x "$TMP_DIR/reject-ssh"
|
||
if RC_COMMIT="0000000000000000000000000000000000000000" SSH_BIN="$TMP_DIR/reject-ssh" \
|
||
bash "$ACTIVATE" >"$TMP_DIR/missing-generation-gate.log" 2>&1; then
|
||
fail "缺生成门引用仍允许激活"
|
||
fi
|
||
[ ! -s "$TMP_DIR/ssh-called" ] || fail "缺生成门引用后仍发生 SSH"
|
||
if RC_COMMIT="0000000000000000000000000000000000000000" \
|
||
GENERATION_GATE_EVIDENCE_REF="sha256:$(printf '1%.0s' $(seq 1 64)):../gate.txt" \
|
||
SSH_BIN="$TMP_DIR/reject-ssh" bash "$ACTIVATE" >"$TMP_DIR/unsafe-generation-gate.log" 2>&1; then
|
||
fail "不安全生成门相对路径仍允许激活"
|
||
fi
|
||
[ ! -s "$TMP_DIR/ssh-called" ] || fail "非法生成门路径后仍发生 SSH"
|
||
|
||
# 真激活缺 bootstrap token 文件路径时必须在 SSH 前 fail-closed,不能回退仓库默认密码。
|
||
if RC_COMMIT="0000000000000000000000000000000000000000" \
|
||
GENERATION_GATE_EVIDENCE_REF="$GENERATION_GATE_REF" SSH_BIN="$TMP_DIR/reject-ssh" \
|
||
bash "$ACTIVATE" >"$TMP_DIR/missing-bootstrap-token.log" 2>&1; then
|
||
fail "缺 BOOTSTRAP_ADMIN_TOKEN_FILE 仍允许激活"
|
||
fi
|
||
[ ! -s "$TMP_DIR/ssh-called" ] || fail "缺 bootstrap token 路径后仍发生 SSH"
|
||
|
||
# 旧变量仅允许受控路径和 hash;任意 URL 必须在 SSH 前失败。
|
||
if RC_COMMIT="0000000000000000000000000000000000000000" \
|
||
GENERATION_GATE_EVIDENCE_REF="$GENERATION_GATE_REF" \
|
||
OBJECT_SNAPSHOT_REF="fake://object-snapshot/rc-test" SSH_BIN="$TMP_DIR/fake-ssh" \
|
||
bash "$ACTIVATE" >"$TMP_DIR/invalid-snapshot.log" 2>&1; then
|
||
fail "任意 OBJECT_SNAPSHOT_REF 仍允许激活"
|
||
fi
|
||
|
||
# prepare 必须在 ready 前运行发布工具回归与安全关键 Java 测试,并把日志哈希绑定 RC commit。
|
||
for test_name in test-staging-rc.sh test-provision-dogfood-users.sh \
|
||
test-provision-dogfood-nacos-transaction.sh test-import-newapi-pool.sh \
|
||
test-dogfood-smoke.sh; do
|
||
rg -q "$test_name" "$PREPARE" || fail "prepare 未运行发布工具回归:$test_name"
|
||
done
|
||
rg -q 'release-tool-tests\.log' "$PREPARE" || fail "prepare 未保存发布工具测试日志"
|
||
rg -q 'security-java-tests\.log' "$PREPARE" || fail "prepare 未保存安全关键 Java 测试日志"
|
||
rg -q 'DogfoodStartupValidatorTest' "$PREPARE" || fail "prepare 未运行 Dogfood 启动安全测试"
|
||
rg -q 'DogfoodSecurityStartupValidatorTest' "$PREPARE" || fail "prepare 未运行 Dogfood 配置安全测试"
|
||
rg -q 'TokenAuthenticationFilterTest' "$PREPARE" || fail "prepare 未运行令牌认证过滤器测试"
|
||
rg -q 'AuthenticationEntryPointImplTest' "$PREPARE" || fail "prepare 未运行未认证响应测试"
|
||
rg -q 'RuntimePackageServiceImplTest' "$PREPARE" || fail "prepare 未运行 artifact CAS 测试"
|
||
rg -q 'RuntimePackageApiImplDogfoodTest' "$PREPARE" || fail "prepare 未运行 runtime RPC 安全测试"
|
||
rg -q 'AdminNewapiQuotaControllerTest' "$PREPARE" || fail "prepare 未运行配额修复入口测试"
|
||
rg -q 'NewapiQuotaClaimConsumerTest' "$PREPARE" || fail "prepare 未运行配额 MQ 消费测试"
|
||
rg -q 'NewapiQuotaServiceImplTest' "$PREPARE" || fail "prepare 未运行配额服务测试"
|
||
rg -q 'GameVersionServiceImplTest' "$PREPARE" || fail "prepare 未运行版本产物绑定测试"
|
||
rg -q 'ProjectVersionApiImplDogfoodTest' "$PREPARE" || fail "prepare 未运行 project RPC 写边界测试"
|
||
rg -q 'FeedApiImplDogfoodTest' "$PREPARE" || fail "prepare 未运行 feed RPC 写边界测试"
|
||
rg -q 'DifyCallbackServiceImplTest' "$PREPARE" || fail "prepare 未运行生成回调产物绑定测试"
|
||
rg -q 'PublishOrchestrationServiceImplTest' "$PREPARE" || fail "prepare 未运行发布编排测试"
|
||
rg -q 'test-dogfood-reviewer-security\.py' "$PREPARE" || fail "prepare 未运行审核账号安全测试"
|
||
rg -q 'test-collect-dogfood-account-report\.sh' "$PREPARE" || fail "prepare 未运行账号数据库签名采集测试"
|
||
rg -q 'test_dogfood_ops\.py' "$PREPARE" || fail "prepare 未运行波次证据账本测试"
|
||
rg -q 'backend_security_tests=PASS' "$PREPARE" || fail "prepare 未把安全 Java 测试结果绑定 RC"
|
||
rg -q 'security_java_tests_sha256=' "$PREPARE" || fail "prepare 摘要未绑定安全测试日志哈希"
|
||
rg -q 'verify_security_java_reports' "$PREPARE" || fail "prepare 未校验安全测试报告实际包含全部目标类"
|
||
rg -q 'security-java-tests-summary\.tsv' "$PREPARE" || fail "prepare 未保存可审计的 Java 测试计数摘要"
|
||
rg -q 'security_java_tests_summary_sha256=' "$PREPARE" || fail "prepare 摘要未绑定 Java 测试计数证据"
|
||
|
||
test_surefire_report_guard
|
||
|
||
rg -q 'FRONTEND_SECRET_SCAN_PASS' "$PREPARE" || fail "prepare 未扫描前端制品中的默认凭据"
|
||
rg -q 'frontend_secret_scan_sha256=' "$PREPARE" || fail "prepare 摘要未绑定前端凭据扫描证据"
|
||
rg -Fq "frontend_forbidden_patterns='admin123|test1|mock-token|mock-studio-token|bootstrap-admin\\.token|DOGFOOD_PASSWORD_SEED'" \
|
||
"$PREPARE" || fail "prepare 前端扫描模式未完整覆盖 test1、mock-studio-token 与既有模式"
|
||
rg -q "printf 'patterns=%s" "$PREPARE" || fail "prepare 扫描证据未记录实际模式集"
|
||
rg -q 'frontend_secret_scan_patterns_sha256=' "$PREPARE" || fail "prepare 摘要未绑定扫描模式集证据"
|
||
rg -q 'runtime-tree-manifest\.py capture' "$PREPARE" || fail "prepare 未签发 Python 运行树清单"
|
||
rg -q 'runtime_tree_manifest_sha256=' "$PREPARE" || fail "prepare 摘要未绑定 Python 运行树清单"
|
||
rg -q 'runtime-tree-manifest\.py verify' "$ACTIVATE" || fail "activate 未校验 Python 运行树漂移"
|
||
frontend_forbidden_patterns="$(sed -n "s/^frontend_forbidden_patterns='\\(.*\\)'$/\\1/p" "$PREPARE")"
|
||
[ -n "$frontend_forbidden_patterns" ] || fail "无法从 prepare 解析前端禁止模式集"
|
||
for forbidden_sample in admin123 test1 mock-token mock-studio-token bootstrap-admin.token DOGFOOD_PASSWORD_SEED; do
|
||
printf '%s\n' "$forbidden_sample" | rg -q -i "$frontend_forbidden_patterns" || \
|
||
fail "前端扫描正则未命中禁止样本:$forbidden_sample"
|
||
done
|
||
if printf '%s\n' 'production-studio-token' | rg -q -i "$frontend_forbidden_patterns"; then
|
||
fail "前端扫描正则误伤安全样本"
|
||
fi
|
||
! rg -q 'backend_tests=NOT_RUN' "$PREPARE" || fail "prepare 仍宣称后端测试未运行"
|
||
venv_line="$(rg -n '准备 commit 内独立 Python 虚拟环境' "$PREPARE" | cut -d: -f1)"
|
||
tool_test_line="$(rg -n '运行发布工具回归测试' "$PREPARE" | cut -d: -f1)"
|
||
[ "$venv_line" -lt "$tool_test_line" ] || fail "发布工具回归未使用 commit 内已建 venv"
|
||
rg -q 'cheap-worker/\.venv/bin.*PATH' "$PREPARE" || fail "发布工具回归未显式使用 commit 内 Python"
|
||
rg -q '原子切换 active' "$PREPARE" || fail "prepare 完成提示仍未使用 active 指针"
|
||
|
||
# 独立签发者用仓外私钥签名;测试 SSH 桩只把固定生产路径映射到临时目录。
|
||
cat >"$TMP_DIR/exec-ssh" <<'SH'
|
||
#!/usr/bin/env bash
|
||
set -euo pipefail
|
||
while [ "$#" -gt 0 ] && [ "$1" != bash ]; do shift; done
|
||
[ "${1:-}" = bash ] || exit 97
|
||
shift
|
||
[ "${1:-}" = -s ] || exit 97
|
||
shift
|
||
[ "${1:-}" = -- ] || exit 97
|
||
shift
|
||
remote_script="$(mktemp)"
|
||
trap 'rm -f "$remote_script"' EXIT
|
||
sed "s|/root/game-staging/trust/r1-attestation-ed25519.pub|$RC_TEST_TRUSTED_PUBLIC_KEY|g" \
|
||
>"$remote_script"
|
||
PATH="$RC_TEST_FAKE_BIN:$PATH" bash "$remote_script" "$@"
|
||
SH
|
||
chmod +x "$TMP_DIR/exec-ssh"
|
||
|
||
mkdir -p "$TMP_DIR/r1-fake-bin"
|
||
cat >"$TMP_DIR/r1-fake-bin/stat" <<'SH'
|
||
#!/usr/bin/env bash
|
||
if [ "${1:-}" = -c ] && [ "${2:-}" = %u ]; then
|
||
printf '%s\n' "${RC_TEST_FORCE_BAD_TRUST_OWNER:-0}"
|
||
exit 0
|
||
fi
|
||
exec /usr/bin/stat "$@"
|
||
SH
|
||
cat >"$TMP_DIR/r1-fake-bin/openssl" <<'SH'
|
||
#!/usr/bin/env bash
|
||
if [ "${1:-}" = pkeyutl ] && printf '%s\n' "$*" | rg -q -- ' -verify( |$)'; then
|
||
signature=""
|
||
while [ "$#" -gt 0 ]; do
|
||
if [ "$1" = -sigfile ]; then signature="$2"; break; fi
|
||
shift
|
||
done
|
||
[ -f "$signature" ] && [ "$(wc -c <"$signature" | tr -d ' ')" -eq 64 ]
|
||
exit
|
||
fi
|
||
exec /usr/bin/openssl "$@"
|
||
SH
|
||
chmod +x "$TMP_DIR/r1-fake-bin/stat" "$TMP_DIR/r1-fake-bin/openssl"
|
||
export RC_TEST_FAKE_BIN="$TMP_DIR/r1-fake-bin"
|
||
|
||
attestation_root="$(realpath "$TMP_DIR")/attestations"
|
||
mkdir -p "$attestation_root/r1"
|
||
openssl genpkey -algorithm ED25519 -out "$TMP_DIR/r1-private.pem" >/dev/null 2>&1
|
||
chmod 600 "$TMP_DIR/r1-private.pem"
|
||
RC_ROOT="$(realpath "$TMP_DIR")/r1-releases"
|
||
trusted_public_key="$(realpath "$TMP_DIR")/r1-trust/r1-attestation-ed25519.pub"
|
||
mkdir -p "$(dirname "$trusted_public_key")"
|
||
cp "$REPOSITORY_GENERATION_ATTESTATION_PUBLIC_KEY" "$trusted_public_key"
|
||
chmod 755 "$(dirname "$trusted_public_key")"
|
||
chmod 644 "$trusted_public_key"
|
||
export RC_ROOT RC_TEST_TRUSTED_PUBLIC_KEY="$trusted_public_key"
|
||
for fixture_commit in 0000000000000000000000000000000000000000 1111111111111111111111111111111111111111; do
|
||
mkdir -p "$RC_ROOT/$fixture_commit/deploy/trust"
|
||
cp "$REPOSITORY_GENERATION_ATTESTATION_PUBLIC_KEY" \
|
||
"$RC_ROOT/$fixture_commit/deploy/trust/r1-attestation-ed25519.pub"
|
||
done
|
||
[ "$(sha256sum "$trusted_public_key" | awk '{print $1}')" = "$FIXED_GENERATION_ATTESTATION_PUBLIC_KEY_SHA256" ] \
|
||
|| fail "仓内 R1 公钥摘要与固定值不一致"
|
||
read -r issued_at expires_at expired_issued_at expired_expires_at < <(python3 - <<'PY'
|
||
from datetime import datetime, timedelta, timezone
|
||
now = datetime.now(timezone.utc).replace(microsecond=0)
|
||
fmt = "%Y-%m-%dT%H:%M:%SZ"
|
||
print((now - timedelta(minutes=1)).strftime(fmt),
|
||
(now + timedelta(minutes=30)).strftime(fmt),
|
||
(now - timedelta(hours=2)).strftime(fmt),
|
||
(now - timedelta(hours=1)).strftime(fmt))
|
||
PY
|
||
)
|
||
attestation_file="$attestation_root/r1/r1.attestation"
|
||
cat >"$attestation_file" <<EOF
|
||
schema=generation-r1-gate/2
|
||
rc_commit=0000000000000000000000000000000000000000
|
||
run_id=$GENERATION_GATE_RUN_ID
|
||
issuer=$FIXED_GENERATION_ATTESTATION_ISSUER
|
||
issued_at=$issued_at
|
||
expires_at=$expires_at
|
||
r1_status=PASS
|
||
actor_prompt_eval=PASS
|
||
judge_prompt_eval=PASS
|
||
game_content_gold=PASS
|
||
live_prompt=PASS
|
||
full_gate=PASS
|
||
fresh_25=PASS
|
||
historical_11=PASS
|
||
production_shadow_20=PASS
|
||
runtime_eligible=true
|
||
EOF
|
||
chmod 600 "$attestation_file"
|
||
attestation_hash="$(sha256sum "$attestation_file" | awk '{print $1}')"
|
||
mv "$attestation_file" "$attestation_root/r1/$attestation_hash.attestation"
|
||
attestation_file="$attestation_root/r1/$attestation_hash.attestation"
|
||
openssl pkeyutl -sign -inkey "$TMP_DIR/r1-private.pem" -rawin \
|
||
-in "$attestation_file" -out "$attestation_file.sig"
|
||
chmod 600 "$attestation_file.sig"
|
||
attestation_ref="sha256:$attestation_hash:r1/$attestation_hash.attestation"
|
||
RC_COMMIT=0000000000000000000000000000000000000000 \
|
||
GENERATION_GATE_EVIDENCE_REF="$attestation_ref" GENERATION_ATTESTATION_ROOT="$attestation_root" \
|
||
SSH_BIN="$TMP_DIR/exec-ssh" bash "$ACTIVATE" --verify-r1-attestation >"$TMP_DIR/attestation-pass.log" 2>&1 || {
|
||
sed -n '1,120p' "$TMP_DIR/attestation-pass.log" >&2
|
||
fail "固定信任根的独立 attestation 正例失败"
|
||
}
|
||
rg -q 'GENERATION_ATTESTATION_PASS' "$TMP_DIR/attestation-pass.log" || fail "独立 attestation 未通过"
|
||
|
||
# 调用者提供的 issuer 与公钥路径必须被忽略,不能替换固定信任根。
|
||
RC_COMMIT=0000000000000000000000000000000000000000 \
|
||
GENERATION_GATE_EVIDENCE_REF="$attestation_ref" GENERATION_ATTESTATION_ROOT="$attestation_root" \
|
||
GENERATION_ATTESTATION_EXPECTED_ISSUER=other-issuer \
|
||
GENERATION_ATTESTATION_TRUSTED_PUBLIC_KEY_FILE="$TMP_DIR/attacker.pem" \
|
||
SSH_BIN="$TMP_DIR/exec-ssh" bash "$ACTIVATE" --verify-r1-attestation >/dev/null
|
||
|
||
cp "$trusted_public_key" "$TMP_DIR/trusted-public-key.backup"
|
||
printf 'tampered\n' >"$trusted_public_key"
|
||
if RC_COMMIT=0000000000000000000000000000000000000000 \
|
||
GENERATION_GATE_EVIDENCE_REF="$attestation_ref" GENERATION_ATTESTATION_ROOT="$attestation_root" \
|
||
SSH_BIN="$TMP_DIR/exec-ssh" bash "$ACTIVATE" --verify-r1-attestation >/dev/null 2>&1; then
|
||
fail "固定公钥内容篡改后仍允许 attestation"
|
||
fi
|
||
mv "$TMP_DIR/trusted-public-key.backup" "$trusted_public_key"
|
||
chmod 644 "$trusted_public_key"
|
||
|
||
release_public_key="$RC_ROOT/0000000000000000000000000000000000000000/deploy/trust/r1-attestation-ed25519.pub"
|
||
printf '\n' >>"$release_public_key"
|
||
if RC_COMMIT=0000000000000000000000000000000000000000 \
|
||
GENERATION_GATE_EVIDENCE_REF="$attestation_ref" GENERATION_ATTESTATION_ROOT="$attestation_root" \
|
||
SSH_BIN="$TMP_DIR/exec-ssh" bash "$ACTIVATE" --verify-r1-attestation >/dev/null 2>&1; then
|
||
fail "远端公钥与 RC 仓内公钥不一致时仍允许 attestation"
|
||
fi
|
||
cp "$REPOSITORY_GENERATION_ATTESTATION_PUBLIC_KEY" "$release_public_key"
|
||
|
||
chmod 600 "$trusted_public_key"
|
||
if RC_COMMIT=0000000000000000000000000000000000000000 \
|
||
GENERATION_GATE_EVIDENCE_REF="$attestation_ref" GENERATION_ATTESTATION_ROOT="$attestation_root" \
|
||
SSH_BIN="$TMP_DIR/exec-ssh" bash "$ACTIVATE" --verify-r1-attestation >/dev/null 2>&1; then
|
||
fail "固定公钥权限漂移后仍允许 attestation"
|
||
fi
|
||
chmod 644 "$trusted_public_key"
|
||
chmod 777 "$(dirname "$trusted_public_key")"
|
||
if RC_COMMIT=0000000000000000000000000000000000000000 \
|
||
GENERATION_GATE_EVIDENCE_REF="$attestation_ref" GENERATION_ATTESTATION_ROOT="$attestation_root" \
|
||
SSH_BIN="$TMP_DIR/exec-ssh" bash "$ACTIVATE" --verify-r1-attestation >/dev/null 2>&1; then
|
||
fail "固定公钥父目录可被其他用户写入时仍允许 attestation"
|
||
fi
|
||
chmod 755 "$(dirname "$trusted_public_key")"
|
||
if RC_TEST_FORCE_BAD_TRUST_OWNER=1000 RC_COMMIT=0000000000000000000000000000000000000000 \
|
||
GENERATION_GATE_EVIDENCE_REF="$attestation_ref" GENERATION_ATTESTATION_ROOT="$attestation_root" \
|
||
SSH_BIN="$TMP_DIR/exec-ssh" bash "$ACTIVATE" --verify-r1-attestation >/dev/null 2>&1; then
|
||
fail "固定公钥 owner 不是 root 时仍允许 attestation"
|
||
fi
|
||
|
||
mv "$attestation_file.sig" "$attestation_file.sig.missing"
|
||
if RC_COMMIT=0000000000000000000000000000000000000000 \
|
||
GENERATION_GATE_EVIDENCE_REF="$attestation_ref" GENERATION_ATTESTATION_ROOT="$attestation_root" \
|
||
SSH_BIN="$TMP_DIR/exec-ssh" bash "$ACTIVATE" --verify-r1-attestation >/dev/null 2>&1; then
|
||
fail "缺 detached signature 仍允许 attestation"
|
||
fi
|
||
mv "$attestation_file.sig.missing" "$attestation_file.sig"
|
||
|
||
cp "$attestation_file.sig" "$TMP_DIR/valid-attestation.sig"
|
||
printf 'x' >>"$attestation_file.sig"
|
||
if RC_COMMIT=0000000000000000000000000000000000000000 \
|
||
GENERATION_GATE_EVIDENCE_REF="$attestation_ref" GENERATION_ATTESTATION_ROOT="$attestation_root" \
|
||
SSH_BIN="$TMP_DIR/exec-ssh" bash "$ACTIVATE" --verify-r1-attestation >/dev/null 2>&1; then
|
||
fail "篡改 detached signature 仍允许 attestation"
|
||
fi
|
||
mv "$TMP_DIR/valid-attestation.sig" "$attestation_file.sig"
|
||
|
||
if RC_COMMIT=0000000000000000000000000000000000000000 \
|
||
GENERATION_GATE_EVIDENCE_REF="$attestation_ref" GENERATION_ATTESTATION_ROOT="$attestation_root" \
|
||
GENERATION_GATE_RUN_ID=other-run \
|
||
SSH_BIN="$TMP_DIR/exec-ssh" bash "$ACTIVATE" --verify-r1-attestation >/dev/null 2>&1; then
|
||
fail "run-id 漂移 attestation 未拒绝"
|
||
fi
|
||
chmod 644 "$attestation_file"
|
||
if RC_COMMIT=0000000000000000000000000000000000000000 \
|
||
GENERATION_GATE_EVIDENCE_REF="$attestation_ref" GENERATION_ATTESTATION_ROOT="$attestation_root" \
|
||
SSH_BIN="$TMP_DIR/exec-ssh" bash "$ACTIVATE" --verify-r1-attestation >/dev/null 2>&1; then
|
||
fail "权限过宽 attestation 未拒绝"
|
||
fi
|
||
chmod 600 "$attestation_file"
|
||
ln -s "$attestation_file" "$attestation_root/r1/symlink.attestation"
|
||
if RC_COMMIT=0000000000000000000000000000000000000000 \
|
||
GENERATION_GATE_EVIDENCE_REF="sha256:$attestation_hash:r1/symlink.attestation" \
|
||
GENERATION_ATTESTATION_ROOT="$attestation_root" \
|
||
SSH_BIN="$TMP_DIR/exec-ssh" bash "$ACTIVATE" --verify-r1-attestation >/dev/null 2>&1; then
|
||
fail "symlink attestation 未拒绝"
|
||
fi
|
||
if RC_COMMIT=1111111111111111111111111111111111111111 \
|
||
GENERATION_GATE_EVIDENCE_REF="$attestation_ref" GENERATION_ATTESTATION_ROOT="$attestation_root" \
|
||
SSH_BIN="$TMP_DIR/exec-ssh" bash "$ACTIVATE" --verify-r1-attestation >/dev/null 2>&1; then
|
||
fail "rc_commit 漂移 attestation 未拒绝"
|
||
fi
|
||
|
||
expired_file="$attestation_root/r1/expired.attestation"
|
||
sed -e "s/^issued_at=.*/issued_at=$expired_issued_at/" \
|
||
-e "s/^expires_at=.*/expires_at=$expired_expires_at/" "$attestation_file" >"$expired_file"
|
||
chmod 600 "$expired_file"
|
||
expired_hash="$(sha256sum "$expired_file" | awk '{print $1}')"
|
||
mv "$expired_file" "$attestation_root/r1/$expired_hash.attestation"
|
||
expired_file="$attestation_root/r1/$expired_hash.attestation"
|
||
openssl pkeyutl -sign -inkey "$TMP_DIR/r1-private.pem" -rawin \
|
||
-in "$expired_file" -out "$expired_file.sig"
|
||
chmod 600 "$expired_file.sig"
|
||
if RC_COMMIT=0000000000000000000000000000000000000000 \
|
||
GENERATION_GATE_EVIDENCE_REF="sha256:$expired_hash:r1/$expired_hash.attestation" \
|
||
GENERATION_ATTESTATION_ROOT="$attestation_root" \
|
||
SSH_BIN="$TMP_DIR/exec-ssh" bash "$ACTIVATE" --verify-r1-attestation >/dev/null 2>&1; then
|
||
fail "已过期 attestation 未拒绝"
|
||
fi
|
||
|
||
# 直接验证 DB-only 审计:生成、外部引用 fail-closed、旧 hash/路径校验。
|
||
FAKE_RC="$TMP_DIR/fake-rc"
|
||
mkdir -p "$FAKE_RC/evidence"
|
||
git -C "$FAKE_RC" init -q
|
||
git -C "$FAKE_RC" config user.email test@example.invalid
|
||
git -C "$FAKE_RC" config user.name staging-test
|
||
printf 'rc\n' >"$FAKE_RC/README"
|
||
git -C "$FAKE_RC" add README
|
||
git -C "$FAKE_RC" commit -qm init
|
||
|
||
cat >"$TMP_DIR/fake-audit-mysql" <<'SH'
|
||
#!/usr/bin/env bash
|
||
set -euo pipefail
|
||
cat >/dev/null
|
||
cat <<'OUT'
|
||
game_version_rows=53
|
||
game_version_package_url_nonempty=0
|
||
game_runtime_package_rows=54
|
||
game_runtime_package_url_nonempty=0
|
||
game_runtime_package_payload_rows=48
|
||
game_source_project_rows=41
|
||
game_source_project_url_nonempty=0
|
||
game_source_project_payload_rows=41
|
||
game_aigc_task_rows=78
|
||
game_aigc_result_package_url_nonempty=0
|
||
infra_file_rows=0
|
||
OUT
|
||
SH
|
||
chmod +x "$TMP_DIR/fake-audit-mysql"
|
||
|
||
audit_output="$(RC_RELEASE="$FAKE_RC" STORAGE_AUDIT_MYSQL_BIN="$TMP_DIR/fake-audit-mysql" \
|
||
bash "$STORAGE_AUDIT")"
|
||
audit_ref="$(printf '%s\n' "$audit_output" | sed -n 's/^STORAGE_AUDIT_PASS ref=//p')"
|
||
[[ "$audit_ref" =~ ^sha256:[0-9a-f]{64}:evidence/storage-audit\.tsv$ ]] || fail "审计引用格式非法"
|
||
rg -q '^object_storage_status=DB_ONLY_NA$' "$FAKE_RC/evidence/storage-audit.tsv" || fail "缺 DB_ONLY_NA"
|
||
rg -q '^minio_involved_prefix_count=0$' "$FAKE_RC/evidence/storage-audit.tsv" || fail "缺 MinIO 前缀事实"
|
||
RC_RELEASE="$FAKE_RC" STORAGE_AUDIT_MYSQL_BIN="$TMP_DIR/fake-audit-mysql" \
|
||
EXPECTED_STORAGE_AUDIT_REF="$audit_ref" bash "$STORAGE_AUDIT" >/dev/null
|
||
|
||
bad_ref="sha256:$(printf '0%.0s' $(seq 1 64)):evidence/storage-audit.tsv"
|
||
if RC_RELEASE="$FAKE_RC" STORAGE_AUDIT_MYSQL_BIN="$TMP_DIR/fake-audit-mysql" \
|
||
EXPECTED_STORAGE_AUDIT_REF="$bad_ref" bash "$STORAGE_AUDIT" >/dev/null 2>&1; then
|
||
fail "错误 storage audit hash 未被拒绝"
|
||
fi
|
||
|
||
cat >"$TMP_DIR/fake-audit-external" <<'SH'
|
||
#!/usr/bin/env bash
|
||
set -euo pipefail
|
||
cat >/dev/null
|
||
sed 's/game_runtime_package_url_nonempty=0/game_runtime_package_url_nonempty=1/' "$RC_TEST_TMP/audit-values"
|
||
SH
|
||
cp "$TMP_DIR/fake-audit-mysql" "$TMP_DIR/audit-values-script"
|
||
"$TMP_DIR/fake-audit-mysql" </dev/null >"$TMP_DIR/audit-values"
|
||
chmod +x "$TMP_DIR/fake-audit-external"
|
||
if RC_RELEASE="$FAKE_RC" STORAGE_AUDIT_MYSQL_BIN="$TMP_DIR/fake-audit-external" \
|
||
bash "$STORAGE_AUDIT" >/dev/null 2>&1; then
|
||
fail "发现外部 URL 时仍签发 DB_ONLY_NA"
|
||
fi
|
||
|
||
# 隔离恢复验证使用假 Docker 覆盖 run/ready/import/query/stop,不触碰真实容器。
|
||
printf 'CREATE DATABASE test;\n' | gzip -9 >"$TMP_DIR/dump.sql.gz"
|
||
cat >"$TMP_DIR/fake-docker" <<'SH'
|
||
#!/usr/bin/env bash
|
||
set -euo pipefail
|
||
printf '%s\n' "$*" >>"$RC_TEST_TMP/docker-calls.log"
|
||
case "$1" in
|
||
run) printf 'fake-container-id\n' ;;
|
||
stop) ;;
|
||
exec)
|
||
if printf '%s\n' "$*" | grep -q 'SELECT 1'; then
|
||
exit 0
|
||
fi
|
||
if printf '%s\n' "$*" | grep -q -- '-N -B'; then
|
||
cat >/dev/null
|
||
cat <<'OUT'
|
||
flyway_failed_rows=0
|
||
game_version_rows=53
|
||
game_version_package_url_nonempty=0
|
||
game_runtime_package_rows=54
|
||
game_runtime_package_url_nonempty=0
|
||
game_runtime_package_payload_rows=48
|
||
game_source_project_rows=41
|
||
game_source_project_url_nonempty=0
|
||
game_source_project_payload_rows=41
|
||
game_aigc_task_rows=78
|
||
game_aigc_result_package_url_nonempty=0
|
||
infra_file_rows=0
|
||
OUT
|
||
else
|
||
cat >/dev/null
|
||
fi
|
||
;;
|
||
*) exit 2 ;;
|
||
esac
|
||
SH
|
||
chmod +x "$TMP_DIR/fake-docker"
|
||
DUMP_FILE="$TMP_DIR/dump.sql.gz" EXPECTED_AUDIT_FILE="$FAKE_RC/evidence/storage-audit.tsv" \
|
||
DOCKER_BIN="$TMP_DIR/fake-docker" RESTORE_VERIFY_TIMEOUT_SEC=10 \
|
||
bash "$RESTORE_VERIFY" >"$TMP_DIR/restore.log"
|
||
rg -q '^RESTORE_VERIFY_PASS ' "$TMP_DIR/restore.log" || fail "假容器恢复未通过"
|
||
rg -q '^run .*--network none ' "$TMP_DIR/docker-calls.log" || fail "恢复容器未隔离网络"
|
||
rg -q '^stop ' "$TMP_DIR/docker-calls.log" || fail "恢复容器未销毁"
|
||
|
||
DUMP_FILE="$TMP_DIR/dump.sql.gz" EXPECTED_AUDIT_FILE="$FAKE_RC/evidence/storage-audit.tsv" \
|
||
RESTORE_VERIFY_DRY_RUN=1 bash "$RESTORE_VERIFY" >"$TMP_DIR/restore-dry.log"
|
||
rg -q '^RESTORE_VERIFY_DRY_RUN_PASS' "$TMP_DIR/restore-dry.log" || fail "恢复 dry-run 输入校验失败"
|
||
|
||
# 本地假服务验证 readiness smoke 全程只有 GET。
|
||
cat >"$TMP_DIR/fake-readiness.py" <<'PY'
|
||
import json
|
||
import os
|
||
from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer
|
||
from socketserver import TCPServer
|
||
|
||
port_file = os.environ["RC_TEST_PORT_FILE"]
|
||
method_file = os.environ["RC_TEST_METHOD_FILE"]
|
||
|
||
class Handler(BaseHTTPRequestHandler):
|
||
def log_message(self, _format, *_args):
|
||
return
|
||
|
||
def send_json(self, payload):
|
||
body = json.dumps(payload).encode()
|
||
self.send_response(200)
|
||
self.send_header("Content-Type", "application/json")
|
||
self.send_header("Content-Length", str(len(body)))
|
||
self.end_headers()
|
||
self.wfile.write(body)
|
||
|
||
def record(self):
|
||
with open(method_file, "a", encoding="utf-8") as handle:
|
||
handle.write(self.command + " " + self.path + "\n")
|
||
|
||
def do_GET(self):
|
||
self.record()
|
||
if self.path == "/actuator/health":
|
||
self.send_json({"status": "UP"})
|
||
elif self.path.startswith("/app-api/feed/zones"):
|
||
self.send_json({"code": 0, "data": [{"id": 2}]})
|
||
elif self.path.startswith("/app-api/feed/stream"):
|
||
self.send_json({"code": 0, "data": {"list": [{"gameId": 1, "versionId": 2}], "hasMore": False}})
|
||
elif self.path.startswith("/app-api/runtime/package/2"):
|
||
self.send_json({"code": 0, "data": {"versionId": 2}})
|
||
else:
|
||
self.send_json({"code": 404, "data": None})
|
||
|
||
def do_POST(self):
|
||
self.record()
|
||
self.send_json({"code": 500})
|
||
|
||
class LocalThreadingHTTPServer(ThreadingHTTPServer):
|
||
def server_bind(self):
|
||
# 跳过 HTTPServer.server_bind 的反向 DNS 查询,离线构建机不能依赖主机名解析。
|
||
TCPServer.server_bind(self)
|
||
self.server_name, self.server_port = self.server_address[:2]
|
||
|
||
server = LocalThreadingHTTPServer(("127.0.0.1", 0), Handler)
|
||
with open(port_file, "w", encoding="utf-8") as handle:
|
||
handle.write(str(server.server_port))
|
||
server.serve_forever()
|
||
PY
|
||
|
||
export RC_TEST_PORT_FILE="$TMP_DIR/port"
|
||
export RC_TEST_METHOD_FILE="$TMP_DIR/methods"
|
||
python3 "$TMP_DIR/fake-readiness.py" &
|
||
SERVER_PID=$!
|
||
for _ in $(seq 1 300); do
|
||
[ -s "$TMP_DIR/port" ] && break
|
||
sleep 0.1
|
||
done
|
||
[ -s "$TMP_DIR/port" ] || fail "假 readiness 服务未启动"
|
||
port="$(<"$TMP_DIR/port")"
|
||
BASE="http://127.0.0.1:$port" STUDIO_BASE="http://127.0.0.1:4173" \
|
||
bash "$SMOKE" >"$TMP_DIR/smoke.log"
|
||
if rg -n -v '^GET ' "$TMP_DIR/methods"; then
|
||
fail "默认 smoke 出现写请求"
|
||
fi
|
||
|
||
printf 'TEST_PASS staging RC 存储审计、隔离恢复与只读 smoke 均通过\n'
|