684 lines
38 KiB
Bash
Executable File
684 lines
38 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="r1-independent-signer"
|
||
GENERATION_ATTESTATION_TRUSTED_PUBLIC_KEY_FILE="/root/game-staging/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 -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)"
|
||
stop_old_line="$(rg -n '候选非数据面探针全绿,停止旧栈' "$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 "候选非数据面探针没有在停止旧栈前完成"
|
||
[ "$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)"
|
||
maintenance_install_line="$(rg -n '^install_maintenance_gate$' "$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 "旧栈完整恢复后未解除维护门"
|
||
! 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 -F GDA_STAGING_MAINT') rm -f "$state/reject-rule" ;;
|
||
'-w 5 -X GDA_STAGING_MAINT') 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_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 '/^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" ] || fail "维护门解除后残留 iptables 状态"
|
||
rg -q '^maintenance_gate=OPENED ' "$TMP_DIR/maintenance-evidence/maintenance-gate.status" || \
|
||
fail "维护门未留下 OPENED 终态证据"
|
||
|
||
# 在关闭 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"
|
||
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 'DifyCallbackServiceImplTest' "$PREPARE" || fail "prepare 未运行生成回调产物绑定测试"
|
||
rg -q 'PublishOrchestrationServiceImplTest' "$PREPARE" || fail "prepare 未运行发布编排测试"
|
||
rg -q 'test-dogfood-reviewer-security\.py' "$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 指针"
|
||
|
||
# 独立签发者用仓外私钥签名;发布机只持有信任公钥,签名、身份、run-id 和有效期任一漂移均拒绝。
|
||
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
|
||
exec "$@"
|
||
SH
|
||
chmod +x "$TMP_DIR/exec-ssh"
|
||
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
|
||
openssl pkey -in "$TMP_DIR/r1-private.pem" -pubout -out "$TMP_DIR/r1-public.pem" >/dev/null 2>&1
|
||
chmod 600 "$TMP_DIR/r1-private.pem" "$TMP_DIR/r1-public.pem"
|
||
trusted_public_key="$(realpath "$TMP_DIR/r1-public.pem")"
|
||
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=$GENERATION_ATTESTATION_EXPECTED_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" \
|
||
GENERATION_ATTESTATION_TRUSTED_PUBLIC_KEY_FILE="$trusted_public_key" \
|
||
SSH_BIN="$TMP_DIR/exec-ssh" bash "$ACTIVATE" --verify-r1-attestation >"$TMP_DIR/attestation-pass.log"
|
||
rg -q 'GENERATION_ATTESTATION_PASS' "$TMP_DIR/attestation-pass.log" || fail "独立 attestation 未通过"
|
||
|
||
if RC_COMMIT=0000000000000000000000000000000000000000 \
|
||
GENERATION_GATE_EVIDENCE_REF="$attestation_ref" GENERATION_ATTESTATION_ROOT="$attestation_root" \
|
||
GENERATION_ATTESTATION_TRUSTED_PUBLIC_KEY_FILE="$TMP_DIR/missing-public.pem" \
|
||
SSH_BIN="$TMP_DIR/exec-ssh" bash "$ACTIVATE" --verify-r1-attestation >/dev/null 2>&1; then
|
||
fail "缺信任公钥仍允许 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" \
|
||
GENERATION_ATTESTATION_TRUSTED_PUBLIC_KEY_FILE="$trusted_public_key" \
|
||
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" \
|
||
GENERATION_ATTESTATION_TRUSTED_PUBLIC_KEY_FILE="$trusted_public_key" \
|
||
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 GENERATION_ATTESTATION_TRUSTED_PUBLIC_KEY_FILE="$trusted_public_key" \
|
||
SSH_BIN="$TMP_DIR/exec-ssh" bash "$ACTIVATE" --verify-r1-attestation >/dev/null 2>&1; then
|
||
fail "run-id 漂移 attestation 未拒绝"
|
||
fi
|
||
if 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="$trusted_public_key" \
|
||
SSH_BIN="$TMP_DIR/exec-ssh" bash "$ACTIVATE" --verify-r1-attestation >/dev/null 2>&1; then
|
||
fail "issuer 漂移 attestation 未拒绝"
|
||
fi
|
||
|
||
chmod 644 "$attestation_file"
|
||
if RC_COMMIT=0000000000000000000000000000000000000000 \
|
||
GENERATION_GATE_EVIDENCE_REF="$attestation_ref" GENERATION_ATTESTATION_ROOT="$attestation_root" \
|
||
GENERATION_ATTESTATION_TRUSTED_PUBLIC_KEY_FILE="$trusted_public_key" \
|
||
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" \
|
||
GENERATION_ATTESTATION_TRUSTED_PUBLIC_KEY_FILE="$trusted_public_key" 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" \
|
||
GENERATION_ATTESTATION_TRUSTED_PUBLIC_KEY_FILE="$trusted_public_key" \
|
||
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" \
|
||
GENERATION_ATTESTATION_TRUSTED_PUBLIC_KEY_FILE="$trusted_public_key" \
|
||
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'
|