e3258f6f1d
新增 Pingora shadow env 回切脚本与对应检查。 补齐直连证据包时间线和 cutoverRunId 审计门禁。 支持 Gitea Host 透传并更新直连多域名文档。 修复百分号编码静态图标路径并补 smoke 覆盖。 更新生产发布与运维护栏对 Pingora 发布包的校验。
2383 lines
71 KiB
JavaScript
2383 lines
71 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 [DIRECT_TLS_PORT, DIRECT_HTTP_REDIRECT_PORT] = allocateLoopbackPorts(2);
|
|
const DIRECT_TLS_LISTEN = `127.0.0.1:${DIRECT_TLS_PORT}`;
|
|
const DIRECT_HTTP_REDIRECT_LISTEN = `127.0.0.1:${DIRECT_HTTP_REDIRECT_PORT}`;
|
|
|
|
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=${DIRECT_TLS_LISTEN}`,
|
|
`GENARRATIVE_PINGORA_GATEWAY_HTTP_REDIRECT_LISTEN=${DIRECT_HTTP_REDIRECT_LISTEN}`,
|
|
'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=${DIRECT_TLS_LISTEN}`,
|
|
`GENARRATIVE_PINGORA_GATEWAY_HTTP_REDIRECT_LISTEN=${DIRECT_HTTP_REDIRECT_LISTEN}`,
|
|
'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=${DIRECT_TLS_LISTEN}`,
|
|
`GENARRATIVE_PINGORA_GATEWAY_HTTP_REDIRECT_LISTEN=${DIRECT_HTTP_REDIRECT_LISTEN}`,
|
|
'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=${DIRECT_TLS_LISTEN}`,
|
|
`GENARRATIVE_PINGORA_GATEWAY_HTTP_REDIRECT_LISTEN=${DIRECT_HTTP_REDIRECT_LISTEN}`,
|
|
'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=${DIRECT_TLS_LISTEN}`,
|
|
`GENARRATIVE_PINGORA_GATEWAY_HTTP_REDIRECT_LISTEN=${DIRECT_HTTP_REDIRECT_LISTEN}`,
|
|
'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}`);
|
|
}
|
|
}
|
|
|
|
function allocateLoopbackPorts(count) {
|
|
const ports = [];
|
|
for (let offset = 0; ports.length < count && offset < 1000; offset += 1) {
|
|
const candidate = 28080 + offset;
|
|
const result = spawnSync(
|
|
process.execPath,
|
|
[
|
|
'-e',
|
|
[
|
|
"const net = require('node:net');",
|
|
`const server = net.createServer();`,
|
|
`server.once('error', () => process.exit(1));`,
|
|
`server.listen(${candidate}, '127.0.0.1', () => server.close(() => process.exit(0)));`,
|
|
].join(''),
|
|
],
|
|
{
|
|
cwd: process.cwd(),
|
|
encoding: 'utf8',
|
|
},
|
|
);
|
|
if ((result.status ?? 1) === 0) {
|
|
ports.push(candidate);
|
|
}
|
|
}
|
|
if (ports.length !== count) {
|
|
throw new Error('无法为 Pingora direct enable 自测分配空闲 loopback 端口。');
|
|
}
|
|
return ports;
|
|
}
|