#!/bin/bash set -euo pipefail REPO_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" TMP_DIR="$(mktemp -d)" trap 'rm -rf "$TMP_DIR"' EXIT cp \ "$REPO_ROOT/start.sh" \ "$REPO_ROOT/https.sh" \ "$REPO_ROOT/docker-compose.yml" \ "$REPO_ROOT/.env.sample" \ "$REPO_ROOT/.env.tools.sample" \ "$REPO_ROOT/.env.ASG.sample" \ "$REPO_ROOT/.env.AMS.sample" \ "$REPO_ROOT/easyai-proxy.conf.sample" \ "$TMP_DIR/" mkdir -p "$TMP_DIR/scripts" cp \ "$REPO_ROOT/scripts/init-security-env.sh" \ "$REPO_ROOT/scripts/init-public-api-base-url.sh" \ "$REPO_ROOT/scripts/init-server-http-bind-ip.sh" \ "$TMP_DIR/scripts/" cd "$TMP_DIR" reset_case() { rm -f \ .env \ .env.tools \ .env.ASG \ .env.AMS \ easyai-proxy.conf \ demo.example.com.conf } assert_compression_config() { local config_file="$1" grep -q '^ gzip on;' "$config_file" grep -q '^ gzip_vary on;' "$config_file" grep -q '^ gzip_min_length 1024;' "$config_file" grep -q '^ application/javascript$' "$config_file" grep -q '^ text/css$' "$config_file" grep -q '^ text/javascript$' "$config_file" if grep -q 'text/event-stream' "$config_file"; then echo "Unexpected SSE compression type in $config_file" >&2 return 1 fi } assert_sandbox_public_access_disabled() { local config_file="$1" grep -q '^ location = /api/sandbox {' "$config_file" grep -q '^ location \^~ /api/sandbox/ {' "$config_file" grep -q '^ location = /jupyterlab {' "$config_file" grep -q '^ location \^~ /jupyterlab/ {' "$config_file" grep -q '^ location = /sandbox {' "$config_file" grep -q '^ location \^~ /sandbox/ {' "$config_file" if grep -Eq 'proxy_pass http://127\.0\.0\.1:(8081|8888)' "$config_file"; then echo "Unexpected public Sandbox/Jupyter proxy in $config_file" >&2 return 1 fi } assert_compose_exposure() { local expected_server_host_ip="$1" docker compose config --format json | python3 -c ' import json, sys expected = sys.argv[1] config = json.load(sys.stdin) sandbox_ports = config["services"]["sandbox"].get("ports") or [] assert sandbox_ports == [], f"sandbox ports published: {sandbox_ports}" server_ports = config["services"]["easyai-server"].get("ports") or [] assert len(server_ports) == 1, server_ports assert server_ports[0].get("host_ip") == expected, server_ports ' "$expected_server_host_ip" } assert_canvas_ws_auth_config() { docker compose config --format json | python3 -c ' import json, sys config = json.load(sys.stdin) server_env = config["services"]["easyai-server"].get("environment") or {} gateway_env = config["services"]["ws-gateway"].get("environment") or {} server_secret = str(server_env.get("WS_AUTH_WS_TICKET_SECRET") or "") gateway_secret = str(gateway_env.get("WS_AUTH_WS_TICKET_SECRET") or "") assert len(server_secret.encode()) >= 32, "server-main WS ticket secret is missing or too short" assert server_secret == gateway_secret, "server-main and ws-gateway WS ticket secrets differ" methods = {item.strip() for item in str(gateway_env.get("WS_AUTH_METHODS") or "").split(",") if item.strip()} assert "ws_ticket" in methods, f"ws-gateway does not advertise ws_ticket: {sorted(methods)}" ' } sed -i.bak 's/^SERVER_HTTP_PORT=.*/SERVER_HTTP_PORT=4100/' .env.sample rm -f .env.sample.bak DEPLOY_NON_INTERACTIVE=1 \ DEPLOY_DRY_RUN=1 \ DEPLOY_ACCESS=ip \ DEPLOY_IP=10.0.0.8 \ bash start.sh > "$TMP_DIR/first-install.log" grep -qx 'NUXT_PUBLIC_BASE_APIURL=http://10.0.0.8:4100' .env grep -qx 'CONFIG_PUBLIC_API_BASE_URL=http://10.0.0.8:4100' .env grep -qx 'SERVER_HTTP_BIND_IP=0.0.0.0' .env initial_admin_password="$(awk -F= '$1 == "CONFIG_INITIAL_ADMIN_PASSWORD" { print $2; exit }' .env)" [ "${#initial_admin_password}" -ge 12 ] || { echo "Initial admin password was not generated" >&2 exit 1 } grep -Fq '登录账号: admin' "$TMP_DIR/first-install.log" grep -Fq "登录密码: ${initial_admin_password}" "$TMP_DIR/first-install.log" grep -Fq '生命周期: 仅在数据库首次创建 admin 时使用;创建后修改 .env 不会重置密码。' "$TMP_DIR/first-install.log" grep -Fq '有效期: 初始密码没有独立过期时间,在管理员修改密码前持续有效。' "$TMP_DIR/first-install.log" DEPLOY_NON_INTERACTIVE=1 \ DEPLOY_DRY_RUN=1 \ DEPLOY_ACCESS=ip \ DEPLOY_IP=10.0.0.8 \ bash start.sh > "$TMP_DIR/existing-env.log" grep -Fq '管理员账号: admin(本次部署不会生成或重置已有密码)' "$TMP_DIR/existing-env.log" if grep -Fq "登录密码: ${initial_admin_password}" "$TMP_DIR/existing-env.log"; then echo "Existing-env deployment must not report CONFIG_INITIAL_ADMIN_PASSWORD as the current login password" >&2 exit 1 fi if command -v docker >/dev/null 2>&1 && docker compose version >/dev/null 2>&1; then assert_compose_exposure 0.0.0.0 assert_canvas_ws_auth_config fi reset_case DEPLOY_NON_INTERACTIVE=1 \ DEPLOY_DRY_RUN=1 \ DEPLOY_ACCESS=domain \ DEPLOY_DOMAIN=demo.example.com \ DEPLOY_HTTPS=false \ bash start.sh >/dev/null grep -qx 'NUXT_PUBLIC_BASE_SOCKETURL=ws://demo.example.com/socket.io' .env grep -qx 'CONFIG_PUBLIC_API_BASE_URL=http://demo.example.com/api' .env grep -q 'proxy_set_header X-Forwarded-Host $easyai_forwarded_host;' demo.example.com.conf grep -q 'proxy_set_header X-Forwarded-Port $server_port;' demo.example.com.conf grep -q "proxy_set_header X-Original-Prefix '/api';" demo.example.com.conf grep -q 'location = /api {' demo.example.com.conf assert_compression_config demo.example.com.conf assert_sandbox_public_access_disabled demo.example.com.conf grep -qx 'SERVER_HTTP_BIND_IP=127.0.0.1' .env if command -v docker >/dev/null 2>&1 && docker compose version >/dev/null 2>&1; then assert_compose_exposure 127.0.0.1 fi # 直接验证 https.sh 的缺省配置生成函数会采用完整模板,而不是只代理 3010。 eval "$(awk ' /^create_conf_by_template\(\)/ { capture = 1 } /^conf_contains_domain\(\)/ { capture = 0 } capture { print } ' https.sh)" create_conf_by_template demo.example.com easyai-proxy.conf grep -q 'server_name demo.example.com;' easyai-proxy.conf grep -q 'location = /api {' easyai-proxy.conf grep -q 'location /api/ {' easyai-proxy.conf grep -q 'proxy_pass http://127.0.0.1:3001/;' easyai-proxy.conf grep -q 'location /socket.io {' easyai-proxy.conf grep -q 'proxy_set_header X-Forwarded-Host $easyai_forwarded_host;' easyai-proxy.conf assert_compression_config easyai-proxy.conf assert_sandbox_public_access_disabled easyai-proxy.conf reset_case DEPLOY_NON_INTERACTIVE=1 \ DEPLOY_DRY_RUN=1 \ DEPLOY_ACCESS=domain \ DEPLOY_DOMAIN=demo.example.com \ DEPLOY_HTTPS=true \ bash start.sh >/dev/null grep -qx 'NUXT_PUBLIC_BASE_SOCKETURL=wss://demo.example.com/socket.io' .env grep -qx 'CONFIG_PUBLIC_API_BASE_URL=https://demo.example.com/api' .env reset_case DEPLOY_NON_INTERACTIVE=1 \ DEPLOY_DRY_RUN=1 \ DEPLOY_ACCESS=ip \ DEPLOY_IP=10.0.0.8 \ DEPLOY_PUBLIC_API_BASE_URL=https://edge.example.com:8443/custom-api \ bash start.sh >/dev/null grep -qx 'CONFIG_PUBLIC_API_BASE_URL=https://edge.example.com:8443/custom-api' .env echo "Deployment public URL dry-run tests passed"