#!/usr/bin/env bash set -euo pipefail SCRIPT_DIR="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" >/dev/null 2>&1 && pwd)" REPO_ROOT="$(cd -- "${SCRIPT_DIR}/../.." >/dev/null 2>&1 && pwd)" SERVICE_NAME="${GENARRATIVE_PINGORA_GATEWAY_SERVICE:-genarrative-pingora-gateway.service}" DEFAULT_SERVICE_UNIT_PATH="${REPO_ROOT}/deploy/systemd/genarrative-pingora-gateway.service" SERVICE_UNIT_PATH="${GENARRATIVE_PINGORA_DIRECT_SERVICE_UNIT_PATH:-${DEFAULT_SERVICE_UNIT_PATH}}" DEFAULT_TEMPLATE_PATH="${REPO_ROOT}/deploy/systemd/genarrative-pingora-gateway-direct-entry.conf" TEMPLATE_PATH="${GENARRATIVE_PINGORA_DIRECT_TEMPLATE_PATH:-${DEFAULT_TEMPLATE_PATH}}" DROPIN_PATH="${GENARRATIVE_PINGORA_DIRECT_DROPIN_PATH:-/etc/systemd/system/genarrative-pingora-gateway.service.d/direct-entry.conf}" PREFLIGHT_SCRIPT="${GENARRATIVE_PINGORA_DIRECT_PREFLIGHT_SCRIPT:-${REPO_ROOT}/scripts/check-pingora-direct-preflight.mjs}" PREFLIGHT_ENV_FILE="${GENARRATIVE_PINGORA_DIRECT_PREFLIGHT_ENV_FILE:-}" CURRENT_RELEASE_AUDIT_SCRIPT="${GENARRATIVE_PINGORA_DIRECT_CURRENT_RELEASE_AUDIT_SCRIPT:-${REPO_ROOT}/scripts/ops/pingora-current-release-audit.mjs}" CURRENT_RELEASE_ROOT="${GENARRATIVE_PINGORA_DIRECT_CURRENT_RELEASE_ROOT:-${REPO_ROOT}}" CURRENT_RELEASE_AUDIT_TIMEOUT_MS="${GENARRATIVE_PINGORA_DIRECT_CURRENT_RELEASE_AUDIT_TIMEOUT_MS:-}" PREFLIGHT_SYSTEMD="false" PREFLIGHT_CHECK_CERT_READABLE="false" PREFLIGHT_CHECK_SERVICE_ENV_FILE="false" PREFLIGHT_CHECK_SERVICE_USER_CERT_READABLE="false" PREFLIGHT_CHECK_SERVICE_BINARY_EXECUTABLE="false" PREFLIGHT_CHECK_PORTS_FREE="false" DIRECT_LIVE_SCRIPT="${GENARRATIVE_PINGORA_DIRECT_LIVE_SCRIPT:-${REPO_ROOT}/scripts/check-pingora-direct-live.mjs}" DIRECT_HTTPS_BASE_URL="${GENARRATIVE_PINGORA_DIRECT_HTTPS_BASE_URL:-}" DIRECT_HTTP_BASE_URL="${GENARRATIVE_PINGORA_DIRECT_HTTP_BASE_URL:-}" DIRECT_HOST="${GENARRATIVE_PINGORA_DIRECT_HOST:-}" DIRECT_REDIRECT_HOST="${GENARRATIVE_PINGORA_DIRECT_REDIRECT_HOST:-}" DIRECT_REDIRECT_BASE_URL="${GENARRATIVE_PINGORA_DIRECT_REDIRECT_BASE_URL:-}" DIRECT_PROBE_TOKEN="${GENARRATIVE_PINGORA_DIRECT_PROBE_TOKEN:-}" DIRECT_PINGORA_ACCESS_LOG="${GENARRATIVE_PINGORA_DIRECT_PINGORA_ACCESS_LOG:-}" DIRECT_ACCESS_LOG_SINCE_LINES="${GENARRATIVE_PINGORA_DIRECT_ACCESS_LOG_SINCE_LINES:-}" DIRECT_SPACETIME_DATABASE="${GENARRATIVE_PINGORA_DIRECT_SPACETIME_DATABASE:-}" DIRECT_TIMEOUT_MS="${GENARRATIVE_PINGORA_DIRECT_TIMEOUT_MS:-}" APPLY="false" STATUS_AFTER="true" VERIFY_SYSTEMD_AFTER="true" usage() { cat <<'EOF' 用法: scripts/deploy/pingora-direct-enable.sh [--apply] [--preflight-env-file ] [--preflight-systemd] [--preflight-check-cert-readable] [--preflight-check-service-env-file] [--preflight-check-service-user-cert-readable] [--preflight-check-service-binary-executable] [--preflight-check-ports-free] [--direct-https-base-url ] [--direct-http-base-url ] [--direct-host ] [--direct-redirect-host ] [--direct-redirect-base-url ] [--direct-spacetime-database ] [--direct-probe-token ] [--direct-pingora-access-log ] [--direct-access-log-since-lines ] [--direct-timeout-ms ] [--direct-live-script ] [--no-postcheck] [--current-release-audit-script ] [--current-release-root ] [--current-release-audit-timeout-ms ] [--service ] [--service-unit-path ] [--template-path ] [--dropin-path ] 说明: 启用 Pingora 直连低端口入口:把已准备好的 direct-entry systemd drop-in 模板安装到 genarrative-pingora-gateway.service.d/direct-entry.conf,执行 daemon-reload, 重启 genarrative-pingora-gateway.service,让服务具备绑定 80/443 的最小 capability。 默认是 dry-run,只打印将执行的命令;必须显式传 --apply 才会修改系统状态。 该脚本不写入 TLS env、不复制证书、不停止 Nginx;--apply 会先执行 current release 自包含自审, 确认发布包已携带 Pingora 网关且 systemd ExecStart 指向随包二进制;--apply 时必须提供 --preflight-env-file、 --preflight-check-cert-readable、--preflight-check-service-env-file、--preflight-check-service-user-cert-readable、--preflight-check-service-binary-executable、--preflight-check-ports-free 和 direct live smoke 参数, 先通过 direct preflight,安装并重启 Pingora,再通过 direct live smoke 后才视为启用成功。 EOF } while [[ $# -gt 0 ]]; do case "$1" in -h|--help) usage exit 0 ;; --apply) APPLY="true" shift ;; --no-status) STATUS_AFTER="false" shift ;; --no-postcheck) VERIFY_SYSTEMD_AFTER="false" shift ;; --service) SERVICE_NAME="${2:-}" if [[ -z "${SERVICE_NAME}" ]]; then echo "[pingora-direct-enable] --service 缺少参数" >&2 exit 1 fi shift 2 ;; --service-unit-path) SERVICE_UNIT_PATH="${2:-}" if [[ -z "${SERVICE_UNIT_PATH}" ]]; then echo "[pingora-direct-enable] --service-unit-path 缺少参数" >&2 exit 1 fi shift 2 ;; --template-path) TEMPLATE_PATH="${2:-}" if [[ -z "${TEMPLATE_PATH}" ]]; then echo "[pingora-direct-enable] --template-path 缺少参数" >&2 exit 1 fi shift 2 ;; --dropin-path) DROPIN_PATH="${2:-}" if [[ -z "${DROPIN_PATH}" ]]; then echo "[pingora-direct-enable] --dropin-path 缺少参数" >&2 exit 1 fi shift 2 ;; --preflight-script) PREFLIGHT_SCRIPT="${2:-}" if [[ -z "${PREFLIGHT_SCRIPT}" ]]; then echo "[pingora-direct-enable] --preflight-script 缺少参数" >&2 exit 1 fi shift 2 ;; --preflight-env-file) PREFLIGHT_ENV_FILE="${2:-}" if [[ -z "${PREFLIGHT_ENV_FILE}" ]]; then echo "[pingora-direct-enable] --preflight-env-file 缺少参数" >&2 exit 1 fi shift 2 ;; --current-release-audit-script) CURRENT_RELEASE_AUDIT_SCRIPT="${2:-}" if [[ -z "${CURRENT_RELEASE_AUDIT_SCRIPT}" ]]; then echo "[pingora-direct-enable] --current-release-audit-script 缺少参数" >&2 exit 1 fi shift 2 ;; --current-release-root) CURRENT_RELEASE_ROOT="${2:-}" if [[ -z "${CURRENT_RELEASE_ROOT}" ]]; then echo "[pingora-direct-enable] --current-release-root 缺少参数" >&2 exit 1 fi shift 2 ;; --current-release-audit-timeout-ms) CURRENT_RELEASE_AUDIT_TIMEOUT_MS="${2:-}" if [[ -z "${CURRENT_RELEASE_AUDIT_TIMEOUT_MS}" ]]; then echo "[pingora-direct-enable] --current-release-audit-timeout-ms 缺少参数" >&2 exit 1 fi shift 2 ;; --preflight-systemd) PREFLIGHT_SYSTEMD="true" shift ;; --preflight-check-cert-readable) PREFLIGHT_CHECK_CERT_READABLE="true" shift ;; --preflight-check-service-env-file) PREFLIGHT_CHECK_SERVICE_ENV_FILE="true" shift ;; --preflight-check-service-user-cert-readable) PREFLIGHT_CHECK_SERVICE_USER_CERT_READABLE="true" shift ;; --preflight-check-service-binary-executable) PREFLIGHT_CHECK_SERVICE_BINARY_EXECUTABLE="true" shift ;; --preflight-check-ports-free) PREFLIGHT_CHECK_PORTS_FREE="true" shift ;; --direct-live-script) DIRECT_LIVE_SCRIPT="${2:-}" if [[ -z "${DIRECT_LIVE_SCRIPT}" ]]; then echo "[pingora-direct-enable] --direct-live-script 缺少参数" >&2 exit 1 fi shift 2 ;; --direct-https-base-url) DIRECT_HTTPS_BASE_URL="${2:-}" if [[ -z "${DIRECT_HTTPS_BASE_URL}" ]]; then echo "[pingora-direct-enable] --direct-https-base-url 缺少参数" >&2 exit 1 fi shift 2 ;; --direct-http-base-url) DIRECT_HTTP_BASE_URL="${2:-}" if [[ -z "${DIRECT_HTTP_BASE_URL}" ]]; then echo "[pingora-direct-enable] --direct-http-base-url 缺少参数" >&2 exit 1 fi shift 2 ;; --direct-host) DIRECT_HOST="${2:-}" if [[ -z "${DIRECT_HOST}" ]]; then echo "[pingora-direct-enable] --direct-host 缺少参数" >&2 exit 1 fi shift 2 ;; --direct-redirect-host) DIRECT_REDIRECT_HOST="${2:-}" if [[ -z "${DIRECT_REDIRECT_HOST}" ]]; then echo "[pingora-direct-enable] --direct-redirect-host 缺少参数" >&2 exit 1 fi shift 2 ;; --direct-redirect-base-url) DIRECT_REDIRECT_BASE_URL="${2:-}" if [[ -z "${DIRECT_REDIRECT_BASE_URL}" ]]; then echo "[pingora-direct-enable] --direct-redirect-base-url 缺少参数" >&2 exit 1 fi shift 2 ;; --direct-probe-token) DIRECT_PROBE_TOKEN="${2:-}" if [[ -z "${DIRECT_PROBE_TOKEN}" ]]; then echo "[pingora-direct-enable] --direct-probe-token 缺少参数" >&2 exit 1 fi shift 2 ;; --direct-pingora-access-log) DIRECT_PINGORA_ACCESS_LOG="${2:-}" if [[ -z "${DIRECT_PINGORA_ACCESS_LOG}" ]]; then echo "[pingora-direct-enable] --direct-pingora-access-log 缺少参数" >&2 exit 1 fi shift 2 ;; --direct-access-log-since-lines) DIRECT_ACCESS_LOG_SINCE_LINES="${2:-}" if [[ -z "${DIRECT_ACCESS_LOG_SINCE_LINES}" ]]; then echo "[pingora-direct-enable] --direct-access-log-since-lines 缺少参数" >&2 exit 1 fi shift 2 ;; --direct-spacetime-database) DIRECT_SPACETIME_DATABASE="${2:-}" if [[ -z "${DIRECT_SPACETIME_DATABASE}" ]]; then echo "[pingora-direct-enable] --direct-spacetime-database 缺少参数" >&2 exit 1 fi shift 2 ;; --direct-timeout-ms) DIRECT_TIMEOUT_MS="${2:-}" if [[ -z "${DIRECT_TIMEOUT_MS}" ]]; then echo "[pingora-direct-enable] --direct-timeout-ms 缺少参数" >&2 exit 1 fi shift 2 ;; *) echo "[pingora-direct-enable] 未知参数: $1" >&2 usage >&2 exit 1 ;; esac done reject_control_characters() { local label="$1" local value="$2" if [[ "${value}" == *$'\n'* || "${value}" == *$'\r'* ]]; then echo "[pingora-direct-enable] ${label} 不能包含换行或 NUL 字符。" >&2 exit 1 fi } is_filesystem_root_path() { local value="$1" local without_slashes="${value//\//}" [[ -n "${value}" && -z "${without_slashes}" ]] } reject_filesystem_root_path() { local label="$1" local value="$2" if is_filesystem_root_path "${value}"; then echo "[pingora-direct-enable] ${label} 不能是文件系统根目录。" >&2 exit 1 fi } reject_control_characters "--service" "${SERVICE_NAME}" reject_control_characters "--service-unit-path" "${SERVICE_UNIT_PATH}" reject_control_characters "--template-path" "${TEMPLATE_PATH}" reject_control_characters "--dropin-path" "${DROPIN_PATH}" reject_control_characters "--preflight-script" "${PREFLIGHT_SCRIPT}" reject_control_characters "--preflight-env-file" "${PREFLIGHT_ENV_FILE}" reject_control_characters "--current-release-audit-script" "${CURRENT_RELEASE_AUDIT_SCRIPT}" reject_control_characters "--current-release-root" "${CURRENT_RELEASE_ROOT}" reject_control_characters "--current-release-audit-timeout-ms" "${CURRENT_RELEASE_AUDIT_TIMEOUT_MS}" reject_control_characters "--direct-live-script" "${DIRECT_LIVE_SCRIPT}" reject_control_characters "--direct-https-base-url" "${DIRECT_HTTPS_BASE_URL}" reject_control_characters "--direct-http-base-url" "${DIRECT_HTTP_BASE_URL}" reject_control_characters "--direct-host" "${DIRECT_HOST}" reject_control_characters "--direct-redirect-host" "${DIRECT_REDIRECT_HOST}" reject_control_characters "--direct-redirect-base-url" "${DIRECT_REDIRECT_BASE_URL}" reject_control_characters "--direct-probe-token" "${DIRECT_PROBE_TOKEN}" reject_control_characters "--direct-pingora-access-log" "${DIRECT_PINGORA_ACCESS_LOG}" reject_control_characters "--direct-access-log-since-lines" "${DIRECT_ACCESS_LOG_SINCE_LINES}" reject_control_characters "--direct-spacetime-database" "${DIRECT_SPACETIME_DATABASE}" reject_control_characters "--direct-timeout-ms" "${DIRECT_TIMEOUT_MS}" reject_filesystem_root_path "--service-unit-path" "${SERVICE_UNIT_PATH}" reject_filesystem_root_path "--template-path" "${TEMPLATE_PATH}" reject_filesystem_root_path "--dropin-path" "${DROPIN_PATH}" reject_filesystem_root_path "--preflight-env-file" "${PREFLIGHT_ENV_FILE}" reject_filesystem_root_path "--preflight-script" "${PREFLIGHT_SCRIPT}" reject_filesystem_root_path "--current-release-audit-script" "${CURRENT_RELEASE_AUDIT_SCRIPT}" reject_filesystem_root_path "--current-release-root" "${CURRENT_RELEASE_ROOT}" reject_filesystem_root_path "--direct-live-script" "${DIRECT_LIVE_SCRIPT}" reject_filesystem_root_path "--direct-pingora-access-log" "${DIRECT_PINGORA_ACCESS_LOG}" if [[ "${TEMPLATE_PATH}" != /* ]]; then echo "[pingora-direct-enable] --template-path 必须是绝对路径: ${TEMPLATE_PATH}" >&2 exit 1 fi if [[ "${SERVICE_UNIT_PATH}" != /* ]]; then echo "[pingora-direct-enable] --service-unit-path 必须是绝对路径: ${SERVICE_UNIT_PATH}" >&2 exit 1 fi if [[ "${DROPIN_PATH}" != /* ]]; then echo "[pingora-direct-enable] --dropin-path 必须是绝对路径: ${DROPIN_PATH}" >&2 exit 1 fi if [[ -n "${PREFLIGHT_ENV_FILE}" && "${PREFLIGHT_ENV_FILE}" != /* ]]; then echo "[pingora-direct-enable] --preflight-env-file 必须是绝对路径: ${PREFLIGHT_ENV_FILE}" >&2 exit 1 fi if [[ "${PREFLIGHT_SCRIPT}" != /* ]]; then echo "[pingora-direct-enable] --preflight-script 必须是绝对路径: ${PREFLIGHT_SCRIPT}" >&2 exit 1 fi if [[ "${CURRENT_RELEASE_AUDIT_SCRIPT}" != /* ]]; then echo "[pingora-direct-enable] --current-release-audit-script 必须是绝对路径: ${CURRENT_RELEASE_AUDIT_SCRIPT}" >&2 exit 1 fi if [[ "${CURRENT_RELEASE_ROOT}" != /* ]]; then echo "[pingora-direct-enable] --current-release-root 必须是绝对路径: ${CURRENT_RELEASE_ROOT}" >&2 exit 1 fi if [[ "${DIRECT_LIVE_SCRIPT}" != /* ]]; then echo "[pingora-direct-enable] --direct-live-script 必须是绝对路径: ${DIRECT_LIVE_SCRIPT}" >&2 exit 1 fi if [[ "${APPLY}" == "true" && -z "${PREFLIGHT_ENV_FILE}" ]]; then echo "[pingora-direct-enable] --apply 必须同时提供 --preflight-env-file,避免跳过直连 env / 证书预检。" >&2 exit 1 fi if [[ "${APPLY}" == "true" && "${PREFLIGHT_CHECK_CERT_READABLE}" != "true" ]]; then echo "[pingora-direct-enable] --apply 必须同时提供 --preflight-check-cert-readable,避免跳过 TLS 证书和私钥可读预检。" >&2 exit 1 fi if [[ "${APPLY}" == "true" && "${PREFLIGHT_CHECK_SERVICE_ENV_FILE}" != "true" ]]; then echo "[pingora-direct-enable] --apply 必须同时提供 --preflight-check-service-env-file,避免检查的 env 文件和 systemd service 实际读取的 EnvironmentFile 不一致。" >&2 exit 1 fi if [[ "${APPLY}" == "true" && "${PREFLIGHT_CHECK_SERVICE_USER_CERT_READABLE}" != "true" ]]; then echo "[pingora-direct-enable] --apply 必须同时提供 --preflight-check-service-user-cert-readable,避免跳过 systemd 服务用户读取 TLS 证书和私钥预检。" >&2 exit 1 fi if [[ "${APPLY}" == "true" && "${PREFLIGHT_CHECK_SERVICE_BINARY_EXECUTABLE}" != "true" ]]; then echo "[pingora-direct-enable] --apply 必须同时提供 --preflight-check-service-binary-executable,避免 current release 缺少可执行 pingora-gateway 时才在重启阶段失败。" >&2 exit 1 fi if [[ "${APPLY}" == "true" && "${PREFLIGHT_CHECK_PORTS_FREE}" != "true" ]]; then echo "[pingora-direct-enable] --apply 必须同时提供 --preflight-check-ports-free,避免 Nginx 或其它进程仍占用 80/443。" >&2 exit 1 fi if [[ "${APPLY}" == "true" && -z "${DIRECT_HTTPS_BASE_URL}" ]]; then echo "[pingora-direct-enable] --apply 必须同时提供 --direct-https-base-url,启用后验证 Pingora HTTPS 直连入口。" >&2 exit 1 fi if [[ "${APPLY}" == "true" && -z "${DIRECT_HTTP_BASE_URL}" ]]; then echo "[pingora-direct-enable] --apply 必须同时提供 --direct-http-base-url,启用后验证 HTTP redirect / ACME 入口。" >&2 exit 1 fi if [[ "${APPLY}" == "true" && -z "${DIRECT_HOST}" ]]; then echo "[pingora-direct-enable] --apply 必须同时提供 --direct-host,启用后用正式域名 Host/SNI 验证直连入口。" >&2 exit 1 fi if [[ "${APPLY}" == "true" && -z "${DIRECT_REDIRECT_HOST}" ]]; then echo "[pingora-direct-enable] --apply 必须同时提供 --direct-redirect-host,启用后验证 HTTP redirect Location host。" >&2 exit 1 fi if [[ "${APPLY}" == "true" && -z "${DIRECT_PINGORA_ACCESS_LOG}" ]]; then echo "[pingora-direct-enable] --apply 必须同时提供 --direct-pingora-access-log,启用后验证直连请求已写入 Pingora access log。" >&2 exit 1 fi if [[ -n "${DIRECT_PINGORA_ACCESS_LOG}" && "${DIRECT_PINGORA_ACCESS_LOG}" != /* ]]; then echo "[pingora-direct-enable] --direct-pingora-access-log 必须是绝对路径: ${DIRECT_PINGORA_ACCESS_LOG}" >&2 exit 1 fi if [[ "${APPLY}" == "true" && -z "${DIRECT_SPACETIME_DATABASE}" ]]; then echo "[pingora-direct-enable] --apply 必须同时提供 --direct-spacetime-database,启用后验证目标库 WSS subscribe。" >&2 exit 1 fi if [[ "${APPLY}" == "true" && ! -f "${TEMPLATE_PATH}" ]]; then echo "[pingora-direct-enable] direct-entry 模板不存在: ${TEMPLATE_PATH}" >&2 exit 1 fi if [[ "${APPLY}" == "true" && ! -f "${SERVICE_UNIT_PATH}" ]]; then echo "[pingora-direct-enable] 主 service 模板不存在: ${SERVICE_UNIT_PATH}" >&2 exit 1 fi if [[ "${APPLY}" == "true" && ! -f "${CURRENT_RELEASE_AUDIT_SCRIPT}" ]]; then echo "[pingora-direct-enable] current release 自审脚本不存在: ${CURRENT_RELEASE_AUDIT_SCRIPT}" >&2 exit 1 fi if [[ "${APPLY}" == "true" && ! -f "${PREFLIGHT_SCRIPT}" ]]; then echo "[pingora-direct-enable] direct preflight 脚本不存在: ${PREFLIGHT_SCRIPT}" >&2 exit 1 fi if [[ "${APPLY}" == "true" && ! -f "${DIRECT_LIVE_SCRIPT}" ]]; then echo "[pingora-direct-enable] direct live smoke 脚本不存在: ${DIRECT_LIVE_SCRIPT}" >&2 exit 1 fi if [[ "${APPLY}" == "true" ]]; then DROPIN_DIR="$(dirname "${DROPIN_PATH}")" if [[ -L "${DROPIN_DIR}" ]]; then echo "[pingora-direct-enable] drop-in 目录不能是符号链接: ${DROPIN_DIR}" >&2 exit 1 fi if [[ -e "${DROPIN_DIR}" && ! -d "${DROPIN_DIR}" ]]; then echo "[pingora-direct-enable] drop-in 父路径必须是目录: ${DROPIN_DIR}" >&2 exit 1 fi if [[ -L "${DROPIN_PATH}" ]]; then echo "[pingora-direct-enable] drop-in 目标不能是符号链接: ${DROPIN_PATH}" >&2 exit 1 fi if [[ -e "${DROPIN_PATH}" && ! -f "${DROPIN_PATH}" ]]; then echo "[pingora-direct-enable] drop-in 目标已存在但不是普通文件: ${DROPIN_PATH}" >&2 exit 1 fi fi run_cmd() { echo "+ $*" if [[ "${APPLY}" == "true" ]]; then "$@" fi } print_redacted_command() { local previous="" output=() for arg in "$@"; do if [[ "${previous}" == "--probe-token" || "${previous}" == "--direct-probe-token" ]]; then output+=("") else output+=("${arg}") fi previous="${arg}" done echo "+ ${output[*]}" } run_systemctl() { if [[ "${APPLY}" != "true" ]]; then run_cmd systemctl "$@" return fi if command -v systemctl >/dev/null 2>&1; then run_cmd systemctl "$@" else echo "[pingora-direct-enable] 未找到 systemctl,无法执行: systemctl $*" >&2 exit 1 fi } run_preflight() { if [[ -z "${PREFLIGHT_ENV_FILE}" ]]; then echo "[pingora-direct-enable] 未提供 --preflight-env-file,仅执行脚本 dry-run;--apply 会强制要求预检。" return fi preflight_args=( "${PREFLIGHT_SCRIPT}" --env-file "${PREFLIGHT_ENV_FILE}" --require-live-env ) if [[ "${PREFLIGHT_SYSTEMD}" == "true" ]]; then preflight_args+=(--systemd-cat) fi if [[ "${PREFLIGHT_CHECK_CERT_READABLE}" == "true" ]]; then preflight_args+=(--check-cert-readable) fi if [[ "${PREFLIGHT_CHECK_SERVICE_ENV_FILE}" == "true" ]]; then preflight_args+=(--check-service-env-file) fi if [[ "${PREFLIGHT_CHECK_SERVICE_USER_CERT_READABLE}" == "true" ]]; then preflight_args+=(--check-service-user-cert-readable) fi if [[ "${PREFLIGHT_CHECK_SERVICE_BINARY_EXECUTABLE}" == "true" ]]; then preflight_args+=(--check-service-binary-executable) fi if [[ "${PREFLIGHT_CHECK_PORTS_FREE}" == "true" ]]; then preflight_args+=(--check-ports-free) fi echo "+ node -- ${preflight_args[*]}" node -- "${preflight_args[@]}" } run_current_release_audit() { audit_args=( "${CURRENT_RELEASE_AUDIT_SCRIPT}" --release-root "${CURRENT_RELEASE_ROOT}" --require-pingora-gateway --systemd-show --systemd-service "${SERVICE_NAME}" ) if [[ -n "${CURRENT_RELEASE_AUDIT_TIMEOUT_MS}" ]]; then audit_args+=(--timeout-ms "${CURRENT_RELEASE_AUDIT_TIMEOUT_MS}") fi echo "+ node -- ${audit_args[*]}" if [[ "${APPLY}" != "true" ]]; then echo "[pingora-direct-enable] dry-run:--apply 前会先执行 current release 自包含自审。" return fi node -- "${audit_args[@]}" } systemd_env_file_matches() { local expected="$1" local line value word normalized while IFS= read -r line; do line="${line#"${line%%[![:space:]]*}"}" line="${line%"${line##*[![:space:]]}"}" [[ "${line}" == EnvironmentFile=* ]] || continue value="${line#EnvironmentFile=}" [[ -n "${value}" ]] || continue for word in ${value}; do normalized="${word%\"}" normalized="${normalized#\"}" normalized="${normalized%\'}" normalized="${normalized#\'}" normalized="${normalized#-}" if [[ "${normalized}" == "${expected}" ]]; then return 0 fi done done <<<"${unit_content}" return 1 } read_expected_exec_start() { local line value if [[ ! -f "${SERVICE_UNIT_PATH}" ]]; then return 1 fi while IFS= read -r line; do line="${line#"${line%%[![:space:]]*}"}" line="${line%"${line##*[![:space:]]}"}" [[ "${line}" == ExecStart=* ]] || continue value="${line#ExecStart=}" [[ -n "${value}" ]] || continue printf '%s\n' "${value}" return 0 done <"${SERVICE_UNIT_PATH}" return 1 } first_exec_word() { local value="$1" value="${value#"${value%%[![:space:]]*}"}" value="${value%"${value##*[![:space:]]}"}" if [[ "${value}" == \"* ]]; then value="${value#\"}" printf '%s\n' "${value%%\"*}" return fi printf '%s\n' "${value%%[[:space:]]*}" } systemd_show_exec_start_matches() { local expected_exec="$1" local expected_binary actual_show actual_binary expected_binary="$(first_exec_word "${expected_exec}")" [[ -n "${expected_binary}" ]] || return 1 actual_show="$(systemctl show "${SERVICE_NAME}" --property=ExecStart --value --no-pager)" actual_binary="${actual_show}" if [[ "${actual_show}" == *"path="* ]]; then actual_binary="${actual_show#*path=}" actual_binary="${actual_binary%% ;*}" elif [[ "${actual_show}" == ExecStart=* ]]; then actual_binary="${actual_show#ExecStart=}" actual_binary="$(first_exec_word "${actual_binary}")" else actual_binary="$(first_exec_word "${actual_binary}")" fi [[ "${actual_binary}" == "${expected_binary}" ]] } verify_systemd_exec_start() { if [[ "${VERIFY_SYSTEMD_AFTER}" != "true" ]]; then echo "[pingora-direct-enable] 已跳过 Pingora ExecStart 指向核验。" return fi expected_exec_start="$(read_expected_exec_start || true)" if [[ -z "${expected_exec_start}" ]]; then echo "[pingora-direct-enable] 无法从主 service 模板读取 ExecStart: ${SERVICE_UNIT_PATH}" >&2 exit 1 fi if [[ "${APPLY}" != "true" ]]; then echo "+ systemctl show ${SERVICE_NAME} --property=ExecStart --value --no-pager" echo "[pingora-direct-enable] dry-run:--apply 后会核验 systemd ExecStart 指向主 service 模板中的 ${expected_exec_start}。" return fi if ! command -v systemctl >/dev/null 2>&1; then echo "[pingora-direct-enable] 未找到 systemctl,无法核验 Pingora ExecStart。" >&2 exit 1 fi echo "+ systemctl show ${SERVICE_NAME} --property=ExecStart --value --no-pager" if ! systemd_show_exec_start_matches "${expected_exec_start}"; then echo "[pingora-direct-enable] systemctl show ExecStart 未指向主 service 模板中的 ${expected_exec_start},请先修正 ${SERVICE_NAME} 指向 current release 网关二进制。" >&2 exit 1 fi } verify_systemd_dropin() { if [[ "${VERIFY_SYSTEMD_AFTER}" != "true" ]]; then echo "[pingora-direct-enable] 已跳过 systemd drop-in 生效核验。" return fi if [[ "${APPLY}" != "true" ]]; then echo "+ systemctl cat ${SERVICE_NAME}" echo "[pingora-direct-enable] dry-run:--apply 后会校验 systemd 最终配置包含 CAP_NET_BIND_SERVICE。" return fi if ! command -v systemctl >/dev/null 2>&1; then echo "[pingora-direct-enable] 未找到 systemctl,无法核验 drop-in 是否生效。" >&2 exit 1 fi echo "+ systemctl cat ${SERVICE_NAME}" unit_content="$(systemctl cat "${SERVICE_NAME}")" if [[ "${unit_content}" != *"AmbientCapabilities=CAP_NET_BIND_SERVICE"* ]]; then echo "[pingora-direct-enable] systemctl cat 未显示 AmbientCapabilities=CAP_NET_BIND_SERVICE,direct-entry drop-in 未生效。" >&2 exit 1 fi if [[ "${unit_content}" != *"CapabilityBoundingSet=CAP_NET_BIND_SERVICE"* ]]; then echo "[pingora-direct-enable] systemctl cat 未显示 CapabilityBoundingSet=CAP_NET_BIND_SERVICE,direct-entry drop-in 未生效。" >&2 exit 1 fi if [[ "${PREFLIGHT_CHECK_SERVICE_ENV_FILE}" == "true" && -n "${PREFLIGHT_ENV_FILE}" ]]; then if ! systemd_env_file_matches "${PREFLIGHT_ENV_FILE}"; then echo "[pingora-direct-enable] systemctl cat 未显示 EnvironmentFile=${PREFLIGHT_ENV_FILE},service 实际读取的 env 与 preflight env 不一致。" >&2 exit 1 fi fi } verify_service_active_after_restart() { if [[ "${VERIFY_SYSTEMD_AFTER}" != "true" ]]; then echo "[pingora-direct-enable] 已跳过 Pingora 重启后状态核验。" return fi if [[ "${APPLY}" != "true" ]]; then echo "+ systemctl is-active ${SERVICE_NAME}" echo "[pingora-direct-enable] dry-run:--apply 后会校验 Pingora service 为 active。" return fi if ! command -v systemctl >/dev/null 2>&1; then echo "[pingora-direct-enable] 未找到 systemctl,无法核验 Pingora 重启后状态。" >&2 exit 1 fi echo "+ systemctl is-active ${SERVICE_NAME}" service_state="$(systemctl is-active "${SERVICE_NAME}")" if [[ "${service_state}" != "active" ]]; then echo "[pingora-direct-enable] Pingora 重启后状态不是 active: ${service_state}" >&2 exit 1 fi } run_direct_live_smoke_after_enable() { if [[ -z "${DIRECT_HTTPS_BASE_URL}" ]]; then echo "+ node ${DIRECT_LIVE_SCRIPT} --https-base-url --http-base-url --host --redirect-host --spacetime-database --require-wss-upgrade" echo "[pingora-direct-enable] dry-run:--apply 后会执行 direct live smoke;--apply 会强制要求 direct live 参数。" return fi direct_live_args=( "${DIRECT_LIVE_SCRIPT}" --https-base-url "${DIRECT_HTTPS_BASE_URL}" ) if [[ -n "${DIRECT_HTTP_BASE_URL}" ]]; then direct_live_args+=(--http-base-url "${DIRECT_HTTP_BASE_URL}") fi if [[ -n "${DIRECT_HOST}" ]]; then direct_live_args+=(--host "${DIRECT_HOST}") fi if [[ -n "${DIRECT_REDIRECT_HOST}" ]]; then direct_live_args+=(--redirect-host "${DIRECT_REDIRECT_HOST}") fi if [[ -n "${DIRECT_REDIRECT_BASE_URL}" ]]; then direct_live_args+=(--redirect-base-url "${DIRECT_REDIRECT_BASE_URL}") fi if [[ -n "${DIRECT_PROBE_TOKEN}" ]]; then direct_live_args+=(--probe-token "${DIRECT_PROBE_TOKEN}") fi if [[ -n "${DIRECT_PINGORA_ACCESS_LOG}" ]]; then direct_live_args+=(--pingora-access-log "${DIRECT_PINGORA_ACCESS_LOG}") fi if [[ -n "${DIRECT_ACCESS_LOG_SINCE_LINES}" ]]; then direct_live_args+=(--access-log-since-lines "${DIRECT_ACCESS_LOG_SINCE_LINES}") fi if [[ -n "${DIRECT_SPACETIME_DATABASE}" ]]; then direct_live_args+=(--spacetime-database "${DIRECT_SPACETIME_DATABASE}") fi direct_live_args+=(--require-wss-upgrade) if [[ -n "${DIRECT_TIMEOUT_MS}" ]]; then direct_live_args+=(--timeout-ms "${DIRECT_TIMEOUT_MS}") fi direct_live_args+=(--json) print_redacted_command node "${direct_live_args[@]}" if [[ "${APPLY}" != "true" ]]; then echo "[pingora-direct-enable] dry-run:--apply 后会验证 Pingora HTTPS / HTTP redirect / WSS 直连入口,并解析 direct-access-log 结构化证据。" return fi set +e direct_live_output="$(node "${direct_live_args[@]}")" direct_live_status=$? set -e printf '%s\n' "${direct_live_output}" if [[ "${direct_live_status}" -ne 0 ]]; then exit "${direct_live_status}" fi DIRECT_LIVE_OUTPUT="${direct_live_output}" node -- <<'NODE' const stdout = process.env.DIRECT_LIVE_OUTPUT || ''; const jsonText = extractFirstJsonObject(stdout); if (!jsonText) { console.error( '[pingora-direct-enable] direct live stdout 中未找到 JSON 结果对象,无法确认 direct-access-log 结构化证据。', ); process.exit(1); } let parsed; try { parsed = JSON.parse(jsonText); } catch (error) { console.error( `[pingora-direct-enable] direct live JSON 解析失败: ${error.message}`, ); process.exit(1); } const accessLogCheck = parsed?.results?.find( (item) => item?.name === 'direct-access-log', ); if (!accessLogCheck) { console.error( '[pingora-direct-enable] direct live JSON 缺少 direct-access-log 结构化结果。', ); process.exit(1); } const checked = toNonNegativeInteger(accessLogCheck.checked); const matchedCount = toNonNegativeInteger(accessLogCheck.matchedCount); const missingCount = toNonNegativeInteger(accessLogCheck.missingCount); const mismatchCount = toNonNegativeInteger(accessLogCheck.mismatchCount); const missingDetailsCount = Array.isArray(accessLogCheck.missing) ? accessLogCheck.missing.length : null; const mismatchDetailsCount = Array.isArray(accessLogCheck.mismatches) ? accessLogCheck.mismatches.length : null; if ( checked === null || matchedCount === null || missingCount !== 0 || mismatchCount !== 0 || matchedCount !== checked || missingDetailsCount !== 0 || mismatchDetailsCount !== 0 ) { console.error( `[pingora-direct-enable] direct-access-log 结构化证据未通过: checked=${checked ?? '-'} matched=${matchedCount ?? '-'} missing=${missingCount ?? '-'} mismatches=${mismatchCount ?? '-'}`, ); process.exit(1); } console.log( `[pingora-direct-enable] direct-access-log evidence checked=${checked} matched=${matchedCount} missing=0 mismatches=0`, ); function extractFirstJsonObject(text) { const startIndex = text.indexOf('{'); if (startIndex < 0) { return ''; } let depth = 0; let inString = false; let escaped = false; for (let index = startIndex; index < text.length; index += 1) { const char = text[index]; if (inString) { if (escaped) { escaped = false; } else if (char === '\\') { escaped = true; } else if (char === '"') { inString = false; } continue; } if (char === '"') { inString = true; continue; } if (char === '{') { depth += 1; continue; } if (char === '}') { depth -= 1; if (depth === 0) { return text.slice(startIndex, index + 1); } } } return ''; } function toNonNegativeInteger(value) { const number = Number(value); if (!Number.isInteger(number) || number < 0) { return null; } return number; } NODE } echo "[pingora-direct-enable] apply=${APPLY} service=${SERVICE_NAME} template=${TEMPLATE_PATH} dropin=${DROPIN_PATH}" if [[ "${APPLY}" != "true" ]]; then echo "[pingora-direct-enable] 当前是 dry-run;确认 preflight 通过后追加 --apply。" if [[ ! -f "${TEMPLATE_PATH}" ]]; then echo "[pingora-direct-enable] dry-run 提示:当前模板不存在,--apply 会失败: ${TEMPLATE_PATH}" >&2 fi fi run_current_release_audit run_preflight run_cmd install -d -m 0755 "$(dirname "${DROPIN_PATH}")" run_cmd install -m 0644 "${TEMPLATE_PATH}" "${DROPIN_PATH}" run_systemctl daemon-reload run_systemctl restart "${SERVICE_NAME}" verify_systemd_dropin verify_systemd_exec_start verify_service_active_after_restart run_direct_live_smoke_after_enable if [[ "${STATUS_AFTER}" == "true" ]]; then run_systemctl status "${SERVICE_NAME}" --no-pager fi cat <} --http-base-url ${DIRECT_HTTP_BASE_URL:-} --host ${DIRECT_HOST:-} --redirect-host ${DIRECT_REDIRECT_HOST:-} ${DIRECT_REDIRECT_BASE_URL:+--redirect-base-url ${DIRECT_REDIRECT_BASE_URL} }--pingora-access-log ${DIRECT_PINGORA_ACCESS_LOG:-} --spacetime-database ${DIRECT_SPACETIME_DATABASE:-} --require-wss-upgrade --json 已通过,且 direct-access-log 结构化证据 matched=checked、missing=0、mismatches=0 EOF