mirror of
https://github.com/whyour/qinglong.git
synced 2026-09-22 19:29:13 +08:00
feat(ql3): commit service manager legacy rollback
This commit is contained in:
@@ -11,7 +11,16 @@ const {
|
||||
const {
|
||||
localServiceManagerLegacyRollbackPreparationPath,
|
||||
prepareLocalServiceManagerLegacyRollback,
|
||||
} = require('../dist/deployment/service-manager/serviceLegacyRollback.js');
|
||||
} = require('../dist/deployment/service-manager/legacy-rollback/preparation.js');
|
||||
const {
|
||||
authorizeLocalServiceManagerLegacyRollback,
|
||||
consumeLocalServiceManagerLegacyRollback,
|
||||
} = require('../dist/deployment/service-manager/legacy-rollback/consumer.js');
|
||||
const {
|
||||
localServiceManagerLegacyStartOutcomeDigest,
|
||||
localServiceManagerLegacyStartOutcomePath,
|
||||
localServiceManagerRollbackObservationDigest,
|
||||
} = require('../dist/deployment/service-manager/legacy-rollback/contract.js');
|
||||
const {
|
||||
prepareLocalServiceManagerIntent,
|
||||
} = require('../dist/deployment/service-manager/serviceManagerIntent.js');
|
||||
@@ -384,6 +393,126 @@ function rollbackPrepareCommand(state, stoppedResult, head) {
|
||||
};
|
||||
}
|
||||
|
||||
function rollbackAuthorizeCommand(state, prepared) {
|
||||
return {
|
||||
schemaVersion: 1,
|
||||
operation: 'local.deployment.service-manager.legacy-rollback.authorize',
|
||||
options: {
|
||||
deploymentRoot: state.root,
|
||||
allowRootService: process.getuid() === 0,
|
||||
},
|
||||
request: {
|
||||
cutoverId: state.cutoverId,
|
||||
profile: 'edge',
|
||||
instanceId: 'edge-router-1',
|
||||
generation: 1,
|
||||
expectedActivationDigest: state.activationDigest,
|
||||
expectedPreparationDigest: prepared.preparationDigest,
|
||||
expectedInstanceHeadDigest: prepared.instanceHeadDigest,
|
||||
requestedAtMs: 1786416000500,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
function rollbackObservation(serviceName, active, observedAtMs) {
|
||||
const payload = {
|
||||
managerKind: 'systemd',
|
||||
serviceName,
|
||||
fragmentPath: `/etc/systemd/system/${serviceName}.service`,
|
||||
loadState: 'loaded',
|
||||
activeState: active ? 'active' : 'inactive',
|
||||
subState: active ? 'running' : 'dead',
|
||||
enabledState: 'enabled',
|
||||
mainPid: active ? 5723 : 0,
|
||||
observedAtMs,
|
||||
};
|
||||
return {
|
||||
...payload,
|
||||
observationDigest: localServiceManagerRollbackObservationDigest(payload),
|
||||
};
|
||||
}
|
||||
|
||||
function publishLegacyStartOutcome(
|
||||
state,
|
||||
prepared,
|
||||
authorized,
|
||||
outcomeState = 'legacy_running',
|
||||
) {
|
||||
const preparation = JSON.parse(
|
||||
fs.readFileSync(
|
||||
localServiceManagerLegacyRollbackPreparationPath(
|
||||
state.root,
|
||||
state.cutoverId,
|
||||
1,
|
||||
),
|
||||
'utf8',
|
||||
),
|
||||
);
|
||||
const legacyObservation = rollbackObservation(
|
||||
'qinglong',
|
||||
outcomeState === 'legacy_running',
|
||||
1786416000600,
|
||||
);
|
||||
const targetObservation = rollbackObservation(
|
||||
'qinglong3',
|
||||
false,
|
||||
1786416000601,
|
||||
);
|
||||
const payload = {
|
||||
schema: 'qinglong3-local-service-manager-legacy-start-outcome',
|
||||
schemaVersion: 1,
|
||||
state: outcomeState,
|
||||
cutoverId: state.cutoverId,
|
||||
profile: 'edge',
|
||||
instanceId: 'edge-router-1',
|
||||
generation: 1,
|
||||
activationDigest: state.activationDigest,
|
||||
managerKind: 'systemd',
|
||||
preparationDigest: prepared.preparationDigest,
|
||||
authorizationDigest: authorized.authorizationDigest,
|
||||
barrierDigest: '9'.repeat(64),
|
||||
legacyDescriptorDigest: 'a'.repeat(64),
|
||||
targetDescriptorDigest: preparation.targetDescriptorDigest,
|
||||
mutationDisposition: 'executed',
|
||||
manualReason:
|
||||
outcomeState === 'legacy_running' ? null : 'manager_state_unproved',
|
||||
legacyObservation,
|
||||
targetObservation,
|
||||
completedAtMs: 1786416000610,
|
||||
};
|
||||
const outcome = {
|
||||
...payload,
|
||||
outcomeDigest: localServiceManagerLegacyStartOutcomeDigest(payload),
|
||||
};
|
||||
writePrivate(
|
||||
localServiceManagerLegacyStartOutcomePath(state.root, state.cutoverId, 1),
|
||||
outcome,
|
||||
);
|
||||
return outcome;
|
||||
}
|
||||
|
||||
function rollbackConsumeCommand(state, prepared, authorized) {
|
||||
return {
|
||||
schemaVersion: 1,
|
||||
operation: 'local.deployment.service-manager.legacy-rollback.consume',
|
||||
options: {
|
||||
deploymentRoot: state.root,
|
||||
allowRootService: process.getuid() === 0,
|
||||
},
|
||||
request: {
|
||||
cutoverId: state.cutoverId,
|
||||
profile: 'edge',
|
||||
instanceId: 'edge-router-1',
|
||||
generation: 1,
|
||||
expectedActivationDigest: state.activationDigest,
|
||||
expectedPreparationDigest: prepared.preparationDigest,
|
||||
expectedAuthorizationDigest: authorized.authorizationDigest,
|
||||
expectedAuthorizationHeadDigest: authorized.instanceHeadDigest,
|
||||
requestedAtMs: 1786416000700,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
test('commits adopted service active evidence and replays from the instance head', async (t) => {
|
||||
const state = fixture(t);
|
||||
const prepared = prepare(
|
||||
@@ -574,6 +703,92 @@ test('prepares and exactly replays lossless service-manager legacy rollback evid
|
||||
assert.equal(replay.instanceHeadDigest, prepared.instanceHeadDigest);
|
||||
});
|
||||
|
||||
test('authorizes, consumes and exactly replays service-manager legacy running evidence', async (t) => {
|
||||
const state = fixture(t);
|
||||
const { stoppedResult, head } = await stopAdoptedTarget(state, '039');
|
||||
const prepared = prepareLocalServiceManagerLegacyRollback(
|
||||
rollbackPrepareCommand(state, stoppedResult, head),
|
||||
);
|
||||
const authorizeCommand = rollbackAuthorizeCommand(state, prepared);
|
||||
const authorized =
|
||||
authorizeLocalServiceManagerLegacyRollback(authorizeCommand);
|
||||
assert.equal(authorized.status, 'prepared');
|
||||
assert.equal(authorized.state, 'legacy_restart_requested');
|
||||
assert.equal(
|
||||
authorizeLocalServiceManagerLegacyRollback(authorizeCommand).status,
|
||||
'existing',
|
||||
);
|
||||
const requestedHead = readLocalCutoverInstanceHead(
|
||||
state.root,
|
||||
'edge-router-1',
|
||||
process.getuid(),
|
||||
);
|
||||
assert.equal(requestedHead.state, 'legacy_restart_requested');
|
||||
assert.equal(
|
||||
requestedHead.sourceRecordDigest,
|
||||
authorized.authorizationDigest,
|
||||
);
|
||||
const outcome = publishLegacyStartOutcome(state, prepared, authorized);
|
||||
const consumeCommand = rollbackConsumeCommand(state, prepared, authorized);
|
||||
const consumed = consumeLocalServiceManagerLegacyRollback(consumeCommand);
|
||||
assert.equal(consumed.status, 'prepared');
|
||||
assert.equal(consumed.state, 'legacy_running');
|
||||
assert.equal(consumed.outcomeDigest, outcome.outcomeDigest);
|
||||
assert.match(consumed.completionDigest, /^[0-9a-f]{64}$/);
|
||||
const replay = consumeLocalServiceManagerLegacyRollback(consumeCommand);
|
||||
assert.equal(replay.status, 'existing');
|
||||
assert.equal(replay.completionDigest, consumed.completionDigest);
|
||||
assert.equal(replay.instanceHeadDigest, consumed.instanceHeadDigest);
|
||||
});
|
||||
|
||||
test('requires current rollback evidence before publishing legacy start authorization', async (t) => {
|
||||
const state = fixture(t);
|
||||
const { stoppedResult, head } = await stopAdoptedTarget(state, '041');
|
||||
const prepared = prepareLocalServiceManagerLegacyRollback(
|
||||
rollbackPrepareCommand(state, stoppedResult, head),
|
||||
);
|
||||
fs.writeFileSync(state.targetPath, 'target-drift-before-authorization\n', {
|
||||
mode: 0o600,
|
||||
});
|
||||
assert.throws(
|
||||
() =>
|
||||
authorizeLocalServiceManagerLegacyRollback(
|
||||
rollbackAuthorizeCommand(state, prepared),
|
||||
),
|
||||
/prepared rollback data drifted after authorization/,
|
||||
);
|
||||
const unchanged = readLocalCutoverInstanceHead(
|
||||
state.root,
|
||||
'edge-router-1',
|
||||
process.getuid(),
|
||||
);
|
||||
assert.equal(unchanged.state, 'rollback_prepared');
|
||||
assert.equal(unchanged.headDigest, prepared.instanceHeadDigest);
|
||||
});
|
||||
|
||||
test('terminalizes an unproved root legacy start outcome', async (t) => {
|
||||
const state = fixture(t);
|
||||
const { stoppedResult, head } = await stopAdoptedTarget(state, '043');
|
||||
const prepared = prepareLocalServiceManagerLegacyRollback(
|
||||
rollbackPrepareCommand(state, stoppedResult, head),
|
||||
);
|
||||
const authorized = authorizeLocalServiceManagerLegacyRollback(
|
||||
rollbackAuthorizeCommand(state, prepared),
|
||||
);
|
||||
publishLegacyStartOutcome(state, prepared, authorized, 'manual_required');
|
||||
const consumed = consumeLocalServiceManagerLegacyRollback(
|
||||
rollbackConsumeCommand(state, prepared, authorized),
|
||||
);
|
||||
assert.equal(consumed.state, 'manual_required');
|
||||
const terminal = readLocalCutoverInstanceHead(
|
||||
state.root,
|
||||
'edge-router-1',
|
||||
process.getuid(),
|
||||
);
|
||||
assert.equal(terminal.state, 'manual_required');
|
||||
assert.equal(terminal.sourceRecordDigest, consumed.completionDigest);
|
||||
});
|
||||
|
||||
test('keeps target_stopped when service-manager rollback would discard target writes', async (t) => {
|
||||
const state = fixture(t);
|
||||
const { stoppedResult, head } = await stopAdoptedTarget(state, '033');
|
||||
|
||||
@@ -0,0 +1,144 @@
|
||||
const assert = require('node:assert/strict');
|
||||
const { test } = require('node:test');
|
||||
|
||||
const {
|
||||
localServiceManagerLegacyStartOutcomeDigest,
|
||||
localServiceManagerRollbackObservationDigest,
|
||||
normalizeLocalServiceManagerLegacyRollbackBridgeCommand,
|
||||
normalizeLocalServiceManagerLegacyStartOutcome,
|
||||
} = require('../dist/deployment/service-manager/legacy-rollback/contract.js');
|
||||
|
||||
function command(kind = 'systemd') {
|
||||
return {
|
||||
schemaVersion: 1,
|
||||
operation: 'local.deployment.service-manager.legacy-rollback.execute',
|
||||
options: {
|
||||
deploymentRoot: '/var/lib/qinglong3',
|
||||
controllerRoot: '/var/lib/qinglong3-service-bridge',
|
||||
allowRootController: true,
|
||||
manager:
|
||||
kind === 'systemd'
|
||||
? { kind, executable: '/usr/bin/systemctl' }
|
||||
: {
|
||||
kind,
|
||||
serviceExecutable: '/sbin/rc-service',
|
||||
updateExecutable: '/sbin/rc-update',
|
||||
},
|
||||
},
|
||||
request: {
|
||||
cutoverId: 'edge-router-cutover',
|
||||
generation: 1,
|
||||
expectedAuthorizationDigest: '1'.repeat(64),
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
function observation(serviceName, active, observedAtMs) {
|
||||
const payload = {
|
||||
managerKind: 'systemd',
|
||||
serviceName,
|
||||
fragmentPath: `/etc/systemd/system/${serviceName}.service`,
|
||||
loadState: 'loaded',
|
||||
activeState: active ? 'active' : 'inactive',
|
||||
subState: active ? 'running' : 'dead',
|
||||
enabledState: 'enabled',
|
||||
mainPid: active ? 3101 : 0,
|
||||
observedAtMs,
|
||||
};
|
||||
return {
|
||||
...payload,
|
||||
observationDigest: localServiceManagerRollbackObservationDigest(payload),
|
||||
};
|
||||
}
|
||||
|
||||
function outcome() {
|
||||
const payload = {
|
||||
schema: 'qinglong3-local-service-manager-legacy-start-outcome',
|
||||
schemaVersion: 1,
|
||||
state: 'legacy_running',
|
||||
cutoverId: 'edge-router-cutover',
|
||||
profile: 'edge',
|
||||
instanceId: 'edge-router-1',
|
||||
generation: 1,
|
||||
activationDigest: '2'.repeat(64),
|
||||
managerKind: 'systemd',
|
||||
preparationDigest: '3'.repeat(64),
|
||||
authorizationDigest: '4'.repeat(64),
|
||||
barrierDigest: '5'.repeat(64),
|
||||
legacyDescriptorDigest: '6'.repeat(64),
|
||||
targetDescriptorDigest: '7'.repeat(64),
|
||||
mutationDisposition: 'response-loss-inspected',
|
||||
manualReason: null,
|
||||
legacyObservation: observation('qinglong', true, 100),
|
||||
targetObservation: observation('qinglong3', false, 101),
|
||||
completedAtMs: 102,
|
||||
};
|
||||
return {
|
||||
...payload,
|
||||
outcomeDigest: localServiceManagerLegacyStartOutcomeDigest(payload),
|
||||
};
|
||||
}
|
||||
|
||||
test('normalizes exact systemd and OpenRC legacy rollback bridge commands', () => {
|
||||
assert.equal(
|
||||
normalizeLocalServiceManagerLegacyRollbackBridgeCommand(command()).options
|
||||
.manager.kind,
|
||||
'systemd',
|
||||
);
|
||||
assert.equal(
|
||||
normalizeLocalServiceManagerLegacyRollbackBridgeCommand(command('openrc'))
|
||||
.options.manager.kind,
|
||||
'openrc',
|
||||
);
|
||||
});
|
||||
|
||||
test('rejects shell surface, path drift and unbound root acknowledgement', () => {
|
||||
assert.throws(
|
||||
() =>
|
||||
normalizeLocalServiceManagerLegacyRollbackBridgeCommand({
|
||||
...command(),
|
||||
shell: 'systemctl start qinglong',
|
||||
}),
|
||||
/shape is invalid/,
|
||||
);
|
||||
assert.throws(
|
||||
() =>
|
||||
normalizeLocalServiceManagerLegacyRollbackBridgeCommand({
|
||||
...command(),
|
||||
options: { ...command().options, deploymentRoot: '/var/lib/../tmp' },
|
||||
}),
|
||||
/supervisor-safe absolute path/,
|
||||
);
|
||||
assert.throws(
|
||||
() =>
|
||||
normalizeLocalServiceManagerLegacyRollbackBridgeCommand({
|
||||
...command(),
|
||||
options: { ...command().options, allowRootController: false },
|
||||
}),
|
||||
/command is invalid/,
|
||||
);
|
||||
});
|
||||
|
||||
test('binds both service observations and all rollback outcome digests', () => {
|
||||
const value = outcome();
|
||||
const normalized = normalizeLocalServiceManagerLegacyStartOutcome(value);
|
||||
assert.equal(normalized.state, 'legacy_running');
|
||||
assert.equal(normalized.legacyObservation.serviceName, 'qinglong');
|
||||
assert.equal(normalized.targetObservation.serviceName, 'qinglong3');
|
||||
assert.throws(
|
||||
() =>
|
||||
normalizeLocalServiceManagerLegacyStartOutcome({
|
||||
...value,
|
||||
targetDescriptorDigest: '8'.repeat(64),
|
||||
}),
|
||||
/outcome drifted/,
|
||||
);
|
||||
assert.throws(
|
||||
() =>
|
||||
normalizeLocalServiceManagerLegacyStartOutcome({
|
||||
...value,
|
||||
legacyObservation: value.targetObservation,
|
||||
}),
|
||||
/outcome drifted/,
|
||||
);
|
||||
});
|
||||
Reference in New Issue
Block a user