#!/usr/bin/env sh set -eu # ============================================================ # 1. 管理员密码来源 # ============================================================ if [ -n "${ADMIN_PASSWORD_V2:-}" ]; then ADMIN_PASSWORD="$ADMIN_PASSWORD_V2" echo "Admin password secret source: ADMIN_PASSWORD_V2" else echo "Admin password secret source: ADMIN_PASSWORD" fi # ============================================================ # 2. 自部署 PostgreSQL 初始化与启动 # ============================================================ init_postgres() { echo "Initializing and starting self-deployed PostgreSQL..." # 如果数据目录不存在,则执行初始化 if [ ! -d "$PGDATA" ]; then echo "PostgreSQL data directory not found, initializing..." mkdir -p "$PGDATA" chown postgres:postgres "$PGDATA" # 使用临时文件传递密码 pwfile=$(mktemp) echo "$POSTGRES_PASSWORD" > "$pwfile" chown postgres:postgres "$pwfile" chmod 600 "$pwfile" su-exec postgres initdb -D "$PGDATA" --auth=scram-sha-256 --pwfile="$pwfile" rm -f "$pwfile" fi # 配置 postgresql.conf echo "listen_addresses = 'localhost'" >> "$PGDATA/postgresql.conf" echo "port = 5432" >> "$PGDATA/postgresql.conf" echo "unix_socket_directories = '/tmp'" >> "$PGDATA/postgresql.conf" # 启动 PostgreSQL echo "Starting PostgreSQL daemon..." su-exec postgres pg_ctl -D "$PGDATA" -o "-c unix_socket_directories=/tmp" start # 等待就绪 echo "Waiting for PostgreSQL to accept connections..." for i in $(seq 1 30); do if su-exec postgres pg_isready -U postgres -h localhost -p 5432 >/dev/null 2>&1; then echo "PostgreSQL is ready." break fi sleep 1 done # 创建应用用户和数据库(如果不存在) echo "Ensuring application user and database exist..." export PGPASSWORD="$POSTGRES_PASSWORD" # 取消可能存在的 PGDATABASE 环境变量,并显式指定数据库为 postgres unset PGDATABASE su-exec postgres psql -h localhost -p 5432 -d postgres -v ON_ERROR_STOP=1 --username postgres <<-EOSQL DO \$\$ BEGIN IF NOT EXISTS (SELECT FROM pg_roles WHERE rolname = '$POSTGRES_USER') THEN CREATE ROLE $POSTGRES_USER WITH LOGIN PASSWORD '$POSTGRES_PASSWORD'; END IF; END \$\$; SELECT 'CREATE DATABASE $POSTGRES_DB OWNER $POSTGRES_USER' WHERE NOT EXISTS (SELECT FROM pg_database WHERE datname = '$POSTGRES_DB')\gexec EOSQL unset PGPASSWORD # 设置 DATABASE_URL 及拆分变量 export DATABASE_URL="postgresql://${POSTGRES_USER}:${POSTGRES_PASSWORD}@localhost:5432/${POSTGRES_DB}?sslmode=disable" export DATABASE_HOST=localhost export DATABASE_PORT=5432 export DATABASE_USER=$POSTGRES_USER export DATABASE_PASSWORD=$POSTGRES_PASSWORD export DATABASE_DBNAME=$POSTGRES_DB export DATABASE_SSLMODE=disable echo "PostgreSQL self-deployment ready. DATABASE_URL set to local instance." } # ============================================================ # 3. 数据库连接解析(优先使用外部连接) # ============================================================ if [ -n "${EXTERNAL_DATABASE_URL:-}" ]; then echo "EXTERNAL_DATABASE_URL provided, using external database instead of self-deployed." export DATABASE_URL="$EXTERNAL_DATABASE_URL" eval $(python3 -c " import os, urllib.parse url = os.environ['DATABASE_URL'] parsed = urllib.parse.urlparse(url) user = parsed.username or '' password = parsed.password or '' host = parsed.hostname or '' port = str(parsed.port or 5432) dbname = parsed.path.lstrip('/') or '' query = urllib.parse.parse_qs(parsed.query) sslmode = query.get('sslmode', ['require'])[0] print(f'export DATABASE_HOST={host}') print(f'export DATABASE_PORT={port}') print(f'export DATABASE_USER={user}') print(f'export DATABASE_PASSWORD={password}') print(f'export DATABASE_DBNAME={dbname}') print(f'export DATABASE_SSLMODE={sslmode}') ") echo "Parsed external database: DATABASE_HOST=$DATABASE_HOST, DATABASE_DBNAME=$DATABASE_DBNAME" else init_postgres fi # ============================================================ # 4. 检查必需的数据库变量 # ============================================================ required_db_vars="DATABASE_HOST DATABASE_PORT DATABASE_USER DATABASE_DBNAME" for var in $required_db_vars; do eval "val=\${$var:-}" if [ -z "$val" ]; then echo "Missing required database variable: $var. Please set DATABASE_URL or EXTERNAL_DATABASE_URL." >&2 exit 1 fi done # ============================================================ # 5. 检查其他必需密钥 # ============================================================ required_vars="ADMIN_PASSWORD JWT_SECRET TOTP_ENCRYPTION_KEY" for var in $required_vars; do eval "val=\${$var:-}" if [ -z "$val" ]; then echo "Missing required secret: $var" >&2 exit 1 fi done # ============================================================ # 6. 默认值设置 # ============================================================ ADMIN_EMAIL="${ADMIN_EMAIL:-2691539771@qq.com}" SERVER_PORT="${SERVER_PORT:-7860}" DATA_DIR="${DATA_DIR:-/tmp/gateway-data}" DATABASE_SSLMODE="${DATABASE_SSLMODE:-require}" REDIS_HOST="${REDIS_HOST:-127.0.0.1}" REDIS_PORT="${REDIS_PORT:-6379}" REDIS_PASSWORD="${REDIS_PASSWORD:-}" REDIS_DB="${REDIS_DB:-0}" # ============================================================ # 7. 创建目录 # ============================================================ mkdir -p "$DATA_DIR" /tmp/gateway-redis mkdir -p "$(dirname "$PGDATA")" chown -R sub2api:sub2api "$DATA_DIR" /tmp/gateway-redis # ============================================================ # 8. 启动 Redis # ============================================================ echo "Starting Redis on ${REDIS_HOST}:${REDIS_PORT}..." redis-server --daemonize yes \ --bind "${REDIS_HOST}" \ --port "${REDIS_PORT}" \ --dir /tmp/gateway-redis \ --save "" \ --appendonly no \ ${REDIS_PASSWORD:+--requirepass "$REDIS_PASSWORD"} echo "Waiting for Redis..." while ! redis-cli -h "$REDIS_HOST" -p "$REDIS_PORT" ${REDIS_PASSWORD:+-a "$REDIS_PASSWORD"} ping >/dev/null 2>&1; do sleep 1 done echo "Redis is ready." # ============================================================ # 9. 导出环境变量供子进程使用 # ============================================================ export AUTO_SETUP="${AUTO_SETUP:-true}" export ADMIN_EMAIL ADMIN_PASSWORD JWT_SECRET TOTP_ENCRYPTION_KEY export DATA_DIR export DATABASE_HOST DATABASE_PORT DATABASE_USER DATABASE_PASSWORD DATABASE_DBNAME DATABASE_SSLMODE export REDIS_HOST REDIS_PORT REDIS_PASSWORD REDIS_DB export SERVER_HOST="${SERVER_HOST:-0.0.0.0}" SERVER_PORT export SERVER_MODE="${SERVER_MODE:-release}" export PORT="${PORT:-$SERVER_PORT}" GIN_MODE="${GIN_MODE:-release}" # ============================================================ # 10. 辅助函数 # ============================================================ migration_count() { PGPASSWORD="${DATABASE_PASSWORD:-}" psql \ -h "$DATABASE_HOST" -p "$DATABASE_PORT" \ -U "$DATABASE_USER" -d "$DATABASE_DBNAME" \ -tAc "select count(1) from schema_migrations;" 2>/dev/null || printf '0\n' } sync_balance_cache_once() { [ "${BALANCE_CACHE_SYNC_ENABLED:-true}" != "true" ] && return 0 rows="$(PGPASSWORD="${DATABASE_PASSWORD:-}" psql -h "$DATABASE_HOST" -p "$DATABASE_PORT" -U "$DATABASE_USER" -d "$DATABASE_DBNAME" -qAt -F '|' -c "select id, balance::double precision from users where deleted_at is null and status = 'active';" 2>/tmp/balance-cache-sync.err || true)" [ -z "$rows" ] && return 0 ttl="${BALANCE_CACHE_SYNC_TTL_SECONDS:-300}" printf '%s\n' "$rows" | while IFS='|' read -r user_id balance; do case "$user_id" in ''|*[!0-9]*) continue ;; esac redis-cli -h "$REDIS_HOST" -p "$REDIS_PORT" ${REDIS_PASSWORD:+-a "$REDIS_PASSWORD"} -n "$REDIS_DB" \ SETEX "billing:balance:${user_id}" "$ttl" "$balance" >/dev/null 2>&1 || true done } start_balance_cache_sync() { [ "${BALANCE_CACHE_SYNC_ENABLED:-true}" != "true" ] && return 0 ( interval="${BALANCE_CACHE_SYNC_INTERVAL_SECONDS:-60}" while :; do sync_balance_cache_once || true sleep "$interval" done ) & echo "Balance cache sync loop started." } write_runtime_config() { cat >"$DATA_DIR/config.yaml" <"$DATA_DIR/.installed" chown sub2api:sub2api "$DATA_DIR/config.yaml" "$DATA_DIR/.installed" chmod 600 "$DATA_DIR/config.yaml" chmod 400 "$DATA_DIR/.installed" } maybe_skip_auto_setup() { [ "${FORCE_RUNTIME_CONFIG:-false}" = "true" ] && { write_runtime_config export AUTO_SETUP=false echo "Runtime config forced; skipping auto setup." return 0 } [ "${SKIP_AUTO_SETUP_WHEN_MIGRATED:-true}" != "true" ] && return 0 applied_count="$(migration_count | tr -d '[:space:]')" case "$applied_count" in ''|*[!0-9]*) applied_count=0 ;; esac required_count="${MIGRATION_BOOTSTRAP_MIN_COUNT:-178}" echo "Detected applied migrations: ${applied_count}/${required_count}" if [ "$applied_count" -ge "$required_count" ]; then write_runtime_config export AUTO_SETUP=false echo "External database is already bootstrapped; skipping auto setup." fi } sync_admin_password() { [ "${SYNC_ADMIN_PASSWORD:-true}" != "true" ] && return 0 echo "Admin password sync loop started." for attempt in $(seq 1 15); do password_hash="$(python3 - <<'PY' import bcrypt, os print(bcrypt.hashpw(os.environ["ADMIN_PASSWORD"].encode("utf-8"), bcrypt.gensalt()).decode("utf-8")) PY )" table_exists="$(PGPASSWORD="${DATABASE_PASSWORD:-}" psql -h "$DATABASE_HOST" -p "$DATABASE_PORT" -U "$DATABASE_USER" -d "$DATABASE_DBNAME" -tAc "select to_regclass('public.users') is not null;" 2>/tmp/admin-user-check.err || true)" table_exists="$(printf '%s' "$table_exists" | tr -d '[:space:]')" if [ "$table_exists" = "t" ]; then PGPASSWORD="${DATABASE_PASSWORD:-}" psql -h "$DATABASE_HOST" -p "$DATABASE_PORT" -U "$DATABASE_USER" -d "$DATABASE_DBNAME" -v ON_ERROR_STOP=1 \ -c "update users set password_hash = '${password_hash}', role = 'admin', status = 'active', updated_at = now() where email = '${ADMIN_EMAIL}'; insert into users (email, password_hash, role, balance, concurrency, status, created_at, updated_at) select '${ADMIN_EMAIL}', '${password_hash}', 'admin', 0, 5, 'active', now(), now() where not exists (select 1 from users where email = '${ADMIN_EMAIL}');" >/tmp/admin-password-sync.out 2>/tmp/admin-password-sync.err echo "Admin password synchronized from secret." return 0 fi sleep 2 done echo "Admin password sync skipped: admin user not found before timeout." >&2 } # ============================================================ # 11. 主流程 # ============================================================ maybe_skip_auto_setup start_balance_cache_sync attempt=1 max_attempts="${BOOTSTRAP_MAX_ATTEMPTS:-6}" while :; do echo "Starting gateway on ${SERVER_HOST}:${SERVER_PORT} (attempt ${attempt}/${max_attempts})" su-exec sub2api /app/hub-gateway & app_pid="$!" trap 'kill -TERM "$app_pid" 2>/dev/null || true' INT TERM sync_admin_password & set +e wait "$app_pid" app_status="$?" set -e if [ "$app_status" -eq 0 ]; then exit 0 fi if [ "$AUTO_SETUP" != "true" ]; then exit "$app_status" fi maybe_skip_auto_setup if [ "$AUTO_SETUP" != "true" ]; then attempt=$((attempt + 1)) continue fi if [ "$attempt" -ge "$max_attempts" ]; then exit "$app_status" fi attempt=$((attempt + 1)) echo "Gateway exited during bootstrap; retrying to continue migrations." sleep 2 done