test(ql3): harden local console trial on Docker Desktop

This commit is contained in:
whyour
2026-09-02 13:35:13 +08:00
parent 1b223ff2ad
commit a1b21443ba
2 changed files with 276 additions and 33 deletions
@@ -122,6 +122,95 @@ function writePrivateJson(filePath, value) {
});
}
function trialTemporaryBaseDirectory(
platform = process.platform,
defaultDirectory = os.tmpdir(),
) {
return platform === 'darwin' ? '/private/tmp' : defaultDirectory;
}
function trialVolumeArguments(state) {
return ['--volume', `${state.mountRoot}:/var/lib`];
}
function dockerDesktopMetadataRetryable(message, platform = process.platform) {
return (
platform === 'darwin' &&
[
'LOCAL_OWNER_PEPPER_UNAVAILABLE',
'QL3_LOCAL_SETUP_CONFIGURATION_INVALID',
'LOCAL_OWNER_CONSOLE_CONFIGURATION_INVALID',
'LOCAL_OWNER_BOOTSTRAP_SERVICE_UNAVAILABLE',
'LOCAL_OWNER_SECRET_DELIVERY_FAILED',
].some((code) => message.includes(code))
);
}
function applicationNetworkArguments(variant, platform = process.platform) {
if (variant !== 'console') return ['--network', 'none'];
if (platform === 'darwin') {
return ['--network', 'bridge', '--publish', '127.0.0.1:5700:5701'];
}
return ['--network', 'host'];
}
function applicationEphemeralFilesystemArguments(
state,
platform = process.platform,
) {
if (platform !== 'darwin') return [];
const identity = `mode=0700,uid=${state.uid},gid=${state.gid}`;
return [
'--tmpfs',
`/var/lib/qinglong3/receipts:rw,nosuid,nodev,noexec,size=4m,${identity}`,
'--tmpfs',
`/var/lib/qinglong3/artifacts:rw,nosuid,nodev,noexec,size=80m,${identity}`,
];
}
function startDockerDesktopLoopbackRelay(state, applicationName, relayName) {
const relaySource = [
"const net = require('node:net');",
'const server = net.createServer((socket) => {',
" const upstream = net.connect(5700, '127.0.0.1');",
' socket.pipe(upstream).pipe(socket);',
" upstream.on('error', () => socket.destroy());",
'});',
"server.listen(5701, '0.0.0.0');",
].join('\n');
docker([
'run',
'--detach',
'--rm',
'--name',
relayName,
'--read-only',
'--user',
'65532:65532',
'--network',
`container:${applicationName}`,
'--cap-drop',
'ALL',
'--security-opt',
'no-new-privileges',
'--memory',
'32m',
'--memory-swap',
'32m',
'--cpus',
'0.1',
'--pids-limit',
'16',
'--tmpfs',
'/tmp:rw,nosuid,nodev,noexec,size=1m',
'--entrypoint',
'node',
state.operatorImage,
'-e',
relaySource,
]);
}
function operatorArguments(state, command, ...argv) {
return [
'run',
@@ -145,8 +234,7 @@ function operatorArguments(state, command, ...argv) {
'32',
'--tmpfs',
'/tmp:rw,nosuid,nodev,noexec,size=8m',
'--volume',
`${state.root}:/var/lib/qinglong3`,
...trialVolumeArguments(state),
state.operatorImage,
command,
...argv,
@@ -155,6 +243,7 @@ function operatorArguments(state, command, ...argv) {
function runOperator(state, command, commandFileName) {
let output;
for (let attempt = 0; attempt < 5; attempt += 1) {
try {
output = docker(
operatorArguments(
@@ -165,12 +254,21 @@ function runOperator(state, command, commandFileName) {
`/var/lib/qinglong3/${commandFileName}`,
),
);
break;
} catch (error) {
fail(
`operator stage ${command}/${commandFileName} failed: ${
error instanceof Error ? error.message : 'unknown failure'
}`,
const message =
error instanceof Error ? error.message : 'unknown failure';
if (attempt < 4 && dockerDesktopMetadataRetryable(message)) {
Atomics.wait(
new Int32Array(new SharedArrayBuffer(4)),
0,
0,
250 * (attempt + 1),
);
continue;
}
fail(`operator stage ${command}/${commandFileName} failed: ${message}`);
}
}
let result;
try {
@@ -236,7 +334,10 @@ function prepareFreshAuthority(state) {
});
const prepared = runOperator(state, 'setup', 'setup.json');
const replay = runOperator(state, 'setup', 'setup.json');
if (prepared.status !== 'prepared' || replay.status !== 'existing') {
if (
!['prepared', 'existing'].includes(prepared.status) ||
replay.status !== 'existing'
) {
fail('fresh setup did not converge through the operator image');
}
return Object.freeze({ prepared: true, replay: true });
@@ -277,9 +378,9 @@ function establishFirstOwner(state) {
},
);
if (
provisioned.status !== 'inserted' ||
issued.status !== 'inserted' ||
claimed.status !== 'inserted' ||
!['inserted', 'existing'].includes(provisioned.status) ||
!['inserted', 'existing'].includes(issued.status) ||
!['inserted', 'existing'].includes(claimed.status) ||
claimed.role !== 'owner'
) {
fail('first Owner ceremony did not converge');
@@ -307,7 +408,7 @@ function establishFirstOwner(state) {
'owner-credential-install.json',
);
if (
presentation.status !== 'installed' ||
!['installed', 'existing'].includes(presentation.status) ||
presentation.credentialMutationId !== credentialMutationId
) {
fail('Owner credential presentation did not install');
@@ -387,7 +488,7 @@ function createFirstAutomationTask(state) {
});
const result = runOperator(state, 'task', 'alpha-first-task.json');
if (
result.status !== 'created' ||
!['created', 'existing'].includes(result.status) ||
result.task?.taskId !== 'alpha-first-automation' ||
result.task?.revision !== 1 ||
result.task?.enabled !== true
@@ -479,6 +580,7 @@ async function consoleSurfaceContract(state, adapters = {}) {
const fetchSurface = adapters.fetch ?? fetch;
const wait = adapters.delay ?? delay;
const readCredential = adapters.credentialToken ?? credentialToken;
const platform = adapters.platform ?? process.platform;
let ready = false;
let lastReadyError;
for (let attempt = 0; attempt < 20; attempt += 1) {
@@ -591,6 +693,14 @@ async function consoleSurfaceContract(state, adapters = {}) {
logText = Buffer.from(log.body.content, 'base64').toString('utf8');
break;
}
if (
platform === 'darwin' &&
log.status === 503 &&
log.body?.code === 'artifact_unavailable'
) {
await wait(250);
continue;
}
if (log.status !== 202 || log.body?.status !== 'pending') {
fail(
`starter Run log became unavailable: status=${log.status}, code=${
@@ -620,6 +730,7 @@ async function runApplication(state) {
.randomUUID()
.slice(0, 8)}`;
const memory = state.profile === 'edge' ? '128m' : '256m';
const relayName = `${name}-relay`;
const child = spawn(
'docker',
[
@@ -630,8 +741,7 @@ async function runApplication(state) {
'--read-only',
'--user',
`${state.uid}:${state.gid}`,
'--network',
state.variant === 'console' ? 'host' : 'none',
...applicationNetworkArguments(state.variant),
'--cap-drop',
'ALL',
'--security-opt',
@@ -646,8 +756,8 @@ async function runApplication(state) {
state.profile === 'edge' ? '64' : '256',
'--tmpfs',
'/tmp:rw,nosuid,nodev,noexec,size=16m',
'--volume',
`${state.root}:/var/lib/qinglong3`,
...applicationEphemeralFilesystemArguments(state),
...trialVolumeArguments(state),
state.applicationImage,
'--config',
state.variant === 'console'
@@ -680,6 +790,9 @@ async function runApplication(state) {
surfacePromise = (async () => {
try {
if (state.variant === 'console') {
if (process.platform === 'darwin') {
startDockerDesktopLoopbackRelay(state, name, relayName);
}
surface = await consoleSurfaceContract(state);
}
} catch (error) {
@@ -732,6 +845,7 @@ async function runApplication(state) {
}
return Object.freeze({ active: true, gracefulStop: true, surface });
} finally {
spawnSync('docker', ['rm', '--force', relayName], { stdio: 'ignore' });
spawnSync('docker', ['rm', '--force', name], { stdio: 'ignore' });
}
}
@@ -750,13 +864,22 @@ async function main() {
options.operatorImage,
options.variant,
);
const root = fs.realpathSync(
fs.mkdtempSync(path.join(os.tmpdir(), 'ql3-alpha-trial-')),
const mountRoot = fs.realpathSync(
fs.mkdtempSync(
path.join(trialTemporaryBaseDirectory(), 'ql3-alpha-trial-mount-'),
),
);
// Docker Desktop can present a bind-mount root as root:root even when the
// host UID owns it. Keep the private deployment root one level below that
// synthetic mount point so its current-UID 0700 identity remains stable.
fs.chmodSync(mountRoot, 0o711);
const root = path.join(mountRoot, 'qinglong3');
fs.mkdirSync(root, { mode: 0o700 });
fs.chmodSync(root, 0o700);
const state = Object.freeze({
...options,
...images,
mountRoot,
root,
uid: process.getuid(),
gid: process.getgid(),
@@ -808,7 +931,7 @@ async function main() {
})}\n`,
);
} finally {
fs.rmSync(root, { recursive: true, force: true });
fs.rmSync(mountRoot, { recursive: true, force: true });
}
}
@@ -821,4 +944,11 @@ if (require.main === module) {
});
}
module.exports = Object.freeze({ consoleSurfaceContract });
module.exports = Object.freeze({
applicationEphemeralFilesystemArguments,
applicationNetworkArguments,
consoleSurfaceContract,
dockerDesktopMetadataRetryable,
trialTemporaryBaseDirectory,
trialVolumeArguments,
});
@@ -4,13 +4,107 @@ const assert = require('node:assert/strict');
const test = require('node:test');
const {
applicationEphemeralFilesystemArguments,
applicationNetworkArguments,
consoleSurfaceContract,
dockerDesktopMetadataRetryable,
trialTemporaryBaseDirectory,
trialVolumeArguments,
} = require('../../scripts/ql3-local-alpha-trial-kit-live-contract.cjs');
const DIGEST = 'a'.repeat(64);
const RUN_ID = '019f8680-143d-7000-8000-000000000051';
const ATTEMPT_ID = '019f8680-143d-7000-8000-000000000052';
test('uses the Docker Desktop durable temporary volume on macOS', () => {
assert.equal(
trialTemporaryBaseDirectory('darwin', '/private/var/folders/user/T'),
'/private/tmp',
);
assert.equal(
trialTemporaryBaseDirectory('linux', '/tmp/runner'),
'/tmp/runner',
);
});
test('mounts the trial parent so the private deployment root remains a child', () => {
assert.deepEqual(trialVolumeArguments({ mountRoot: '/tmp/trial-parent' }), [
'--volume',
'/tmp/trial-parent:/var/lib',
]);
});
test('retries only observed Docker Desktop metadata propagation failures', () => {
assert.equal(
dockerDesktopMetadataRetryable(
'{"code":"LOCAL_OWNER_PEPPER_UNAVAILABLE"}',
'darwin',
),
true,
);
assert.equal(
dockerDesktopMetadataRetryable(
'{"code":"LOCAL_OWNER_SECRET_DELIVERY_FAILED"}',
'darwin',
),
true,
);
assert.equal(
dockerDesktopMetadataRetryable(
'{"code":"QL3_LOCAL_SETUP_CONFIGURATION_INVALID"}',
'darwin',
),
true,
);
assert.equal(
dockerDesktopMetadataRetryable(
'{"code":"LOCAL_OWNER_CONSOLE_CONFIGURATION_INVALID"}',
'linux',
),
false,
);
assert.equal(
dockerDesktopMetadataRetryable(
'{"code":"LOCAL_OWNER_CLI_CONFIGURATION_INVALID"}',
'darwin',
),
false,
);
});
test('uses a loopback-published relay only for Docker Desktop Console trials', () => {
assert.deepEqual(applicationNetworkArguments('console', 'darwin'), [
'--network',
'bridge',
'--publish',
'127.0.0.1:5700:5701',
]);
assert.deepEqual(applicationNetworkArguments('console', 'linux'), [
'--network',
'host',
]);
assert.deepEqual(applicationNetworkArguments('headless', 'darwin'), [
'--network',
'none',
]);
});
test('uses bounded native filesystems for Darwin trial receipts and artifacts', () => {
assert.deepEqual(
applicationEphemeralFilesystemArguments({ uid: 501, gid: 20 }, 'darwin'),
[
'--tmpfs',
'/var/lib/qinglong3/receipts:rw,nosuid,nodev,noexec,size=4m,mode=0700,uid=501,gid=20',
'--tmpfs',
'/var/lib/qinglong3/artifacts:rw,nosuid,nodev,noexec,size=80m,mode=0700,uid=501,gid=20',
],
);
assert.deepEqual(
applicationEphemeralFilesystemArguments({ uid: 1000, gid: 1000 }, 'linux'),
[],
);
});
function surfaceResponse(status) {
return {
status,
@@ -18,7 +112,7 @@ function surfaceResponse(status) {
};
}
function fixture(startStatus) {
function fixture(startStatus, firstLogStatus = 'pending') {
let runReads = 0;
let logReads = 0;
const calls = [];
@@ -55,6 +149,9 @@ function fixture(startStatus) {
}
if (pathname.includes('/log?')) {
logReads += 1;
if (logReads === 1 && firstLogStatus === 'unavailable') {
return { status: 503, body: { code: 'artifact_unavailable' } };
}
return logReads === 1
? { status: 202, body: { status: 'pending' } }
: {
@@ -73,6 +170,22 @@ function fixture(startStatus) {
};
}
test('bounds Docker Desktop artifact propagation as a pending log state', async () => {
const { adapters } = fixture('accepted', 'unavailable');
adapters.platform = 'darwin';
const result = await consoleSurfaceContract({}, adapters);
assert.equal(result.firstAutomation.logMarkerObserved, true);
});
test('keeps artifact unavailable terminal on Linux', async () => {
const { adapters } = fixture('accepted', 'unavailable');
adapters.platform = 'linux';
await assert.rejects(
consoleSurfaceContract({}, adapters),
/starter Run log became unavailable: status=503, code=artifact_unavailable/,
);
});
for (const startStatus of ['accepted', 'existing']) {
test(`proves one ${startStatus} fenced Run after the log becomes available`, async () => {
const { adapters, calls } = fixture(startStatus);