Files
sub2api-cn-relay-manager/scripts/deploy/deploy_tksea_portal.sh
phamnazage-jpg 47ced19c7b fix(deploy): production CRM deployment improvements
- Fix deploy_crm_only.sh: non-destructive hot reload
  - Enhanced stop logic with pgrep + fuser for port release
  - Added 3-layer verification (process/control/user)
  - Check /proc/$pid/exe for (deleted) marker
  - Never delete DB

- Fix portal script contracts: crm_session → crm_subject
  - deploy_tksea_portal.sh: use $cookie_crm_subject
  - test_tksea_portal_assets.sh: assert crm_subject exists
  - nginx.example.conf: updated trusted subject header

- Add systemd service management
  - sub2api-crm.service.template
  - install_crm_systemd.sh
  - verify_crm_deployment.sh

Update docs/plans/2026-06-04-next-version-plan.md with deployment findings.
2026-06-10 15:44:45 +08:00

213 lines
7.2 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.
#!/usr/bin/env bash
set -euo pipefail
ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)"
DEPLOY_ENV_FILE="${DEPLOY_ENV_FILE:-$ROOT_DIR/scripts/deploy/.env.deploy}"
if [[ -f "$DEPLOY_ENV_FILE" ]]; then
set -a
# shellcheck disable=SC1090
source "$DEPLOY_ENV_FILE"
set +a
fi
KEY="${KEY:-}"
REMOTE="${REMOTE:-}"
REMOTE_PORTAL_DIR="${REMOTE_PORTAL_DIR:-/var/www/sub2api-portal}"
REMOTE_NGINX_SITE="${REMOTE_NGINX_SITE:-/etc/nginx/sites-available/tksea}"
REMOTE_HOST_PORT="${REMOTE_HOST_PORT:-8080}"
REMOTE_CRM_PORT="${REMOTE_CRM_PORT:-}"
LOCAL_PORTAL_DIR="${LOCAL_PORTAL_DIR:-$ROOT_DIR/deploy/tksea-portal}"
REMOTE_STAGE_DIR="${REMOTE_STAGE_DIR:-/tmp/sub2api-portal-deploy}"
DRY_RUN="${DRY_RUN:-0}"
die() {
echo "$*" >&2
exit 1
}
require_cmd() {
command -v "$1" >/dev/null 2>&1 || die "missing command: $1"
}
run_cmd() {
if [[ "$DRY_RUN" == "1" ]]; then
printf 'DRY_RUN:'
printf ' %q' "$@"
printf '\n'
return 0
fi
"$@"
}
ssh_remote() {
run_cmd ssh -i "$KEY" -o StrictHostKeyChecking=no "$REMOTE" "$@"
}
scp_remote() {
run_cmd scp -i "$KEY" -o StrictHostKeyChecking=no "$@"
}
main() {
require_cmd python3
require_cmd ssh
require_cmd scp
[[ -d "$LOCAL_PORTAL_DIR" ]] || die "missing portal dir: $LOCAL_PORTAL_DIR"
[[ -f "$LOCAL_PORTAL_DIR/index.html" ]] || die "missing portal index: $LOCAL_PORTAL_DIR/index.html"
[[ -n "$KEY" ]] || die "KEY is required; copy scripts/deploy/.env.deploy.example to scripts/deploy/.env.deploy and fill it"
[[ -n "$REMOTE" ]] || die "REMOTE is required; copy scripts/deploy/.env.deploy.example to scripts/deploy/.env.deploy and fill it"
[[ -n "$REMOTE_CRM_PORT" ]] || die "REMOTE_CRM_PORT is required; copy scripts/deploy/.env.deploy.example to scripts/deploy/.env.deploy and fill it"
if [[ "$DRY_RUN" != "1" ]]; then
[[ -f "$KEY" ]] || die "missing ssh key: $KEY"
fi
local tmpdir patch_file portal_stage_dir
tmpdir="$(mktemp -d)"
trap "rm -rf $(printf '%q' "$tmpdir")" EXIT
patch_file="$tmpdir/patch_tksea_portal_nginx.py"
portal_stage_dir="$tmpdir/portal"
mkdir -p "$portal_stage_dir"
cp -R "$LOCAL_PORTAL_DIR/." "$portal_stage_dir/"
cat > "$patch_file" <<EOF
from pathlib import Path
import re
import textwrap
path = Path(${REMOTE_NGINX_SITE@Q})
text = path.read_text()
block = textwrap.dedent("""\
location = /portal {
return 302 /portal/;
}
location = /portal/admin {
return 302 /portal/admin/;
}
location = /kimi-portal {
return 302 /portal/;
}
# BEGIN sub2api-portal
location /portal/ {
alias ${REMOTE_PORTAL_DIR}/;
index index.html;
try_files \$uri \$uri/ /portal/index.html;
}
location /portal-proxy/ {
proxy_pass http://127.0.0.1:${REMOTE_HOST_PORT}/;
proxy_set_header Host \$host;
proxy_set_header X-Real-IP \$remote_addr;
proxy_set_header X-Forwarded-For \$proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto \$scheme;
proxy_http_version 1.1;
}
location /portal-admin-api/ {
# 必须由受信登录/鉴权层把用户签名放进 $cookie_crm_subject不能信任浏览器自带 header。
# 这是 CRM 配置 TRUSTED_SUBJECT_COOKIE=crm_subject 对应的 cookie 名。
# 同时 CRM 需配置:
# SUB2API_CRM_TRUSTED_SUBJECT_HEADER=X-CRM-Authenticated-Subject
# SUB2API_CRM_TRUSTED_PROXY_SECRET_HEADER=X-CRM-Trusted-Proxy
# SUB2API_CRM_TRUSTED_PROXY_SECRET=<same-secret-as-nginx>
proxy_pass http://127.0.0.1:${REMOTE_CRM_PORT}/;
proxy_set_header Host \$host;
proxy_set_header X-Real-IP \$remote_addr;
proxy_set_header X-Forwarded-For \$proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto \$scheme;
proxy_set_header X-CRM-Authenticated-Subject \$cookie_crm_subject;
proxy_set_header X-CRM-Trusted-Proxy "REPLACE_WITH_SUB2API_CRM_TRUSTED_PROXY_SECRET";
proxy_http_version 1.1;
}
location = /v1/chat/completions {
proxy_pass http://127.0.0.1:${REMOTE_CRM_PORT}/v1/chat/completions;
proxy_set_header Host \$host;
proxy_set_header X-Real-IP \$remote_addr;
proxy_set_header X-Forwarded-For \$proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto \$scheme;
proxy_http_version 1.1;
}
location /kimi-portal/ {
return 302 /portal/;
}
location /kimi-portal-proxy/ {
proxy_pass http://127.0.0.1:${REMOTE_HOST_PORT}/;
proxy_set_header Host \$host;
proxy_set_header X-Real-IP \$remote_addr;
proxy_set_header X-Forwarded-For \$proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto \$scheme;
proxy_http_version 1.1;
}
location /kimi/ {
proxy_pass http://127.0.0.1:${REMOTE_HOST_PORT}/;
proxy_set_header Host \$host;
proxy_set_header X-Real-IP \$remote_addr;
proxy_set_header X-Forwarded-For \$proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto \$scheme;
proxy_http_version 1.1;
}
location /kimi-v1/ {
proxy_pass http://127.0.0.1:${REMOTE_HOST_PORT}/v1/;
proxy_set_header Host \$host;
proxy_set_header X-Real-IP \$remote_addr;
proxy_set_header X-Forwarded-For \$proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto \$scheme;
proxy_http_version 1.1;
}
# END sub2api-portal
""")
patterns = [
re.compile(r"\\n\\s*location = /portal \\{.*?# END sub2api-portal\\n\\n", re.S),
re.compile(r"\\n\\s*location = /kimi-portal \\{.*?# END kimi-portal\\n\\n", re.S),
]
for pattern in patterns:
if pattern.search(text):
text = pattern.sub("\\n" + block + "\\n", text, count=1)
path.write_text(text)
raise SystemExit(0)
needle = "\\n location / {\\n proxy_pass http://127.0.0.1:"
index = text.rfind(needle)
if index == -1:
raise SystemExit("failed to locate sub.tksea.top root location block")
text = text[:index] + "\\n" + block + text[index:]
path.write_text(text)
EOF
ssh_remote "mkdir -p $(printf '%q' "$REMOTE_STAGE_DIR")"
scp_remote -r "$portal_stage_dir" "$REMOTE:$REMOTE_STAGE_DIR/"
scp_remote "$patch_file" "$REMOTE:$REMOTE_STAGE_DIR/patch_tksea_portal_nginx.py"
ssh_remote "sudo install -d -m 755 $(printf '%q' "$REMOTE_PORTAL_DIR") && sudo cp -R $(printf '%q' "$REMOTE_STAGE_DIR/portal/.") $(printf '%q' "$REMOTE_PORTAL_DIR/") && sudo python3 $(printf '%q' "$REMOTE_STAGE_DIR/patch_tksea_portal_nginx.py") && sudo nginx -t && sudo systemctl reload nginx"
cat <<EOF
tksea portal deployed
remote: ${REMOTE}
portal url: https://sub.tksea.top/portal/
portal admin home url: https://sub.tksea.top/portal/admin/
logical groups admin url: https://sub.tksea.top/portal/admin/logical-groups.html
route health admin url: https://sub.tksea.top/portal/admin/route-health.html
accounts admin url: https://sub.tksea.top/portal/admin/accounts.html
provider admin url: https://sub.tksea.top/portal/admin/providers.html
batch import admin url: https://sub.tksea.top/portal/admin/batch-import.html
batch import admin url: https://sub.tksea.top/portal/admin-batch-import.html
legacy url: https://sub.tksea.top/kimi-portal/
portal dir: ${REMOTE_PORTAL_DIR}
nginx site: ${REMOTE_NGINX_SITE}
EOF
}
main "$@"