#!/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)" TEMPLATE_PATH="${GENARRATIVE_PINGORA_REALPATH_CANARY_TEMPLATE_PATH:-${REPO_ROOT}/deploy/nginx/snippets/genarrative-pingora-realpath-canary.conf}" TARGET_PATH="${GENARRATIVE_PINGORA_REALPATH_CANARY_TARGET_PATH:-/etc/nginx/conf.d/zz-genarrative-pingora-realpath-canary.conf}" PROBE_TOKEN="${GENARRATIVE_PINGORA_REALPATH_CANARY_PROBE_TOKEN:-${GENARRATIVE_PINGORA_GATEWAY_PROBE_TOKEN:-}}" BASE_URL="${GENARRATIVE_PINGORA_REALPATH_CANARY_BASE_URL:-http://127.0.0.1:18083}" HOST_HEADER="${GENARRATIVE_PINGORA_REALPATH_CANARY_HOST:-}" NGINX_BINARY="${GENARRATIVE_NGINX_BINARY:-nginx}" NGINX_SERVICE="${GENARRATIVE_NGINX_SERVICE:-nginx.service}" SYSTEMCTL_BINARY="${GENARRATIVE_SYSTEMCTL_BINARY:-systemctl}" LIVE_SCRIPT="${GENARRATIVE_PINGORA_REALPATH_CANARY_LIVE_SCRIPT:-${REPO_ROOT}/scripts/check-pingora-canary-live.mjs}" APPLY="false" RUN_LIVE="true" STATUS_AFTER="true" PLACEHOLDER="__GENARRATIVE_PINGORA_PROBE_TOKEN__" usage() { cat <<'EOF' 用法: scripts/deploy/pingora-realpath-canary-enable.sh [--apply] [--probe-token ] [--host ] [--base-url ] [--template-path ] [--target-path ] [--nginx-binary ] [--nginx-service ] [--systemctl-binary ] [--live-script ] [--skip-live] [--no-status] 说明: 启用 Nginx -> Pingora 真实路径 canary。脚本会把随包 deploy/nginx/snippets/genarrative-pingora-realpath-canary.conf 渲染到 /etc/nginx/conf.d/zz-genarrative-pingora-realpath-canary.conf。 默认是 dry-run,只打印计划;必须显式传 --apply 才会写 Nginx 配置。 --apply 会替换 probe token、写入目标文件、执行 nginx -t、reload nginx, 然后运行 realpath canary live smoke。任一阶段失败都会恢复写入前的目标文件状态。 EOF } while [[ $# -gt 0 ]]; do case "$1" in -h|--help) usage exit 0 ;; --apply) APPLY="true" shift ;; --probe-token) PROBE_TOKEN="${2:-}" if [[ -z "${PROBE_TOKEN}" ]]; then echo "[pingora-realpath-canary-enable] --probe-token 缺少参数" >&2 exit 1 fi shift 2 ;; --host) HOST_HEADER="${2:-}" if [[ -z "${HOST_HEADER}" ]]; then echo "[pingora-realpath-canary-enable] --host 缺少参数" >&2 exit 1 fi shift 2 ;; --base-url) BASE_URL="${2:-}" if [[ -z "${BASE_URL}" ]]; then echo "[pingora-realpath-canary-enable] --base-url 缺少参数" >&2 exit 1 fi shift 2 ;; --template-path) TEMPLATE_PATH="${2:-}" if [[ -z "${TEMPLATE_PATH}" ]]; then echo "[pingora-realpath-canary-enable] --template-path 缺少参数" >&2 exit 1 fi shift 2 ;; --target-path) TARGET_PATH="${2:-}" if [[ -z "${TARGET_PATH}" ]]; then echo "[pingora-realpath-canary-enable] --target-path 缺少参数" >&2 exit 1 fi shift 2 ;; --nginx-binary) NGINX_BINARY="${2:-}" if [[ -z "${NGINX_BINARY}" ]]; then echo "[pingora-realpath-canary-enable] --nginx-binary 缺少参数" >&2 exit 1 fi shift 2 ;; --nginx-service) NGINX_SERVICE="${2:-}" if [[ -z "${NGINX_SERVICE}" ]]; then echo "[pingora-realpath-canary-enable] --nginx-service 缺少参数" >&2 exit 1 fi shift 2 ;; --systemctl-binary) SYSTEMCTL_BINARY="${2:-}" if [[ -z "${SYSTEMCTL_BINARY}" ]]; then echo "[pingora-realpath-canary-enable] --systemctl-binary 缺少参数" >&2 exit 1 fi shift 2 ;; --live-script) LIVE_SCRIPT="${2:-}" if [[ -z "${LIVE_SCRIPT}" ]]; then echo "[pingora-realpath-canary-enable] --live-script 缺少参数" >&2 exit 1 fi shift 2 ;; --skip-live) RUN_LIVE="false" shift ;; --no-status) STATUS_AFTER="false" shift ;; *) echo "[pingora-realpath-canary-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-realpath-canary-enable] ${label} 不能包含换行或 NUL 字符。" >&2 exit 1 fi } is_filesystem_root_path() { local value="$1" local without_slashes="${value//\//}" [[ -n "${value}" && -z "${without_slashes}" ]] } require_absolute_path() { local label="$1" local value="$2" if [[ "${value}" != /* ]]; then echo "[pingora-realpath-canary-enable] ${label} 必须是绝对路径。" >&2 exit 1 fi if is_filesystem_root_path "${value}"; then echo "[pingora-realpath-canary-enable] ${label} 不能是文件系统根目录。" >&2 exit 1 fi } validate_command_or_absolute_path() { local label="$1" local value="$2" if [[ "${value}" == */* ]]; then require_absolute_path "${label}" "${value}" fi } validate_host() { local label="$1" local value="$2" if [[ -z "${value}" ]]; then echo "[pingora-realpath-canary-enable] ${label} 必须提供。" >&2 exit 1 fi if [[ "${value}" == http://* || "${value}" == https://* || "${value}" == */* || "${value}" == *\?* || "${value}" =~ [[:space:]] ]]; then echo "[pingora-realpath-canary-enable] ${label} 必须是 Host 或 host:port,不能是 URL、路径、查询或包含空白。" >&2 exit 1 fi } validate_base_url() { if [[ "${BASE_URL}" != http://* && "${BASE_URL}" != https://* ]]; then echo "[pingora-realpath-canary-enable] --base-url 必须以 http:// 或 https:// 开头。" >&2 exit 1 fi } validate_probe_token() { if [[ "${APPLY}" != "true" ]]; then return fi if [[ -z "${PROBE_TOKEN}" ]]; then echo "[pingora-realpath-canary-enable] --apply 必须提供 --probe-token 或 GENARRATIVE_PINGORA_REALPATH_CANARY_PROBE_TOKEN。" >&2 exit 1 fi if [[ "${PROBE_TOKEN}" == "${PLACEHOLDER}" || "${#PROBE_TOKEN}" -lt 8 ]]; then echo "[pingora-realpath-canary-enable] probe token 不能是占位值或过短。" >&2 exit 1 fi } escape_nginx_double_quoted() { local value="$1" value="${value//\\/\\\\}" value="${value//\"/\\\"}" printf '%s' "${value}" } render_template_to() { local output_path="$1" local content escaped_token content="$(<"${TEMPLATE_PATH}")" escaped_token="$(escape_nginx_double_quoted "${PROBE_TOKEN}")" printf '%s' "${content//${PLACEHOLDER}/${escaped_token}}" >"${output_path}" } print_command() { printf '+' for arg in "$@"; do printf ' %q' "${arg}" done printf '\n' } run_or_print() { if [[ "${APPLY}" == "true" ]]; then "$@" else print_command "$@" fi } restore_previous() { local backup_path="$1" local had_target="$2" if [[ "${had_target}" == "true" ]]; then cp -p -- "${backup_path}" "${TARGET_PATH}" else rm -f -- "${TARGET_PATH}" fi } rollback_after_failure() { local backup_path="$1" local had_target="$2" local reason="$3" echo "[pingora-realpath-canary-enable] ${reason},恢复写入前配置。" >&2 restore_previous "${backup_path}" "${had_target}" "${NGINX_BINARY}" -t >/dev/null 2>&1 || true "${SYSTEMCTL_BINARY}" reload "${NGINX_SERVICE}" >/dev/null 2>&1 || true } reject_control_characters "--template-path" "${TEMPLATE_PATH}" reject_control_characters "--target-path" "${TARGET_PATH}" reject_control_characters "--probe-token" "${PROBE_TOKEN}" reject_control_characters "--base-url" "${BASE_URL}" reject_control_characters "--host" "${HOST_HEADER}" reject_control_characters "--nginx-binary" "${NGINX_BINARY}" reject_control_characters "--nginx-service" "${NGINX_SERVICE}" reject_control_characters "--systemctl-binary" "${SYSTEMCTL_BINARY}" reject_control_characters "--live-script" "${LIVE_SCRIPT}" require_absolute_path "--template-path" "${TEMPLATE_PATH}" require_absolute_path "--target-path" "${TARGET_PATH}" if [[ "${RUN_LIVE}" == "true" ]]; then require_absolute_path "--live-script" "${LIVE_SCRIPT}" fi validate_command_or_absolute_path "--nginx-binary" "${NGINX_BINARY}" validate_command_or_absolute_path "--systemctl-binary" "${SYSTEMCTL_BINARY}" validate_host "--host" "${HOST_HEADER}" validate_base_url validate_probe_token if [[ ! -f "${TEMPLATE_PATH}" ]]; then echo "[pingora-realpath-canary-enable] 未找到 realpath canary 模板: ${TEMPLATE_PATH}" >&2 exit 1 fi if [[ "${RUN_LIVE}" == "true" && ! -f "${LIVE_SCRIPT}" ]]; then echo "[pingora-realpath-canary-enable] 未找到 realpath canary live smoke 脚本: ${LIVE_SCRIPT}" >&2 exit 1 fi TARGET_DIR="$(dirname -- "${TARGET_PATH}")" if [[ "${APPLY}" == "true" ]]; then if [[ ! -d "${TARGET_DIR}" ]]; then echo "[pingora-realpath-canary-enable] 目标目录不存在: ${TARGET_DIR}" >&2 exit 1 fi if [[ -L "${TARGET_DIR}" || -L "${TARGET_PATH}" ]]; then echo "[pingora-realpath-canary-enable] 目标目录或目标文件不能是符号链接。" >&2 exit 1 fi fi if [[ "${APPLY}" != "true" ]]; then echo "[pingora-realpath-canary-enable] dry-run:将渲染 realpath canary 到 ${TARGET_PATH},probe token 输出已隐藏。" print_command install -m 0644 "" "${TARGET_PATH}" run_or_print "${NGINX_BINARY}" -t run_or_print "${SYSTEMCTL_BINARY}" reload "${NGINX_SERVICE}" if [[ "${RUN_LIVE}" == "true" ]]; then run_or_print node -- "${LIVE_SCRIPT}" --realpath --base-url "${BASE_URL}" --host "${HOST_HEADER}" fi exit 0 fi tmp_path="$(mktemp "${TARGET_DIR}/.genarrative-pingora-realpath-canary.XXXXXX")" backup_path="$(mktemp "${TARGET_DIR}/.genarrative-pingora-realpath-canary.backup.XXXXXX")" had_target="false" cleanup_paths=("${tmp_path}" "${backup_path}") trap 'rm -f -- "${cleanup_paths[@]}"' EXIT render_template_to "${tmp_path}" chmod 0644 "${tmp_path}" if [[ -e "${TARGET_PATH}" ]]; then if [[ ! -f "${TARGET_PATH}" ]]; then echo "[pingora-realpath-canary-enable] 目标已存在但不是普通文件: ${TARGET_PATH}" >&2 exit 1 fi cp -p -- "${TARGET_PATH}" "${backup_path}" had_target="true" fi install -m 0644 "${tmp_path}" "${TARGET_PATH}" if ! "${NGINX_BINARY}" -t; then rollback_after_failure "${backup_path}" "${had_target}" "nginx -t 失败" exit 1 fi if ! "${SYSTEMCTL_BINARY}" reload "${NGINX_SERVICE}"; then rollback_after_failure "${backup_path}" "${had_target}" "reload ${NGINX_SERVICE} 失败" exit 1 fi if [[ "${RUN_LIVE}" == "true" ]]; then if ! node -- "${LIVE_SCRIPT}" --realpath --base-url "${BASE_URL}" --host "${HOST_HEADER}"; then rollback_after_failure "${backup_path}" "${had_target}" "realpath canary live smoke 失败" exit 1 fi fi if [[ "${STATUS_AFTER}" == "true" ]]; then "${SYSTEMCTL_BINARY}" is-active "${NGINX_SERVICE}" fi echo "[pingora-realpath-canary-enable] 已启用 realpath canary: ${TARGET_PATH}"