Files
Genarrative/scripts/deploy/pingora-gateway-env-shadow-switch.mjs
T
kdletters 387a1c26e3 完成全量检查整改
清理已跟踪原始日志与个人本地配置
修复前后端有效门禁、测试和构建问题
补齐生产 Jenkins、SpacetimeDB 本地命令与文档约束
收紧图片编辑器状态、附件与媒体引用测试
排除已下线旧创作入口测试并清理 warning
2026-07-17 16:56:46 +08:00

255 lines
6.9 KiB
JavaScript

#!/usr/bin/env node
import {
chmodSync,
chownSync,
lstatSync,
mkdtempSync,
readFileSync,
renameSync,
rmSync,
writeFileSync,
} from 'node:fs';
import { tmpdir } from 'node:os';
import path from 'node:path';
const TARGET_KEYS = [
'GENARRATIVE_PINGORA_GATEWAY_LISTEN',
'GENARRATIVE_PINGORA_GATEWAY_TLS_LISTEN',
'GENARRATIVE_PINGORA_GATEWAY_HTTP_REDIRECT_LISTEN',
'GENARRATIVE_PINGORA_GATEWAY_TLS_CERT_FILE',
'GENARRATIVE_PINGORA_GATEWAY_TLS_KEY_FILE',
];
const SHADOW_VALUES = {
GENARRATIVE_PINGORA_GATEWAY_LISTEN: '127.0.0.1:18081',
GENARRATIVE_PINGORA_GATEWAY_TLS_LISTEN: '',
GENARRATIVE_PINGORA_GATEWAY_HTTP_REDIRECT_LISTEN: '',
GENARRATIVE_PINGORA_GATEWAY_TLS_CERT_FILE: '',
GENARRATIVE_PINGORA_GATEWAY_TLS_KEY_FILE: '',
};
const DRY_RUN_ENV_FILE_MODE = 0o600;
const config = parseArgs(process.argv.slice(2));
if (config.apply) {
assertEnvFileIsWritableRegularFile(config.envFile);
}
const originalText = readFileSync(config.envFile, 'utf8');
const nextText = rewriteEnvFile(originalText, SHADOW_VALUES);
const checkFile = writeDryRunEnv(nextText);
try {
console.log(
`[pingora-gateway-env-shadow-switch] apply=${config.apply} env=${config.envFile}`,
);
for (const key of TARGET_KEYS) {
console.log(`~ ${key}=${SHADOW_VALUES[key]}`);
}
assertShadowEnv(checkFile);
if (!config.apply) {
console.log(
'[pingora-gateway-env-shadow-switch] 当前是 dry-run;追加 --apply 后才会写入 env 文件。',
);
} else {
writeEnvFileAtomically(config.envFile, nextText);
}
console.log('[pingora-gateway-env-shadow-switch] 完成');
} finally {
rmSync(path.dirname(checkFile), { recursive: true, force: true });
}
function parseArgs(argv) {
const result = {
envFile:
process.env.GENARRATIVE_PINGORA_GATEWAY_ENV_FILE ||
'/etc/genarrative/pingora-gateway.env',
apply: false,
};
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 '--apply':
result.apply = true;
break;
default:
throw new Error(`未知参数: ${arg}`);
}
}
if (!path.isAbsolute(result.envFile)) {
throw new Error(`--env-file 必须是绝对路径: ${result.envFile}`);
}
validateNoControlCharacters(result.envFile, '--env-file');
if (isFilesystemRootPath(result.envFile)) {
throw new Error('--env-file 不能是文件系统根目录。');
}
return result;
}
function printUsage() {
console.log(`Usage:
node scripts/deploy/pingora-gateway-env-shadow-switch.mjs [options]
Options:
--env-file <path> Pingora gateway env 文件,默认 /etc/genarrative/pingora-gateway.env。
--apply 写入 env 文件;默认只 dry-run 并用临时 env 复核。
`);
}
function requireValue(argv, index, flag) {
const value = argv[index];
if (value === undefined || value.startsWith('--')) {
throw new Error(`${flag} 缺少参数值`);
}
return value;
}
function rewriteEnvFile(text, nextValuesByKey) {
const lines = text.replace(/\r\n/gu, '\n').split('\n');
if (lines.at(-1) === '') {
lines.pop();
}
const seen = new Set();
const output = [];
for (const line of lines) {
const assignment = parseAssignmentLine(line);
if (!assignment || !TARGET_KEYS.includes(assignment.key)) {
output.push(line);
continue;
}
if (seen.has(assignment.key)) {
throw new Error(`pingora gateway env 中存在重复配置: ${assignment.key}`);
}
seen.add(assignment.key);
output.push(
`${assignment.leading}${assignment.exportPrefix}${assignment.key}=${nextValuesByKey[assignment.key]}`,
);
}
for (const key of TARGET_KEYS) {
if (!seen.has(key)) {
output.push(`${key}=${nextValuesByKey[key]}`);
}
}
return `${output.join('\n')}\n`;
}
function parseAssignmentLine(line) {
const match = line.match(/^(\s*)(export\s+)?([A-Za-z_][A-Za-z0-9_]*)\s*=/u);
if (!match) {
return null;
}
return {
leading: match[1] || '',
exportPrefix: match[2] || '',
key: match[3],
};
}
function writeDryRunEnv(content) {
const dir = mkdtempSync(
path.join(tmpdir(), 'genarrative-pingora-gateway-env-shadow-switch-'),
);
const filePath = path.join(dir, 'pingora-gateway.env');
writeFileSync(filePath, content, {
encoding: 'utf8',
mode: DRY_RUN_ENV_FILE_MODE,
});
chmodSync(filePath, DRY_RUN_ENV_FILE_MODE);
return filePath;
}
function assertShadowEnv(filePath) {
const values = parseEnvFile(readFileSync(filePath, 'utf8'));
if (
values.GENARRATIVE_PINGORA_GATEWAY_LISTEN !==
SHADOW_VALUES.GENARRATIVE_PINGORA_GATEWAY_LISTEN
) {
throw new Error(
'shadow env 复核失败:GENARRATIVE_PINGORA_GATEWAY_LISTEN 必须是 127.0.0.1:18081。',
);
}
for (const key of [
'GENARRATIVE_PINGORA_GATEWAY_TLS_LISTEN',
'GENARRATIVE_PINGORA_GATEWAY_HTTP_REDIRECT_LISTEN',
'GENARRATIVE_PINGORA_GATEWAY_TLS_CERT_FILE',
'GENARRATIVE_PINGORA_GATEWAY_TLS_KEY_FILE',
]) {
if ((values[key] || '') !== '') {
throw new Error(`shadow env 复核失败:${key} 必须为空。`);
}
}
}
function parseEnvFile(text) {
const values = {};
for (const line of text.split(/\r?\n/u)) {
const assignment = parseAssignmentLine(line);
if (!assignment) {
continue;
}
values[assignment.key] = line
.slice(line.indexOf('=') + 1)
.trim()
.replace(/^['"]|['"]$/gu, '');
}
return values;
}
function writeEnvFileAtomically(filePath, content) {
const currentStat = assertEnvFileIsWritableRegularFile(filePath);
const currentMode = currentStat.mode & 0o777;
const tempFile = path.join(
path.dirname(filePath),
`.${path.basename(filePath)}.${process.pid}.${Date.now()}.tmp`,
);
try {
writeFileSync(tempFile, content, { encoding: 'utf8', mode: currentMode });
chownSync(tempFile, currentStat.uid, currentStat.gid);
chmodSync(tempFile, currentMode);
assertEnvFileIsWritableRegularFile(filePath);
renameSync(tempFile, filePath);
} catch (error) {
rmSync(tempFile, { force: true });
throw error;
}
}
function assertEnvFileIsWritableRegularFile(filePath) {
const currentStat = lstatSync(filePath);
if (currentStat.isSymbolicLink()) {
throw new Error(
`--env-file 不能是符号链接;请直接传真实 Pingora gateway env 文件路径: ${filePath}`,
);
}
if (!currentStat.isFile()) {
throw new Error(`--env-file 必须指向普通文件: ${filePath}`);
}
return currentStat;
}
function validateNoControlCharacters(value, label) {
if (/[\0\r\n]/u.test(String(value ?? ''))) {
throw new Error(`${label} 不能包含换行或 NUL 字符。`);
}
}
function isFilesystemRootPath(value) {
const resolved = path.resolve(String(value));
return resolved === path.parse(resolved).root;
}