加固部署隔离与备份来源契约(07-26-internal-ops-recovery-baseline)
这个提交包含在:
@@ -4,10 +4,21 @@ const path = require('node:path');
|
||||
|
||||
const rootDir = path.resolve(__dirname, '..', '..');
|
||||
const configPath = path.join(rootDir, 'server/src/config.js');
|
||||
const envExamplePath = path.join(rootDir, 'deploy/trialrun/.env.example');
|
||||
const composePath = path.join(rootDir, 'deploy/trialrun/docker-compose.yml');
|
||||
const envExamplePath = path.resolve(
|
||||
process.env.ENV_CONTRACT_ENV_EXAMPLE_PATH || path.join(rootDir, 'deploy/trialrun/.env.example'),
|
||||
);
|
||||
const composePath = path.resolve(
|
||||
process.env.ENV_CONTRACT_COMPOSE_PATH || path.join(rootDir, 'deploy/trialrun/docker-compose.yml'),
|
||||
);
|
||||
const runbookPath = path.join(rootDir, 'deploy/trialrun/runbook.md');
|
||||
const checklistPath = path.join(rootDir, 'deploy/trialrun/checklist.md');
|
||||
const deployScriptPath = path.resolve(
|
||||
process.env.ENV_CONTRACT_DEPLOY_SCRIPT_PATH || path.join(rootDir, 'deploy/trialrun/deploy.sh'),
|
||||
);
|
||||
const timerScriptPath = path.resolve(
|
||||
process.env.ENV_CONTRACT_TIMER_SCRIPT_PATH
|
||||
|| path.join(rootDir, 'deploy/trialrun/install-warch3-ops-timer.sh'),
|
||||
);
|
||||
const nginxPaths = [
|
||||
path.join(rootDir, 'deploy/trialrun/nginx-hyb-oa.conf'),
|
||||
path.join(rootDir, 'deploy/trialrun/nginx.conf.example'),
|
||||
@@ -130,6 +141,8 @@ const envExampleContent = readFile(envExamplePath);
|
||||
const composeContent = readFile(composePath);
|
||||
const runbookContent = readFile(runbookPath);
|
||||
const checklistContent = readFile(checklistPath);
|
||||
const deployScriptContent = readFile(deployScriptPath);
|
||||
const timerScriptContent = readFile(timerScriptPath);
|
||||
const nginxContents = nginxPaths.map((filePath) => ({
|
||||
filePath,
|
||||
content: readFile(filePath),
|
||||
@@ -150,6 +163,15 @@ for (const key of deploymentInjectedEnvKeys) {
|
||||
}
|
||||
}
|
||||
|
||||
for (const key of ['APP_HOST_PORT', 'POSTGRES_DATA_DIR', 'STORAGE_HOST_DIR']) {
|
||||
if (!envExampleKeys.has(key)) {
|
||||
errors.push(`.env.example 缺少 Compose 宿主契约键 ${key}`);
|
||||
}
|
||||
}
|
||||
if (envExampleKeys.has('COMPOSE_PROJECT_NAME')) {
|
||||
errors.push('.env.example 不应持久化 COMPOSE_PROJECT_NAME,调用方必须通过 -p 显式选择项目');
|
||||
}
|
||||
|
||||
for (const serviceName of requiredServices) {
|
||||
const block = extractServiceBlock(composeContent, serviceName);
|
||||
if (!block) {
|
||||
@@ -189,6 +211,38 @@ if (!serverBlock || !/^\s{6}-\s*["']127\.0\.0\.1:\$\{APP_HOST_PORT:-3000\}:3000[
|
||||
errors.push('服务 server 必须通过 APP_HOST_PORT 将容器 3000 端口只绑定到宿主回环地址');
|
||||
}
|
||||
|
||||
const dbBlock = extractServiceBlock(composeContent, 'db');
|
||||
if (!dbBlock) {
|
||||
errors.push('docker-compose.yml 缺少服务 db');
|
||||
} else {
|
||||
if (/^\s{4}ports:\s*$/m.test(dbBlock)) {
|
||||
errors.push('服务 db 不得向宿主发布端口');
|
||||
}
|
||||
if (!/\$\{POSTGRES_DATA_DIR:-\.\/data\/postgres\}:\/var\/lib\/postgresql\/data/.test(dbBlock)) {
|
||||
errors.push('服务 db 必须通过 POSTGRES_DATA_DIR 参数化宿主数据目录');
|
||||
}
|
||||
}
|
||||
|
||||
if (/^\s+container_name:\s*/m.test(composeContent)) {
|
||||
errors.push('docker-compose.yml 不得固定 container_name,容器身份必须由 Compose project 隔离');
|
||||
}
|
||||
|
||||
for (const serviceName of ['server', 'worker', 'init']) {
|
||||
const block = extractServiceBlock(composeContent, serviceName);
|
||||
if (block && !/\$\{STORAGE_HOST_DIR:-\.\/data\/storage\}:\$\{STORAGE_DIR\}/.test(block)) {
|
||||
errors.push(`服务 ${serviceName} 必须通过 STORAGE_HOST_DIR 参数化宿主附件目录`);
|
||||
}
|
||||
}
|
||||
|
||||
for (const [name, content] of [
|
||||
['deploy.sh', deployScriptContent],
|
||||
['install-warch3-ops-timer.sh', timerScriptContent],
|
||||
]) {
|
||||
if (!content.includes('-p "$COMPOSE_PROJECT_NAME"')) {
|
||||
errors.push(`${name} 必须向 Compose 显式传入独立 project name`);
|
||||
}
|
||||
}
|
||||
|
||||
for (const { filePath, content } of nginxContents) {
|
||||
const relativePath = path.relative(rootDir, filePath);
|
||||
if (content.includes('$proxy_add_x_forwarded_for')) {
|
||||
|
||||
在新工单中引用
屏蔽一个用户