96 行
3.3 KiB
Bash
可执行文件
96 行
3.3 KiB
Bash
可执行文件
#!/usr/bin/env bash
|
|
|
|
# Read-only identity bootstrap. The caller captures the database identity in
|
|
# memory and must never forward this script's stdout to an ordinary log.
|
|
set -euo pipefail
|
|
|
|
fail() {
|
|
printf '[production-identity-bootstrap][FAIL] code=%s\n' "$1" >&2
|
|
exit 1
|
|
}
|
|
|
|
for key in REMOTE_DIR REMOTE_ENV_FILE COMPOSE_FILE COMPOSE_PROJECT_NAME \
|
|
EXPECTED_PRODUCTION_ORIGIN_SHA256; do
|
|
[[ -n "${!key:-}" ]] || fail "${key}_MISSING"
|
|
done
|
|
|
|
[[ "$EXPECTED_PRODUCTION_ORIGIN_SHA256" =~ ^[0-9a-f]{64}$ ]] \
|
|
|| fail "PROD_TARGET_ORIGIN_HASH_INVALID"
|
|
cd "$REMOTE_DIR"
|
|
[[ -f "$REMOTE_ENV_FILE" && ! -L "$REMOTE_ENV_FILE" && -r "$REMOTE_ENV_FILE" ]] \
|
|
|| fail "PROD_TARGET_ENV_UNREADABLE"
|
|
|
|
dotenv_value() {
|
|
local key="$1"
|
|
local value
|
|
value="$(awk -v wanted="$key" '
|
|
$0 !~ /^[[:space:]]*#/ && $0 ~ "^[[:space:]]*" wanted "=" {
|
|
sub("^[[:space:]]*" wanted "=", "")
|
|
print
|
|
exit
|
|
}
|
|
' "$REMOTE_ENV_FILE")" || return 1
|
|
value="${value%$'\r'}"
|
|
case "$value" in
|
|
\"*\") value="${value:1:${#value}-2}" ;;
|
|
\'*\') value="${value:1:${#value}-2}" ;;
|
|
esac
|
|
[[ -n "$value" ]] || return 1
|
|
printf '%s' "$value"
|
|
}
|
|
|
|
hash_text() {
|
|
if command -v sha256sum >/dev/null 2>&1; then
|
|
printf '%s' "$1" | sha256sum | awk '{print $1}'
|
|
return 0
|
|
fi
|
|
if command -v shasum >/dev/null 2>&1; then
|
|
printf '%s' "$1" | shasum -a 256 | awk '{print $1}'
|
|
return 0
|
|
fi
|
|
return 1
|
|
}
|
|
|
|
reject_non_production_identity() {
|
|
local value="$(printf '%s' "$1" | tr '[:upper:]' '[:lower:]')"
|
|
[[ "$value" != *"<"* && "$value" != *">"* ]] || return 1
|
|
[[ "$value" != *"localhost"* && "$value" != *"127.0.0.1"* \
|
|
&& "$value" != *"::1"* && "$value" != *"hyb_oa_regression"* \
|
|
&& "$value" != *"staging"* && "$value" != *"stage"* \
|
|
&& "$value" != *"regression"* && "$value" != *"shadow"* \
|
|
&& "$value" != *"/test"* ]]
|
|
}
|
|
|
|
actual_origin="$(dotenv_value APP_ORIGIN)" || fail "PROD_TARGET_ORIGIN_MISSING"
|
|
actual_db_name="$(dotenv_value POSTGRES_DB)" || fail "PROD_TARGET_DB_NAME_MISSING"
|
|
[[ "$(hash_text "$actual_origin")" == "$EXPECTED_PRODUCTION_ORIGIN_SHA256" ]] \
|
|
|| fail "PROD_TARGET_ORIGIN_MISMATCH"
|
|
reject_non_production_identity "$actual_db_name" \
|
|
|| fail "PROD_TARGET_DB_NON_PRODUCTION_IDENTITY"
|
|
|
|
if docker compose version >/dev/null 2>&1; then
|
|
compose_cmd=(docker compose -p "$COMPOSE_PROJECT_NAME" -f "$COMPOSE_FILE" --env-file "$REMOTE_ENV_FILE")
|
|
elif command -v docker-compose >/dev/null 2>&1; then
|
|
compose_cmd=(docker-compose -p "$COMPOSE_PROJECT_NAME" -f "$COMPOSE_FILE" --env-file "$REMOTE_ENV_FILE")
|
|
else
|
|
fail "PROD_TARGET_COMPOSE_UNAVAILABLE"
|
|
fi
|
|
|
|
mapfile -t server_ids < <(
|
|
docker ps \
|
|
--filter "label=com.docker.compose.project=$COMPOSE_PROJECT_NAME" \
|
|
--filter "label=com.docker.compose.service=server" \
|
|
--format '{{.ID}}' 2>/dev/null || true
|
|
)
|
|
[[ "${#server_ids[@]}" -eq 1 ]] || fail "PROD_TARGET_RUNTIME_NOT_UNIQUE"
|
|
runtime_origin="$(
|
|
docker inspect -f '{{range .Config.Env}}{{println .}}{{end}}' "${server_ids[0]}" \
|
|
| sed -n 's/^APP_ORIGIN=//p' | tail -n 1
|
|
)"
|
|
[[ "$(hash_text "$runtime_origin")" == "$EXPECTED_PRODUCTION_ORIGIN_SHA256" ]] \
|
|
|| fail "PROD_TARGET_RUNTIME_ORIGIN_MISMATCH"
|
|
|
|
# Only the comparison baseline crosses the SSH channel. The database name
|
|
# itself remains remote and cannot be reconstructed from task evidence.
|
|
printf '[production-identity-bootstrap][DB_SHA256] %s\n' "$(hash_text "$actual_db_name")"
|