158 行
7.4 KiB
JavaScript
158 行
7.4 KiB
JavaScript
const fs = require('node:fs');
|
|
const path = require('node:path');
|
|
const { spawnSync } = require('node:child_process');
|
|
|
|
const GLOBAL_VALUES = new Set(['-p', '--project-name', '-f', '--file', '--env-file', '--profile']);
|
|
const OPTION_VALUES = new Set(['--entrypoint', '-e', '--env', '--tail', '--format', '--type', '--timeout', '-t']);
|
|
const SWITCHES = new Set(['-d', '--detach', '--build', '--no-build', '--no-deps', '--rm', '-T', '-q', '--quiet', '-a', '--all', '-f', '--force', '--no-trunc']);
|
|
const LIFECYCLE = new Set(['up', 'down', 'run', 'start', 'stop', 'restart', 'rm', 'create', 'kill', 'pause', 'unpause']);
|
|
|
|
function parseCompose(argv) {
|
|
const args = [...argv];
|
|
if (args[0] === 'compose') args.shift();
|
|
const globals = {};
|
|
while (args[0]?.startsWith('-')) {
|
|
const option = args.shift();
|
|
if (!GLOBAL_VALUES.has(option) || !args.length) throw new Error('unknown_compose_global');
|
|
globals[option] = args.shift();
|
|
}
|
|
const operation = args.shift();
|
|
const targets = [];
|
|
let noDeps = false;
|
|
while (args.length) {
|
|
const argument = args.shift();
|
|
if (argument.startsWith('-')) {
|
|
if (OPTION_VALUES.has(argument)) {
|
|
if (!args.length) throw new Error('missing_option_value');
|
|
args.shift();
|
|
} else if (!SWITCHES.has(argument)) throw new Error('unknown_compose_option');
|
|
if (argument === '--no-deps') noDeps = true;
|
|
} else {
|
|
targets.push(argument);
|
|
if (['run', 'exec', 'port'].includes(operation)) break;
|
|
}
|
|
}
|
|
return { operation, targets, noDeps, globals, containerArgs: args };
|
|
}
|
|
|
|
function lifecycleServices(call, services) {
|
|
if (!LIFECYCLE.has(call.operation)) return [];
|
|
const selected = call.operation === 'down' || !call.targets.length ? Object.keys(services) : call.targets;
|
|
const result = new Set();
|
|
function visit(service) {
|
|
if (!Object.hasOwn(services, service)) throw new Error('unknown_compose_service');
|
|
if (result.has(service)) return;
|
|
result.add(service);
|
|
if (['up', 'run'].includes(call.operation) && !call.noDeps) {
|
|
const dependencies = services[service].depends_on ?? {};
|
|
for (const dependency of Array.isArray(dependencies) ? dependencies : Object.keys(dependencies)) visit(dependency);
|
|
}
|
|
}
|
|
selected.forEach(visit);
|
|
return [...result];
|
|
}
|
|
|
|
function main() {
|
|
const root = process.env.HYB_DEPLOYMENT_FIXTURE_ROOT;
|
|
if (!root || !path.basename(root).startsWith('hyb-db-lifecycle-') || path.dirname(process.argv[1]) !== path.join(root, 'bin')) {
|
|
throw new Error('fixture_only');
|
|
}
|
|
const config = JSON.parse(fs.readFileSync(path.join(root, 'config.json'), 'utf8'));
|
|
const command = path.basename(process.argv[1]);
|
|
const args = process.argv.slice(2);
|
|
const event = { command, args };
|
|
if (command === 'docker-compose' || (command === 'docker' && args[0] === 'compose')) {
|
|
event.compose = parseCompose(args);
|
|
event.lifecycle = lifecycleServices(event.compose, config.services);
|
|
}
|
|
fs.appendFileSync(path.join(root, 'trace.jsonl'), JSON.stringify(event) + '\n');
|
|
if (event.compose && config.driftAfter === event.compose.operation + ':' + event.compose.targets.join()) {
|
|
fs.writeFileSync(path.join(root, 'health-drift'), '1');
|
|
}
|
|
if (command === 'timeout') {
|
|
const child = spawnSync('/usr/bin/timeout', args, { env: process.env, stdio: 'inherit' });
|
|
process.exit(child.status ?? (child.signal === 'SIGKILL' ? 137 : 99));
|
|
}
|
|
if (command === 'sleep') return;
|
|
if (command === 'curl') {
|
|
if (args.join(' ') !== '-fsS http://127.0.0.1:31001/ready') throw new Error('unexpected_network_request');
|
|
console.log(JSON.stringify({ data: { buildSha: '123456789abc' } }));
|
|
return;
|
|
}
|
|
if (command === 'docker' && args[0] === 'inspect') {
|
|
if (args.at(-1) === 'e'.repeat(64) && args.includes('{{.Config.Image}}')) {
|
|
console.log('synthetic-project-worker:latest');
|
|
return;
|
|
}
|
|
if (args.at(-1) !== 'd'.repeat(64)) throw new Error('unexpected_container');
|
|
if (config.health === 'inspect_error') process.exit(18);
|
|
const counterFile = path.join(root, 'inspect-count');
|
|
const count = fs.existsSync(counterFile) ? Number(fs.readFileSync(counterFile, 'utf8')) : 0;
|
|
fs.writeFileSync(counterFile, String(count + 1));
|
|
if (config.health === 'query_hang') {
|
|
setTimeout(() => {}, 20000);
|
|
return;
|
|
}
|
|
const states = { stopped: 'exited|healthy', unhealthy: 'running|unhealthy', no_health: 'running|missing', starting: 'running|starting' };
|
|
const state = fs.existsSync(path.join(root, 'health-drift')) ? 'running|unhealthy' : config.health === 'warming' && count === 0 ? 'running|starting' : states[config.health] ?? 'running|healthy';
|
|
console.log(state);
|
|
return;
|
|
}
|
|
if (command === 'docker' && args.join(' ') === 'image inspect synthetic-project-worker:latest') process.exit(config.workerTagPresent ? 0 : 1);
|
|
const call = event.compose;
|
|
if (!call) throw new Error('forbidden_external_command');
|
|
if (call.operation === 'version') {
|
|
process.exit(command === 'docker' && config.engine === 'v1' ? 1 : 0);
|
|
}
|
|
if (call.globals['-p'] !== 'synthetic-project') throw new Error('wrong_compose_project');
|
|
if (call.noDeps && config.unsupportedIsolation) process.exit(2);
|
|
if (LIFECYCLE.has(call.operation)) {
|
|
if (call.operation === 'up' && call.targets.includes('db')) fs.writeFileSync(path.join(root, 'db-started'), '1');
|
|
if (call.operation === 'run') {
|
|
if (call.targets.join() !== 'init' || call.containerArgs.join(' ') !== '-lc cd /app/server && npm run deploy:init' ||
|
|
!args.includes('DEPLOY_DATA_MODE=' + process.env.DEPLOY_DATA_MODE)) {
|
|
throw new Error('unexpected_init_command');
|
|
}
|
|
if (config.initFails) process.exit(23);
|
|
}
|
|
return;
|
|
}
|
|
if (call.operation === 'ps') {
|
|
if (!call.targets.length) return;
|
|
if (call.targets.join() === 'worker') {
|
|
if (config.workerImagePresent) console.log('e'.repeat(64));
|
|
return;
|
|
}
|
|
if (call.targets.join() !== 'db') throw new Error('unexpected_ps_service');
|
|
if (config.health === 'query_error') process.exit(17);
|
|
if (config.health === 'missing' && !fs.existsSync(path.join(root, 'db-started'))) return;
|
|
console.log(config.health === 'multiple' ? 'd'.repeat(64) + '\n' + 'e'.repeat(64) : 'd'.repeat(64));
|
|
return;
|
|
}
|
|
if (call.operation === 'port' && call.targets.join() === 'server' && call.containerArgs.join() === '3000') {
|
|
console.log('127.0.0.1:31001');
|
|
return;
|
|
}
|
|
if (call.operation === 'config') return;
|
|
if (call.operation === 'build' && ['init', 'server', 'worker'].includes(call.targets.join())) return;
|
|
if (call.operation === 'logs' && call.targets.join() === 'server') return;
|
|
if (call.operation === 'exec' && call.targets.join() === 'server') {
|
|
const expected = ['sh -lc cd /app/server && npx prisma migrate status', 'node src/scripts/smoke-external-deps.js', 'node src/scripts/smoke-trialrun-runtime.js'];
|
|
if (expected.includes(call.containerArgs.join(' '))) return;
|
|
if (call.containerArgs[0] === 'node' && call.containerArgs[1] === '-e' && call.containerArgs[2]?.includes('EXPECTED_BUILD_SHA')) {
|
|
const input = JSON.parse(fs.readFileSync(0, 'utf8'));
|
|
if (input.data?.buildSha !== '123456789abc') throw new Error('wrong_ready_build');
|
|
return;
|
|
}
|
|
}
|
|
throw new Error('unknown_fake_command');
|
|
}
|
|
|
|
module.exports = { parseCompose, lifecycleServices };
|
|
if (require.main === module) {
|
|
try { main(); } catch (error) {
|
|
console.error('[deployment-fixture] ' + (/^[a-z_]+$/.test(error.message) ? error.message : 'unexpected_failure'));
|
|
process.exitCode = 97;
|
|
}
|
|
}
|