From 75b85ee5a991d590a72b72704f06cbd4a652c9e8 Mon Sep 17 00:00:00 2001 From: kdletters Date: Wed, 17 Jun 2026 23:00:28 +0800 Subject: [PATCH] =?UTF-8?q?=E4=BF=AE=E6=AD=A3=20Pingora=20=E9=AB=98?= =?UTF-8?q?=E7=AB=AF=E5=8F=A3=E9=87=8D=E5=AE=9A=E5=90=91=E9=AA=8C=E6=94=B6?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit direct live 支持独立 redirect base URL 预期 release readiness 和切换证据链透传 redirect base URL 直连启用脚本透传高端口 rehearsal 参数 补充 direct live guard、运维门禁和文档示例 --- deploy/env/pingora-direct-live.env.example | 5 ++- deploy/nginx/README.md | 2 +- ...开发运维】Pingora独立网关试点-2026-06-11.md | 2 +- scripts/check-pingora-direct-enable.mjs | 10 +++++ scripts/check-pingora-direct-live-guard.mjs | 34 +++++++++++++++ scripts/check-pingora-direct-live.mjs | 34 +++++++++++++++ scripts/check-pingora-release-readiness.mjs | 41 +++++++++++++++++++ scripts/check-production-ops-guardrails.mjs | 18 ++++++++ scripts/deploy/pingora-direct-enable.sh | 17 +++++++- .../ops/pingora-cutover-evidence-bundle.mjs | 28 +++++++++++++ 10 files changed, 186 insertions(+), 5 deletions(-) diff --git a/deploy/env/pingora-direct-live.env.example b/deploy/env/pingora-direct-live.env.example index 87dc1369b..b3d04c609 100644 --- a/deploy/env/pingora-direct-live.env.example +++ b/deploy/env/pingora-direct-live.env.example @@ -28,9 +28,12 @@ GENARRATIVE_PINGORA_DIRECT_HTTP_BASE_URL=http://127.0.0.1 # 本机直连 127.0.0.1 时必须设置正式域名;direct live 会同时用作 Host header 和 TLS SNI。release readiness --require-direct 必须提供。只填域名或 host:port,不要填 https:// URL。 GENARRATIVE_PINGORA_DIRECT_HOST= -# 单独 direct live 可选;release readiness --require-direct 必须提供,用于显式校验 HTTP redirect Location 的 host。只填域名或 host:port,非标准端口本机 smoke 可设为 127.0.0.1:18443。 +# 单独 direct live 可选;release readiness --require-direct 必须提供,用于显式校验 HTTP redirect Location 的 host。只填域名或 host:port。 GENARRATIVE_PINGORA_DIRECT_REDIRECT_HOST= +# 可选:高端口 rehearsal 访问 127.0.0.1:18443、但期望 Location 指向正式域名默认 HTTPS 入口时设置;只能填 https:// base URL,不要带路径、查询或片段。 +GENARRATIVE_PINGORA_DIRECT_REDIRECT_BASE_URL= + # 可选:配置后会额外检查 /__genarrative_pingora/healthz 内部探针。 GENARRATIVE_PINGORA_DIRECT_PROBE_TOKEN= diff --git a/deploy/nginx/README.md b/deploy/nginx/README.md index 7f8e0f8d0..d5db0afec 100644 --- a/deploy/nginx/README.md +++ b/deploy/nginx/README.md @@ -50,7 +50,7 @@ - 目标机如果使用 Certbot / Let’s Encrypt 证书,不要直接放宽 `/etc/letsencrypt/live` 或 `archive` 的目录 / 私钥权限。先用随包脚本把 root 可读的 live symlink 解析并复制到 Pingora 私有目录,例如 `sudo -n node -- /opt/genarrative/current/scripts/deploy/pingora-tls-cert-sync.mjs --apply --source-cert-file /etc/letsencrypt/live/<域名>/fullchain.pem --source-key-file /etc/letsencrypt/live/<域名>/privkey.pem --target-dir /etc/genarrative/pingora-tls/<域名>`;脚本默认 dry-run,`--apply` 才原子写入 `fullchain.pem` / `privkey.pem`,目标目录默认 `root:genarrative 0750`,文件默认 `root:genarrative 0640`,并复核 `genarrative` 服务用户可读。随后把 `GENARRATIVE_PINGORA_GATEWAY_TLS_CERT_FILE` / `TLS_KEY_FILE` 指向 `/etc/genarrative/pingora-tls/<域名>/` 下的副本。 - 默认 systemd service 以 `genarrative` 非 root 用户运行,不具备绑定 `80/443` 的能力。Server-Provision 只安装 `/etc/genarrative/pingora/genarrative-pingora-gateway-direct-entry.conf` 作为人工启用模板;切直连低端口时必须先确认 `genarrative` 可读证书文件、Nginx 已释放 `80/443`,再执行 `/opt/genarrative/current/scripts/deploy/pingora-direct-enable.sh --apply --preflight-env-file /etc/genarrative/pingora-gateway.env --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 https://127.0.0.1 --direct-http-base-url http://127.0.0.1 --direct-host <域名> --direct-redirect-host <域名或host:port> --direct-spacetime-database <库名> --direct-pingora-access-log /var/log/genarrative/pingora-gateway.access.log`;脚本会先执行随包 current release 自审,确认 `pingora-gateway` 可执行且 systemd `ExecStart` 指向 current release,失败时不会安装 drop-in。自审通过后才安装 drop-in、`systemctl daemon-reload` 并重启 Pingora;脚本会用 `systemctl cat` 确认 `CAP_NET_BIND_SERVICE` drop-in 已进入 systemd 最终配置,用 `systemctl show ... ExecStart` 确认最终 service 指向随包主 service 模板中的 current release `pingora-gateway`,用 `systemctl is-active` 确认 Pingora active,并执行 direct live smoke,强制覆盖 HTTPS / HTTP redirect / ACME / WSS 101 和 Pingora access log request_id 落盘。 - `npm run check:pingora-gateway-smoke` 会用临时自签证书覆盖 TLS 直连、HTTP redirect 和 WSS subscribe 行为;正式公网切换前仍必须先走 Nginx canary 和 release readiness 门禁。 -- 目标机直连入口启用后先运行 `npm run check:pingora-direct-preflight -- --env-file /etc/genarrative/pingora-gateway.env --require-live-env --systemd-cat --check-cert-readable --check-service-env-file --check-service-user-cert-readable --check-service-binary-executable --check-ports-free`,再运行 `npm run check:pingora-direct-live`,检查 HTTPS 根路径、HTTP/2 ALPN、代表性 API、SpacetimeDB identity、WSS subscribe 握手、generated 拒绝、公开 `/healthz` 拒绝、可选 shadow probe,以及 HTTP 301 / ACME challenge;真实 SpacetimeDB 对 `GET /v1/identity` 返回 `405 Method Not Allowed` 属于可接受语义,direct live 只把它作为路径转发代表,不要求该 GET 创建 identity;release readiness 可用 `--require-direct --direct-https-base-url https://127.0.0.1 --direct-http-base-url http://127.0.0.1 --direct-host <域名> --direct-redirect-host <域名或host:port> --direct-spacetime-database <库名> --direct-pingora-access-log /var/log/genarrative/pingora-gateway.access.log --direct-health-patrol-env-file /etc/genarrative/health-patrol.env --direct-preflight-env-file /etc/genarrative/pingora-gateway.env --direct-preflight-systemd --direct-preflight-check-cert-readable --direct-preflight-check-service-env-file --direct-preflight-check-service-user-cert-readable --direct-preflight-check-service-binary-executable --direct-preflight-check-ports-free` 强制纳入同一条门禁,漏掉 direct HTTP base URL、正式域名 Host/SNI、redirect Location host、目标 SpacetimeDB 库、Pingora access log request_id 落盘、health patrol env direct 模式复核、systemd drop-in 生效检查、service EnvironmentFile 一致性检查、当前用户 / systemd 服务用户证书可读检查、service 二进制可执行检查或 80/443 端口释放检查都会直接失败。 +- 目标机直连入口启用后先运行 `npm run check:pingora-direct-preflight -- --env-file /etc/genarrative/pingora-gateway.env --require-live-env --systemd-cat --check-cert-readable --check-service-env-file --check-service-user-cert-readable --check-service-binary-executable --check-ports-free`,再运行 `npm run check:pingora-direct-live`,检查 HTTPS 根路径、HTTP/2 ALPN、代表性 API、SpacetimeDB identity、WSS subscribe 握手、generated 拒绝、公开 `/healthz` 拒绝、可选 shadow probe,以及 HTTP 301 / ACME challenge;真实 SpacetimeDB 对 `GET /v1/identity` 返回 `405 Method Not Allowed` 属于可接受语义,direct live 只把它作为路径转发代表,不要求该 GET 创建 identity;release readiness 可用 `--require-direct --direct-https-base-url https://127.0.0.1 --direct-http-base-url http://127.0.0.1 --direct-host <域名> --direct-redirect-host <域名或host:port> --direct-spacetime-database <库名> --direct-pingora-access-log /var/log/genarrative/pingora-gateway.access.log --direct-health-patrol-env-file /etc/genarrative/health-patrol.env --direct-preflight-env-file /etc/genarrative/pingora-gateway.env --direct-preflight-systemd --direct-preflight-check-cert-readable --direct-preflight-check-service-env-file --direct-preflight-check-service-user-cert-readable --direct-preflight-check-service-binary-executable --direct-preflight-check-ports-free` 强制纳入同一条门禁,漏掉 direct HTTP base URL、正式域名 Host/SNI、redirect Location host、目标 SpacetimeDB 库、Pingora access log request_id 落盘、health patrol env direct 模式复核、systemd drop-in 生效检查、service EnvironmentFile 一致性检查、当前用户 / systemd 服务用户证书可读检查、service 二进制可执行检查或 80/443 端口释放检查都会直接失败。高端口 rehearsal 如果通过 `https://127.0.0.1:18443` 打入、但 Pingora 应返回正式域名默认 HTTPS Location,可额外传 `--direct-redirect-base-url https://<域名>`;`--direct-redirect-host` 仍必须保留,用于直连切换 Host 一致性约束。 - direct live 在 HTTPS 根路径返回 `200` 且 HTML 中发现 `/assets/` 或 `/admin/assets/` 引用时,会自动请求该静态资源,确认 `Cache-Control`、`ETag`、`Last-Modified`、`Accept-Ranges: bytes`、`HEAD` 头响应、`If-None-Match` / `If-Modified-Since` 的 `304` 响应和 `Range: bytes=0-0` 的 `206 + Content-Range` 响应,并纳入 Pingora access log method/path/status 对账;若首页引用 Vite 指纹资源,还会额外确认 `Cache-Control: public, max-age=31536000, immutable` 以及指纹资源 GET / HEAD / 304 / Range access log 证据。维护模式、非 HTML 或发布包首页没有资产引用时该项标记为 skipped,不阻断维护窗口。 - 直连启用前先 dry-run `/opt/genarrative/current/scripts/deploy/pingora-direct-enable.sh --no-status`;验证失败时执行 `/opt/genarrative/current/scripts/deploy/pingora-direct-rollback.sh --apply --reload-nginx --nginx-smoke-url http://127.0.0.1/healthz --nginx-smoke-host <域名> --nginx-smoke-expect-body '"ok":true'`,脚本会先运行 `nginx -t`,通过后移除 direct-entry drop-in、重启 Pingora,用 `systemctl cat` 确认 `CAP_NET_BIND_SERVICE` 已从 systemd 最终配置中移除,用 `systemctl show ... ExecStart` 确认最终 service 仍指向随包主 service 模板中的 current release `pingora-gateway`,并 reload Nginx、确认 Nginx service 仍为 `active`,最后用 smoke URL 证明 Nginx 入口真实可访问;传入 `--nginx-smoke-expect-body` 时还会要求响应体包含该片段。仓库工作区可用 `npm run deploy:pingora-direct-enable -- --apply --preflight-env-file /etc/genarrative/pingora-gateway.env --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 https://127.0.0.1 --direct-http-base-url http://127.0.0.1 --direct-host <域名> --direct-redirect-host <域名或host:port> --direct-spacetime-database <库名> --direct-pingora-access-log /var/log/genarrative/pingora-gateway.access.log` 和 `npm run deploy:pingora-direct-rollback -- --apply --reload-nginx --nginx-smoke-url http://127.0.0.1/healthz --nginx-smoke-host <域名> --nginx-smoke-expect-body '"ok":true'`。两个脚本默认 dry-run;启用脚本 `--apply` 必须先通过 current release 自审,并带 env、证书可读、service EnvironmentFile 一致性、服务用户证书可读、service 二进制可执行、端口释放预检、direct live 参数和 Pingora access log 参数,回退脚本 `--apply` 必须带 `--reload-nginx` 和 `--nginx-smoke-url`;如果 health patrol env 已预先切回 Nginx,也可追加 `--health-patrol-env-file /etc/genarrative/health-patrol.env --health-patrol-expected-public-base-url <切换前Nginx巡检入口> --health-patrol-require-empty-public-host` 让回退脚本在 Nginx smoke 后复核 `gateway mode=nginx` 且 public base URL / Host 已恢复,若切换前 Nginx 巡检本来需要 Host 覆盖,则把最后一项换成 `--health-patrol-expected-public-host <切换前Host>`;如果还要同时证明 shadow 高端口仍健康,可追加 `--pingora-shadow-probe-url http://127.0.0.1:18081/__genarrative_pingora/healthz --pingora-shadow-probe-token `,脚本会隐藏 token 并要求响应包含 `gateway=pingora-shadow`。本机 `npm run check:pingora-direct-enable` / `npm run check:pingora-direct-rollback` 会验证 dry-run 不会安装或删除临时 drop-in、current release 自审失败时启用脚本不会安装 drop-in,并展示启用 / 回退后的 systemd 最终配置核验、ExecStart 指向核验、Pingora active 核验、direct live smoke、Nginx 语法检查、reload 状态核验、Nginx smoke 及响应体片段核验、可选 health patrol env 复核和可选 shadow probe 复核。 - `npm run check:pingora-current-release-audit` 会烟测 current release 自审脚本;正式直连 runbook 会先执行 `/opt/genarrative/current/scripts/ops/pingora-current-release-audit.mjs --release-root /opt/genarrative/current --require-pingora-gateway --systemd-show`,只读确认发布包自包含、`api-server.sha256` / `pingora-gateway.sha256` 匹配、`release-manifest.api-server.json` 已登记 `pingora-gateway`、`pingora-gateway` 可执行且 systemd `ExecStart` 指向 current release。 diff --git a/docs/technical/【开发运维】Pingora独立网关试点-2026-06-11.md b/docs/technical/【开发运维】Pingora独立网关试点-2026-06-11.md index 17aab3968..c2404c9fa 100644 --- a/docs/technical/【开发运维】Pingora独立网关试点-2026-06-11.md +++ b/docs/technical/【开发运维】Pingora独立网关试点-2026-06-11.md @@ -472,7 +472,7 @@ dev 根盘空间在安装后曾接近满盘;2026-06-17 进入 canary 前已清 4. 使用 `deploy/nginx/snippets/genarrative-pingora-canary.conf` 做 Nginx 前缀 canary;启用前先跑 `npm run check:nginx-pingora-canary` 和 `npm run check:pingora-canary-docker`,有 Nginx 或 Docker 的目标环境分别强制跑 `node scripts/check-nginx-pingora-canary.mjs --require-nginx` 与 `node scripts/check-pingora-canary-docker.mjs --require-docker --pull`,其中 Docker handoff 会自动对账临时 Nginx 与 Pingora access log。启用后跑 `npm run check:pingora-canary-live`,再用 current release 随包 access log parity 脚本对账目标机 Nginx 与 Pingora access log。canary live 的 base URL、prefix、Host、额外 path 和 timeout 不能包含换行或 NUL;canary live timeout 和 access log `since-lines` 必须是正整数,非法值直接失败。 5. 前缀 canary 通过后,再使用 `deploy/nginx/snippets/genarrative-pingora-realpath-canary.conf` 做真实路径 canary;它必须作为独立本机 `server` include 到 Nginx `http` 上下文,不能 include 到生产 `443` server 内。启用后跑 `node scripts/check-pingora-canary-live.mjs --realpath --base-url http://127.0.0.1:18083 --host <域名>`,再用 current release 随包 access log parity 脚本传 `--realpath --nginx-log-file /var/log/nginx/genarrative-pingora-realpath-canary.access.log` 对账 `/api/creation-entry/config`、`/v1/identity` 和 `/assets/app.js` 等真实路径。 6. 目标机 canary include 后必须跑正式切换聚合门禁:源码 checkout / CI / 构建环境执行默认全量命令 `node scripts/check-pingora-release-readiness.mjs --require-docker --pull-docker --require-nginx --require-live --live-base-url http://127.0.0.1 --live-host <域名> --live-nginx-access-log /var/log/nginx/genarrative.access.log --live-pingora-access-log /var/log/genarrative/pingora-gateway.access.log`;目标机 current release 执行 `/opt/genarrative/current/scripts/check-pingora-release-readiness.mjs --release-runtime-only --require-live --live-base-url http://127.0.0.1 --live-host <域名> --live-nginx-access-log /var/log/nginx/genarrative.access.log --live-pingora-access-log /var/log/genarrative/pingora-gateway.access.log`。启用真实路径 canary 时,两种命令都追加 `--require-realpath-live --realpath-live-base-url http://127.0.0.1:18083 --realpath-live-host <域名> --realpath-live-nginx-access-log /var/log/nginx/genarrative-pingora-realpath-canary.access.log --realpath-live-pingora-access-log /var/log/genarrative/pingora-gateway.access.log`。缺少 Host 会直接失败,避免 live canary 误测默认 vhost;live smoke 后还会按 `request_id` 对账 Nginx 与 Pingora access log,缺少同一请求的 Pingora 日志、状态码、方法或 path 漂移都会失败。 -7. 如需评估 Pingora 直连公网入口,必须显式配置 `TLS_LISTEN`、证书、私钥和 `HTTP_REDIRECT_LISTEN`;Certbot 证书先用随包 `scripts/deploy/pingora-tls-cert-sync.mjs` 同步到 `/etc/genarrative/pingora-tls/<域名>/`,不要直接 chmod Let’s Encrypt live/archive 原路径;绑定 `80/443` 时还必须人工启用 `genarrative-pingora-gateway-direct-entry.conf` drop-in 授予 `CAP_NET_BIND_SERVICE`。随后用 `npm run check:pingora-gateway-smoke` 覆盖 TLS / HTTP/2 ALPN / redirect / WSS subscribe;目标机必须先跑 `npm run check:pingora-direct-preflight -- --env-file /etc/genarrative/pingora-gateway.env --require-live-env --systemd-cat --check-cert-readable --check-service-env-file --check-service-user-cert-readable --check-service-binary-executable --check-ports-free`,再跑 `npm run check:pingora-direct-live` 或 release readiness 的 `--require-direct`,且 `--require-direct` 必须带 direct HTTPS base URL、direct HTTP base URL、正式域名 Host/SNI、redirect Location host、Pingora access log 文件、health patrol env 文件、direct preflight env 文件、systemd drop-in 生效检查、service EnvironmentFile 一致性检查、当前用户和 systemd 服务用户证书可读检查、service 二进制可执行检查、端口释放检查和显式 SpacetimeDB 数据库名,并会拒绝 `--skip-wss`。direct live 会用生成的 `request_id` 反查 Pingora access log;缺少对应日志、method 漂移、path 漂移或 status 漂移都算直连门禁失败。direct preflight 会拒绝开启网关保护但未确认共享保护层的 `GENARRATIVE_PINGORA_GATEWAY_INSTANCE_COUNT>1` 配置;`--env-file`、`--systemd-service`、服务用户和 env 中的 listen / cert / key 值都不能包含换行或 NUL,执行 `systemctl cat` 或 `sudo -u test -r ` 前还会复核子命令参数,避免污染参数进入目标机预检命令;direct live 的 URL、Host、probe token、额外 path、数据库名、access log 路径、timeout 和布尔 env 也不能包含换行或 NUL,且会在发起请求前失败;direct live timeout 必须是正整数,直连相关布尔 env 只接受 `true/false`、`1/0`、`yes/no`、`on/off` 或空值,非法值直接失败。证书申请与续期仍由 Certbot / 外部自动化承担,网关只读取现有文件。 +7. 如需评估 Pingora 直连公网入口,必须显式配置 `TLS_LISTEN`、证书、私钥和 `HTTP_REDIRECT_LISTEN`;Certbot 证书先用随包 `scripts/deploy/pingora-tls-cert-sync.mjs` 同步到 `/etc/genarrative/pingora-tls/<域名>/`,不要直接 chmod Let’s Encrypt live/archive 原路径;绑定 `80/443` 时还必须人工启用 `genarrative-pingora-gateway-direct-entry.conf` drop-in 授予 `CAP_NET_BIND_SERVICE`。随后用 `npm run check:pingora-gateway-smoke` 覆盖 TLS / HTTP/2 ALPN / redirect / WSS subscribe;目标机必须先跑 `npm run check:pingora-direct-preflight -- --env-file /etc/genarrative/pingora-gateway.env --require-live-env --systemd-cat --check-cert-readable --check-service-env-file --check-service-user-cert-readable --check-service-binary-executable --check-ports-free`,再跑 `npm run check:pingora-direct-live` 或 release readiness 的 `--require-direct`,且 `--require-direct` 必须带 direct HTTPS base URL、direct HTTP base URL、正式域名 Host/SNI、redirect Location host、Pingora access log 文件、health patrol env 文件、direct preflight env 文件、systemd drop-in 生效检查、service EnvironmentFile 一致性检查、当前用户和 systemd 服务用户证书可读检查、service 二进制可执行检查、端口释放检查和显式 SpacetimeDB 数据库名,并会拒绝 `--skip-wss`。高端口 rehearsal 使用 `https://127.0.0.1:<高端口>` 打入但期望 HTTP redirect Location 指向正式域名默认 HTTPS 入口时,额外传 `--direct-redirect-base-url https://<域名>`;`--direct-redirect-host` 仍必须保留,用于 runbook Host 一致性约束。direct live 会用生成的 `request_id` 反查 Pingora access log;缺少对应日志、method 漂移、path 漂移或 status 漂移都算直连门禁失败。direct preflight 会拒绝开启网关保护但未确认共享保护层的 `GENARRATIVE_PINGORA_GATEWAY_INSTANCE_COUNT>1` 配置;`--env-file`、`--systemd-service`、服务用户和 env 中的 listen / cert / key 值都不能包含换行或 NUL,执行 `systemctl cat` 或 `sudo -u test -r ` 前还会复核子命令参数,避免污染参数进入目标机预检命令;direct live 的 URL、Host、redirect base URL、probe token、额外 path、数据库名、access log 路径、timeout 和布尔 env 也不能包含换行或 NUL,且会在发起请求前失败;direct live timeout 必须是正整数,直连相关布尔 env 只接受 `true/false`、`1/0`、`yes/no`、`on/off` 或空值,非法值直接失败。证书申请与续期仍由 Certbot / 外部自动化承担,网关只读取现有文件。 8. 正式直连 runbook 的启用前基础门禁和启用后 `--require-direct` 复核必须调用 `/opt/genarrative/current/scripts/check-pingora-release-readiness.mjs --release-runtime-only`。该脚本、`scripts/check-pingora-canary-live.mjs` 和 `deploy/nginx/` 必须进入生产 API release、Jenkins API Build 归档、Jenkins API Deploy 复制清单和目标机 current release;缺失时部署应 fail-fast,切换窗口不能依赖源码 checkout 或 Jenkins workspace。 9. direct preflight / direct live timeout 必须是正整数;直连相关布尔 env 和 Pingora gateway env 里的 `TRUST_X_FORWARDED_FOR` / `TRUSTED_FRONT_PROXY_CONFIRMED` 只接受 `true/false`、`1/0`、`yes/no`、`on/off` 或空值,非法值直接失败。 10. Pingora 正式化口径固定为 gzip-only;Brotli 继续由 Nginx / 前置代理承担,直连 Pingora 不以 Brotli parity 作为切换门禁。如需多实例,开启网关接流保护时必须先引入共享限流 / 共享并发保护层并设置 `GENARRATIVE_PINGORA_GATEWAY_SHARED_PROTECTION_CONFIRMED=true`,否则保持 `GENARRATIVE_PINGORA_GATEWAY_INSTANCE_COUNT=1`;关闭网关保护的多实例方案必须明确由前置 Nginx / LB 承担全局限流。 diff --git a/scripts/check-pingora-direct-enable.mjs b/scripts/check-pingora-direct-enable.mjs index 050a70a94..bc3f50d08 100644 --- a/scripts/check-pingora-direct-enable.mjs +++ b/scripts/check-pingora-direct-enable.mjs @@ -558,6 +558,11 @@ function assertScriptShape() { '--apply 必须同时提供 --direct-host', '启用脚本 apply 时必须强制正式域名 Host/SNI 参数。', ); + assertIncludes( + content, + '--direct-redirect-base-url', + '启用脚本必须支持 direct live redirect base URL 覆盖,用于高端口 rehearsal。', + ); assertIncludes( content, '--apply 必须同时提供 --direct-redirect-host', @@ -653,6 +658,11 @@ function assertScriptShape() { 'print_redacted_command node "${direct_live_args[@]}"', '启用脚本展示 direct live smoke 命令时必须隐藏 probe token。', ); + assertIncludes( + content, + 'direct_live_args+=(--redirect-base-url "${DIRECT_REDIRECT_BASE_URL}")', + '启用脚本必须把 direct redirect base URL 透传给 direct live smoke。', + ); assertIncludes( content, 'drop-in 目录不能是符号链接', diff --git a/scripts/check-pingora-direct-live-guard.mjs b/scripts/check-pingora-direct-live-guard.mjs index 1b2bcef2d..7984f2d38 100644 --- a/scripts/check-pingora-direct-live-guard.mjs +++ b/scripts/check-pingora-direct-live-guard.mjs @@ -50,6 +50,13 @@ function main() { '--redirect-host', 'example.com\nhidden', ]); + assertRejectsControlCharacter('--redirect-base-url', [ + '--https-base-url', + 'https://127.0.0.1', + '--redirect-base-url', + 'https://example.com\nhidden', + ]); + assertRejectsInvalidRedirectBaseUrl(); assertRejectsControlCharacter('--probe-token', [ '--https-base-url', 'https://127.0.0.1', @@ -87,6 +94,33 @@ function main() { ]); } +function assertRejectsInvalidRedirectBaseUrl() { + const result = spawnSync( + 'node', + [ + DIRECT_LIVE_SCRIPT, + '--https-base-url', + 'https://127.0.0.1', + '--redirect-base-url', + 'http://example.com', + ], + { + cwd: process.cwd(), + encoding: 'utf8', + env: process.env, + }, + ); + if ((result.status ?? 0) === 0) { + failures.push('--redirect-base-url 必须拒绝非 HTTPS base URL。'); + return; + } + assertIncludes( + `${result.stdout}\n${result.stderr}`, + '--redirect-base-url 必须使用 https://', + '--redirect-base-url 非 HTTPS 时必须给出明确错误。', + ); +} + function assertRejectsControlCharacter(label, args) { const result = spawnSync('node', [DIRECT_LIVE_SCRIPT, ...args], { cwd: process.cwd(), diff --git a/scripts/check-pingora-direct-live.mjs b/scripts/check-pingora-direct-live.mjs index b9a8f7659..1fb3c48fe 100644 --- a/scripts/check-pingora-direct-live.mjs +++ b/scripts/check-pingora-direct-live.mjs @@ -41,6 +41,8 @@ Options: --http-base-url Optional HTTP base URL that should 301 to HTTPS. --host Optional Host header for local --resolve style checks. --redirect-host Optional expected Location host for HTTP redirects. + --redirect-base-url + Optional expected HTTPS base URL for HTTP redirects. --probe-token Optional Pingora shadow probe token. --spacetime-database SpacetimeDB database for WSS subscribe smoke, default genarrative-prod. @@ -60,6 +62,7 @@ Environment aliases: GENARRATIVE_PINGORA_DIRECT_HTTP_BASE_URL GENARRATIVE_PINGORA_DIRECT_HOST GENARRATIVE_PINGORA_DIRECT_REDIRECT_HOST + GENARRATIVE_PINGORA_DIRECT_REDIRECT_BASE_URL GENARRATIVE_PINGORA_DIRECT_PROBE_TOKEN GENARRATIVE_PINGORA_DIRECT_SPACETIME_DATABASE GENARRATIVE_PINGORA_DIRECT_REQUIRE_WSS_UPGRADE @@ -77,6 +80,8 @@ function parseArgs(argv) { httpBaseUrl: process.env.GENARRATIVE_PINGORA_DIRECT_HTTP_BASE_URL || '', host: process.env.GENARRATIVE_PINGORA_DIRECT_HOST || '', redirectHost: process.env.GENARRATIVE_PINGORA_DIRECT_REDIRECT_HOST || '', + redirectBaseUrl: + process.env.GENARRATIVE_PINGORA_DIRECT_REDIRECT_BASE_URL || '', probeToken: process.env.GENARRATIVE_PINGORA_DIRECT_PROBE_TOKEN || '', spacetimeDatabase: process.env.GENARRATIVE_PINGORA_DIRECT_SPACETIME_DATABASE || @@ -133,6 +138,9 @@ function parseArgs(argv) { case '--redirect-host': result.redirectHost = requireValue(argv, ++index, arg); break; + case '--redirect-base-url': + result.redirectBaseUrl = requireValue(argv, ++index, arg); + break; case '--probe-token': result.probeToken = requireValue(argv, ++index, arg); break; @@ -197,6 +205,9 @@ function parseArgs(argv) { if (result.redirectHost) { validateHostOption(result.redirectHost, '--redirect-host'); } + if (result.redirectBaseUrl) { + validateHttpsBaseUrl(result.redirectBaseUrl, '--redirect-base-url'); + } if (result.probeToken) { validateNoControlCharacters(result.probeToken, '--probe-token'); } @@ -255,6 +266,26 @@ function validateHostOption(value, flag) { } } +function validateHttpsBaseUrl(value, flag) { + validateNoControlCharacters(value, flag); + const parsed = new URL(value); + if (parsed.protocol !== 'https:') { + throw new Error(`${flag} 必须使用 https://`); + } + if ( + !parsed.hostname || + parsed.pathname !== '/' || + parsed.search || + parsed.hash || + parsed.username || + parsed.password + ) { + throw new Error( + `${flag} 只能是 HTTPS base URL,不能包含路径、查询、片段或认证信息。`, + ); + } +} + function validateSafeAbsoluteFilePath(filePath, label) { validateNoControlCharacters(filePath, label); if (!nodePath.isAbsolute(filePath)) { @@ -1065,6 +1096,9 @@ function assertPingoraGatewayHeader(check, response) { } function directHttpsLocation(httpsBaseUrl, path) { + if (config.redirectBaseUrl) { + return joinUrl(config.redirectBaseUrl, path); + } if (!config.redirectHost) { return joinUrl(httpsBaseUrl, path); } diff --git a/scripts/check-pingora-release-readiness.mjs b/scripts/check-pingora-release-readiness.mjs index 4909e369d..bbdd1ee81 100644 --- a/scripts/check-pingora-release-readiness.mjs +++ b/scripts/check-pingora-release-readiness.mjs @@ -86,6 +86,8 @@ function parseArgs(argv) { directHost: process.env.GENARRATIVE_PINGORA_DIRECT_HOST || '', directRedirectHost: process.env.GENARRATIVE_PINGORA_DIRECT_REDIRECT_HOST || '', + directRedirectBaseUrl: + process.env.GENARRATIVE_PINGORA_DIRECT_REDIRECT_BASE_URL || '', directProbeToken: process.env.GENARRATIVE_PINGORA_DIRECT_PROBE_TOKEN || '', directPingoraAccessLog: process.env.GENARRATIVE_PINGORA_DIRECT_PINGORA_ACCESS_LOG || '', @@ -228,6 +230,9 @@ function parseArgs(argv) { case '--direct-redirect-host': result.directRedirectHost = requireValue(argv, ++index, arg); break; + case '--direct-redirect-base-url': + result.directRedirectBaseUrl = requireValue(argv, ++index, arg); + break; case '--direct-probe-token': result.directProbeToken = requireValue(argv, ++index, arg); break; @@ -412,6 +417,24 @@ function parseArgs(argv) { if (result.directRedirectHost) { validateHostOption(result.directRedirectHost, '--direct-redirect-host'); } + if (result.directRedirectBaseUrl) { + validateHttpUrl(result.directRedirectBaseUrl, '--direct-redirect-base-url'); + const parsed = new URL(result.directRedirectBaseUrl); + if (parsed.protocol !== 'https:') { + throw new Error('--direct-redirect-base-url 必须使用 https://。'); + } + if ( + parsed.pathname !== '/' || + parsed.search || + parsed.hash || + parsed.username || + parsed.password + ) { + throw new Error( + '--direct-redirect-base-url 只能是 HTTPS base URL,不能包含路径、查询、片段或认证信息。', + ); + } + } if (result.requireDirect && !result.directPingoraAccessLog) { throw new Error( '启用 --require-direct 时必须提供 --direct-pingora-access-log 或 GENARRATIVE_PINGORA_DIRECT_PINGORA_ACCESS_LOG', @@ -661,6 +684,8 @@ Options: --direct-http-base-url direct HTTP redirect base URL;也可用 GENARRATIVE_PINGORA_DIRECT_HTTP_BASE_URL。 --direct-host direct live Host header 和 TLS SNI;也可用 GENARRATIVE_PINGORA_DIRECT_HOST。--require-direct 时必须显式提供。 --direct-redirect-host direct redirect Location host;也可用 GENARRATIVE_PINGORA_DIRECT_REDIRECT_HOST。--require-direct 时必须显式提供,cutover runbook 中必须和 --direct-host 使用同一 hostname。 + --direct-redirect-base-url + 可选 direct redirect Location HTTPS base URL;用于高端口 rehearsal 访问 127.0.0.1:18443 但期望 Location 指向正式 HTTPS 入口。 --direct-probe-token direct live shadow probe token;也可用 GENARRATIVE_PINGORA_DIRECT_PROBE_TOKEN。 --direct-pingora-access-log direct live 后 Pingora access log 落盘校验路径;--require-direct 时必须显式提供。 @@ -971,6 +996,9 @@ function buildCutoverPlan(config) { config.directHost, '--direct-redirect-host', config.directRedirectHost, + ...(config.directRedirectBaseUrl + ? ['--direct-redirect-base-url', config.directRedirectBaseUrl] + : []), '--direct-spacetime-database', config.directSpacetimeDatabase, '--direct-pingora-access-log', @@ -1023,6 +1051,7 @@ function buildCutoverPlan(config) { `cutover-run-id=${config.cutoverRunId}`, `direct-host=${config.directHost}`, `direct-redirect-host=${config.directRedirectHost}`, + `direct-redirect-base-url=${config.directRedirectBaseUrl || '-'}`, `health-patrol-public-host=${config.directHost}`, `rollback-nginx-smoke-host=${config.rollbackNginxSmokeHost}`, `rollback-nginx-smoke-expect-body=${config.rollbackNginxSmokeExpectBody}`, @@ -1229,6 +1258,9 @@ function buildCutoverPlan(config) { config.directHost, '--direct-redirect-host', config.directRedirectHost, + ...(config.directRedirectBaseUrl + ? ['--direct-redirect-base-url', config.directRedirectBaseUrl] + : []), ...(config.directProbeToken ? ['--direct-probe-token', config.directProbeToken] : []), @@ -1485,6 +1517,9 @@ function readinessArgs(config, { includeDirect }) { config.directHost, '--direct-redirect-host', config.directRedirectHost, + ...(config.directRedirectBaseUrl + ? ['--direct-redirect-base-url', config.directRedirectBaseUrl] + : []), ...(config.directProbeToken ? ['--direct-probe-token', config.directProbeToken] : []), @@ -1718,6 +1753,9 @@ function buildSteps(config) { ...(config.directRedirectHost ? ['--redirect-host', config.directRedirectHost] : []), + ...(config.directRedirectBaseUrl + ? ['--redirect-base-url', config.directRedirectBaseUrl] + : []), ...(config.directProbeToken ? ['--probe-token', config.directProbeToken] : []), @@ -1910,6 +1948,9 @@ function appendTargetDirectSteps(steps, config, scriptPath) { ...(config.directRedirectHost ? ['--redirect-host', config.directRedirectHost] : []), + ...(config.directRedirectBaseUrl + ? ['--redirect-base-url', config.directRedirectBaseUrl] + : []), ...(config.directProbeToken ? ['--probe-token', config.directProbeToken] : []), diff --git a/scripts/check-production-ops-guardrails.mjs b/scripts/check-production-ops-guardrails.mjs index a3c75e041..4a0c65f98 100644 --- a/scripts/check-production-ops-guardrails.mjs +++ b/scripts/check-production-ops-guardrails.mjs @@ -2933,6 +2933,12 @@ const checks = [ reason: 'Pingora direct live smoke 环境示例必须暴露 HTTP redirect Location host。', }, + { + file: 'deploy/env/pingora-direct-live.env.example', + includes: 'GENARRATIVE_PINGORA_DIRECT_REDIRECT_BASE_URL', + reason: + 'Pingora direct live smoke 环境示例必须暴露高端口 rehearsal 的 redirect Location base URL 覆盖。', + }, { file: 'deploy/env/pingora-direct-live.env.example', includes: '只填域名或 host:port', @@ -3106,6 +3112,12 @@ const checks = [ reason: 'Pingora direct live smoke 必须提前拒绝非法 redirect Location host 参数。', }, + { + file: 'scripts/check-pingora-direct-live.mjs', + includes: 'validateHttpsBaseUrl(result.redirectBaseUrl,', + reason: + 'Pingora direct live smoke 必须提前拒绝非法 redirect Location base URL 参数。', + }, { file: 'scripts/check-pingora-direct-live.mjs', includes: 'parseRequiredPositiveInt', @@ -3638,6 +3650,12 @@ const checks = [ includes: '--apply 必须同时提供 --direct-redirect-host', reason: 'Pingora 直连启用 apply 后必须验证 redirect Location host。', }, + { + file: 'scripts/deploy/pingora-direct-enable.sh', + includes: '--direct-redirect-base-url', + reason: + 'Pingora 直连启用脚本必须支持把高端口 rehearsal 的 redirect base URL 透传给 direct live smoke。', + }, { file: 'scripts/deploy/pingora-direct-enable.sh', includes: '--apply 必须同时提供 --direct-spacetime-database', diff --git a/scripts/deploy/pingora-direct-enable.sh b/scripts/deploy/pingora-direct-enable.sh index 99ca08846..55bfa52ff 100644 --- a/scripts/deploy/pingora-direct-enable.sh +++ b/scripts/deploy/pingora-direct-enable.sh @@ -26,6 +26,7 @@ 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:-}" @@ -39,7 +40,7 @@ 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-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 ] @@ -211,6 +212,14 @@ while [[ $# -gt 0 ]]; do 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 @@ -297,6 +306,7 @@ 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}" @@ -732,6 +742,9 @@ run_direct_live_smoke_after_enable() { 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 @@ -905,5 +918,5 @@ cat <} --http-base-url ${DIRECT_HTTP_BASE_URL:-} --host ${DIRECT_HOST:-} --redirect-host ${DIRECT_REDIRECT_HOST:-} --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 +- node ${DIRECT_LIVE_SCRIPT} --https-base-url ${DIRECT_HTTPS_BASE_URL:-} --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 diff --git a/scripts/ops/pingora-cutover-evidence-bundle.mjs b/scripts/ops/pingora-cutover-evidence-bundle.mjs index 46765da95..c9cb176a8 100644 --- a/scripts/ops/pingora-cutover-evidence-bundle.mjs +++ b/scripts/ops/pingora-cutover-evidence-bundle.mjs @@ -442,6 +442,8 @@ function parseArgs(argv) { directHost: process.env.GENARRATIVE_PINGORA_DIRECT_HOST || '', directRedirectHost: process.env.GENARRATIVE_PINGORA_DIRECT_REDIRECT_HOST || '', + directRedirectBaseUrl: + process.env.GENARRATIVE_PINGORA_DIRECT_REDIRECT_BASE_URL || '', directProbeToken: process.env.GENARRATIVE_PINGORA_DIRECT_PROBE_TOKEN || '', directSpacetimeDatabase: process.env.GENARRATIVE_PINGORA_DIRECT_SPACETIME_DATABASE || '', @@ -521,6 +523,9 @@ function parseArgs(argv) { case '--direct-redirect-host': result.directRedirectHost = requireValue(argv, ++index, arg); break; + case '--direct-redirect-base-url': + result.directRedirectBaseUrl = requireValue(argv, ++index, arg); + break; case '--direct-probe-token': result.directProbeToken = requireValue(argv, ++index, arg); break; @@ -597,6 +602,7 @@ Options: --direct-http-base-url 透传给 direct live smoke 的 HTTP redirect / ACME 入口。 --direct-host 透传给 direct live smoke 的正式 Host/SNI。 --direct-redirect-host 透传给 direct live smoke 的 redirect Location host。 + --direct-redirect-base-url 可选,透传给 direct live smoke 的 redirect Location HTTPS base URL。 --direct-probe-token 可选,透传给 direct live smoke 的内部探针 token;证据中会脱敏。 --direct-spacetime-database 透传给 direct live smoke 的 SpacetimeDB 数据库名。 --direct-pingora-access-log @@ -681,6 +687,9 @@ function validateDirectLiveConfig(config) { throw new Error('--run-direct-live 必须提供 --direct-redirect-host。'); } validateHostOption(config.directRedirectHost, '--direct-redirect-host'); + if (config.directRedirectBaseUrl) { + validateHttpsBaseUrl(config.directRedirectBaseUrl, '--direct-redirect-base-url'); + } if (!config.directSpacetimeDatabase) { throw new Error('--run-direct-live 必须提供 --direct-spacetime-database。'); } @@ -807,6 +816,22 @@ function validateHttpsUrl(value, label) { } } +function validateHttpsBaseUrl(value, label) { + validateHttpsUrl(value, label); + const parsed = new URL(value); + if ( + parsed.pathname !== '/' || + parsed.search || + parsed.hash || + parsed.username || + parsed.password + ) { + throw new Error( + `${label} 只能是 HTTPS base URL,不能包含路径、查询、片段或认证信息。`, + ); + } +} + function validateHttpOnlyUrl(value, label) { validateHttpUrl(value, label); if (new URL(value).protocol !== 'http:') { @@ -961,6 +986,9 @@ function buildDirectLiveArgs(config) { config.directHost, '--redirect-host', config.directRedirectHost, + ...(config.directRedirectBaseUrl + ? ['--redirect-base-url', config.directRedirectBaseUrl] + : []), ...(config.directProbeToken ? ['--probe-token', config.directProbeToken] : []),