101 lines
4.3 KiB
JavaScript
101 lines
4.3 KiB
JavaScript
import fs from 'node:fs';
|
|
|
|
const MAX_SAFE_DEV_MEMORY = 4 * 1024 * 1024 * 1024;
|
|
const MAX_SAFE_DEV_CPUS = 4;
|
|
const MAX_SAFE_DEV_PIDS = 256;
|
|
|
|
const positive = (value) => typeof value === 'number' ? value > 0 : Number(value) > 0;
|
|
|
|
export const validateComposeModel = (model, mode) => {
|
|
const errors = [];
|
|
const runtime = model?.services?.runtime;
|
|
if (!runtime) return ['runtime service is missing'];
|
|
|
|
const requiredImageEnvironment = {
|
|
GATEWAY_IMAGE_UPLOAD_SECRET_FILE: '/run/secrets/image_upload_core2026_secret',
|
|
GAME_IMAGE_UPLOAD_SECRET_FILE: '/run/secrets/image_upload_core2026_secret',
|
|
IMAGE_SYNC_SECRET_FILE: '/run/secrets/image_sync_core2026_secret',
|
|
};
|
|
for (const [key, expected] of Object.entries(requiredImageEnvironment)) {
|
|
if (runtime.environment?.[key] !== expected) errors.push(`${key} must be ${expected}`);
|
|
}
|
|
for (const key of [
|
|
'GATEWAY_IMAGE_UPLOAD_URL',
|
|
'GATEWAY_SHARED_ICON_PUBLIC_URL',
|
|
'GATEWAY_USER_ICON_PUBLIC_URL',
|
|
'GAME_IMAGE_UPLOAD_URL',
|
|
'GAME_CONTENT_IMAGE_PUBLIC_URL',
|
|
'IMAGE_SYNC_URL',
|
|
'VITE_IMAGE_PUBLIC_URL',
|
|
'VITE_GATEWAY_USER_ICON_BASE_URL',
|
|
]) {
|
|
if (!runtime.environment?.[key]) errors.push(`${key} must be configured`);
|
|
}
|
|
const mounts = Array.isArray(runtime.volumes) ? runtime.volumes : [];
|
|
for (const target of [
|
|
'/run/secrets/image_upload_core2026_secret',
|
|
'/run/secrets/image_sync_core2026_secret',
|
|
]) {
|
|
const mount = mounts.find((candidate) => candidate?.target === target);
|
|
if (!mount || mount.type !== 'bind' || mount.read_only !== true) {
|
|
errors.push(`${target} must be a read-only bind mount`);
|
|
}
|
|
}
|
|
|
|
if (!positive(runtime.mem_limit)) errors.push('runtime memory limit must be positive');
|
|
if (!positive(runtime.memswap_limit)) errors.push('runtime memory+swap limit must be positive');
|
|
if (Number(runtime.memswap_limit) !== Number(runtime.mem_limit)) {
|
|
errors.push('runtime memory+swap limit must equal its memory limit');
|
|
}
|
|
if (!positive(runtime.cpus)) errors.push('runtime CPU limit must be positive');
|
|
if (!positive(runtime.pids_limit)) errors.push('runtime PID limit must be positive');
|
|
|
|
const nodeOptions = runtime.environment?.NODE_OPTIONS ?? '';
|
|
const heapMatch = /(?:^|\s)--max-old-space-size=(\d+)(?:\s|$)/.exec(nodeOptions);
|
|
const heapMiB = Number(heapMatch?.[1] ?? 0);
|
|
if (!heapMatch || heapMiB < 512 || heapMiB > 2048) {
|
|
errors.push('runtime Node heap limit must be between 512 and 2048 MiB');
|
|
}
|
|
const rayonThreads = Number(runtime.environment?.RAYON_NUM_THREADS ?? 0);
|
|
if (!Number.isInteger(rayonThreads) || rayonThreads < 1 || rayonThreads > 4) {
|
|
errors.push('runtime Rayon thread count must be between 1 and 4');
|
|
}
|
|
|
|
if (mode === 'development') {
|
|
if (runtime.environment?.RUNTIME_MODE !== 'development') {
|
|
errors.push('development runtime mode must be literal development');
|
|
}
|
|
if (runtime.environment?.CORE_SOURCE_MODE !== 'bind') {
|
|
errors.push('development Core source mode must be bind');
|
|
}
|
|
} else if (runtime.environment?.CORE_SOURCE_MODE !== 'clone') {
|
|
errors.push(`${mode} Core source mode must be clone`);
|
|
} else if (runtime.environment?.RUNTIME_MODE === 'development') {
|
|
errors.push(`${mode} runtime must not use development mode`);
|
|
}
|
|
|
|
if (mode === 'development' || mode === 'smoke') {
|
|
for (const name of ['postgres', 'redis', 'runtime', 'caddy']) {
|
|
if (model?.services?.[name]?.restart !== 'no') errors.push(`${name} ${mode} restart policy must be no`);
|
|
}
|
|
if (Number(runtime.mem_limit) > MAX_SAFE_DEV_MEMORY) errors.push(`${mode} runtime memory limit exceeds 4 GiB`);
|
|
if (Number(runtime.cpus) > MAX_SAFE_DEV_CPUS) errors.push(`${mode} runtime CPU limit exceeds 4`);
|
|
if (Number(runtime.pids_limit) > MAX_SAFE_DEV_PIDS) errors.push(`${mode} runtime PID limit exceeds 256`);
|
|
}
|
|
|
|
return errors;
|
|
};
|
|
|
|
if (process.argv[1] && process.argv[1].endsWith('validate-compose-model.mjs')) {
|
|
const mode = process.argv[2];
|
|
const model = JSON.parse(fs.readFileSync(0, 'utf8'));
|
|
const errors = validateComposeModel(model, mode);
|
|
if (errors.length) {
|
|
console.error(`Compose ${mode} safety validation failed:`);
|
|
for (const error of errors) console.error(`- ${error}`);
|
|
process.exitCode = 65;
|
|
} else {
|
|
console.log(`Compose ${mode} safety validation passed.`);
|
|
}
|
|
}
|