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:
+56
-1
@@ -6,6 +6,7 @@ import {
|
||||
readPrivateLocalCommandFile,
|
||||
readPrivateLocalJsonFile,
|
||||
} from '@qinglong/local-command-file';
|
||||
import { normalizeLocalDataDirectoryApplicationCommit } from '@qinglong/local-sqlite/data-directory-application-commit';
|
||||
|
||||
import {
|
||||
advanceLocalCutoverInstanceHead,
|
||||
@@ -283,6 +284,8 @@ function adoptedPaths(
|
||||
command: Readonly<LocalServiceManagerLegacyRollbackPrepareCommand>,
|
||||
expectedApplicationDigest: string,
|
||||
expectedCommitmentDigest: string,
|
||||
expectedLegacyDataApplicationCommitDigest: string | null | undefined,
|
||||
expectedLegacyDataApplicationReceiptDigest: string | null | undefined,
|
||||
uid: number,
|
||||
gid: number,
|
||||
): Readonly<AdoptedPaths> {
|
||||
@@ -302,6 +305,17 @@ function adoptedPaths(
|
||||
);
|
||||
const storage = object(application.storage, 'adopted storage');
|
||||
const cutover = object(application.cutover, 'adopted cutover');
|
||||
const legacyDataApplication =
|
||||
application.schema === 'qinglong/local-application-process@v4'
|
||||
? object(application.legacyDataApplication, 'legacy data application')
|
||||
: undefined;
|
||||
if (legacyDataApplication !== undefined) {
|
||||
exact(
|
||||
legacyDataApplication,
|
||||
['commitPath', 'expectedCommitDigest', 'expectedReceiptDigest'],
|
||||
'legacy data application',
|
||||
);
|
||||
}
|
||||
const commitmentPath = safeAbsolutePath(
|
||||
cutover.commitmentPath,
|
||||
'commitmentPath',
|
||||
@@ -319,7 +333,8 @@ function adoptedPaths(
|
||||
);
|
||||
const { commitmentDigest, ...commitmentPayload } = commitment;
|
||||
if (
|
||||
application.schema !== 'qinglong/local-application-process@v3' ||
|
||||
(application.schema !== 'qinglong/local-application-process@v3' &&
|
||||
application.schema !== 'qinglong/local-application-process@v4') ||
|
||||
application.profile !== command.request.profile ||
|
||||
application.instanceId !== command.request.instanceId ||
|
||||
storage.mode !== 'adopted' ||
|
||||
@@ -340,6 +355,44 @@ function adoptedPaths(
|
||||
) {
|
||||
configurationError('adopted application rollback binding drifted');
|
||||
}
|
||||
if (legacyDataApplication === undefined) {
|
||||
if (
|
||||
expectedLegacyDataApplicationCommitDigest != null ||
|
||||
expectedLegacyDataApplicationReceiptDigest != null
|
||||
) {
|
||||
configurationError('legacy data application rollback lineage drifted');
|
||||
}
|
||||
} else {
|
||||
try {
|
||||
const dataCommit = normalizeLocalDataDirectoryApplicationCommit(
|
||||
readPrivateLocalCommandFile(
|
||||
safeAbsolutePath(
|
||||
legacyDataApplication.commitPath,
|
||||
'legacyDataApplication.commitPath',
|
||||
),
|
||||
),
|
||||
);
|
||||
if (
|
||||
typeof expectedLegacyDataApplicationCommitDigest !== 'string' ||
|
||||
typeof expectedLegacyDataApplicationReceiptDigest !== 'string' ||
|
||||
dataCommit.profile !== command.request.profile ||
|
||||
dataCommit.commitDigest !==
|
||||
legacyDataApplication.expectedCommitDigest ||
|
||||
dataCommit.receiptDigest !==
|
||||
legacyDataApplication.expectedReceiptDigest ||
|
||||
dataCommit.commitDigest !== expectedLegacyDataApplicationCommitDigest ||
|
||||
dataCommit.receiptDigest !== expectedLegacyDataApplicationReceiptDigest
|
||||
) {
|
||||
configurationError('legacy data application rollback lineage drifted');
|
||||
}
|
||||
} catch (error) {
|
||||
if (error instanceof LocalDeploymentConfigurationError) throw error;
|
||||
configurationError(
|
||||
'legacy data application rollback lineage is invalid',
|
||||
error,
|
||||
);
|
||||
}
|
||||
}
|
||||
return Object.freeze({
|
||||
activationPath: safeAbsolutePath(storage.activationPath, 'activationPath'),
|
||||
legacySourcePath: safeAbsolutePath(storage.sourcePath, 'legacySourcePath'),
|
||||
@@ -571,6 +624,8 @@ export function prepareLocalServiceManagerLegacyRollback(
|
||||
command,
|
||||
stopped.evidence.applicationConfigDigest,
|
||||
stopped.evidence.commitmentDigest,
|
||||
stopped.evidence.legacyDataApplicationCommitDigest,
|
||||
stopped.evidence.legacyDataApplicationReceiptDigest,
|
||||
identity.uid,
|
||||
identity.gid,
|
||||
);
|
||||
|
||||
+88
-17
@@ -7,6 +7,7 @@ import {
|
||||
readPrivateLocalCommandFile,
|
||||
readPrivateLocalJsonFile,
|
||||
} from '@qinglong/local-command-file';
|
||||
import { normalizeLocalDataDirectoryApplicationCommit } from '@qinglong/local-sqlite/data-directory-application-commit';
|
||||
|
||||
import { currentIdentity } from '../foundation/contract';
|
||||
import { LocalDeploymentConfigurationError } from '../foundation/error';
|
||||
@@ -96,6 +97,17 @@ interface AdoptedBinding {
|
||||
readonly recoveryPath: string;
|
||||
readonly manifestPath: string;
|
||||
readonly applicationConfigDigest: string;
|
||||
readonly legacyDataApplication?: Readonly<{
|
||||
commitPath: string;
|
||||
commitDigest: string;
|
||||
receiptDigest: string;
|
||||
}>;
|
||||
}
|
||||
|
||||
interface VerifiedAdoptedEvidence {
|
||||
readonly targetDataIdentityDigest: string;
|
||||
readonly legacyDataApplicationCommitDigest: string | null;
|
||||
readonly legacyDataApplicationReceiptDigest: string | null;
|
||||
}
|
||||
|
||||
function configurationError(message: string, cause?: unknown): never {
|
||||
@@ -249,6 +261,17 @@ function adoptedBinding(
|
||||
);
|
||||
const storage = object(config.storage, 'adopted storage');
|
||||
const cutover = object(config.cutover, 'adopted cutover');
|
||||
const legacyDataApplication =
|
||||
config.schema === 'qinglong/local-application-process@v4'
|
||||
? object(config.legacyDataApplication, 'legacy data application binding')
|
||||
: undefined;
|
||||
if (legacyDataApplication !== undefined) {
|
||||
exact(
|
||||
legacyDataApplication,
|
||||
['commitPath', 'expectedCommitDigest', 'expectedReceiptDigest'],
|
||||
'legacy data application binding',
|
||||
);
|
||||
}
|
||||
const expectedCommitmentPath = path.join(
|
||||
intent.deployment.root,
|
||||
'service',
|
||||
@@ -257,7 +280,8 @@ function adoptedBinding(
|
||||
'0002-legacy-stopped.json',
|
||||
);
|
||||
if (
|
||||
config.schema !== 'qinglong/local-application-process@v3' ||
|
||||
(config.schema !== 'qinglong/local-application-process@v3' &&
|
||||
config.schema !== 'qinglong/local-application-process@v4') ||
|
||||
config.profile !== intent.profile ||
|
||||
config.instanceId !== intent.instanceId ||
|
||||
storage.mode !== 'adopted' ||
|
||||
@@ -265,7 +289,12 @@ function adoptedBinding(
|
||||
intent.lineage.expectedActivationDigest ||
|
||||
cutover.cutoverId !== intent.lineage.cutoverId ||
|
||||
typeof cutover.expectedCommitmentDigest !== 'string' ||
|
||||
!DIGEST_PATTERN.test(cutover.expectedCommitmentDigest)
|
||||
!DIGEST_PATTERN.test(cutover.expectedCommitmentDigest) ||
|
||||
(legacyDataApplication !== undefined &&
|
||||
(typeof legacyDataApplication.expectedCommitDigest !== 'string' ||
|
||||
!DIGEST_PATTERN.test(legacyDataApplication.expectedCommitDigest) ||
|
||||
typeof legacyDataApplication.expectedReceiptDigest !== 'string' ||
|
||||
!DIGEST_PATTERN.test(legacyDataApplication.expectedReceiptDigest)))
|
||||
) {
|
||||
configurationError('adopted application configuration drifted');
|
||||
}
|
||||
@@ -280,6 +309,19 @@ function adoptedBinding(
|
||||
recoveryPath: safeAbsolutePath(storage.recoveryPath, 'recoveryPath'),
|
||||
manifestPath: safeAbsolutePath(storage.manifestPath, 'manifestPath'),
|
||||
applicationConfigDigest: intent.deployment.applicationConfigSha256,
|
||||
...(legacyDataApplication === undefined
|
||||
? {}
|
||||
: {
|
||||
legacyDataApplication: Object.freeze({
|
||||
commitPath: safeAbsolutePath(
|
||||
legacyDataApplication.commitPath,
|
||||
'legacyDataApplication.commitPath',
|
||||
),
|
||||
commitDigest: legacyDataApplication.expectedCommitDigest as string,
|
||||
receiptDigest:
|
||||
legacyDataApplication.expectedReceiptDigest as string,
|
||||
}),
|
||||
}),
|
||||
});
|
||||
if (binding.commitmentPath !== expectedCommitmentPath) {
|
||||
configurationError('adopted application material binding drifted');
|
||||
@@ -381,7 +423,7 @@ function verifyAdoptedEvidence(
|
||||
intent: Readonly<LocalServiceManagerIntent>,
|
||||
binding: Readonly<AdoptedBinding>,
|
||||
uid: number,
|
||||
): string {
|
||||
): Readonly<VerifiedAdoptedEvidence> {
|
||||
const activation = object(
|
||||
readPrivateLocalCommandFile(binding.activationPath),
|
||||
'activation',
|
||||
@@ -428,6 +470,31 @@ function verifyAdoptedEvidence(
|
||||
) {
|
||||
configurationError('adopted activation or commitment drifted');
|
||||
}
|
||||
let legacyDataApplicationCommitDigest: string | null = null;
|
||||
let legacyDataApplicationReceiptDigest: string | null = null;
|
||||
if (binding.legacyDataApplication !== undefined) {
|
||||
try {
|
||||
const dataCommit = normalizeLocalDataDirectoryApplicationCommit(
|
||||
readPrivateLocalCommandFile(binding.legacyDataApplication.commitPath),
|
||||
);
|
||||
if (
|
||||
dataCommit.profile !== intent.profile ||
|
||||
dataCommit.commitDigest !==
|
||||
binding.legacyDataApplication.commitDigest ||
|
||||
dataCommit.receiptDigest !== binding.legacyDataApplication.receiptDigest
|
||||
) {
|
||||
configurationError('legacy data application commitment drifted');
|
||||
}
|
||||
legacyDataApplicationCommitDigest = dataCommit.commitDigest;
|
||||
legacyDataApplicationReceiptDigest = dataCommit.receiptDigest;
|
||||
} catch (error) {
|
||||
if (error instanceof LocalDeploymentConfigurationError) throw error;
|
||||
configurationError(
|
||||
'legacy data application commitment is invalid',
|
||||
error,
|
||||
);
|
||||
}
|
||||
}
|
||||
const target = privateFileIdentity(
|
||||
binding.targetPath,
|
||||
uid,
|
||||
@@ -458,14 +525,18 @@ function verifyAdoptedEvidence(
|
||||
) {
|
||||
configurationError('adopted data evidence drifted');
|
||||
}
|
||||
return cutoverDigest({
|
||||
target: target.digest,
|
||||
source: source.digest,
|
||||
recovery: recovery.digest,
|
||||
recoverySha256,
|
||||
manifest: manifestIdentity.digest,
|
||||
manifestDigest,
|
||||
sourceSha256,
|
||||
return Object.freeze({
|
||||
targetDataIdentityDigest: cutoverDigest({
|
||||
target: target.digest,
|
||||
source: source.digest,
|
||||
recovery: recovery.digest,
|
||||
recoverySha256,
|
||||
manifest: manifestIdentity.digest,
|
||||
manifestDigest,
|
||||
sourceSha256,
|
||||
}),
|
||||
legacyDataApplicationCommitDigest,
|
||||
legacyDataApplicationReceiptDigest,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -721,11 +792,7 @@ export async function consumeLocalServiceManagerCutoverOutcome(
|
||||
}
|
||||
const state = desiredState(intent, outcome);
|
||||
const binding = adoptedBinding(intent);
|
||||
const targetDataIdentityDigest = verifyAdoptedEvidence(
|
||||
intent,
|
||||
binding,
|
||||
identity.uid,
|
||||
);
|
||||
const adoptedEvidence = verifyAdoptedEvidence(intent, binding, identity.uid);
|
||||
let startupReceiptDigest: string | null = null;
|
||||
let shutdownReceiptDigest: string | null = null;
|
||||
let processIdentityDigest: string | null = null;
|
||||
@@ -806,7 +873,11 @@ export async function consumeLocalServiceManagerCutoverOutcome(
|
||||
applicationConfigDigest: binding.applicationConfigDigest,
|
||||
activationDigest: binding.activationDigest,
|
||||
commitmentDigest: binding.commitmentDigest,
|
||||
targetDataIdentityDigest,
|
||||
targetDataIdentityDigest: adoptedEvidence.targetDataIdentityDigest,
|
||||
legacyDataApplicationCommitDigest:
|
||||
adoptedEvidence.legacyDataApplicationCommitDigest,
|
||||
legacyDataApplicationReceiptDigest:
|
||||
adoptedEvidence.legacyDataApplicationReceiptDigest,
|
||||
startupReceiptDigest,
|
||||
shutdownReceiptDigest,
|
||||
processIdentityDigest,
|
||||
|
||||
+32
-3
@@ -27,6 +27,8 @@ export interface LocalServiceManagerCutoverEvidence {
|
||||
readonly activationDigest: string;
|
||||
readonly commitmentDigest: string;
|
||||
readonly targetDataIdentityDigest: string;
|
||||
readonly legacyDataApplicationCommitDigest?: string | null;
|
||||
readonly legacyDataApplicationReceiptDigest?: string | null;
|
||||
readonly startupReceiptDigest: string | null;
|
||||
readonly shutdownReceiptDigest: string | null;
|
||||
readonly processIdentityDigest: string | null;
|
||||
@@ -35,7 +37,7 @@ export interface LocalServiceManagerCutoverEvidence {
|
||||
|
||||
export interface LocalServiceManagerCutoverRecord {
|
||||
readonly schema: typeof SCHEMA;
|
||||
readonly schemaVersion: 1;
|
||||
readonly schemaVersion: 1 | 2;
|
||||
readonly actionId: string;
|
||||
readonly action: LocalServiceManagerAction;
|
||||
readonly state: LocalServiceManagerCutoverState;
|
||||
@@ -135,9 +137,25 @@ export function localServiceManagerCutoverRecord(
|
||||
if (intent.lineage.mode !== 'adopted') {
|
||||
configurationError('fresh service intent has no cutover lineage');
|
||||
}
|
||||
const hasCommitDigest = Object.hasOwn(
|
||||
evidence,
|
||||
'legacyDataApplicationCommitDigest',
|
||||
);
|
||||
const hasReceiptDigest = Object.hasOwn(
|
||||
evidence,
|
||||
'legacyDataApplicationReceiptDigest',
|
||||
);
|
||||
if (
|
||||
hasCommitDigest !== hasReceiptDigest ||
|
||||
(hasCommitDigest &&
|
||||
(evidence.legacyDataApplicationCommitDigest === null) !==
|
||||
(evidence.legacyDataApplicationReceiptDigest === null))
|
||||
) {
|
||||
configurationError('legacy data application evidence is incomplete');
|
||||
}
|
||||
const payload = Object.freeze({
|
||||
schema: SCHEMA,
|
||||
schemaVersion: 1 as const,
|
||||
schemaVersion: hasCommitDigest ? (2 as const) : (1 as const),
|
||||
actionId: intent.actionId,
|
||||
action: intent.action,
|
||||
state,
|
||||
@@ -190,6 +208,12 @@ export function normalizeLocalServiceManagerCutoverRecord(
|
||||
'commitmentDigest',
|
||||
'managerObservationDigest',
|
||||
'managerOutcomeDigest',
|
||||
...(record.schemaVersion === 2
|
||||
? [
|
||||
'legacyDataApplicationCommitDigest',
|
||||
'legacyDataApplicationReceiptDigest',
|
||||
]
|
||||
: []),
|
||||
'manualReason',
|
||||
'processIdentityDigest',
|
||||
'shutdownReceiptDigest',
|
||||
@@ -204,7 +228,7 @@ export function normalizeLocalServiceManagerCutoverRecord(
|
||||
const { recordDigest, ...payload } = record;
|
||||
if (
|
||||
record.schema !== SCHEMA ||
|
||||
record.schemaVersion !== 1 ||
|
||||
(record.schemaVersion !== 1 && record.schemaVersion !== 2) ||
|
||||
typeof record.actionId !== 'string' ||
|
||||
(record.action !== 'install-enable-start' &&
|
||||
record.action !== 'start' &&
|
||||
@@ -240,6 +264,11 @@ export function normalizeLocalServiceManagerCutoverRecord(
|
||||
!DIGEST_PATTERN.test(evidence.commitmentDigest) ||
|
||||
typeof evidence.targetDataIdentityDigest !== 'string' ||
|
||||
!DIGEST_PATTERN.test(evidence.targetDataIdentityDigest) ||
|
||||
(record.schemaVersion === 2 &&
|
||||
(!nullableDigest(evidence.legacyDataApplicationCommitDigest) ||
|
||||
!nullableDigest(evidence.legacyDataApplicationReceiptDigest) ||
|
||||
(evidence.legacyDataApplicationCommitDigest === null) !==
|
||||
(evidence.legacyDataApplicationReceiptDigest === null))) ||
|
||||
!nullableDigest(evidence.startupReceiptDigest) ||
|
||||
!nullableDigest(evidence.shutdownReceiptDigest) ||
|
||||
!nullableDigest(evidence.processIdentityDigest) ||
|
||||
|
||||
+77
-2
@@ -6,6 +6,7 @@ import {
|
||||
MAX_PRIVATE_LOCAL_JSON_FILE_BYTES,
|
||||
readPrivateLocalJsonFile,
|
||||
} from '@qinglong/local-command-file';
|
||||
import { normalizeLocalDataDirectoryApplicationCommit } from '@qinglong/local-sqlite/data-directory-application-commit';
|
||||
|
||||
import { currentIdentity } from '../foundation/contract';
|
||||
import { LocalDeploymentConfigurationError } from '../foundation/error';
|
||||
@@ -249,6 +250,11 @@ function parseApplicationIdentity(bytes: Buffer): Readonly<{
|
||||
targetDatabasePath: string;
|
||||
recoveryPath: string;
|
||||
manifestPath: string;
|
||||
legacyDataApplication?: Readonly<{
|
||||
commitPath: string;
|
||||
expectedCommitDigest: string;
|
||||
expectedReceiptDigest: string;
|
||||
}>;
|
||||
}>;
|
||||
}> {
|
||||
let value: unknown;
|
||||
@@ -262,7 +268,8 @@ function parseApplicationIdentity(bytes: Buffer): Readonly<{
|
||||
const application = object(value, 'application configuration');
|
||||
if (
|
||||
(application.schema !== 'qinglong/local-application-process@v2' &&
|
||||
application.schema !== 'qinglong/local-application-process@v3') ||
|
||||
application.schema !== 'qinglong/local-application-process@v3' &&
|
||||
application.schema !== 'qinglong/local-application-process@v4') ||
|
||||
(application.profile !== 'edge' && application.profile !== 'standalone') ||
|
||||
typeof application.instanceId !== 'string'
|
||||
) {
|
||||
@@ -280,13 +287,32 @@ function parseApplicationIdentity(bytes: Buffer): Readonly<{
|
||||
});
|
||||
}
|
||||
const cutover = object(application.cutover, 'application cutover');
|
||||
const legacyDataApplication =
|
||||
application.schema === 'qinglong/local-application-process@v4'
|
||||
? object(
|
||||
application.legacyDataApplication,
|
||||
'legacy data application binding',
|
||||
)
|
||||
: undefined;
|
||||
if (legacyDataApplication !== undefined) {
|
||||
exact(
|
||||
legacyDataApplication,
|
||||
['commitPath', 'expectedCommitDigest', 'expectedReceiptDigest'],
|
||||
'legacy data application binding',
|
||||
);
|
||||
}
|
||||
if (
|
||||
storage.mode !== 'adopted' ||
|
||||
typeof cutover.cutoverId !== 'string' ||
|
||||
typeof storage.expectedActivationDigest !== 'string' ||
|
||||
!DIGEST_PATTERN.test(storage.expectedActivationDigest) ||
|
||||
typeof cutover.expectedCommitmentDigest !== 'string' ||
|
||||
!DIGEST_PATTERN.test(cutover.expectedCommitmentDigest)
|
||||
!DIGEST_PATTERN.test(cutover.expectedCommitmentDigest) ||
|
||||
(legacyDataApplication !== undefined &&
|
||||
(typeof legacyDataApplication.expectedCommitDigest !== 'string' ||
|
||||
!DIGEST_PATTERN.test(legacyDataApplication.expectedCommitDigest) ||
|
||||
typeof legacyDataApplication.expectedReceiptDigest !== 'string' ||
|
||||
!DIGEST_PATTERN.test(legacyDataApplication.expectedReceiptDigest)))
|
||||
) {
|
||||
configurationError('adopted application binding is invalid');
|
||||
}
|
||||
@@ -316,10 +342,55 @@ function parseApplicationIdentity(bytes: Buffer): Readonly<{
|
||||
),
|
||||
recoveryPath: safeAbsolutePath(storage.recoveryPath, 'recoveryPath'),
|
||||
manifestPath: safeAbsolutePath(storage.manifestPath, 'manifestPath'),
|
||||
...(legacyDataApplication === undefined
|
||||
? {}
|
||||
: {
|
||||
legacyDataApplication: Object.freeze({
|
||||
commitPath: safeAbsolutePath(
|
||||
legacyDataApplication.commitPath,
|
||||
'legacyDataApplication.commitPath',
|
||||
),
|
||||
expectedCommitDigest:
|
||||
legacyDataApplication.expectedCommitDigest as string,
|
||||
expectedReceiptDigest:
|
||||
legacyDataApplication.expectedReceiptDigest as string,
|
||||
}),
|
||||
}),
|
||||
}),
|
||||
});
|
||||
}
|
||||
|
||||
function verifyApplicationDataCommitment(
|
||||
application: ReturnType<typeof parseApplicationIdentity>,
|
||||
): void {
|
||||
if (
|
||||
application.deployment.mode !== 'adopted' ||
|
||||
application.deployment.legacyDataApplication === undefined
|
||||
) {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
const binding = application.deployment.legacyDataApplication;
|
||||
const commit = normalizeLocalDataDirectoryApplicationCommit(
|
||||
readPrivateLocalJsonFile(binding.commitPath, {
|
||||
maxBytes: 64 * 1024,
|
||||
}),
|
||||
);
|
||||
if (
|
||||
commit.profile !== application.profile ||
|
||||
commit.commitDigest !== binding.expectedCommitDigest ||
|
||||
commit.receiptDigest !== binding.expectedReceiptDigest
|
||||
) {
|
||||
configurationError(
|
||||
'legacy data application commit does not match the application binding',
|
||||
);
|
||||
}
|
||||
} catch (error) {
|
||||
if (error instanceof LocalDeploymentConfigurationError) throw error;
|
||||
configurationError('legacy data application commitment is invalid', error);
|
||||
}
|
||||
}
|
||||
|
||||
function assertApplicationLineageBinding(
|
||||
application: ReturnType<typeof parseApplicationIdentity>,
|
||||
intent: Readonly<LocalServiceManagerIntent>,
|
||||
@@ -546,6 +617,7 @@ export function prepareLocalServiceManagerIntent(
|
||||
intentDigest: localServiceManagerIntentDigest(payload),
|
||||
});
|
||||
assertApplicationLineageBinding(application, intent);
|
||||
verifyApplicationDataCommitment(application);
|
||||
assertIntentLineageHead(intent, identity.uid);
|
||||
const intentPath = localServiceManagerIntentPath(root, intent.actionId);
|
||||
const contents = `${JSON.stringify(intent, null, 2)}\n`;
|
||||
@@ -700,12 +772,15 @@ export function consumeLocalServiceManagerOutcome(
|
||||
'service descriptor',
|
||||
);
|
||||
try {
|
||||
const application = parseApplicationIdentity(applicationBytes);
|
||||
if (
|
||||
sha256(applicationBytes) !== intent.deployment.applicationConfigSha256 ||
|
||||
sha256(descriptorBytes) !== intent.descriptor.sha256
|
||||
) {
|
||||
configurationError('service manager source material drifted');
|
||||
}
|
||||
assertApplicationLineageBinding(application, intent);
|
||||
verifyApplicationDataCommitment(application);
|
||||
} finally {
|
||||
applicationBytes.fill(0);
|
||||
descriptorBytes.fill(0);
|
||||
|
||||
@@ -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