mirror of
https://github.com/whyour/qinglong.git
synced 2026-09-22 19:29:13 +08:00
feat(ql3): bind service lineage to legacy data receipt
This commit is contained in:
+56
-1
@@ -6,6 +6,7 @@ import {
|
|||||||
readPrivateLocalCommandFile,
|
readPrivateLocalCommandFile,
|
||||||
readPrivateLocalJsonFile,
|
readPrivateLocalJsonFile,
|
||||||
} from '@qinglong/local-command-file';
|
} from '@qinglong/local-command-file';
|
||||||
|
import { normalizeLocalDataDirectoryApplicationCommit } from '@qinglong/local-sqlite/data-directory-application-commit';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
advanceLocalCutoverInstanceHead,
|
advanceLocalCutoverInstanceHead,
|
||||||
@@ -283,6 +284,8 @@ function adoptedPaths(
|
|||||||
command: Readonly<LocalServiceManagerLegacyRollbackPrepareCommand>,
|
command: Readonly<LocalServiceManagerLegacyRollbackPrepareCommand>,
|
||||||
expectedApplicationDigest: string,
|
expectedApplicationDigest: string,
|
||||||
expectedCommitmentDigest: string,
|
expectedCommitmentDigest: string,
|
||||||
|
expectedLegacyDataApplicationCommitDigest: string | null | undefined,
|
||||||
|
expectedLegacyDataApplicationReceiptDigest: string | null | undefined,
|
||||||
uid: number,
|
uid: number,
|
||||||
gid: number,
|
gid: number,
|
||||||
): Readonly<AdoptedPaths> {
|
): Readonly<AdoptedPaths> {
|
||||||
@@ -302,6 +305,17 @@ function adoptedPaths(
|
|||||||
);
|
);
|
||||||
const storage = object(application.storage, 'adopted storage');
|
const storage = object(application.storage, 'adopted storage');
|
||||||
const cutover = object(application.cutover, 'adopted cutover');
|
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(
|
const commitmentPath = safeAbsolutePath(
|
||||||
cutover.commitmentPath,
|
cutover.commitmentPath,
|
||||||
'commitmentPath',
|
'commitmentPath',
|
||||||
@@ -319,7 +333,8 @@ function adoptedPaths(
|
|||||||
);
|
);
|
||||||
const { commitmentDigest, ...commitmentPayload } = commitment;
|
const { commitmentDigest, ...commitmentPayload } = commitment;
|
||||||
if (
|
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.profile !== command.request.profile ||
|
||||||
application.instanceId !== command.request.instanceId ||
|
application.instanceId !== command.request.instanceId ||
|
||||||
storage.mode !== 'adopted' ||
|
storage.mode !== 'adopted' ||
|
||||||
@@ -340,6 +355,44 @@ function adoptedPaths(
|
|||||||
) {
|
) {
|
||||||
configurationError('adopted application rollback binding drifted');
|
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({
|
return Object.freeze({
|
||||||
activationPath: safeAbsolutePath(storage.activationPath, 'activationPath'),
|
activationPath: safeAbsolutePath(storage.activationPath, 'activationPath'),
|
||||||
legacySourcePath: safeAbsolutePath(storage.sourcePath, 'legacySourcePath'),
|
legacySourcePath: safeAbsolutePath(storage.sourcePath, 'legacySourcePath'),
|
||||||
@@ -571,6 +624,8 @@ export function prepareLocalServiceManagerLegacyRollback(
|
|||||||
command,
|
command,
|
||||||
stopped.evidence.applicationConfigDigest,
|
stopped.evidence.applicationConfigDigest,
|
||||||
stopped.evidence.commitmentDigest,
|
stopped.evidence.commitmentDigest,
|
||||||
|
stopped.evidence.legacyDataApplicationCommitDigest,
|
||||||
|
stopped.evidence.legacyDataApplicationReceiptDigest,
|
||||||
identity.uid,
|
identity.uid,
|
||||||
identity.gid,
|
identity.gid,
|
||||||
);
|
);
|
||||||
|
|||||||
+88
-17
@@ -7,6 +7,7 @@ import {
|
|||||||
readPrivateLocalCommandFile,
|
readPrivateLocalCommandFile,
|
||||||
readPrivateLocalJsonFile,
|
readPrivateLocalJsonFile,
|
||||||
} from '@qinglong/local-command-file';
|
} from '@qinglong/local-command-file';
|
||||||
|
import { normalizeLocalDataDirectoryApplicationCommit } from '@qinglong/local-sqlite/data-directory-application-commit';
|
||||||
|
|
||||||
import { currentIdentity } from '../foundation/contract';
|
import { currentIdentity } from '../foundation/contract';
|
||||||
import { LocalDeploymentConfigurationError } from '../foundation/error';
|
import { LocalDeploymentConfigurationError } from '../foundation/error';
|
||||||
@@ -96,6 +97,17 @@ interface AdoptedBinding {
|
|||||||
readonly recoveryPath: string;
|
readonly recoveryPath: string;
|
||||||
readonly manifestPath: string;
|
readonly manifestPath: string;
|
||||||
readonly applicationConfigDigest: 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 {
|
function configurationError(message: string, cause?: unknown): never {
|
||||||
@@ -249,6 +261,17 @@ function adoptedBinding(
|
|||||||
);
|
);
|
||||||
const storage = object(config.storage, 'adopted storage');
|
const storage = object(config.storage, 'adopted storage');
|
||||||
const cutover = object(config.cutover, 'adopted cutover');
|
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(
|
const expectedCommitmentPath = path.join(
|
||||||
intent.deployment.root,
|
intent.deployment.root,
|
||||||
'service',
|
'service',
|
||||||
@@ -257,7 +280,8 @@ function adoptedBinding(
|
|||||||
'0002-legacy-stopped.json',
|
'0002-legacy-stopped.json',
|
||||||
);
|
);
|
||||||
if (
|
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.profile !== intent.profile ||
|
||||||
config.instanceId !== intent.instanceId ||
|
config.instanceId !== intent.instanceId ||
|
||||||
storage.mode !== 'adopted' ||
|
storage.mode !== 'adopted' ||
|
||||||
@@ -265,7 +289,12 @@ function adoptedBinding(
|
|||||||
intent.lineage.expectedActivationDigest ||
|
intent.lineage.expectedActivationDigest ||
|
||||||
cutover.cutoverId !== intent.lineage.cutoverId ||
|
cutover.cutoverId !== intent.lineage.cutoverId ||
|
||||||
typeof cutover.expectedCommitmentDigest !== 'string' ||
|
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');
|
configurationError('adopted application configuration drifted');
|
||||||
}
|
}
|
||||||
@@ -280,6 +309,19 @@ function adoptedBinding(
|
|||||||
recoveryPath: safeAbsolutePath(storage.recoveryPath, 'recoveryPath'),
|
recoveryPath: safeAbsolutePath(storage.recoveryPath, 'recoveryPath'),
|
||||||
manifestPath: safeAbsolutePath(storage.manifestPath, 'manifestPath'),
|
manifestPath: safeAbsolutePath(storage.manifestPath, 'manifestPath'),
|
||||||
applicationConfigDigest: intent.deployment.applicationConfigSha256,
|
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) {
|
if (binding.commitmentPath !== expectedCommitmentPath) {
|
||||||
configurationError('adopted application material binding drifted');
|
configurationError('adopted application material binding drifted');
|
||||||
@@ -381,7 +423,7 @@ function verifyAdoptedEvidence(
|
|||||||
intent: Readonly<LocalServiceManagerIntent>,
|
intent: Readonly<LocalServiceManagerIntent>,
|
||||||
binding: Readonly<AdoptedBinding>,
|
binding: Readonly<AdoptedBinding>,
|
||||||
uid: number,
|
uid: number,
|
||||||
): string {
|
): Readonly<VerifiedAdoptedEvidence> {
|
||||||
const activation = object(
|
const activation = object(
|
||||||
readPrivateLocalCommandFile(binding.activationPath),
|
readPrivateLocalCommandFile(binding.activationPath),
|
||||||
'activation',
|
'activation',
|
||||||
@@ -428,6 +470,31 @@ function verifyAdoptedEvidence(
|
|||||||
) {
|
) {
|
||||||
configurationError('adopted activation or commitment drifted');
|
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(
|
const target = privateFileIdentity(
|
||||||
binding.targetPath,
|
binding.targetPath,
|
||||||
uid,
|
uid,
|
||||||
@@ -458,14 +525,18 @@ function verifyAdoptedEvidence(
|
|||||||
) {
|
) {
|
||||||
configurationError('adopted data evidence drifted');
|
configurationError('adopted data evidence drifted');
|
||||||
}
|
}
|
||||||
return cutoverDigest({
|
return Object.freeze({
|
||||||
target: target.digest,
|
targetDataIdentityDigest: cutoverDigest({
|
||||||
source: source.digest,
|
target: target.digest,
|
||||||
recovery: recovery.digest,
|
source: source.digest,
|
||||||
recoverySha256,
|
recovery: recovery.digest,
|
||||||
manifest: manifestIdentity.digest,
|
recoverySha256,
|
||||||
manifestDigest,
|
manifest: manifestIdentity.digest,
|
||||||
sourceSha256,
|
manifestDigest,
|
||||||
|
sourceSha256,
|
||||||
|
}),
|
||||||
|
legacyDataApplicationCommitDigest,
|
||||||
|
legacyDataApplicationReceiptDigest,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -721,11 +792,7 @@ export async function consumeLocalServiceManagerCutoverOutcome(
|
|||||||
}
|
}
|
||||||
const state = desiredState(intent, outcome);
|
const state = desiredState(intent, outcome);
|
||||||
const binding = adoptedBinding(intent);
|
const binding = adoptedBinding(intent);
|
||||||
const targetDataIdentityDigest = verifyAdoptedEvidence(
|
const adoptedEvidence = verifyAdoptedEvidence(intent, binding, identity.uid);
|
||||||
intent,
|
|
||||||
binding,
|
|
||||||
identity.uid,
|
|
||||||
);
|
|
||||||
let startupReceiptDigest: string | null = null;
|
let startupReceiptDigest: string | null = null;
|
||||||
let shutdownReceiptDigest: string | null = null;
|
let shutdownReceiptDigest: string | null = null;
|
||||||
let processIdentityDigest: string | null = null;
|
let processIdentityDigest: string | null = null;
|
||||||
@@ -806,7 +873,11 @@ export async function consumeLocalServiceManagerCutoverOutcome(
|
|||||||
applicationConfigDigest: binding.applicationConfigDigest,
|
applicationConfigDigest: binding.applicationConfigDigest,
|
||||||
activationDigest: binding.activationDigest,
|
activationDigest: binding.activationDigest,
|
||||||
commitmentDigest: binding.commitmentDigest,
|
commitmentDigest: binding.commitmentDigest,
|
||||||
targetDataIdentityDigest,
|
targetDataIdentityDigest: adoptedEvidence.targetDataIdentityDigest,
|
||||||
|
legacyDataApplicationCommitDigest:
|
||||||
|
adoptedEvidence.legacyDataApplicationCommitDigest,
|
||||||
|
legacyDataApplicationReceiptDigest:
|
||||||
|
adoptedEvidence.legacyDataApplicationReceiptDigest,
|
||||||
startupReceiptDigest,
|
startupReceiptDigest,
|
||||||
shutdownReceiptDigest,
|
shutdownReceiptDigest,
|
||||||
processIdentityDigest,
|
processIdentityDigest,
|
||||||
|
|||||||
+32
-3
@@ -27,6 +27,8 @@ export interface LocalServiceManagerCutoverEvidence {
|
|||||||
readonly activationDigest: string;
|
readonly activationDigest: string;
|
||||||
readonly commitmentDigest: string;
|
readonly commitmentDigest: string;
|
||||||
readonly targetDataIdentityDigest: string;
|
readonly targetDataIdentityDigest: string;
|
||||||
|
readonly legacyDataApplicationCommitDigest?: string | null;
|
||||||
|
readonly legacyDataApplicationReceiptDigest?: string | null;
|
||||||
readonly startupReceiptDigest: string | null;
|
readonly startupReceiptDigest: string | null;
|
||||||
readonly shutdownReceiptDigest: string | null;
|
readonly shutdownReceiptDigest: string | null;
|
||||||
readonly processIdentityDigest: string | null;
|
readonly processIdentityDigest: string | null;
|
||||||
@@ -35,7 +37,7 @@ export interface LocalServiceManagerCutoverEvidence {
|
|||||||
|
|
||||||
export interface LocalServiceManagerCutoverRecord {
|
export interface LocalServiceManagerCutoverRecord {
|
||||||
readonly schema: typeof SCHEMA;
|
readonly schema: typeof SCHEMA;
|
||||||
readonly schemaVersion: 1;
|
readonly schemaVersion: 1 | 2;
|
||||||
readonly actionId: string;
|
readonly actionId: string;
|
||||||
readonly action: LocalServiceManagerAction;
|
readonly action: LocalServiceManagerAction;
|
||||||
readonly state: LocalServiceManagerCutoverState;
|
readonly state: LocalServiceManagerCutoverState;
|
||||||
@@ -135,9 +137,25 @@ export function localServiceManagerCutoverRecord(
|
|||||||
if (intent.lineage.mode !== 'adopted') {
|
if (intent.lineage.mode !== 'adopted') {
|
||||||
configurationError('fresh service intent has no cutover lineage');
|
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({
|
const payload = Object.freeze({
|
||||||
schema: SCHEMA,
|
schema: SCHEMA,
|
||||||
schemaVersion: 1 as const,
|
schemaVersion: hasCommitDigest ? (2 as const) : (1 as const),
|
||||||
actionId: intent.actionId,
|
actionId: intent.actionId,
|
||||||
action: intent.action,
|
action: intent.action,
|
||||||
state,
|
state,
|
||||||
@@ -190,6 +208,12 @@ export function normalizeLocalServiceManagerCutoverRecord(
|
|||||||
'commitmentDigest',
|
'commitmentDigest',
|
||||||
'managerObservationDigest',
|
'managerObservationDigest',
|
||||||
'managerOutcomeDigest',
|
'managerOutcomeDigest',
|
||||||
|
...(record.schemaVersion === 2
|
||||||
|
? [
|
||||||
|
'legacyDataApplicationCommitDigest',
|
||||||
|
'legacyDataApplicationReceiptDigest',
|
||||||
|
]
|
||||||
|
: []),
|
||||||
'manualReason',
|
'manualReason',
|
||||||
'processIdentityDigest',
|
'processIdentityDigest',
|
||||||
'shutdownReceiptDigest',
|
'shutdownReceiptDigest',
|
||||||
@@ -204,7 +228,7 @@ export function normalizeLocalServiceManagerCutoverRecord(
|
|||||||
const { recordDigest, ...payload } = record;
|
const { recordDigest, ...payload } = record;
|
||||||
if (
|
if (
|
||||||
record.schema !== SCHEMA ||
|
record.schema !== SCHEMA ||
|
||||||
record.schemaVersion !== 1 ||
|
(record.schemaVersion !== 1 && record.schemaVersion !== 2) ||
|
||||||
typeof record.actionId !== 'string' ||
|
typeof record.actionId !== 'string' ||
|
||||||
(record.action !== 'install-enable-start' &&
|
(record.action !== 'install-enable-start' &&
|
||||||
record.action !== 'start' &&
|
record.action !== 'start' &&
|
||||||
@@ -240,6 +264,11 @@ export function normalizeLocalServiceManagerCutoverRecord(
|
|||||||
!DIGEST_PATTERN.test(evidence.commitmentDigest) ||
|
!DIGEST_PATTERN.test(evidence.commitmentDigest) ||
|
||||||
typeof evidence.targetDataIdentityDigest !== 'string' ||
|
typeof evidence.targetDataIdentityDigest !== 'string' ||
|
||||||
!DIGEST_PATTERN.test(evidence.targetDataIdentityDigest) ||
|
!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.startupReceiptDigest) ||
|
||||||
!nullableDigest(evidence.shutdownReceiptDigest) ||
|
!nullableDigest(evidence.shutdownReceiptDigest) ||
|
||||||
!nullableDigest(evidence.processIdentityDigest) ||
|
!nullableDigest(evidence.processIdentityDigest) ||
|
||||||
|
|||||||
+77
-2
@@ -6,6 +6,7 @@ import {
|
|||||||
MAX_PRIVATE_LOCAL_JSON_FILE_BYTES,
|
MAX_PRIVATE_LOCAL_JSON_FILE_BYTES,
|
||||||
readPrivateLocalJsonFile,
|
readPrivateLocalJsonFile,
|
||||||
} from '@qinglong/local-command-file';
|
} from '@qinglong/local-command-file';
|
||||||
|
import { normalizeLocalDataDirectoryApplicationCommit } from '@qinglong/local-sqlite/data-directory-application-commit';
|
||||||
|
|
||||||
import { currentIdentity } from '../foundation/contract';
|
import { currentIdentity } from '../foundation/contract';
|
||||||
import { LocalDeploymentConfigurationError } from '../foundation/error';
|
import { LocalDeploymentConfigurationError } from '../foundation/error';
|
||||||
@@ -249,6 +250,11 @@ function parseApplicationIdentity(bytes: Buffer): Readonly<{
|
|||||||
targetDatabasePath: string;
|
targetDatabasePath: string;
|
||||||
recoveryPath: string;
|
recoveryPath: string;
|
||||||
manifestPath: string;
|
manifestPath: string;
|
||||||
|
legacyDataApplication?: Readonly<{
|
||||||
|
commitPath: string;
|
||||||
|
expectedCommitDigest: string;
|
||||||
|
expectedReceiptDigest: string;
|
||||||
|
}>;
|
||||||
}>;
|
}>;
|
||||||
}> {
|
}> {
|
||||||
let value: unknown;
|
let value: unknown;
|
||||||
@@ -262,7 +268,8 @@ function parseApplicationIdentity(bytes: Buffer): Readonly<{
|
|||||||
const application = object(value, 'application configuration');
|
const application = object(value, 'application configuration');
|
||||||
if (
|
if (
|
||||||
(application.schema !== 'qinglong/local-application-process@v2' &&
|
(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') ||
|
(application.profile !== 'edge' && application.profile !== 'standalone') ||
|
||||||
typeof application.instanceId !== 'string'
|
typeof application.instanceId !== 'string'
|
||||||
) {
|
) {
|
||||||
@@ -280,13 +287,32 @@ function parseApplicationIdentity(bytes: Buffer): Readonly<{
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
const cutover = object(application.cutover, 'application cutover');
|
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 (
|
if (
|
||||||
storage.mode !== 'adopted' ||
|
storage.mode !== 'adopted' ||
|
||||||
typeof cutover.cutoverId !== 'string' ||
|
typeof cutover.cutoverId !== 'string' ||
|
||||||
typeof storage.expectedActivationDigest !== 'string' ||
|
typeof storage.expectedActivationDigest !== 'string' ||
|
||||||
!DIGEST_PATTERN.test(storage.expectedActivationDigest) ||
|
!DIGEST_PATTERN.test(storage.expectedActivationDigest) ||
|
||||||
typeof cutover.expectedCommitmentDigest !== 'string' ||
|
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');
|
configurationError('adopted application binding is invalid');
|
||||||
}
|
}
|
||||||
@@ -316,10 +342,55 @@ function parseApplicationIdentity(bytes: Buffer): Readonly<{
|
|||||||
),
|
),
|
||||||
recoveryPath: safeAbsolutePath(storage.recoveryPath, 'recoveryPath'),
|
recoveryPath: safeAbsolutePath(storage.recoveryPath, 'recoveryPath'),
|
||||||
manifestPath: safeAbsolutePath(storage.manifestPath, 'manifestPath'),
|
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(
|
function assertApplicationLineageBinding(
|
||||||
application: ReturnType<typeof parseApplicationIdentity>,
|
application: ReturnType<typeof parseApplicationIdentity>,
|
||||||
intent: Readonly<LocalServiceManagerIntent>,
|
intent: Readonly<LocalServiceManagerIntent>,
|
||||||
@@ -546,6 +617,7 @@ export function prepareLocalServiceManagerIntent(
|
|||||||
intentDigest: localServiceManagerIntentDigest(payload),
|
intentDigest: localServiceManagerIntentDigest(payload),
|
||||||
});
|
});
|
||||||
assertApplicationLineageBinding(application, intent);
|
assertApplicationLineageBinding(application, intent);
|
||||||
|
verifyApplicationDataCommitment(application);
|
||||||
assertIntentLineageHead(intent, identity.uid);
|
assertIntentLineageHead(intent, identity.uid);
|
||||||
const intentPath = localServiceManagerIntentPath(root, intent.actionId);
|
const intentPath = localServiceManagerIntentPath(root, intent.actionId);
|
||||||
const contents = `${JSON.stringify(intent, null, 2)}\n`;
|
const contents = `${JSON.stringify(intent, null, 2)}\n`;
|
||||||
@@ -700,12 +772,15 @@ export function consumeLocalServiceManagerOutcome(
|
|||||||
'service descriptor',
|
'service descriptor',
|
||||||
);
|
);
|
||||||
try {
|
try {
|
||||||
|
const application = parseApplicationIdentity(applicationBytes);
|
||||||
if (
|
if (
|
||||||
sha256(applicationBytes) !== intent.deployment.applicationConfigSha256 ||
|
sha256(applicationBytes) !== intent.deployment.applicationConfigSha256 ||
|
||||||
sha256(descriptorBytes) !== intent.descriptor.sha256
|
sha256(descriptorBytes) !== intent.descriptor.sha256
|
||||||
) {
|
) {
|
||||||
configurationError('service manager source material drifted');
|
configurationError('service manager source material drifted');
|
||||||
}
|
}
|
||||||
|
assertApplicationLineageBinding(application, intent);
|
||||||
|
verifyApplicationDataCommitment(application);
|
||||||
} finally {
|
} finally {
|
||||||
applicationBytes.fill(0);
|
applicationBytes.fill(0);
|
||||||
descriptorBytes.fill(0);
|
descriptorBytes.fill(0);
|
||||||
|
|||||||
@@ -36,6 +36,9 @@ const {
|
|||||||
const {
|
const {
|
||||||
cutoverDigest,
|
cutoverDigest,
|
||||||
} = require('../dist/deployment/cutover/targetEvidence.js');
|
} = require('../dist/deployment/cutover/targetEvidence.js');
|
||||||
|
const {
|
||||||
|
createLocalDataDirectoryApplicationCommit,
|
||||||
|
} = require('@qinglong/local-sqlite/data-directory-application-commit');
|
||||||
|
|
||||||
function sha256(value) {
|
function sha256(value) {
|
||||||
return crypto.createHash('sha256').update(value).digest('hex');
|
return crypto.createHash('sha256').update(value).digest('hex');
|
||||||
@@ -124,9 +127,29 @@ function fixture(t) {
|
|||||||
...commitmentPayload,
|
...commitmentPayload,
|
||||||
commitmentDigest,
|
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');
|
const applicationPath = path.join(root, 'local-application.json');
|
||||||
writePrivate(applicationPath, {
|
writePrivate(applicationPath, {
|
||||||
schema: 'qinglong/local-application-process@v3',
|
schema: 'qinglong/local-application-process@v4',
|
||||||
instanceId: 'edge-router-1',
|
instanceId: 'edge-router-1',
|
||||||
profile: 'edge',
|
profile: 'edge',
|
||||||
storage: {
|
storage: {
|
||||||
@@ -146,6 +169,11 @@ function fixture(t) {
|
|||||||
commitmentPath,
|
commitmentPath,
|
||||||
expectedCommitmentDigest: commitmentDigest,
|
expectedCommitmentDigest: commitmentDigest,
|
||||||
},
|
},
|
||||||
|
legacyDataApplication: {
|
||||||
|
commitPath: dataCommitPath,
|
||||||
|
expectedCommitDigest: dataCommit.commitDigest,
|
||||||
|
expectedReceiptDigest: dataCommit.receiptDigest,
|
||||||
|
},
|
||||||
});
|
});
|
||||||
writePrivate(
|
writePrivate(
|
||||||
path.join(service, 'qinglong3.service'),
|
path.join(service, 'qinglong3.service'),
|
||||||
@@ -178,6 +206,8 @@ function fixture(t) {
|
|||||||
activationDigest,
|
activationDigest,
|
||||||
commitmentDigest,
|
commitmentDigest,
|
||||||
commitmentPath,
|
commitmentPath,
|
||||||
|
dataCommit,
|
||||||
|
dataCommitPath,
|
||||||
applicationPath,
|
applicationPath,
|
||||||
sourcePath,
|
sourcePath,
|
||||||
targetPath,
|
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'));
|
const record = JSON.parse(fs.readFileSync(recordPath, 'utf8'));
|
||||||
assert.equal(record.evidence.startupReceiptDigest, receiptDigest);
|
assert.equal(record.evidence.startupReceiptDigest, receiptDigest);
|
||||||
assert.match(record.evidence.processIdentityDigest, /^[0-9a-f]{64}$/);
|
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(
|
assert.equal(
|
||||||
(
|
(
|
||||||
await consumeLocalServiceManagerCutoverOutcome(command, {
|
await consumeLocalServiceManagerCutoverOutcome(command, {
|
||||||
@@ -930,6 +969,40 @@ test('rejects legacy source content drift before committing service active', asy
|
|||||||
assert.equal(head.state, 'legacy_stopped');
|
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) => {
|
test('terminalizes a manager PID replaced before Owner receipt verification', async (t) => {
|
||||||
const state = fixture(t);
|
const state = fixture(t);
|
||||||
const prepared = prepare(
|
const prepared = prepare(
|
||||||
|
|||||||
@@ -19,6 +19,9 @@ const {
|
|||||||
advanceLocalCutoverInstanceHead,
|
advanceLocalCutoverInstanceHead,
|
||||||
claimLocalCutoverInstance,
|
claimLocalCutoverInstance,
|
||||||
} = require('../dist/deployment/cutover/instanceLineage.js');
|
} = require('../dist/deployment/cutover/instanceLineage.js');
|
||||||
|
const {
|
||||||
|
createLocalDataDirectoryApplicationCommit,
|
||||||
|
} = require('@qinglong/local-sqlite/data-directory-application-commit');
|
||||||
|
|
||||||
const roots = [];
|
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', () => {
|
test('does not allow fresh service intent to bypass an existing cutover head', () => {
|
||||||
const { root } = fixture();
|
const { root } = fixture();
|
||||||
adoptedHead(root);
|
adoptedHead(root);
|
||||||
|
|||||||
@@ -1781,6 +1781,15 @@ function auditSourceImports(root, packagePath, findings) {
|
|||||||
'src/lifecycle/data-directory-adoption/application/cleanup.ts' &&
|
'src/lifecycle/data-directory-adoption/application/cleanup.ts' &&
|
||||||
specifier === '@qinglong/local-sqlite/data-directory-adoption'
|
specifier === '@qinglong/local-sqlite/data-directory-adoption'
|
||||||
) &&
|
) &&
|
||||||
|
!(
|
||||||
|
[
|
||||||
|
'src/deployment/service-manager/serviceManagerIntent.ts',
|
||||||
|
'src/deployment/service-manager/serviceCutoverConsumer.ts',
|
||||||
|
'src/deployment/service-manager/legacy-rollback/preparation.ts',
|
||||||
|
].includes(path.relative(packageDirectory, filePath)) &&
|
||||||
|
specifier ===
|
||||||
|
'@qinglong/local-sqlite/data-directory-application-commit'
|
||||||
|
) &&
|
||||||
!(
|
!(
|
||||||
path.relative(packageDirectory, filePath) ===
|
path.relative(packageDirectory, filePath) ===
|
||||||
'src/plugin-package/pluginPackageCommand.ts' &&
|
'src/plugin-package/pluginPackageCommand.ts' &&
|
||||||
|
|||||||
@@ -813,7 +813,12 @@ function execute(root, controllerRoot, managerOptions, prepared, uid, gid) {
|
|||||||
return { commandPath, result, consumed };
|
return { commandPath, result, consumed };
|
||||||
}
|
}
|
||||||
|
|
||||||
function serviceProcessUid(kind, outcome) {
|
function serviceProcessUid(
|
||||||
|
kind,
|
||||||
|
outcome,
|
||||||
|
expectedCommand = '/bin/sleep',
|
||||||
|
expectedArguments = ['300'],
|
||||||
|
) {
|
||||||
let pid = kind === 'systemd' ? outcome.observation.mainPid : 0;
|
let pid = kind === 'systemd' ? outcome.observation.mainPid : 0;
|
||||||
if (kind === 'openrc') {
|
if (kind === 'openrc') {
|
||||||
for (const entry of fs.readdirSync('/proc')) {
|
for (const entry of fs.readdirSync('/proc')) {
|
||||||
@@ -822,7 +827,12 @@ function serviceProcessUid(kind, outcome) {
|
|||||||
const command = fs
|
const command = fs
|
||||||
.readFileSync(`/proc/${entry}/cmdline`, 'utf8')
|
.readFileSync(`/proc/${entry}/cmdline`, 'utf8')
|
||||||
.split('\0');
|
.split('\0');
|
||||||
if (command[0] === '/bin/sleep' && command[1] === '300') {
|
if (
|
||||||
|
command[0] === expectedCommand &&
|
||||||
|
expectedArguments.every(
|
||||||
|
(argument, index) => command[index + 1] === argument,
|
||||||
|
)
|
||||||
|
) {
|
||||||
pid = Number(entry);
|
pid = Number(entry);
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
@@ -1128,9 +1138,17 @@ async function main(argv) {
|
|||||||
) {
|
) {
|
||||||
fail('adopted legacy rollback did not converge to legacy_running');
|
fail('adopted legacy rollback did not converge to legacy_running');
|
||||||
}
|
}
|
||||||
const legacyUid = serviceProcessUid(kind, {
|
const legacyUid = serviceProcessUid(
|
||||||
observation: adoptedLegacyStarted.outcome.legacyObservation,
|
kind,
|
||||||
});
|
{
|
||||||
|
observation: adoptedLegacyStarted.outcome.legacyObservation,
|
||||||
|
},
|
||||||
|
fs.realpathSync(process.execPath),
|
||||||
|
[
|
||||||
|
'/workspace/scripts/ql3-service-manager-legacy-live-service.cjs',
|
||||||
|
String(legacyHttpPort),
|
||||||
|
],
|
||||||
|
);
|
||||||
if (legacyUid !== uid) {
|
if (legacyUid !== uid) {
|
||||||
fail(`legacy service process UID drifted: ${legacyUid} != ${uid}`);
|
fail(`legacy service process UID drifted: ${legacyUid} != ${uid}`);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2518,6 +2518,55 @@ test('local application receives only the pure data application commit codec', (
|
|||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('service-manager Owner consumers receive only the pure data commit codec', (t) => {
|
||||||
|
const root = fs.mkdtempSync(
|
||||||
|
path.join(os.tmpdir(), 'ql3-service-data-commit-codec-boundary-'),
|
||||||
|
);
|
||||||
|
const managerDirectory = path.join(
|
||||||
|
root,
|
||||||
|
'packages/ql3-local-owner-cli/src/deployment/service-manager',
|
||||||
|
);
|
||||||
|
const rollbackDirectory = path.join(managerDirectory, 'legacy-rollback');
|
||||||
|
fs.mkdirSync(rollbackDirectory, { recursive: true });
|
||||||
|
for (const filePath of [
|
||||||
|
path.join(managerDirectory, 'serviceManagerIntent.ts'),
|
||||||
|
path.join(managerDirectory, 'serviceCutoverConsumer.ts'),
|
||||||
|
path.join(rollbackDirectory, 'preparation.ts'),
|
||||||
|
]) {
|
||||||
|
fs.writeFileSync(
|
||||||
|
filePath,
|
||||||
|
"import { normalize } from '@qinglong/local-sqlite/data-directory-application-commit';",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
fs.writeFileSync(
|
||||||
|
path.join(managerDirectory, 'neighbor.ts'),
|
||||||
|
"import { normalize } from '@qinglong/local-sqlite/data-directory-application-commit';",
|
||||||
|
);
|
||||||
|
fs.appendFileSync(
|
||||||
|
path.join(managerDirectory, 'serviceCutoverConsumer.ts'),
|
||||||
|
"\nimport { mutate } from '@qinglong/local-sqlite/data-directory-adoption';",
|
||||||
|
);
|
||||||
|
t.after(() => fs.rmSync(root, { recursive: true, force: true }));
|
||||||
|
|
||||||
|
const findings = [];
|
||||||
|
auditSourceImports(root, 'packages/ql3-local-owner-cli', findings);
|
||||||
|
assert.deepEqual(
|
||||||
|
findings.map(({ code, file, specifier }) => ({ code, file, specifier })),
|
||||||
|
[
|
||||||
|
{
|
||||||
|
code: 'FORBIDDEN_LOCAL_ADOPTION_CLI_AUTHORITY_IMPORT',
|
||||||
|
file: 'packages/ql3-local-owner-cli/src/deployment/service-manager/neighbor.ts',
|
||||||
|
specifier: '@qinglong/local-sqlite/data-directory-application-commit',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
code: 'FORBIDDEN_LOCAL_ADOPTION_CLI_AUTHORITY_IMPORT',
|
||||||
|
file: 'packages/ql3-local-owner-cli/src/deployment/service-manager/serviceCutoverConsumer.ts',
|
||||||
|
specifier: '@qinglong/local-sqlite/data-directory-adoption',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
test('local AI application imports only the reviewed dynamic composition subpaths', (t) => {
|
test('local AI application imports only the reviewed dynamic composition subpaths', (t) => {
|
||||||
const root = fs.mkdtempSync(
|
const root = fs.mkdtempSync(
|
||||||
path.join(os.tmpdir(), 'ql3-local-ai-application-boundary-'),
|
path.join(os.tmpdir(), 'ql3-local-ai-application-boundary-'),
|
||||||
|
|||||||
Reference in New Issue
Block a user