XuqmGroup-PrivateDeploy/scripts/lib.sh
2026-07-29 01:37:09 +08:00

215 行
6.5 KiB
Bash
可执行文件

#!/usr/bin/env bash
set -euo pipefail
ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
AUDIT_LOG="$ROOT_DIR/logs/audit.log"
PROGRESS_FILE="$ROOT_DIR/.deploy-state/progress.md"
now() {
date +"%Y-%m-%dT%H:%M:%S%z"
}
audit() {
local action="$1"
local status="$2"
local message="${3:-}"
mkdir -p "$(dirname "$AUDIT_LOG")"
printf '{"time":"%s","action":"%s","status":"%s","message":"%s"}\n' \
"$(now)" "$action" "$status" "$message" >> "$AUDIT_LOG"
}
progress() {
local step="$1"
local status="$2"
local notes="${3:-}"
mkdir -p "$(dirname "$PROGRESS_FILE")"
if [ ! -f "$PROGRESS_FILE" ]; then
printf '# Deployment Progress\n\n| Time | Step | Status | Notes |\n|------|------|--------|-------|\n' > "$PROGRESS_FILE"
fi
printf '| %s | %s | %s | %s |\n' "$(now)" "$step" "$status" "$notes" >> "$PROGRESS_FILE"
}
fail_json() {
local code="$1"
local message="$2"
local step="${3:-unknown}"
printf '{"code":"%s","message":"%s","step":"%s","recoverable":true}\n' "$code" "$message" "$step" >&2
audit "$step" "FAILED" "$message"
progress "$step" "FAILED" "$message"
exit 1
}
load_env() {
set -a
# infra.env 只提供仓库级基础默认值;实际部署生成的 .env 必须后加载并成为
# MySQL/Redis 模式、主机和 profile 的权威来源。旧顺序会让 managed 部署被误判成
# external,导致备份在宿主机解析 Docker 服务名失败。
[ -f "$ROOT_DIR/config/infra.env" ] && . "$ROOT_DIR/config/infra.env"
[ -f "$ROOT_DIR/.env" ] && . "$ROOT_DIR/.env"
[ -f "$ROOT_DIR/config/xuqm.env" ] && . "$ROOT_DIR/config/xuqm.env"
[ -f "$ROOT_DIR/config/secrets.env" ] && . "$ROOT_DIR/config/secrets.env"
set +a
}
require_cmd() {
command -v "$1" >/dev/null 2>&1 || fail_json "XUQM_PRIVATE_4001" "missing required command: $1" "preflight"
}
random_secret() {
if command -v openssl >/dev/null 2>&1; then
openssl rand -base64 32 | tr -d '\n'
else
printf '%s_%s' "$(date +%s)" "$RANDOM"
fi
}
random_aes256_base64() {
if command -v openssl >/dev/null 2>&1; then
openssl rand -base64 32 | tr -d '\n'
else
dd if=/dev/urandom bs=32 count=1 2>/dev/null | base64 | tr -d '\n'
fi
}
ensure_secret_file() {
if [ ! -f "$ROOT_DIR/config/secrets.env" ]; then
cp "$ROOT_DIR/config/secrets.env.example" "$ROOT_DIR/config/secrets.env"
chmod 600 "$ROOT_DIR/config/secrets.env" || true
fi
}
set_env_value() {
local file="$1"
local key="$2"
local value="$3"
mkdir -p "$(dirname "$file")"
touch "$file"
if grep -q "^${key}=" "$file"; then
sed -i.bak "s|^${key}=.*|${key}=${value}|" "$file"
rm -f "${file}.bak"
else
printf '%s=%s\n' "$key" "$value" >> "$file"
fi
}
ensure_env_value() {
local file="$1"
local key="$2"
local current="${3:-}"
local generated="$4"
if [ -z "$current" ] || [ "$current" = "change-me" ]; then
set_env_value "$file" "$key" "$generated"
printf '%s' "$generated"
else
printf '%s' "$current"
fi
}
ensure_internal_service_token() {
local secrets_file="${1:-$ROOT_DIR/config/secrets.env}"
local current generated
if [ ! -f "$secrets_file" ]; then
mkdir -p "$(dirname "$secrets_file")"
cp "$ROOT_DIR/config/secrets.env.example" "$secrets_file"
fi
current="$(grep '^SDK_INTERNAL_TOKEN=' "$secrets_file" 2>/dev/null | cut -d= -f2- || true)"
if [ -n "$current" ] && [ "$current" != "xuqm-internal-token" ] && [ "$current" != "change-me" ]; then
return 0
fi
generated="$(random_secret)"
[ -n "$generated" ] || {
printf '生成内部服务令牌失败\n' >&2
return 1
}
set_env_value "$secrets_file" "SDK_INTERNAL_TOKEN" "$generated"
chmod 600 "$secrets_file" || true
}
ensure_sdk_config_signing_keypair() {
local secrets_file="${1:-$ROOT_DIR/config/secrets.env}"
local private_key public_key key_id key_dir
if [ ! -f "$secrets_file" ]; then
mkdir -p "$(dirname "$secrets_file")"
cp "$ROOT_DIR/config/secrets.env.example" "$secrets_file"
chmod 600 "$secrets_file" || true
fi
private_key="$(grep '^SDK_CONFIG_SIGNING_PRIVATE_KEY_BASE64=' "$secrets_file" 2>/dev/null | cut -d= -f2- || true)"
public_key="$(grep '^SDK_CONFIG_SIGNING_PUBLIC_KEY_BASE64=' "$secrets_file" 2>/dev/null | cut -d= -f2- || true)"
key_id="$(grep '^SDK_CONFIG_SIGNING_KEY_ID=' "$secrets_file" 2>/dev/null | cut -d= -f2- || true)"
if [ -n "$private_key" ] && [ -n "$public_key" ] && [ -n "$key_id" ]; then
return 0
fi
if [ -n "$private_key" ] || [ -n "$public_key" ] || [ -n "$key_id" ]; then
printf 'SDK 配置签名密钥只配置了一部分,拒绝自动轮换;请恢复完整密钥对后重试\n' >&2
return 1
fi
command -v openssl >/dev/null 2>&1 || {
printf '生成 SDK 配置签名密钥需要 openssl\n' >&2
return 1
}
key_dir="$(mktemp -d "${TMPDIR:-/tmp}/xuqm-config-signing.XXXXXX")"
if ! openssl genpkey -algorithm Ed25519 -out "$key_dir/private.pem" >/dev/null 2>&1 ||
! openssl pkey -in "$key_dir/private.pem" -outform DER -out "$key_dir/private.der" >/dev/null 2>&1 ||
! openssl pkey -in "$key_dir/private.pem" -pubout -outform DER -out "$key_dir/public.der" >/dev/null 2>&1; then
rm -rf "$key_dir"
printf '生成 Ed25519 SDK 配置签名密钥失败\n' >&2
return 1
fi
private_key="$(base64 < "$key_dir/private.der" | tr -d '\r\n')"
public_key="$(base64 < "$key_dir/public.der" | tr -d '\r\n')"
key_id="private-$(openssl dgst -sha256 "$key_dir/public.der" | awk '{print substr($NF,1,16)}')"
rm -rf "$key_dir"
set_env_value "$secrets_file" "SDK_CONFIG_SIGNING_PRIVATE_KEY_BASE64" "$private_key"
set_env_value "$secrets_file" "SDK_CONFIG_SIGNING_PUBLIC_KEY_BASE64" "$public_key"
set_env_value "$secrets_file" "SDK_CONFIG_SIGNING_KEY_ID" "$key_id"
chmod 600 "$secrets_file" || true
}
profile_contains() {
local profiles="$1"
local target="$2"
case ",${profiles}," in
*,"${target}",*) return 0 ;;
*) return 1 ;;
esac
}
add_profile() {
local profiles="${1:-base}"
local target="$2"
if profile_contains "$profiles" "$target"; then
printf '%s' "$profiles"
else
printf '%s,%s' "$profiles" "$target"
fi
}
remove_profile() {
local profiles="$1"
local target="$2"
local result=""
local item
IFS=',' read -r -a items <<< "$profiles"
for item in "${items[@]}"; do
[ "$item" = "$target" ] && continue
[ -z "$item" ] && continue
if [ -z "$result" ]; then
result="$item"
else
result="$result,$item"
fi
done
printf '%s' "${result:-base}"
}
compose() {
docker compose --env-file "$ROOT_DIR/.env" -f "$ROOT_DIR/docker-compose.yml" -f "$ROOT_DIR/docker-compose.infra.yml" "$@"
}