Files
OverCooked-Installer/install.sh
T

278 lines
13 KiB
Bash
Executable File
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#!/bin/sh
set -eu
umask 077
# Standalone public customer bootstrap. It never checks out application source
# and it never pulls an Auth image.
REGISTRY=${OVERCOOKED_REGISTRY:-git.leee.xin/overcooked}
VERSION=${OVERCOOKED_VERSION:-latest}
ROOT=${OVERCOOKED_ROOT:-/opt/overcooked}
SERVER_IP=${OVERCOOKED_SERVER_IP:-}
ACTIVATION_CODE=${OVERCOOKED_ACTIVATION_CODE:-}
AUTH_URL=${OVERCOOKED_AUTH_URL:-http://148.135.97.10:18091}
AUTH_PUBLIC_KEYS=${OVERCOOKED_AUTH_PUBLIC_KEYS:-}
INSTALLER_BASE_URL=${OVERCOOKED_INSTALLER_BASE_URL:-https://git.leee.xin/OverCooked/OverCooked-Installer/raw/branch/main}
TURNSTILE_SITE_KEY=${OVERCOOKED_TURNSTILE_SITE_KEY:-}
TURNSTILE_SECRET_KEY=${OVERCOOKED_TURNSTILE_SECRET_KEY:-}
S_COMPOSE=$ROOT/compose/s.yml
E_COMPOSE=$ROOT/compose/e.yml
ENV_FILE=$ROOT/production.env
S_PROJECT=overcooked-s
E_PROJECT=overcooked-e
FAILURE_REPORTED=0
S_POSTGRES_PASSWORD=not-generated
S_VALKEY_PASSWORD=not-generated
S_INTERNAL_SHARED_SECRET=not-generated
S_ACCESS_TOKEN_SECRET=not-generated
S_SETUP_TOKEN=not-generated
E_POSTGRES_PASSWORD=not-generated
E_PLAYBACK_TICKET_SECRET=not-generated
E_PLAYBACK_LEASE_KEY=not-generated
E_STORAGE_CREDENTIAL_KEY=not-generated
E_PLUGIN_SECRET_KEY=not-generated
die() { echo "overcooked-install: $*" >&2; exit 1; }
is_ipv4() {
old_ifs=$IFS; IFS=.; set -- $1; IFS=$old_ifs
[ "$#" = 4 ] || return 1
for octet in "$@"; do
case "$octet" in ''|*[!0-9]*) return 1 ;; esac
[ "$octet" -le 255 ] 2>/dev/null || return 1
done
}
is_private_ipv4() {
second=$(printf '%s\n' "$1" | cut -d. -f2)
case "$1" in
0.*|10.*|127.*|169.254.*|192.168.*|100.64.*|100.65.*|100.66.*|100.67.*|100.68.*|100.69.*|100.70.*|100.71.*|100.72.*|100.73.*|100.74.*|100.75.*|100.76.*|100.77.*|100.78.*|100.79.*|100.8*.*|100.9*.*) return 0 ;;
172.*) [ "$second" -ge 16 ] 2>/dev/null && [ "$second" -le 31 ] 2>/dev/null; return ;;
esac
return 1
}
check_port() {
port=$1
if command -v ss >/dev/null 2>&1 && ss -H -ltn 2>/dev/null | awk -v p=":$port" '$4 ~ p "$" || $4 ~ p "\\]$" { found=1 } END { exit !found }'; then
die "端口 $port 已被占用,请停止占用进程或调整部署端口"
fi
}
redact_logs() {
sed -e "s|$S_POSTGRES_PASSWORD|[REDACTED]|g" \
-e "s|$S_VALKEY_PASSWORD|[REDACTED]|g" \
-e "s|$S_INTERNAL_SHARED_SECRET|[REDACTED]|g" \
-e "s|$S_ACCESS_TOKEN_SECRET|[REDACTED]|g" \
-e "s|$S_SETUP_TOKEN|[REDACTED]|g" \
-e "s|$E_POSTGRES_PASSWORD|[REDACTED]|g" \
-e "s|$E_PLAYBACK_TICKET_SECRET|[REDACTED]|g" \
-e "s|$E_PLAYBACK_LEASE_KEY|[REDACTED]|g" \
-e "s|$E_STORAGE_CREDENTIAL_KEY|[REDACTED]|g" \
-e "s|$E_PLUGIN_SECRET_KEY|[REDACTED]|g"
}
show_failure() {
[ "$FAILURE_REPORTED" = 1 ] && return 0
FAILURE_REPORTED=1
[ -f "$ENV_FILE" ] || return 0
echo "安装失败,以下是脱敏后的容器状态和最近日志:" >&2
docker compose --env-file "$ENV_FILE" -f "$E_COMPOSE" -p "$E_PROJECT" ps >&2 || true
docker compose --env-file "$ENV_FILE" -f "$S_COMPOSE" -p "$S_PROJECT" ps >&2 || true
docker compose --env-file "$ENV_FILE" -f "$E_COMPOSE" -p "$E_PROJECT" logs --tail=100 2>&1 | redact_logs >&2 || true
docker compose --env-file "$ENV_FILE" -f "$S_COMPOSE" -p "$S_PROJECT" logs --tail=100 2>&1 | redact_logs >&2 || true
}
on_exit() { rc=$?; [ "$rc" -eq 0 ] || show_failure; exit "$rc"; }
trap on_exit EXIT
[ "$(id -u)" = 0 ] || die "请使用 sudo/root 执行"
command -v docker >/dev/null 2>&1 || die "未找到 Docker Engine"
docker compose version >/dev/null 2>&1 || die "未找到 Docker Compose v2"
command -v curl >/dev/null 2>&1 || die "未找到 curl"
command -v openssl >/dev/null 2>&1 || die "未找到 openssl"
command -v tar >/dev/null 2>&1 || die "未找到 tar"
command -v mktemp >/dev/null 2>&1 || die "未找到 mktemp"
for pair in "OVERCOOKED_REGISTRY:$REGISTRY" "OVERCOOKED_VERSION:$VERSION" "OVERCOOKED_ROOT:$ROOT" "OVERCOOKED_AUTH_URL:$AUTH_URL" "OVERCOOKED_INSTALLER_BASE_URL:$INSTALLER_BASE_URL"; do
name=${pair%%:*}; value=${pair#*:}; [ -n "$value" ] || die "$name 不能为空"
case "$value" in *' '*|*' '*|*'\r'*|*'\n'*) die "$name 包含非法空白字符" ;; esac
done
case "$REGISTRY" in *://*|*/|*//*|*=*) die "OVERCOOKED_REGISTRY 必须是镜像 Registry 路径" ;; esac
case "$VERSION" in */*|*:*|*[!A-Za-z0-9._-]*) die "OVERCOOKED_VERSION 格式无效" ;; esac
case "$ROOT" in /*) ;; *) die "OVERCOOKED_ROOT 必须是绝对路径" ;; esac
case "$INSTALLER_BASE_URL" in *://*) ;; *) die "Installer 地址必须是 HTTP(S) URL" ;; esac
if [ -z "$SERVER_IP" ]; then
command -v ip >/dev/null 2>&1 || die "未找到 ip,请设置 OVERCOOKED_SERVER_IP"
SERVER_IP=$(ip -4 route get 1.1.1.1 2>/dev/null | awk '{for (i=1;i<=NF;i++) if ($i=="src") {print $(i+1); exit}}')
[ -n "$SERVER_IP" ] || die "无法识别服务器公网 IPv4,请设置 OVERCOOKED_SERVER_IP"
EXPLICIT_SERVER_IP=0
else
EXPLICIT_SERVER_IP=1
fi
is_ipv4 "$SERVER_IP" || die "仅支持有效 IPv4 地址:$SERVER_IP"
if [ "$EXPLICIT_SERVER_IP" = 0 ] && is_private_ipv4 "$SERVER_IP"; then
die "检测到私有地址,请设置 OVERCOOKED_SERVER_IP=公网IPv4"
fi
[ -n "$AUTH_PUBLIC_KEYS" ] || die "请设置 OVERCOOKED_AUTH_PUBLIC_KEYS(Auth 公钥 JSON)"
case "$AUTH_PUBLIC_KEYS" in \{*\}) ;; *) die "OVERCOOKED_AUTH_PUBLIC_KEYS 必须是 JSON 对象" ;; esac
case "$AUTH_PUBLIC_KEYS" in *'\n'*|*'\r'*) die "OVERCOOKED_AUTH_PUBLIC_KEYS 不得包含换行" ;; esac
if [ -z "$ACTIVATION_CODE" ]; then
[ -t 0 ] || die "授权码不能为空,请设置 OVERCOOKED_ACTIVATION_CODE"
printf '请输入授权码: ' >&2; IFS= read -r ACTIVATION_CODE
fi
[ -n "$ACTIVATION_CODE" ] || die "授权码不能为空"
if [ -z "$TURNSTILE_SITE_KEY" ] || [ -z "$TURNSTILE_SECRET_KEY" ]; then
die "当前 S 生产镜像需要 OVERCOOKED_TURNSTILE_SITE_KEY 和 OVERCOOKED_TURNSTILE_SECRET_KEY;请提供成对配置"
fi
check_port 18080; check_port 18083; check_port 8096
install -d -m 700 "$ROOT" "$ROOT/config" "$ROOT/compose" "$ROOT/migrations" "$ROOT/node-images"
SCRIPT_DIR=$(CDPATH= cd -- "$(dirname -- "$0")" && pwd)
fetch_asset() {
relative=$1; destination=$2; source="$SCRIPT_DIR/$relative"
if [ -f "$source" ]; then install -m 600 "$source" "$destination"; return; fi
temporary=$(mktemp "$destination.download.XXXXXX")
curl -fsSL --retry 3 --connect-timeout 10 --max-time 120 "$INSTALLER_BASE_URL/$relative" -o "$temporary" || die "下载公开 Installer 资产失败:$relative"
[ -s "$temporary" ] || die "公开 Installer 资产为空:$relative"
install -m 600 "$temporary" "$destination"; rm -f "$temporary"
}
fetch_asset compose/s.yml "$S_COMPOSE"
fetch_asset compose/e.yml "$E_COMPOSE"
fetch_asset s-migrate.sh "$ROOT/s-migrate.sh"
fetch_asset config/e-server.production.yaml "$ROOT/config/e-server.production.yaml"
fetch_asset s-migrations.tar.gz "$ROOT/s-migrations.tar.gz"
tar -xzf "$ROOT/s-migrations.tar.gz" -C "$ROOT/migrations"; rm -f "$ROOT/s-migrations.tar.gz"
find "$ROOT/migrations" -type f ! -name '*.up.sql' -delete
find "$ROOT/migrations" -type f -name '*.up.sql' -exec chmod 600 {} \;
secret() { openssl rand -hex 32; }
S_POSTGRES_PASSWORD=$(secret); S_VALKEY_PASSWORD=$(secret)
S_INTERNAL_SHARED_SECRET=$(secret); S_ACCESS_TOKEN_SECRET=$(secret); S_SETUP_TOKEN=$(secret)
E_POSTGRES_PASSWORD=$(secret); E_PLAYBACK_TICKET_SECRET=$(secret); E_PLAYBACK_LEASE_KEY=$(secret)
E_STORAGE_CREDENTIAL_KEY=$(secret); E_PLUGIN_SECRET_KEY=$(secret)
INSTANCE_SUFFIX=$(hostname 2>/dev/null | tr -cd '[:alnum:]' | cut -c1-24); [ -n "$INSTANCE_SUFFIX" ] || INSTANCE_SUFFIX=customer
cat > "$ENV_FILE" <<EOF
OVERCOOKED_ROOT=$ROOT
S_CONTROL_IMAGE=$REGISTRY/overcooked-s-control:$VERSION
S_WEBUI_IMAGE=$REGISTRY/overcooked-s-webui:$VERSION
S_POSTGRES_PASSWORD=$S_POSTGRES_PASSWORD
S_VALKEY_PASSWORD=$S_VALKEY_PASSWORD
S_INTERNAL_KEY_ID=customer-s
S_INTERNAL_SHARED_SECRET=$S_INTERNAL_SHARED_SECRET
S_ACCESS_TOKEN_SECRET=$S_ACCESS_TOKEN_SECRET
S_SETUP_TOKEN=$S_SETUP_TOKEN
S_E_BASE_URL=http://e-server:8096
S_E_PUBLIC_URL=http://$SERVER_IP:8096
S_E_KEY_ID=customer-e
S_E_INTERNAL_SECRET=$S_INTERNAL_SHARED_SECRET
S_E_ALLOW_INSECURE_HTTP=true
S_AUTH_BASE_URL=$AUTH_URL
S_AUTH_ALLOW_INSECURE_HTTP=$(case "$AUTH_URL" in http://*) echo true ;; *) echo false ;; esac)
S_AUTH_PUBLIC_KEYS=$AUTH_PUBLIC_KEYS
S_INSTANCE_ID=s-$INSTANCE_SUFFIX
S_PLATFORM=linux-amd64
S_VERSION=$VERSION
S_PUBLIC_HOST=$SERVER_IP
S_PORTAL_SERVER_URL=http://$SERVER_IP:18080
S_PORTAL_WEB_CLIENT_URL=http://$SERVER_IP:18083/login
S_API_PORT_MAPPING=0.0.0.0:18080:8080
S_WEB_PORT_MAPPING=0.0.0.0:18083:3000
S_TURNSTILE_SITE_KEY=$TURNSTILE_SITE_KEY
S_TURNSTILE_SECRET_KEY=$TURNSTILE_SECRET_KEY
PRODUCT_E_IMAGE=$REGISTRY/overcooked-e:$VERSION
PRODUCT_E_INSTANCE_ID=e-$INSTANCE_SUFFIX
PRODUCT_E_DATABASE_URL=postgres://product_e:$E_POSTGRES_PASSWORD@postgres:5432/product_e?sslmode=disable
PRODUCT_E_POSTGRES_PASSWORD=$E_POSTGRES_PASSWORD
PRODUCT_E_VALKEY_PASSWORD=$S_VALKEY_PASSWORD
PRODUCT_E_CONTROL_PLANE_URL=http://s-control:8080
PRODUCT_E_CONTROL_PLANE_KEY_ID=customer-s
PRODUCT_E_CONTROL_PLANE_SHARED_SECRET=$S_INTERNAL_SHARED_SECRET
PRODUCT_E_LICENSE_ENABLED=true
PRODUCT_E_AUTH_URL=$AUTH_URL
PRODUCT_E_AUTH_ALLOW_INSECURE_HTTP=$(case "$AUTH_URL" in http://*) echo true ;; *) echo false ;; esac)
PRODUCT_E_AUTH_ACTIVATION_CODE=$ACTIVATION_CODE
PRODUCT_E_AUTH_PUBLIC_KEYS=$AUTH_PUBLIC_KEYS
PRODUCT_E_PLAYBACK_TICKET_KEY_ID=customer-playback
PRODUCT_E_PLAYBACK_TICKET_SECRET=$E_PLAYBACK_TICKET_SECRET
PRODUCT_E_PLAYBACK_LEASE_KEY=$E_PLAYBACK_LEASE_KEY
PRODUCT_E_STORAGE_CREDENTIAL_KEY_ID=customer-storage
PRODUCT_E_STORAGE_CREDENTIAL_KEY=$E_STORAGE_CREDENTIAL_KEY
PRODUCT_E_PLUGIN_SECRET_KEY_ID=customer-plugin
PRODUCT_E_PLUGIN_SECRET_KEY=$E_PLUGIN_SECRET_KEY
PRODUCT_E_PLUGIN_TRUSTED_PUBLISHERS={}
EOF
chmod 600 "$ENV_FILE"
cat > "$ROOT/config/e-server.production.yaml" <<EOF
service:
name: overcooked-e
instance_id: e-$INSTANCE_SUFFIX
environment: production
emby:
server_name: OverCooked
server_id: 10000000-0000-4000-8000-000000000001
compatibility_version: 4.8.0.80
control_plane:
base_url: https://s-control.invalid
key_id: customer-s
shared_secret: replace-by-environment
timeout: 10s
server:
address: 0.0.0.0:8096
database:
url: postgres://product_e:replace-by-environment@postgres:5432/product_e?sslmode=disable
valkey:
address: valkey:6379
observability:
log_level: info
trace_sample_ratio: 0.05
playback:
node_id: 10000000-0000-4000-8000-000000000001
plugins:
enabled: true
root: /data/plugins
sdk_socket: unix:///data/plugins/runtime/core.sock
core_version: 0.1.0
unsigned_policy: require-signature
trusted_publishers: {}
secret_key_id: customer-plugin
previous_secret_keys: {}
network_allowed_domains: []
network_allowed_cidrs: []
EOF
chmod 600 "$ROOT/config/e-server.production.yaml"
docker network inspect overcooked-control >/dev/null 2>&1 || docker network create overcooked-control >/dev/null
docker compose --env-file "$ENV_FILE" -f "$E_COMPOSE" -p "$E_PROJECT" config >/dev/null
docker compose --env-file "$ENV_FILE" -f "$S_COMPOSE" -p "$S_PROJECT" config >/dev/null
for image in "$REGISTRY/overcooked-e:$VERSION" "$REGISTRY/overcooked-s-control:$VERSION" "$REGISTRY/overcooked-s-webui:$VERSION" "$REGISTRY/overcooked-media-node:$VERSION"; do
docker pull "$image"
docker image inspect "$image" >/dev/null 2>&1 || die "镜像校验失败:$image"
done
docker compose --env-file "$ENV_FILE" -f "$E_COMPOSE" -p "$E_PROJECT" up -d postgres valkey
docker compose --env-file "$ENV_FILE" -f "$E_COMPOSE" -p "$E_PROJECT" run --rm --no-deps e-server migrate -config /etc/overcooked-e/config.yaml
docker compose --env-file "$ENV_FILE" -f "$S_COMPOSE" -p "$S_PROJECT" up -d postgres valkey
docker compose --env-file "$ENV_FILE" -f "$S_COMPOSE" -p "$S_PROJECT" run --rm --no-deps s-migrate
docker compose --env-file "$ENV_FILE" -f "$E_COMPOSE" -p "$E_PROJECT" up -d e-server
docker compose --env-file "$ENV_FILE" -f "$S_COMPOSE" -p "$S_PROJECT" up -d s-control webui
wait_for() {
label=$1; url=$2; attempt=1
while [ "$attempt" -le 60 ]; do
curl -fsS --max-time 5 "$url" >/dev/null 2>&1 && return 0
sleep 2; attempt=$((attempt + 1))
done
die "$label 健康检查失败:$url"
}
wait_for "S live" http://127.0.0.1:18080/health/live
wait_for "S ready" http://127.0.0.1:18080/health/ready
wait_for "E live" http://127.0.0.1:8096/health/live
wait_for "WebUI" http://127.0.0.1:18083/
wait_for "Setup" http://127.0.0.1:18083/setup
echo "OverCooked 客户部署已通过容器健康检查。"
echo "WebUI: http://$SERVER_IP:18083/"
echo "API: http://$SERVER_IP:18080/"
echo "E: http://$SERVER_IP:8096/"
echo "首次初始化: http://$SERVER_IP:18083/setup"
echo "Setup Token 已保存于 $ENV_FILE(不会在终端显示,请限制文件访问权限)。"
echo "镜像来源:$REGISTRY(仅公开 E、S Control、S WebUI、Media Node;不包含 Auth 镜像)"