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

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

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

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

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

314 lines
9.0 KiB
JavaScript

#!/usr/bin/env node
import { readFileSync } from 'node:fs';
import path from 'node:path';
const GATEWAY_MODES = new Set(['nginx', 'pingora-direct']);
const config = parseArgs(process.argv.slice(2));
const failures = [];
const envValues = readEnvFile(config.envFile);
checkGatewayMode();
checkPublicBaseUrl();
checkPublicHost();
if (failures.length > 0) {
console.error('[check:production-health-patrol-env] FAILED');
for (const failure of failures) {
console.error(`- ${failure}`);
}
process.exit(1);
}
console.log('[check:production-health-patrol-env] OK');
function parseArgs(argv) {
const result = {
envFile:
process.env.GENARRATIVE_HEALTH_PATROL_ENV_FILE ||
'/etc/genarrative/health-patrol.env',
expectedGatewayMode:
process.env.GENARRATIVE_HEALTH_PATROL_EXPECTED_GATEWAY_MODE || '',
expectedPublicBaseUrl:
process.env.GENARRATIVE_HEALTH_PATROL_EXPECTED_PUBLIC_BASE_URL || '',
expectedPublicHost:
process.env.GENARRATIVE_HEALTH_PATROL_EXPECTED_PUBLIC_HOST || null,
requireEmptyPublicHost:
readBoolEnv('GENARRATIVE_HEALTH_PATROL_REQUIRE_EMPTY_PUBLIC_HOST'),
};
for (let index = 0; index < argv.length; index += 1) {
const arg = argv[index];
switch (arg) {
case '-h':
case '--help':
printUsage();
process.exit(0);
break;
case '--env-file':
result.envFile = requireValue(argv, ++index, arg);
break;
case '--expected-gateway-mode':
result.expectedGatewayMode = requireValue(argv, ++index, arg);
break;
case '--expected-public-base-url':
result.expectedPublicBaseUrl = requireValue(argv, ++index, arg);
break;
case '--expected-public-host':
result.expectedPublicHost = requireValue(argv, ++index, arg);
break;
case '--require-empty-public-host':
result.requireEmptyPublicHost = true;
break;
default:
throw new Error(`未知参数: ${arg}`);
}
}
if (!result.expectedGatewayMode) {
throw new Error(
'必须提供 --expected-gateway-mode 或 GENARRATIVE_HEALTH_PATROL_EXPECTED_GATEWAY_MODE。',
);
}
if (!path.isAbsolute(result.envFile)) {
throw new Error(`--env-file 必须是绝对路径: ${result.envFile}`);
}
if (isFilesystemRootPath(result.envFile)) {
throw new Error('--env-file 不能是文件系统根目录。');
}
if (!GATEWAY_MODES.has(result.expectedGatewayMode)) {
throw new Error(
`--expected-gateway-mode 只支持 nginx 或 pingora-direct: ${result.expectedGatewayMode}`,
);
}
if (result.expectedPublicBaseUrl) {
normalizeBaseUrl(result.expectedPublicBaseUrl, '--expected-public-base-url');
}
if (result.expectedPublicHost !== null) {
validateHostOption(result.expectedPublicHost, '--expected-public-host');
}
if (result.expectedPublicHost !== null && result.requireEmptyPublicHost) {
throw new Error(
'--expected-public-host 和 --require-empty-public-host 不能同时使用。',
);
}
return result;
}
function printUsage() {
console.log(`Usage:
node -- scripts/check-production-health-patrol-env.mjs --expected-gateway-mode <mode> [options]
Options:
--env-file <path> health-patrol env 文件,默认 /etc/genarrative/health-patrol.env。
--expected-gateway-mode <mode> nginx 或 pingora-direct。
--expected-public-base-url <url> 可选,要求 GENARRATIVE_HEALTH_PATROL_PUBLIC_BASE_URL 与该 URL 一致。
--expected-public-host <host> 可选,要求 GENARRATIVE_HEALTH_PATROL_PUBLIC_HOST 与该 Host 一致。
--require-empty-public-host 要求 GENARRATIVE_HEALTH_PATROL_PUBLIC_HOST 为空,适合回退到 Nginx 后复核。
`);
}
function readBoolEnv(name) {
const value = process.env[name];
if (value === undefined || value === null || String(value).trim() === '') {
return false;
}
const normalized = String(value).trim().toLowerCase();
if (['1', 'true', 'yes', 'on'].includes(normalized)) {
return true;
}
if (['0', 'false', 'no', 'off'].includes(normalized)) {
return false;
}
throw new Error(`${name} 必须是布尔值 true/false 或 1/0。`);
}
function requireValue(argv, index, flag) {
const value = argv[index];
if (value === undefined || value.startsWith('--')) {
throw new Error(`${flag} 缺少参数值`);
}
return value;
}
function readEnvFile(filePath) {
let text;
try {
text = readFileSync(filePath, 'utf8');
} catch (error) {
throw new Error(`无法读取 health patrol env 文件 ${filePath}: ${error.message}`);
}
const values = new Map();
for (const [index, rawLine] of text.split(/\r?\n/u).entries()) {
let line = rawLine.trim();
if (!line || line.startsWith('#')) {
continue;
}
if (line.startsWith('export ')) {
line = line.slice('export '.length).trim();
}
const equalsIndex = line.indexOf('=');
if (equalsIndex <= 0) {
failures.push(`第 ${index + 1} 行不是 KEY=VALUE 格式: ${rawLine}`);
continue;
}
const key = line.slice(0, equalsIndex).trim();
const value = stripQuotes(line.slice(equalsIndex + 1).trim());
values.set(key, value);
}
return values;
}
function stripQuotes(value) {
if (value.length >= 2) {
const first = value[0];
const last = value[value.length - 1];
if ((first === '"' && last === '"') || (first === "'" && last === "'")) {
return value.slice(1, -1);
}
}
return value;
}
function envValue(name) {
return envValues.get(name) ?? '';
}
function checkGatewayMode() {
const actual = envValue('GENARRATIVE_HEALTH_PATROL_GATEWAY_MODE');
if (!actual) {
failures.push(
'health patrol env 缺少 GENARRATIVE_HEALTH_PATROL_GATEWAY_MODE。',
);
return;
}
if (actual !== config.expectedGatewayMode) {
failures.push(
`GENARRATIVE_HEALTH_PATROL_GATEWAY_MODE 应为 ${config.expectedGatewayMode},实际 ${actual}。`,
);
}
}
function checkPublicBaseUrl() {
if (!config.expectedPublicBaseUrl) {
return;
}
const actual = envValue('GENARRATIVE_HEALTH_PATROL_PUBLIC_BASE_URL');
if (!actual) {
failures.push(
'health patrol env 缺少 GENARRATIVE_HEALTH_PATROL_PUBLIC_BASE_URL。',
);
return;
}
const expected = normalizeBaseUrl(
config.expectedPublicBaseUrl,
'--expected-public-base-url',
);
const normalizedActual = normalizeBaseUrl(
actual,
'GENARRATIVE_HEALTH_PATROL_PUBLIC_BASE_URL',
);
if (normalizedActual !== expected) {
failures.push(
`GENARRATIVE_HEALTH_PATROL_PUBLIC_BASE_URL 应为 ${expected},实际 ${normalizedActual}。`,
);
}
}
function checkPublicHost() {
const publicHost = envValue('GENARRATIVE_HEALTH_PATROL_PUBLIC_HOST');
const baseUrl =
config.expectedPublicBaseUrl ||
envValue('GENARRATIVE_HEALTH_PATROL_PUBLIC_BASE_URL');
if (publicHost) {
try {
validateHostOption(publicHost, 'GENARRATIVE_HEALTH_PATROL_PUBLIC_HOST');
} catch (error) {
failures.push(error.message);
}
}
if (config.expectedPublicHost !== null && publicHost !== config.expectedPublicHost) {
failures.push(
`GENARRATIVE_HEALTH_PATROL_PUBLIC_HOST 应为 ${config.expectedPublicHost},实际 ${publicHost || '(空)'}。`,
);
}
if (config.requireEmptyPublicHost && publicHost) {
failures.push(
`GENARRATIVE_HEALTH_PATROL_PUBLIC_HOST 应为空,实际 ${publicHost}。`,
);
}
if (
config.expectedGatewayMode === 'pingora-direct' &&
baseUrl &&
isLoopbackUrl(baseUrl) &&
!publicHost
) {
failures.push(
'Pingora direct 模式使用本机 public base URL 时必须配置 GENARRATIVE_HEALTH_PATROL_PUBLIC_HOST,避免巡检命中默认 vhost。',
);
}
}
function normalizeBaseUrl(value, label) {
try {
const parsed = new URL(value);
if (!['http:', 'https:'].includes(parsed.protocol)) {
throw new Error('unsupported protocol');
}
return parsed.toString().replace(/\/+$/u, '');
} catch {
throw new Error(`${label} 必须是合法 http(s) URL: ${value}`);
}
}
function validateHostOption(value, label) {
const raw = String(value);
if (raw !== raw.trim() || raw.includes('://') || /[\s/?#@]/.test(raw)) {
throw new Error(
`${label} 只能是 host 或 host:port,不能包含 scheme、路径、查询、片段或空白字符。`,
);
}
try {
const parsed = new URL(`https://${raw}`);
if (
!parsed.hostname ||
parsed.pathname !== '/' ||
parsed.search ||
parsed.hash ||
parsed.username ||
parsed.password
) {
throw new Error('invalid host');
}
} catch {
throw new Error(`${label} 不是合法的 host 或 host:port。`);
}
}
function isFilesystemRootPath(value) {
const resolved = path.resolve(String(value));
return resolved === path.parse(resolved).root;
}
function isLoopbackUrl(value) {
try {
const hostname = new URL(value).hostname.toLowerCase();
return (
hostname === 'localhost' ||
hostname === '::1' ||
hostname === '[::1]' ||
hostname.startsWith('127.')
);
} catch {
return false;
}
}