Files
qinglong/packages/ql3-local-owner-cli/src/deployment/reconciliation/application/secret-and-config/planReader.ts
T

495 lines
17 KiB
TypeScript

import { createHash } from 'node:crypto';
import fs from 'node:fs';
import { LocalDeploymentConfigurationError } from '../../../foundation/error';
import { cutoverDigest } from '../../../cutover/targetEvidence';
import type {
LocalReconciliationSecretConfigCandidateRequirement,
LocalReconciliationSecretConfigPlanHeader,
LocalReconciliationSecretConfigPlanReceipt,
} from './rowPlan';
const HEADER_KIND = 'qinglong3-local-reconciliation-secret-config-plan-header';
const ROW_KIND = 'qinglong3-local-reconciliation-secret-config-plan-row';
const CANDIDATE_KIND =
'qinglong3-local-reconciliation-secret-config-plan-candidate';
const FOOTER_KIND = 'qinglong3-local-reconciliation-secret-config-plan-footer';
const READ_CHUNK_BYTES = 64 * 1024;
const MAX_LINE_BYTES = 64 * 1024;
const DIGEST_PATTERN = /^[0-9a-f]{64}$/;
interface FileLine {
readonly value: Buffer;
readonly framed: Buffer;
}
export interface LocalReconciliationSecretConfigDecisionRequirement {
readonly candidateOrdinal: number;
readonly candidateType: 'active_binding' | 'disabled_preservation';
readonly candidateDigest: string;
readonly sourceSetDigest: string;
readonly requirement: LocalReconciliationSecretConfigCandidateRequirement;
}
function configurationError(message: string, cause?: unknown): never {
throw new LocalDeploymentConfigurationError(
`reconciliation secret config plan reader ${message}`,
{ cause },
);
}
function exact(
value: unknown,
keys: readonly string[],
label: string,
): Record<string, unknown> {
if (!value || typeof value !== 'object' || Array.isArray(value)) {
configurationError(`${label} must be an object`);
}
const record = value as Record<string, unknown>;
const actual = Object.keys(record).sort();
const expected = [...keys].sort();
if (
actual.length !== expected.length ||
actual.some((key, index) => key !== expected[index])
) {
configurationError(`${label} shape is invalid`);
}
return record;
}
function parse(line: Buffer, label: string): unknown {
if (line.length < 2 || line.length > MAX_LINE_BYTES) {
configurationError(`${label} exceeds its line bound`);
}
try {
return JSON.parse(
new TextDecoder('utf-8', { fatal: true }).decode(line),
) as unknown;
} catch (error) {
return configurationError(`${label} is not UTF-8 JSON`, error);
}
}
function* lines(descriptor: number, size: number): Iterable<FileLine> {
let position = 0;
let pending = Buffer.alloc(0);
try {
while (position < size) {
const chunk = Buffer.allocUnsafe(
Math.min(READ_CHUNK_BYTES, size - position),
);
const bytesRead = fs.readSync(
descriptor,
chunk,
0,
chunk.length,
position,
);
if (bytesRead < 1) {
chunk.fill(0);
configurationError('file ended unexpectedly');
}
position += bytesRead;
const material = pending.length
? Buffer.concat([pending, chunk.subarray(0, bytesRead)])
: Buffer.from(chunk.subarray(0, bytesRead));
pending.fill(0);
chunk.fill(0);
let cursor = 0;
for (;;) {
const newline = material.indexOf(0x0a, cursor);
if (newline < 0) break;
const framed = Buffer.from(material.subarray(cursor, newline + 1));
const value = Buffer.from(material.subarray(cursor, newline));
cursor = newline + 1;
yield Object.freeze({ value, framed });
}
pending = Buffer.from(material.subarray(cursor));
material.fill(0);
if (pending.length > MAX_LINE_BYTES) {
configurationError('record exceeds its line bound');
}
}
if (pending.length !== 0) configurationError('file is not newline framed');
} finally {
pending.fill(0);
}
}
function candidate(
value: unknown,
expectedOrdinal: number,
): Readonly<LocalReconciliationSecretConfigDecisionRequirement> {
const record = exact(
value,
[
'candidateDigest',
'candidateOrdinal',
'candidatePlanDigest',
'candidateType',
'kind',
'proposedSecretName',
'requirement',
'schemaVersion',
'sourceRowCount',
'sourceSetDigest',
'target',
],
'candidate',
);
const { candidatePlanDigest, ...payload } = record;
const target = record.target as Record<string, unknown> | undefined;
if (
record.schemaVersion !== 1 ||
record.kind !== CANDIDATE_KIND ||
record.candidateOrdinal !== expectedOrdinal ||
(record.candidateType !== 'active_binding' &&
record.candidateType !== 'disabled_preservation') ||
typeof record.candidateDigest !== 'string' ||
!DIGEST_PATTERN.test(record.candidateDigest) ||
!Number.isSafeInteger(record.sourceRowCount) ||
(record.sourceRowCount as number) < 1 ||
typeof record.sourceSetDigest !== 'string' ||
!DIGEST_PATTERN.test(record.sourceSetDigest) ||
typeof record.proposedSecretName !== 'string' ||
record.proposedSecretName.length < 1 ||
![
'review_apply_binding',
'review_preserve_disabled',
'review_skip_conflict',
].includes(record.requirement as string) ||
!target ||
Array.isArray(target) ||
typeof candidatePlanDigest !== 'string' ||
!DIGEST_PATTERN.test(candidatePlanDigest) ||
cutoverDigest(payload) !== candidatePlanDigest
) {
configurationError('candidate binding is invalid');
}
const targetKeys = Object.keys(target).sort().join('\0');
if (
(target.state === 'absent' && targetKeys !== 'state') ||
(target.state === 'occupied' &&
(targetKeys !== ['contentDigest', 'state', 'version'].sort().join('\0') ||
!Number.isSafeInteger(target.version) ||
(target.version as number) < 1 ||
typeof target.contentDigest !== 'string' ||
!DIGEST_PATTERN.test(target.contentDigest))) ||
(target.state !== 'absent' && target.state !== 'occupied') ||
(record.requirement === 'review_skip_conflict' &&
target.state !== 'occupied') ||
(record.requirement !== 'review_skip_conflict' &&
target.state !== 'absent') ||
(record.requirement === 'review_apply_binding' &&
record.candidateType !== 'active_binding') ||
(record.requirement === 'review_preserve_disabled' &&
record.candidateType !== 'disabled_preservation')
) {
configurationError('candidate target requirement is invalid');
}
return Object.freeze({
candidateOrdinal: record.candidateOrdinal as number,
candidateType: record.candidateType as
| 'active_binding'
| 'disabled_preservation',
candidateDigest: record.candidateDigest,
sourceSetDigest: record.sourceSetDigest,
requirement:
record.requirement as LocalReconciliationSecretConfigCandidateRequirement,
});
}
function header(
value: unknown,
receipt: Readonly<LocalReconciliationSecretConfigPlanReceipt>,
): Readonly<LocalReconciliationSecretConfigPlanHeader> {
const record = exact(
value,
[
'applicationId',
'applicationPlanDigest',
'bundleDigest',
'bundleFingerprintDigest',
'headerDigest',
'kind',
'preparedAtMs',
'preparedHeadDigest',
'profile',
'projectId',
'reviewAuthorizationDigest',
'reviewDecisionFileDigest',
'reviewDecisionSetDigest',
'reviewDigest',
'schemaVersion',
'secretConfigId',
'tableDisposition',
'unadaptedLegacyConfigCount',
],
'header',
);
const { headerDigest, ...payload } = record;
if (
record.schemaVersion !== 1 ||
record.kind !== HEADER_KIND ||
record.secretConfigId !== receipt.secretConfigId ||
record.applicationId !== receipt.applicationId ||
record.applicationPlanDigest !== receipt.applicationPlanDigest ||
record.preparedHeadDigest !== receipt.preparedHeadDigest ||
record.preparedAtMs !== receipt.preparedAtMs ||
(record.profile !== 'edge' && record.profile !== 'standalone') ||
typeof record.projectId !== 'string' ||
record.projectId.length < 1 ||
(record.tableDisposition !== 'absent' &&
record.tableDisposition !== 'manual_external') ||
!Number.isSafeInteger(record.unadaptedLegacyConfigCount) ||
(record.unadaptedLegacyConfigCount as number) < 0 ||
![
record.applicationPlanDigest,
record.bundleDigest,
record.bundleFingerprintDigest,
record.reviewAuthorizationDigest,
record.reviewDecisionFileDigest,
record.reviewDecisionSetDigest,
record.reviewDigest,
record.preparedHeadDigest,
headerDigest,
].every(
(selected) =>
typeof selected === 'string' && DIGEST_PATTERN.test(selected),
) ||
cutoverDigest(payload) !== headerDigest
) {
configurationError('header binding is invalid');
}
return record as unknown as Readonly<LocalReconciliationSecretConfigPlanHeader>;
}
export function readLocalReconciliationSecretConfigPlanHeader(
filePath: string,
receipt: Readonly<LocalReconciliationSecretConfigPlanReceipt>,
uid: number,
): Readonly<LocalReconciliationSecretConfigPlanHeader> {
let descriptor: number | undefined;
const bytes = Buffer.allocUnsafe(MAX_LINE_BYTES + 1);
try {
const before = fs.lstatSync(filePath, { bigint: true });
if (
!before.isFile() ||
before.isSymbolicLink() ||
Number(before.uid) !== uid ||
(Number(before.mode) & 0o777) !== 0o400 ||
before.nlink !== 1n ||
before.size !== BigInt(receipt.planFileBytes)
) {
configurationError('plan file identity is invalid');
}
descriptor = fs.openSync(
filePath,
fs.constants.O_RDONLY | (fs.constants.O_NOFOLLOW ?? 0),
);
const opened = fs.fstatSync(descriptor, { bigint: true });
if (
opened.dev !== before.dev ||
opened.ino !== before.ino ||
opened.size !== before.size ||
opened.mtimeNs !== before.mtimeNs ||
opened.ctimeNs !== before.ctimeNs
) {
configurationError('plan file changed while opening');
}
const bytesRead = fs.readSync(
descriptor,
bytes,
0,
Math.min(bytes.length, Number(opened.size)),
0,
);
const newline = bytes.subarray(0, bytesRead).indexOf(0x0a);
if (newline < 2 || newline > MAX_LINE_BYTES) {
configurationError('header is missing or exceeds its line bound');
}
const selected = header(
parse(bytes.subarray(0, newline), 'header'),
receipt,
);
const after = fs.fstatSync(descriptor, { bigint: true });
const linked = fs.lstatSync(filePath, { bigint: true });
if (
after.dev !== opened.dev ||
after.ino !== opened.ino ||
after.size !== opened.size ||
after.mtimeNs !== opened.mtimeNs ||
after.ctimeNs !== opened.ctimeNs ||
linked.dev !== opened.dev ||
linked.ino !== opened.ino ||
linked.size !== opened.size ||
linked.mtimeNs !== opened.mtimeNs ||
linked.ctimeNs !== opened.ctimeNs
) {
configurationError('plan file changed while reading header');
}
return selected;
} catch (error) {
if (error instanceof LocalDeploymentConfigurationError) throw error;
return configurationError('plan header cannot be read', error);
} finally {
bytes.fill(0);
if (descriptor !== undefined) fs.closeSync(descriptor);
}
}
export function createLocalReconciliationSecretConfigDecisionRequirementFactory(
filePath: string,
receipt: Readonly<LocalReconciliationSecretConfigPlanReceipt>,
uid: number,
): () => Iterable<LocalReconciliationSecretConfigDecisionRequirement> {
return () =>
(function* (): Iterable<LocalReconciliationSecretConfigDecisionRequirement> {
let descriptor: number | undefined;
try {
const before = fs.lstatSync(filePath, { bigint: true });
if (
!before.isFile() ||
before.isSymbolicLink() ||
Number(before.uid) !== uid ||
(Number(before.mode) & 0o777) !== 0o400 ||
before.nlink !== 1n ||
before.size !== BigInt(receipt.planFileBytes)
) {
configurationError('plan file identity is invalid');
}
descriptor = fs.openSync(
filePath,
fs.constants.O_RDONLY | (fs.constants.O_NOFOLLOW ?? 0),
);
const opened = fs.fstatSync(descriptor, { bigint: true });
if (
opened.dev !== before.dev ||
opened.ino !== before.ino ||
opened.size !== before.size ||
opened.mtimeNs !== before.mtimeNs ||
opened.ctimeNs !== before.ctimeNs
) {
configurationError('plan file changed while opening');
}
const fileHash = createHash('sha256');
const candidateHash = createHash('sha256').update(
'qinglong3.local-reconciliation-secret-config-candidate-set.v1\0',
);
let headerSeen = false;
let footerSeen = false;
let candidateCount = 0;
for (const selected of lines(descriptor, Number(opened.size))) {
try {
fileHash.update(selected.framed);
const value = parse(selected.value, 'record');
const kind = (value as { readonly kind?: unknown })?.kind;
if (!headerSeen) {
header(value, receipt);
headerSeen = true;
continue;
}
if (footerSeen) configurationError('record follows footer');
if (kind === ROW_KIND) continue;
if (kind === CANDIDATE_KIND) {
const requirement = candidate(value, candidateCount + 1);
candidateHash.update(selected.framed);
candidateCount += 1;
yield requirement;
continue;
}
const footer = exact(
value,
[
'activeGroupCount',
'activeRowCount',
'adoptedLegacyTaskCount',
'adoptedLegacyTriggerCount',
'adoptionProvenanceTaskCount',
'adoptionProvenanceTriggerCount',
'automationAdoptionRecordCount',
'automationAdoptionProvenanceState',
'automationAdoptionSetDigest',
'bindingReadyCount',
'candidateSetDigest',
'disabledRowCount',
'eligibleBindingCount',
'eligiblePreservationCount',
'kind',
'legacyInventoryDigest',
'manualGroupCount',
'manualRowCount',
'outcome',
'preservationReadyCount',
'rowCount',
'rowSetDigest',
'schemaVersion',
'secretConfigId',
'secretConfigPlanDigest',
'tableState',
'targetConflictCount',
'unadaptedLegacyConfigCount',
],
'footer',
);
if (
footer.schemaVersion !== 1 ||
footer.kind !== FOOTER_KIND ||
footer.secretConfigId !== receipt.secretConfigId ||
footer.secretConfigPlanDigest !==
receipt.secretConfigPlanDigest ||
footer.candidateSetDigest !== candidateHash.digest('hex') ||
footer.candidateSetDigest !== receipt.candidateSetDigest ||
footer.eligibleBindingCount !== receipt.eligibleBindingCount ||
footer.eligiblePreservationCount !==
receipt.eligiblePreservationCount ||
footer.targetConflictCount !== receipt.targetConflictCount ||
footer.automationAdoptionRecordCount !==
receipt.automationAdoptionRecordCount ||
footer.adoptedLegacyTaskCount !==
receipt.adoptedLegacyTaskCount ||
footer.adoptedLegacyTriggerCount !==
receipt.adoptedLegacyTriggerCount ||
footer.adoptionProvenanceTaskCount !==
receipt.adoptionProvenanceTaskCount ||
footer.adoptionProvenanceTriggerCount !==
receipt.adoptionProvenanceTriggerCount ||
footer.automationAdoptionProvenanceState !==
receipt.automationAdoptionProvenanceState ||
footer.outcome !== receipt.outcome ||
candidateCount !==
receipt.eligibleBindingCount +
receipt.eligiblePreservationCount +
receipt.targetConflictCount
) {
configurationError('footer binding is invalid');
}
footerSeen = true;
} finally {
selected.value.fill(0);
selected.framed.fill(0);
}
}
const after = fs.fstatSync(descriptor, { bigint: true });
if (
!headerSeen ||
!footerSeen ||
fileHash.digest('hex') !== receipt.planFileDigest ||
after.dev !== opened.dev ||
after.ino !== opened.ino ||
after.size !== opened.size ||
after.mtimeNs !== opened.mtimeNs ||
after.ctimeNs !== opened.ctimeNs
) {
configurationError('plan file content drifted');
}
} catch (error) {
if (error instanceof LocalDeploymentConfigurationError) throw error;
configurationError('plan file cannot be read', error);
} finally {
if (descriptor !== undefined) fs.closeSync(descriptor);
}
})();
}