Files
qinglong/scripts/ql3-release-deployment-readiness-contract.cjs
T

760 lines
24 KiB
JavaScript

#!/usr/bin/env node
'use strict';
const crypto = require('node:crypto');
const fs = require('node:fs');
const path = require('node:path');
const {
SCHEMA: CATALOG_CONSUMPTION_SCHEMA,
auditCeremonyBundle,
} = require('./ql3-release-catalog-consumption-ceremony.cjs');
const SCHEMA = 'qinglong/release-deployment-readiness-receipt@v1';
const MAX_REPORT_BYTES = 1024 * 1024;
const DIGEST_PATTERN = /^sha256:[a-f0-9]{64}$/u;
const REVISION_PATTERN = /^[a-f0-9]{40}$/u;
const OWNER_PATTERN = /^[a-z0-9](?:[a-z0-9-]{0,37}[a-z0-9])?$/u;
const REPOSITORY_PATTERN =
/^[a-z0-9](?:[a-z0-9-]{0,37}[a-z0-9])?\/[a-z0-9._-]{1,100}$/u;
const LOCAL_REPORT_KEYS = Object.freeze([
'schemaVersion',
'profile',
'generation',
'exactRepoDigest',
'composeMerge',
'rolloutActive',
'durableReceipt',
'sqliteWriteContract',
'sqliteBackup',
'sqliteWriteObservation',
'sqliteRestorePrepared',
'sqliteRestoreCommitted',
'sqliteRestoreRolloutRecovered',
'sqliteRestoreReplayUnchanged',
'sqliteEvidenceCollected',
'sqliteCollectedRolloutReplayUnchanged',
'gracefulCleanup',
'releaseAuthority',
'compatible',
]);
const LOCAL_AUTHORITY_KEYS = Object.freeze([
'mode',
'sourceRevision',
'sourceRef',
'scope',
'releaseSetDigest',
'catalogManifestDigest',
'catalogConsumptionDigest',
'selectionDigest',
]);
const CLUSTER_REPORT_KEYS = Object.freeze([
'schemaVersion',
'schema',
'kubernetes',
'deployment',
'releaseAuthority',
'preflightDigest',
'receiptDigest',
'receiptAuditCompatible',
'retirement',
'serverSideDryRun',
'serverSideApply',
'convergenceRead',
'deploymentHeadCas',
'resourceInventoryClosed',
'crossResourceAtomicity',
'cleanupComplete',
]);
const CLUSTER_AUTHORITY_KEYS = Object.freeze([
'mode',
'version',
'sourceRevision',
'sourceRef',
'scope',
'releaseSetDigest',
'catalogManifestDigest',
'catalogConsumptionDigest',
'immutableReference',
]);
const VERIFICATION = Object.freeze({
finalizerCatalogConsumption: 'independently_verified',
deploymentEvidence: 'scope_exact_catalog_bound_live_reports',
reportEncoding: 'bounded_canonical_json',
reportBytesBound: true,
syntheticFixturesAccepted: false,
jobResultOnlyAuthority: false,
tagMutation: false,
});
class QingLong3ReleaseDeploymentReadinessError extends Error {
constructor(message) {
super(`QingLong 3 release deployment readiness failed: ${message}`);
this.name = 'QingLong3ReleaseDeploymentReadinessError';
}
}
function fail(message) {
throw new QingLong3ReleaseDeploymentReadinessError(message);
}
function sha256(value) {
return `sha256:${crypto.createHash('sha256').update(value).digest('hex')}`;
}
function canonicalJson(value) {
return `${JSON.stringify(value)}\n`;
}
function exactKeys(value, expected) {
return (
value !== null &&
typeof value === 'object' &&
!Array.isArray(value) &&
JSON.stringify(Object.keys(value)) === JSON.stringify(expected)
);
}
function releaseFromIdentity(identity) {
return Object.freeze({
version: identity.version,
sourceRevision: identity.sourceRevision,
sourceRef: identity.sourceRef,
scope: identity.releaseScope,
});
}
function validateIdentity(identity) {
if (
identity === null ||
typeof identity !== 'object' ||
typeof identity.version !== 'string' ||
identity.version.length < 1 ||
identity.version.length > 64 ||
!REVISION_PATTERN.test(identity.sourceRevision || '') ||
identity.sourceRef !== `refs/tags/v${identity.version}` ||
!['local', 'cluster', 'all'].includes(identity.releaseScope) ||
!OWNER_PATTERN.test(identity.repositoryOwner || '') ||
!REPOSITORY_PATTERN.test(identity.sourceRepository || '') ||
!identity.sourceRepository.startsWith(`${identity.repositoryOwner}/`)
) {
fail('release identity is invalid');
}
return releaseFromIdentity(identity);
}
function requiredFamilies(scope) {
return scope === 'local'
? ['local']
: scope === 'cluster'
? ['cluster']
: ['local', 'cluster'];
}
function validateConsumption(consumption, identity, label) {
const expectedImmutable = `ghcr.io/${identity.repositoryOwner}/qinglong3-release-catalog@${consumption?.catalogManifestDigest}`;
if (
consumption === null ||
typeof consumption !== 'object' ||
consumption.compatible !== true ||
consumption.releaseScope !== identity.releaseScope ||
consumption.sourceRepository !== identity.sourceRepository ||
consumption.workflowIdentity !==
`https://github.com/${identity.sourceRepository}/.github/workflows/ql3-image-release.yml@${identity.sourceRef}` ||
!DIGEST_PATTERN.test(consumption.releaseSetDigest || '') ||
!DIGEST_PATTERN.test(consumption.catalogManifestDigest || '') ||
consumption.immutableReference !== expectedImmutable ||
consumption.discoveryTagAuthority !== 'none' ||
consumption.externalToolResultsReplayed !== false ||
consumption.deploymentMutation !== false ||
!DIGEST_PATTERN.test(consumption.contentDigest || '') ||
consumption.releaseSet?.release?.version !== identity.version ||
consumption.releaseSet?.release?.sourceRevision !==
identity.sourceRevision ||
consumption.releaseSet?.release?.sourceRef !== identity.sourceRef ||
consumption.releaseSet?.release?.scope !== identity.releaseScope ||
consumption.releaseSet?.releaseSetDigest !== consumption.releaseSetDigest
) {
fail(`${label} catalog consumption is invalid`);
}
return consumption;
}
function sameCatalog(authority, consumption, identity, keys) {
return (
exactKeys(authority, keys) &&
authority.mode === 'verified_release_catalog' &&
authority.sourceRevision === identity.sourceRevision &&
authority.sourceRef === identity.sourceRef &&
authority.scope === identity.releaseScope &&
authority.releaseSetDigest === consumption.releaseSetDigest &&
authority.catalogManifestDigest === consumption.catalogManifestDigest &&
authority.catalogConsumptionDigest === consumption.contentDigest
);
}
function validateLocalReport(entry, profile, consumption, identity) {
const report = entry?.value;
if (
!DIGEST_PATTERN.test(entry?.digest || '') ||
!exactKeys(report, LOCAL_REPORT_KEYS) ||
report.schemaVersion !== 1 ||
report.profile !== profile ||
!Number.isSafeInteger(report.generation) ||
report.generation < 1 ||
report.exactRepoDigest !== true ||
report.composeMerge !== true ||
report.rolloutActive !== true ||
report.durableReceipt !== true ||
!Number.isSafeInteger(report.sqliteWriteContract) ||
report.sqliteWriteContract < 1 ||
report.sqliteWriteContract > 65_535 ||
report.sqliteBackup !== true ||
report.sqliteRestorePrepared !== true ||
report.sqliteRestoreCommitted !== true ||
report.sqliteRestoreRolloutRecovered !== true ||
report.sqliteRestoreReplayUnchanged !== true ||
report.sqliteEvidenceCollected !== true ||
report.sqliteCollectedRolloutReplayUnchanged !== true ||
report.gracefulCleanup !== true ||
report.compatible !== true ||
!sameCatalog(
report.releaseAuthority,
consumption,
identity,
LOCAL_AUTHORITY_KEYS,
) ||
!DIGEST_PATTERN.test(report.releaseAuthority.selectionDigest || '')
) {
fail(`${profile} deployment evidence is invalid`);
}
return report;
}
function validateClusterReport(entry, consumption, identity) {
const report = entry?.value;
if (
!DIGEST_PATTERN.test(entry?.digest || '') ||
!exactKeys(report, CLUSTER_REPORT_KEYS) ||
report.schemaVersion !== 1 ||
report.schema !== 'qinglong/kubernetes-deployment-live-evidence@v1' ||
report.kubernetes === null ||
typeof report.kubernetes !== 'object' ||
typeof report.kubernetes.serverVersion !== 'string' ||
!/^linux\/(?:amd64|arm64)$/u.test(report.kubernetes.architecture || '') ||
report.kubernetes.nodeCount !== 3 ||
typeof report.kubernetes.clusterUid !== 'string' ||
report.deployment === null ||
typeof report.deployment !== 'object' ||
report.deployment.replicas !== 0 ||
report.deployment.immutableImages !== true ||
report.deployment.headPhase !== 'committed' ||
report.deployment.headGeneration !== 2 ||
!DIGEST_PATTERN.test(report.deployment.deploymentDigest || '') ||
!sameCatalog(
report.releaseAuthority,
consumption,
identity,
CLUSTER_AUTHORITY_KEYS,
) ||
report.releaseAuthority.version !== identity.version ||
report.releaseAuthority.immutableReference !==
consumption.immutableReference ||
!DIGEST_PATTERN.test(report.preflightDigest || '') ||
!DIGEST_PATTERN.test(report.receiptDigest || '') ||
report.receiptAuditCompatible !== true ||
report.retirement === null ||
typeof report.retirement !== 'object' ||
report.retirement.receiptAuditCompatible !== true ||
report.retirement.targetAbsent !== true ||
report.retirement.uidResourceVersionDeletePreconditions !== true ||
report.retirement.deploymentHeadCas !== true ||
report.retirement.unixSocketProxy !== true ||
report.serverSideDryRun !== true ||
report.serverSideApply !== true ||
report.convergenceRead !== true ||
report.deploymentHeadCas !== true ||
report.resourceInventoryClosed !== true ||
report.crossResourceAtomicity !== false ||
report.cleanupComplete !== true
) {
fail('cluster deployment evidence is invalid');
}
return report;
}
function validateEvidenceCatalog(consumption, finalizer) {
if (
consumption.releaseSetDigest !== finalizer.releaseSetDigest ||
consumption.catalogManifestDigest !== finalizer.catalogManifestDigest ||
consumption.immutableReference !== finalizer.immutableReference
) {
fail('deployment evidence is detached from the finalizer catalog');
}
}
function createDeploymentReadinessReceipt(input) {
const identity = input?.identity;
const release = validateIdentity(identity);
const finalizer = validateConsumption(
input.finalizerConsumption,
identity,
'finalizer',
);
const evidence = [];
if (identity.releaseScope !== 'cluster') {
const localConsumption = validateConsumption(
input.local?.consumption,
identity,
'local deployment',
);
validateEvidenceCatalog(localConsumption, finalizer);
const edge = validateLocalReport(
input.local?.edge,
'edge',
localConsumption,
identity,
);
const standalone = validateLocalReport(
input.local?.standalone,
'standalone',
localConsumption,
identity,
);
if (
edge.releaseAuthority.selectionDigest !==
standalone.releaseAuthority.selectionDigest
) {
fail('local profiles used different release selections');
}
if (edge.sqliteWriteContract !== standalone.sqliteWriteContract) {
fail('local profiles used different SQLite write contracts');
}
evidence.push({
family: 'local',
catalogConsumptionDigest: localConsumption.contentDigest,
selectionDigest: edge.releaseAuthority.selectionDigest,
reports: [
{
profile: 'edge',
digest: input.local.edge.digest,
generation: edge.generation,
},
{
profile: 'standalone',
digest: input.local.standalone.digest,
generation: standalone.generation,
},
],
verification: {
exactRepoDigest: true,
rolloutActive: true,
sqliteBackupRestore: true,
gracefulCleanup: true,
},
});
} else if (input.local !== undefined) {
fail('local deployment evidence is forbidden for cluster scope');
}
if (identity.releaseScope !== 'local') {
const clusterConsumption = validateConsumption(
input.cluster?.consumption,
identity,
'cluster deployment',
);
validateEvidenceCatalog(clusterConsumption, finalizer);
const cluster = validateClusterReport(
input.cluster?.report,
clusterConsumption,
identity,
);
evidence.push({
family: 'cluster',
catalogConsumptionDigest: clusterConsumption.contentDigest,
reportDigest: input.cluster.report.digest,
kubernetes: {
serverVersion: cluster.kubernetes.serverVersion,
architecture: cluster.kubernetes.architecture,
nodeCount: cluster.kubernetes.nodeCount,
},
verification: {
immutableImages: true,
serverSideDryRun: true,
serverSideApply: true,
receiptAudited: true,
retirementAudited: true,
cleanupComplete: true,
},
});
} else if (input.cluster !== undefined) {
fail('cluster deployment evidence is forbidden for local scope');
}
const unsigned = {
schemaVersion: 1,
schema: SCHEMA,
release,
sourceRepository: identity.sourceRepository,
catalog: {
schema: CATALOG_CONSUMPTION_SCHEMA,
immutableReference: finalizer.immutableReference,
manifestDigest: finalizer.catalogManifestDigest,
releaseSetDigest: finalizer.releaseSetDigest,
finalizerConsumptionDigest: finalizer.contentDigest,
},
requiredDeploymentFamilies: requiredFamilies(identity.releaseScope),
evidence,
verification: { ...VERIFICATION },
};
return Object.freeze({
...unsigned,
receiptDigest: sha256(JSON.stringify(unsigned)),
});
}
function validateDeploymentReadinessReceipt(receipt, binding = {}) {
if (
!exactKeys(receipt, [
'schemaVersion',
'schema',
'release',
'sourceRepository',
'catalog',
'requiredDeploymentFamilies',
'evidence',
'verification',
'receiptDigest',
]) ||
receipt.schemaVersion !== 1 ||
receipt.schema !== SCHEMA ||
!exactKeys(receipt.release, [
'version',
'sourceRevision',
'sourceRef',
'scope',
]) ||
receipt.release.sourceRef !== `refs/tags/v${receipt.release.version}` ||
!REVISION_PATTERN.test(receipt.release.sourceRevision || '') ||
!['local', 'cluster', 'all'].includes(receipt.release.scope) ||
!REPOSITORY_PATTERN.test(receipt.sourceRepository || '') ||
!exactKeys(receipt.catalog, [
'schema',
'immutableReference',
'manifestDigest',
'releaseSetDigest',
'finalizerConsumptionDigest',
]) ||
receipt.catalog.schema !== CATALOG_CONSUMPTION_SCHEMA ||
!DIGEST_PATTERN.test(receipt.catalog.manifestDigest || '') ||
!DIGEST_PATTERN.test(receipt.catalog.releaseSetDigest || '') ||
!DIGEST_PATTERN.test(receipt.catalog.finalizerConsumptionDigest || '') ||
!receipt.catalog.immutableReference.endsWith(
`@${receipt.catalog.manifestDigest}`,
) ||
JSON.stringify(receipt.requiredDeploymentFamilies) !==
JSON.stringify(requiredFamilies(receipt.release.scope)) ||
!Array.isArray(receipt.evidence) ||
JSON.stringify(receipt.evidence.map((entry) => entry.family)) !==
JSON.stringify(receipt.requiredDeploymentFamilies) ||
JSON.stringify(receipt.verification) !== JSON.stringify(VERIFICATION) ||
!DIGEST_PATTERN.test(receipt.receiptDigest || '')
) {
fail('deployment readiness receipt shape is invalid');
}
const { receiptDigest, ...unsigned } = receipt;
if (receiptDigest !== sha256(JSON.stringify(unsigned))) {
fail('deployment readiness receipt digest is invalid');
}
if (
(binding.release !== undefined &&
JSON.stringify(receipt.release) !== JSON.stringify(binding.release)) ||
(binding.sourceRepository !== undefined &&
receipt.sourceRepository !== binding.sourceRepository) ||
(binding.releaseSetDigest !== undefined &&
receipt.catalog.releaseSetDigest !== binding.releaseSetDigest) ||
(binding.catalogManifestDigest !== undefined &&
receipt.catalog.manifestDigest !== binding.catalogManifestDigest) ||
(binding.immutableReference !== undefined &&
receipt.catalog.immutableReference !== binding.immutableReference)
) {
fail('deployment readiness receipt authority is detached');
}
for (const entry of receipt.evidence) {
if (
!DIGEST_PATTERN.test(entry.catalogConsumptionDigest || '') ||
(entry.family === 'local' &&
(!exactKeys(entry, [
'family',
'catalogConsumptionDigest',
'selectionDigest',
'reports',
'verification',
]) ||
!DIGEST_PATTERN.test(entry.selectionDigest || '') ||
!Array.isArray(entry.reports) ||
entry.reports.length !== 2 ||
JSON.stringify(entry.reports.map((report) => report.profile)) !==
JSON.stringify(['edge', 'standalone']) ||
entry.reports.some(
(report) =>
!exactKeys(report, ['profile', 'digest', 'generation']) ||
!DIGEST_PATTERN.test(report.digest || '') ||
!Number.isSafeInteger(report.generation) ||
report.generation < 1,
) ||
JSON.stringify(entry.verification) !==
JSON.stringify({
exactRepoDigest: true,
rolloutActive: true,
sqliteBackupRestore: true,
gracefulCleanup: true,
}))) ||
(entry.family === 'cluster' &&
(!exactKeys(entry, [
'family',
'catalogConsumptionDigest',
'reportDigest',
'kubernetes',
'verification',
]) ||
!DIGEST_PATTERN.test(entry.reportDigest || '') ||
!exactKeys(entry.kubernetes, [
'serverVersion',
'architecture',
'nodeCount',
]) ||
typeof entry.kubernetes.serverVersion !== 'string' ||
entry.kubernetes?.nodeCount !== 3 ||
!/^linux\/(?:amd64|arm64)$/u.test(
entry.kubernetes?.architecture || '',
) ||
JSON.stringify(entry.verification) !==
JSON.stringify({
immutableImages: true,
serverSideDryRun: true,
serverSideApply: true,
receiptAudited: true,
retirementAudited: true,
cleanupComplete: true,
}))) ||
!['local', 'cluster'].includes(entry.family)
) {
fail('deployment readiness evidence summary is invalid');
}
}
return receipt;
}
function auditDeploymentReadinessReceipt(actual, input) {
const expected = createDeploymentReadinessReceipt(input);
validateDeploymentReadinessReceipt(actual);
if (JSON.stringify(actual) !== JSON.stringify(expected)) {
fail('deployment readiness receipt differs from the exact live evidence');
}
return Object.freeze({
compatible: true,
releaseScope: actual.release.scope,
releaseSetDigest: actual.catalog.releaseSetDigest,
catalogManifestDigest: actual.catalog.manifestDigest,
requiredDeploymentFamilies: [...actual.requiredDeploymentFamilies],
reportCount: actual.evidence.reduce(
(count, entry) => count + (entry.family === 'local' ? 2 : 1),
0,
),
tagMutation: false,
});
}
function canonicalAbsolute(value, label) {
if (
typeof value !== 'string' ||
!path.isAbsolute(value) ||
path.resolve(value) !== value
) {
fail(`${label} path must be canonical and absolute`);
}
return value;
}
function readCanonicalReport(filePath, label) {
const resolved = canonicalAbsolute(filePath, label);
const stat = fs.lstatSync(resolved);
if (
!stat.isFile() ||
stat.isSymbolicLink() ||
stat.size < 2 ||
stat.size > MAX_REPORT_BYTES ||
fs.realpathSync(resolved) !== resolved ||
fs.realpathSync(path.dirname(resolved)) !== path.dirname(resolved) ||
(typeof process.getuid === 'function' && stat.uid !== process.getuid()) ||
(stat.mode & 0o077) !== 0
) {
fail(`${label} must be one owner-private bounded regular file`);
}
const contents = fs.readFileSync(resolved);
let value;
try {
value = JSON.parse(contents.toString('utf8'));
} catch {
fail(`${label} must contain valid JSON`);
}
if (!contents.equals(Buffer.from(canonicalJson(value), 'utf8'))) {
fail(`${label} must use canonical JSON encoding`);
}
return Object.freeze({ value, digest: sha256(contents) });
}
function writeNoReplace(filePath, value) {
const resolved = canonicalAbsolute(filePath, 'output');
const parent = fs.lstatSync(path.dirname(resolved));
if (
!parent.isDirectory() ||
parent.isSymbolicLink() ||
fs.realpathSync(path.dirname(resolved)) !== path.dirname(resolved)
) {
fail('output parent must be one canonical directory');
}
const descriptor = fs.openSync(resolved, 'wx', 0o600);
try {
fs.writeFileSync(descriptor, canonicalJson(value));
fs.fsyncSync(descriptor);
} finally {
fs.closeSync(descriptor);
}
}
function parseArguments(argv) {
const values = {};
for (const argument of argv) {
const match = /^--([a-z-]+)=(.+)$/u.exec(argument);
if (!match || Object.hasOwn(values, match[1]))
fail('arguments are invalid');
values[match[1]] = match[2];
}
if (!['create', 'audit'].includes(values.mode)) fail('arguments are invalid');
const common = [
'finalizer-consumption-bundle',
'mode',
'release-scope',
'repository-owner',
'source-ref',
'source-repository',
'source-revision',
'version',
];
const local =
values['release-scope'] === 'cluster'
? []
: ['edge-report', 'local-consumption-bundle', 'standalone-report'];
const cluster =
values['release-scope'] === 'local'
? []
: ['cluster-consumption-bundle', 'cluster-report'];
const terminal = values.mode === 'create' ? ['output'] : ['receipt'];
const expected = [...common, ...local, ...cluster, ...terminal].sort();
if (JSON.stringify(Object.keys(values).sort()) !== JSON.stringify(expected)) {
fail('arguments are invalid');
}
return Object.freeze({
mode: values.mode,
version: values.version,
sourceRevision: values['source-revision'],
sourceRef: values['source-ref'],
releaseScope: values['release-scope'],
repositoryOwner: values['repository-owner'],
sourceRepository: values['source-repository'],
finalizerConsumptionBundle: values['finalizer-consumption-bundle'],
localConsumptionBundle: values['local-consumption-bundle'],
edgeReport: values['edge-report'],
standaloneReport: values['standalone-report'],
clusterConsumptionBundle: values['cluster-consumption-bundle'],
clusterReport: values['cluster-report'],
output: values.output,
receipt: values.receipt,
});
}
function evidenceInput(options) {
const identity = {
version: options.version,
sourceRevision: options.sourceRevision,
sourceRef: options.sourceRef,
releaseScope: options.releaseScope,
repositoryOwner: options.repositoryOwner,
sourceRepository: options.sourceRepository,
};
const auditOptions = {
...identity,
outputDirectory: options.finalizerConsumptionBundle,
};
const input = {
identity,
finalizerConsumption: auditCeremonyBundle(auditOptions),
};
if (options.releaseScope !== 'cluster') {
input.local = {
consumption: auditCeremonyBundle({
...identity,
outputDirectory: options.localConsumptionBundle,
}),
edge: readCanonicalReport(options.edgeReport, 'edge report'),
standalone: readCanonicalReport(
options.standaloneReport,
'standalone report',
),
};
}
if (options.releaseScope !== 'local') {
input.cluster = {
consumption: auditCeremonyBundle({
...identity,
outputDirectory: options.clusterConsumptionBundle,
}),
report: readCanonicalReport(options.clusterReport, 'cluster report'),
};
}
return input;
}
function runCli(argv, output = process.stdout) {
const options = parseArguments(argv);
const input = evidenceInput(options);
if (options.mode === 'create') {
const receipt = createDeploymentReadinessReceipt(input);
writeNoReplace(options.output, receipt);
output.write(canonicalJson(receipt));
return receipt;
}
const actual = readCanonicalReport(
options.receipt,
'deployment readiness receipt',
).value;
const audit = auditDeploymentReadinessReceipt(actual, input);
output.write(canonicalJson(audit));
return audit;
}
if (require.main === module) {
try {
runCli(process.argv.slice(2));
} catch (error) {
process.stderr.write(
`${
error instanceof Error
? error.message
: 'release deployment readiness failed'
}\n`,
);
process.exitCode = 1;
}
}
module.exports = Object.freeze({
SCHEMA,
QingLong3ReleaseDeploymentReadinessError,
auditDeploymentReadinessReceipt,
createDeploymentReadinessReceipt,
parseArguments,
runCli,
validateDeploymentReadinessReceipt,
});