mirror of
https://github.com/whyour/qinglong.git
synced 2026-09-20 16:07:11 +08:00
feat(ql3): bind service lineage to legacy data receipt
This commit is contained in:
@@ -36,6 +36,9 @@ const {
|
||||
const {
|
||||
cutoverDigest,
|
||||
} = require('../dist/deployment/cutover/targetEvidence.js');
|
||||
const {
|
||||
createLocalDataDirectoryApplicationCommit,
|
||||
} = require('@qinglong/local-sqlite/data-directory-application-commit');
|
||||
|
||||
function sha256(value) {
|
||||
return crypto.createHash('sha256').update(value).digest('hex');
|
||||
@@ -124,9 +127,29 @@ function fixture(t) {
|
||||
...commitmentPayload,
|
||||
commitmentDigest,
|
||||
});
|
||||
const dataCommit = createLocalDataDirectoryApplicationCommit({
|
||||
mutationId: '00000000-0000-4000-8000-000000000001',
|
||||
projectId: 'project-edge-router-1',
|
||||
profile: 'edge',
|
||||
sourceStageManifestDigest: '9'.repeat(64),
|
||||
transformationDigest: 'a'.repeat(64),
|
||||
modelDigest: 'b'.repeat(64),
|
||||
publicationDigest: 'c'.repeat(64),
|
||||
receiptDigest: 'd'.repeat(64),
|
||||
committedAtMs: 1786416000025,
|
||||
receipt: {
|
||||
secretCount: 2,
|
||||
environmentSecretCount: 1,
|
||||
sshSecretCount: 1,
|
||||
},
|
||||
});
|
||||
const transformationRoot = path.join(root, 'transformation');
|
||||
fs.mkdirSync(transformationRoot, { mode: 0o700 });
|
||||
const dataCommitPath = path.join(transformationRoot, 'commit.json');
|
||||
writePrivate(dataCommitPath, dataCommit);
|
||||
const applicationPath = path.join(root, 'local-application.json');
|
||||
writePrivate(applicationPath, {
|
||||
schema: 'qinglong/local-application-process@v3',
|
||||
schema: 'qinglong/local-application-process@v4',
|
||||
instanceId: 'edge-router-1',
|
||||
profile: 'edge',
|
||||
storage: {
|
||||
@@ -146,6 +169,11 @@ function fixture(t) {
|
||||
commitmentPath,
|
||||
expectedCommitmentDigest: commitmentDigest,
|
||||
},
|
||||
legacyDataApplication: {
|
||||
commitPath: dataCommitPath,
|
||||
expectedCommitDigest: dataCommit.commitDigest,
|
||||
expectedReceiptDigest: dataCommit.receiptDigest,
|
||||
},
|
||||
});
|
||||
writePrivate(
|
||||
path.join(service, 'qinglong3.service'),
|
||||
@@ -178,6 +206,8 @@ function fixture(t) {
|
||||
activationDigest,
|
||||
commitmentDigest,
|
||||
commitmentPath,
|
||||
dataCommit,
|
||||
dataCommitPath,
|
||||
applicationPath,
|
||||
sourcePath,
|
||||
targetPath,
|
||||
@@ -554,6 +584,15 @@ test('commits adopted service active evidence and replays from the instance head
|
||||
const record = JSON.parse(fs.readFileSync(recordPath, 'utf8'));
|
||||
assert.equal(record.evidence.startupReceiptDigest, receiptDigest);
|
||||
assert.match(record.evidence.processIdentityDigest, /^[0-9a-f]{64}$/);
|
||||
assert.equal(record.schemaVersion, 2);
|
||||
assert.equal(
|
||||
record.evidence.legacyDataApplicationCommitDigest,
|
||||
state.dataCommit.commitDigest,
|
||||
);
|
||||
assert.equal(
|
||||
record.evidence.legacyDataApplicationReceiptDigest,
|
||||
state.dataCommit.receiptDigest,
|
||||
);
|
||||
assert.equal(
|
||||
(
|
||||
await consumeLocalServiceManagerCutoverOutcome(command, {
|
||||
@@ -930,6 +969,40 @@ test('rejects legacy source content drift before committing service active', asy
|
||||
assert.equal(head.state, 'legacy_stopped');
|
||||
});
|
||||
|
||||
test('rejects legacy data receipt drift after manager outcome without advancing lineage', async (t) => {
|
||||
const state = fixture(t);
|
||||
const prepared = prepare(
|
||||
state,
|
||||
1,
|
||||
'install-enable-start',
|
||||
state.commitmentDigest,
|
||||
'123e4567-e89b-42d3-a456-426614174040',
|
||||
);
|
||||
publishOutcome(
|
||||
prepared,
|
||||
'install-enable-start',
|
||||
'active',
|
||||
4823,
|
||||
1786416000200,
|
||||
);
|
||||
publishReceipt(state, 4823, '100008');
|
||||
const drifted = JSON.parse(fs.readFileSync(state.dataCommitPath, 'utf8'));
|
||||
drifted.receiptDigest = '0'.repeat(64);
|
||||
writePrivate(state.dataCommitPath, drifted);
|
||||
await assert.rejects(
|
||||
consumeLocalServiceManagerCutoverOutcome(consumeCommand(state, prepared), {
|
||||
procRoot: state.procRoot,
|
||||
}),
|
||||
/legacy data application commitment is invalid/,
|
||||
);
|
||||
const head = readLocalCutoverInstanceHead(
|
||||
state.root,
|
||||
'edge-router-1',
|
||||
process.getuid(),
|
||||
);
|
||||
assert.equal(head.state, 'legacy_stopped');
|
||||
});
|
||||
|
||||
test('terminalizes a manager PID replaced before Owner receipt verification', async (t) => {
|
||||
const state = fixture(t);
|
||||
const prepared = prepare(
|
||||
|
||||
@@ -19,6 +19,9 @@ const {
|
||||
advanceLocalCutoverInstanceHead,
|
||||
claimLocalCutoverInstance,
|
||||
} = require('../dist/deployment/cutover/instanceLineage.js');
|
||||
const {
|
||||
createLocalDataDirectoryApplicationCommit,
|
||||
} = require('@qinglong/local-sqlite/data-directory-application-commit');
|
||||
|
||||
const roots = [];
|
||||
|
||||
@@ -247,6 +250,68 @@ test('binds an adopted first start to the current legacy-stopped instance head',
|
||||
);
|
||||
});
|
||||
|
||||
test('rejects v4 legacy data receipt drift before publishing a service intent', () => {
|
||||
const { root } = fixture();
|
||||
const { identity, previousRecordDigest } = adoptedHead(root);
|
||||
adoptedApplication(root, identity, previousRecordDigest);
|
||||
const applicationPath = path.join(root, 'local-application.json');
|
||||
const application = JSON.parse(fs.readFileSync(applicationPath, 'utf8'));
|
||||
const commit = createLocalDataDirectoryApplicationCommit({
|
||||
mutationId: '00000000-0000-4000-8000-000000000001',
|
||||
projectId: 'project-edge-router-1',
|
||||
profile: 'edge',
|
||||
sourceStageManifestDigest: '1'.repeat(64),
|
||||
transformationDigest: '2'.repeat(64),
|
||||
modelDigest: '3'.repeat(64),
|
||||
publicationDigest: '4'.repeat(64),
|
||||
receiptDigest: '5'.repeat(64),
|
||||
committedAtMs: 1786416000001,
|
||||
receipt: {
|
||||
secretCount: 2,
|
||||
environmentSecretCount: 1,
|
||||
sshSecretCount: 1,
|
||||
},
|
||||
});
|
||||
const commitPath = path.join(root, 'legacy-data-commit.json');
|
||||
fs.writeFileSync(commitPath, `${JSON.stringify(commit)}\n`, { mode: 0o600 });
|
||||
fs.writeFileSync(
|
||||
applicationPath,
|
||||
`${JSON.stringify({
|
||||
...application,
|
||||
schema: 'qinglong/local-application-process@v4',
|
||||
legacyDataApplication: {
|
||||
commitPath,
|
||||
expectedCommitDigest: commit.commitDigest,
|
||||
expectedReceiptDigest: '0'.repeat(64),
|
||||
},
|
||||
})}\n`,
|
||||
{ mode: 0o600 },
|
||||
);
|
||||
const command = prepareCommand(root);
|
||||
command.request.lineage = {
|
||||
mode: 'adopted',
|
||||
cutoverId: identity.request.cutoverId,
|
||||
generation: 1,
|
||||
expectedActivationDigest: identity.request.expectedActivationDigest,
|
||||
previousRecordDigest,
|
||||
};
|
||||
assert.throws(
|
||||
() => prepareLocalServiceManagerIntent(command),
|
||||
/legacy data application commit does not match the application binding/,
|
||||
);
|
||||
assert.equal(
|
||||
fs.existsSync(
|
||||
path.join(
|
||||
root,
|
||||
'service',
|
||||
'service-manager-intents',
|
||||
`${command.request.actionId}.json`,
|
||||
),
|
||||
),
|
||||
false,
|
||||
);
|
||||
});
|
||||
|
||||
test('does not allow fresh service intent to bypass an existing cutover head', () => {
|
||||
const { root } = fixture();
|
||||
adoptedHead(root);
|
||||
|
||||
Reference in New Issue
Block a user