387a1c26e3
清理已跟踪原始日志与个人本地配置 修复前后端有效门禁、测试和构建问题 补齐生产 Jenkins、SpacetimeDB 本地命令与文档约束 收紧图片编辑器状态、附件与媒体引用测试 排除已下线旧创作入口测试并清理 warning
255 lines
6.9 KiB
JavaScript
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;
|
|
}
|