117 lines
5.5 KiB
Bash
Executable File
117 lines
5.5 KiB
Bash
Executable File
#!/usr/bin/env bash
|
||
# 为 commit-pinned staging RC 生成可信存储审计。
|
||
# 当前狗粮主链只允许 DB 内嵌产物;一旦发现外部 URL 或 infra 文件行,立即拒绝把对象存储标为 N/A。
|
||
set -euo pipefail
|
||
|
||
RC_RELEASE="${RC_RELEASE:-}"
|
||
MYSQL_CONTAINER="${MYSQL_CONTAINER:-game-staging-mysql}"
|
||
MYSQL_DATABASE="${MYSQL_DATABASE:-ruoyi-vue-pro}"
|
||
STORAGE_AUDIT_MYSQL_BIN="${STORAGE_AUDIT_MYSQL_BIN:-}"
|
||
EXPECTED_STORAGE_AUDIT_REF="${EXPECTED_STORAGE_AUDIT_REF:-}"
|
||
|
||
if [ -z "$RC_RELEASE" ] || [ ! -d "$RC_RELEASE/evidence" ]; then
|
||
printf 'STORAGE_AUDIT_FAIL RC_RELEASE 必须指向已准备且含 evidence 的 RC。\n' >&2
|
||
exit 2
|
||
fi
|
||
|
||
# 输出位置由脚本固定,调用方不能把审计引用引向 RC 之外的任意文件。
|
||
audit_file="$RC_RELEASE/evidence/storage-audit.tsv"
|
||
audit_tmp="$audit_file.tmp.$$"
|
||
trap 'rm -f "$audit_tmp"' EXIT
|
||
|
||
# 兼容旧调用方时,先校验它引用的是 RC 内由本脚本产生的既有审计;URL、绝对路径和错误 hash 都不能通过。
|
||
if [ -n "$EXPECTED_STORAGE_AUDIT_REF" ]; then
|
||
if [[ ! "$EXPECTED_STORAGE_AUDIT_REF" =~ ^sha256:[0-9a-f]{64}:evidence/storage-audit\.tsv$ ]]; then
|
||
printf 'STORAGE_AUDIT_FAIL 旧快照引用格式或路径不受控。\n' >&2
|
||
exit 5
|
||
fi
|
||
[ -s "$audit_file" ] || {
|
||
printf 'STORAGE_AUDIT_FAIL 旧快照引用指向的 RC 审计不存在。\n' >&2
|
||
exit 5
|
||
}
|
||
expected_hash="${EXPECTED_STORAGE_AUDIT_REF#sha256:}"
|
||
expected_hash="${expected_hash%%:*}"
|
||
existing_hash="$(sha256sum "$audit_file" | awk '{print $1}')"
|
||
[ "$expected_hash" = "$existing_hash" ] || {
|
||
printf 'STORAGE_AUDIT_FAIL 调用方提供的 hash 与 RC 既有审计不一致。\n' >&2
|
||
exit 5
|
||
}
|
||
fi
|
||
|
||
run_mysql_query() {
|
||
if [ -n "$STORAGE_AUDIT_MYSQL_BIN" ]; then
|
||
"$STORAGE_AUDIT_MYSQL_BIN"
|
||
return
|
||
fi
|
||
docker inspect "$MYSQL_CONTAINER" >/dev/null
|
||
# `-i` 必须保留:审计 SQL 从本脚本 heredoc 经 stdin 送入容器内 mysql。
|
||
docker exec -i "$MYSQL_CONTAINER" sh -lc \
|
||
'MYSQL_PWD="$MYSQL_ROOT_PASSWORD" exec mysql -uroot -N -B "$1"' sh "$MYSQL_DATABASE"
|
||
}
|
||
|
||
value_of() {
|
||
local key="$1" file="$2"
|
||
awk -F= -v wanted="$key" '$1 == wanted {print substr($0, length($1) + 2); found=1} END {exit !found}' "$file"
|
||
}
|
||
|
||
{
|
||
printf 'schema=staging-storage-audit/1\n'
|
||
printf 'captured_at=%s\n' "$(date -u +%Y-%m-%dT%H:%M:%SZ)"
|
||
printf 'rc_commit=%s\n' "$(git -C "$RC_RELEASE" rev-parse HEAD)"
|
||
run_mysql_query <<'SQL'
|
||
SELECT CONCAT('game_version_rows=', COUNT(*)) FROM game_version WHERE deleted=0;
|
||
SELECT CONCAT('game_version_package_url_nonempty=', COALESCE(SUM(package_url<>''),0)) FROM game_version WHERE deleted=0;
|
||
SELECT CONCAT('game_runtime_package_rows=', COUNT(*)) FROM game_runtime_package WHERE deleted=0;
|
||
SELECT CONCAT('game_runtime_package_url_nonempty=', COALESCE(SUM(package_url<>''),0)) FROM game_runtime_package WHERE deleted=0;
|
||
SELECT CONCAT('game_runtime_package_payload_rows=', COALESCE(SUM(package_json IS NOT NULL AND CHAR_LENGTH(package_json)>0),0)) FROM game_runtime_package WHERE deleted=0;
|
||
SELECT CONCAT('game_source_project_rows=', COUNT(*)) FROM game_source_project WHERE deleted=0;
|
||
SELECT CONCAT('game_source_project_url_nonempty=', COALESCE(SUM(source_url<>''),0)) FROM game_source_project WHERE deleted=0;
|
||
SELECT CONCAT('game_source_project_payload_rows=', COALESCE(SUM(source_json IS NOT NULL AND CHAR_LENGTH(source_json)>0),0)) FROM game_source_project WHERE deleted=0;
|
||
SELECT CONCAT('game_aigc_task_rows=', COUNT(*)) FROM game_aigc_task WHERE deleted=0;
|
||
SELECT CONCAT('game_aigc_result_package_url_nonempty=', COALESCE(SUM(result_package_url<>''),0)) FROM game_aigc_task WHERE deleted=0;
|
||
SELECT CONCAT('infra_file_rows=', COUNT(*)) FROM infra_file WHERE deleted=0;
|
||
SQL
|
||
} >"$audit_tmp"
|
||
|
||
required_numeric_keys='game_version_rows game_version_package_url_nonempty game_runtime_package_rows game_runtime_package_url_nonempty game_runtime_package_payload_rows game_source_project_rows game_source_project_url_nonempty game_source_project_payload_rows game_aigc_task_rows game_aigc_result_package_url_nonempty infra_file_rows'
|
||
for key in $required_numeric_keys; do
|
||
value="$(value_of "$key" "$audit_tmp")" || {
|
||
printf 'STORAGE_AUDIT_FAIL 缺少审计字段:%s\n' "$key" >&2
|
||
exit 3
|
||
}
|
||
[[ "$value" =~ ^[0-9]+$ ]] || {
|
||
printf 'STORAGE_AUDIT_FAIL 审计字段不是非负整数:%s\n' "$key" >&2
|
||
exit 3
|
||
}
|
||
done
|
||
|
||
# 任一外部引用或 infra 文件都意味着存在尚未纳入快照的对象,不能继续使用 DB_ONLY_NA。
|
||
external_total=$((
|
||
$(value_of game_version_package_url_nonempty "$audit_tmp") +
|
||
$(value_of game_runtime_package_url_nonempty "$audit_tmp") +
|
||
$(value_of game_source_project_url_nonempty "$audit_tmp") +
|
||
$(value_of game_aigc_result_package_url_nonempty "$audit_tmp") +
|
||
$(value_of infra_file_rows "$audit_tmp")
|
||
))
|
||
if [ "$external_total" -ne 0 ]; then
|
||
printf 'STORAGE_AUDIT_FAIL 发现 %s 个外部存储引用/文件行,必须先实现对应 MinIO 前缀快照。\n' \
|
||
"$external_total" >&2
|
||
exit 4
|
||
fi
|
||
|
||
{
|
||
cat "$audit_tmp"
|
||
printf 'object_storage_status=DB_ONLY_NA\n'
|
||
printf 'minio_involved_prefix_count=0\n'
|
||
printf 'minio_involved_prefixes=DB_ONLY_NA\n'
|
||
printf 'minio_scope_reason=no_external_package_or_source_url_and_no_infra_file_rows\n'
|
||
} >"$audit_file"
|
||
chmod 600 "$audit_file"
|
||
|
||
audit_hash="$(sha256sum "$audit_file" | awk '{print $1}')"
|
||
audit_ref="sha256:${audit_hash}:evidence/storage-audit.tsv"
|
||
|
||
printf '%s\n' "$audit_ref" >"$RC_RELEASE/evidence/storage-audit.ref"
|
||
chmod 600 "$RC_RELEASE/evidence/storage-audit.ref"
|
||
printf 'STORAGE_AUDIT_PASS ref=%s\n' "$audit_ref"
|