mirror of
https://github.com/whyour/qinglong.git
synced 2026-09-21 00:17:47 +08:00
455 lines
13 KiB
JavaScript
455 lines
13 KiB
JavaScript
const assert = require('node:assert/strict');
|
|
const fs = require('node:fs');
|
|
const os = require('node:os');
|
|
const path = require('node:path');
|
|
const { afterEach, test } = require('node:test');
|
|
|
|
const {
|
|
runLocalServiceBridge,
|
|
} = require('../dist/deployment/service-manager/serviceBridge.js');
|
|
const {
|
|
prepareLocalServiceManagerIntent,
|
|
consumeLocalServiceManagerOutcome,
|
|
} = require('../dist/deployment/service-manager/serviceManagerIntent.js');
|
|
|
|
const roots = [];
|
|
const destinations = [
|
|
'/etc/systemd/system/qinglong3.service',
|
|
'/etc/init.d/qinglong3',
|
|
];
|
|
|
|
afterEach(() => {
|
|
for (const root of roots.splice(0)) {
|
|
fs.rmSync(root, { force: true, recursive: true });
|
|
}
|
|
if (process.getuid?.() === 0) {
|
|
for (const destination of destinations) {
|
|
fs.rmSync(destination, { force: true });
|
|
fs.rmSync(
|
|
path.join(
|
|
path.dirname(destination),
|
|
`.${path.basename(destination)}.ql3-service-bridge-stage`,
|
|
),
|
|
{ force: true },
|
|
);
|
|
}
|
|
}
|
|
});
|
|
|
|
function fixture(kind) {
|
|
const root = fs.realpathSync(
|
|
fs.mkdtempSync(path.join(os.tmpdir(), `ql3-service-bridge-${kind}-`)),
|
|
);
|
|
roots.push(root);
|
|
fs.chmodSync(root, 0o700);
|
|
const service = path.join(root, 'service');
|
|
fs.mkdirSync(service, { mode: 0o700 });
|
|
fs.writeFileSync(
|
|
path.join(root, 'local-application.json'),
|
|
`${JSON.stringify({
|
|
schema: 'qinglong/local-application-process@v2',
|
|
instanceId: `${kind}-edge-1`,
|
|
profile: 'edge',
|
|
storage: { mode: 'fresh' },
|
|
})}\n`,
|
|
{ mode: 0o600 },
|
|
);
|
|
fs.writeFileSync(
|
|
path.join(
|
|
service,
|
|
kind === 'systemd' ? 'qinglong3.service' : 'qinglong3.openrc',
|
|
),
|
|
kind === 'systemd'
|
|
? '[Service]\nExecStart=/usr/bin/node /opt/qinglong3/app.js\n'
|
|
: '#!/sbin/openrc-run\ncommand=/usr/bin/node\n',
|
|
{ mode: kind === 'systemd' ? 0o600 : 0o700 },
|
|
);
|
|
const controllerRoot = path.join(root, 'root-controller');
|
|
return { root, controllerRoot };
|
|
}
|
|
|
|
function prepare(root, kind, actionId, action) {
|
|
return prepareLocalServiceManagerIntent({
|
|
schemaVersion: 1,
|
|
operation: 'local.deployment.service-manager.intent.prepare',
|
|
options: { deploymentRoot: root, allowRootService: true },
|
|
request: {
|
|
actionId,
|
|
action,
|
|
serviceKind: kind,
|
|
lineage: { mode: 'fresh' },
|
|
requestedAtMs: 1786416100000,
|
|
},
|
|
});
|
|
}
|
|
|
|
function bridgeCommand(prepared, controllerRoot, kind) {
|
|
return {
|
|
schemaVersion: 1,
|
|
operation: 'local.deployment.service-manager.execute',
|
|
options: {
|
|
controllerRoot,
|
|
allowRootController: true,
|
|
manager:
|
|
kind === 'systemd'
|
|
? { kind, executable: '/usr/bin/true' }
|
|
: {
|
|
kind,
|
|
serviceExecutable: '/usr/bin/true',
|
|
updateExecutable: '/usr/bin/true',
|
|
},
|
|
},
|
|
request: {
|
|
intentPath: prepared.intentPath,
|
|
expectedIntentDigest: prepared.intentDigest,
|
|
},
|
|
};
|
|
}
|
|
|
|
function result(status, stdout = '', responseLost = false) {
|
|
return { status, signal: null, stdout, stderr: '', responseLost };
|
|
}
|
|
|
|
test(
|
|
'root systemd bridge installs, starts, survives response loss and replays without mutation',
|
|
{ skip: process.getuid?.() !== 0 },
|
|
() => {
|
|
const { root, controllerRoot } = fixture('systemd');
|
|
const state = { active: false, enabled: false, pid: 0, loseRestart: false };
|
|
const calls = [];
|
|
const runner = ({ args }) => {
|
|
calls.push([...args]);
|
|
if (args[0] === 'show') {
|
|
return result(
|
|
0,
|
|
[
|
|
'LoadState=loaded',
|
|
`ActiveState=${state.active ? 'active' : 'inactive'}`,
|
|
`SubState=${state.active ? 'running' : 'dead'}`,
|
|
'FragmentPath=/etc/systemd/system/qinglong3.service',
|
|
`MainPID=${state.pid}`,
|
|
`UnitFileState=${state.enabled ? 'enabled' : 'disabled'}`,
|
|
'',
|
|
].join('\n'),
|
|
);
|
|
}
|
|
if (args[0] === 'enable') state.enabled = true;
|
|
if (args[0] === 'start') {
|
|
state.active = true;
|
|
state.pid = 4101;
|
|
}
|
|
if (args[0] === 'restart') {
|
|
state.active = true;
|
|
state.pid += 1;
|
|
if (state.loseRestart) return result(null, '', true);
|
|
}
|
|
if (args[0] === 'stop') {
|
|
state.active = false;
|
|
state.pid = 0;
|
|
}
|
|
return result(0);
|
|
};
|
|
let now = 1786416100100;
|
|
const dependencies = { runManager: runner, now: () => now++ };
|
|
|
|
const first = prepare(
|
|
root,
|
|
'systemd',
|
|
'123e4567-e89b-42d3-a456-426614174021',
|
|
'install-enable-start',
|
|
);
|
|
const firstResult = runLocalServiceBridge(
|
|
bridgeCommand(first, controllerRoot, 'systemd'),
|
|
dependencies,
|
|
);
|
|
assert.equal(firstResult.state, 'active');
|
|
assert.equal(fs.statSync('/etc/systemd/system/qinglong3.service').uid, 0);
|
|
assert.equal(
|
|
fs.statSync('/etc/systemd/system/qinglong3.service').mode & 0o777,
|
|
0o644,
|
|
);
|
|
const callCount = calls.length;
|
|
assert.equal(
|
|
runLocalServiceBridge(
|
|
bridgeCommand(first, controllerRoot, 'systemd'),
|
|
dependencies,
|
|
).status,
|
|
'existing',
|
|
);
|
|
assert.equal(calls.length, callCount);
|
|
|
|
state.loseRestart = true;
|
|
const restart = prepare(
|
|
root,
|
|
'systemd',
|
|
'123e4567-e89b-42d3-a456-426614174022',
|
|
'restart',
|
|
);
|
|
const restarted = runLocalServiceBridge(
|
|
bridgeCommand(restart, controllerRoot, 'systemd'),
|
|
dependencies,
|
|
);
|
|
assert.equal(restarted.state, 'active');
|
|
const outcome = JSON.parse(fs.readFileSync(restart.outcomePath, 'utf8'));
|
|
assert.equal(outcome.mutationDisposition, 'response-loss-inspected');
|
|
assert.equal(outcome.observation.mainPid, 4102);
|
|
assert.equal(
|
|
consumeLocalServiceManagerOutcome({
|
|
schemaVersion: 1,
|
|
operation: 'local.deployment.service-manager.outcome.consume',
|
|
options: { deploymentRoot: root, allowRootService: true },
|
|
request: {
|
|
actionId: restart.actionId,
|
|
expectedIntentDigest: restart.intentDigest,
|
|
},
|
|
}).state,
|
|
'active',
|
|
);
|
|
},
|
|
);
|
|
|
|
test(
|
|
'root bridge rejects Owner descriptor drift before publishing a barrier',
|
|
{ skip: process.getuid?.() !== 0 },
|
|
() => {
|
|
const { root, controllerRoot } = fixture('systemd');
|
|
const prepared = prepare(
|
|
root,
|
|
'systemd',
|
|
'123e4567-e89b-42d3-a456-426614174041',
|
|
'install-enable-start',
|
|
);
|
|
fs.writeFileSync(
|
|
path.join(root, 'service', 'qinglong3.service'),
|
|
'[Service]\nExecStart=/usr/bin/false\n',
|
|
{ mode: 0o600 },
|
|
);
|
|
assert.throws(
|
|
() =>
|
|
runLocalServiceBridge(
|
|
bridgeCommand(prepared, controllerRoot, 'systemd'),
|
|
{ runManager: () => result(0) },
|
|
),
|
|
/service manager source material drifted/,
|
|
);
|
|
assert.equal(fs.existsSync(controllerRoot), false);
|
|
},
|
|
);
|
|
|
|
test(
|
|
'barrier replay never repeats mutation after a crash and replaced installed unit',
|
|
{ skip: process.getuid?.() !== 0 },
|
|
() => {
|
|
const { root, controllerRoot } = fixture('systemd');
|
|
const prepared = prepare(
|
|
root,
|
|
'systemd',
|
|
'123e4567-e89b-42d3-a456-426614174042',
|
|
'install-enable-start',
|
|
);
|
|
let crash = true;
|
|
const calls = [];
|
|
const runner = ({ args }) => {
|
|
calls.push([...args]);
|
|
if (args[0] === 'show') {
|
|
return result(
|
|
0,
|
|
[
|
|
'LoadState=loaded',
|
|
'ActiveState=inactive',
|
|
'SubState=dead',
|
|
'FragmentPath=/etc/systemd/system/qinglong3.service',
|
|
'MainPID=0',
|
|
'UnitFileState=disabled',
|
|
'',
|
|
].join('\n'),
|
|
);
|
|
}
|
|
if (args[0] === 'enable' && crash) throw new Error('injected crash');
|
|
return result(0);
|
|
};
|
|
assert.throws(
|
|
() =>
|
|
runLocalServiceBridge(
|
|
bridgeCommand(prepared, controllerRoot, 'systemd'),
|
|
{ runManager: runner, now: () => 1786416300000 },
|
|
),
|
|
/injected crash/,
|
|
);
|
|
assert.equal(
|
|
fs.existsSync(
|
|
path.join(controllerRoot, prepared.actionId, 'barrier.json'),
|
|
),
|
|
true,
|
|
);
|
|
assert.equal(
|
|
fs.existsSync(
|
|
path.join(controllerRoot, prepared.actionId, 'outcome.json'),
|
|
),
|
|
false,
|
|
);
|
|
fs.writeFileSync(
|
|
'/etc/systemd/system/qinglong3.service',
|
|
'[Service]\nExecStart=/usr/bin/false\n',
|
|
{ mode: 0o644 },
|
|
);
|
|
crash = false;
|
|
const mutationsBeforeReplay = calls.filter(
|
|
(args) => args[0] !== 'show',
|
|
).length;
|
|
const replay = runLocalServiceBridge(
|
|
bridgeCommand(prepared, controllerRoot, 'systemd'),
|
|
{ runManager: runner, now: () => 1786416300100 },
|
|
);
|
|
assert.equal(replay.state, 'manual_required');
|
|
const outcome = JSON.parse(fs.readFileSync(prepared.outcomePath, 'utf8'));
|
|
assert.equal(outcome.manualReason, 'descriptor_install_unproved');
|
|
assert.equal(outcome.mutationDisposition, 'replay-inspected');
|
|
assert.equal(
|
|
calls.filter((args) => args[0] !== 'show').length,
|
|
mutationsBeforeReplay,
|
|
);
|
|
},
|
|
);
|
|
|
|
test(
|
|
'root outcome survives Owner outcome preoccupation without overwriting it',
|
|
{ skip: process.getuid?.() !== 0 },
|
|
() => {
|
|
const { root, controllerRoot } = fixture('systemd');
|
|
const prepared = prepare(
|
|
root,
|
|
'systemd',
|
|
'123e4567-e89b-42d3-a456-426614174043',
|
|
'install-enable-start',
|
|
);
|
|
fs.writeFileSync(prepared.outcomePath, '{}\n', { mode: 0o600 });
|
|
const state = { active: false, enabled: false, pid: 0 };
|
|
let mutations = 0;
|
|
const runner = ({ args }) => {
|
|
if (args[0] === 'show') {
|
|
return result(
|
|
0,
|
|
[
|
|
'LoadState=loaded',
|
|
`ActiveState=${state.active ? 'active' : 'inactive'}`,
|
|
`SubState=${state.active ? 'running' : 'dead'}`,
|
|
'FragmentPath=/etc/systemd/system/qinglong3.service',
|
|
`MainPID=${state.pid}`,
|
|
`UnitFileState=${state.enabled ? 'enabled' : 'disabled'}`,
|
|
'',
|
|
].join('\n'),
|
|
);
|
|
}
|
|
mutations += 1;
|
|
if (args[0] === 'enable') state.enabled = true;
|
|
if (args[0] === 'start') {
|
|
state.active = true;
|
|
state.pid = 6101;
|
|
}
|
|
return result(0);
|
|
};
|
|
assert.throws(
|
|
() =>
|
|
runLocalServiceBridge(
|
|
bridgeCommand(prepared, controllerRoot, 'systemd'),
|
|
{ runManager: runner, now: () => 1786416400000 },
|
|
),
|
|
/service bridge Owner outcome drifted/,
|
|
);
|
|
assert.equal(fs.readFileSync(prepared.outcomePath, 'utf8'), '{}\n');
|
|
assert.equal(
|
|
fs.existsSync(
|
|
path.join(controllerRoot, prepared.actionId, 'outcome.json'),
|
|
),
|
|
true,
|
|
);
|
|
const mutationsAfterFirst = mutations;
|
|
assert.throws(
|
|
() =>
|
|
runLocalServiceBridge(
|
|
bridgeCommand(prepared, controllerRoot, 'systemd'),
|
|
{ runManager: runner, now: () => 1786416400100 },
|
|
),
|
|
/service bridge Owner outcome drifted/,
|
|
);
|
|
assert.equal(mutations, mutationsAfterFirst);
|
|
assert.equal(fs.readFileSync(prepared.outcomePath, 'utf8'), '{}\n');
|
|
},
|
|
);
|
|
|
|
test(
|
|
'root OpenRC bridge uses fixed update/service argv for install and stop',
|
|
{ skip: process.getuid?.() !== 0 },
|
|
() => {
|
|
fs.mkdirSync('/etc/init.d', { mode: 0o755, recursive: true });
|
|
const { root, controllerRoot } = fixture('openrc');
|
|
const state = { active: false, enabled: false, loseStart: true };
|
|
const calls = [];
|
|
const runner = ({ args }) => {
|
|
calls.push([...args]);
|
|
if (args[0] === 'show') {
|
|
return result(0, state.enabled ? ' qinglong3 | default\n' : '');
|
|
}
|
|
if (args[1] === 'status') {
|
|
return state.active
|
|
? result(0, 'status: started\n')
|
|
: result(3, 'status: stopped\n');
|
|
}
|
|
if (args[0] === 'add') state.enabled = true;
|
|
if (args[1] === 'start') {
|
|
state.active = true;
|
|
if (state.loseStart) return result(null, '', true);
|
|
}
|
|
if (args[1] === 'stop') state.active = false;
|
|
return result(0);
|
|
};
|
|
let now = 1786416200000;
|
|
const dependencies = { runManager: runner, now: () => now++ };
|
|
const install = prepare(
|
|
root,
|
|
'openrc',
|
|
'123e4567-e89b-42d3-a456-426614174031',
|
|
'install-enable-start',
|
|
);
|
|
const installed = runLocalServiceBridge(
|
|
bridgeCommand(install, controllerRoot, 'openrc'),
|
|
dependencies,
|
|
);
|
|
assert.equal(installed.state, 'active');
|
|
const installedOutcome = JSON.parse(
|
|
fs.readFileSync(install.outcomePath, 'utf8'),
|
|
);
|
|
assert.equal(
|
|
installedOutcome.mutationDisposition,
|
|
'response-loss-inspected',
|
|
);
|
|
const callCount = calls.length;
|
|
assert.equal(
|
|
runLocalServiceBridge(
|
|
bridgeCommand(install, controllerRoot, 'openrc'),
|
|
dependencies,
|
|
).status,
|
|
'existing',
|
|
);
|
|
assert.equal(calls.length, callCount);
|
|
state.loseStart = false;
|
|
const stop = prepare(
|
|
root,
|
|
'openrc',
|
|
'123e4567-e89b-42d3-a456-426614174032',
|
|
'stop',
|
|
);
|
|
assert.equal(
|
|
runLocalServiceBridge(
|
|
bridgeCommand(stop, controllerRoot, 'openrc'),
|
|
dependencies,
|
|
).state,
|
|
'stopped',
|
|
);
|
|
assert.ok(calls.some((args) => args.join(' ') === 'add qinglong3 default'));
|
|
assert.ok(calls.some((args) => args.join(' ') === 'qinglong3 start'));
|
|
assert.ok(calls.some((args) => args.join(' ') === 'qinglong3 stop'));
|
|
},
|
|
);
|