完善 Pingora 直连切换门禁
新增 Pingora shadow env 回切脚本与对应检查。 补齐直连证据包时间线和 cutoverRunId 审计门禁。 支持 Gitea Host 透传并更新直连多域名文档。 修复百分号编码静态图标路径并补 smoke 覆盖。 更新生产发布与运维护栏对 Pingora 发布包的校验。
This commit is contained in:
@@ -0,0 +1,254 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
import {
|
||||
chownSync,
|
||||
chmodSync,
|
||||
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;
|
||||
}
|
||||
@@ -706,6 +706,7 @@ PINGORA_ROLLBACK_SCRIPT_SOURCE="${SOURCE_DIR}/scripts/deploy/pingora-direct-roll
|
||||
PINGORA_REALPATH_CANARY_ENABLE_SCRIPT_SOURCE="${SOURCE_DIR}/scripts/deploy/pingora-realpath-canary-enable.sh"
|
||||
PINGORA_REALPATH_CANARY_DISABLE_SCRIPT_SOURCE="${SOURCE_DIR}/scripts/deploy/pingora-realpath-canary-disable.sh"
|
||||
PINGORA_HEALTH_PATROL_ENV_SWITCH_SCRIPT_SOURCE="${SOURCE_DIR}/scripts/deploy/pingora-health-patrol-env-switch.mjs"
|
||||
PINGORA_GATEWAY_ENV_SHADOW_SWITCH_SCRIPT_SOURCE="${SOURCE_DIR}/scripts/deploy/pingora-gateway-env-shadow-switch.mjs"
|
||||
PINGORA_TLS_CERT_SYNC_SCRIPT_SOURCE="${SOURCE_DIR}/scripts/deploy/pingora-tls-cert-sync.mjs"
|
||||
PINGORA_PREFLIGHT_SCRIPT_SOURCE="${SOURCE_DIR}/scripts/check-pingora-direct-preflight.mjs"
|
||||
PINGORA_LIVE_SCRIPT_SOURCE="${SOURCE_DIR}/scripts/check-pingora-direct-live.mjs"
|
||||
@@ -812,6 +813,12 @@ if [[ ! -f "${PINGORA_HEALTH_PATROL_ENV_SWITCH_SCRIPT_SOURCE}" ]]; then
|
||||
fi
|
||||
cp "${PINGORA_HEALTH_PATROL_ENV_SWITCH_SCRIPT_SOURCE}" "${RELEASE_CONTENT_DIR}/scripts/deploy/pingora-health-patrol-env-switch.mjs"
|
||||
chmod 0755 "${RELEASE_CONTENT_DIR}/scripts/deploy/pingora-health-patrol-env-switch.mjs"
|
||||
if [[ ! -f "${PINGORA_GATEWAY_ENV_SHADOW_SWITCH_SCRIPT_SOURCE}" ]]; then
|
||||
echo "[production-api-deploy] 发布产物缺少 Pingora gateway env shadow 切换脚本: ${SOURCE_DIR}/scripts/deploy/pingora-gateway-env-shadow-switch.mjs" >&2
|
||||
exit 1
|
||||
fi
|
||||
cp "${PINGORA_GATEWAY_ENV_SHADOW_SWITCH_SCRIPT_SOURCE}" "${RELEASE_CONTENT_DIR}/scripts/deploy/pingora-gateway-env-shadow-switch.mjs"
|
||||
chmod 0755 "${RELEASE_CONTENT_DIR}/scripts/deploy/pingora-gateway-env-shadow-switch.mjs"
|
||||
if [[ ! -f "${PINGORA_TLS_CERT_SYNC_SCRIPT_SOURCE}" ]]; then
|
||||
echo "[production-api-deploy] 发布产物缺少 Pingora TLS 证书同步脚本: ${SOURCE_DIR}/scripts/deploy/pingora-tls-cert-sync.mjs" >&2
|
||||
exit 1
|
||||
|
||||
Reference in New Issue
Block a user