mirror of
https://github.com/whyour/qinglong.git
synced 2026-09-21 01:00:24 +08:00
181 lines
5.5 KiB
JavaScript
181 lines
5.5 KiB
JavaScript
#!/usr/bin/env node
|
|
|
|
const fs = require('node:fs');
|
|
const os = require('node:os');
|
|
const path = require('node:path');
|
|
const { spawnSync } = require('node:child_process');
|
|
|
|
const IMAGE_PATTERN =
|
|
/^[A-Za-z0-9][A-Za-z0-9._:/-]{0,254}@sha256:[0-9a-f]{64}$/;
|
|
|
|
function fail(message) {
|
|
throw new Error(`QingLong local Compose preflight failed: ${message}`);
|
|
}
|
|
|
|
function argumentsValue(argv) {
|
|
const values = Object.fromEntries(
|
|
argv.map((argument) => {
|
|
const separator = argument.indexOf('=');
|
|
if (separator < 3 || !argument.startsWith('--')) {
|
|
fail('arguments must use --name=value');
|
|
}
|
|
return [argument.slice(2, separator), argument.slice(separator + 1)];
|
|
}),
|
|
);
|
|
if (
|
|
Object.keys(values).length !== argv.length ||
|
|
!IMAGE_PATTERN.test(values.image ?? '') ||
|
|
(values.profile !== undefined &&
|
|
values.profile !== 'edge' &&
|
|
values.profile !== 'standalone') ||
|
|
typeof values['docker-executable'] !== 'string' ||
|
|
typeof values['docker-socket'] !== 'string'
|
|
) {
|
|
fail(
|
|
'usage: --image=repository@sha256:digest --docker-executable=/absolute/docker --docker-socket=/absolute/docker.sock [--profile=edge|standalone]',
|
|
);
|
|
}
|
|
const dockerExecutable = fs.realpathSync(values['docker-executable']);
|
|
const dockerSocket = fs.realpathSync(values['docker-socket']);
|
|
if (
|
|
dockerExecutable !== values['docker-executable'] ||
|
|
dockerSocket !== values['docker-socket']
|
|
) {
|
|
fail('Docker executable and socket must be canonical paths');
|
|
}
|
|
return Object.freeze({
|
|
image: values.image,
|
|
profile: values.profile ?? 'edge',
|
|
dockerExecutable,
|
|
dockerSocket,
|
|
});
|
|
}
|
|
|
|
async function main() {
|
|
if (process.versions.node.split('.')[0] !== '24') {
|
|
fail('Node 24 is required');
|
|
}
|
|
if (
|
|
typeof process.getuid !== 'function' ||
|
|
typeof process.getgid !== 'function'
|
|
) {
|
|
fail('a POSIX identity is required');
|
|
}
|
|
const input = argumentsValue(process.argv.slice(2));
|
|
const root = path.resolve(__dirname, '..');
|
|
const { prepareLocalDeployment } = require(path.join(
|
|
root,
|
|
'packages/ql3-local-owner-cli/dist/deployment/localDeployment.js',
|
|
));
|
|
const temporaryRoot = fs.realpathSync(
|
|
fs.mkdtempSync(path.join(os.tmpdir(), 'ql3-compose-preflight-')),
|
|
);
|
|
fs.chmodSync(temporaryRoot, 0o700);
|
|
const deploymentRoot = path.join(temporaryRoot, 'deployment');
|
|
const commandPath = path.join(temporaryRoot, 'preflight.json');
|
|
const uid = process.getuid();
|
|
|
|
try {
|
|
const setup = await prepareLocalDeployment({
|
|
schemaVersion: 1,
|
|
operation: 'local.deployment.prepare',
|
|
options: {
|
|
deploymentRoot,
|
|
profile: input.profile,
|
|
instanceId: 'compose-preflight-live',
|
|
busyTimeoutMs: 100,
|
|
service: {
|
|
kind: 'compose',
|
|
image: input.image,
|
|
allowRootService: uid === 0,
|
|
},
|
|
},
|
|
request: {
|
|
ownerPepperKeyId: 'owner-v1',
|
|
registerMutationId: '019f8680-143d-4000-8000-000000000101',
|
|
activateMutationId: '019f8680-143d-4000-8000-000000000102',
|
|
registeredAtMs: 1_785_254_500_000,
|
|
activatedAtMs: 1_785_254_500_001,
|
|
},
|
|
});
|
|
if (setup.status !== 'prepared' || setup.profile !== input.profile) {
|
|
fail('fresh deployment did not prepare');
|
|
}
|
|
const command = {
|
|
schemaVersion: 1,
|
|
operation: 'local.deployment.compose.preflight',
|
|
options: {
|
|
deploymentRoot,
|
|
dockerExecutable: input.dockerExecutable,
|
|
dockerSocketPath: input.dockerSocket,
|
|
allowRootService: uid === 0,
|
|
},
|
|
request: {
|
|
expectedGeneration: 1,
|
|
},
|
|
};
|
|
fs.writeFileSync(commandPath, `${JSON.stringify(command)}\n`, {
|
|
mode: 0o600,
|
|
});
|
|
const cli = path.join(
|
|
root,
|
|
'packages/ql3-local-owner-cli/dist/deployment/localDeploymentCli.js',
|
|
);
|
|
const result = spawnSync(
|
|
process.execPath,
|
|
[cli, 'compose-preflight', '--command-file', commandPath],
|
|
{
|
|
encoding: 'utf8',
|
|
maxBuffer: 64 * 1024,
|
|
timeout: 45_000,
|
|
killSignal: 'SIGKILL',
|
|
},
|
|
);
|
|
if (
|
|
result.error ||
|
|
result.status !== 0 ||
|
|
result.stderr !== '' ||
|
|
typeof result.stdout !== 'string' ||
|
|
result.stdout.includes(deploymentRoot) ||
|
|
/sha256|mutation|socket|executable/i.test(result.stdout)
|
|
) {
|
|
fail('private CLI did not return a low-sensitive ready result');
|
|
}
|
|
const report = JSON.parse(result.stdout);
|
|
if (
|
|
report.schemaVersion !== 1 ||
|
|
report.operation !== 'local.deployment.compose.preflight' ||
|
|
report.status !== 'ready' ||
|
|
report.generation !== 1 ||
|
|
report.profile !== input.profile ||
|
|
report.sqlite?.contractVersion !== 43 ||
|
|
(report.image?.architecture !== 'amd64' &&
|
|
report.image?.architecture !== 'arm64') ||
|
|
report.service?.kind !== 'compose'
|
|
) {
|
|
fail('preflight report is incompatible');
|
|
}
|
|
process.stdout.write(
|
|
`${JSON.stringify({
|
|
schemaVersion: 1,
|
|
profile: report.profile,
|
|
generation: report.generation,
|
|
sqliteContractVersion: report.sqlite.contractVersion,
|
|
architecture: report.image.architecture,
|
|
exactRepoDigest: true,
|
|
composeMerge: true,
|
|
compatible: true,
|
|
})}\n`,
|
|
);
|
|
} finally {
|
|
fs.rmSync(temporaryRoot, { recursive: true, force: true });
|
|
}
|
|
}
|
|
|
|
main().catch((error) => {
|
|
process.stderr.write(
|
|
`${error instanceof Error ? error.message : String(error)}\n`,
|
|
);
|
|
process.exitCode = 1;
|
|
});
|