75b85ee5a9
direct live 支持独立 redirect base URL 预期 release readiness 和切换证据链透传 redirect base URL 直连启用脚本透传高端口 rehearsal 参数 补充 direct live guard、运维门禁和文档示例
2350 lines
70 KiB
JavaScript
2350 lines
70 KiB
JavaScript
#!/usr/bin/env node
|
|
|
|
import { spawnSync } from 'node:child_process';
|
|
import {
|
|
existsSync,
|
|
mkdtempSync,
|
|
mkdirSync,
|
|
readFileSync,
|
|
rmSync,
|
|
symlinkSync,
|
|
writeFileSync,
|
|
} from 'node:fs';
|
|
import { tmpdir, userInfo } from 'node:os';
|
|
import path from 'node:path';
|
|
|
|
const ENABLE_SCRIPT = 'scripts/deploy/pingora-direct-enable.sh';
|
|
const PREFLIGHT_SCRIPT = 'scripts/check-pingora-direct-preflight.mjs';
|
|
const failures = [];
|
|
|
|
const tmpRoot = mkdtempSync(path.join(tmpdir(), 'genarrative-pingora-enable-'));
|
|
|
|
try {
|
|
main();
|
|
} finally {
|
|
rmSync(tmpRoot, { recursive: true, force: true });
|
|
}
|
|
|
|
if (failures.length > 0) {
|
|
console.error('[check:pingora-direct-enable] FAILED');
|
|
for (const failure of failures) {
|
|
console.error(`- ${failure}`);
|
|
}
|
|
process.exit(1);
|
|
}
|
|
|
|
console.log('[check:pingora-direct-enable] OK');
|
|
|
|
function main() {
|
|
assertScriptShape();
|
|
assertDirectPreflightChecksServiceEnvironmentFile();
|
|
assertDirectPreflightRejectsPublicForwardedForTrust();
|
|
assertDryRunDoesNotInstallDropin();
|
|
assertDryRunRunsPreflightWhenRequested();
|
|
assertReleaseLayoutDryRunUsesBundledDirectChecks();
|
|
assertMissingTemplateDryRunWarns();
|
|
assertRelativePathsRejected();
|
|
assertFilesystemRootPathsRejected();
|
|
assertApplyRequiresPreflightEnvFile();
|
|
assertApplyRequiresCertReadablePreflight();
|
|
assertApplyRequiresServiceEnvFilePreflight();
|
|
assertApplyRequiresServiceUserCertReadablePreflight();
|
|
assertApplyRequiresServiceBinaryExecutablePreflight();
|
|
assertApplyRequiresPortsFreePreflight();
|
|
assertApplyRequiresDirectLiveArgs();
|
|
assertRejectsControlCharacterInputsBeforeApply();
|
|
assertApplyFailsWhenCurrentReleaseAuditFailsBeforeInstall();
|
|
assertApplyFailsWhenPreflightScriptMissingBeforeInstall();
|
|
assertApplyFailsWhenDirectLiveScriptMissingBeforeInstall();
|
|
assertApplyRejectsSymlinkDropinDirectoryBeforeInstall();
|
|
assertApplyRejectsSymlinkDropinFileBeforeInstall();
|
|
assertApplyFailsWhenSystemdEnvFileDiffers();
|
|
assertApplyFailsWhenSystemdEnvFileOnlySharesPrefix();
|
|
assertApplyFailsWhenSystemdExecStartDiffers();
|
|
assertApplyFailsWhenDirectLiveSmokeFails();
|
|
assertApplyFailsWhenDirectLiveAccessLogJsonMissing();
|
|
}
|
|
|
|
function assertDirectPreflightChecksServiceEnvironmentFile() {
|
|
const preflightRoot = path.join(tmpRoot, 'preflight-env-file');
|
|
const envPath = path.join(preflightRoot, 'pingora-gateway.env');
|
|
const servicePath = path.join(preflightRoot, 'genarrative-pingora-gateway.service');
|
|
const mismatchServicePath = path.join(
|
|
preflightRoot,
|
|
'genarrative-pingora-gateway-mismatch.service',
|
|
);
|
|
const dropinPath = path.join(
|
|
preflightRoot,
|
|
'genarrative-pingora-gateway-direct-entry.conf',
|
|
);
|
|
const envExamplePath = path.join(preflightRoot, 'pingora-gateway.env.example');
|
|
const systemctlBinary = path.join(preflightRoot, 'systemctl');
|
|
|
|
mkdirSync(preflightRoot, { recursive: true });
|
|
writeFileSync(envPath, 'GENARRATIVE_PINGORA_GATEWAY_FORWARDED_PROTO=https\n', 'utf8');
|
|
writeFileSync(
|
|
servicePath,
|
|
[
|
|
'[Unit]',
|
|
'Description=Pingora 影子网关只监听本机高端口',
|
|
'[Service]',
|
|
`EnvironmentFile=-${envPath}`,
|
|
'ExecStart=/opt/genarrative/current/pingora-gateway',
|
|
'',
|
|
].join('\n'),
|
|
'utf8',
|
|
);
|
|
writeFileSync(
|
|
mismatchServicePath,
|
|
[
|
|
'[Unit]',
|
|
'Description=Pingora 影子网关只监听本机高端口',
|
|
'[Service]',
|
|
'EnvironmentFile=/etc/genarrative/other-pingora.env',
|
|
'ExecStart=/opt/genarrative/current/pingora-gateway',
|
|
'',
|
|
].join('\n'),
|
|
'utf8',
|
|
);
|
|
writeFileSync(
|
|
dropinPath,
|
|
[
|
|
'# 人工复制到 /etc/systemd/system/genarrative-pingora-gateway.service.d/direct-entry.conf 后生效',
|
|
'[Service]',
|
|
'AmbientCapabilities=CAP_NET_BIND_SERVICE',
|
|
'CapabilityBoundingSet=CAP_NET_BIND_SERVICE',
|
|
'',
|
|
].join('\n'),
|
|
'utf8',
|
|
);
|
|
writeFileSync(
|
|
envExamplePath,
|
|
'# CAP_NET_BIND_SERVICE 只由 direct-entry drop-in 授予\n',
|
|
'utf8',
|
|
);
|
|
|
|
const baseArgs = [
|
|
'--env-file',
|
|
envPath,
|
|
'--check-service-env-file',
|
|
'--service-unit',
|
|
servicePath,
|
|
'--dropin-template',
|
|
dropinPath,
|
|
'--env-example',
|
|
envExamplePath,
|
|
];
|
|
const okResult = runPreflight(baseArgs);
|
|
assertStatus(
|
|
okResult,
|
|
0,
|
|
'direct preflight 应接受 optional EnvironmentFile=-<env> 指向本次 env。',
|
|
);
|
|
|
|
const serviceMismatchResult = runPreflight([
|
|
'--env-file',
|
|
envPath,
|
|
'--check-service-env-file',
|
|
'--service-unit',
|
|
mismatchServicePath,
|
|
'--dropin-template',
|
|
dropinPath,
|
|
'--env-example',
|
|
envExamplePath,
|
|
]);
|
|
if (serviceMismatchResult.status === 0) {
|
|
failures.push('service 模板 EnvironmentFile 漂移时 direct preflight 必须失败。');
|
|
}
|
|
assertIncludes(
|
|
serviceMismatchResult.stderr,
|
|
`${mismatchServicePath} EnvironmentFile 未包含本次 --env-file`,
|
|
'service 模板 EnvironmentFile 漂移时必须给出明确错误。',
|
|
);
|
|
|
|
writeFileSync(
|
|
systemctlBinary,
|
|
[
|
|
'#!/usr/bin/env bash',
|
|
'if [[ "$1" == "cat" ]]; then',
|
|
' echo "[Service]"',
|
|
' echo "EnvironmentFile=/etc/genarrative/other-pingora.env"',
|
|
' echo "AmbientCapabilities=CAP_NET_BIND_SERVICE"',
|
|
' echo "CapabilityBoundingSet=CAP_NET_BIND_SERVICE"',
|
|
' exit 0',
|
|
'fi',
|
|
'exit 1',
|
|
'',
|
|
].join('\n'),
|
|
'utf8',
|
|
);
|
|
chmodExecutable(systemctlBinary);
|
|
|
|
const systemdMismatchResult = runPreflight([...baseArgs, '--systemd-cat'], {
|
|
PATH: `${preflightRoot}:${process.env.PATH || ''}`,
|
|
});
|
|
if (systemdMismatchResult.status === 0) {
|
|
failures.push('systemctl cat EnvironmentFile 漂移时 direct preflight 必须失败。');
|
|
}
|
|
assertIncludes(
|
|
systemdMismatchResult.stderr,
|
|
'systemctl cat genarrative-pingora-gateway.service EnvironmentFile 未包含本次 --env-file',
|
|
'systemd 最终配置 EnvironmentFile 漂移时必须给出明确错误。',
|
|
);
|
|
}
|
|
|
|
function assertDirectPreflightRejectsPublicForwardedForTrust() {
|
|
const preflightRoot = path.join(tmpRoot, 'preflight-forwarded-for');
|
|
const envPath = path.join(preflightRoot, 'pingora-gateway.env');
|
|
const servicePath = path.join(preflightRoot, 'genarrative-pingora-gateway.service');
|
|
const dropinPath = path.join(
|
|
preflightRoot,
|
|
'genarrative-pingora-gateway-direct-entry.conf',
|
|
);
|
|
const envExamplePath = path.join(preflightRoot, 'pingora-gateway.env.example');
|
|
|
|
mkdirSync(preflightRoot, { recursive: true });
|
|
writeFileSync(
|
|
servicePath,
|
|
[
|
|
'[Unit]',
|
|
'Description=Pingora 影子网关只监听本机高端口',
|
|
'[Service]',
|
|
`EnvironmentFile=${envPath}`,
|
|
'ExecStart=/opt/genarrative/current/pingora-gateway',
|
|
'',
|
|
].join('\n'),
|
|
'utf8',
|
|
);
|
|
writeFileSync(
|
|
dropinPath,
|
|
[
|
|
'# 人工复制到 /etc/systemd/system/genarrative-pingora-gateway.service.d/direct-entry.conf 后生效',
|
|
'[Service]',
|
|
'AmbientCapabilities=CAP_NET_BIND_SERVICE',
|
|
'CapabilityBoundingSet=CAP_NET_BIND_SERVICE',
|
|
'',
|
|
].join('\n'),
|
|
'utf8',
|
|
);
|
|
writeFileSync(
|
|
envExamplePath,
|
|
'# CAP_NET_BIND_SERVICE 只由 direct-entry drop-in 授予\n',
|
|
'utf8',
|
|
);
|
|
|
|
const baseArgs = [
|
|
'--env-file',
|
|
envPath,
|
|
'--require-live-env',
|
|
'--service-unit',
|
|
servicePath,
|
|
'--dropin-template',
|
|
dropinPath,
|
|
'--env-example',
|
|
envExamplePath,
|
|
];
|
|
|
|
writeFileSync(
|
|
envPath,
|
|
[
|
|
'GENARRATIVE_PINGORA_GATEWAY_TLS_LISTEN=0.0.0.0:443',
|
|
'GENARRATIVE_PINGORA_GATEWAY_HTTP_REDIRECT_LISTEN=0.0.0.0:80',
|
|
'GENARRATIVE_PINGORA_GATEWAY_TLS_CERT_FILE=/tmp/cert.pem',
|
|
'GENARRATIVE_PINGORA_GATEWAY_TLS_KEY_FILE=/tmp/key.pem',
|
|
'GENARRATIVE_PINGORA_GATEWAY_FORWARDED_PROTO=https',
|
|
'GENARRATIVE_PINGORA_GATEWAY_TRUST_X_FORWARDED_FOR=true',
|
|
'GENARRATIVE_PINGORA_GATEWAY_TRUSTED_FRONT_PROXY_CONFIRMED=true',
|
|
'',
|
|
].join('\n'),
|
|
'utf8',
|
|
);
|
|
const publicResult = runPreflight(baseArgs);
|
|
if (publicResult.status === 0) {
|
|
failures.push('公网直连开启 X-Forwarded-For 信任时 direct preflight 必须失败。');
|
|
}
|
|
assertIncludes(
|
|
publicResult.stderr,
|
|
'公网直连 Pingora 时 GENARRATIVE_PINGORA_GATEWAY_TRUST_X_FORWARDED_FOR 必须保持 false',
|
|
'公网直连误信任 X-Forwarded-For 时必须给出明确错误。',
|
|
);
|
|
|
|
writeFileSync(
|
|
envPath,
|
|
[
|
|
'GENARRATIVE_PINGORA_GATEWAY_TLS_LISTEN=127.0.0.1:18443',
|
|
'GENARRATIVE_PINGORA_GATEWAY_HTTP_REDIRECT_LISTEN=127.0.0.1:18080',
|
|
'GENARRATIVE_PINGORA_GATEWAY_TLS_CERT_FILE=/tmp/cert.pem',
|
|
'GENARRATIVE_PINGORA_GATEWAY_TLS_KEY_FILE=/tmp/key.pem',
|
|
'GENARRATIVE_PINGORA_GATEWAY_FORWARDED_PROTO=https',
|
|
'GENARRATIVE_PINGORA_GATEWAY_TRUST_X_FORWARDED_FOR=true',
|
|
'GENARRATIVE_PINGORA_GATEWAY_TRUSTED_FRONT_PROXY_CONFIRMED=true',
|
|
'',
|
|
].join('\n'),
|
|
'utf8',
|
|
);
|
|
const loopbackResult = runPreflight(baseArgs);
|
|
assertStatus(
|
|
loopbackResult,
|
|
0,
|
|
'loopback 入口在显式确认前置代理清洗 X-Forwarded-For 后应允许 preflight 通过。',
|
|
);
|
|
|
|
writeFileSync(
|
|
envPath,
|
|
[
|
|
'GENARRATIVE_PINGORA_GATEWAY_TLS_LISTEN=127.0.0.1:18443',
|
|
'GENARRATIVE_PINGORA_GATEWAY_HTTP_REDIRECT_LISTEN=127.0.0.1:18080',
|
|
'GENARRATIVE_PINGORA_GATEWAY_TLS_CERT_FILE=/tmp/cert.pem',
|
|
'GENARRATIVE_PINGORA_GATEWAY_TLS_KEY_FILE=/tmp/key.pem',
|
|
'GENARRATIVE_PINGORA_GATEWAY_FORWARDED_PROTO=https',
|
|
'GENARRATIVE_PINGORA_GATEWAY_TRUST_X_FORWARDED_FOR=true',
|
|
'GENARRATIVE_PINGORA_GATEWAY_TRUSTED_FRONT_PROXY_CONFIRMED=false',
|
|
'',
|
|
].join('\n'),
|
|
'utf8',
|
|
);
|
|
const missingConfirmationResult = runPreflight(baseArgs);
|
|
if (missingConfirmationResult.status === 0) {
|
|
failures.push('开启 X-Forwarded-For 信任但缺少确认开关时 direct preflight 必须失败。');
|
|
}
|
|
assertIncludes(
|
|
missingConfirmationResult.stderr,
|
|
'GENARRATIVE_PINGORA_GATEWAY_TRUST_X_FORWARDED_FOR=true 时必须同时设置 GENARRATIVE_PINGORA_GATEWAY_TRUSTED_FRONT_PROXY_CONFIRMED=true',
|
|
'X-Forwarded-For 信任缺少确认开关时必须给出明确错误。',
|
|
);
|
|
}
|
|
|
|
function assertReleaseLayoutDryRunUsesBundledDirectChecks() {
|
|
const currentUser = currentUsername();
|
|
const releaseRoot = path.join(tmpRoot, 'current-release');
|
|
const releaseScript = path.join(
|
|
releaseRoot,
|
|
'scripts',
|
|
'deploy',
|
|
'pingora-direct-enable.sh',
|
|
);
|
|
const templatePath = path.join(
|
|
releaseRoot,
|
|
'deploy',
|
|
'systemd',
|
|
'genarrative-pingora-gateway-direct-entry.conf',
|
|
);
|
|
const dropinPath = path.join(
|
|
tmpRoot,
|
|
'release-layout-service.d',
|
|
'direct-entry.conf',
|
|
);
|
|
const envPath = path.join(tmpRoot, 'release-layout-pingora-gateway.env');
|
|
const certPath = path.join(tmpRoot, 'release-layout-cert.pem');
|
|
const keyPath = path.join(tmpRoot, 'release-layout-key.pem');
|
|
const pingoraGatewayPath = path.join(releaseRoot, 'pingora-gateway');
|
|
|
|
mkdirSync(path.join(releaseRoot, 'scripts', 'deploy'), { recursive: true });
|
|
mkdirSync(path.join(releaseRoot, 'deploy', 'systemd'), { recursive: true });
|
|
mkdirSync(path.join(releaseRoot, 'deploy', 'pingora'), { recursive: true });
|
|
writeFileSync(releaseScript, readFileSync(ENABLE_SCRIPT, 'utf8'), 'utf8');
|
|
writeFileSync(
|
|
path.join(releaseRoot, 'scripts', 'check-pingora-direct-preflight.mjs'),
|
|
readFileSync('scripts/check-pingora-direct-preflight.mjs', 'utf8'),
|
|
'utf8',
|
|
);
|
|
writeFileSync(
|
|
path.join(releaseRoot, 'scripts', 'check-pingora-direct-live.mjs'),
|
|
readFileSync('scripts/check-pingora-direct-live.mjs', 'utf8'),
|
|
'utf8',
|
|
);
|
|
writeFileSync(
|
|
path.join(
|
|
releaseRoot,
|
|
'deploy',
|
|
'systemd',
|
|
'genarrative-pingora-gateway.service',
|
|
),
|
|
`[Unit]\nDescription=Pingora 影子网关只监听本机高端口\n[Service]\nUser=${currentUser}\nEnvironmentFile=${envPath}\nExecStart=${pingoraGatewayPath}\n`,
|
|
'utf8',
|
|
);
|
|
writeFileSync(
|
|
templatePath,
|
|
[
|
|
'# 人工复制到 /etc/systemd/system/genarrative-pingora-gateway.service.d/direct-entry.conf 后生效',
|
|
'[Service]',
|
|
'AmbientCapabilities=CAP_NET_BIND_SERVICE',
|
|
'CapabilityBoundingSet=CAP_NET_BIND_SERVICE',
|
|
'',
|
|
].join('\n'),
|
|
'utf8',
|
|
);
|
|
writeFileSync(
|
|
path.join(releaseRoot, 'deploy', 'pingora', 'pingora-gateway.env.example'),
|
|
[
|
|
'# CAP_NET_BIND_SERVICE 只由 direct-entry drop-in 授予',
|
|
'GENARRATIVE_PINGORA_GATEWAY_TLS_LISTEN=0.0.0.0:443',
|
|
'GENARRATIVE_PINGORA_GATEWAY_HTTP_REDIRECT_LISTEN=0.0.0.0:80',
|
|
'GENARRATIVE_PINGORA_GATEWAY_FORWARDED_PROTO=https',
|
|
'',
|
|
].join('\n'),
|
|
'utf8',
|
|
);
|
|
writeFileSync(certPath, 'fake-cert\n', 'utf8');
|
|
writeFileSync(keyPath, 'fake-key\n', 'utf8');
|
|
writeFileSync(
|
|
envPath,
|
|
[
|
|
'GENARRATIVE_PINGORA_GATEWAY_TLS_LISTEN=127.0.0.1:18443',
|
|
'GENARRATIVE_PINGORA_GATEWAY_HTTP_REDIRECT_LISTEN=127.0.0.1:18080',
|
|
'GENARRATIVE_PINGORA_GATEWAY_FORWARDED_PROTO=https',
|
|
`GENARRATIVE_PINGORA_GATEWAY_TLS_CERT_FILE=${certPath}`,
|
|
`GENARRATIVE_PINGORA_GATEWAY_TLS_KEY_FILE=${keyPath}`,
|
|
'',
|
|
].join('\n'),
|
|
'utf8',
|
|
);
|
|
writeFileSync(
|
|
pingoraGatewayPath,
|
|
'#!/usr/bin/env bash\nprintf "fake pingora gateway\\n"\n',
|
|
'utf8',
|
|
);
|
|
chmodExecutable(releaseScript);
|
|
chmodExecutable(pingoraGatewayPath);
|
|
|
|
const result = spawnSync(
|
|
'bash',
|
|
[
|
|
releaseScript,
|
|
'--dropin-path',
|
|
dropinPath,
|
|
'--preflight-env-file',
|
|
envPath,
|
|
'--preflight-check-cert-readable',
|
|
'--preflight-check-service-env-file',
|
|
'--preflight-check-service-user-cert-readable',
|
|
'--preflight-check-service-binary-executable',
|
|
'--preflight-check-ports-free',
|
|
'--no-status',
|
|
],
|
|
{
|
|
cwd: tmpRoot,
|
|
encoding: 'utf8',
|
|
env: process.env,
|
|
},
|
|
);
|
|
|
|
assertStatus(
|
|
result,
|
|
0,
|
|
'release current 目录中的 enable dry-run 应能使用随包携带的 direct preflight。',
|
|
);
|
|
assertIncludes(
|
|
result.stdout,
|
|
`${releaseRoot}/scripts/check-pingora-direct-preflight.mjs`,
|
|
'release current 目录中的 enable dry-run 必须调用随包携带的 direct preflight。',
|
|
);
|
|
assertIncludes(
|
|
result.stdout,
|
|
`template=${templatePath}`,
|
|
'release current 目录中的 enable dry-run 必须默认读取随包携带的 direct-entry 模板。',
|
|
);
|
|
assertIncludes(
|
|
result.stdout,
|
|
`+ install -m 0644 ${templatePath} ${dropinPath}`,
|
|
'release current 目录中的 enable dry-run 必须展示安装随包 direct-entry 模板。',
|
|
);
|
|
assertIncludes(
|
|
result.stdout,
|
|
'[pingora-direct-preflight] OK',
|
|
'release current 目录中的 direct preflight 必须带服务用户证书可读和 service 二进制可执行检查并真实通过。',
|
|
);
|
|
if (existsSync(dropinPath)) {
|
|
failures.push('release current 目录 dry-run 不应安装临时 drop-in 文件。');
|
|
}
|
|
}
|
|
|
|
function currentUsername() {
|
|
if (process.env.USER || process.env.LOGNAME) {
|
|
return process.env.USER || process.env.LOGNAME;
|
|
}
|
|
try {
|
|
return userInfo().username;
|
|
} catch {
|
|
return 'root';
|
|
}
|
|
}
|
|
|
|
function assertScriptShape() {
|
|
const content = readFileSync(ENABLE_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,
|
|
'GENARRATIVE_PINGORA_DIRECT_TEMPLATE_PATH',
|
|
'启用脚本必须支持覆盖 direct-entry 模板路径。',
|
|
);
|
|
assertIncludes(
|
|
content,
|
|
'deploy/systemd/genarrative-pingora-gateway-direct-entry.conf',
|
|
'启用脚本默认模板必须来自 current release 随包 deploy/systemd。',
|
|
);
|
|
assertIncludes(
|
|
content,
|
|
'direct-entry.conf',
|
|
'启用脚本必须默认指向 direct-entry drop-in。',
|
|
);
|
|
assertIncludes(
|
|
content,
|
|
'该脚本不写入 TLS env',
|
|
'启用脚本必须声明不改 TLS/env/证书边界。',
|
|
);
|
|
assertIncludes(
|
|
content,
|
|
'--preflight-env-file',
|
|
'启用脚本必须支持直连 env 预检参数。',
|
|
);
|
|
assertIncludes(
|
|
content,
|
|
'--apply 必须同时提供 --preflight-env-file',
|
|
'启用脚本 apply 时必须强制预检 env 文件。',
|
|
);
|
|
assertIncludes(
|
|
content,
|
|
'--apply 必须同时提供 --preflight-check-cert-readable',
|
|
'启用脚本 apply 时必须强制证书可读预检。',
|
|
);
|
|
assertIncludes(
|
|
content,
|
|
'--apply 必须同时提供 --preflight-check-service-env-file',
|
|
'启用脚本 apply 时必须强制 service EnvironmentFile 一致性预检。',
|
|
);
|
|
assertIncludes(
|
|
content,
|
|
'--apply 必须同时提供 --preflight-check-service-user-cert-readable',
|
|
'启用脚本 apply 时必须强制 systemd 服务用户证书可读预检。',
|
|
);
|
|
assertIncludes(
|
|
content,
|
|
'--apply 必须同时提供 --preflight-check-service-binary-executable',
|
|
'启用脚本 apply 时必须强制 Pingora service 二进制可执行预检。',
|
|
);
|
|
assertIncludes(
|
|
content,
|
|
'--apply 必须同时提供 --preflight-check-ports-free',
|
|
'启用脚本 apply 时必须强制端口释放预检。',
|
|
);
|
|
assertIncludes(
|
|
content,
|
|
'--apply 必须同时提供 --direct-https-base-url',
|
|
'启用脚本 apply 时必须强制 direct HTTPS live smoke 参数。',
|
|
);
|
|
assertIncludes(
|
|
content,
|
|
'--apply 必须同时提供 --direct-http-base-url',
|
|
'启用脚本 apply 时必须强制 direct HTTP live smoke 参数。',
|
|
);
|
|
assertIncludes(
|
|
content,
|
|
'--apply 必须同时提供 --direct-host',
|
|
'启用脚本 apply 时必须强制正式域名 Host/SNI 参数。',
|
|
);
|
|
assertIncludes(
|
|
content,
|
|
'--direct-redirect-base-url',
|
|
'启用脚本必须支持 direct live redirect base URL 覆盖,用于高端口 rehearsal。',
|
|
);
|
|
assertIncludes(
|
|
content,
|
|
'--apply 必须同时提供 --direct-redirect-host',
|
|
'启用脚本 apply 时必须强制 redirect Location host 参数。',
|
|
);
|
|
assertIncludes(
|
|
content,
|
|
'--apply 必须同时提供 --direct-pingora-access-log',
|
|
'启用脚本 apply 时必须强制 direct access log 落盘校验参数。',
|
|
);
|
|
assertIncludes(
|
|
content,
|
|
'--apply 必须同时提供 --direct-spacetime-database',
|
|
'启用脚本 apply 时必须强制 WSS subscribe 数据库名参数。',
|
|
);
|
|
assertIncludes(
|
|
content,
|
|
'GENARRATIVE_PINGORA_DIRECT_CURRENT_RELEASE_AUDIT_SCRIPT',
|
|
'启用脚本必须支持固定 current release 自审脚本路径。',
|
|
);
|
|
assertIncludes(
|
|
content,
|
|
'--current-release-root',
|
|
'启用脚本必须支持固定 current release 根目录。',
|
|
);
|
|
assertIncludes(
|
|
content,
|
|
'--require-pingora-gateway',
|
|
'启用脚本 apply 前 current release 自审必须要求 Pingora 网关产物存在。',
|
|
);
|
|
assertIncludes(
|
|
content,
|
|
'--systemd-show',
|
|
'启用脚本 apply 前 current release 自审必须核验 systemd ExecStart。',
|
|
);
|
|
assertIncludes(
|
|
content,
|
|
'systemctl cat ${SERVICE_NAME}',
|
|
'启用脚本必须在 apply 后核验 systemd 最终配置。',
|
|
);
|
|
assertIncludes(
|
|
content,
|
|
'systemctl is-active ${SERVICE_NAME}',
|
|
'启用脚本必须在 apply 后核验 Pingora service active。',
|
|
);
|
|
assertIncludes(
|
|
content,
|
|
'systemctl show ${SERVICE_NAME} --property=ExecStart --value --no-pager',
|
|
'启用脚本必须在 apply 后核验 systemd 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 cat 未显示 EnvironmentFile=',
|
|
'启用脚本必须在 apply 后核验 systemd 最终配置读取的 env 和 preflight env 一致。',
|
|
);
|
|
assertIncludes(
|
|
content,
|
|
'--require-wss-upgrade',
|
|
'启用脚本必须在 apply 后强制 direct live WSS 101。',
|
|
);
|
|
assertIncludes(
|
|
content,
|
|
'--json',
|
|
'启用脚本必须用 JSON 模式运行 direct live smoke,便于校验结构化接流证据。',
|
|
);
|
|
assertIncludes(
|
|
content,
|
|
'direct live JSON 缺少 direct-access-log 结构化结果',
|
|
'启用脚本必须拒绝缺少 direct-access-log 结构化结果的 direct live 输出。',
|
|
);
|
|
assertIncludes(
|
|
content,
|
|
'direct-access-log evidence checked=',
|
|
'启用脚本必须在 direct-access-log 结构化证据通过后输出确认摘要。',
|
|
);
|
|
assertIncludes(
|
|
content,
|
|
'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 目录不能是符号链接',
|
|
'启用脚本 apply 前必须拒绝符号链接 drop-in 目录。',
|
|
);
|
|
assertIncludes(
|
|
content,
|
|
'drop-in 目标不能是符号链接',
|
|
'启用脚本 apply 前必须拒绝符号链接 drop-in 目标。',
|
|
);
|
|
assertIncludes(
|
|
content,
|
|
'reject_control_characters',
|
|
'启用脚本必须统一拒绝换行或 NUL 参数。',
|
|
);
|
|
assertIncludes(
|
|
content,
|
|
'不能包含换行或 NUL 字符',
|
|
'启用脚本拒绝控制字符时必须给出明确错误。',
|
|
);
|
|
assertIncludes(
|
|
content,
|
|
'reject_filesystem_root_path',
|
|
'启用脚本必须统一拒绝文件系统根目录路径参数。',
|
|
);
|
|
assertIncludes(
|
|
content,
|
|
'不能是文件系统根目录',
|
|
'启用脚本拒绝文件系统根目录路径时必须给出明确错误。',
|
|
);
|
|
}
|
|
|
|
function assertDryRunDoesNotInstallDropin() {
|
|
const templatePath = path.join(tmpRoot, 'template.conf');
|
|
const dropinPath = path.join(tmpRoot, 'service.d', 'direct-entry.conf');
|
|
writeFileSync(
|
|
templatePath,
|
|
'[Service]\nAmbientCapabilities=CAP_NET_BIND_SERVICE\n',
|
|
'utf8',
|
|
);
|
|
|
|
const result = runEnable([
|
|
'--template-path',
|
|
templatePath,
|
|
'--dropin-path',
|
|
dropinPath,
|
|
'--service',
|
|
'genarrative-pingora-gateway.service',
|
|
'--direct-https-base-url',
|
|
'https://127.0.0.1',
|
|
'--direct-http-base-url',
|
|
'http://127.0.0.1',
|
|
'--direct-host',
|
|
'example.com',
|
|
'--direct-redirect-host',
|
|
'example.com',
|
|
'--direct-probe-token',
|
|
'direct-secret-token',
|
|
'--direct-pingora-access-log',
|
|
'/var/log/genarrative/pingora-gateway.access.log',
|
|
'--direct-spacetime-database',
|
|
'genarrative-prod',
|
|
'--no-status',
|
|
]);
|
|
|
|
assertStatus(result, 0, 'dry-run 应成功退出。');
|
|
assertIncludes(
|
|
result.stdout,
|
|
'apply=false',
|
|
'dry-run 输出必须明确 apply=false。',
|
|
);
|
|
assertIncludes(
|
|
result.stdout,
|
|
`+ install -d -m 0755 ${path.dirname(dropinPath)}`,
|
|
'dry-run 必须展示创建 drop-in 目录。',
|
|
);
|
|
assertIncludes(
|
|
result.stdout,
|
|
`+ install -m 0644 ${templatePath} ${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,
|
|
'+ systemctl cat genarrative-pingora-gateway.service',
|
|
'dry-run 必须展示启用后 systemd drop-in 生效核验。',
|
|
);
|
|
assertIncludes(
|
|
result.stdout,
|
|
'+ systemctl is-active genarrative-pingora-gateway.service',
|
|
'dry-run 必须展示启用后 service active 核验。',
|
|
);
|
|
assertIncludes(
|
|
result.stdout,
|
|
'+ systemctl show genarrative-pingora-gateway.service --property=ExecStart --value --no-pager',
|
|
'dry-run 必须展示启用后 ExecStart 指向核验。',
|
|
);
|
|
assertIncludes(
|
|
result.stdout,
|
|
'+ node ',
|
|
'dry-run 必须展示 current release 自审和启用后 direct live smoke。',
|
|
);
|
|
assertIncludes(
|
|
result.stdout,
|
|
'pingora-current-release-audit.mjs',
|
|
'dry-run 必须展示 apply 前 current release 自审。',
|
|
);
|
|
assertIncludes(
|
|
result.stdout,
|
|
'--require-pingora-gateway --systemd-show',
|
|
'dry-run current release 自审必须要求 Pingora 产物和 systemd ExecStart。',
|
|
);
|
|
assertIncludes(
|
|
result.stdout,
|
|
'--require-wss-upgrade',
|
|
'dry-run 必须展示 direct live smoke 强制 WSS 101。',
|
|
);
|
|
assertIncludes(
|
|
result.stdout,
|
|
'--json',
|
|
'dry-run 必须展示 direct live smoke 会输出 JSON 结构化证据。',
|
|
);
|
|
assertIncludes(
|
|
result.stdout,
|
|
'--pingora-access-log',
|
|
'dry-run 必须展示 direct live smoke 会校验 Pingora access log 落盘。',
|
|
);
|
|
assertIncludes(
|
|
result.stdout,
|
|
'--probe-token <redacted>',
|
|
'dry-run direct live smoke 命令必须隐藏 probe token。',
|
|
);
|
|
if (result.stdout.includes('direct-secret-token')) {
|
|
failures.push('dry-run direct live smoke 命令不能泄露 probe token 原文。');
|
|
}
|
|
|
|
if (existsSync(dropinPath)) {
|
|
failures.push('dry-run 不应安装临时 drop-in 文件。');
|
|
}
|
|
}
|
|
|
|
function assertDryRunRunsPreflightWhenRequested() {
|
|
const templatePath = path.join(tmpRoot, 'preflight-template.conf');
|
|
const dropinPath = path.join(
|
|
tmpRoot,
|
|
'preflight-service.d',
|
|
'direct-entry.conf',
|
|
);
|
|
const envPath = path.join(tmpRoot, 'pingora-gateway.env');
|
|
const certPath = path.join(tmpRoot, 'cert.pem');
|
|
const keyPath = path.join(tmpRoot, 'key.pem');
|
|
writeFileSync(
|
|
templatePath,
|
|
'[Service]\nAmbientCapabilities=CAP_NET_BIND_SERVICE\n',
|
|
'utf8',
|
|
);
|
|
writeFileSync(certPath, 'fake-cert\n', 'utf8');
|
|
writeFileSync(keyPath, 'fake-key\n', 'utf8');
|
|
writeFileSync(
|
|
envPath,
|
|
[
|
|
'GENARRATIVE_PINGORA_GATEWAY_TLS_LISTEN=127.0.0.1:18443',
|
|
'GENARRATIVE_PINGORA_GATEWAY_HTTP_REDIRECT_LISTEN=127.0.0.1:18080',
|
|
'GENARRATIVE_PINGORA_GATEWAY_FORWARDED_PROTO=https',
|
|
`GENARRATIVE_PINGORA_GATEWAY_TLS_CERT_FILE=${certPath}`,
|
|
`GENARRATIVE_PINGORA_GATEWAY_TLS_KEY_FILE=${keyPath}`,
|
|
'',
|
|
].join('\n'),
|
|
'utf8',
|
|
);
|
|
|
|
const result = runEnable([
|
|
'--template-path',
|
|
templatePath,
|
|
'--dropin-path',
|
|
dropinPath,
|
|
'--preflight-env-file',
|
|
envPath,
|
|
'--preflight-check-cert-readable',
|
|
'--preflight-check-ports-free',
|
|
'--no-status',
|
|
]);
|
|
|
|
assertStatus(result, 0, '带 preflight 的 dry-run 应成功退出。');
|
|
assertIncludes(
|
|
result.stdout,
|
|
'+ node ',
|
|
'dry-run 必须展示并执行 direct preflight。',
|
|
);
|
|
assertIncludes(
|
|
result.stdout,
|
|
'check-pingora-direct-preflight.mjs',
|
|
'dry-run preflight 命令必须调用 direct preflight 脚本。',
|
|
);
|
|
assertIncludes(
|
|
result.stdout,
|
|
`--env-file ${envPath} --require-live-env --check-cert-readable --check-ports-free`,
|
|
'dry-run preflight 命令必须带 env、证书和端口释放检查参数。',
|
|
);
|
|
assertIncludes(
|
|
result.stdout,
|
|
'[pingora-direct-preflight] OK',
|
|
'dry-run preflight 必须真实通过。',
|
|
);
|
|
assertIncludes(
|
|
result.stdout,
|
|
'--https-base-url <direct-https-base-url>',
|
|
'带 preflight 的 dry-run 仍必须提示 apply 后会执行 direct live smoke。',
|
|
);
|
|
if (existsSync(dropinPath)) {
|
|
failures.push('带 preflight 的 dry-run 不应安装临时 drop-in 文件。');
|
|
}
|
|
}
|
|
|
|
function assertMissingTemplateDryRunWarns() {
|
|
const templatePath = path.join(tmpRoot, 'missing-template.conf');
|
|
const dropinPath = path.join(
|
|
tmpRoot,
|
|
'missing-service.d',
|
|
'direct-entry.conf',
|
|
);
|
|
const result = runEnable([
|
|
'--template-path',
|
|
templatePath,
|
|
'--dropin-path',
|
|
dropinPath,
|
|
'--no-status',
|
|
]);
|
|
|
|
assertStatus(result, 0, '模板不存在时 dry-run 应只提示不失败。');
|
|
assertIncludes(
|
|
result.stderr,
|
|
'dry-run 提示:当前模板不存在',
|
|
'模板不存在的 dry-run 必须提示 --apply 会失败。',
|
|
);
|
|
}
|
|
|
|
function assertRelativePathsRejected() {
|
|
const templateResult = runEnable([
|
|
'--template-path',
|
|
'relative/template.conf',
|
|
'--dropin-path',
|
|
path.join(tmpRoot, 'service.d', 'direct-entry.conf'),
|
|
'--no-status',
|
|
]);
|
|
if (templateResult.status === 0) {
|
|
failures.push('相对路径 template 必须被拒绝。');
|
|
}
|
|
assertIncludes(
|
|
templateResult.stderr,
|
|
'--template-path 必须是绝对路径',
|
|
'相对 template 路径负例必须说明需要绝对路径。',
|
|
);
|
|
|
|
const dropinResult = runEnable([
|
|
'--template-path',
|
|
path.join(tmpRoot, 'template.conf'),
|
|
'--dropin-path',
|
|
'relative/direct-entry.conf',
|
|
'--no-status',
|
|
]);
|
|
if (dropinResult.status === 0) {
|
|
failures.push('相对路径 drop-in 必须被拒绝。');
|
|
}
|
|
assertIncludes(
|
|
dropinResult.stderr,
|
|
'--dropin-path 必须是绝对路径',
|
|
'相对 drop-in 路径负例必须说明需要绝对路径。',
|
|
);
|
|
|
|
const preflightResult = runEnable([
|
|
'--template-path',
|
|
path.join(tmpRoot, 'template.conf'),
|
|
'--dropin-path',
|
|
path.join(tmpRoot, 'service.d', 'direct-entry.conf'),
|
|
'--preflight-env-file',
|
|
'relative/pingora-gateway.env',
|
|
'--no-status',
|
|
]);
|
|
if (preflightResult.status === 0) {
|
|
failures.push('相对路径 preflight env 必须被拒绝。');
|
|
}
|
|
assertIncludes(
|
|
preflightResult.stderr,
|
|
'--preflight-env-file 必须是绝对路径',
|
|
'相对 preflight env 路径负例必须说明需要绝对路径。',
|
|
);
|
|
|
|
const auditScriptResult = runEnable([
|
|
'--template-path',
|
|
path.join(tmpRoot, 'template.conf'),
|
|
'--dropin-path',
|
|
path.join(tmpRoot, 'service.d', 'direct-entry.conf'),
|
|
'--current-release-audit-script',
|
|
'relative/pingora-current-release-audit.mjs',
|
|
'--no-status',
|
|
]);
|
|
if (auditScriptResult.status === 0) {
|
|
failures.push('相对路径 current release 自审脚本必须被拒绝。');
|
|
}
|
|
assertIncludes(
|
|
auditScriptResult.stderr,
|
|
'--current-release-audit-script 必须是绝对路径',
|
|
'相对 current release 自审脚本路径负例必须说明需要绝对路径。',
|
|
);
|
|
|
|
const preflightScriptResult = runEnable([
|
|
'--template-path',
|
|
path.join(tmpRoot, 'template.conf'),
|
|
'--dropin-path',
|
|
path.join(tmpRoot, 'service.d', 'direct-entry.conf'),
|
|
'--preflight-script',
|
|
'relative/check-pingora-direct-preflight.mjs',
|
|
'--no-status',
|
|
]);
|
|
if (preflightScriptResult.status === 0) {
|
|
failures.push('相对路径 direct preflight 脚本必须被拒绝。');
|
|
}
|
|
assertIncludes(
|
|
preflightScriptResult.stderr,
|
|
'--preflight-script 必须是绝对路径',
|
|
'相对 direct preflight 脚本路径负例必须说明需要绝对路径。',
|
|
);
|
|
|
|
const auditRootResult = runEnable([
|
|
'--template-path',
|
|
path.join(tmpRoot, 'template.conf'),
|
|
'--dropin-path',
|
|
path.join(tmpRoot, 'service.d', 'direct-entry.conf'),
|
|
'--current-release-root',
|
|
'relative/current',
|
|
'--no-status',
|
|
]);
|
|
if (auditRootResult.status === 0) {
|
|
failures.push('相对路径 current release 根目录必须被拒绝。');
|
|
}
|
|
assertIncludes(
|
|
auditRootResult.stderr,
|
|
'--current-release-root 必须是绝对路径',
|
|
'相对 current release 根目录负例必须说明需要绝对路径。',
|
|
);
|
|
|
|
const directLiveScriptResult = runEnable([
|
|
'--template-path',
|
|
path.join(tmpRoot, 'template.conf'),
|
|
'--dropin-path',
|
|
path.join(tmpRoot, 'service.d', 'direct-entry.conf'),
|
|
'--direct-live-script',
|
|
'relative/check-pingora-direct-live.mjs',
|
|
'--no-status',
|
|
]);
|
|
if (directLiveScriptResult.status === 0) {
|
|
failures.push('相对路径 direct live smoke 脚本必须被拒绝。');
|
|
}
|
|
assertIncludes(
|
|
directLiveScriptResult.stderr,
|
|
'--direct-live-script 必须是绝对路径',
|
|
'相对 direct live smoke 脚本路径负例必须说明需要绝对路径。',
|
|
);
|
|
}
|
|
|
|
function assertFilesystemRootPathsRejected() {
|
|
const rootDropinResult = runEnable([
|
|
'--template-path',
|
|
path.join(tmpRoot, 'template.conf'),
|
|
'--dropin-path',
|
|
'/',
|
|
'--no-status',
|
|
]);
|
|
if (rootDropinResult.status === 0) {
|
|
failures.push('文件系统根目录 --dropin-path 必须被拒绝。');
|
|
}
|
|
assertIncludes(
|
|
rootDropinResult.stderr,
|
|
'--dropin-path 不能是文件系统根目录',
|
|
'文件系统根目录 drop-in 路径负例必须给出明确错误。',
|
|
);
|
|
|
|
const rootReleaseResult = runEnable([
|
|
'--template-path',
|
|
path.join(tmpRoot, 'template.conf'),
|
|
'--dropin-path',
|
|
path.join(tmpRoot, 'service.d', 'direct-entry.conf'),
|
|
'--current-release-root',
|
|
'/',
|
|
'--no-status',
|
|
]);
|
|
if (rootReleaseResult.status === 0) {
|
|
failures.push('文件系统根目录 --current-release-root 必须被拒绝。');
|
|
}
|
|
assertIncludes(
|
|
rootReleaseResult.stderr,
|
|
'--current-release-root 不能是文件系统根目录',
|
|
'文件系统根目录 current release 根目录负例必须给出明确错误。',
|
|
);
|
|
}
|
|
|
|
function assertApplyRequiresPreflightEnvFile() {
|
|
const result = runEnable(['--apply', '--no-status']);
|
|
|
|
if (result.status === 0) {
|
|
failures.push('--apply 缺少 --preflight-env-file 必须被拒绝。');
|
|
}
|
|
assertIncludes(
|
|
result.stderr,
|
|
'--apply 必须同时提供 --preflight-env-file',
|
|
'apply 负例必须说明不能跳过 preflight env。',
|
|
);
|
|
}
|
|
|
|
function assertApplyRequiresCertReadablePreflight() {
|
|
const result = runEnable([
|
|
'--apply',
|
|
'--preflight-env-file',
|
|
path.join(tmpRoot, 'pingora-gateway.env'),
|
|
'--preflight-check-service-env-file',
|
|
'--preflight-check-service-user-cert-readable',
|
|
'--preflight-check-ports-free',
|
|
'--no-status',
|
|
]);
|
|
|
|
if (result.status === 0) {
|
|
failures.push('--apply 缺少 --preflight-check-cert-readable 必须被拒绝。');
|
|
}
|
|
assertIncludes(
|
|
result.stderr,
|
|
'--apply 必须同时提供 --preflight-check-cert-readable',
|
|
'apply 负例必须说明不能跳过证书可读 preflight。',
|
|
);
|
|
}
|
|
|
|
function assertApplyRequiresServiceEnvFilePreflight() {
|
|
const result = runEnable([
|
|
'--apply',
|
|
'--preflight-env-file',
|
|
path.join(tmpRoot, 'pingora-gateway.env'),
|
|
'--preflight-check-cert-readable',
|
|
'--preflight-check-service-user-cert-readable',
|
|
'--preflight-check-ports-free',
|
|
'--no-status',
|
|
]);
|
|
|
|
if (result.status === 0) {
|
|
failures.push(
|
|
'--apply 缺少 --preflight-check-service-env-file 必须被拒绝。',
|
|
);
|
|
}
|
|
assertIncludes(
|
|
result.stderr,
|
|
'--apply 必须同时提供 --preflight-check-service-env-file',
|
|
'apply 负例必须说明不能跳过 service EnvironmentFile 一致性 preflight。',
|
|
);
|
|
}
|
|
|
|
function assertApplyRequiresServiceUserCertReadablePreflight() {
|
|
const result = runEnable([
|
|
'--apply',
|
|
'--preflight-env-file',
|
|
path.join(tmpRoot, 'pingora-gateway.env'),
|
|
'--preflight-check-cert-readable',
|
|
'--preflight-check-service-env-file',
|
|
'--preflight-check-ports-free',
|
|
'--no-status',
|
|
]);
|
|
|
|
if (result.status === 0) {
|
|
failures.push(
|
|
'--apply 缺少 --preflight-check-service-user-cert-readable 必须被拒绝。',
|
|
);
|
|
}
|
|
assertIncludes(
|
|
result.stderr,
|
|
'--apply 必须同时提供 --preflight-check-service-user-cert-readable',
|
|
'apply 负例必须说明不能跳过 systemd 服务用户证书可读 preflight。',
|
|
);
|
|
}
|
|
|
|
function assertApplyRequiresServiceBinaryExecutablePreflight() {
|
|
const result = runEnable([
|
|
'--apply',
|
|
'--preflight-env-file',
|
|
path.join(tmpRoot, 'pingora-gateway.env'),
|
|
'--preflight-check-cert-readable',
|
|
'--preflight-check-service-env-file',
|
|
'--preflight-check-service-user-cert-readable',
|
|
'--preflight-check-ports-free',
|
|
'--no-status',
|
|
]);
|
|
|
|
if (result.status === 0) {
|
|
failures.push(
|
|
'--apply 缺少 --preflight-check-service-binary-executable 必须被拒绝。',
|
|
);
|
|
}
|
|
assertIncludes(
|
|
result.stderr,
|
|
'--apply 必须同时提供 --preflight-check-service-binary-executable',
|
|
'apply 负例必须说明不能跳过 service 二进制可执行 preflight。',
|
|
);
|
|
}
|
|
|
|
function assertApplyRequiresPortsFreePreflight() {
|
|
const result = runEnable([
|
|
'--apply',
|
|
'--preflight-env-file',
|
|
path.join(tmpRoot, 'pingora-gateway.env'),
|
|
'--preflight-check-cert-readable',
|
|
'--preflight-check-service-env-file',
|
|
'--preflight-check-service-user-cert-readable',
|
|
'--preflight-check-service-binary-executable',
|
|
'--no-status',
|
|
]);
|
|
|
|
if (result.status === 0) {
|
|
failures.push('--apply 缺少 --preflight-check-ports-free 必须被拒绝。');
|
|
}
|
|
assertIncludes(
|
|
result.stderr,
|
|
'--apply 必须同时提供 --preflight-check-ports-free',
|
|
'apply 负例必须说明不能跳过端口释放 preflight。',
|
|
);
|
|
}
|
|
|
|
function assertApplyRequiresDirectLiveArgs() {
|
|
const auditArgs = createFakeCurrentReleaseAuditArgs('direct-live-args-audit-ok');
|
|
const baseArgs = [
|
|
'--apply',
|
|
'--preflight-env-file',
|
|
path.join(tmpRoot, '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',
|
|
...auditArgs,
|
|
'--no-status',
|
|
];
|
|
|
|
const httpsResult = runEnable(baseArgs);
|
|
assertApplyFailure(
|
|
httpsResult,
|
|
'--apply 必须同时提供 --direct-https-base-url',
|
|
'--apply 缺少 --direct-https-base-url 必须被拒绝。',
|
|
);
|
|
|
|
const httpResult = runEnable([
|
|
...baseArgs,
|
|
'--direct-https-base-url',
|
|
'https://127.0.0.1',
|
|
]);
|
|
assertApplyFailure(
|
|
httpResult,
|
|
'--apply 必须同时提供 --direct-http-base-url',
|
|
'--apply 缺少 --direct-http-base-url 必须被拒绝。',
|
|
);
|
|
|
|
const hostResult = runEnable([
|
|
...baseArgs,
|
|
'--direct-https-base-url',
|
|
'https://127.0.0.1',
|
|
'--direct-http-base-url',
|
|
'http://127.0.0.1',
|
|
]);
|
|
assertApplyFailure(
|
|
hostResult,
|
|
'--apply 必须同时提供 --direct-host',
|
|
'--apply 缺少 --direct-host 必须被拒绝。',
|
|
);
|
|
|
|
const redirectHostResult = runEnable([
|
|
...baseArgs,
|
|
'--direct-https-base-url',
|
|
'https://127.0.0.1',
|
|
'--direct-http-base-url',
|
|
'http://127.0.0.1',
|
|
'--direct-host',
|
|
'example.com',
|
|
]);
|
|
assertApplyFailure(
|
|
redirectHostResult,
|
|
'--apply 必须同时提供 --direct-redirect-host',
|
|
'--apply 缺少 --direct-redirect-host 必须被拒绝。',
|
|
);
|
|
|
|
const accessLogResult = runEnable([
|
|
...baseArgs,
|
|
'--direct-https-base-url',
|
|
'https://127.0.0.1',
|
|
'--direct-http-base-url',
|
|
'http://127.0.0.1',
|
|
'--direct-host',
|
|
'example.com',
|
|
'--direct-redirect-host',
|
|
'example.com',
|
|
]);
|
|
assertApplyFailure(
|
|
accessLogResult,
|
|
'--apply 必须同时提供 --direct-pingora-access-log',
|
|
'--apply 缺少 --direct-pingora-access-log 必须被拒绝。',
|
|
);
|
|
|
|
const databaseResult = runEnable([
|
|
...baseArgs,
|
|
'--direct-https-base-url',
|
|
'https://127.0.0.1',
|
|
'--direct-http-base-url',
|
|
'http://127.0.0.1',
|
|
'--direct-host',
|
|
'example.com',
|
|
'--direct-redirect-host',
|
|
'example.com',
|
|
'--direct-pingora-access-log',
|
|
'/var/log/genarrative/pingora-gateway.access.log',
|
|
]);
|
|
assertApplyFailure(
|
|
databaseResult,
|
|
'--apply 必须同时提供 --direct-spacetime-database',
|
|
'--apply 缺少 --direct-spacetime-database 必须被拒绝。',
|
|
);
|
|
}
|
|
|
|
function assertRejectsControlCharacterInputsBeforeApply() {
|
|
const fixture = createApplyPathSafetyFixture('control-character-input');
|
|
const dropinPath = path.join(fixture.root, 'service.d', 'direct-entry.conf');
|
|
const result = runEnable([
|
|
...fixture.args,
|
|
'--dropin-path',
|
|
dropinPath,
|
|
'--direct-host',
|
|
'example.com\ninjected.example.com',
|
|
]);
|
|
|
|
if (result.status === 0) {
|
|
failures.push('带换行的 direct host 必须在 apply 修改系统前被拒绝。');
|
|
}
|
|
assertIncludes(
|
|
result.stderr,
|
|
'--direct-host 不能包含换行或 NUL 字符',
|
|
'direct host 控制字符负例必须给出明确错误。',
|
|
);
|
|
if (existsSync(dropinPath)) {
|
|
failures.push('带换行的 direct host 被拒绝后不应安装 direct-entry drop-in。');
|
|
}
|
|
}
|
|
|
|
function assertApplyFailsWhenCurrentReleaseAuditFailsBeforeInstall() {
|
|
const templatePath = path.join(tmpRoot, 'audit-fail-template.conf');
|
|
const dropinPath = path.join(tmpRoot, 'audit-fail-service.d', 'direct-entry.conf');
|
|
const envPath = path.join(tmpRoot, 'audit-fail-pingora-gateway.env');
|
|
const auditScript = path.join(tmpRoot, 'fake-current-release-audit-fail.mjs');
|
|
|
|
writeFileSync(
|
|
templatePath,
|
|
[
|
|
'[Service]',
|
|
'AmbientCapabilities=CAP_NET_BIND_SERVICE',
|
|
'CapabilityBoundingSet=CAP_NET_BIND_SERVICE',
|
|
'',
|
|
].join('\n'),
|
|
'utf8',
|
|
);
|
|
writeFileSync(envPath, 'GENARRATIVE_PINGORA_GATEWAY_FORWARDED_PROTO=https\n', 'utf8');
|
|
writeFileSync(
|
|
auditScript,
|
|
'#!/usr/bin/env node\nconsole.error("fake current release audit failed");\nprocess.exit(21);\n',
|
|
'utf8',
|
|
);
|
|
chmodExecutable(auditScript);
|
|
|
|
const result = runEnable([
|
|
'--apply',
|
|
'--template-path',
|
|
templatePath,
|
|
'--dropin-path',
|
|
dropinPath,
|
|
'--current-release-audit-script',
|
|
auditScript,
|
|
'--current-release-root',
|
|
tmpRoot,
|
|
'--preflight-env-file',
|
|
envPath,
|
|
'--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',
|
|
'example.com',
|
|
'--direct-redirect-host',
|
|
'example.com',
|
|
'--direct-pingora-access-log',
|
|
'/var/log/genarrative/pingora-gateway.access.log',
|
|
'--direct-spacetime-database',
|
|
'genarrative-prod',
|
|
'--no-status',
|
|
]);
|
|
|
|
if (result.status === 0) {
|
|
failures.push('current release 自审失败时 enable apply 必须失败。');
|
|
}
|
|
assertIncludes(
|
|
result.stderr,
|
|
'fake current release audit failed',
|
|
'current release 自审失败时必须暴露自审错误。',
|
|
);
|
|
if (existsSync(dropinPath)) {
|
|
failures.push('current release 自审失败发生在安装前,不应留下 direct-entry drop-in。');
|
|
}
|
|
}
|
|
|
|
function assertApplyFailsWhenPreflightScriptMissingBeforeInstall() {
|
|
const templatePath = path.join(tmpRoot, 'preflight-missing-template.conf');
|
|
const dropinPath = path.join(
|
|
tmpRoot,
|
|
'preflight-missing-service.d',
|
|
'direct-entry.conf',
|
|
);
|
|
const envPath = path.join(tmpRoot, 'preflight-missing-pingora-gateway.env');
|
|
const missingPreflightScript = path.join(
|
|
tmpRoot,
|
|
'missing-check-pingora-direct-preflight.mjs',
|
|
);
|
|
const directLiveScript = path.join(tmpRoot, 'fake-direct-live-preflight-missing.mjs');
|
|
const auditArgs = createFakeCurrentReleaseAuditArgs('preflight-missing-audit-ok');
|
|
|
|
writeFileSync(
|
|
templatePath,
|
|
[
|
|
'[Service]',
|
|
'AmbientCapabilities=CAP_NET_BIND_SERVICE',
|
|
'CapabilityBoundingSet=CAP_NET_BIND_SERVICE',
|
|
'',
|
|
].join('\n'),
|
|
'utf8',
|
|
);
|
|
writeFileSync(envPath, 'GENARRATIVE_PINGORA_GATEWAY_FORWARDED_PROTO=https\n', 'utf8');
|
|
writeFileSync(
|
|
directLiveScript,
|
|
'#!/usr/bin/env node\nconsole.log("[fake-direct-live] OK");\n',
|
|
'utf8',
|
|
);
|
|
chmodExecutable(directLiveScript);
|
|
|
|
const result = runEnable([
|
|
'--apply',
|
|
'--template-path',
|
|
templatePath,
|
|
'--dropin-path',
|
|
dropinPath,
|
|
...auditArgs,
|
|
'--preflight-script',
|
|
missingPreflightScript,
|
|
'--preflight-env-file',
|
|
envPath,
|
|
'--preflight-check-cert-readable',
|
|
'--preflight-check-service-env-file',
|
|
'--preflight-check-service-user-cert-readable',
|
|
'--preflight-check-service-binary-executable',
|
|
'--preflight-check-ports-free',
|
|
'--direct-live-script',
|
|
directLiveScript,
|
|
'--direct-https-base-url',
|
|
'https://127.0.0.1',
|
|
'--direct-http-base-url',
|
|
'http://127.0.0.1',
|
|
'--direct-host',
|
|
'example.com',
|
|
'--direct-redirect-host',
|
|
'example.com',
|
|
'--direct-pingora-access-log',
|
|
'/var/log/genarrative/pingora-gateway.access.log',
|
|
'--direct-spacetime-database',
|
|
'genarrative-prod',
|
|
'--no-status',
|
|
]);
|
|
|
|
if (result.status === 0) {
|
|
failures.push('direct preflight 脚本缺失时 enable apply 必须失败。');
|
|
}
|
|
assertIncludes(
|
|
result.stderr,
|
|
'direct preflight 脚本不存在',
|
|
'direct preflight 脚本缺失时必须给出明确错误。',
|
|
);
|
|
if (existsSync(dropinPath)) {
|
|
failures.push('direct preflight 脚本缺失发生在安装前,不应留下 direct-entry drop-in。');
|
|
}
|
|
}
|
|
|
|
function assertApplyFailsWhenDirectLiveScriptMissingBeforeInstall() {
|
|
const templatePath = path.join(tmpRoot, 'direct-live-missing-template.conf');
|
|
const dropinPath = path.join(
|
|
tmpRoot,
|
|
'direct-live-missing-service.d',
|
|
'direct-entry.conf',
|
|
);
|
|
const envPath = path.join(tmpRoot, 'direct-live-missing-pingora-gateway.env');
|
|
const preflightScript = path.join(tmpRoot, 'fake-preflight-direct-live-missing.mjs');
|
|
const missingDirectLiveScript = path.join(
|
|
tmpRoot,
|
|
'missing-check-pingora-direct-live.mjs',
|
|
);
|
|
const auditArgs = createFakeCurrentReleaseAuditArgs('direct-live-missing-audit-ok');
|
|
|
|
writeFileSync(
|
|
templatePath,
|
|
[
|
|
'[Service]',
|
|
'AmbientCapabilities=CAP_NET_BIND_SERVICE',
|
|
'CapabilityBoundingSet=CAP_NET_BIND_SERVICE',
|
|
'',
|
|
].join('\n'),
|
|
'utf8',
|
|
);
|
|
writeFileSync(envPath, 'GENARRATIVE_PINGORA_GATEWAY_FORWARDED_PROTO=https\n', 'utf8');
|
|
writeFileSync(
|
|
preflightScript,
|
|
'#!/usr/bin/env node\nconsole.log("[fake-preflight] OK");\n',
|
|
'utf8',
|
|
);
|
|
chmodExecutable(preflightScript);
|
|
|
|
const result = runEnable([
|
|
'--apply',
|
|
'--template-path',
|
|
templatePath,
|
|
'--dropin-path',
|
|
dropinPath,
|
|
...auditArgs,
|
|
'--preflight-script',
|
|
preflightScript,
|
|
'--preflight-env-file',
|
|
envPath,
|
|
'--preflight-check-cert-readable',
|
|
'--preflight-check-service-env-file',
|
|
'--preflight-check-service-user-cert-readable',
|
|
'--preflight-check-service-binary-executable',
|
|
'--preflight-check-ports-free',
|
|
'--direct-live-script',
|
|
missingDirectLiveScript,
|
|
'--direct-https-base-url',
|
|
'https://127.0.0.1',
|
|
'--direct-http-base-url',
|
|
'http://127.0.0.1',
|
|
'--direct-host',
|
|
'example.com',
|
|
'--direct-redirect-host',
|
|
'example.com',
|
|
'--direct-pingora-access-log',
|
|
'/var/log/genarrative/pingora-gateway.access.log',
|
|
'--direct-spacetime-database',
|
|
'genarrative-prod',
|
|
'--no-status',
|
|
]);
|
|
|
|
if (result.status === 0) {
|
|
failures.push('direct live smoke 脚本缺失时 enable apply 必须失败。');
|
|
}
|
|
assertIncludes(
|
|
result.stderr,
|
|
'direct live smoke 脚本不存在',
|
|
'direct live smoke 脚本缺失时必须给出明确错误。',
|
|
);
|
|
if (existsSync(dropinPath)) {
|
|
failures.push('direct live smoke 脚本缺失发生在安装前,不应留下 direct-entry drop-in。');
|
|
}
|
|
}
|
|
|
|
function assertApplyRejectsSymlinkDropinDirectoryBeforeInstall() {
|
|
const fixture = createApplyPathSafetyFixture('symlink-dropin-dir');
|
|
const realDropinDir = path.join(fixture.root, 'real-service.d');
|
|
const symlinkDropinDir = path.join(fixture.root, 'linked-service.d');
|
|
mkdirSync(realDropinDir, { recursive: true });
|
|
symlinkSync(realDropinDir, symlinkDropinDir);
|
|
const dropinPath = path.join(symlinkDropinDir, 'direct-entry.conf');
|
|
|
|
const result = runEnable([...fixture.args, '--dropin-path', dropinPath]);
|
|
|
|
if (result.status === 0) {
|
|
failures.push('drop-in 目录是符号链接时 enable apply 必须失败。');
|
|
}
|
|
assertIncludes(
|
|
result.stderr,
|
|
'drop-in 目录不能是符号链接',
|
|
'drop-in 符号链接目录负例必须给出明确错误。',
|
|
);
|
|
if (existsSync(path.join(realDropinDir, 'direct-entry.conf'))) {
|
|
failures.push('drop-in 目录是符号链接时不应写入真实目标目录。');
|
|
}
|
|
}
|
|
|
|
function assertApplyRejectsSymlinkDropinFileBeforeInstall() {
|
|
const fixture = createApplyPathSafetyFixture('symlink-dropin-file');
|
|
const dropinDir = path.join(fixture.root, 'service.d');
|
|
const realDropinFile = path.join(fixture.root, 'real-direct-entry.conf');
|
|
const symlinkDropinFile = path.join(dropinDir, 'direct-entry.conf');
|
|
mkdirSync(dropinDir, { recursive: true });
|
|
writeFileSync(realDropinFile, 'original dropin\n', 'utf8');
|
|
symlinkSync(realDropinFile, symlinkDropinFile);
|
|
|
|
const result = runEnable([...fixture.args, '--dropin-path', symlinkDropinFile]);
|
|
|
|
if (result.status === 0) {
|
|
failures.push('drop-in 目标是符号链接时 enable apply 必须失败。');
|
|
}
|
|
assertIncludes(
|
|
result.stderr,
|
|
'drop-in 目标不能是符号链接',
|
|
'drop-in 符号链接目标负例必须给出明确错误。',
|
|
);
|
|
const realContent = readFileSync(realDropinFile, 'utf8');
|
|
if (realContent !== 'original dropin\n') {
|
|
failures.push('drop-in 目标是符号链接时不应改写真实目标文件。');
|
|
}
|
|
}
|
|
|
|
function assertApplyFailsWhenSystemdEnvFileDiffers() {
|
|
const templatePath = path.join(tmpRoot, 'systemd-env-mismatch-template.conf');
|
|
const dropinPath = path.join(
|
|
tmpRoot,
|
|
'systemd-env-mismatch-service.d',
|
|
'direct-entry.conf',
|
|
);
|
|
const envPath = path.join(tmpRoot, 'systemd-env-mismatch-pingora-gateway.env');
|
|
const preflightScript = path.join(tmpRoot, 'fake-preflight-env-ok.mjs');
|
|
const directLiveScript = path.join(tmpRoot, 'fake-direct-live-env-ok.mjs');
|
|
const auditArgs = createFakeCurrentReleaseAuditArgs('systemd-env-mismatch-audit-ok');
|
|
const systemctlBinary = path.join(tmpRoot, 'systemctl');
|
|
|
|
writeFileSync(
|
|
templatePath,
|
|
[
|
|
'[Service]',
|
|
'AmbientCapabilities=CAP_NET_BIND_SERVICE',
|
|
'CapabilityBoundingSet=CAP_NET_BIND_SERVICE',
|
|
'',
|
|
].join('\n'),
|
|
'utf8',
|
|
);
|
|
writeFileSync(envPath, 'GENARRATIVE_PINGORA_GATEWAY_FORWARDED_PROTO=https\n', 'utf8');
|
|
writeFileSync(
|
|
preflightScript,
|
|
'#!/usr/bin/env node\nconsole.log("[fake-preflight] OK");\n',
|
|
'utf8',
|
|
);
|
|
writeFileSync(
|
|
directLiveScript,
|
|
'#!/usr/bin/env node\nconsole.log("[fake-direct-live] OK");\n',
|
|
'utf8',
|
|
);
|
|
writeFileSync(
|
|
systemctlBinary,
|
|
[
|
|
'#!/usr/bin/env bash',
|
|
'if [[ "$1" == "cat" ]]; then',
|
|
' echo "[Service]"',
|
|
' echo "EnvironmentFile=/etc/genarrative/other-pingora.env"',
|
|
' echo "AmbientCapabilities=CAP_NET_BIND_SERVICE"',
|
|
' echo "CapabilityBoundingSet=CAP_NET_BIND_SERVICE"',
|
|
' exit 0',
|
|
'fi',
|
|
'if [[ "$1" == "is-active" ]]; then',
|
|
' echo active',
|
|
' exit 0',
|
|
'fi',
|
|
'exit 0',
|
|
'',
|
|
].join('\n'),
|
|
'utf8',
|
|
);
|
|
chmodExecutable(preflightScript);
|
|
chmodExecutable(directLiveScript);
|
|
chmodExecutable(systemctlBinary);
|
|
|
|
const result = runEnable(
|
|
[
|
|
'--apply',
|
|
'--template-path',
|
|
templatePath,
|
|
'--dropin-path',
|
|
dropinPath,
|
|
...auditArgs,
|
|
'--preflight-script',
|
|
preflightScript,
|
|
'--preflight-env-file',
|
|
envPath,
|
|
'--preflight-check-cert-readable',
|
|
'--preflight-check-service-env-file',
|
|
'--preflight-check-service-user-cert-readable',
|
|
'--preflight-check-service-binary-executable',
|
|
'--preflight-check-ports-free',
|
|
'--direct-live-script',
|
|
directLiveScript,
|
|
'--direct-https-base-url',
|
|
'https://127.0.0.1',
|
|
'--direct-http-base-url',
|
|
'http://127.0.0.1',
|
|
'--direct-host',
|
|
'example.com',
|
|
'--direct-redirect-host',
|
|
'example.com',
|
|
'--direct-pingora-access-log',
|
|
'/var/log/genarrative/pingora-gateway.access.log',
|
|
'--direct-spacetime-database',
|
|
'genarrative-prod',
|
|
'--no-status',
|
|
],
|
|
{ PATH: `${tmpRoot}:${process.env.PATH || ''}` },
|
|
);
|
|
|
|
if (result.status === 0) {
|
|
failures.push('systemctl cat 的 EnvironmentFile 漂移时 enable apply 必须失败。');
|
|
}
|
|
assertIncludes(
|
|
result.stderr,
|
|
`systemctl cat 未显示 EnvironmentFile=${envPath}`,
|
|
'systemd 最终配置读取的 env 和 preflight env 不一致时必须给出明确错误。',
|
|
);
|
|
}
|
|
|
|
function assertApplyFailsWhenSystemdEnvFileOnlySharesPrefix() {
|
|
const templatePath = path.join(tmpRoot, 'systemd-env-prefix-template.conf');
|
|
const dropinPath = path.join(
|
|
tmpRoot,
|
|
'systemd-env-prefix-service.d',
|
|
'direct-entry.conf',
|
|
);
|
|
const envPath = path.join(tmpRoot, 'systemd-env-prefix-pingora-gateway.env');
|
|
const preflightScript = path.join(tmpRoot, 'fake-preflight-prefix-ok.mjs');
|
|
const directLiveScript = path.join(tmpRoot, 'fake-direct-live-prefix-ok.mjs');
|
|
const auditArgs = createFakeCurrentReleaseAuditArgs('systemd-env-prefix-audit-ok');
|
|
const systemctlBinary = path.join(tmpRoot, 'systemctl');
|
|
|
|
writeFileSync(
|
|
templatePath,
|
|
[
|
|
'[Service]',
|
|
'AmbientCapabilities=CAP_NET_BIND_SERVICE',
|
|
'CapabilityBoundingSet=CAP_NET_BIND_SERVICE',
|
|
'',
|
|
].join('\n'),
|
|
'utf8',
|
|
);
|
|
writeFileSync(envPath, 'GENARRATIVE_PINGORA_GATEWAY_FORWARDED_PROTO=https\n', 'utf8');
|
|
writeFileSync(
|
|
preflightScript,
|
|
'#!/usr/bin/env node\nconsole.log("[fake-preflight] OK");\n',
|
|
'utf8',
|
|
);
|
|
writeFileSync(
|
|
directLiveScript,
|
|
'#!/usr/bin/env node\nconsole.log("[fake-direct-live] OK");\n',
|
|
'utf8',
|
|
);
|
|
writeFileSync(
|
|
systemctlBinary,
|
|
[
|
|
'#!/usr/bin/env bash',
|
|
'if [[ "$1" == "cat" ]]; then',
|
|
' echo "[Service]"',
|
|
` echo "EnvironmentFile=${envPath}.bak"`,
|
|
' echo "AmbientCapabilities=CAP_NET_BIND_SERVICE"',
|
|
' echo "CapabilityBoundingSet=CAP_NET_BIND_SERVICE"',
|
|
' exit 0',
|
|
'fi',
|
|
'if [[ "$1" == "is-active" ]]; then',
|
|
' echo active',
|
|
' exit 0',
|
|
'fi',
|
|
'exit 0',
|
|
'',
|
|
].join('\n'),
|
|
'utf8',
|
|
);
|
|
chmodExecutable(preflightScript);
|
|
chmodExecutable(directLiveScript);
|
|
chmodExecutable(systemctlBinary);
|
|
|
|
const result = runEnable(
|
|
[
|
|
'--apply',
|
|
'--template-path',
|
|
templatePath,
|
|
'--dropin-path',
|
|
dropinPath,
|
|
...auditArgs,
|
|
'--preflight-script',
|
|
preflightScript,
|
|
'--preflight-env-file',
|
|
envPath,
|
|
'--preflight-check-cert-readable',
|
|
'--preflight-check-service-env-file',
|
|
'--preflight-check-service-user-cert-readable',
|
|
'--preflight-check-service-binary-executable',
|
|
'--preflight-check-ports-free',
|
|
'--direct-live-script',
|
|
directLiveScript,
|
|
'--direct-https-base-url',
|
|
'https://127.0.0.1',
|
|
'--direct-http-base-url',
|
|
'http://127.0.0.1',
|
|
'--direct-host',
|
|
'example.com',
|
|
'--direct-redirect-host',
|
|
'example.com',
|
|
'--direct-pingora-access-log',
|
|
'/var/log/genarrative/pingora-gateway.access.log',
|
|
'--direct-spacetime-database',
|
|
'genarrative-prod',
|
|
'--no-status',
|
|
],
|
|
{ PATH: `${tmpRoot}:${process.env.PATH || ''}` },
|
|
);
|
|
|
|
if (result.status === 0) {
|
|
failures.push(
|
|
'systemctl cat 的 EnvironmentFile 只共享路径前缀时 enable apply 必须失败。',
|
|
);
|
|
}
|
|
assertIncludes(
|
|
result.stderr,
|
|
`systemctl cat 未显示 EnvironmentFile=${envPath}`,
|
|
'systemd 最终配置 EnvironmentFile 不能用路径前缀误判为一致。',
|
|
);
|
|
}
|
|
|
|
function assertApplyFailsWhenDirectLiveSmokeFails() {
|
|
const templatePath = path.join(tmpRoot, 'direct-live-fail-template.conf');
|
|
const serviceUnitPath = path.join(tmpRoot, 'direct-live-fail-service.service');
|
|
const dropinPath = path.join(
|
|
tmpRoot,
|
|
'direct-live-fail-service.d',
|
|
'direct-entry.conf',
|
|
);
|
|
const envPath = path.join(tmpRoot, 'direct-live-fail-pingora-gateway.env');
|
|
const certPath = path.join(tmpRoot, 'direct-live-fail-cert.pem');
|
|
const keyPath = path.join(tmpRoot, 'direct-live-fail-key.pem');
|
|
const preflightScript = path.join(tmpRoot, 'fake-preflight-ok.mjs');
|
|
const directLiveScript = path.join(tmpRoot, 'fake-direct-live-fail.mjs');
|
|
const auditArgs = createFakeCurrentReleaseAuditArgs('direct-live-fail-audit-ok');
|
|
const systemctlBinary = path.join(tmpRoot, 'systemctl');
|
|
|
|
writeFileSync(
|
|
templatePath,
|
|
[
|
|
'[Service]',
|
|
'AmbientCapabilities=CAP_NET_BIND_SERVICE',
|
|
'CapabilityBoundingSet=CAP_NET_BIND_SERVICE',
|
|
'',
|
|
].join('\n'),
|
|
'utf8',
|
|
);
|
|
writeFileSync(
|
|
serviceUnitPath,
|
|
[
|
|
'[Unit]',
|
|
'Description=Pingora 影子网关只监听本机高端口',
|
|
'[Service]',
|
|
`EnvironmentFile=${envPath}`,
|
|
'ExecStart=/opt/genarrative/current/pingora-gateway',
|
|
'',
|
|
].join('\n'),
|
|
'utf8',
|
|
);
|
|
writeFileSync(certPath, 'fake-cert\n', 'utf8');
|
|
writeFileSync(keyPath, 'fake-key\n', 'utf8');
|
|
writeFileSync(
|
|
envPath,
|
|
[
|
|
'GENARRATIVE_PINGORA_GATEWAY_TLS_LISTEN=127.0.0.1:18443',
|
|
'GENARRATIVE_PINGORA_GATEWAY_HTTP_REDIRECT_LISTEN=127.0.0.1:18080',
|
|
'GENARRATIVE_PINGORA_GATEWAY_FORWARDED_PROTO=https',
|
|
`GENARRATIVE_PINGORA_GATEWAY_TLS_CERT_FILE=${certPath}`,
|
|
`GENARRATIVE_PINGORA_GATEWAY_TLS_KEY_FILE=${keyPath}`,
|
|
'',
|
|
].join('\n'),
|
|
'utf8',
|
|
);
|
|
writeFileSync(
|
|
preflightScript,
|
|
'#!/usr/bin/env node\nconsole.log("[fake-preflight] OK");\n',
|
|
'utf8',
|
|
);
|
|
writeFileSync(
|
|
directLiveScript,
|
|
'#!/usr/bin/env node\nconsole.error("fake direct live failed");\nprocess.exit(12);\n',
|
|
'utf8',
|
|
);
|
|
writeFileSync(
|
|
systemctlBinary,
|
|
[
|
|
'#!/usr/bin/env bash',
|
|
'if [[ "$1" == "cat" ]]; then',
|
|
' echo "[Service]"',
|
|
` echo "EnvironmentFile=${envPath}"`,
|
|
' echo "AmbientCapabilities=CAP_NET_BIND_SERVICE"',
|
|
' echo "CapabilityBoundingSet=CAP_NET_BIND_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',
|
|
);
|
|
chmodExecutable(preflightScript);
|
|
chmodExecutable(directLiveScript);
|
|
chmodExecutable(systemctlBinary);
|
|
|
|
const result = runEnable(
|
|
[
|
|
'--apply',
|
|
'--service-unit-path',
|
|
serviceUnitPath,
|
|
'--template-path',
|
|
templatePath,
|
|
'--dropin-path',
|
|
dropinPath,
|
|
...auditArgs,
|
|
'--preflight-script',
|
|
preflightScript,
|
|
'--preflight-env-file',
|
|
envPath,
|
|
'--preflight-check-cert-readable',
|
|
'--preflight-check-service-env-file',
|
|
'--preflight-check-service-user-cert-readable',
|
|
'--preflight-check-service-binary-executable',
|
|
'--preflight-check-ports-free',
|
|
'--direct-live-script',
|
|
directLiveScript,
|
|
'--direct-https-base-url',
|
|
'https://127.0.0.1',
|
|
'--direct-http-base-url',
|
|
'http://127.0.0.1',
|
|
'--direct-host',
|
|
'example.com',
|
|
'--direct-redirect-host',
|
|
'example.com',
|
|
'--direct-pingora-access-log',
|
|
'/var/log/genarrative/pingora-gateway.access.log',
|
|
'--direct-spacetime-database',
|
|
'genarrative-prod',
|
|
'--no-status',
|
|
],
|
|
{ PATH: `${tmpRoot}:${process.env.PATH || ''}` },
|
|
);
|
|
|
|
if (result.status === 0) {
|
|
failures.push('direct live smoke 失败时 enable apply 必须失败。');
|
|
}
|
|
assertIncludes(
|
|
result.stderr,
|
|
'fake direct live failed',
|
|
'direct live smoke 失败时必须暴露检查错误。',
|
|
);
|
|
if (!existsSync(dropinPath)) {
|
|
failures.push(
|
|
'direct live smoke 失败发生在安装后,临时 drop-in 应已存在供后续回退脚本处理。',
|
|
);
|
|
}
|
|
}
|
|
|
|
function assertApplyFailsWhenDirectLiveAccessLogJsonMissing() {
|
|
const templatePath = path.join(tmpRoot, 'direct-live-json-missing-template.conf');
|
|
const serviceUnitPath = path.join(
|
|
tmpRoot,
|
|
'direct-live-json-missing-service.service',
|
|
);
|
|
const dropinPath = path.join(
|
|
tmpRoot,
|
|
'direct-live-json-missing-service.d',
|
|
'direct-entry.conf',
|
|
);
|
|
const envPath = path.join(
|
|
tmpRoot,
|
|
'direct-live-json-missing-pingora-gateway.env',
|
|
);
|
|
const preflightScript = path.join(
|
|
tmpRoot,
|
|
'fake-preflight-json-missing-ok.mjs',
|
|
);
|
|
const directLiveScript = path.join(
|
|
tmpRoot,
|
|
'fake-direct-live-json-missing.mjs',
|
|
);
|
|
const auditArgs = createFakeCurrentReleaseAuditArgs(
|
|
'direct-live-json-missing-audit-ok',
|
|
);
|
|
const systemctlBinary = path.join(tmpRoot, 'systemctl');
|
|
|
|
writeFileSync(
|
|
templatePath,
|
|
[
|
|
'[Service]',
|
|
'AmbientCapabilities=CAP_NET_BIND_SERVICE',
|
|
'CapabilityBoundingSet=CAP_NET_BIND_SERVICE',
|
|
'',
|
|
].join('\n'),
|
|
'utf8',
|
|
);
|
|
writeFileSync(
|
|
serviceUnitPath,
|
|
[
|
|
'[Unit]',
|
|
'Description=Pingora 影子网关只监听本机高端口',
|
|
'[Service]',
|
|
`EnvironmentFile=${envPath}`,
|
|
'ExecStart=/opt/genarrative/current/pingora-gateway',
|
|
'',
|
|
].join('\n'),
|
|
'utf8',
|
|
);
|
|
writeFileSync(envPath, 'GENARRATIVE_PINGORA_GATEWAY_FORWARDED_PROTO=https\n', 'utf8');
|
|
writeFileSync(
|
|
preflightScript,
|
|
'#!/usr/bin/env node\nconsole.log("[fake-preflight] OK");\n',
|
|
'utf8',
|
|
);
|
|
writeFileSync(
|
|
directLiveScript,
|
|
[
|
|
'#!/usr/bin/env node',
|
|
'console.log("[fake-direct-live] OK");',
|
|
'console.log(JSON.stringify({ ok: true, results: [{ name: "https-root" }] }, null, 2));',
|
|
'',
|
|
].join('\n'),
|
|
'utf8',
|
|
);
|
|
writeFileSync(
|
|
systemctlBinary,
|
|
[
|
|
'#!/usr/bin/env bash',
|
|
'if [[ "$1" == "cat" ]]; then',
|
|
' echo "[Service]"',
|
|
` echo "EnvironmentFile=${envPath}"`,
|
|
' echo "AmbientCapabilities=CAP_NET_BIND_SERVICE"',
|
|
' echo "CapabilityBoundingSet=CAP_NET_BIND_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',
|
|
);
|
|
chmodExecutable(preflightScript);
|
|
chmodExecutable(directLiveScript);
|
|
chmodExecutable(systemctlBinary);
|
|
|
|
const result = runEnable(
|
|
[
|
|
'--apply',
|
|
'--service-unit-path',
|
|
serviceUnitPath,
|
|
'--template-path',
|
|
templatePath,
|
|
'--dropin-path',
|
|
dropinPath,
|
|
...auditArgs,
|
|
'--preflight-script',
|
|
preflightScript,
|
|
'--preflight-env-file',
|
|
envPath,
|
|
'--preflight-check-cert-readable',
|
|
'--preflight-check-service-env-file',
|
|
'--preflight-check-service-user-cert-readable',
|
|
'--preflight-check-service-binary-executable',
|
|
'--preflight-check-ports-free',
|
|
'--direct-live-script',
|
|
directLiveScript,
|
|
'--direct-https-base-url',
|
|
'https://127.0.0.1',
|
|
'--direct-http-base-url',
|
|
'http://127.0.0.1',
|
|
'--direct-host',
|
|
'example.com',
|
|
'--direct-redirect-host',
|
|
'example.com',
|
|
'--direct-pingora-access-log',
|
|
'/var/log/genarrative/pingora-gateway.access.log',
|
|
'--direct-spacetime-database',
|
|
'genarrative-prod',
|
|
'--no-status',
|
|
],
|
|
{ PATH: `${tmpRoot}:${process.env.PATH || ''}` },
|
|
);
|
|
|
|
if (result.status === 0) {
|
|
failures.push(
|
|
'direct live JSON 缺少 direct-access-log 时 enable apply 必须失败。',
|
|
);
|
|
}
|
|
assertIncludes(
|
|
result.stderr,
|
|
'direct live JSON 缺少 direct-access-log 结构化结果',
|
|
'direct live JSON 缺少 access log 结构化结果时必须给出明确错误。',
|
|
);
|
|
if (!existsSync(dropinPath)) {
|
|
failures.push(
|
|
'direct live JSON 结构化证据失败发生在安装后,临时 drop-in 应已存在供后续回退脚本处理。',
|
|
);
|
|
}
|
|
}
|
|
|
|
function assertApplyFailsWhenSystemdExecStartDiffers() {
|
|
const templatePath = path.join(tmpRoot, 'systemd-exec-mismatch-template.conf');
|
|
const serviceUnitPath = path.join(tmpRoot, 'systemd-exec-mismatch.service');
|
|
const dropinPath = path.join(
|
|
tmpRoot,
|
|
'systemd-exec-mismatch-service.d',
|
|
'direct-entry.conf',
|
|
);
|
|
const envPath = path.join(tmpRoot, 'systemd-exec-mismatch-pingora-gateway.env');
|
|
const preflightScript = path.join(tmpRoot, 'fake-preflight-exec-ok.mjs');
|
|
const directLiveScript = path.join(tmpRoot, 'fake-direct-live-exec-ok.mjs');
|
|
const auditArgs = createFakeCurrentReleaseAuditArgs('systemd-exec-mismatch-audit-ok');
|
|
const systemctlBinary = path.join(tmpRoot, 'systemctl');
|
|
|
|
writeFileSync(
|
|
templatePath,
|
|
[
|
|
'[Service]',
|
|
'AmbientCapabilities=CAP_NET_BIND_SERVICE',
|
|
'CapabilityBoundingSet=CAP_NET_BIND_SERVICE',
|
|
'',
|
|
].join('\n'),
|
|
'utf8',
|
|
);
|
|
writeFileSync(
|
|
serviceUnitPath,
|
|
[
|
|
'[Unit]',
|
|
'Description=Pingora 影子网关只监听本机高端口',
|
|
'[Service]',
|
|
`EnvironmentFile=${envPath}`,
|
|
'ExecStart=/opt/genarrative/current/pingora-gateway',
|
|
'',
|
|
].join('\n'),
|
|
'utf8',
|
|
);
|
|
writeFileSync(envPath, 'GENARRATIVE_PINGORA_GATEWAY_FORWARDED_PROTO=https\n', 'utf8');
|
|
writeFileSync(
|
|
preflightScript,
|
|
'#!/usr/bin/env node\nconsole.log("[fake-preflight] OK");\n',
|
|
'utf8',
|
|
);
|
|
writeFileSync(
|
|
directLiveScript,
|
|
'#!/usr/bin/env node\nconsole.log("[fake-direct-live] OK");\n',
|
|
'utf8',
|
|
);
|
|
writeFileSync(
|
|
systemctlBinary,
|
|
[
|
|
'#!/usr/bin/env bash',
|
|
'if [[ "$1" == "cat" ]]; then',
|
|
' echo "[Service]"',
|
|
` echo "EnvironmentFile=${envPath}"`,
|
|
' echo "AmbientCapabilities=CAP_NET_BIND_SERVICE"',
|
|
' echo "CapabilityBoundingSet=CAP_NET_BIND_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',
|
|
);
|
|
chmodExecutable(preflightScript);
|
|
chmodExecutable(directLiveScript);
|
|
chmodExecutable(systemctlBinary);
|
|
|
|
const result = runEnable(
|
|
[
|
|
'--apply',
|
|
'--service-unit-path',
|
|
serviceUnitPath,
|
|
'--template-path',
|
|
templatePath,
|
|
'--dropin-path',
|
|
dropinPath,
|
|
...auditArgs,
|
|
'--preflight-script',
|
|
preflightScript,
|
|
'--preflight-env-file',
|
|
envPath,
|
|
'--preflight-check-cert-readable',
|
|
'--preflight-check-service-env-file',
|
|
'--preflight-check-service-user-cert-readable',
|
|
'--preflight-check-service-binary-executable',
|
|
'--preflight-check-ports-free',
|
|
'--direct-live-script',
|
|
directLiveScript,
|
|
'--direct-https-base-url',
|
|
'https://127.0.0.1',
|
|
'--direct-http-base-url',
|
|
'http://127.0.0.1',
|
|
'--direct-host',
|
|
'example.com',
|
|
'--direct-redirect-host',
|
|
'example.com',
|
|
'--direct-pingora-access-log',
|
|
'/var/log/genarrative/pingora-gateway.access.log',
|
|
'--direct-spacetime-database',
|
|
'genarrative-prod',
|
|
'--no-status',
|
|
],
|
|
{ PATH: `${tmpRoot}:${process.env.PATH || ''}` },
|
|
);
|
|
|
|
if (result.status === 0) {
|
|
failures.push('systemctl show ExecStart 指向旧 release 时 enable apply 必须失败。');
|
|
}
|
|
assertIncludes(
|
|
result.stderr,
|
|
'systemctl show ExecStart 未指向主 service 模板中的 /opt/genarrative/current/pingora-gateway',
|
|
'systemd 最终 ExecStart 指向旧 release 时必须给出明确错误。',
|
|
);
|
|
}
|
|
|
|
function runEnable(args, env = {}) {
|
|
return spawnSync('bash', [ENABLE_SCRIPT, ...args], {
|
|
cwd: process.cwd(),
|
|
encoding: 'utf8',
|
|
env: {
|
|
...process.env,
|
|
...env,
|
|
},
|
|
});
|
|
}
|
|
|
|
function runPreflight(args, env = {}) {
|
|
return spawnSync(process.execPath, ['--', PREFLIGHT_SCRIPT, ...args], {
|
|
cwd: process.cwd(),
|
|
encoding: 'utf8',
|
|
env: {
|
|
...process.env,
|
|
...env,
|
|
},
|
|
});
|
|
}
|
|
|
|
function chmodExecutable(filePath) {
|
|
spawnSync('chmod', ['0755', filePath], {
|
|
cwd: process.cwd(),
|
|
encoding: 'utf8',
|
|
});
|
|
}
|
|
|
|
function createFakeCurrentReleaseAuditArgs(name) {
|
|
const auditScript = path.join(tmpRoot, `${name}.mjs`);
|
|
writeFileSync(
|
|
auditScript,
|
|
'#!/usr/bin/env node\nconsole.log("[fake-current-release-audit] OK");\n',
|
|
'utf8',
|
|
);
|
|
chmodExecutable(auditScript);
|
|
return ['--current-release-audit-script', auditScript, '--current-release-root', tmpRoot];
|
|
}
|
|
|
|
function createApplyPathSafetyFixture(name) {
|
|
const root = path.join(tmpRoot, name);
|
|
mkdirSync(root, { recursive: true });
|
|
const templatePath = path.join(root, 'template.conf');
|
|
const envPath = path.join(root, 'pingora-gateway.env');
|
|
const preflightScript = path.join(root, 'fake-preflight.mjs');
|
|
const directLiveScript = path.join(root, 'fake-direct-live.mjs');
|
|
const auditArgs = createFakeCurrentReleaseAuditArgs(`${name}-audit-ok`);
|
|
|
|
writeFileSync(
|
|
templatePath,
|
|
[
|
|
'[Service]',
|
|
'AmbientCapabilities=CAP_NET_BIND_SERVICE',
|
|
'CapabilityBoundingSet=CAP_NET_BIND_SERVICE',
|
|
'',
|
|
].join('\n'),
|
|
'utf8',
|
|
);
|
|
writeFileSync(envPath, 'GENARRATIVE_PINGORA_GATEWAY_FORWARDED_PROTO=https\n', 'utf8');
|
|
writeFileSync(
|
|
preflightScript,
|
|
'#!/usr/bin/env node\nconsole.log("[fake-preflight] OK");\n',
|
|
'utf8',
|
|
);
|
|
writeFileSync(
|
|
directLiveScript,
|
|
'#!/usr/bin/env node\nconsole.log("[fake-direct-live] OK");\n',
|
|
'utf8',
|
|
);
|
|
chmodExecutable(preflightScript);
|
|
chmodExecutable(directLiveScript);
|
|
|
|
return {
|
|
root,
|
|
args: [
|
|
'--apply',
|
|
'--template-path',
|
|
templatePath,
|
|
...auditArgs,
|
|
'--preflight-script',
|
|
preflightScript,
|
|
'--preflight-env-file',
|
|
envPath,
|
|
'--preflight-check-cert-readable',
|
|
'--preflight-check-service-env-file',
|
|
'--preflight-check-service-user-cert-readable',
|
|
'--preflight-check-service-binary-executable',
|
|
'--preflight-check-ports-free',
|
|
'--direct-live-script',
|
|
directLiveScript,
|
|
'--direct-https-base-url',
|
|
'https://127.0.0.1',
|
|
'--direct-http-base-url',
|
|
'http://127.0.0.1',
|
|
'--direct-host',
|
|
'example.com',
|
|
'--direct-redirect-host',
|
|
'example.com',
|
|
'--direct-pingora-access-log',
|
|
'/var/log/genarrative/pingora-gateway.access.log',
|
|
'--direct-spacetime-database',
|
|
'genarrative-prod',
|
|
'--no-status',
|
|
],
|
|
};
|
|
}
|
|
|
|
function assertApplyFailure(result, needle, reason) {
|
|
if (result.status === 0) {
|
|
failures.push(reason);
|
|
return;
|
|
}
|
|
assertIncludes(result.stderr, needle, `${reason} 错误信息应明确。`);
|
|
}
|
|
|
|
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}`);
|
|
}
|
|
}
|