mirror of
https://github.com/whyour/qinglong.git
synced 2026-09-22 02:27:44 +08:00
fix(ql3): preserve adopted baseline during capture
This commit is contained in:
@@ -1,9 +1,14 @@
|
||||
import crypto from 'node:crypto';
|
||||
import fs from 'node:fs';
|
||||
import path from 'node:path';
|
||||
|
||||
import { readPrivateLocalCommandFile } from '@qinglong/local-command-file';
|
||||
|
||||
import { LocalDeploymentConfigurationError } from '../foundation/error';
|
||||
import {
|
||||
adoptedTargetBaselinePath,
|
||||
readAdoptedTargetBaseline,
|
||||
} from '../cutover/targetBaseline';
|
||||
import { cutoverDigest } from '../cutover/targetEvidence';
|
||||
import {
|
||||
readTargetDataReconciliationEvidenceForPaths,
|
||||
@@ -177,6 +182,11 @@ export function proveLocalReconciliationStoppedState(
|
||||
command.request.stoppedAuthority === 'docker'
|
||||
? dockerStoppedEvidence(command)
|
||||
: (serviceManagerStoppedRecord(command), undefined);
|
||||
const adoptedTargetBaseline =
|
||||
persisted?.baselineKind === 'adopted_target' &&
|
||||
persisted.baselineDigest !== undefined
|
||||
? readAdoptedTargetBaselineProjection(command, persisted.baselineDigest)
|
||||
: undefined;
|
||||
const current = readTargetDataReconciliationEvidenceForPaths(
|
||||
{
|
||||
profile: command.request.profile,
|
||||
@@ -184,6 +194,7 @@ export function proveLocalReconciliationStoppedState(
|
||||
legacySourcePath: command.request.legacySourcePath,
|
||||
targetDatabasePath: command.request.targetDatabasePath,
|
||||
expectedActivationDigest: command.request.expectedActivationDigest,
|
||||
...(adoptedTargetBaseline === undefined ? {} : { adoptedTargetBaseline }),
|
||||
},
|
||||
uid,
|
||||
);
|
||||
@@ -203,3 +214,39 @@ export function proveLocalReconciliationStoppedState(
|
||||
});
|
||||
return Object.freeze({ ...payload, proofDigest: cutoverDigest(payload) });
|
||||
}
|
||||
|
||||
function readAdoptedTargetBaselineProjection(
|
||||
command: Readonly<LocalReconciliationCapturePrepareCommand>,
|
||||
expectedBaselineDigest: string,
|
||||
): Readonly<{
|
||||
baselineDigest: string;
|
||||
targetDevice: string;
|
||||
targetInode: string;
|
||||
targetSha256: string;
|
||||
}> {
|
||||
const baseline = readAdoptedTargetBaseline(
|
||||
adoptedTargetBaselinePath(command.options.deploymentRoot),
|
||||
);
|
||||
const targetPathDigest = crypto
|
||||
.createHash('sha256')
|
||||
.update(command.request.targetDatabasePath, 'utf8')
|
||||
.digest('hex');
|
||||
if (
|
||||
baseline.profile !== command.request.profile ||
|
||||
baseline.instanceId !== command.request.instanceId ||
|
||||
baseline.cutoverId !== command.request.cutoverId ||
|
||||
baseline.activationDigest !== command.request.expectedActivationDigest ||
|
||||
baseline.targetPathDigest !== targetPathDigest ||
|
||||
baseline.baselineDigest !== expectedBaselineDigest
|
||||
) {
|
||||
configurationError(
|
||||
'adopted target baseline is detached from stopped evidence',
|
||||
);
|
||||
}
|
||||
return Object.freeze({
|
||||
baselineDigest: baseline.baselineDigest,
|
||||
targetDevice: baseline.targetDevice,
|
||||
targetInode: baseline.targetInode,
|
||||
targetSha256: baseline.targetSha256,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -66,6 +66,10 @@ const {
|
||||
const {
|
||||
readTargetDataReconciliationEvidenceForPaths,
|
||||
} = require('../dist/deployment/cutover/targetDataEvidence.js');
|
||||
const {
|
||||
adoptedTargetBaselinePath,
|
||||
createAdoptedTargetBaseline,
|
||||
} = require('../dist/deployment/cutover/targetBaseline.js');
|
||||
const {
|
||||
targetRunJournalRecord,
|
||||
targetStopPhasePath,
|
||||
@@ -121,6 +125,7 @@ function fixture(
|
||||
stoppedAuthority = 'docker',
|
||||
profile = 'edge',
|
||||
createDefaultSidecars = true,
|
||||
useAdoptedTargetBaseline = false,
|
||||
initializeDatabases,
|
||||
mutateTarget,
|
||||
} = {},
|
||||
@@ -323,6 +328,32 @@ function fixture(
|
||||
};
|
||||
const applicationContents = `${JSON.stringify(application, null, 2)}\n`;
|
||||
fs.writeFileSync(applicationConfigPath, applicationContents, { mode: 0o600 });
|
||||
let adoptedTargetBaseline;
|
||||
if (useAdoptedTargetBaseline) {
|
||||
adoptedTargetBaseline = createAdoptedTargetBaseline({
|
||||
preparedAtMs: 1_350,
|
||||
profile,
|
||||
instanceId: 'edge-router-1',
|
||||
cutoverId,
|
||||
activationDigest,
|
||||
commitmentDigest,
|
||||
applicationConfigDigest: crypto
|
||||
.createHash('sha256')
|
||||
.update(applicationContents, 'utf8')
|
||||
.digest('hex'),
|
||||
legacyDataApplicationCommitDigest: dataCommit.commitDigest,
|
||||
legacyDataApplicationReceiptDigest: dataCommit.receiptDigest,
|
||||
targetPathDigest: activationPayload.targetPathDigest,
|
||||
targetDevice: activationPayload.targetDevice,
|
||||
targetInode: activationPayload.targetInode,
|
||||
targetSha256: activationPayload.targetSha256,
|
||||
});
|
||||
fs.writeFileSync(
|
||||
adoptedTargetBaselinePath(deploymentRoot),
|
||||
`${JSON.stringify(adoptedTargetBaseline)}\n`,
|
||||
{ mode: 0o600 },
|
||||
);
|
||||
}
|
||||
const adoptedBundlePayload = {
|
||||
schemaVersion: 1,
|
||||
kind: 'qinglong3-local-adopted-deployment-bundle',
|
||||
@@ -397,6 +428,16 @@ function fixture(
|
||||
legacySourcePath,
|
||||
targetDatabasePath,
|
||||
expectedActivationDigest: activationDigest,
|
||||
...(adoptedTargetBaseline === undefined
|
||||
? {}
|
||||
: {
|
||||
adoptedTargetBaseline: {
|
||||
baselineDigest: adoptedTargetBaseline.baselineDigest,
|
||||
targetDevice: adoptedTargetBaseline.targetDevice,
|
||||
targetInode: adoptedTargetBaseline.targetInode,
|
||||
targetSha256: adoptedTargetBaseline.targetSha256,
|
||||
},
|
||||
}),
|
||||
},
|
||||
uid,
|
||||
);
|
||||
@@ -514,6 +555,8 @@ function fixture(
|
||||
legacySourcePath,
|
||||
targetDatabasePath,
|
||||
recoveryPath,
|
||||
adoptedTargetBaseline,
|
||||
reconciliation,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -587,6 +630,42 @@ test('capture prepare rejects rollback-candidate stopped data', (t) => {
|
||||
);
|
||||
});
|
||||
|
||||
test('capture prepare preserves exact adopted-target baseline evidence', (t) => {
|
||||
const state = fixture(t, {
|
||||
createDefaultSidecars: false,
|
||||
useAdoptedTargetBaseline: true,
|
||||
});
|
||||
assert.ok(state.adoptedTargetBaseline);
|
||||
assert.equal(state.reconciliation.baselineKind, 'adopted_target');
|
||||
assert.equal(
|
||||
state.reconciliation.baselineDigest,
|
||||
state.adoptedTargetBaseline.baselineDigest,
|
||||
);
|
||||
assert.equal(state.reconciliation.targetMatchesBaseline, false);
|
||||
const prepared = prepareLocalReconciliationCapture(state.command);
|
||||
assert.equal(prepared.status, 'prepared');
|
||||
assert.equal(prepared.state, 'reconciliation_capture_prepared');
|
||||
});
|
||||
|
||||
test('capture prepare rejects a valid but detached adopted-target baseline', (t) => {
|
||||
const state = fixture(t, {
|
||||
createDefaultSidecars: false,
|
||||
useAdoptedTargetBaseline: true,
|
||||
});
|
||||
const baselinePath = adoptedTargetBaselinePath(state.deploymentRoot);
|
||||
const detachedBaseline = createAdoptedTargetBaseline({
|
||||
...state.adoptedTargetBaseline,
|
||||
targetSha256: 'f'.repeat(64),
|
||||
});
|
||||
fs.writeFileSync(baselinePath, `${JSON.stringify(detachedBaseline)}\n`, {
|
||||
mode: 0o600,
|
||||
});
|
||||
assert.throws(
|
||||
() => prepareLocalReconciliationCapture(state.command),
|
||||
/adopted target baseline is detached from stopped evidence/,
|
||||
);
|
||||
});
|
||||
|
||||
test('service-manager stopped authority uses the same capture fence', (t) => {
|
||||
const state = fixture(t, { stoppedAuthority: 'service-manager' });
|
||||
const prepared = prepareLocalReconciliationCapture(state.command);
|
||||
|
||||
Reference in New Issue
Block a user