Files
Genarrative/scripts/check-pingora-direct-rollback.mjs
T
kdletters a7711d2dc3 引入Pingora独立网关正式化链路
新增 pingora-gateway 独立二进制 crate,覆盖路由、静态资源、压缩、接流保护、TLS 直连和访问日志能力。

新增 Nginx canary、realpath canary、direct preflight、direct live、direct enable 和 rollback 脚本。

新增 Pingora 切流证据包、命令证据、manifest 验真、根目录总审计和 release readiness 聚合门禁。

完善 API release、Jenkins、systemd、health patrol、生产部署和发布包自包含校验。

更新 Pingora 试点文档、Nginx README 与 Hermes 共享记忆。
2026-06-17 20:16:51 +08:00

2063 lines
58 KiB
JavaScript
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 node
import { spawnSync } from 'node:child_process';
import {
existsSync,
mkdirSync,
mkdtempSync,
readFileSync,
rmSync,
symlinkSync,
writeFileSync,
} from 'node:fs';
import { tmpdir } from 'node:os';
import path from 'node:path';
const ROLLBACK_SCRIPT = 'scripts/deploy/pingora-direct-rollback.sh';
const failures = [];
const tmpRoot = mkdtempSync(
path.join(tmpdir(), 'genarrative-pingora-rollback-'),
);
try {
main();
} finally {
rmSync(tmpRoot, { recursive: true, force: true });
}
if (failures.length > 0) {
console.error('[check:pingora-direct-rollback] FAILED');
for (const failure of failures) {
console.error(`- ${failure}`);
}
process.exit(1);
}
console.log('[check:pingora-direct-rollback] OK');
function main() {
assertScriptShape();
assertDryRunKeepsDropin();
assertDryRunMissingDropin();
assertRelativeDropinRejected();
assertRelativeServiceUnitPathRejected();
assertRejectsRelativeNginxBinaryPath();
assertRejectsRelativeCurlBinaryPath();
assertRejectsFilesystemRootPathsBeforeApply();
assertApplyRequiresReloadNginx();
assertApplyRequiresNginxSmokeUrl();
assertRejectsInvalidNginxSmokeUrl();
assertApplyRequiresHostForLoopbackSmokeUrl();
assertRejectsInvalidNginxSmokeHost();
assertRejectsRelativeHealthPatrolEnvFile();
assertRejectsRelativeHealthPatrolEnvCheckScript();
assertRejectsConflictingHealthPatrolPublicHostOptions();
assertRejectsInvalidHealthPatrolExpectedPublicBaseUrl();
assertRejectsInvalidHealthPatrolExpectedPublicHost();
assertRejectsIncompletePingoraShadowProbe();
assertRejectsInvalidPingoraShadowProbeUrl();
assertRejectsControlCharacterInputsBeforeApply();
assertApplyRejectsSymlinkDropinDirectoryBeforeNginxTest();
assertApplyRejectsSymlinkDropinFileBeforeRemoval();
assertApplyRejectsDirectoryDropinTargetBeforeNginxTest();
assertApplyStopsBeforeRemovingDropinWhenNginxTestFails();
assertApplyFailsWhenNginxSmokeFails();
assertApplyFailsWhenNginxSmokeBodyMismatches();
assertApplyFailsWhenHealthPatrolEnvStillDirect();
assertApplyFailsWhenHealthPatrolPublicBaseUrlDrifts();
assertApplySupportsExpectedHealthPatrolPublicHost();
assertApplyFailsWhenSystemdExecStartDiffers();
assertApplyFailsWhenPingoraShadowProbeBodyIsWrong();
}
function assertScriptShape() {
const content = readFileSync(ROLLBACK_SCRIPT, 'utf8');
assertIncludes(content, 'APPLY="false"', '回退脚本必须默认 dry-run。');
assertIncludes(
content,
'--apply',
'回退脚本必须显式要求 --apply 才修改系统。',
);
assertIncludes(
content,
'systemctl daemon-reload',
'移除 drop-in 后必须 reload systemd。',
);
assertIncludes(
content,
'restart "${SERVICE_NAME}"',
'回退脚本必须重启 Pingora 服务。',
);
assertIncludes(
content,
'--reload-nginx',
'回退脚本必须保留显式 Nginx reload。',
);
assertIncludes(
content,
'--nginx-binary',
'回退脚本必须支持覆盖 Nginx 可执行文件,便于测试和特殊环境。',
);
assertIncludes(
content,
'--nginx-smoke-url',
'回退脚本必须支持 Nginx 入口 smoke URL。',
);
assertIncludes(
content,
'--nginx-smoke-host',
'回退脚本必须支持 Nginx 入口 smoke Host。',
);
assertIncludes(
content,
'--nginx-smoke-expect-body',
'回退脚本必须支持 Nginx smoke 响应体证据片段。',
);
assertIncludes(
content,
'--nginx-smoke-url 指向本机地址时必须同时提供 --nginx-smoke-host',
'回退脚本 apply 用本机 smoke URL 时必须强制正式 Host。',
);
assertIncludes(
content,
'只能是 host 或 host:port',
'回退脚本必须拒绝非法 Nginx smoke Host。',
);
assertIncludes(
content,
'--curl-binary',
'回退脚本必须支持覆盖 curl 可执行文件,便于测试和特殊环境。',
);
assertIncludes(
content,
'--health-patrol-env-file',
'回退脚本必须支持回退后 health patrol env 复核。',
);
assertIncludes(
content,
'--health-patrol-env-check-script',
'回退脚本必须支持覆盖 health patrol env 复核脚本,便于 release layout 与测试。',
);
assertIncludes(
content,
'--pingora-shadow-probe-url',
'回退脚本必须支持回退后 Pingora shadow probe URL。',
);
assertIncludes(
content,
'--pingora-shadow-probe-token',
'回退脚本必须支持回退后 Pingora shadow probe token。',
);
assertIncludes(
content,
'--expected-gateway-mode',
'回退脚本必须调用 health patrol env 复核脚本并要求 nginx 模式。',
);
assertIncludes(
content,
'--require-empty-public-host',
'回退脚本必须要求回退后 health patrol public Host 为空。',
);
assertIncludes(
content,
'--health-patrol-expected-public-base-url',
'回退脚本必须支持校验回退后 health patrol public base URL。',
);
assertIncludes(
content,
'--health-patrol-expected-public-host',
'回退脚本必须支持校验回退后 health patrol public Host。',
);
assertIncludes(
content,
'--health-patrol-require-empty-public-host',
'回退脚本必须支持显式要求回退后 health patrol public Host 为空。',
);
assertIncludes(
content,
'dry-run--apply 后会复核 health patrol env 已切回 nginx,且 public base URL / Host 已恢复为预期值。',
'回退脚本 dry-run 必须说明 health patrol env 回退后复核。',
);
assertIncludes(
content,
'dry-run--apply 后会验证 Pingora shadow 探针仍为 gateway=pingora-shadow。',
'回退脚本 dry-run 必须说明 Pingora shadow probe 回退后复核。',
);
assertIncludes(
content,
'--apply 必须同时提供 --reload-nginx',
'回退脚本 apply 时必须强制 reload Nginx。',
);
assertIncludes(
content,
'--apply 必须同时提供 --nginx-smoke-url',
'回退脚本 apply 时必须强制 Nginx 入口 smoke。',
);
assertIncludes(
content,
'${NGINX_BINARY} -t',
'回退脚本 reload Nginx 前必须先跑 nginx -t。',
);
assertIncludes(
content,
'drop-in 目录不能是符号链接',
'回退脚本 apply 前必须拒绝符号链接 drop-in 目录。',
);
assertIncludes(
content,
'drop-in 目标不能是符号链接',
'回退脚本 apply 前必须拒绝符号链接 drop-in 目标。',
);
assertIncludes(
content,
'drop-in 目标已存在但不是普通文件',
'回退脚本 apply 前必须拒绝非普通文件 drop-in 目标。',
);
assertIncludes(
content,
'direct-entry.conf',
'回退脚本必须默认指向 direct-entry drop-in。',
);
assertIncludes(
content,
'--no-postcheck',
'回退脚本必须支持少数异常场景跳过 post-check。',
);
assertIncludes(
content,
'--service-unit-path',
'回退脚本必须支持覆盖主 service 模板路径,便于 current release 随包执行。',
);
assertIncludes(
content,
'systemctl cat ${SERVICE_NAME}',
'回退脚本必须在 apply 后核验 systemd 最终配置。',
);
assertIncludes(
content,
'systemctl show ${SERVICE_NAME} --property=ExecStart --value --no-pager',
'回退脚本必须在 apply 后核验 Pingora ExecStart 指向 current release。',
);
assertIncludes(
content,
'主 service 模板中的',
'回退脚本 ExecStart 核验必须以随包主 service 模板为真相源。',
);
assertIncludes(
content,
'仍显示 AmbientCapabilities=CAP_NET_BIND_SERVICE',
'回退脚本必须拒绝 AmbientCapabilities 残留。',
);
assertIncludes(
content,
'仍显示 CapabilityBoundingSet=CAP_NET_BIND_SERVICE',
'回退脚本必须拒绝 CapabilityBoundingSet 残留。',
);
assertIncludes(
content,
'systemctl is-active ${NGINX_SERVICE}',
'回退脚本必须在 reload Nginx 后核验 Nginx 状态。',
);
assertIncludes(
content,
'${CURL_BINARY} --fail',
'回退脚本必须在 reload Nginx 后执行 Nginx smoke。',
);
assertIncludes(
content,
'nginx smoke evidence matched expected body fragment',
'回退脚本必须在 Nginx smoke 响应体匹配时打印证据摘要。',
);
assertIncludes(
content,
'Nginx smoke 响应缺少预期片段',
'回退脚本必须拒绝缺少预期响应体片段的 Nginx smoke。',
);
assertIncludes(
content,
'Nginx reload 后状态不是 active',
'回退脚本必须拒绝 Nginx reload 后非 active 状态。',
);
assertIncludes(
content,
'reject_control_characters',
'回退脚本必须统一拒绝换行或 NUL 参数。',
);
assertIncludes(
content,
'不能包含换行或 NUL 字符',
'回退脚本拒绝控制字符时必须给出明确错误。',
);
assertIncludes(
content,
'reject_filesystem_root_path',
'回退脚本必须统一拒绝文件系统根目录路径参数。',
);
assertIncludes(
content,
'不能是文件系统根目录',
'回退脚本拒绝文件系统根目录路径时必须给出明确错误。',
);
}
function assertDryRunKeepsDropin() {
const dropinPath = path.join(tmpRoot, 'direct-entry.conf');
writeFileSync(
dropinPath,
'[Service]\nAmbientCapabilities=CAP_NET_BIND_SERVICE\n',
'utf8',
);
const result = runRollback([
'--dropin-path',
dropinPath,
'--service',
'genarrative-pingora-gateway.service',
'--reload-nginx',
'--nginx-service',
'nginx.service',
'--nginx-smoke-url',
'http://127.0.0.1/healthz',
'--nginx-smoke-host',
'example.com',
'--nginx-smoke-expect-body',
'"ok":true',
'--no-status',
]);
assertStatus(result, 0, 'dry-run 应成功退出。');
assertIncludes(
result.stdout,
'apply=false',
'dry-run 输出必须明确 apply=false。',
);
assertIncludes(
result.stdout,
`+ rm -f ${dropinPath}`,
'dry-run 必须展示将移除 drop-in。',
);
assertIncludes(
result.stdout,
'+ systemctl daemon-reload',
'dry-run 必须展示 daemon-reload。',
);
assertIncludes(
result.stdout,
'+ systemctl restart genarrative-pingora-gateway.service',
'dry-run 必须展示重启 Pingora。',
);
assertIncludes(
result.stdout,
'+ nginx -t',
'dry-run 必须展示 reload Nginx 前的 nginx -t。',
);
assertIncludes(
result.stdout,
'dry-run--apply --reload-nginx 后会先执行 nginx -t,通过后再 reload。',
'dry-run 必须说明 apply reload 前会先跑 nginx -t。',
);
assertIncludes(
result.stdout,
'+ systemctl reload nginx.service',
'dry-run 必须展示可选 reload Nginx。',
);
assertIncludes(
result.stdout,
'+ systemctl is-active nginx.service',
'dry-run 必须展示 reload Nginx 后 active 核验。',
);
assertIncludes(
result.stdout,
'+ curl --fail --silent --show-error --max-time 5 -H Host: example.com http://127.0.0.1/healthz',
'dry-run 必须展示 Nginx 入口 smoke。',
);
assertIncludes(
result.stdout,
'dry-run--apply --reload-nginx 后会验证 Nginx smoke URL 可访问,且响应体包含预期片段。',
'dry-run 带响应体片段时必须说明会校验 Nginx smoke body。',
);
const healthPatrolEnvFile = path.join(tmpRoot, 'health-patrol.env');
const healthPatrolEnvCheckScript = path.join(
process.cwd(),
'scripts/check-production-health-patrol-env.mjs',
);
const healthPatrolResult = runRollback([
'--dropin-path',
dropinPath,
'--service',
'genarrative-pingora-gateway.service',
'--reload-nginx',
'--nginx-service',
'nginx.service',
'--nginx-smoke-url',
'http://127.0.0.1/healthz',
'--nginx-smoke-host',
'example.com',
'--health-patrol-env-file',
healthPatrolEnvFile,
'--health-patrol-env-check-script',
healthPatrolEnvCheckScript,
'--health-patrol-expected-public-base-url',
'https://nginx.example.com',
'--no-status',
]);
assertStatus(
healthPatrolResult,
0,
'带 health patrol env 的 dry-run 应成功退出。',
);
assertIncludes(
healthPatrolResult.stdout,
`+ node -- ${healthPatrolEnvCheckScript} --env-file ${healthPatrolEnvFile} --expected-gateway-mode nginx --expected-public-base-url https://nginx.example.com --require-empty-public-host`,
'dry-run 必须展示 health patrol env 回退后复核命令。',
);
assertIncludes(
healthPatrolResult.stdout,
'dry-run--apply 后会复核 health patrol env 已切回 nginx,且 public base URL / Host 已恢复为预期值。',
'dry-run 必须说明 apply 后会复核 health patrol env。',
);
const healthPatrolHostResult = runRollback([
'--dropin-path',
dropinPath,
'--service',
'genarrative-pingora-gateway.service',
'--reload-nginx',
'--nginx-service',
'nginx.service',
'--nginx-smoke-url',
'http://127.0.0.1/healthz',
'--nginx-smoke-host',
'example.com',
'--health-patrol-env-file',
healthPatrolEnvFile,
'--health-patrol-env-check-script',
healthPatrolEnvCheckScript,
'--health-patrol-expected-public-base-url',
'https://nginx.example.com',
'--health-patrol-expected-public-host',
'nginx.example.com',
'--no-status',
]);
assertStatus(
healthPatrolHostResult,
0,
'带显式 health patrol public Host 的 dry-run 应成功退出。',
);
assertIncludes(
healthPatrolHostResult.stdout,
`+ node -- ${healthPatrolEnvCheckScript} --env-file ${healthPatrolEnvFile} --expected-gateway-mode nginx --expected-public-base-url https://nginx.example.com --expected-public-host nginx.example.com`,
'dry-run 必须展示显式 public Host 的 health patrol env 回退后复核命令。',
);
const shadowProbeResult = runRollback([
'--dropin-path',
dropinPath,
'--service',
'genarrative-pingora-gateway.service',
'--reload-nginx',
'--nginx-service',
'nginx.service',
'--nginx-smoke-url',
'http://127.0.0.1/healthz',
'--nginx-smoke-host',
'example.com',
'--pingora-shadow-probe-url',
'http://127.0.0.1:18081/__genarrative_pingora/healthz',
'--pingora-shadow-probe-token',
'test-shadow-probe-token',
'--no-status',
]);
assertStatus(
shadowProbeResult,
0,
'带 Pingora shadow probe 的 dry-run 应成功退出。',
);
assertIncludes(
shadowProbeResult.stdout,
'+ curl --fail --silent --show-error --max-time 5 -H X-Genarrative-Pingora-Probe: <redacted> http://127.0.0.1:18081/__genarrative_pingora/healthz',
'dry-run 必须展示 Pingora shadow probe 命令且隐藏 token。',
);
if (shadowProbeResult.stdout.includes('test-shadow-probe-token')) {
failures.push('dry-run Pingora shadow probe 命令不应输出 probe token 原文。');
}
assertIncludes(
shadowProbeResult.stdout,
'dry-run--apply 后会验证 Pingora shadow 探针仍为 gateway=pingora-shadow。',
'dry-run 必须说明 apply 后会复核 Pingora shadow probe。',
);
assertIncludes(
result.stdout,
'+ systemctl cat genarrative-pingora-gateway.service',
'dry-run 必须展示回退后 systemd drop-in 移除核验。',
);
assertIncludes(
result.stdout,
'+ systemctl show genarrative-pingora-gateway.service --property=ExecStart --value --no-pager',
'dry-run 必须展示回退后 ExecStart 指向核验。',
);
assertIncludes(
result.stdout,
'dry-run--apply 后会校验 systemd 最终配置不再包含 CAP_NET_BIND_SERVICE。',
'dry-run 必须说明 apply 后会核验 capability 已移除。',
);
assertIncludes(
result.stdout,
'dry-run--apply 后会核验 systemd ExecStart 指向主 service 模板中的 /opt/genarrative/current/pingora-gateway。',
'dry-run 必须说明 apply 后会核验 Pingora ExecStart 指向 current release。',
);
assertIncludes(
result.stdout,
'dry-run--apply --reload-nginx 后会 reload Nginx 并校验 service 仍为 active。',
'dry-run 必须说明 apply reload 后会核验 Nginx active。',
);
if (!existsSync(dropinPath)) {
failures.push('dry-run 不应删除临时 drop-in 文件。');
}
}
function assertDryRunMissingDropin() {
const dropinPath = path.join(tmpRoot, 'missing-direct-entry.conf');
const result = runRollback(['--dropin-path', dropinPath, '--no-status']);
assertStatus(result, 0, 'drop-in 不存在时 dry-run 应视为已回退。');
assertIncludes(
result.stdout,
'direct-entry drop-in 不存在,视为已移除',
'drop-in 不存在时必须给出清晰提示。',
);
assertIncludes(
result.stdout,
'+ systemctl daemon-reload',
'drop-in 不存在时仍应 reload systemd。',
);
assertIncludes(
result.stdout,
'+ systemctl restart genarrative-pingora-gateway.service',
'drop-in 不存在时仍应重启 Pingora 进入 shadow 口径。',
);
}
function assertRelativeDropinRejected() {
const result = runRollback([
'--dropin-path',
'relative/direct-entry.conf',
'--no-status',
]);
if (result.status === 0) {
failures.push('相对路径 drop-in 必须被拒绝。');
}
assertIncludes(
result.stderr,
'--dropin-path 必须是绝对路径',
'相对路径负例必须说明需要绝对路径。',
);
}
function assertRelativeServiceUnitPathRejected() {
const dropinPath = path.join(tmpRoot, 'relative-service-unit-direct-entry.conf');
writeFileSync(
dropinPath,
'[Service]\nAmbientCapabilities=CAP_NET_BIND_SERVICE\n',
'utf8',
);
const result = runRollback([
'--service-unit-path',
'relative/genarrative-pingora-gateway.service',
'--dropin-path',
dropinPath,
'--no-status',
]);
if (result.status === 0) {
failures.push('相对路径 --service-unit-path 必须被拒绝。');
}
assertIncludes(
result.stderr,
'--service-unit-path 必须是绝对路径',
'service unit 相对路径负例必须说明需要绝对路径。',
);
if (!existsSync(dropinPath)) {
failures.push('service unit 相对路径负例不应删除 drop-in。');
}
}
function assertRejectsRelativeNginxBinaryPath() {
const dropinPath = path.join(tmpRoot, 'relative-nginx-binary-direct-entry.conf');
writeFileSync(
dropinPath,
'[Service]\nAmbientCapabilities=CAP_NET_BIND_SERVICE\n',
'utf8',
);
const result = runRollback([
'--apply',
'--reload-nginx',
'--dropin-path',
dropinPath,
'--nginx-binary',
'./nginx',
'--nginx-smoke-url',
'http://127.0.0.1/healthz',
'--nginx-smoke-host',
'example.com',
'--no-status',
]);
if (result.status === 0) {
failures.push('相对路径 --nginx-binary 必须被拒绝。');
}
assertIncludes(
result.stderr,
'--nginx-binary 包含路径分隔符时必须是绝对路径',
'Nginx 二进制相对路径负例必须说明需要绝对路径。',
);
if (!existsSync(dropinPath)) {
failures.push('Nginx 二进制相对路径负例不应删除 drop-in。');
}
}
function assertRejectsRelativeCurlBinaryPath() {
const dropinPath = path.join(tmpRoot, 'relative-curl-binary-direct-entry.conf');
writeFileSync(
dropinPath,
'[Service]\nAmbientCapabilities=CAP_NET_BIND_SERVICE\n',
'utf8',
);
const result = runRollback([
'--apply',
'--reload-nginx',
'--dropin-path',
dropinPath,
'--curl-binary',
'tools/curl',
'--nginx-smoke-url',
'http://127.0.0.1/healthz',
'--nginx-smoke-host',
'example.com',
'--no-status',
]);
if (result.status === 0) {
failures.push('相对路径 --curl-binary 必须被拒绝。');
}
assertIncludes(
result.stderr,
'--curl-binary 包含路径分隔符时必须是绝对路径',
'curl 二进制相对路径负例必须说明需要绝对路径。',
);
if (!existsSync(dropinPath)) {
failures.push('curl 二进制相对路径负例不应删除 drop-in。');
}
}
function assertRejectsFilesystemRootPathsBeforeApply() {
const nginxBinary = path.join(tmpRoot, 'fake-nginx-should-not-run-root.sh');
const dropinPath = path.join(tmpRoot, 'root-path-direct-entry.conf');
writeFileSync(
dropinPath,
'[Service]\nAmbientCapabilities=CAP_NET_BIND_SERVICE\n',
'utf8',
);
writeFileSync(
nginxBinary,
'#!/usr/bin/env bash\necho "nginx should not run for root path" >&2\nexit 99\n',
'utf8',
);
chmodExecutable(nginxBinary);
const rootDropinResult = runRollback([
'--apply',
'--reload-nginx',
'--dropin-path',
'/',
'--nginx-binary',
nginxBinary,
'--nginx-smoke-url',
'http://127.0.0.1/healthz',
'--nginx-smoke-host',
'example.com',
'--no-status',
]);
if (rootDropinResult.status === 0) {
failures.push('文件系统根目录 --dropin-path 必须被拒绝。');
}
assertIncludes(
rootDropinResult.stderr,
'--dropin-path 不能是文件系统根目录',
'文件系统根目录 drop-in 路径负例必须给出明确错误。',
);
if (rootDropinResult.stderr.includes('nginx should not run for root path')) {
failures.push('文件系统根目录 drop-in 被拒绝后不应继续执行 nginx -t。');
}
const rootHealthPatrolEnvResult = runRollback([
'--apply',
'--reload-nginx',
'--dropin-path',
dropinPath,
'--nginx-binary',
nginxBinary,
'--nginx-smoke-url',
'http://127.0.0.1/healthz',
'--nginx-smoke-host',
'example.com',
'--health-patrol-env-file',
'/',
'--no-status',
]);
if (rootHealthPatrolEnvResult.status === 0) {
failures.push('文件系统根目录 --health-patrol-env-file 必须被拒绝。');
}
assertIncludes(
rootHealthPatrolEnvResult.stderr,
'--health-patrol-env-file 不能是文件系统根目录',
'文件系统根目录 health patrol env 路径负例必须给出明确错误。',
);
if (rootHealthPatrolEnvResult.stderr.includes('nginx should not run for root path')) {
failures.push('文件系统根目录 health patrol env 被拒绝后不应继续执行 nginx -t。');
}
if (!existsSync(dropinPath)) {
failures.push('文件系统根目录 health patrol env 被拒绝后不应删除 drop-in。');
}
}
function assertApplyRequiresReloadNginx() {
const dropinPath = path.join(tmpRoot, 'apply-direct-entry.conf');
writeFileSync(
dropinPath,
'[Service]\nAmbientCapabilities=CAP_NET_BIND_SERVICE\n',
'utf8',
);
const result = runRollback([
'--apply',
'--dropin-path',
dropinPath,
'--no-status',
]);
if (result.status === 0) {
failures.push('--apply 缺少 --reload-nginx 必须被拒绝。');
}
assertIncludes(
result.stderr,
'--apply 必须同时提供 --reload-nginx',
'apply 负例必须说明不能跳过 Nginx reload。',
);
if (!existsSync(dropinPath)) {
failures.push('--apply 缺少 --reload-nginx 时不应删除 drop-in。');
}
}
function assertApplyRequiresNginxSmokeUrl() {
const dropinPath = path.join(tmpRoot, 'apply-smoke-direct-entry.conf');
writeFileSync(
dropinPath,
'[Service]\nAmbientCapabilities=CAP_NET_BIND_SERVICE\n',
'utf8',
);
const result = runRollback([
'--apply',
'--reload-nginx',
'--dropin-path',
dropinPath,
'--no-status',
]);
if (result.status === 0) {
failures.push('--apply 缺少 --nginx-smoke-url 必须被拒绝。');
}
assertIncludes(
result.stderr,
'--apply 必须同时提供 --nginx-smoke-url',
'apply 负例必须说明不能跳过 Nginx smoke。',
);
if (!existsSync(dropinPath)) {
failures.push('--apply 缺少 --nginx-smoke-url 时不应删除 drop-in。');
}
}
function assertRejectsInvalidNginxSmokeUrl() {
const dropinPath = path.join(tmpRoot, 'invalid-smoke-url-direct-entry.conf');
writeFileSync(
dropinPath,
'[Service]\nAmbientCapabilities=CAP_NET_BIND_SERVICE\n',
'utf8',
);
const result = runRollback([
'--apply',
'--reload-nginx',
'--dropin-path',
dropinPath,
'--nginx-smoke-url',
'127.0.0.1/healthz',
'--nginx-smoke-host',
'example.com',
'--no-status',
]);
if (result.status === 0) {
failures.push('非法 --nginx-smoke-url 必须被拒绝。');
}
assertIncludes(
result.stderr,
'--nginx-smoke-url 必须是 http(s) URL',
'非法 Nginx smoke URL 负例必须给出明确错误。',
);
if (!existsSync(dropinPath)) {
failures.push('非法 Nginx smoke URL 时不应删除 drop-in。');
}
}
function assertApplyRequiresHostForLoopbackSmokeUrl() {
const dropinPath = path.join(tmpRoot, 'apply-smoke-host-direct-entry.conf');
writeFileSync(
dropinPath,
'[Service]\nAmbientCapabilities=CAP_NET_BIND_SERVICE\n',
'utf8',
);
const result = runRollback([
'--apply',
'--reload-nginx',
'--dropin-path',
dropinPath,
'--nginx-smoke-url',
'http://127.0.0.1/healthz',
'--no-status',
]);
if (result.status === 0) {
failures.push(
'--apply 使用 loopback --nginx-smoke-url 时缺少 --nginx-smoke-host 必须被拒绝。',
);
}
assertIncludes(
result.stderr,
'--nginx-smoke-url 指向本机地址时必须同时提供 --nginx-smoke-host',
'loopback Nginx smoke 负例必须说明需要正式 Host。',
);
if (!existsSync(dropinPath)) {
failures.push('缺少 loopback smoke Host 时不应删除 drop-in。');
}
}
function assertRejectsInvalidNginxSmokeHost() {
const dropinPath = path.join(tmpRoot, 'invalid-smoke-host-direct-entry.conf');
writeFileSync(
dropinPath,
'[Service]\nAmbientCapabilities=CAP_NET_BIND_SERVICE\n',
'utf8',
);
const result = runRollback([
'--apply',
'--reload-nginx',
'--dropin-path',
dropinPath,
'--nginx-smoke-url',
'http://127.0.0.1/healthz',
'--nginx-smoke-host',
'https://example.com/path',
'--no-status',
]);
if (result.status === 0) {
failures.push('非法 --nginx-smoke-host 必须被拒绝。');
}
assertIncludes(
result.stderr,
'--nginx-smoke-host 只能是 host 或 host:port',
'非法 Nginx smoke Host 负例必须给出明确错误。',
);
if (!existsSync(dropinPath)) {
failures.push('非法 smoke Host 时不应删除 drop-in。');
}
}
function assertRejectsRelativeHealthPatrolEnvFile() {
const dropinPath = path.join(
tmpRoot,
'relative-health-patrol-env-direct-entry.conf',
);
writeFileSync(
dropinPath,
'[Service]\nAmbientCapabilities=CAP_NET_BIND_SERVICE\n',
'utf8',
);
const result = runRollback([
'--apply',
'--reload-nginx',
'--dropin-path',
dropinPath,
'--nginx-smoke-url',
'http://127.0.0.1/healthz',
'--nginx-smoke-host',
'example.com',
'--health-patrol-env-file',
'relative/health-patrol.env',
'--no-status',
]);
if (result.status === 0) {
failures.push('相对路径 --health-patrol-env-file 必须被拒绝。');
}
assertIncludes(
result.stderr,
'--health-patrol-env-file 必须是绝对路径',
'health patrol env 相对路径负例必须说明需要绝对路径。',
);
if (!existsSync(dropinPath)) {
failures.push('health patrol env 相对路径负例不应删除 drop-in。');
}
}
function assertRejectsRelativeHealthPatrolEnvCheckScript() {
const dropinPath = path.join(
tmpRoot,
'relative-health-patrol-check-direct-entry.conf',
);
writeFileSync(
dropinPath,
'[Service]\nAmbientCapabilities=CAP_NET_BIND_SERVICE\n',
'utf8',
);
const result = runRollback([
'--apply',
'--reload-nginx',
'--dropin-path',
dropinPath,
'--nginx-smoke-url',
'http://127.0.0.1/healthz',
'--nginx-smoke-host',
'example.com',
'--health-patrol-env-file',
path.join(tmpRoot, 'health-patrol.env'),
'--health-patrol-env-check-script',
'relative/check-production-health-patrol-env.mjs',
'--no-status',
]);
if (result.status === 0) {
failures.push('相对路径 --health-patrol-env-check-script 必须被拒绝。');
}
assertIncludes(
result.stderr,
'--health-patrol-env-check-script 必须是绝对路径',
'health patrol env check script 相对路径负例必须说明需要绝对路径。',
);
if (!existsSync(dropinPath)) {
failures.push('health patrol check script 相对路径负例不应删除 drop-in。');
}
}
function assertRejectsConflictingHealthPatrolPublicHostOptions() {
const dropinPath = path.join(
tmpRoot,
'conflicting-health-patrol-host-direct-entry.conf',
);
writeFileSync(
dropinPath,
'[Service]\nAmbientCapabilities=CAP_NET_BIND_SERVICE\n',
'utf8',
);
const result = runRollback([
'--apply',
'--reload-nginx',
'--dropin-path',
dropinPath,
'--nginx-smoke-url',
'http://127.0.0.1/healthz',
'--nginx-smoke-host',
'example.com',
'--health-patrol-env-file',
path.join(tmpRoot, 'health-patrol.env'),
'--health-patrol-expected-public-host',
'nginx.example.com',
'--health-patrol-require-empty-public-host',
'--no-status',
]);
if (result.status === 0) {
failures.push(
'显式 public Host 与 require empty public Host 同时提供时必须被拒绝。',
);
}
assertIncludes(
result.stderr,
'--health-patrol-expected-public-host 和 --health-patrol-require-empty-public-host 不能同时使用',
'health patrol public Host 冲突参数必须给出明确错误。',
);
if (!existsSync(dropinPath)) {
failures.push('health patrol public Host 冲突参数时不应删除 drop-in。');
}
}
function assertRejectsInvalidHealthPatrolExpectedPublicBaseUrl() {
const dropinPath = path.join(
tmpRoot,
'invalid-health-patrol-public-base-url-direct-entry.conf',
);
writeFileSync(
dropinPath,
'[Service]\nAmbientCapabilities=CAP_NET_BIND_SERVICE\n',
'utf8',
);
const result = runRollback([
'--apply',
'--reload-nginx',
'--dropin-path',
dropinPath,
'--nginx-smoke-url',
'http://127.0.0.1/healthz',
'--nginx-smoke-host',
'example.com',
'--health-patrol-env-file',
path.join(tmpRoot, 'health-patrol.env'),
'--health-patrol-expected-public-base-url',
'127.0.0.1',
'--no-status',
]);
if (result.status === 0) {
failures.push('非法 health patrol expected public base URL 必须被拒绝。');
}
assertIncludes(
result.stderr,
'--health-patrol-expected-public-base-url 必须是 http(s) URL',
'非法 health patrol expected public base URL 负例必须给出明确错误。',
);
if (!existsSync(dropinPath)) {
failures.push(
'非法 health patrol expected public base URL 时不应删除 drop-in。',
);
}
}
function assertRejectsInvalidHealthPatrolExpectedPublicHost() {
const dropinPath = path.join(
tmpRoot,
'invalid-health-patrol-public-host-direct-entry.conf',
);
writeFileSync(
dropinPath,
'[Service]\nAmbientCapabilities=CAP_NET_BIND_SERVICE\n',
'utf8',
);
const result = runRollback([
'--apply',
'--reload-nginx',
'--dropin-path',
dropinPath,
'--nginx-smoke-url',
'http://127.0.0.1/healthz',
'--nginx-smoke-host',
'example.com',
'--health-patrol-env-file',
path.join(tmpRoot, 'health-patrol.env'),
'--health-patrol-expected-public-host',
'https://nginx.example.com/path',
'--no-status',
]);
if (result.status === 0) {
failures.push('非法 health patrol expected public Host 必须被拒绝。');
}
assertIncludes(
result.stderr,
'--health-patrol-expected-public-host 只能是 host 或 host:port',
'非法 health patrol expected public Host 负例必须给出明确错误。',
);
if (!existsSync(dropinPath)) {
failures.push(
'非法 health patrol expected public Host 时不应删除 drop-in。',
);
}
}
function assertRejectsIncompletePingoraShadowProbe() {
const dropinPath = path.join(
tmpRoot,
'incomplete-shadow-probe-direct-entry.conf',
);
writeFileSync(
dropinPath,
'[Service]\nAmbientCapabilities=CAP_NET_BIND_SERVICE\n',
'utf8',
);
const missingToken = runRollback([
'--apply',
'--reload-nginx',
'--dropin-path',
dropinPath,
'--nginx-smoke-url',
'http://127.0.0.1/healthz',
'--nginx-smoke-host',
'example.com',
'--pingora-shadow-probe-url',
'http://127.0.0.1:18081/__genarrative_pingora/healthz',
'--no-status',
]);
if (missingToken.status === 0) {
failures.push('shadow probe URL 缺少 token 必须被拒绝。');
}
assertIncludes(
missingToken.stderr,
'--pingora-shadow-probe-url 必须同时提供 --pingora-shadow-probe-token',
'shadow probe URL 缺少 token 负例必须给出明确错误。',
);
const missingUrl = runRollback([
'--apply',
'--reload-nginx',
'--dropin-path',
dropinPath,
'--nginx-smoke-url',
'http://127.0.0.1/healthz',
'--nginx-smoke-host',
'example.com',
'--pingora-shadow-probe-token',
'test-shadow-probe-token',
'--no-status',
]);
if (missingUrl.status === 0) {
failures.push('shadow probe token 缺少 URL 必须被拒绝。');
}
assertIncludes(
missingUrl.stderr,
'--pingora-shadow-probe-token 必须同时提供 --pingora-shadow-probe-url',
'shadow probe token 缺少 URL 负例必须给出明确错误。',
);
if (!existsSync(dropinPath)) {
failures.push('shadow probe 参数不完整时不应删除 drop-in。');
}
}
function assertRejectsInvalidPingoraShadowProbeUrl() {
const dropinPath = path.join(
tmpRoot,
'invalid-shadow-probe-url-direct-entry.conf',
);
writeFileSync(
dropinPath,
'[Service]\nAmbientCapabilities=CAP_NET_BIND_SERVICE\n',
'utf8',
);
const result = runRollback([
'--apply',
'--reload-nginx',
'--dropin-path',
dropinPath,
'--nginx-smoke-url',
'http://127.0.0.1/healthz',
'--nginx-smoke-host',
'example.com',
'--pingora-shadow-probe-url',
'127.0.0.1:18081/__genarrative_pingora/healthz',
'--pingora-shadow-probe-token',
'test-shadow-probe-token',
'--no-status',
]);
if (result.status === 0) {
failures.push('非法 --pingora-shadow-probe-url 必须被拒绝。');
}
assertIncludes(
result.stderr,
'--pingora-shadow-probe-url 必须是 http(s) URL',
'非法 shadow probe URL 负例必须给出明确错误。',
);
if (!existsSync(dropinPath)) {
failures.push('非法 shadow probe URL 时不应删除 drop-in。');
}
}
function assertRejectsControlCharacterInputsBeforeApply() {
const dropinPath = path.join(tmpRoot, 'control-character-direct-entry.conf');
const nginxBinary = path.join(tmpRoot, 'fake-nginx-should-not-run-control.sh');
writeFileSync(
dropinPath,
'[Service]\nAmbientCapabilities=CAP_NET_BIND_SERVICE\n',
'utf8',
);
writeFileSync(
nginxBinary,
'#!/usr/bin/env bash\necho "nginx should not run" >&2\nexit 99\n',
'utf8',
);
chmodExecutable(nginxBinary);
const result = runRollback([
'--apply',
'--reload-nginx',
'--dropin-path',
dropinPath,
'--nginx-binary',
nginxBinary,
'--nginx-smoke-url',
'http://127.0.0.1/healthz',
'--nginx-smoke-host',
'example.com\ninjected.example.com',
'--no-status',
]);
if (result.status === 0) {
failures.push('带换行的 Nginx smoke Host 必须在 rollback apply 修改系统前被拒绝。');
}
assertIncludes(
result.stderr,
'--nginx-smoke-host 不能包含换行或 NUL 字符',
'Nginx smoke Host 控制字符负例必须给出明确错误。',
);
if (result.stderr.includes('nginx should not run')) {
failures.push('带换行的 Nginx smoke Host 被拒绝后不应继续执行 nginx -t。');
}
if (!existsSync(dropinPath)) {
failures.push('带换行的 Nginx smoke Host 被拒绝后不应删除 drop-in。');
}
}
function assertApplyRejectsSymlinkDropinDirectoryBeforeNginxTest() {
const realDropinDir = path.join(tmpRoot, 'rollback-real-service.d');
const symlinkDropinDir = path.join(tmpRoot, 'rollback-linked-service.d');
const nginxBinary = path.join(tmpRoot, 'fake-nginx-should-not-run-dir.sh');
mkdirSync(realDropinDir, { recursive: true });
symlinkSync(realDropinDir, symlinkDropinDir);
writeFileSync(
nginxBinary,
'#!/usr/bin/env bash\necho "nginx should not run" >&2\nexit 99\n',
'utf8',
);
chmodExecutable(nginxBinary);
const result = runRollback([
'--apply',
'--reload-nginx',
'--dropin-path',
path.join(symlinkDropinDir, 'direct-entry.conf'),
'--nginx-binary',
nginxBinary,
'--nginx-smoke-url',
'http://127.0.0.1/healthz',
'--nginx-smoke-host',
'example.com',
'--no-status',
]);
if (result.status === 0) {
failures.push('drop-in 目录是符号链接时 rollback apply 必须失败。');
}
assertIncludes(
result.stderr,
'drop-in 目录不能是符号链接',
'rollback drop-in 符号链接目录负例必须给出明确错误。',
);
if (result.stderr.includes('nginx should not run')) {
failures.push('drop-in 目录是符号链接时不应继续执行 nginx -t。');
}
if (existsSync(path.join(realDropinDir, 'direct-entry.conf'))) {
failures.push('drop-in 目录是符号链接时不应删除或写入真实目标目录。');
}
}
function assertApplyRejectsSymlinkDropinFileBeforeRemoval() {
const dropinDir = path.join(tmpRoot, 'rollback-symlink-file-service.d');
const realDropinFile = path.join(tmpRoot, 'rollback-real-direct-entry.conf');
const symlinkDropinFile = path.join(dropinDir, 'direct-entry.conf');
const nginxBinary = path.join(tmpRoot, 'fake-nginx-should-not-run-file.sh');
mkdirSync(dropinDir, { recursive: true });
writeFileSync(realDropinFile, 'original rollback dropin\n', 'utf8');
symlinkSync(realDropinFile, symlinkDropinFile);
writeFileSync(
nginxBinary,
'#!/usr/bin/env bash\necho "nginx should not run" >&2\nexit 99\n',
'utf8',
);
chmodExecutable(nginxBinary);
const result = runRollback([
'--apply',
'--reload-nginx',
'--dropin-path',
symlinkDropinFile,
'--nginx-binary',
nginxBinary,
'--nginx-smoke-url',
'http://127.0.0.1/healthz',
'--nginx-smoke-host',
'example.com',
'--no-status',
]);
if (result.status === 0) {
failures.push('drop-in 目标是符号链接时 rollback apply 必须失败。');
}
assertIncludes(
result.stderr,
'drop-in 目标不能是符号链接',
'rollback drop-in 符号链接目标负例必须给出明确错误。',
);
if (result.stderr.includes('nginx should not run')) {
failures.push('drop-in 目标是符号链接时不应继续执行 nginx -t。');
}
const realContent = readFileSync(realDropinFile, 'utf8');
if (realContent !== 'original rollback dropin\n') {
failures.push('drop-in 目标是符号链接时不应改写真实目标文件。');
}
}
function assertApplyRejectsDirectoryDropinTargetBeforeNginxTest() {
const dropinDir = path.join(tmpRoot, 'rollback-directory-target-service.d');
const dropinPath = path.join(dropinDir, 'direct-entry.conf');
const nginxBinary = path.join(tmpRoot, 'fake-nginx-should-not-run-target.sh');
mkdirSync(dropinPath, { recursive: true });
writeFileSync(
nginxBinary,
'#!/usr/bin/env bash\necho "nginx should not run" >&2\nexit 99\n',
'utf8',
);
chmodExecutable(nginxBinary);
const result = runRollback([
'--apply',
'--reload-nginx',
'--dropin-path',
dropinPath,
'--nginx-binary',
nginxBinary,
'--nginx-smoke-url',
'http://127.0.0.1/healthz',
'--nginx-smoke-host',
'example.com',
'--no-status',
]);
if (result.status === 0) {
failures.push('drop-in 目标是目录时 rollback apply 必须失败。');
}
assertIncludes(
result.stderr,
'drop-in 目标已存在但不是普通文件',
'rollback drop-in 目录目标负例必须给出明确错误。',
);
if (result.stderr.includes('nginx should not run')) {
failures.push('drop-in 目标是目录时不应继续执行 nginx -t。');
}
if (!existsSync(dropinPath)) {
failures.push('drop-in 目标是目录时不应删除该目录。');
}
}
function assertApplyStopsBeforeRemovingDropinWhenNginxTestFails() {
const dropinPath = path.join(tmpRoot, 'nginx-test-fail-direct-entry.conf');
const nginxBinary = path.join(tmpRoot, 'fake-nginx-fail.sh');
writeFileSync(
dropinPath,
'[Service]\nAmbientCapabilities=CAP_NET_BIND_SERVICE\n',
'utf8',
);
writeFileSync(
nginxBinary,
'#!/usr/bin/env bash\necho "fake nginx -t failed" >&2\nexit 1\n',
'utf8',
);
chmodExecutable(nginxBinary);
const result = runRollback([
'--apply',
'--reload-nginx',
'--dropin-path',
dropinPath,
'--nginx-binary',
nginxBinary,
'--nginx-smoke-url',
'http://127.0.0.1/healthz',
'--nginx-smoke-host',
'example.com',
'--no-status',
]);
if (result.status === 0) {
failures.push('nginx -t 失败时 rollback apply 必须失败。');
}
assertIncludes(
result.stderr,
'fake nginx -t failed',
'nginx -t 失败时必须暴露 Nginx 检查错误。',
);
if (!existsSync(dropinPath)) {
failures.push('nginx -t 失败时不应删除 drop-in。');
}
}
function assertApplyFailsWhenNginxSmokeFails() {
const dropinPath = path.join(tmpRoot, 'nginx-smoke-fail-direct-entry.conf');
const nginxBinary = path.join(tmpRoot, 'fake-nginx-ok.sh');
const systemctlBinary = path.join(tmpRoot, 'systemctl');
const curlBinary = path.join(tmpRoot, 'fake-curl-fail.sh');
writeFileSync(
dropinPath,
'[Service]\nAmbientCapabilities=CAP_NET_BIND_SERVICE\n',
'utf8',
);
writeFileSync(nginxBinary, '#!/usr/bin/env bash\nexit 0\n', 'utf8');
writeFileSync(
systemctlBinary,
[
'#!/usr/bin/env bash',
'if [[ "$1" == "cat" ]]; then',
' echo "[Service]"',
' exit 0',
'fi',
'if [[ "$1" == "show" ]]; then',
' echo "{ path=/opt/genarrative/current/pingora-gateway ; argv[]=/opt/genarrative/current/pingora-gateway ; }"',
' exit 0',
'fi',
'if [[ "$1" == "is-active" ]]; then',
' echo active',
' exit 0',
'fi',
'exit 0',
'',
].join('\n'),
'utf8',
);
writeFileSync(
curlBinary,
'#!/usr/bin/env bash\necho "fake nginx smoke failed" >&2\nexit 22\n',
'utf8',
);
chmodExecutable(nginxBinary);
chmodExecutable(systemctlBinary);
chmodExecutable(curlBinary);
const result = runRollback(
[
'--apply',
'--reload-nginx',
'--dropin-path',
dropinPath,
'--nginx-binary',
nginxBinary,
'--curl-binary',
curlBinary,
'--nginx-smoke-url',
'http://127.0.0.1/healthz',
'--nginx-smoke-host',
'example.com',
'--no-status',
],
{ PATH: `${tmpRoot}:${process.env.PATH || ''}` },
);
if (result.status === 0) {
failures.push('Nginx smoke 失败时 rollback apply 必须失败。');
}
assertIncludes(
result.stderr,
'fake nginx smoke failed',
'Nginx smoke 失败时必须暴露 curl 错误。',
);
}
function assertApplyFailsWhenNginxSmokeBodyMismatches() {
const dropinPath = path.join(
tmpRoot,
'nginx-smoke-body-mismatch-direct-entry.conf',
);
const nginxBinary = path.join(tmpRoot, 'fake-nginx-body-ok.sh');
const systemctlBinary = path.join(tmpRoot, 'systemctl');
const curlBinary = path.join(tmpRoot, 'fake-curl-body-mismatch.sh');
writeFileSync(
dropinPath,
'[Service]\nAmbientCapabilities=CAP_NET_BIND_SERVICE\n',
'utf8',
);
writeFileSync(nginxBinary, '#!/usr/bin/env bash\nexit 0\n', 'utf8');
writeFileSync(
systemctlBinary,
[
'#!/usr/bin/env bash',
'if [[ "$1" == "cat" ]]; then',
' echo "[Service]"',
' exit 0',
'fi',
'if [[ "$1" == "show" ]]; then',
' echo "{ path=/opt/genarrative/current/pingora-gateway ; argv[]=/opt/genarrative/current/pingora-gateway ; }"',
' exit 0',
'fi',
'if [[ "$1" == "is-active" ]]; then',
' echo active',
' exit 0',
'fi',
'exit 0',
'',
].join('\n'),
'utf8',
);
writeFileSync(
curlBinary,
'#!/usr/bin/env bash\necho \'{"ok":false,"service":"unexpected"}\'\n',
'utf8',
);
chmodExecutable(nginxBinary);
chmodExecutable(systemctlBinary);
chmodExecutable(curlBinary);
const result = runRollback(
[
'--apply',
'--reload-nginx',
'--dropin-path',
dropinPath,
'--nginx-binary',
nginxBinary,
'--curl-binary',
curlBinary,
'--nginx-smoke-url',
'http://127.0.0.1/healthz',
'--nginx-smoke-host',
'example.com',
'--nginx-smoke-expect-body',
'"ok":true',
'--no-status',
],
{ PATH: `${tmpRoot}:${process.env.PATH || ''}` },
);
if (result.status === 0) {
failures.push('Nginx smoke 响应体不匹配时 rollback apply 必须失败。');
}
assertIncludes(
result.stderr,
'Nginx smoke 响应缺少预期片段: "ok":true',
'Nginx smoke 响应体不匹配时必须暴露明确错误。',
);
assertIncludes(
result.stderr,
'Nginx smoke 响应前 500 字符',
'Nginx smoke 响应体不匹配时必须输出截断响应便于排障。',
);
}
function assertApplyFailsWhenHealthPatrolEnvStillDirect() {
const dropinPath = path.join(
tmpRoot,
'health-patrol-direct-leftover-direct-entry.conf',
);
const nginxBinary = path.join(tmpRoot, 'fake-nginx-health-ok.sh');
const systemctlBinary = path.join(tmpRoot, 'systemctl');
const curlBinary = path.join(tmpRoot, 'fake-curl-health-ok.sh');
const healthPatrolEnvFile = path.join(
tmpRoot,
'health-patrol-direct-leftover.env',
);
const healthPatrolEnvCheckScript = path.join(
process.cwd(),
'scripts/check-production-health-patrol-env.mjs',
);
writeFileSync(
dropinPath,
'[Service]\nAmbientCapabilities=CAP_NET_BIND_SERVICE\n',
'utf8',
);
writeFileSync(nginxBinary, '#!/usr/bin/env bash\nexit 0\n', 'utf8');
writeFileSync(
systemctlBinary,
[
'#!/usr/bin/env bash',
'if [[ "$1" == "cat" ]]; then',
' echo "[Service]"',
' exit 0',
'fi',
'if [[ "$1" == "show" ]]; then',
' echo "{ path=/opt/genarrative/current/pingora-gateway ; argv[]=/opt/genarrative/current/pingora-gateway ; }"',
' exit 0',
'fi',
'if [[ "$1" == "is-active" ]]; then',
' echo active',
' exit 0',
'fi',
'exit 0',
'',
].join('\n'),
'utf8',
);
writeFileSync(
curlBinary,
'#!/usr/bin/env bash\necho \'{"ok":true,"service":"genarrative-api-server"}\'\n',
'utf8',
);
writeFileSync(
healthPatrolEnvFile,
[
'GENARRATIVE_HEALTH_PATROL_GATEWAY_MODE=pingora-direct',
'GENARRATIVE_HEALTH_PATROL_PUBLIC_BASE_URL=https://127.0.0.1',
'GENARRATIVE_HEALTH_PATROL_PUBLIC_HOST=example.com',
'',
].join('\n'),
'utf8',
);
chmodExecutable(nginxBinary);
chmodExecutable(systemctlBinary);
chmodExecutable(curlBinary);
const result = runRollback(
[
'--apply',
'--reload-nginx',
'--dropin-path',
dropinPath,
'--nginx-binary',
nginxBinary,
'--curl-binary',
curlBinary,
'--nginx-smoke-url',
'http://127.0.0.1/healthz',
'--nginx-smoke-host',
'example.com',
'--health-patrol-env-file',
healthPatrolEnvFile,
'--health-patrol-env-check-script',
healthPatrolEnvCheckScript,
'--no-status',
],
{ PATH: `${tmpRoot}:${process.env.PATH || ''}` },
);
if (result.status === 0) {
failures.push(
'health patrol env 仍停留在 pingora-direct 时 rollback apply 必须失败。',
);
}
assertIncludes(
result.stderr,
'GENARRATIVE_HEALTH_PATROL_GATEWAY_MODE 应为 nginx,实际 pingora-direct',
'health patrol env 模式错误时必须暴露明确错误。',
);
assertIncludes(
result.stderr,
'GENARRATIVE_HEALTH_PATROL_PUBLIC_HOST 应为空,实际 example.com',
'health patrol env Host 残留时必须暴露明确错误。',
);
}
function assertApplyFailsWhenHealthPatrolPublicBaseUrlDrifts() {
const dropinPath = path.join(
tmpRoot,
'health-patrol-base-url-drift-direct-entry.conf',
);
const nginxBinary = path.join(tmpRoot, 'fake-nginx-base-url-ok.sh');
const systemctlBinary = path.join(tmpRoot, 'systemctl');
const curlBinary = path.join(tmpRoot, 'fake-curl-base-url-ok.sh');
const healthPatrolEnvFile = path.join(
tmpRoot,
'health-patrol-base-url-drift.env',
);
const healthPatrolEnvCheckScript = path.join(
process.cwd(),
'scripts/check-production-health-patrol-env.mjs',
);
writeFileSync(
dropinPath,
'[Service]\nAmbientCapabilities=CAP_NET_BIND_SERVICE\n',
'utf8',
);
writeFileSync(nginxBinary, '#!/usr/bin/env bash\nexit 0\n', 'utf8');
writeFileSync(
systemctlBinary,
[
'#!/usr/bin/env bash',
'if [[ "$1" == "cat" ]]; then',
' echo "[Service]"',
' exit 0',
'fi',
'if [[ "$1" == "show" ]]; then',
' echo "{ path=/opt/genarrative/current/pingora-gateway ; argv[]=/opt/genarrative/current/pingora-gateway ; }"',
' exit 0',
'fi',
'if [[ "$1" == "is-active" ]]; then',
' echo active',
' exit 0',
'fi',
'exit 0',
'',
].join('\n'),
'utf8',
);
writeFileSync(
curlBinary,
'#!/usr/bin/env bash\necho \'{"ok":true,"service":"genarrative-api-server"}\'\n',
'utf8',
);
writeFileSync(
healthPatrolEnvFile,
[
'GENARRATIVE_HEALTH_PATROL_GATEWAY_MODE=nginx',
'GENARRATIVE_HEALTH_PATROL_PUBLIC_BASE_URL=https://wrong.example.com',
'GENARRATIVE_HEALTH_PATROL_PUBLIC_HOST=',
'',
].join('\n'),
'utf8',
);
chmodExecutable(nginxBinary);
chmodExecutable(systemctlBinary);
chmodExecutable(curlBinary);
const result = runRollback(
[
'--apply',
'--reload-nginx',
'--dropin-path',
dropinPath,
'--nginx-binary',
nginxBinary,
'--curl-binary',
curlBinary,
'--nginx-smoke-url',
'http://127.0.0.1/healthz',
'--nginx-smoke-host',
'example.com',
'--health-patrol-env-file',
healthPatrolEnvFile,
'--health-patrol-env-check-script',
healthPatrolEnvCheckScript,
'--health-patrol-expected-public-base-url',
'https://nginx.example.com',
'--no-status',
],
{ PATH: `${tmpRoot}:${process.env.PATH || ''}` },
);
if (result.status === 0) {
failures.push(
'health patrol public base URL 漂移时 rollback apply 必须失败。',
);
}
assertIncludes(
result.stderr,
'GENARRATIVE_HEALTH_PATROL_PUBLIC_BASE_URL 应为 https://nginx.example.com,实际 https://wrong.example.com',
'health patrol public base URL 漂移时必须暴露明确错误。',
);
}
function assertApplySupportsExpectedHealthPatrolPublicHost() {
const dropinPath = path.join(
tmpRoot,
'health-patrol-expected-host-direct-entry.conf',
);
const nginxBinary = path.join(tmpRoot, 'fake-nginx-expected-host-ok.sh');
const systemctlBinary = path.join(tmpRoot, 'systemctl');
const curlBinary = path.join(tmpRoot, 'fake-curl-expected-host-ok.sh');
const healthPatrolEnvFile = path.join(
tmpRoot,
'health-patrol-expected-host.env',
);
const healthPatrolEnvCheckScript = path.join(
process.cwd(),
'scripts/check-production-health-patrol-env.mjs',
);
writeFileSync(
dropinPath,
'[Service]\nAmbientCapabilities=CAP_NET_BIND_SERVICE\n',
'utf8',
);
writeFileSync(nginxBinary, '#!/usr/bin/env bash\nexit 0\n', 'utf8');
writeFileSync(
systemctlBinary,
[
'#!/usr/bin/env bash',
'if [[ "$1" == "cat" ]]; then',
' echo "[Service]"',
' exit 0',
'fi',
'if [[ "$1" == "show" ]]; then',
' echo "{ path=/opt/genarrative/current/pingora-gateway ; argv[]=/opt/genarrative/current/pingora-gateway ; }"',
' exit 0',
'fi',
'if [[ "$1" == "is-active" ]]; then',
' echo active',
' exit 0',
'fi',
'exit 0',
'',
].join('\n'),
'utf8',
);
writeFileSync(
curlBinary,
'#!/usr/bin/env bash\necho \'{"ok":true,"service":"genarrative-api-server"}\'\n',
'utf8',
);
writeFileSync(
healthPatrolEnvFile,
[
'GENARRATIVE_HEALTH_PATROL_GATEWAY_MODE=nginx',
'GENARRATIVE_HEALTH_PATROL_PUBLIC_BASE_URL=https://nginx.example.com',
'GENARRATIVE_HEALTH_PATROL_PUBLIC_HOST=nginx.example.com',
'',
].join('\n'),
'utf8',
);
chmodExecutable(nginxBinary);
chmodExecutable(systemctlBinary);
chmodExecutable(curlBinary);
const result = runRollback(
[
'--apply',
'--reload-nginx',
'--dropin-path',
dropinPath,
'--nginx-binary',
nginxBinary,
'--curl-binary',
curlBinary,
'--nginx-smoke-url',
'http://127.0.0.1/healthz',
'--nginx-smoke-host',
'example.com',
'--health-patrol-env-file',
healthPatrolEnvFile,
'--health-patrol-env-check-script',
healthPatrolEnvCheckScript,
'--health-patrol-expected-public-base-url',
'https://nginx.example.com',
'--health-patrol-expected-public-host',
'nginx.example.com',
'--nginx-smoke-expect-body',
'"ok":true',
'--no-status',
],
{ PATH: `${tmpRoot}:${process.env.PATH || ''}` },
);
assertStatus(
result,
0,
'health patrol public Host 与切换前记录一致时 rollback apply 应成功。',
);
assertIncludes(
result.stdout,
'--expected-public-host nginx.example.com',
'rollback apply 必须把显式 health patrol public Host 传给复核脚本。',
);
assertIncludes(
result.stdout,
'[check:production-health-patrol-env] OK',
'rollback apply 应执行 health patrol env 复核并通过。',
);
assertIncludes(
result.stdout,
'nginx smoke evidence matched expected body fragment.',
'rollback apply 应在 Nginx smoke body 匹配后输出证据摘要。',
);
}
function assertApplyFailsWhenSystemdExecStartDiffers() {
const dropinPath = path.join(
tmpRoot,
'systemd-exec-mismatch-direct-entry.conf',
);
const serviceUnitPath = path.join(
tmpRoot,
'systemd-exec-mismatch.service',
);
const nginxBinary = path.join(tmpRoot, 'fake-nginx-exec-ok.sh');
const systemctlBinary = path.join(tmpRoot, 'systemctl');
const curlBinary = path.join(tmpRoot, 'fake-curl-exec-ok.sh');
writeFileSync(
dropinPath,
'[Service]\nAmbientCapabilities=CAP_NET_BIND_SERVICE\n',
'utf8',
);
writeFileSync(
serviceUnitPath,
[
'[Service]',
'ExecStart=/opt/genarrative/current/pingora-gateway',
'',
].join('\n'),
'utf8',
);
writeFileSync(nginxBinary, '#!/usr/bin/env bash\nexit 0\n', 'utf8');
writeFileSync(
systemctlBinary,
[
'#!/usr/bin/env bash',
'if [[ "$1" == "cat" ]]; then',
' echo "[Service]"',
' exit 0',
'fi',
'if [[ "$1" == "show" ]]; then',
' echo "{ path=/opt/genarrative/old-release/pingora-gateway ; argv[]=/opt/genarrative/old-release/pingora-gateway ; }"',
' exit 0',
'fi',
'if [[ "$1" == "is-active" ]]; then',
' echo active',
' exit 0',
'fi',
'exit 0',
'',
].join('\n'),
'utf8',
);
writeFileSync(curlBinary, '#!/usr/bin/env bash\nexit 0\n', 'utf8');
chmodExecutable(nginxBinary);
chmodExecutable(systemctlBinary);
chmodExecutable(curlBinary);
const result = runRollback(
[
'--apply',
'--reload-nginx',
'--service-unit-path',
serviceUnitPath,
'--dropin-path',
dropinPath,
'--nginx-binary',
nginxBinary,
'--curl-binary',
curlBinary,
'--nginx-smoke-url',
'http://127.0.0.1/healthz',
'--nginx-smoke-host',
'example.com',
'--no-status',
],
{ PATH: `${tmpRoot}:${process.env.PATH || ''}` },
);
if (result.status === 0) {
failures.push(
'systemctl show ExecStart 指向旧 release 时 rollback apply 必须失败。',
);
}
assertIncludes(
result.stderr,
'systemctl show ExecStart 未指向主 service 模板中的 /opt/genarrative/current/pingora-gateway',
'systemd 最终 ExecStart 指向旧 release 时必须给出明确错误。',
);
}
function assertApplyFailsWhenPingoraShadowProbeBodyIsWrong() {
const dropinPath = path.join(
tmpRoot,
'shadow-probe-body-wrong-direct-entry.conf',
);
const nginxBinary = path.join(tmpRoot, 'fake-nginx-shadow-ok.sh');
const systemctlBinary = path.join(tmpRoot, 'systemctl');
const curlBinary = path.join(tmpRoot, 'fake-curl-shadow-body-wrong.sh');
writeFileSync(
dropinPath,
'[Service]\nAmbientCapabilities=CAP_NET_BIND_SERVICE\n',
'utf8',
);
writeFileSync(nginxBinary, '#!/usr/bin/env bash\nexit 0\n', 'utf8');
writeFileSync(
systemctlBinary,
[
'#!/usr/bin/env bash',
'if [[ "$1" == "cat" ]]; then',
' echo "[Service]"',
' exit 0',
'fi',
'if [[ "$1" == "show" ]]; then',
' echo "{ path=/opt/genarrative/current/pingora-gateway ; argv[]=/opt/genarrative/current/pingora-gateway ; }"',
' exit 0',
'fi',
'if [[ "$1" == "is-active" ]]; then',
' echo active',
' exit 0',
'fi',
'exit 0',
'',
].join('\n'),
'utf8',
);
writeFileSync(
curlBinary,
[
'#!/usr/bin/env bash',
'last_arg="${@: -1}"',
'if [[ "${last_arg}" == *"__genarrative_pingora/healthz"* ]]; then',
' echo \'{"ok":true,"gateway":"not-pingora"}\'',
' exit 0',
'fi',
'exit 0',
'',
].join('\n'),
'utf8',
);
chmodExecutable(nginxBinary);
chmodExecutable(systemctlBinary);
chmodExecutable(curlBinary);
const result = runRollback(
[
'--apply',
'--reload-nginx',
'--dropin-path',
dropinPath,
'--nginx-binary',
nginxBinary,
'--curl-binary',
curlBinary,
'--nginx-smoke-url',
'http://127.0.0.1/healthz',
'--nginx-smoke-host',
'example.com',
'--pingora-shadow-probe-url',
'http://127.0.0.1:18081/__genarrative_pingora/healthz',
'--pingora-shadow-probe-token',
'test-shadow-probe-token',
'--no-status',
],
{ PATH: `${tmpRoot}:${process.env.PATH || ''}` },
);
if (result.status === 0) {
failures.push('Pingora shadow probe body 错误时 rollback apply 必须失败。');
}
assertIncludes(
result.stderr,
'Pingora shadow 探针响应不是预期 JSON',
'Pingora shadow probe body 错误时必须暴露明确错误。',
);
}
function runRollback(args, env = {}) {
return spawnSync('bash', [ROLLBACK_SCRIPT, ...args], {
cwd: process.cwd(),
encoding: 'utf8',
env: {
...process.env,
...env,
},
});
}
function chmodExecutable(filePath) {
spawnSync('chmod', ['0755', filePath], {
cwd: process.cwd(),
encoding: 'utf8',
});
}
function assertStatus(result, expected, reason) {
const actual = result.status ?? 0;
if (actual !== expected) {
failures.push(
`${reason} 预期退出码 ${expected},实际 ${actual}。\nstdout:\n${result.stdout}\nstderr:\n${result.stderr}`,
);
}
}
function assertIncludes(content, needle, reason) {
if (!content.includes(needle)) {
failures.push(`${reason} 缺少: ${needle}`);
}
}