fix(release): close staging activation safety gaps
This commit is contained in:
parent
65aecc56aa
commit
d48cc60bbf
@ -6,7 +6,16 @@ set -euo pipefail
|
||||
RC_COMMIT="${RC_COMMIT:-}"
|
||||
RC_HOST="${RC_HOST:-root@100.64.0.7}"
|
||||
RC_ROOT="${RC_ROOT:-/root/game-staging/releases}"
|
||||
GENERATION_ATTESTATION_ROOT="${GENERATION_ATTESTATION_ROOT:-/root/game-staging/attestations}"
|
||||
BOOTSTRAP_ADMIN_TOKEN_FILE="${BOOTSTRAP_ADMIN_TOKEN_FILE:-}"
|
||||
ACTIVATION_TIMEOUT_SEC="${ACTIVATION_TIMEOUT_SEC:-180}"
|
||||
ACTIVATION_MODE="activate"
|
||||
if [ "${1:-}" = "--verify-r1-attestation" ]; then
|
||||
ACTIVATION_MODE="verify-r1-attestation"
|
||||
elif [ -n "${1:-}" ]; then
|
||||
printf '未知参数:%s\n' "$1" >&2
|
||||
exit 2
|
||||
fi
|
||||
if [ -n "${GENERATION_GATE_EVIDENCE_REF:-}" ] && [ -n "${GENERATION_GATE_REF:-}" ] \
|
||||
&& [ "$GENERATION_GATE_EVIDENCE_REF" != "$GENERATION_GATE_REF" ]; then
|
||||
printf 'GENERATION_GATE_EVIDENCE_REF 与 GENERATION_GATE_REF 冲突,拒绝选择其一继续。\n' >&2
|
||||
@ -51,13 +60,22 @@ if [ -n "$STORAGE_AUDIT_REF" ] \
|
||||
printf 'STORAGE_AUDIT_REF 非法:只接受 sha256:<64hex>:evidence/storage-audit.tsv;留空则远端实时生成。\n' >&2
|
||||
exit 2
|
||||
fi
|
||||
if [ "$ACTIVATION_MODE" = activate ] && [[ ! "$BOOTSTRAP_ADMIN_TOKEN_FILE" =~ ^/[A-Za-z0-9._/-]+$ ]]; then
|
||||
printf 'BOOTSTRAP_ADMIN_TOKEN_FILE 必填,且必须是 mini-desktop 上的安全绝对路径。\n' >&2
|
||||
exit 2
|
||||
fi
|
||||
|
||||
printf '激活 staging RC:host=%s commit=%s\n' "$RC_HOST" "$RC_COMMIT"
|
||||
printf '注意:该操作会执行数据库迁移并切换五个 staging 服务。\n'
|
||||
if [ "$ACTIVATION_MODE" = activate ]; then
|
||||
printf '注意:该操作会执行数据库迁移并切换五个 staging 服务。\n'
|
||||
else
|
||||
printf '仅校验独立 R1 attestation,不访问数据库、不切换服务。\n'
|
||||
fi
|
||||
|
||||
"$SSH_BIN" -o ProxyCommand=none "$RC_HOST" bash -s -- \
|
||||
"$RC_COMMIT" "$RC_ROOT" "$ACTIVATION_TIMEOUT_SEC" "$STORAGE_AUDIT_REF" \
|
||||
"$GENERATION_GATE_EVIDENCE_REF" <<'REMOTE_SCRIPT'
|
||||
"$GENERATION_GATE_EVIDENCE_REF" "$GENERATION_ATTESTATION_ROOT" "$ACTIVATION_MODE" \
|
||||
"$BOOTSTRAP_ADMIN_TOKEN_FILE" <<'REMOTE_SCRIPT'
|
||||
set -Eeuo pipefail
|
||||
|
||||
commit="$1"
|
||||
@ -65,6 +83,9 @@ root="$2"
|
||||
timeout_sec="$3"
|
||||
expected_storage_audit_ref="$4"
|
||||
expected_generation_gate_ref="$5"
|
||||
attestation_root="$6"
|
||||
activation_mode="$7"
|
||||
bootstrap_admin_token_file="$8"
|
||||
release="$root/$commit"
|
||||
active_pointer="$root/active"
|
||||
stable_prefix="gda-staging"
|
||||
@ -79,16 +100,36 @@ pointer_changed=0
|
||||
previous_was_active=0
|
||||
previous="none"
|
||||
previous_valid=0
|
||||
migration_started=0
|
||||
database_restore_required=0
|
||||
database_restore_rc=not-run
|
||||
bootstrap_token_validated=0
|
||||
|
||||
mkdir -p "$evidence"
|
||||
chmod 700 "$evidence"
|
||||
umask 077
|
||||
exec > >(tee -a "$activation_log") 2>&1
|
||||
if [ "$activation_mode" = activate ]; then
|
||||
mkdir -p "$evidence"
|
||||
chmod 700 "$evidence"
|
||||
exec > >(tee -a "$activation_log") 2>&1
|
||||
else
|
||||
evidence="$(mktemp -d)"
|
||||
trap 'rm -rf "$evidence"' EXIT
|
||||
fi
|
||||
|
||||
log_step() {
|
||||
printf '[%s] %s\n' "$(date -u +%Y-%m-%dT%H:%M:%SZ)" "$1"
|
||||
}
|
||||
|
||||
delete_bootstrap_token() {
|
||||
[ "$bootstrap_token_validated" -eq 1 ] || return 0
|
||||
if [ -f "$bootstrap_admin_token_file" ] && [ ! -L "$bootstrap_admin_token_file" ]; then
|
||||
rm -f -- "$bootstrap_admin_token_file"
|
||||
log_step "bootstrap admin token 文件已安全删除"
|
||||
else
|
||||
log_step "bootstrap admin token 文件状态异常,拒绝跟随或删除 symlink"
|
||||
return 1
|
||||
fi
|
||||
}
|
||||
|
||||
unit_name() {
|
||||
printf '%s-%s' "$1" "$2"
|
||||
}
|
||||
@ -187,7 +228,8 @@ set +a
|
||||
export SPRING_PROFILES_ACTIVE=staging
|
||||
export AIGC_WORKER_URL=http://127.0.0.1:${worker_port}/generate
|
||||
exec java -jar ${quoted_target}/game-cloud/huijing-server/target/huijing-server.jar \\
|
||||
--server.port=${backend_port} --spring.cloud.nacos.discovery.enabled=${discovery_enabled}
|
||||
--server.port=${backend_port} --spring.cloud.nacos.discovery.enabled=${discovery_enabled} \\
|
||||
--spring.flyway.enabled=false
|
||||
EOF
|
||||
|
||||
cat >"$launch_dir/studio.sh" <<EOF
|
||||
@ -257,6 +299,24 @@ start_stack() {
|
||||
wait_url "$label admin" "http://127.0.0.1:$admin_port/"
|
||||
}
|
||||
|
||||
start_candidate_probe() {
|
||||
local target="$1" label="$2" prefix="$3" studio_port="$4" admin_port="$5"
|
||||
local agent_port="$6" worker_port="$7" launch_dir
|
||||
launch_dir="$(write_launchers "$target" "$label" 18080 "$studio_port" "$admin_port" \
|
||||
"$agent_port" "$worker_port" false)"
|
||||
|
||||
# 当前应用没有可证明关闭全部 @Scheduled 的全局开关,候选 backend 不得连接共享数据面启动。
|
||||
printf 'candidate_backend=SKIPPED_NO_ISOLATED_DATA_PLANE\n' >"$evidence/candidate-backend.status"
|
||||
run_unit "$(unit_name "$prefix" cheap-agent)" "$launch_dir/cheap-agent.sh"
|
||||
wait_port "$label cheap-agent" "$agent_port"
|
||||
run_unit "$(unit_name "$prefix" cheap-worker)" "$launch_dir/cheap-worker.sh"
|
||||
wait_json_health "$label cheap-worker" "http://127.0.0.1:$worker_port/health" ok true
|
||||
run_unit "$(unit_name "$prefix" studio)" "$launch_dir/studio.sh"
|
||||
wait_url "$label studio" "http://127.0.0.1:$studio_port/"
|
||||
run_unit "$(unit_name "$prefix" admin)" "$launch_dir/admin.sh"
|
||||
wait_url "$label admin" "http://127.0.0.1:$admin_port/"
|
||||
}
|
||||
|
||||
valid_prepared_release() {
|
||||
local target="$1" expected="${2:-}"
|
||||
[ -d "$target/.git" ] || return 1
|
||||
@ -271,29 +331,55 @@ valid_prepared_release() {
|
||||
}
|
||||
|
||||
verify_generation_gate_evidence() {
|
||||
local ref="$1" expected_commit="$2" hash_and_path expected_hash relative_path actual_hash object_type
|
||||
local ref="$1" expected_commit="$2" hash_and_path expected_hash relative_path actual_hash
|
||||
local source_path canonical_root canonical_source component walk mode
|
||||
hash_and_path="${ref#sha256:}"
|
||||
expected_hash="${hash_and_path%%:*}"
|
||||
relative_path="${hash_and_path#*:}"
|
||||
|
||||
# wrapper 已做路径语法校验;远端仍独立确认它是候选 commit 中的普通 tracked 文件。
|
||||
object_type="$(git -C "$release" cat-file -t "$expected_commit:$relative_path" 2>/dev/null || true)"
|
||||
[ "$object_type" = blob ] || {
|
||||
log_step "生成门证据不是候选 commit 内的 tracked blob path=$relative_path"
|
||||
# attestation 是 R1 执行方在 commit 之外生成的内容寻址文件;hash 在外部引用中,避免“证明文件属于被证明 commit”的自引用。
|
||||
[ "${relative_path##*/}" = "$expected_hash.attestation" ] || {
|
||||
log_step "生成门 attestation 文件名未绑定 SHA-256 path=$relative_path"
|
||||
return 1
|
||||
}
|
||||
[ -f "$release/$relative_path" ] && [ ! -L "$release/$relative_path" ] || {
|
||||
log_step "生成门证据不是 RC 内普通文件 path=$relative_path"
|
||||
[ -d "$attestation_root" ] && [ ! -L "$attestation_root" ] || {
|
||||
log_step "attestation 根目录缺失或为 symlink root=$attestation_root"
|
||||
return 1
|
||||
}
|
||||
actual_hash="$(sha256sum "$release/$relative_path" | awk '{print $1}')"
|
||||
canonical_root="$(realpath "$attestation_root")" || return 1
|
||||
[ "$canonical_root" = "$attestation_root" ] || {
|
||||
log_step "attestation 根目录必须使用 canonical 绝对路径 root=$attestation_root"
|
||||
return 1
|
||||
}
|
||||
walk="$attestation_root"
|
||||
IFS='/' read -r -a path_components <<<"$relative_path"
|
||||
for component in "${path_components[@]}"; do
|
||||
walk="$walk/$component"
|
||||
[ ! -L "$walk" ] || {
|
||||
log_step "attestation 路径不得包含 symlink path=$relative_path"
|
||||
return 1
|
||||
}
|
||||
done
|
||||
source_path="$attestation_root/$relative_path"
|
||||
[ -f "$source_path" ] || {
|
||||
log_step "attestation 不是普通文件 path=$relative_path"
|
||||
return 1
|
||||
}
|
||||
canonical_source="$(realpath "$source_path")" || return 1
|
||||
[ "$canonical_source" = "$source_path" ] || return 1
|
||||
mode="$(stat -c '%a' "$source_path" 2>/dev/null || stat -f '%Lp' "$source_path")"
|
||||
[ "$mode" = 600 ] || {
|
||||
log_step "attestation 权限必须为 0600 actual=$mode path=$relative_path"
|
||||
return 1
|
||||
}
|
||||
actual_hash="$(sha256sum "$source_path" | awk '{print $1}')"
|
||||
[ "$actual_hash" = "$expected_hash" ] || {
|
||||
log_step "生成门证据 SHA-256 不匹配 path=$relative_path"
|
||||
return 1
|
||||
}
|
||||
|
||||
# 证据由 R1 执行方生成,激活器只验证,不创建或改写任何“通过”结论。
|
||||
python3 - "$release/$relative_path" "$expected_commit" <<'PY'
|
||||
python3 - "$source_path" "$expected_commit" <<'PY'
|
||||
import sys
|
||||
|
||||
path, expected_commit = sys.argv[1:]
|
||||
@ -336,6 +422,78 @@ PY
|
||||
log_step "生成 R1 门证据校验 PASS path=$relative_path"
|
||||
}
|
||||
|
||||
if [ "$activation_mode" = verify-r1-attestation ]; then
|
||||
verify_generation_gate_evidence "$expected_generation_gate_ref" "$commit"
|
||||
printf 'GENERATION_ATTESTATION_PASS commit=%s ref=%s\n' "$commit" "$expected_generation_gate_ref"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
exec 9>"$root/.activation.lock"
|
||||
if ! flock -n 9; then
|
||||
log_step "已有 staging activation 正在执行,拒绝并发部署"
|
||||
exit 75
|
||||
fi
|
||||
|
||||
flyway_history() {
|
||||
docker exec game-staging-mysql sh -lc \
|
||||
'MYSQL_PWD="$MYSQL_ROOT_PASSWORD" exec mysql -uroot -N -B ruoyi-vue-pro -e "SELECT installed_rank,COALESCE(version,\"\"),description,type,script,checksum,success FROM flyway_schema_history ORDER BY installed_rank"'
|
||||
}
|
||||
|
||||
run_flyway_migrations() {
|
||||
local runtime_dir="$evidence/flyway-runtime" source_file="$evidence/StagingFlywayMigration.java"
|
||||
local class_dir="$evidence/flyway-classes" jar="$release/game-cloud/huijing-server/target/huijing-server.jar"
|
||||
mkdir -p "$runtime_dir" "$class_dir"
|
||||
java -Djarmode=tools -jar "$jar" extract --destination "$runtime_dir" >/dev/null
|
||||
cat >"$source_file" <<'JAVA'
|
||||
import org.flywaydb.core.Flyway;
|
||||
|
||||
/** 只执行 Flyway,不启动 Spring、消费者、定时任务或 Web 服务。 */
|
||||
public final class StagingFlywayMigration {
|
||||
public static void main(String[] args) {
|
||||
if (args.length != 2) {
|
||||
throw new IllegalArgumentException("需要 migration 与 staging migration 两个目录");
|
||||
}
|
||||
String url = System.getenv("STAGING_FLYWAY_URL");
|
||||
String user = System.getenv("STAGING_FLYWAY_USER");
|
||||
String password = System.getenv("STAGING_FLYWAY_PASSWORD");
|
||||
if (url == null || user == null || password == null) {
|
||||
throw new IllegalStateException("缺少 Flyway 数据库凭据");
|
||||
}
|
||||
Flyway flyway = Flyway.configure()
|
||||
.dataSource(url, user, password)
|
||||
.locations("filesystem:" + args[0], "filesystem:" + args[1])
|
||||
.failOnMissingLocations(true)
|
||||
.load();
|
||||
flyway.validate();
|
||||
var result = flyway.migrate();
|
||||
System.out.printf("FLYWAY_MIGRATION_PASS executed=%d target=%s%n",
|
||||
result.migrationsExecuted, result.targetSchemaVersion);
|
||||
}
|
||||
}
|
||||
JAVA
|
||||
javac -cp "$runtime_dir/lib/*" -d "$class_dir" "$source_file"
|
||||
STAGING_FLYWAY_URL='jdbc:mysql://127.0.0.1:13306/ruoyi-vue-pro?useSSL=false&serverTimezone=Asia/Shanghai&allowPublicKeyRetrieval=true&nullCatalogMeansCurrent=true' \
|
||||
STAGING_FLYWAY_USER=root STAGING_FLYWAY_PASSWORD="$MYSQL_ROOT_PASSWORD" \
|
||||
java -cp "$class_dir:$runtime_dir/lib/*" StagingFlywayMigration \
|
||||
"$release/game-cloud/huijing-server/src/main/resources/db/migration" \
|
||||
"$release/game-cloud/huijing-server/src/main/resources/db/staging" \
|
||||
>"$evidence/flyway-migration.log" 2>&1
|
||||
grep -q '^FLYWAY_MIGRATION_PASS ' "$evidence/flyway-migration.log"
|
||||
}
|
||||
|
||||
restore_database_snapshot() {
|
||||
local dump="$evidence/pre-migration-ruoyi-vue-pro.sql.gz"
|
||||
[ -s "$dump" ] || return 1
|
||||
stop_stack "$stable_prefix"
|
||||
docker exec game-staging-mysql sh -lc \
|
||||
'MYSQL_PWD="$MYSQL_ROOT_PASSWORD" exec mysql -uroot -e "DROP DATABASE IF EXISTS `ruoyi-vue-pro`"' || return 1
|
||||
gzip -dc "$dump" | docker exec -i game-staging-mysql sh -lc \
|
||||
'MYSQL_PWD="$MYSQL_ROOT_PASSWORD" exec mysql -uroot' || return 1
|
||||
flyway_history >"$evidence/flyway-after-compensation.tsv" || return 1
|
||||
cmp -s "$evidence/flyway-before.tsv" "$evidence/flyway-after-compensation.tsv" || return 1
|
||||
printf 'DATABASE_COMPENSATION=PASS snapshot=%s\n' "$dump" >"$evidence/database-compensation.status"
|
||||
}
|
||||
|
||||
restore_active_pointer() {
|
||||
local target="$1" temp="$root/.active-rollback-$run_id"
|
||||
rm -f "$temp"
|
||||
@ -355,6 +513,17 @@ failure_handler() {
|
||||
capture_journals "$candidate_prefix" candidate-failed
|
||||
capture_journals "$stable_prefix" stable-failed
|
||||
stop_stack "$candidate_prefix"
|
||||
if [ "$database_restore_required" -eq 1 ]; then
|
||||
log_step "迁移后失败,停止新栈并恢复迁移前数据库快照"
|
||||
restore_database_snapshot
|
||||
database_restore_rc=$?
|
||||
printf 'database_restore_rc=%s\n' "$database_restore_rc" >"$evidence/database-restore-result.txt"
|
||||
if [ "$database_restore_rc" -ne 0 ]; then
|
||||
log_step "数据库补偿失败,禁止宣称服务回滚成功"
|
||||
printf 'FIRST_RC_FULL_ROLLBACK=UNAVAILABLE database_restore=FAILED\n' >"$evidence/rollback-result.txt"
|
||||
exit 71
|
||||
fi
|
||||
fi
|
||||
if [ "$switch_started" -eq 1 ]; then
|
||||
stop_stack "$stable_prefix"
|
||||
if [ "$previous_was_active" -eq 1 ] && [ "$previous_valid" -eq 1 ]; then
|
||||
@ -363,12 +532,15 @@ failure_handler() {
|
||||
rollback_rc=$?
|
||||
printf 'rollback_service_rc=%s\n' "$rollback_rc" >"$evidence/rollback-result.txt"
|
||||
else
|
||||
printf 'rollback_service_rc=not-required\n' >"$evidence/rollback-result.txt"
|
||||
printf 'rollback_service_rc=not-available\nFIRST_RC_FULL_ROLLBACK=UNAVAILABLE\n' >"$evidence/rollback-result.txt"
|
||||
fi
|
||||
fi
|
||||
if [ "$pointer_changed" -eq 1 ]; then
|
||||
restore_active_pointer "$previous"
|
||||
fi
|
||||
if [ "$bootstrap_token_validated" -eq 1 ]; then
|
||||
log_step "激活失败,保留 0600 bootstrap admin token 供补偿后重试或人工吊销"
|
||||
fi
|
||||
printf 'failed commit=%s at=%s evidence=%s\n' "$commit" "$(date -u +%Y-%m-%dT%H:%M:%SZ)" "$evidence" \
|
||||
>"$release/RC_STATUS"
|
||||
chmod -R go-rwx "$evidence" "$release/RC_STATUS"
|
||||
@ -391,15 +563,17 @@ case "$(stat -c '%a' "$env_file")" in
|
||||
*) log_step "拒绝读取权限过宽的 $env_file"; false ;;
|
||||
esac
|
||||
|
||||
# 只校验键存在和非空,绝不输出值。
|
||||
(
|
||||
set -a
|
||||
source "$env_file"
|
||||
set +a
|
||||
: "${MYSQL_ROOT_PASSWORD:?}" "${REDIS_PASSWORD:?}" "${NACOS_PASSWORD:?}"
|
||||
: "${AIGC_CALLBACK_SECRET:?}" "${NEWAPI_KEY:?}"
|
||||
[ "${AIGC_EXECUTOR_ENABLED:-}" = "true" ]
|
||||
)
|
||||
# 只校验键存在和非空,绝不输出值;后续独立 Flyway 进程从当前受控环境读取数据库凭据。
|
||||
set -a
|
||||
source "$env_file"
|
||||
set +a
|
||||
: "${MYSQL_ROOT_PASSWORD:?}" "${REDIS_PASSWORD:?}" "${NACOS_PASSWORD:?}"
|
||||
: "${AIGC_CALLBACK_SECRET:?}" "${NEWAPI_KEY:?}"
|
||||
[ -n "${DOGFOOD_PASSWORD_SEED:-}" ] || {
|
||||
log_step "缺 DOGFOOD_PASSWORD_SEED,无法确定性预置内测账号"
|
||||
false
|
||||
}
|
||||
[ "${AIGC_EXECUTOR_ENABLED:-}" = "true" ]
|
||||
|
||||
cd "$release"
|
||||
sha256sum -c evidence/artifacts.sha256 >"$evidence/artifact-verification.txt"
|
||||
@ -436,6 +610,22 @@ if [ "$previous_was_active" -eq 1 ] && [ "$previous_valid" -ne 1 ]; then
|
||||
fi
|
||||
printf 'previous=%s\nprevious_was_active=%s\n' "$previous" "$previous_was_active" >"$evidence/activation-context.txt"
|
||||
|
||||
log_step "启动不接共享数据面的候选探针;backend 因缺全局禁调度开关明确跳过"
|
||||
for port in 14173 14174 18300 19501; do
|
||||
if port_listening "$port"; then
|
||||
log_step "候选端口已占用 port=$port"
|
||||
false
|
||||
fi
|
||||
done
|
||||
start_candidate_probe "$release" candidate "$candidate_prefix" 14173 14174 18300 19501
|
||||
capture_journals "$candidate_prefix" candidate-ready
|
||||
|
||||
log_step "候选非数据面探针全绿,停止旧栈后执行独立 Flyway 迁移"
|
||||
switch_started=1
|
||||
capture_journals "$stable_prefix" pre-switch-old
|
||||
stop_stack "$stable_prefix"
|
||||
|
||||
# 旧栈停写后再生成补偿快照,避免候选探针期间的新写入在失败恢复时丢失。
|
||||
log_step "迁移前数据库快照"
|
||||
docker inspect game-staging-mysql >/dev/null
|
||||
docker exec game-staging-mysql sh -lc \
|
||||
@ -452,31 +642,15 @@ EXPECTED_AUDIT_FILE="$evidence/storage-audit.tsv" MYSQL_IMAGE="$mysql_image" \
|
||||
grep -q '^RESTORE_VERIFY_PASS ' "$evidence/mysql-restore-verify.log"
|
||||
log_step "数据库快照隔离恢复验证 PASS"
|
||||
|
||||
log_step "启动候选栈;backend 启动时先执行 Flyway migrations"
|
||||
for port in 18080 14173 14174 18300 19501; do
|
||||
if port_listening "$port"; then
|
||||
log_step "候选端口已占用 port=$port"
|
||||
false
|
||||
fi
|
||||
done
|
||||
start_stack "$release" candidate "$candidate_prefix" 18080 14173 14174 18300 19501 false
|
||||
|
||||
docker exec game-staging-mysql sh -lc \
|
||||
'MYSQL_PWD="$MYSQL_ROOT_PASSWORD" exec mysql -uroot -N -B ruoyi-vue-pro -e "SELECT installed_rank,version,success FROM flyway_schema_history ORDER BY installed_rank DESC LIMIT 20"' \
|
||||
>"$evidence/flyway-after-candidate.tsv"
|
||||
flyway_history >"$evidence/flyway-before.tsv"
|
||||
migration_started=1
|
||||
database_restore_required=1
|
||||
run_flyway_migrations
|
||||
flyway_history >"$evidence/flyway-after-migration.tsv"
|
||||
flyway_failed_count="$(docker exec game-staging-mysql sh -lc \
|
||||
'MYSQL_PWD="$MYSQL_ROOT_PASSWORD" exec mysql -uroot -N -B ruoyi-vue-pro -e "SELECT COUNT(*) FROM flyway_schema_history WHERE success <> 1"')"
|
||||
[ "$flyway_failed_count" = "0" ]
|
||||
|
||||
log_step "候选栈执行只读 smoke"
|
||||
BASE=http://127.0.0.1:18080 STUDIO_BASE=http://127.0.0.1:14173 \
|
||||
bash "$release/deploy/smoke-test.sh" >"$evidence/candidate-smoke.log" 2>&1
|
||||
capture_journals "$candidate_prefix" candidate-ready
|
||||
|
||||
log_step "候选栈全绿,开始切换正式五端口"
|
||||
switch_started=1
|
||||
capture_journals "$stable_prefix" pre-switch-old
|
||||
stop_stack "$stable_prefix"
|
||||
log_step "独立 Flyway 迁移 PASS,开始切换正式五端口"
|
||||
for port in 48080 4173 4174 8300 9501; do
|
||||
if port_listening "$port"; then
|
||||
log_step "正式端口仍被非受管进程占用 port=$port"
|
||||
@ -485,6 +659,114 @@ for port in 48080 4173 4174 8300 9501; do
|
||||
done
|
||||
|
||||
start_stack "$release" stable "$stable_prefix" 48080 4173 4174 8300 9501 true
|
||||
|
||||
log_step "校验 bootstrap admin token 文件"
|
||||
[ -f "$bootstrap_admin_token_file" ] && [ ! -L "$bootstrap_admin_token_file" ]
|
||||
bootstrap_token_mode="$(stat -c '%a' "$bootstrap_admin_token_file" 2>/dev/null || stat -f '%Lp' "$bootstrap_admin_token_file")"
|
||||
[ "$bootstrap_token_mode" = 600 ] || {
|
||||
log_step "bootstrap admin token 文件权限必须为 0600 actual=$bootstrap_token_mode"
|
||||
false
|
||||
}
|
||||
bootstrap_token_validated=1
|
||||
|
||||
# 默认 admin 尚有效时先完成 40 人预置;事务返回即表示 Nacos 已逐字恢复,之后才允许禁用 admin。
|
||||
log_step "使用 bootstrap admin token 执行 40 人账号预置事务"
|
||||
account_provision_evidence="$evidence/account-provision"
|
||||
ADMIN_TOKEN_FILE="$bootstrap_admin_token_file" \
|
||||
DOGFOOD_PROVISION_ACK='PROVISION_DOGFOOD_WITH_NACOS_TRANSACTION' \
|
||||
BASE=http://127.0.0.1:48080 MYSQL_SSH_HOST='' \
|
||||
DOGFOOD_ACCOUNTS_FILE="$evidence/dogfood-accounts.tsv" \
|
||||
EVIDENCE_DIR="$account_provision_evidence" \
|
||||
bash "$release/deploy/provision-dogfood-with-nacos-transaction.sh" \
|
||||
>"$evidence/account-provision-transaction.log" 2>&1
|
||||
grep -q '^pass restored=true ' "$account_provision_evidence/transaction.status"
|
||||
grep -q 'PROVISION_PASS accounts=40 creators=10 role_mismatches=0 claimed_accounts=40 duplicate_claims=0' \
|
||||
"$account_provision_evidence/provision.log" || {
|
||||
log_step "40 人终验缺失;若额度池不足,请先运行受控 new-api import 补建 neice_051-054"
|
||||
false
|
||||
}
|
||||
chmod 600 "$evidence/account-provision-transaction.log" "$evidence/dogfood-accounts.tsv"
|
||||
log_step "40 人账号与额度终验 PASS,Nacos 已恢复"
|
||||
|
||||
log_step "轮换并验证狗粮审核账号"
|
||||
reviewer_credentials="$evidence/reviewer-credentials.json"
|
||||
reviewer_evidence="$evidence/reviewer-evidence.json"
|
||||
reviewer_runtime_evidence="$evidence/reviewer-runtime-verification.json"
|
||||
python3 "$release/deploy/manage-dogfood-reviewers.py" \
|
||||
--ack ROTATE_DOGFOOD_REVIEWERS \
|
||||
--admin-token-file "$bootstrap_admin_token_file" \
|
||||
--credentials-out "$reviewer_credentials" \
|
||||
--evidence-out "$reviewer_evidence" \
|
||||
>"$evidence/reviewer-rotation.log" 2>&1
|
||||
|
||||
# 工具已在轮换时验证默认公开密码失败;这里再验证新账号可登录,且被禁用 admin 的 bootstrap token 已失效。
|
||||
python3 - "$reviewer_credentials" "$reviewer_evidence" "$bootstrap_admin_token_file" \
|
||||
"$reviewer_runtime_evidence" <<'PY'
|
||||
import json,os,stat,sys
|
||||
from urllib.error import HTTPError,URLError
|
||||
from urllib.request import Request,urlopen
|
||||
|
||||
credentials_path,evidence_path,token_path,output_path=map(os.path.abspath,sys.argv[1:])
|
||||
|
||||
def read_private(path):
|
||||
flags=os.O_RDONLY|getattr(os,"O_NOFOLLOW",0)
|
||||
fd=os.open(path,flags)
|
||||
with os.fdopen(fd,"r",encoding="utf-8") as handle:
|
||||
mode=stat.S_IMODE(os.fstat(handle.fileno()).st_mode)
|
||||
if mode != 0o600 or not stat.S_ISREG(os.fstat(handle.fileno()).st_mode):
|
||||
raise RuntimeError(f"敏感文件必须是 0600 普通文件 path={path}")
|
||||
return handle.read()
|
||||
|
||||
credentials=json.loads(read_private(credentials_path))
|
||||
rotation=json.loads(read_private(evidence_path))
|
||||
bootstrap_token=read_private(token_path).strip()
|
||||
if credentials.get("status") != "COMPLETE" or rotation.get("status") != "PASS":
|
||||
raise RuntimeError("审核账号轮换输出未收口")
|
||||
accounts=credentials.get("accounts") or {}
|
||||
rotation_accounts={item.get("username"):item for item in rotation.get("accounts") or []}
|
||||
if rotation_accounts.get("admin",{}).get("defaultPasswordLoginCode") not in (1002000000,1002000001):
|
||||
raise RuntimeError("默认公开 admin 密码没有确定拒绝证据")
|
||||
|
||||
def request_json(request):
|
||||
try:
|
||||
with urlopen(request,timeout=10) as response:
|
||||
return response.status,json.loads(response.read().decode("utf-8"))
|
||||
except HTTPError as error:
|
||||
body=error.read().decode("utf-8")
|
||||
return error.code,json.loads(body) if body else {}
|
||||
except (URLError,TimeoutError,json.JSONDecodeError) as error:
|
||||
raise RuntimeError("审核账号运行态验证请求失败") from error
|
||||
|
||||
verified=[]
|
||||
for username in ("dogfood_reviewer","dogfood_readonly"):
|
||||
password=accounts.get(username)
|
||||
if not isinstance(password,str) or not password:
|
||||
raise RuntimeError(f"审核账号凭据缺失 username={username}")
|
||||
payload=json.dumps({"username":username,"password":password}).encode("utf-8")
|
||||
status,body=request_json(Request("http://127.0.0.1:48080/admin-api/system/auth/login",
|
||||
data=payload,method="POST",headers={"Content-Type":"application/json","tenant-id":"1"}))
|
||||
if status != 200 or body.get("code") != 0 or not (body.get("data") or {}).get("accessToken"):
|
||||
raise RuntimeError(f"审核账号无法登录 username={username}")
|
||||
verified.append(username)
|
||||
|
||||
status,body=request_json(Request(
|
||||
"http://127.0.0.1:48080/admin-api/system/auth/get-permission-info",
|
||||
method="GET",headers={"Authorization":f"Bearer {bootstrap_token}","tenant-id":"1"}))
|
||||
if status == 200 and body.get("code") == 0:
|
||||
raise RuntimeError("bootstrap admin 旧 token 仍然有效")
|
||||
|
||||
flags=os.O_WRONLY|os.O_CREAT|os.O_EXCL|getattr(os,"O_NOFOLLOW",0)
|
||||
fd=os.open(output_path,flags,0o600)
|
||||
with os.fdopen(fd,"w",encoding="utf-8") as handle:
|
||||
json.dump({"status":"PASS","loginVerified":verified,
|
||||
"defaultAdminPasswordRejected":True,"bootstrapTokenInvalidated":True},
|
||||
handle,ensure_ascii=False,indent=2)
|
||||
handle.write("\n")
|
||||
PY
|
||||
chmod 600 "$reviewer_credentials" "$reviewer_evidence" "$reviewer_runtime_evidence" \
|
||||
"$evidence/reviewer-rotation.log"
|
||||
log_step "狗粮审核账号轮换与运行态验证 PASS"
|
||||
|
||||
BASE=http://127.0.0.1:48080 STUDIO_BASE=http://127.0.0.1:4173 \
|
||||
bash "$release/deploy/smoke-test.sh" >"$evidence/stable-smoke.log" 2>&1
|
||||
capture_journals "$stable_prefix" stable-ready
|
||||
@ -499,6 +781,7 @@ printf 'active commit=%s at=%s evidence=%s\n' "$commit" "$(date -u +%Y-%m-%dT%H:
|
||||
|
||||
stop_stack "$candidate_prefix"
|
||||
chmod -R go-rwx "$evidence" "$release/RC_STATUS"
|
||||
delete_bootstrap_token
|
||||
trap - ERR
|
||||
log_step "RC_ACTIVATION_PASS commit=$commit active=$release"
|
||||
log_step "ACTIVATION_EVIDENCE=$evidence"
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
#!/usr/bin/env bash
|
||||
# 在 mini-desktop 的全新目录准备一个 commit-pinned staging RC。
|
||||
# 本脚本只克隆、构建和留证,不切换 current、不停止或启动任何服务。
|
||||
# 本脚本只克隆、构建和留证,不切换 active、不停止或启动任何服务。
|
||||
set -euo pipefail
|
||||
|
||||
RC_COMMIT="${RC_COMMIT:-}"
|
||||
@ -85,28 +85,90 @@ printf '%s\n' "$previous" >"$evidence/rollback-target.txt"
|
||||
printf 'admin_stage_env_sha256=%s\n' "$(sha256sum game-admin/.env.stage | awk '{print $1}')"
|
||||
} >"$evidence/config-snapshot.txt"
|
||||
|
||||
printf '[1/6] 构建 Java 后端(跳过全量后端测试)\n'
|
||||
printf '[1/7] 构建 Java 后端(构建阶段跳过全量测试,下一步单独执行安全关键集)\n'
|
||||
(cd game-cloud && mvn clean install -pl huijing-server -am -DskipTests)
|
||||
|
||||
printf '[2/6] 构建 game-studio staging\n'
|
||||
printf '[2/7] 运行安全关键 Java 测试\n'
|
||||
security_java_tests=(
|
||||
DogfoodStartupValidatorTest
|
||||
DogfoodSecurityStartupValidatorTest
|
||||
DogfoodRbacSeedTest
|
||||
TokenAuthenticationFilterTest
|
||||
AuthenticationEntryPointImplTest
|
||||
PassportServiceImplTest
|
||||
ProjectServiceImplTest
|
||||
RuntimePackageServiceImplTest
|
||||
RuntimePackageApiImplDogfoodTest
|
||||
AdminRuntimeControllerDogfoodTest
|
||||
AppFeedControllerDogfoodTest
|
||||
AdControllerDogfoodTest
|
||||
TradeControllerDogfoodTest
|
||||
AdminNewapiQuotaControllerTest
|
||||
NewapiQuotaClaimConsumerTest
|
||||
NewapiQuotaServiceImplTest
|
||||
)
|
||||
security_test_selector="$(IFS=,; printf '%s' "${security_java_tests[*]}")"
|
||||
(
|
||||
cd game-cloud
|
||||
mvn test -pl huijing-server -am \
|
||||
-Dtest="$security_test_selector" -Dsurefire.failIfNoSpecifiedTests=false
|
||||
) >"$evidence/security-java-tests.log" 2>&1
|
||||
|
||||
# 不能只相信 Maven 退出码:逐个核对 Surefire XML,防止选择器漂移后零测试仍签发 RC。
|
||||
verify_security_java_reports() {
|
||||
python3 - "$evidence/security-java-tests-summary.tsv" "${security_java_tests[@]}" <<'PY'
|
||||
import pathlib,sys,xml.etree.ElementTree as ET
|
||||
|
||||
output=pathlib.Path(sys.argv[1])
|
||||
expected=sys.argv[2:]
|
||||
rows={}
|
||||
for report in pathlib.Path("game-cloud").glob("**/target/surefire-reports/TEST-*.xml"):
|
||||
root=ET.parse(report).getroot()
|
||||
name=root.attrib.get("name","").rsplit(".",1)[-1]
|
||||
if name not in expected:
|
||||
continue
|
||||
tests=int(root.attrib.get("tests","0"))
|
||||
failures=int(root.attrib.get("failures","0"))
|
||||
errors=int(root.attrib.get("errors","0"))
|
||||
skipped=int(root.attrib.get("skipped","0"))
|
||||
if name in rows:
|
||||
raise SystemExit(f"安全测试报告重复: {name}")
|
||||
rows[name]=(tests,failures,errors,skipped)
|
||||
missing=[name for name in expected if name not in rows]
|
||||
if missing:
|
||||
raise SystemExit("安全测试报告缺失: "+",".join(missing))
|
||||
for name,(tests,failures,errors,skipped) in rows.items():
|
||||
if tests < 1 or failures or errors:
|
||||
raise SystemExit(f"安全测试未通过: {name} tests={tests} failures={failures} errors={errors}")
|
||||
with output.open("w",encoding="utf-8") as handle:
|
||||
handle.write("test_class\ttests\tfailures\terrors\tskipped\n")
|
||||
for name in expected:
|
||||
handle.write(name+"\t"+"\t".join(map(str,rows[name]))+"\n")
|
||||
PY
|
||||
chmod 600 "$evidence/security-java-tests-summary.tsv"
|
||||
}
|
||||
verify_security_java_reports
|
||||
|
||||
printf '[3/7] 构建 game-studio staging\n'
|
||||
(cd game-studio && npm ci && npm run build -- --mode staging)
|
||||
|
||||
printf '[3/6] 构建 game-admin stage\n'
|
||||
printf '[4/7] 构建 game-admin stage\n'
|
||||
corepack prepare pnpm@9.15.4 --activate
|
||||
(cd game-admin && pnpm install --frozen-lockfile && pnpm build:stage)
|
||||
|
||||
printf '[4/6] 准备 commit 内独立 Python 虚拟环境\n'
|
||||
printf '[5/7] 准备 commit 内独立 Python 虚拟环境\n'
|
||||
UV_BIN="${UV_BIN:-/root/.local/bin/uv}"
|
||||
[ -x "$UV_BIN" ] || { printf '缺少 uv:%s\n' "$UV_BIN" >&2; exit 5; }
|
||||
(cd cheap-worker && "$UV_BIN" venv .venv && "$UV_BIN" pip install --python .venv/bin/python -r requirements.txt)
|
||||
(cd tier2/gen-worker && "$UV_BIN" venv .venv && "$UV_BIN" pip install --python .venv/bin/python -r requirements.txt)
|
||||
|
||||
printf '[5/6] 运行发布工具回归测试\n'
|
||||
printf '[6/7] 运行发布工具回归测试\n'
|
||||
release_tool_tests=(
|
||||
deploy/tests/test-staging-rc.sh
|
||||
deploy/tests/test-provision-dogfood-users.sh
|
||||
deploy/tests/test-provision-dogfood-nacos-transaction.sh
|
||||
deploy/tests/test-import-newapi-pool.sh
|
||||
deploy/tests/test-dogfood-smoke.sh
|
||||
)
|
||||
(
|
||||
export PATH="$release/cheap-worker/.venv/bin:$PATH"
|
||||
@ -116,10 +178,14 @@ release_tool_tests=(
|
||||
done
|
||||
printf 'RUN game-runtime/tools/test_newapi_pool_provision.py\n'
|
||||
python3 -m unittest game-runtime/tools/test_newapi_pool_provision.py
|
||||
printf 'RELEASE_TOOL_TESTS_PASS shell=%s python=1\n' "${#release_tool_tests[@]}"
|
||||
printf 'RUN deploy/tests/test-dogfood-reviewer-security.py\n'
|
||||
python3 -m unittest deploy/tests/test-dogfood-reviewer-security.py
|
||||
printf 'RUN deploy/dogfood-ops/tests/test_dogfood_ops.py\n'
|
||||
python3 -m unittest deploy/dogfood-ops/tests/test_dogfood_ops.py
|
||||
printf 'RELEASE_TOOL_TESTS_PASS shell=%s python=3\n' "${#release_tool_tests[@]}"
|
||||
) >"$evidence/release-tool-tests.log" 2>&1
|
||||
|
||||
printf '[6/6] 记录制品与启动坐标\n'
|
||||
printf '[7/7] 记录制品与启动坐标\n'
|
||||
{
|
||||
printf 'backend_jar=%s/game-cloud/huijing-server/target/huijing-server.jar\n' "$release"
|
||||
printf 'studio_dist=%s/game-studio/dist\n' "$release"
|
||||
@ -142,9 +208,13 @@ printf '[6/6] 记录制品与启动坐标\n'
|
||||
printf 'release_tool_shell_tests=PASS\n'
|
||||
printf 'release_tool_shell_test_count=%s\n' "${#release_tool_tests[@]}"
|
||||
printf 'newapi_pool_python_tests=PASS\n'
|
||||
printf 'dogfood_reviewer_python_tests=PASS\n'
|
||||
printf 'dogfood_ops_python_tests=PASS\n'
|
||||
printf 'backend_build=PASS\n'
|
||||
printf 'backend_tests=NOT_RUN\n'
|
||||
printf 'backend_tests_reason=maven_build_used_DskipTests\n'
|
||||
printf 'backend_security_tests=PASS\n'
|
||||
printf 'backend_security_test_count=%s\n' "${#security_java_tests[@]}"
|
||||
printf 'security_java_tests_sha256=%s\n' "$(sha256sum "$evidence/security-java-tests.log" | awk '{print $1}')"
|
||||
printf 'security_java_tests_summary_sha256=%s\n' "$(sha256sum "$evidence/security-java-tests-summary.tsv" | awk '{print $1}')"
|
||||
printf 'studio_build=PASS\n'
|
||||
printf 'admin_build=PASS\n'
|
||||
} >"$evidence/verification-summary.txt"
|
||||
@ -156,5 +226,5 @@ trap - ERR
|
||||
|
||||
printf 'RC_PREPARE_PASS release=%s\n' "$release"
|
||||
printf 'rollback_target=%s\n' "$previous"
|
||||
printf '下一步由统一部署窗口读取 evidence/artifact-paths.env,探活通过后再原子切换 current。\n'
|
||||
printf '下一步由统一部署窗口读取 evidence/artifact-paths.env,探活通过后再原子切换 active。\n'
|
||||
REMOTE_SCRIPT
|
||||
|
||||
@ -6,6 +6,8 @@ set -euo pipefail
|
||||
readonly EXPECTED_ACK='PROVISION_DOGFOOD_40_USERS_WITH_TEMP_CONFIG'
|
||||
readonly CODE_USERNAME_EXISTS='1002090004'
|
||||
readonly CODE_REGISTER_DISABLED='1002090007'
|
||||
readonly CODE_AD_DOGFOOD_DISABLED='1111002006'
|
||||
readonly CODE_TRADE_DOGFOOD_DISABLED='1106008000'
|
||||
|
||||
BASE="${BASE:-http://100.64.0.7:48080}"
|
||||
TENANT="${TENANT:-1}"
|
||||
@ -18,6 +20,9 @@ MYSQL_BIN="${MYSQL_BIN:-mysql}"
|
||||
MYSQL_SSH_HOST="${MYSQL_SSH_HOST-__AUTO__}"
|
||||
MYSQL_ENV_FILE="${MYSQL_ENV_FILE:-/root/huijing-dev.env}"
|
||||
VERIFY_TIMEOUT_SEC="${VERIFY_TIMEOUT_SEC:-60}"
|
||||
ADMIN_TOKEN_FILE="${ADMIN_TOKEN_FILE:-}"
|
||||
ADMIN_USERNAME="${ADMIN_USERNAME:-}"
|
||||
ADMIN_PASSWORD="${ADMIN_PASSWORD:-}"
|
||||
|
||||
log() { printf '%s\n' "$*"; }
|
||||
die() { printf 'PROVISION_FAIL %s\n' "$*" >&2; exit 1; }
|
||||
@ -29,8 +34,14 @@ require_env() {
|
||||
done
|
||||
}
|
||||
|
||||
require_env PROVISION_ACK DOGFOOD_PASSWORD_SEED ADMIN_USERNAME ADMIN_PASSWORD
|
||||
require_env PROVISION_ACK DOGFOOD_PASSWORD_SEED
|
||||
[ "$PROVISION_ACK" = "$EXPECTED_ACK" ] || die "PROVISION_ACK 不匹配;确认临时配置后必须精确设为 $EXPECTED_ACK"
|
||||
if [ -n "$ADMIN_TOKEN_FILE" ]; then
|
||||
[ -z "$ADMIN_USERNAME" ] && [ -z "$ADMIN_PASSWORD" ] || \
|
||||
die "ADMIN_TOKEN_FILE 不能与 ADMIN_USERNAME/ADMIN_PASSWORD 混用"
|
||||
else
|
||||
require_env ADMIN_USERNAME ADMIN_PASSWORD
|
||||
fi
|
||||
|
||||
# 只允许调用 mini-desktop 本机或 Tailscale 地址,防止误向其它环境批量建号。
|
||||
BASE_HOST="$(python3 - "$BASE" <<'PY'
|
||||
@ -59,6 +70,26 @@ fi
|
||||
command -v curl >/dev/null || die "缺少 curl"
|
||||
command -v python3 >/dev/null || die "缺少 python3"
|
||||
|
||||
read_private_token() {
|
||||
python3 - "$1" <<'PY'
|
||||
import os,stat,sys
|
||||
path=sys.argv[1]
|
||||
flags=os.O_RDONLY|getattr(os,"O_NOFOLLOW",0)
|
||||
try:
|
||||
fd=os.open(path,flags)
|
||||
except OSError as error:
|
||||
raise SystemExit(f"无法安全打开 ADMIN_TOKEN_FILE:{error}")
|
||||
with os.fdopen(fd,"r",encoding="utf-8") as handle:
|
||||
metadata=os.fstat(handle.fileno())
|
||||
if not stat.S_ISREG(metadata.st_mode) or stat.S_IMODE(metadata.st_mode) != 0o600:
|
||||
raise SystemExit("ADMIN_TOKEN_FILE 必须是 0600 普通文件")
|
||||
token=handle.read().strip()
|
||||
if not token or any(character.isspace() for character in token):
|
||||
raise SystemExit("ADMIN_TOKEN_FILE 内容为空或包含空白字符")
|
||||
print(token,end="")
|
||||
PY
|
||||
}
|
||||
|
||||
umask 077
|
||||
BODY_FILE="$(mktemp)"
|
||||
TMP_ACCOUNTS="$(mktemp)"
|
||||
@ -226,12 +257,12 @@ read_missing_claim_user_ids() {
|
||||
}
|
||||
|
||||
log "预置目标:10 creator + 30 player;BASE host=$BASE_HOST"
|
||||
log "运行前置:临时 dogfood=false、password-register/login=true、注册与登录 IP 时/日上限均为 60;结束后恢复狗粮配置。"
|
||||
log "运行前置:dogfood=true,临时 password-register/provisioning=true,注册与登录 IP 时/日上限均为 60;结束后恢复配置。"
|
||||
|
||||
# dogfood=false 的只读行为校验;若仍在狗粮态,广告入口会被后端守卫拒绝。
|
||||
# 临时开窗只能放行密码注册;广告和资金入口必须继续由 dogfood 后端守卫拒绝。
|
||||
request '' GET /app-api/ad/slot/list-enabled
|
||||
[ "$LAST_HTTP" = '200' ] && [ "$(json_value code)" = '0' ] \
|
||||
|| die "临时 provisioning 配置未生效:要求 dogfood=false(http=$LAST_HTTP code=$(json_value code))"
|
||||
[ "$LAST_HTTP" = '200' ] && [ "$(json_value code)" = "$CODE_AD_DOGFOOD_DISABLED" ] \
|
||||
|| die "dogfood 广告守卫未保持关闭(http=$LAST_HTTP code=$(json_value code))"
|
||||
|
||||
read_counts
|
||||
MISSING_COUNT=$((40 - ACCOUNT_COUNT))
|
||||
@ -242,18 +273,23 @@ UNCLAIMED_COUNT=$((40 - CLAIMED_ACCOUNT_COUNT))
|
||||
[ "$UNCLAIMED_COUNT" -ge 0 ] || die "目标账号 claim 计数异常:$CLAIMED_ACCOUNT_COUNT"
|
||||
REQUIRED_FREE=$((UNCLAIMED_COUNT + 12))
|
||||
if [ "$FREE_COUNT" -lt "$REQUIRED_FREE" ]; then
|
||||
die "额度池 FREE=${FREE_COUNT},不足以让剩余账号各 claim 一条并保留 12(claimed_accounts=${CLAIMED_ACCOUNT_COUNT} required_free=${REQUIRED_FREE});请先受控补池,本脚本不会创建 new-api 资源"
|
||||
die "额度池 FREE=${FREE_COUNT},不足以让剩余账号各 claim 一条并保留 12(claimed_accounts=${CLAIMED_ACCOUNT_COUNT} required_free=${REQUIRED_FREE});请先运行受控 new-api import 补建 neice_051-054,本脚本不会创建 new-api 资源"
|
||||
fi
|
||||
|
||||
# admin token 仅驻留内存,不输出、不写 accounts 文件。
|
||||
ADMIN_BODY="$(json_body "$ADMIN_USERNAME" "$ADMIN_PASSWORD")"
|
||||
request '' POST /admin-api/system/auth/login "$ADMIN_BODY"
|
||||
[ "$LAST_HTTP" = '200' ] && [ "$(json_value code)" = '0' ] || die "admin 登录失败"
|
||||
ADMIN_TOKEN="$(json_value data.accessToken)"
|
||||
[ -n "$ADMIN_TOKEN" ] || die "admin 登录响应缺 accessToken"
|
||||
if [ -n "$ADMIN_TOKEN_FILE" ]; then
|
||||
ADMIN_TOKEN="$(read_private_token "$ADMIN_TOKEN_FILE")" || die "ADMIN_TOKEN_FILE 安全校验失败"
|
||||
else
|
||||
ADMIN_BODY="$(json_body "$ADMIN_USERNAME" "$ADMIN_PASSWORD")"
|
||||
request '' POST /admin-api/system/auth/login "$ADMIN_BODY"
|
||||
[ "$LAST_HTTP" = '200' ] && [ "$(json_value code)" = '0' ] || die "admin 登录失败"
|
||||
ADMIN_TOKEN="$(json_value data.accessToken)"
|
||||
[ -n "$ADMIN_TOKEN" ] || die "admin 登录响应缺 accessToken"
|
||||
fi
|
||||
|
||||
printf 'username\trole\tuser_id\tpassword\n' >"$TMP_ACCOUNTS"
|
||||
TARGET_USER_IDS=()
|
||||
FUND_GUARD_VERIFIED=0
|
||||
for index in "${!TARGET_NAMES[@]}"; do
|
||||
username="${TARGET_NAMES[$index]}"
|
||||
if [ "$index" -lt 10 ]; then role='creator'; else role='player'; fi
|
||||
@ -285,6 +321,21 @@ for index in "${!TARGET_NAMES[@]}"; do
|
||||
is_target_user_id "$user_id" && die "多个目标账号返回同一 userId=$user_id,拒绝继续"
|
||||
TARGET_USER_IDS+=("$user_id")
|
||||
|
||||
# mock token 已关闭;只用首个真实登录账号的 token 验证一次资金后端守卫,且不落日志或证据。
|
||||
if [ "$FUND_GUARD_VERIFIED" -eq 0 ]; then
|
||||
PLAYER_TOKEN="$(json_value data.accessToken)"
|
||||
[ -n "$PLAYER_TOKEN" ] || die "账号 $username 登录响应缺 accessToken,无法验证资金守卫"
|
||||
request "$PLAYER_TOKEN" GET /app-api/trade/charge/points-balance
|
||||
[ "$LAST_HTTP" = '200' ] && [ "$(json_value code)" = "$CODE_TRADE_DOGFOOD_DISABLED" ] \
|
||||
|| die "dogfood 充值守卫未保持关闭(http=$LAST_HTTP code=$(json_value code))"
|
||||
request "$PLAYER_TOKEN" GET '/app-api/trade/withdraw/page?pageNo=1&pageSize=1'
|
||||
[ "$LAST_HTTP" = '200' ] && [ "$(json_value code)" = "$CODE_TRADE_DOGFOOD_DISABLED" ] \
|
||||
|| die "dogfood 提现守卫未保持关闭(http=$LAST_HTTP code=$(json_value code))"
|
||||
unset PLAYER_TOKEN
|
||||
FUND_GUARD_VERIFIED=1
|
||||
log "已用真实玩家身份验证充值/提现狗粮守卫"
|
||||
fi
|
||||
|
||||
if [ "$role" = 'creator' ]; then
|
||||
request "$ADMIN_TOKEN" PUT /admin-api/passport/player/set-creator \
|
||||
"{\"userId\":$user_id,\"creatorFlag\":1}"
|
||||
@ -317,7 +368,7 @@ while :; do
|
||||
&& [ "$DUPLICATE_CLAIM_COUNT" -eq 0 ] && [ "$FREE_COUNT" -ge 12 ]; then
|
||||
break
|
||||
fi
|
||||
[ "$SECONDS" -lt "$deadline" ] || die "终验未收敛:accounts=$ACCOUNT_COUNT creators=$CREATOR_COUNT role_mismatches=$ROLE_MISMATCH_COUNT claimed_accounts=$CLAIMED_ACCOUNT_COUNT duplicate_claims=$DUPLICATE_CLAIM_COUNT FREE=${FREE_COUNT};FREE 不足请受控补池,勿用本脚本创建 new-api 资源"
|
||||
[ "$SECONDS" -lt "$deadline" ] || die "终验未收敛:accounts=$ACCOUNT_COUNT creators=$CREATOR_COUNT role_mismatches=$ROLE_MISMATCH_COUNT claimed_accounts=$CLAIMED_ACCOUNT_COUNT duplicate_claims=$DUPLICATE_CLAIM_COUNT FREE=${FREE_COUNT};额度池不足请先运行受控 new-api import 补建 neice_051-054"
|
||||
sleep 2
|
||||
done
|
||||
|
||||
|
||||
@ -10,9 +10,19 @@ readonly CODE_USERNAME_OR_PASSWORD_ERROR='1002090005'
|
||||
readonly CODE_PASSWORD_REGISTER_DISABLED='1002090007'
|
||||
readonly CODE_DOGFOOD_SMS_DISABLED='1002090008'
|
||||
readonly CODE_AD_DOGFOOD_DISABLED='1111002006'
|
||||
readonly CODE_TRADE_DOGFOOD_DISABLED='1106008000'
|
||||
|
||||
MODE='transaction'
|
||||
WATCHDOG_RECOVERY_DIR=''
|
||||
if [ "${1:-}" = '--watchdog' ]; then
|
||||
MODE='watchdog'
|
||||
WATCHDOG_RECOVERY_DIR="${2:-}"
|
||||
elif [ -n "${1:-}" ]; then
|
||||
printf 'DOGFOOD_NACOS_TX_FAIL 未知参数:%s\n' "$1" >&2
|
||||
exit 2
|
||||
fi
|
||||
|
||||
DOGFOOD_PROVISION_ACK="${DOGFOOD_PROVISION_ACK:-}"
|
||||
ADMIN_TOKEN_FILE="${ADMIN_TOKEN_FILE:-}"
|
||||
BASE="${BASE:-http://100.64.0.7:48080}"
|
||||
TENANT="${TENANT:-1}"
|
||||
NACOS_BASE_URL="${NACOS_BASE_URL:-http://100.64.0.8:8848/nacos}"
|
||||
@ -23,11 +33,14 @@ BACKEND_UNIT="${BACKEND_UNIT:-gda-staging-backend}"
|
||||
PROVISION_BIN="${PROVISION_BIN:-$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)/provision-dogfood-users.sh}"
|
||||
CURL_BIN="${CURL_BIN:-curl}"
|
||||
SYSTEMCTL_BIN="${SYSTEMCTL_BIN:-systemctl}"
|
||||
SYSTEMD_RUN_BIN="${SYSTEMD_RUN_BIN:-systemd-run}"
|
||||
FLOCK_BIN="${FLOCK_BIN:-flock}"
|
||||
SLEEP_BIN="${SLEEP_BIN:-sleep}"
|
||||
RESTART_TIMEOUT_SEC="${RESTART_TIMEOUT_SEC:-180}"
|
||||
DOGFOOD_NEGATIVE_TOKEN="${DOGFOOD_NEGATIVE_TOKEN:-test1}"
|
||||
PROVISION_TTL_SEC="${PROVISION_TTL_SEC:-900}"
|
||||
RUN_ID="${RUN_ID:-$(date -u +%Y%m%dT%H%M%SZ)-$$}"
|
||||
EVIDENCE_DIR="${EVIDENCE_DIR:-/root/game-staging/evidence/dogfood-provision-$RUN_ID}"
|
||||
LOCK_FILE="${LOCK_FILE:-/root/game-staging/locks/dogfood-provision.lock}"
|
||||
EVENT_LOG=''
|
||||
|
||||
log() {
|
||||
@ -38,14 +51,31 @@ log() {
|
||||
}
|
||||
die() { printf 'DOGFOOD_NACOS_TX_FAIL %s\n' "$*" >&2; exit 1; }
|
||||
|
||||
[ "$DOGFOOD_PROVISION_ACK" = "$EXPECTED_ACK" ] || \
|
||||
die "DOGFOOD_PROVISION_ACK 不匹配;必须精确确认 $EXPECTED_ACK"
|
||||
[[ "$RESTART_TIMEOUT_SEC" =~ ^[0-9]+$ ]] && [ "$RESTART_TIMEOUT_SEC" -ge 30 ] || \
|
||||
die 'RESTART_TIMEOUT_SEC 必须是至少 30 秒的整数'
|
||||
[ -x "$PROVISION_BIN" ] || die "预置脚本不可执行:$PROVISION_BIN"
|
||||
[[ "$PROVISION_TTL_SEC" =~ ^[0-9]+$ ]] && [ "$PROVISION_TTL_SEC" -ge 60 ] || \
|
||||
die 'PROVISION_TTL_SEC 必须是至少 60 秒的整数'
|
||||
[ "$MODE" = watchdog ] || [ "$DOGFOOD_PROVISION_ACK" = "$EXPECTED_ACK" ] || \
|
||||
die "DOGFOOD_PROVISION_ACK 不匹配;必须精确确认 $EXPECTED_ACK"
|
||||
[ "$MODE" = watchdog ] || [ -x "$PROVISION_BIN" ] || die "预置脚本不可执行:$PROVISION_BIN"
|
||||
[ "$MODE" = watchdog ] || [ -n "$ADMIN_TOKEN_FILE" ] || die 'ADMIN_TOKEN_FILE 必填'
|
||||
[ "$MODE" = watchdog ] || { [ -f "$ADMIN_TOKEN_FILE" ] && [ ! -L "$ADMIN_TOKEN_FILE" ]; } || \
|
||||
die 'ADMIN_TOKEN_FILE 必须是非 symlink 普通文件'
|
||||
if [ "$MODE" != watchdog ]; then
|
||||
token_mode="$(stat -f '%Lp' "$ADMIN_TOKEN_FILE" 2>/dev/null || stat -c '%a' "$ADMIN_TOKEN_FILE")"
|
||||
[ "$token_mode" = 600 ] || die "ADMIN_TOKEN_FILE 权限必须为 0600 actual=$token_mode"
|
||||
fi
|
||||
command -v python3 >/dev/null || die '缺少 python3'
|
||||
python3 -c 'import yaml' >/dev/null 2>&1 || die '缺少 PyYAML,无法安全合并既有 Nacos YAML'
|
||||
|
||||
mkdir -p "$(dirname "$LOCK_FILE")"
|
||||
exec 9>"$LOCK_FILE"
|
||||
if [ "$MODE" = watchdog ]; then
|
||||
"$FLOCK_BIN" -w 30 9 || die 'watchdog 无法取得预置恢复锁'
|
||||
else
|
||||
"$FLOCK_BIN" -n 9 || die '已有 dogfood 账号预置事务运行,拒绝并发'
|
||||
fi
|
||||
|
||||
if [ -z "$NACOS_PASSWORD" ]; then
|
||||
[ -f "$NACOS_ENV_FILE" ] || die "缺少 Nacos 凭据来源:$NACOS_ENV_FILE"
|
||||
set -a
|
||||
@ -57,11 +87,16 @@ fi
|
||||
[ -n "$NACOS_PASSWORD" ] || die 'Nacos 口令为空'
|
||||
|
||||
umask 077
|
||||
[ ! -e "$EVIDENCE_DIR" ] || die "证据目录已存在,拒绝覆盖:$EVIDENCE_DIR"
|
||||
mkdir -p "$EVIDENCE_DIR"
|
||||
if [ "$MODE" = watchdog ]; then
|
||||
[ -d "$WATCHDOG_RECOVERY_DIR" ] && [ ! -L "$WATCHDOG_RECOVERY_DIR" ] || die 'watchdog recovery 目录非法'
|
||||
EVIDENCE_DIR="$(dirname "$WATCHDOG_RECOVERY_DIR")"
|
||||
else
|
||||
[ ! -e "$EVIDENCE_DIR" ] || die "证据目录已存在,拒绝覆盖:$EVIDENCE_DIR"
|
||||
mkdir -p "$EVIDENCE_DIR"
|
||||
fi
|
||||
chmod 700 "$EVIDENCE_DIR"
|
||||
EVENT_LOG="$EVIDENCE_DIR/transaction.log"
|
||||
: >"$EVENT_LOG"
|
||||
[ "$MODE" = watchdog ] || : >"$EVENT_LOG"
|
||||
chmod 600 "$EVENT_LOG"
|
||||
BODY_FILE="$(mktemp)"
|
||||
NACOS_PASSWORD_FILE="$(mktemp)"
|
||||
@ -72,8 +107,11 @@ ORIGINAL_CONFIG="$EVIDENCE_DIR/nacos-original.yaml"
|
||||
TEMP_CONFIG="$EVIDENCE_DIR/nacos-temporary.yaml"
|
||||
ORIGINAL_STATUS_FILE="$EVIDENCE_DIR/nacos-original.status"
|
||||
SUMMARY_FILE="$EVIDENCE_DIR/transaction.status"
|
||||
RECOVERY_DIR="$EVIDENCE_DIR/watchdog"
|
||||
WATCHDOG_UNIT="dogfood-provision-watchdog-${RUN_ID//[^A-Za-z0-9_-]/-}"
|
||||
restore_required=0
|
||||
finalizing=0
|
||||
watchdog_armed=0
|
||||
NACOS_TOKEN=''
|
||||
LAST_HTTP=''
|
||||
LAST_BODY=''
|
||||
@ -130,16 +168,19 @@ nacos_get() {
|
||||
[ "$LAST_HTTP" = 200 ] || [ "$LAST_HTTP" = 404 ]
|
||||
}
|
||||
|
||||
nacos_publish() {
|
||||
local content_file="$1"
|
||||
request POST "$NACOS_BASE_URL/v1/cs/configs" \
|
||||
"dataId=$DATA_ID&group=$NACOS_GROUP&tenant=" "$NACOS_TOKEN" "$content_file"
|
||||
[ "$LAST_HTTP" = 200 ] && [ "$LAST_BODY" = true ]
|
||||
nacos_md5() {
|
||||
python3 - "$1" <<'PY'
|
||||
import hashlib,sys
|
||||
with open(sys.argv[1],"rb") as handle:
|
||||
print(hashlib.md5(handle.read()).hexdigest())
|
||||
PY
|
||||
}
|
||||
|
||||
nacos_delete() {
|
||||
request DELETE "$NACOS_BASE_URL/v1/cs/configs" \
|
||||
"dataId=$DATA_ID&group=$NACOS_GROUP&tenant=" "$NACOS_TOKEN"
|
||||
nacos_publish() {
|
||||
local content_file="$1" expected_md5="$2"
|
||||
[[ "$expected_md5" =~ ^[0-9a-f]{32}$ ]] || return 1
|
||||
request POST "$NACOS_BASE_URL/v1/cs/configs" \
|
||||
"dataId=$DATA_ID&group=$NACOS_GROUP&tenant=&casMd5=$expected_md5" "$NACOS_TOKEN" "$content_file"
|
||||
[ "$LAST_HTTP" = 200 ] && [ "$LAST_BODY" = true ]
|
||||
}
|
||||
|
||||
@ -181,9 +222,10 @@ expect_code() {
|
||||
}
|
||||
|
||||
verify_temporary_runtime() {
|
||||
# 匿名广告位在 dogfood=false 时应正常返回;真实注册/登录由随后 provision 脚本继续实证。
|
||||
request GET "$BASE/app-api/ad/slot/list-enabled"
|
||||
[ "$LAST_HTTP" = 200 ] && [ "$(json_value code)" = 0 ]
|
||||
# 此时还没有真实玩家 token,只校验免登录边界;资金守卫由预置脚本在首次真实登录后验证。
|
||||
expect_code 'provisioning SMS still disabled' "$CODE_DOGFOOD_SMS_DISABLED" POST \
|
||||
/app-api/passport/send-sms-code '{"mobile":"13800000000"}'
|
||||
expect_code 'provisioning ad still disabled' "$CODE_AD_DOGFOOD_DISABLED" GET /app-api/ad/slot/list-enabled
|
||||
}
|
||||
|
||||
verify_restored_runtime() {
|
||||
@ -198,32 +240,78 @@ verify_restored_runtime() {
|
||||
expect_code 'SMS disabled' "$CODE_DOGFOOD_SMS_DISABLED" POST \
|
||||
/app-api/passport/send-sms-code '{"mobile":"13800000000"}'
|
||||
expect_code 'ad disabled' "$CODE_AD_DOGFOOD_DISABLED" GET /app-api/ad/slot/list-enabled
|
||||
expect_code 'charge disabled' "$CODE_TRADE_DOGFOOD_DISABLED" GET \
|
||||
/app-api/trade/charge/points-balance '' "$DOGFOOD_NEGATIVE_TOKEN"
|
||||
expect_code 'withdraw disabled' "$CODE_TRADE_DOGFOOD_DISABLED" GET \
|
||||
'/app-api/trade/withdraw/page?pageNo=1&pageSize=1' '' "$DOGFOOD_NEGATIVE_TOKEN"
|
||||
}
|
||||
|
||||
restore_original() {
|
||||
local current="$EVIDENCE_DIR/nacos-before-restore.tmp" temporary_md5
|
||||
log '开始恢复原 Nacos 配置'
|
||||
nacos_login || return 1
|
||||
original_status="$(<"$ORIGINAL_STATUS_FILE")"
|
||||
if [ "$original_status" = 200 ]; then
|
||||
nacos_publish "$ORIGINAL_CONFIG" || return 1
|
||||
verify_nacos_exact "$ORIGINAL_CONFIG" || return 1
|
||||
elif [ "$original_status" = 404 ]; then
|
||||
nacos_delete || return 1
|
||||
verify_file="$EVIDENCE_DIR/nacos-restore-verify.tmp"
|
||||
nacos_get "$verify_file" || return 1
|
||||
[ "$LAST_HTTP" = 404 ] || return 1
|
||||
else
|
||||
return 1
|
||||
fi
|
||||
nacos_get "$current" || return 1
|
||||
[ "$LAST_HTTP" = 200 ] || return 1
|
||||
cmp -s "$TEMP_CONFIG" "$current" || return 1
|
||||
temporary_md5="$(nacos_md5 "$TEMP_CONFIG")"
|
||||
nacos_publish "$ORIGINAL_CONFIG" "$temporary_md5" || return 1
|
||||
verify_nacos_exact "$ORIGINAL_CONFIG" || return 1
|
||||
restart_backend || return 1
|
||||
verify_restored_runtime || return 1
|
||||
log '原 Nacos 配置与狗粮安全边界恢复完成'
|
||||
}
|
||||
|
||||
arm_watchdog() {
|
||||
mkdir -p "$RECOVERY_DIR"
|
||||
chmod 700 "$RECOVERY_DIR"
|
||||
{
|
||||
printf 'schema=dogfood-provision-watchdog/1\n'
|
||||
printf 'run_id=%s\n' "$RUN_ID"
|
||||
printf 'ttl_sec=%s\n' "$PROVISION_TTL_SEC"
|
||||
printf 'temporary_md5=%s\n' "$(nacos_md5 "$TEMP_CONFIG")"
|
||||
printf 'original_md5=%s\n' "$(nacos_md5 "$ORIGINAL_CONFIG")"
|
||||
} >"$RECOVERY_DIR/metadata"
|
||||
: >"$RECOVERY_DIR/armed"
|
||||
chmod 600 "$RECOVERY_DIR/metadata" "$RECOVERY_DIR/armed"
|
||||
if ! "$SYSTEMD_RUN_BIN" --quiet --collect --unit="$WATCHDOG_UNIT" \
|
||||
--on-active="${PROVISION_TTL_SEC}s" --property=Type=oneshot \
|
||||
--property=Restart=on-failure --property=RestartSec=15s \
|
||||
/bin/bash "$(realpath "${BASH_SOURCE[0]}")" --watchdog "$RECOVERY_DIR"; then
|
||||
rm -f "$RECOVERY_DIR/armed"
|
||||
return 1
|
||||
fi
|
||||
watchdog_armed=1
|
||||
log "独立恢复 watchdog 已注册 unit=$WATCHDOG_UNIT ttl=${PROVISION_TTL_SEC}s"
|
||||
}
|
||||
|
||||
disarm_watchdog() {
|
||||
[ "$watchdog_armed" -eq 1 ] || return 0
|
||||
rm -f "$RECOVERY_DIR/armed"
|
||||
"$SYSTEMCTL_BIN" stop "$WATCHDOG_UNIT.timer" "$WATCHDOG_UNIT.service" >/dev/null 2>&1 || true
|
||||
printf 'disarmed_at=%s\n' "$(date -u +%Y-%m-%dT%H:%M:%SZ)" >"$RECOVERY_DIR/disarmed"
|
||||
chmod 600 "$RECOVERY_DIR/disarmed"
|
||||
watchdog_armed=0
|
||||
}
|
||||
|
||||
if [ "$MODE" = watchdog ]; then
|
||||
EVENT_LOG="$EVIDENCE_DIR/watchdog.log"
|
||||
touch "$EVENT_LOG"
|
||||
chmod 600 "$EVENT_LOG"
|
||||
[ -f "$RECOVERY_DIR/armed" ] || {
|
||||
log 'watchdog 已解除,无需恢复'
|
||||
cleanup_body
|
||||
exit 0
|
||||
}
|
||||
log 'watchdog TTL 到期,开始独立恢复 Nacos 安全配置'
|
||||
if restore_original; then
|
||||
rm -f "$RECOVERY_DIR/armed"
|
||||
printf 'WATCHDOG_RESTORE=PASS at=%s\n' "$(date -u +%Y-%m-%dT%H:%M:%SZ)" >"$RECOVERY_DIR/watchdog.status"
|
||||
chmod 600 "$RECOVERY_DIR/watchdog.status"
|
||||
cleanup_body
|
||||
exit 0
|
||||
fi
|
||||
printf 'WATCHDOG_RESTORE=FAILED at=%s\n' "$(date -u +%Y-%m-%dT%H:%M:%SZ)" >"$RECOVERY_DIR/watchdog.status"
|
||||
chmod 600 "$RECOVERY_DIR/watchdog.status"
|
||||
cleanup_body
|
||||
die "watchdog 恢复失败,保留 armed 标记等待人工处置:$RECOVERY_DIR"
|
||||
fi
|
||||
|
||||
finalize() {
|
||||
local main_rc=$? restore_rc=0
|
||||
[ "$finalizing" -eq 0 ] || return
|
||||
@ -241,6 +329,7 @@ finalize() {
|
||||
printf 'DOGFOOD_NACOS_TX_RESTORE_FAIL evidence=%s\n' "$EVIDENCE_DIR" >&2
|
||||
exit 70
|
||||
fi
|
||||
disarm_watchdog
|
||||
if [ "$main_rc" -ne 0 ]; then
|
||||
printf 'provision_failed rc=%s restored=true evidence=%s\n' "$main_rc" "$EVIDENCE_DIR" >"$SUMMARY_FILE"
|
||||
chmod 600 "$SUMMARY_FILE"
|
||||
@ -260,59 +349,55 @@ nacos_login || die 'Nacos 登录失败'
|
||||
nacos_get "$ORIGINAL_CONFIG" || die "读取 $DATA_ID 失败 http=$LAST_HTTP"
|
||||
printf '%s\n' "$LAST_HTTP" >"$ORIGINAL_STATUS_FILE"
|
||||
chmod 600 "$ORIGINAL_STATUS_FILE"
|
||||
restore_required=1
|
||||
[ "$LAST_HTTP" = 200 ] || die 'Nacos dataId 不存在,无法执行内容 CAS;拒绝创建临时配置'
|
||||
|
||||
# 在原 YAML 上只覆盖预置窗口所需键;原文始终单独留证并在 EXIT trap 逐字恢复。
|
||||
ORIGINAL_STATUS="$LAST_HTTP" python3 - "$ORIGINAL_CONFIG" "$TEMP_CONFIG" <<'PY'
|
||||
import os,sys,yaml
|
||||
PROVISION_TTL_SEC="$PROVISION_TTL_SEC" python3 - "$ORIGINAL_CONFIG" "$TEMP_CONFIG" <<'PY'
|
||||
import datetime,os,sys,yaml
|
||||
source,target=sys.argv[1:]
|
||||
if os.environ["ORIGINAL_STATUS"] == "404":
|
||||
doc={}
|
||||
else:
|
||||
with open(source,encoding="utf-8") as f:
|
||||
doc=yaml.safe_load(f) or {}
|
||||
with open(source,encoding="utf-8") as f:
|
||||
doc=yaml.safe_load(f) or {}
|
||||
if not isinstance(doc,dict):
|
||||
raise SystemExit("Nacos staging 配置顶层必须是 YAML object")
|
||||
# 若历史配置使用过 dotted key,先移除同义键,避免 Spring 扁平化后与嵌套键冲突;原文仍会逐字恢复。
|
||||
for key in (
|
||||
"huijing.dogfood.enabled",
|
||||
"huijing.dogfood.provisioning-enabled",
|
||||
"huijing.dogfood.provisioning-expires-at",
|
||||
"huijing.dogfood.provisioning-run-id",
|
||||
"wanxiang.passport.password-register-enabled",
|
||||
"wanxiang.passport.password-login-enabled",
|
||||
"wanxiang.passport.register-ip-limit-per-hour",
|
||||
"wanxiang.passport.register-ip-limit-per-day",
|
||||
"wanxiang.passport.login-ip-limit-per-hour",
|
||||
"wanxiang.passport.login-ip-limit-per-day",
|
||||
):
|
||||
doc.pop(key,None)
|
||||
huijing=doc.setdefault("huijing",{})
|
||||
if not isinstance(huijing,dict): raise SystemExit("huijing 必须是 object")
|
||||
dogfood=huijing.setdefault("dogfood",{})
|
||||
if not isinstance(dogfood,dict): raise SystemExit("huijing.dogfood 必须是 object")
|
||||
dogfood["enabled"]=False
|
||||
dogfood["enabled"]=True
|
||||
dogfood["provisioning-enabled"]=True
|
||||
dogfood["provisioning-run-id"]=os.environ["RUN_ID"]
|
||||
expires_at=datetime.datetime.now(datetime.timezone.utc)+datetime.timedelta(seconds=int(os.environ["PROVISION_TTL_SEC"]))
|
||||
dogfood["provisioning-expires-at"]=expires_at.isoformat().replace("+00:00","Z")
|
||||
wanxiang=doc.setdefault("wanxiang",{})
|
||||
if not isinstance(wanxiang,dict): raise SystemExit("wanxiang 必须是 object")
|
||||
passport=wanxiang.setdefault("passport",{})
|
||||
if not isinstance(passport,dict): raise SystemExit("wanxiang.passport 必须是 object")
|
||||
passport.update({
|
||||
"password-register-enabled": True,
|
||||
"password-login-enabled": True,
|
||||
"register-ip-limit-per-hour": 60,
|
||||
"register-ip-limit-per-day": 60,
|
||||
"login-ip-limit-per-hour": 60,
|
||||
"login-ip-limit-per-day": max(60,int(passport.get("login-ip-limit-per-day",60))),
|
||||
})
|
||||
passport["password-register-enabled"]=True
|
||||
with open(target,"w",encoding="utf-8") as f:
|
||||
yaml.safe_dump(doc,f,allow_unicode=True,sort_keys=False)
|
||||
PY
|
||||
chmod 600 "$TEMP_CONFIG"
|
||||
|
||||
nacos_publish "$TEMP_CONFIG" || die "发布临时 Nacos 配置失败 http=$LAST_HTTP"
|
||||
arm_watchdog || die '无法注册独立恢复 watchdog,未修改 Nacos'
|
||||
original_md5="$(nacos_md5 "$ORIGINAL_CONFIG")"
|
||||
nacos_publish "$TEMP_CONFIG" "$original_md5" || die "发布临时 Nacos 配置 CAS 失败 http=$LAST_HTTP"
|
||||
restore_required=1
|
||||
verify_nacos_exact "$TEMP_CONFIG" || die '临时 Nacos 配置回读不一致'
|
||||
restart_backend || die '临时配置发布后 backend 未恢复健康'
|
||||
verify_temporary_runtime || die '临时 dogfood=false 未在后端可信边界生效'
|
||||
verify_temporary_runtime || die '窄 provisioning 开关未生效或安全守卫被关闭'
|
||||
|
||||
log '临时配置已生效,执行既有账号预置脚本'
|
||||
PROVISION_ACK='PROVISION_DOGFOOD_40_USERS_WITH_TEMP_CONFIG' \
|
||||
"$PROVISION_BIN" >"$EVIDENCE_DIR/provision.log" 2>&1
|
||||
env -u ADMIN_USERNAME -u ADMIN_PASSWORD \
|
||||
PROVISION_ACK='PROVISION_DOGFOOD_40_USERS_WITH_TEMP_CONFIG' DOGFOOD_PROVISIONING_MODE=narrow \
|
||||
ADMIN_TOKEN_FILE="$ADMIN_TOKEN_FILE" "$PROVISION_BIN" >"$EVIDENCE_DIR/provision.log" 2>&1
|
||||
chmod 600 "$EVIDENCE_DIR/provision.log"
|
||||
log '账号预置脚本完成,退出时恢复原配置'
|
||||
|
||||
@ -15,6 +15,9 @@ elif [ -n "${1:-}" ]; then
|
||||
exit 2
|
||||
fi
|
||||
|
||||
SYSTEMCTL_BIN="${SYSTEMCTL_BIN:-systemctl}"
|
||||
PROC_ROOT="${PROC_ROOT:-/proc}"
|
||||
|
||||
PASS=0
|
||||
FAIL=0
|
||||
FAILED_ITEMS=()
|
||||
@ -203,6 +206,50 @@ if [ "$MODE" = "dogfood-e2e" ]; then
|
||||
log "${R}DOGFOOD_E2E_ACK 无效;必须显式设置为 I_ACKNOWLEDGE_DOGFOOD_WRITES${N}" >&2
|
||||
exit 2
|
||||
fi
|
||||
require_env EXPECTED_RC_COMMIT
|
||||
if [[ ! "$EXPECTED_RC_COMMIT" =~ ^[0-9a-f]{40}$ ]]; then
|
||||
log "${R}EXPECTED_RC_COMMIT 必须是完整 40 位小写提交哈希${N}" >&2
|
||||
exit 2
|
||||
fi
|
||||
# 写模式只允许在 mini-desktop 本机对固定 staging 端口执行,禁止 ACK 配合 BASE 重定向到其它环境。
|
||||
if [ "$BASE" != "http://127.0.0.1:48080" ] || [ "$STUDIO_BASE" != "http://127.0.0.1:4173" ]; then
|
||||
log "${R}写 smoke 仅允许 BASE=http://127.0.0.1:48080、STUDIO_BASE=http://127.0.0.1:4173${N}" >&2
|
||||
exit 2
|
||||
fi
|
||||
if [ "$(hostname -s)" != "mini-desktop" ]; then
|
||||
log "${R}写 smoke 只能在 mini-desktop 本机执行${N}" >&2
|
||||
exit 2
|
||||
fi
|
||||
STAGING_ACTIVE_POINTER="${STAGING_ACTIVE_POINTER:-/root/game-staging/releases/active}"
|
||||
if [ ! -L "$STAGING_ACTIVE_POINTER" ]; then
|
||||
log "${R}缺少 staging active 指针:$STAGING_ACTIVE_POINTER${N}" >&2
|
||||
exit 2
|
||||
fi
|
||||
ACTIVE_RELEASE="$(readlink -f "$STAGING_ACTIVE_POINTER" 2>/dev/null || true)"
|
||||
if [ -z "$ACTIVE_RELEASE" ] || [ "${ACTIVE_RELEASE##*/}" != "$EXPECTED_RC_COMMIT" ]; then
|
||||
log "${R}active 指针与 EXPECTED_RC_COMMIT 不一致${N}" >&2
|
||||
exit 2
|
||||
fi
|
||||
if [ ! -f "$ACTIVE_RELEASE/RC_STATUS" ] || \
|
||||
! grep -q "^active commit=$EXPECTED_RC_COMMIT " "$ACTIVE_RELEASE/RC_STATUS"; then
|
||||
log "${R}active RC_STATUS 未证明目标 commit 已激活${N}" >&2
|
||||
exit 2
|
||||
fi
|
||||
# active 指针只能证明部署意图;运行中 backend 的真实命令行也必须来自同一 RC。
|
||||
if ! "$SYSTEMCTL_BIN" is-active --quiet gda-staging-backend; then
|
||||
log "${R}staging backend systemd unit 未运行${N}" >&2
|
||||
exit 2
|
||||
fi
|
||||
BACKEND_PID="$("$SYSTEMCTL_BIN" show --property=MainPID --value gda-staging-backend 2>/dev/null || true)"
|
||||
if ! [[ "$BACKEND_PID" =~ ^[1-9][0-9]*$ ]] || [ ! -r "$PROC_ROOT/$BACKEND_PID/cmdline" ]; then
|
||||
log "${R}无法读取 staging backend 运行进程身份${N}" >&2
|
||||
exit 2
|
||||
fi
|
||||
EXPECTED_BACKEND_JAR="$ACTIVE_RELEASE/game-cloud/huijing-server/target/huijing-server.jar"
|
||||
if ! tr '\0' '\n' <"$PROC_ROOT/$BACKEND_PID/cmdline" | grep -Fqx -- "$EXPECTED_BACKEND_JAR"; then
|
||||
log "${R}运行中的 backend 不属于 active RC${N}" >&2
|
||||
exit 2
|
||||
fi
|
||||
fi
|
||||
|
||||
head_line "公开只读 readiness"
|
||||
@ -390,8 +437,11 @@ if ! envelope_ok || ! printf '%s' "$LAST_BODY" | python3 -c 'import json,sys; gi
|
||||
fi
|
||||
mark_pass "readonly 审核写权限被拒绝且状态未变"
|
||||
|
||||
PRE_APPROVE_VERSION="$(scan_main_feed)"
|
||||
PRE_APPROVE_SCAN_RC=$?
|
||||
if PRE_APPROVE_VERSION="$(scan_main_feed)"; then
|
||||
PRE_APPROVE_SCAN_RC=0
|
||||
else
|
||||
PRE_APPROVE_SCAN_RC=$?
|
||||
fi
|
||||
if [ "$PRE_APPROVE_SCAN_RC" -eq 0 ]; then
|
||||
e2e_fail "审核通过前 gameId=$GAME_ID 已进入主 feed(versionId=${PRE_APPROVE_VERSION})"
|
||||
elif [ "$PRE_APPROVE_SCAN_RC" -eq 1 ]; then
|
||||
@ -415,8 +465,11 @@ done
|
||||
[ "$project_status" = "4" ] || e2e_fail "审核通过后项目未进入 published(4)"
|
||||
mark_pass "project published status=4"
|
||||
|
||||
FEED_VERSION_ID="$(scan_main_feed)"
|
||||
POST_APPROVE_SCAN_RC=$?
|
||||
if FEED_VERSION_ID="$(scan_main_feed)"; then
|
||||
POST_APPROVE_SCAN_RC=0
|
||||
else
|
||||
POST_APPROVE_SCAN_RC=$?
|
||||
fi
|
||||
[ "$POST_APPROVE_SCAN_RC" -eq 0 ] || e2e_fail "审核通过后未能在主 feed 找到 gameId=${GAME_ID}(scanRc=${POST_APPROVE_SCAN_RC})"
|
||||
[ "$FEED_VERSION_ID" = "$VERSION_ID" ] || e2e_fail "feed versionId=$FEED_VERSION_ID 与生成版本 $VERSION_ID 不一致"
|
||||
mark_pass "exact game appears in main feed"
|
||||
|
||||
@ -20,10 +20,10 @@ assert_contains() {
|
||||
|
||||
assert_exit_code() {
|
||||
local expected="$1" actual="$2" message="$3"
|
||||
[ "$actual" -eq "$expected" ] || fail "$message(expected=$expected actual=$actual)"
|
||||
[ "$actual" -eq "$expected" ] || fail "${message}(expected=${expected} actual=${actual})"
|
||||
}
|
||||
|
||||
# 假 curl 只实现 readiness 所需响应,并记录方法与 URL;测试不访问任何真实服务。
|
||||
# 假 curl 实现完整写链状态机,并记录方法与 URL;测试不访问任何真实服务。
|
||||
mkdir -p "$TMP_DIR/bin"
|
||||
cat >"$TMP_DIR/bin/curl" <<'FAKE_CURL'
|
||||
#!/usr/bin/env bash
|
||||
@ -31,26 +31,77 @@ set -euo pipefail
|
||||
method="GET"
|
||||
output=""
|
||||
url=""
|
||||
auth=""
|
||||
data=""
|
||||
while [ "$#" -gt 0 ]; do
|
||||
case "$1" in
|
||||
-X) method="$2"; shift 2 ;;
|
||||
-o) output="$2"; shift 2 ;;
|
||||
-w|-H|--connect-timeout|--max-time|--data) shift 2 ;;
|
||||
-H)
|
||||
[[ "$2" == Authorization:* ]] && auth="${2#Authorization: Bearer }"
|
||||
shift 2 ;;
|
||||
-w|--connect-timeout|--max-time) shift 2 ;;
|
||||
--data) data="$2"; shift 2 ;;
|
||||
-sS) shift ;;
|
||||
http://*|https://*) url="$1"; shift ;;
|
||||
*) shift ;;
|
||||
esac
|
||||
done
|
||||
printf '%s %s\n' "$method" "$url" >>"$REQUEST_LOG"
|
||||
case "$url" in
|
||||
*/actuator/health)
|
||||
body='{"status":"UP"}' ;;
|
||||
*/app-api/feed/zones)
|
||||
body='{"code":0,"data":[{"id":1}]}' ;;
|
||||
*/app-api/feed/stream*)
|
||||
body='{"code":0,"data":{"list":[{"gameId":11,"versionId":22}],"hasMore":false}}' ;;
|
||||
*/app-api/runtime/package/22*)
|
||||
approved_file="$REQUEST_LOG.approved"
|
||||
telemetry_file="$REQUEST_LOG.telemetry"
|
||||
case "$method $url" in
|
||||
"POST "*/app-api/passport/password-login)
|
||||
body='{"code":0,"data":{"accessToken":"creator-token","userId":101,"creatorFlag":1}}' ;;
|
||||
"POST "*/admin-api/system/auth/login)
|
||||
if grep -q reviewer <<<"$data"; then token=reviewer-token; else token=readonly-token; fi
|
||||
body="{\"code\":0,\"data\":{\"accessToken\":\"$token\"}}" ;;
|
||||
"POST "*/app-api/passport/password-register)
|
||||
body='{"code":1002090007,"data":null}' ;;
|
||||
"POST "*/app-api/passport/send-sms-code)
|
||||
body='{"code":1002090008,"data":null}' ;;
|
||||
"GET "*/app-api/ad/slot/list-enabled)
|
||||
body='{"code":1111002006,"data":null}' ;;
|
||||
"GET "*/app-api/trade/charge/points-balance|"GET "*/app-api/trade/withdraw/page*)
|
||||
body='{"code":1106008000,"data":null}' ;;
|
||||
"POST "*/app-api/studio/create)
|
||||
body='{"code":0,"data":{"id":31,"aigcTaskId":32,"gameId":11}}' ;;
|
||||
"GET "*/app-api/studio/task/31)
|
||||
body='{"code":0,"data":{"status":"2","aigcTaskId":32,"gameId":11,"versionId":22}}' ;;
|
||||
"GET "*/app-api/runtime/package/22/manifest*)
|
||||
body='{"entry":"index.html"}' ;;
|
||||
"GET "*/app-api/runtime/package/22*)
|
||||
body='{"code":0,"data":{"gameId":11,"versionId":22,"checksum":"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"}}' ;;
|
||||
"POST "*/app-api/project/11/publish)
|
||||
body='{"code":0,"data":{"admitted":true,"gates":[]}}' ;;
|
||||
"GET "*/admin-api/project/review/page*)
|
||||
body='{"code":0,"data":{"list":[{"id":11}]}}' ;;
|
||||
"POST "*/admin-api/project/review)
|
||||
if [ "$auth" = reviewer-token ]; then : >"$approved_file"; body='{"code":0,"data":true}';
|
||||
else body='{"code":1004030000,"data":null}'; fi ;;
|
||||
"GET "*/app-api/project/11)
|
||||
body='{"code":0,"data":{"status":"4"}}' ;;
|
||||
"POST "*/app-api/runtime/session/start)
|
||||
body='{"code":0,"data":{"sessionId":41}}' ;;
|
||||
"POST "*/app-api/runtime/session/end)
|
||||
body='{"code":0,"data":true}' ;;
|
||||
"POST "*/app-api/telemetry/events/batch)
|
||||
: >"$telemetry_file"; body='{"code":0,"data":{"accepted":2,"rejected":0}}' ;;
|
||||
"GET "*/admin-api/telemetry/game-stat/11)
|
||||
if [ -f "$telemetry_file" ]; then body='{"code":0,"data":{"playCount":1,"playEndCount":1}}';
|
||||
else body='{"code":0,"data":{"playCount":0,"playEndCount":0}}'; fi ;;
|
||||
"GET "*/actuator/health)
|
||||
body='{"status":"UP"}' ;;
|
||||
"GET "*/app-api/feed/zones)
|
||||
body='{"code":0,"data":[{"id":1}]}' ;;
|
||||
"GET "*/app-api/feed/stream*)
|
||||
if [ -f "$approved_file" ]; then
|
||||
body='{"code":0,"data":{"list":[{"gameId":11,"versionId":22}],"hasMore":false}}'
|
||||
else
|
||||
body='{"code":0,"data":{"list":[{"gameId":99,"versionId":98}],"hasMore":false}}'
|
||||
fi ;;
|
||||
"GET "*/app-api/runtime/package/98*)
|
||||
body='{"code":0,"data":{"gameId":99,"versionId":98,"checksum":"bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb"}}' ;;
|
||||
*)
|
||||
body='{"code":404,"data":null}' ;;
|
||||
esac
|
||||
@ -62,6 +113,26 @@ chmod +x "$TMP_DIR/bin/curl"
|
||||
export PATH="$TMP_DIR/bin:$PATH"
|
||||
export REQUEST_LOG
|
||||
|
||||
cat >"$TMP_DIR/bin/hostname" <<'SH'
|
||||
#!/usr/bin/env bash
|
||||
printf 'mini-desktop\n'
|
||||
SH
|
||||
chmod +x "$TMP_DIR/bin/hostname"
|
||||
|
||||
cat >"$TMP_DIR/bin/systemctl" <<'SH'
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
if [ "$*" = 'is-active --quiet gda-staging-backend' ]; then
|
||||
exit 0
|
||||
fi
|
||||
if [ "$*" = 'show --property=MainPID --value gda-staging-backend' ]; then
|
||||
printf '4242\n'
|
||||
exit 0
|
||||
fi
|
||||
exit 2
|
||||
SH
|
||||
chmod +x "$TMP_DIR/bin/systemctl"
|
||||
|
||||
bash -n "$SMOKE"
|
||||
|
||||
# 构建配置只能包含内网地址和模式开关,不能烘焙登录 token。
|
||||
@ -94,6 +165,17 @@ set -e
|
||||
assert_exit_code 2 "$missing_ack_rc" '缺 ACK 时没有 fail-closed'
|
||||
[ ! -s "$REQUEST_LOG" ] || fail '缺 ACK 时已发出网络请求'
|
||||
|
||||
# 写模式固定到 mini-desktop staging;ACK 不能把 BASE 重定向到任意远端。
|
||||
set +e
|
||||
BASE=http://example.invalid DOGFOOD_CREATOR_USERNAME=creator DOGFOOD_CREATOR_PASSWORD=creator-pass \
|
||||
DOGFOOD_REVIEWER_USERNAME=reviewer DOGFOOD_REVIEWER_PASSWORD=reviewer-pass \
|
||||
DOGFOOD_READONLY_USERNAME=readonly DOGFOOD_READONLY_PASSWORD=readonly-pass \
|
||||
DOGFOOD_E2E_ACK=I_ACKNOWLEDGE_DOGFOOD_WRITES EXPECTED_RC_COMMIT=0000000000000000000000000000000000000000 \
|
||||
PATH="$TMP_DIR/bin:$PATH" "$SMOKE" --dogfood-e2e >"$TMP_DIR/wrong-target.out" 2>&1
|
||||
wrong_target_rc=$?
|
||||
set -e
|
||||
assert_exit_code 2 "$wrong_target_rc" '写模式允许重定向到非 staging BASE'
|
||||
|
||||
# 默认 readiness 只能发送 GET,且假服务完整返回时应通过。
|
||||
PATH="$TMP_DIR/bin:$PATH" "$SMOKE" >"$TMP_DIR/readiness.out" 2>&1
|
||||
assert_contains "$TMP_DIR/readiness.out" 'SMOKE_VERDICT=PASS'
|
||||
@ -110,4 +192,51 @@ set -e
|
||||
assert_exit_code 2 "$unknown_rc" '未知参数未拒绝'
|
||||
[ ! -s "$REQUEST_LOG" ] || fail '未知参数触发了网络请求'
|
||||
|
||||
# 完整写模式回归:审核前 scan_main_feed 返回 1 是正确缺席,bash -e 不得提前退出。
|
||||
: >"$REQUEST_LOG"
|
||||
rm -f "$REQUEST_LOG.approved" "$REQUEST_LOG.telemetry"
|
||||
active_root="$TMP_DIR/releases"
|
||||
active_release="$active_root/0000000000000000000000000000000000000000"
|
||||
mkdir -p "$active_release"
|
||||
ln -s "$active_release" "$active_root/active"
|
||||
printf 'active commit=0000000000000000000000000000000000000000 at=test evidence=test\n' >"$active_release/RC_STATUS"
|
||||
active_release="$(readlink -f "$active_root/active")"
|
||||
proc_root="$TMP_DIR/proc"
|
||||
mkdir -p "$proc_root/4242"
|
||||
printf 'java\0-jar\0%s\0' "$TMP_DIR/releases/wrong/game-cloud/huijing-server/target/huijing-server.jar" \
|
||||
>"$proc_root/4242/cmdline"
|
||||
set +e
|
||||
DOGFOOD_CREATOR_USERNAME=creator DOGFOOD_CREATOR_PASSWORD=creator-pass \
|
||||
DOGFOOD_REVIEWER_USERNAME=reviewer DOGFOOD_REVIEWER_PASSWORD=reviewer-pass \
|
||||
DOGFOOD_READONLY_USERNAME=readonly DOGFOOD_READONLY_PASSWORD=readonly-pass \
|
||||
DOGFOOD_E2E_ACK=I_ACKNOWLEDGE_DOGFOOD_WRITES EXPECTED_RC_COMMIT=0000000000000000000000000000000000000000 \
|
||||
STAGING_ACTIVE_POINTER="$active_root/active" SYSTEMCTL_BIN="$TMP_DIR/bin/systemctl" PROC_ROOT="$proc_root" \
|
||||
BASE=http://127.0.0.1:48080 STUDIO_BASE=http://127.0.0.1:4173 \
|
||||
PATH="$TMP_DIR/bin:$PATH" bash "$SMOKE" --dogfood-e2e >"$TMP_DIR/wrong-process.out" 2>&1
|
||||
wrong_process_rc=$?
|
||||
set -e
|
||||
assert_exit_code 2 "$wrong_process_rc" '写 smoke 未拒绝与 active RC 不一致的 backend 进程'
|
||||
|
||||
mkdir -p "$active_release/game-cloud/huijing-server/target"
|
||||
printf 'java\0-jar\0%s\0' "$active_release/game-cloud/huijing-server/target/huijing-server.jar" \
|
||||
>"$proc_root/4242/cmdline"
|
||||
set +e
|
||||
DOGFOOD_CREATOR_USERNAME=creator DOGFOOD_CREATOR_PASSWORD=creator-pass \
|
||||
DOGFOOD_REVIEWER_USERNAME=reviewer DOGFOOD_REVIEWER_PASSWORD=reviewer-pass \
|
||||
DOGFOOD_READONLY_USERNAME=readonly DOGFOOD_READONLY_PASSWORD=readonly-pass \
|
||||
DOGFOOD_E2E_ACK=I_ACKNOWLEDGE_DOGFOOD_WRITES EXPECTED_RC_COMMIT=0000000000000000000000000000000000000000 \
|
||||
STAGING_ACTIVE_POINTER="$active_root/active" SYSTEMCTL_BIN="$TMP_DIR/bin/systemctl" PROC_ROOT="$proc_root" \
|
||||
BASE=http://127.0.0.1:48080 STUDIO_BASE=http://127.0.0.1:4173 \
|
||||
E2E_TIMEOUT_SEC=5 POLL_INTERVAL_SEC=0 EVIDENCE_OUT="$TMP_DIR/e2e.json" \
|
||||
PATH="$TMP_DIR/bin:$PATH" bash -e "$SMOKE" --dogfood-e2e >"$TMP_DIR/e2e.out" 2>&1
|
||||
e2e_rc=$?
|
||||
set -e
|
||||
if [ "$e2e_rc" -ne 0 ]; then
|
||||
sed -n '1,200p' "$TMP_DIR/e2e.out" >&2
|
||||
fail "完整写模式回归失败 rc=$e2e_rc"
|
||||
fi
|
||||
assert_contains "$TMP_DIR/e2e.out" 'pre-approve game absent from main feed'
|
||||
assert_contains "$TMP_DIR/e2e.out" 'DOGFOOD_E2E_VERDICT=PASS'
|
||||
assert_contains "$TMP_DIR/e2e.json" '"status": "passed"'
|
||||
|
||||
printf 'PASS: dogfood smoke fail-closed regression\n'
|
||||
|
||||
@ -15,6 +15,7 @@ mode_of() {
|
||||
cat >"$TMP_DIR/fake-curl" <<'PY'
|
||||
#!/usr/bin/env python3
|
||||
import json,os,sys
|
||||
import hashlib
|
||||
from pathlib import Path
|
||||
import yaml
|
||||
|
||||
@ -61,6 +62,11 @@ if "/v1/cs/configs" in url:
|
||||
respond(500,"false"); raise SystemExit
|
||||
content_arg=next((x for x in data if x.startswith("content@")),None)
|
||||
if not content_arg: respond(400,"false"); raise SystemExit
|
||||
joined="&".join(data)
|
||||
cas=next((part.split("=",1)[1] for part in joined.split("&") if part.startswith("casMd5=")),"")
|
||||
current=hashlib.md5(config.read_bytes()).hexdigest() if config.exists() else ""
|
||||
if not cas or cas != current:
|
||||
respond(200,"false"); raise SystemExit
|
||||
config.write_bytes(Path(content_arg[len("content@"):]).read_bytes())
|
||||
respond(200,"true"); raise SystemExit
|
||||
if method == "DELETE":
|
||||
@ -82,7 +88,9 @@ elif url.endswith("/app-api/passport/password-login"):
|
||||
elif url.endswith("/app-api/passport/send-sms-code"):
|
||||
code=1002090008 if enabled else 0
|
||||
elif "/app-api/trade/charge/points-balance" in url or "/app-api/trade/withdraw/page" in url:
|
||||
code=1106008000 if enabled else 0
|
||||
# mock token 已关闭;Nacos 事务若仍用 test1 探测受保护端点,应立即因 401 失败。
|
||||
respond(401,json.dumps({"code":401,"data":None},separators=(",",":")))
|
||||
raise SystemExit
|
||||
else:
|
||||
code=404
|
||||
respond(200,json.dumps({"code":code,"data":[]},separators=(",",":")))
|
||||
@ -94,6 +102,18 @@ set -euo pipefail
|
||||
printf '%s\n' "$*" >>"$MOCK_SYSTEMCTL_LOG"
|
||||
SH
|
||||
|
||||
cat >"$TMP_DIR/fake-systemd-run" <<'SH'
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
printf '%s\n' "$*" >>"$MOCK_SYSTEMD_RUN_LOG"
|
||||
[ "${MOCK_SYSTEMD_RUN_FAIL:-0}" != 1 ] || exit 19
|
||||
SH
|
||||
|
||||
cat >"$TMP_DIR/fake-flock" <<'SH'
|
||||
#!/usr/bin/env bash
|
||||
exit 0
|
||||
SH
|
||||
|
||||
cat >"$TMP_DIR/fake-provision" <<'SH'
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
@ -101,48 +121,75 @@ python3 - <<'PY'
|
||||
import os,yaml
|
||||
p=os.environ["MOCK_NACOS_CONFIG"]
|
||||
d=yaml.safe_load(open(p,encoding="utf-8"))
|
||||
assert d["huijing"]["dogfood"]["enabled"] is False
|
||||
assert d["huijing"]["dogfood"]["enabled"] is True
|
||||
assert d["huijing"]["dogfood"]["provisioning-enabled"] is True
|
||||
assert d["huijing"]["dogfood"]["provisioning-expires-at"]
|
||||
assert d["huijing"]["dogfood"]["provisioning-run-id"] == os.environ["RUN_ID"]
|
||||
ppt=d["wanxiang"]["passport"]
|
||||
assert ppt["password-register-enabled"] is True
|
||||
assert ppt["password-login-enabled"] is True
|
||||
assert min(ppt["register-ip-limit-per-hour"],ppt["register-ip-limit-per-day"],
|
||||
ppt["login-ip-limit-per-hour"],ppt["login-ip-limit-per-day"]) >= 60
|
||||
PY
|
||||
[ "${PROVISION_ACK:-}" = 'PROVISION_DOGFOOD_40_USERS_WITH_TEMP_CONFIG' ]
|
||||
[ "${DOGFOOD_PROVISIONING_MODE:-}" = 'narrow' ]
|
||||
[ -f "${ADMIN_TOKEN_FILE:-}" ] && [ ! -L "$ADMIN_TOKEN_FILE" ]
|
||||
[ "$(stat -f '%Lp' "$ADMIN_TOKEN_FILE" 2>/dev/null || stat -c '%a' "$ADMIN_TOKEN_FILE")" = 600 ]
|
||||
[ -z "${ADMIN_PASSWORD:-}" ]
|
||||
printf 'MOCK_PROVISION_CALLED\n'
|
||||
[ "${MOCK_PROVISION_FAIL:-0}" != 1 ] || exit 23
|
||||
SH
|
||||
chmod +x "$TMP_DIR/fake-curl" "$TMP_DIR/fake-systemctl" "$TMP_DIR/fake-provision"
|
||||
chmod +x "$TMP_DIR/fake-curl" "$TMP_DIR/fake-systemctl" "$TMP_DIR/fake-systemd-run" \
|
||||
"$TMP_DIR/fake-flock" "$TMP_DIR/fake-provision"
|
||||
|
||||
run_tx() {
|
||||
local evidence="$1"
|
||||
DOGFOOD_PROVISION_ACK='PROVISION_DOGFOOD_WITH_NACOS_TRANSACTION' \
|
||||
ADMIN_TOKEN_FILE="$TMP_DIR/bootstrap-admin.token" \
|
||||
NACOS_PASSWORD='test-secret-must-not-leak' \
|
||||
CURL_BIN="$TMP_DIR/fake-curl" SYSTEMCTL_BIN="$TMP_DIR/fake-systemctl" \
|
||||
SYSTEMD_RUN_BIN="$TMP_DIR/fake-systemd-run" FLOCK_BIN="$TMP_DIR/fake-flock" \
|
||||
PROVISION_BIN="$TMP_DIR/fake-provision" MOCK_NACOS_CONFIG="$TMP_DIR/nacos.yaml" \
|
||||
MOCK_PUBLISH_COUNT="$TMP_DIR/publish-count" MOCK_SYSTEMCTL_LOG="$TMP_DIR/systemctl.log" \
|
||||
MOCK_SYSTEMD_RUN_LOG="$TMP_DIR/systemd-run.log" LOCK_FILE="$TMP_DIR/provision.lock" \
|
||||
EVIDENCE_DIR="$evidence" RUN_ID="$(basename "$evidence")" RESTART_TIMEOUT_SEC=30 \
|
||||
bash "$SCRIPT"
|
||||
}
|
||||
|
||||
bash -n "$SCRIPT"
|
||||
run_watchdog() {
|
||||
local recovery="$1"
|
||||
NACOS_PASSWORD='test-secret-must-not-leak' CURL_BIN="$TMP_DIR/fake-curl" \
|
||||
SYSTEMCTL_BIN="$TMP_DIR/fake-systemctl" FLOCK_BIN="$TMP_DIR/fake-flock" \
|
||||
MOCK_NACOS_CONFIG="$TMP_DIR/nacos.yaml" MOCK_PUBLISH_COUNT="$TMP_DIR/publish-count" \
|
||||
MOCK_SYSTEMCTL_LOG="$TMP_DIR/systemctl.log" LOCK_FILE="$TMP_DIR/provision.lock" \
|
||||
RESTART_TIMEOUT_SEC=30 bash "$SCRIPT" --watchdog "$recovery"
|
||||
}
|
||||
|
||||
# 场景一:原 dataId=404,成功后必须 DELETE 回 404;重复运行仍幂等。
|
||||
bash -n "$SCRIPT"
|
||||
printf 'bootstrap-token-from-file\n' >"$TMP_DIR/bootstrap-admin.token"
|
||||
chmod 600 "$TMP_DIR/bootstrap-admin.token"
|
||||
rg -q 'flock' "$SCRIPT" || fail "缺单实例锁"
|
||||
rg -q 'casMd5' "$SCRIPT" || fail "Nacos 发布/恢复缺内容 CAS"
|
||||
rg -q 'provisioning-expires-at' "$SCRIPT" || fail "窄 provisioning 开关缺 TTL"
|
||||
rg -q 'provisioning-run-id' "$SCRIPT" || fail "窄 provisioning 开关缺运行版本令牌"
|
||||
rg -q -- '--watchdog' "$SCRIPT" || fail "缺 SIGKILL 独立 watchdog"
|
||||
! rg -q 'dogfood.*enabled.*False' "$SCRIPT" || fail "事务仍会关闭 dogfood 总守卫"
|
||||
rg -q 'password-register-enabled.*True' "$SCRIPT" || fail "事务未临时打开密码注册"
|
||||
! rg -q 'provisioning charge still disabled\|provisioning withdraw still disabled' "$SCRIPT" || \
|
||||
fail "Nacos 临时窗口仍使用无效 mock token 探测受保护资金端点"
|
||||
|
||||
# 场景一:原 dataId=404 时无法做内容 CAS,必须在修改 Nacos 和调用 provision 前 fail-closed。
|
||||
rm -f "$TMP_DIR/nacos.yaml" "$TMP_DIR/publish-count"
|
||||
run_tx "$TMP_DIR/evidence-404" >"$TMP_DIR/run-404.log"
|
||||
set +e
|
||||
run_tx "$TMP_DIR/evidence-404" >"$TMP_DIR/run-404.log" 2>&1
|
||||
rc=$?
|
||||
set -e
|
||||
[ "$rc" -ne 0 ] || fail "原 404 仍允许创建临时 Nacos 配置"
|
||||
[ ! -e "$TMP_DIR/nacos.yaml" ] || fail "原 404 成功后未删除临时配置"
|
||||
rg -q '^404$' "$TMP_DIR/evidence-404/nacos-original.status" || fail "未记录原 404"
|
||||
rg -q '^pass restored=true ' "$TMP_DIR/evidence-404/transaction.status" || fail "404 场景未收口"
|
||||
[ "$(mode_of "$TMP_DIR/evidence-404/nacos-temporary.yaml")" = 600 ] || fail "临时配置证据权限不是 600"
|
||||
while IFS= read -r evidence_file; do
|
||||
[ "$(mode_of "$evidence_file")" = 600 ] || fail "证据文件权限不是 600:$evidence_file"
|
||||
done < <(find "$TMP_DIR/evidence-404" -type f -print)
|
||||
if rg -q 'test-secret-must-not-leak' "$TMP_DIR/evidence-404"; then fail "Nacos 密钥进入证据"; fi
|
||||
|
||||
rm -f "$TMP_DIR/publish-count"
|
||||
run_tx "$TMP_DIR/evidence-404-repeat" >"$TMP_DIR/run-404-repeat.log"
|
||||
[ ! -e "$TMP_DIR/nacos.yaml" ] || fail "重复运行后未恢复 404"
|
||||
|
||||
# 场景二:已有配置必须逐字恢复,临时结构化合并不能抹掉 custom 键。
|
||||
cat >"$TMP_DIR/original.yaml" <<'YAML'
|
||||
custom:
|
||||
@ -161,20 +208,42 @@ run_tx "$TMP_DIR/evidence-existing" >"$TMP_DIR/run-existing.log"
|
||||
cmp -s "$TMP_DIR/original.yaml" "$TMP_DIR/nacos.yaml" || fail "已有 Nacos 配置未逐字恢复"
|
||||
rg -q '^200$' "$TMP_DIR/evidence-existing/nacos-original.status" || fail "未记录原 200"
|
||||
rg -q 'retained: true' "$TMP_DIR/evidence-existing/nacos-temporary.yaml" || fail "临时合并丢失 custom 键"
|
||||
rg -q '^pass restored=true ' "$TMP_DIR/evidence-existing/transaction.status" || fail "已有配置场景未收口"
|
||||
[ "$(mode_of "$TMP_DIR/evidence-existing/nacos-temporary.yaml")" = 600 ] || fail "临时配置证据权限不是 600"
|
||||
rg -q -- '--watchdog' "$TMP_DIR/systemd-run.log" || fail "未注册独立 watchdog"
|
||||
rg -q 'Restart=on-failure' "$TMP_DIR/systemd-run.log" || fail "watchdog 抢锁失败后不会自动重试"
|
||||
|
||||
# 场景三:provision 失败仍必须恢复原 404,并保留原失败码。
|
||||
rm -f "$TMP_DIR/nacos.yaml" "$TMP_DIR/publish-count"
|
||||
# watchdog 注册失败必须发生在 Nacos 写入前,且不能留下假的 armed 状态。
|
||||
cp "$TMP_DIR/original.yaml" "$TMP_DIR/nacos.yaml"
|
||||
rm -f "$TMP_DIR/publish-count"
|
||||
set +e
|
||||
MOCK_SYSTEMD_RUN_FAIL=1 run_tx "$TMP_DIR/evidence-watchdog-arm-fail" >"$TMP_DIR/watchdog-arm-fail.log" 2>&1
|
||||
rc=$?
|
||||
set -e
|
||||
[ "$rc" -ne 0 ] || fail "watchdog 注册失败仍继续预置"
|
||||
cmp -s "$TMP_DIR/original.yaml" "$TMP_DIR/nacos.yaml" || fail "watchdog 注册失败前已修改 Nacos"
|
||||
[ ! -e "$TMP_DIR/evidence-watchdog-arm-fail/watchdog/armed" ] || fail "watchdog 注册失败留下 armed 假状态"
|
||||
|
||||
# 重复运行必须仍以最新原文做 CAS,并在退出时逐字恢复。
|
||||
rm -f "$TMP_DIR/publish-count"
|
||||
run_tx "$TMP_DIR/evidence-existing-repeat" >"$TMP_DIR/run-existing-repeat.log"
|
||||
cmp -s "$TMP_DIR/original.yaml" "$TMP_DIR/nacos.yaml" || fail "重复运行后未逐字恢复"
|
||||
|
||||
# 场景三:provision 失败仍必须恢复原配置,并保留原失败码。
|
||||
cp "$TMP_DIR/original.yaml" "$TMP_DIR/nacos.yaml"
|
||||
rm -f "$TMP_DIR/publish-count"
|
||||
set +e
|
||||
MOCK_PROVISION_FAIL=1 run_tx "$TMP_DIR/evidence-provision-fail" >"$TMP_DIR/run-provision-fail.log" 2>&1
|
||||
rc=$?
|
||||
set -e
|
||||
[ "$rc" -eq 23 ] || fail "provision 失败未保留 rc=23,actual=$rc"
|
||||
[ ! -e "$TMP_DIR/nacos.yaml" ] || fail "provision 失败后未恢复原 404"
|
||||
cmp -s "$TMP_DIR/original.yaml" "$TMP_DIR/nacos.yaml" || fail "provision 失败后未恢复原配置"
|
||||
rg -q '^provision_failed rc=23 restored=true ' "$TMP_DIR/evidence-provision-fail/transaction.status" || \
|
||||
fail "provision 失败证据状态错误"
|
||||
|
||||
# 场景四:恢复 DELETE 失败必须硬失败 70,不能把 provision 成功当事务成功。
|
||||
rm -f "$TMP_DIR/nacos.yaml" "$TMP_DIR/publish-count"
|
||||
cp "$TMP_DIR/original.yaml" "$TMP_DIR/nacos.yaml"
|
||||
rm -f "$TMP_DIR/publish-count"
|
||||
set +e
|
||||
MOCK_RESTORE_FAIL=1 run_tx "$TMP_DIR/evidence-restore-fail" >"$TMP_DIR/run-restore-fail.log" 2>&1
|
||||
rc=$?
|
||||
@ -184,6 +253,29 @@ rg -q '^restore_failed main_rc=0 ' "$TMP_DIR/evidence-restore-fail/transaction.s
|
||||
fail "恢复失败证据状态错误"
|
||||
rg -q 'DOGFOOD_NACOS_TX_RESTORE_FAIL' "$TMP_DIR/run-restore-fail.log" || fail "恢复失败未明确报警"
|
||||
|
||||
# 场景五:模拟父进程 SIGKILL 后遗留临时配置;独立 watchdog 必须凭 armed + CAS 恢复原文。
|
||||
watchdog_recovery="$TMP_DIR/evidence-existing/watchdog"
|
||||
cp "$TMP_DIR/evidence-existing/nacos-temporary.yaml" "$TMP_DIR/nacos.yaml"
|
||||
: >"$watchdog_recovery/armed"
|
||||
chmod 600 "$watchdog_recovery/armed"
|
||||
rm -f "$TMP_DIR/publish-count"
|
||||
run_watchdog "$watchdog_recovery" >"$TMP_DIR/watchdog.log"
|
||||
cmp -s "$TMP_DIR/original.yaml" "$TMP_DIR/nacos.yaml" || fail "watchdog 未恢复原配置"
|
||||
rg -q '^WATCHDOG_RESTORE=PASS ' "$watchdog_recovery/watchdog.status" || fail "watchdog 未留下恢复证据"
|
||||
[ ! -e "$watchdog_recovery/armed" ] || fail "watchdog 成功后仍保留 armed"
|
||||
|
||||
# CAS 冲突时 watchdog 不能覆盖外部新配置,必须保留 armed 并硬失败等待人工处置。
|
||||
printf 'external: changed\n' >"$TMP_DIR/nacos.yaml"
|
||||
: >"$watchdog_recovery/armed"
|
||||
chmod 600 "$watchdog_recovery/armed"
|
||||
set +e
|
||||
run_watchdog "$watchdog_recovery" >"$TMP_DIR/watchdog-cas-conflict.log" 2>&1
|
||||
rc=$?
|
||||
set -e
|
||||
[ "$rc" -ne 0 ] || fail "watchdog CAS 冲突仍报告成功"
|
||||
rg -q '^external: changed$' "$TMP_DIR/nacos.yaml" || fail "watchdog CAS 冲突覆盖了外部配置"
|
||||
[ -e "$watchdog_recovery/armed" ] || fail "watchdog CAS 冲突后错误解除 armed"
|
||||
|
||||
restart_count="$(wc -l <"$TMP_DIR/systemctl.log" | tr -d ' ')"
|
||||
[ "$restart_count" -ge 8 ] || fail "临时/恢复重启次数不足:$restart_count"
|
||||
|
||||
|
||||
@ -32,6 +32,7 @@ from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer
|
||||
|
||||
state_file = os.environ["DOGFOOD_FAKE_STATE"]
|
||||
port_file = os.environ["DOGFOOD_FAKE_PORT_FILE"]
|
||||
request_log = os.environ["DOGFOOD_FAKE_REQUEST_LOG"]
|
||||
|
||||
|
||||
def load_state():
|
||||
@ -67,20 +68,39 @@ class Handler(BaseHTTPRequestHandler):
|
||||
length = int(self.headers.get("Content-Length", "0"))
|
||||
return json.loads(self.rfile.read(length) or b"{}")
|
||||
|
||||
def record(self):
|
||||
with open(request_log, "a", encoding="utf-8") as handle:
|
||||
handle.write(f"{self.command} {self.path}\n")
|
||||
|
||||
def do_GET(self):
|
||||
self.record()
|
||||
if self.path == "/app-api/ad/slot/list-enabled":
|
||||
self.response({"code": 0, "data": []})
|
||||
self.response({"code": 1111002006, "data": None})
|
||||
return
|
||||
if self.path == "/app-api/trade/charge/points-balance" or self.path.startswith(
|
||||
"/app-api/trade/withdraw/page?"
|
||||
):
|
||||
authorization = self.headers.get("Authorization")
|
||||
if authorization == "Bearer fake-player-token":
|
||||
self.response({"code": 1106008000, "data": None})
|
||||
else:
|
||||
# mock token 已关闭:test1 或无 token 都先被可信鉴权边界拒绝。
|
||||
self.response({"code": 401, "data": None}, 401)
|
||||
return
|
||||
self.response({"code": 404}, 404)
|
||||
|
||||
def do_POST(self):
|
||||
self.record()
|
||||
body = self.json_body()
|
||||
if self.path == "/admin-api/system/auth/login":
|
||||
self.response({"code": 0, "data": {"accessToken": "fake-admin-token"}})
|
||||
return
|
||||
state = load_state()
|
||||
if self.path.startswith("/admin-api/aigc/quota/claims/") and self.path.endswith("/reconcile"):
|
||||
if self.headers.get("Authorization") != "Bearer fake-admin-token":
|
||||
if self.headers.get("Authorization") not in (
|
||||
"Bearer fake-admin-token",
|
||||
"Bearer bootstrap-token-from-file",
|
||||
):
|
||||
self.response({"code": 401, "msg": "未授权"}, 401)
|
||||
return
|
||||
try:
|
||||
@ -125,6 +145,7 @@ class Handler(BaseHTTPRequestHandler):
|
||||
self.response({"code": 404}, 404)
|
||||
|
||||
def do_PUT(self):
|
||||
self.record()
|
||||
if self.path != "/admin-api/passport/player/set-creator":
|
||||
self.response({"code": 404}, 404)
|
||||
return
|
||||
@ -222,9 +243,10 @@ chmod +x "$TMP_DIR/ssh"
|
||||
|
||||
DOGFOOD_FAKE_STATE="$TMP_DIR/state.json" \
|
||||
DOGFOOD_FAKE_PORT_FILE="$TMP_DIR/port" \
|
||||
DOGFOOD_FAKE_REQUEST_LOG="$TMP_DIR/requests.log" \
|
||||
python3 "$TMP_DIR/fake-server.py" >"$TMP_DIR/server.log" 2>&1 &
|
||||
SERVER_PID=$!
|
||||
for _ in $(seq 1 50); do
|
||||
for _ in $(seq 1 300); do
|
||||
[ -s "$TMP_DIR/port" ] && break
|
||||
sleep 0.1
|
||||
done
|
||||
@ -237,8 +259,7 @@ run_provision() {
|
||||
local expected="${3:-pass}"
|
||||
if ! PROVISION_ACK='PROVISION_DOGFOOD_40_USERS_WITH_TEMP_CONFIG' \
|
||||
DOGFOOD_PASSWORD_SEED='test-seed-must-not-appear' \
|
||||
ADMIN_USERNAME='admin' \
|
||||
ADMIN_PASSWORD='admin-password-must-not-appear' \
|
||||
ADMIN_TOKEN_FILE="$TMP_DIR/bootstrap-admin.token" \
|
||||
BASE="http://127.0.0.1:$PORT" \
|
||||
MYSQL_BIN="$TMP_DIR/mysql" \
|
||||
MYSQL_PASSWORD='fake-mysql-password' \
|
||||
@ -257,8 +278,19 @@ run_provision() {
|
||||
[ "$expected" = pass ] || fail "缺失 claim 的终验场景被错误放行:$DOGFOOD_FAKE_COUNTS_MODE"
|
||||
}
|
||||
|
||||
printf 'bootstrap-token-from-file\n' >"$TMP_DIR/bootstrap-admin.token"
|
||||
chmod 600 "$TMP_DIR/bootstrap-admin.token"
|
||||
|
||||
# 第一遍模拟 40 条注册消息全部异常:账号已创建但 claim 全缺,脚本必须逐个主动补偿。
|
||||
run_provision "$TMP_DIR/first.log"
|
||||
! rg -q '^POST /admin-api/system/auth/login$' "$TMP_DIR/requests.log" || \
|
||||
fail "ADMIN_TOKEN_FILE 场景仍触发 admin 密码登录"
|
||||
for protected_path in \
|
||||
'/app-api/ad/slot/list-enabled' \
|
||||
'/app-api/trade/charge/points-balance' \
|
||||
'/app-api/trade/withdraw/page'; do
|
||||
rg -q "^GET ${protected_path}" "$TMP_DIR/requests.log" || fail "未预检安全守卫:$protected_path"
|
||||
done
|
||||
python3 - "$TMP_DIR/state.json" <<'PY'
|
||||
import json, sys
|
||||
with open(sys.argv[1], encoding="utf-8") as handle:
|
||||
@ -310,9 +342,42 @@ for log_file in "$TMP_DIR/first.log" "$TMP_DIR/second.log" "$TMP_DIR/recover-one
|
||||
|| fail "缺少终验通过证据:$log_file"
|
||||
! grep -q 'test-seed-must-not-appear' "$log_file" || fail "日志泄漏 seed"
|
||||
! grep -q 'admin-password-must-not-appear' "$log_file" || fail "日志泄漏 admin 密码"
|
||||
! grep -q 'fake-admin-token\|fake-player-token\|fake-mysql-password' "$log_file" || fail "日志泄漏 token 或数据库密码"
|
||||
! grep -q 'bootstrap-token-from-file\|fake-admin-token\|fake-player-token\|fake-mysql-password' "$log_file" || \
|
||||
fail "日志泄漏 token 或数据库密码"
|
||||
done
|
||||
|
||||
# 手工场景仍可显式使用用户名密码;不能与 token 文件混用。
|
||||
rm -f "$TMP_DIR/manual-requests.log"
|
||||
mv "$TMP_DIR/requests.log" "$TMP_DIR/token-requests.log"
|
||||
: >"$TMP_DIR/requests.log"
|
||||
PROVISION_ACK='PROVISION_DOGFOOD_40_USERS_WITH_TEMP_CONFIG' \
|
||||
DOGFOOD_PASSWORD_SEED='test-seed-must-not-appear' ADMIN_USERNAME='admin' \
|
||||
ADMIN_PASSWORD='admin-password-must-not-appear' BASE="http://127.0.0.1:$PORT" \
|
||||
MYSQL_BIN="$TMP_DIR/mysql" MYSQL_PASSWORD='fake-mysql-password' MYSQL_SSH_HOST='' \
|
||||
DOGFOOD_FAKE_STATE="$TMP_DIR/state.json" DOGFOOD_ACCOUNTS_FILE="$TMP_DIR/manual-accounts.tsv" \
|
||||
VERIFY_TIMEOUT_SEC=2 PATH="$TMP_DIR:$PATH" "$SCRIPT" >"$TMP_DIR/manual.log" 2>&1
|
||||
rg -q '^POST /admin-api/system/auth/login$' "$TMP_DIR/requests.log" || fail "手工账号密码模式未登录 admin"
|
||||
|
||||
# token 文件必须是 0600 普通文件,且两种认证来源不能同时提供。
|
||||
chmod 644 "$TMP_DIR/bootstrap-admin.token"
|
||||
if PROVISION_ACK='PROVISION_DOGFOOD_40_USERS_WITH_TEMP_CONFIG' DOGFOOD_PASSWORD_SEED=x \
|
||||
ADMIN_TOKEN_FILE="$TMP_DIR/bootstrap-admin.token" BASE="http://127.0.0.1:$PORT" \
|
||||
"$SCRIPT" >"$TMP_DIR/wide-token.log" 2>&1; then
|
||||
fail "权限过宽的 ADMIN_TOKEN_FILE 未被拒绝"
|
||||
fi
|
||||
chmod 600 "$TMP_DIR/bootstrap-admin.token"
|
||||
ln -s "$TMP_DIR/bootstrap-admin.token" "$TMP_DIR/bootstrap-admin-link.token"
|
||||
if PROVISION_ACK='PROVISION_DOGFOOD_40_USERS_WITH_TEMP_CONFIG' DOGFOOD_PASSWORD_SEED=x \
|
||||
ADMIN_TOKEN_FILE="$TMP_DIR/bootstrap-admin-link.token" BASE="http://127.0.0.1:$PORT" \
|
||||
"$SCRIPT" >"$TMP_DIR/symlink-token.log" 2>&1; then
|
||||
fail "symlink ADMIN_TOKEN_FILE 未被拒绝"
|
||||
fi
|
||||
if PROVISION_ACK='PROVISION_DOGFOOD_40_USERS_WITH_TEMP_CONFIG' DOGFOOD_PASSWORD_SEED=x \
|
||||
ADMIN_TOKEN_FILE="$TMP_DIR/bootstrap-admin.token" ADMIN_USERNAME=admin ADMIN_PASSWORD=secret \
|
||||
BASE="http://127.0.0.1:$PORT" "$SCRIPT" >"$TMP_DIR/mixed-auth.log" 2>&1; then
|
||||
fail "token 文件与用户名密码混用未被拒绝"
|
||||
fi
|
||||
|
||||
# 数据库缺失查询若返回本次 40 人之外的编号,脚本必须在请求后端前拒绝,防止越权消耗额度。
|
||||
DOGFOOD_FAKE_MISSING_MODE=rogue run_provision "$TMP_DIR/rogue-missing-id.log" '' fail
|
||||
rg -q '不属于本次 40 个目标账号' "$TMP_DIR/rogue-missing-id.log" || fail "未拒绝越界补偿 userId"
|
||||
@ -322,12 +387,12 @@ DOGFOOD_FAKE_COUNTS_MODE=duplicate run_provision "$TMP_DIR/duplicate-claims.log"
|
||||
rg -q 'duplicate_claims=1' "$TMP_DIR/duplicate-claims.log" || fail "未报告重复 claim"
|
||||
|
||||
# 危险边界必须在任何网络请求前拒绝。
|
||||
if PROVISION_ACK='wrong' DOGFOOD_PASSWORD_SEED=x ADMIN_USERNAME=x ADMIN_PASSWORD=x \
|
||||
if PROVISION_ACK='wrong' DOGFOOD_PASSWORD_SEED=x ADMIN_TOKEN_FILE="$TMP_DIR/bootstrap-admin.token" \
|
||||
BASE="http://127.0.0.1:$PORT" "$SCRIPT" >"$TMP_DIR/bad-ack.log" 2>&1; then
|
||||
fail "错误 ACK 未被拒绝"
|
||||
fi
|
||||
if PROVISION_ACK='PROVISION_DOGFOOD_40_USERS_WITH_TEMP_CONFIG' DOGFOOD_PASSWORD_SEED=x \
|
||||
ADMIN_USERNAME=x ADMIN_PASSWORD=x BASE='http://localhost:48080' \
|
||||
ADMIN_TOKEN_FILE="$TMP_DIR/bootstrap-admin.token" BASE='http://localhost:48080' \
|
||||
"$SCRIPT" >"$TMP_DIR/bad-base.log" 2>&1; then
|
||||
fail "非法 BASE host 未被拒绝"
|
||||
fi
|
||||
|
||||
@ -8,7 +8,7 @@ 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)):evidence/generation-r1-gate.txt"
|
||||
GENERATION_GATE_REF="sha256:$(printf '0%.0s' $(seq 1 64)):r1/$(printf '0%.0s' $(seq 1 64)).attestation"
|
||||
TMP_DIR="$(mktemp -d)"
|
||||
SERVER_PID=""
|
||||
|
||||
@ -41,6 +41,7 @@ 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"
|
||||
|
||||
@ -55,15 +56,60 @@ rg -q 'start_stack.*48080 4173 4174 8300 9501 true' "$TMP_DIR/remote-script.sh"
|
||||
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_stack.*18080 14173 14174 18300 19501 false' "$TMP_DIR/remote-script.sh" || \
|
||||
fail "候选栈没有关闭 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 '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 -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" | 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)"
|
||||
[ "$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 前切换"
|
||||
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"
|
||||
if rg -q '/root/games-development-ai' "$TMP_DIR/remote-script.sh"; then
|
||||
fail "激活脚本引用了远端脏旧仓"
|
||||
fi
|
||||
@ -88,6 +134,14 @@ if RC_COMMIT="0000000000000000000000000000000000000000" \
|
||||
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" \
|
||||
@ -96,17 +150,92 @@ if RC_COMMIT="0000000000000000000000000000000000000000" \
|
||||
fail "任意 OBJECT_SNAPSHOT_REF 仍允许激活"
|
||||
fi
|
||||
|
||||
# prepare 必须在 ready 前运行本任务全部 shell 回归,并明确记录跳过的后端测试。
|
||||
# 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; do
|
||||
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 'backend_tests=NOT_RUN' "$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 '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 测试计数证据"
|
||||
! 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 指针"
|
||||
|
||||
# 独立 attestation 不属于被证明 commit:内容寻址文件应通过,宽权限、symlink、commit 漂移必须拒绝。
|
||||
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"
|
||||
attestation_file="$attestation_root/r1/r1.attestation"
|
||||
cat >"$attestation_file" <<EOF
|
||||
schema=generation-r1-gate/1
|
||||
rc_commit=0000000000000000000000000000000000000000
|
||||
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"
|
||||
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"
|
||||
rg -q 'GENERATION_ATTESTATION_PASS' "$TMP_DIR/attestation-pass.log" || fail "独立 attestation 未通过"
|
||||
|
||||
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
|
||||
|
||||
# 直接验证 DB-only 审计:生成、外部引用 fail-closed、旧 hash/路径校验。
|
||||
FAKE_RC="$TMP_DIR/fake-rc"
|
||||
@ -267,7 +396,7 @@ 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 50); do
|
||||
for _ in $(seq 1 300); do
|
||||
[ -s "$TMP_DIR/port" ] && break
|
||||
sleep 0.1
|
||||
done
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user