'use strict'; const crypto = require('node:crypto'); const fs = require('node:fs'); const os = require('node:os'); const path = require('node:path'); const { spawn, spawnSync } = require('node:child_process'); const yaml = require('js-yaml'); const semver = require('semver'); const COMMAND_SCHEMA = 'qinglong/kubernetes-deployment-command@v2'; const LOCK_SCHEMA = 'qinglong/kubernetes-deployment-lock@v2'; const CATALOG_SCHEMA = 'qinglong/release-catalog-consumption-ceremony@v1'; const PREFLIGHT_SCHEMA = 'qinglong/kubernetes-deployment-preflight@v2'; const RECEIPT_SCHEMA = 'qinglong/kubernetes-deployment-receipt@v2'; const RETIREMENT_PREFLIGHT_SCHEMA = 'qinglong/kubernetes-resource-retirement-preflight@v1'; const RETIREMENT_RECEIPT_SCHEMA = 'qinglong/kubernetes-resource-retirement-receipt@v1'; const HEAD_SCHEMA = 'qinglong/kubernetes-deployment-head@v2'; const HEAD_NAME = 'qinglong3-deployment-head'; const HEAD_DATA_KEY = 'head.json'; const FIELD_MANAGER = 'qinglong3-catalog-lock'; const MAX_COMMAND_BYTES = 64 * 1024; const MAX_MANIFEST_BYTES = 8 * 1024 * 1024; const MAX_REPORT_BYTES = 1024 * 1024; const MAX_KUBECONFIG_BYTES = 1024 * 1024; const MAX_EXECUTABLE_BYTES = 256 * 1024 * 1024; const MAX_PROCESS_OUTPUT_BYTES = 4 * 1024 * 1024; const MAX_RESOURCE_COUNT = 2048; const MAX_RETIREMENT_COUNT = 64; const MAX_STRUCTURE_DEPTH = 64; const PROCESS_TIMEOUT_MS = 60_000; const DIGEST_PATTERN = /^sha256:[a-f0-9]{64}$/u; const UUID_PATTERN = /^[a-f0-9]{8}-[a-f0-9]{4}-[1-8][a-f0-9]{3}-[89ab][a-f0-9]{3}-[a-f0-9]{12}$/u; const VERSION_PATTERN = /^3\.(?:0|[1-9]\d*)\.(?:0|[1-9]\d*)(?:-[0-9A-Za-z-]+(?:\.[0-9A-Za-z-]+)*)?(?:\+[0-9A-Za-z-]+(?:\.[0-9A-Za-z-]+)*)?$/u; const CONTEXT_PATTERN = /^[A-Za-z0-9](?:[A-Za-z0-9._-]{0,252})$/u; const REPOSITORY_PATTERN = /^[a-z0-9](?:[a-z0-9-]{0,37}[a-z0-9])?\/[a-z0-9._-]{1,100}$/u; const ROLE_ORDER = Object.freeze(['control', 'control-ai', 'admin', 'worker']); const TRANSITION_KINDS = Object.freeze(['install', 'upgrade', 'rollback']); const HEAD_TRANSITION_KINDS = Object.freeze([...TRANSITION_KINDS, 'retire']); const FORBIDDEN_RETIREMENT_IDENTITIES = Object.freeze([ 'v1\u0000PersistentVolumeClaim', 'v1\u0000Secret', 'v1\u0000ServiceAccount', ]); const IMAGE_NAMES = Object.freeze({ control: 'qinglong3-cluster-control', 'control-ai': 'qinglong3-cluster-control-ai', admin: 'qinglong3-cluster-admin', worker: 'qinglong3-worker', }); const ANNOTATION_KEYS = Object.freeze({ releaseSet: 'qinglong.io/release-set-digest', catalogManifest: 'qinglong.io/release-catalog-manifest-digest', catalogReport: 'qinglong.io/release-catalog-report-digest', sourceRevision: 'qinglong.io/release-source-revision', version: 'qinglong.io/release-version', }); class QingLong3KubernetesDeploymentCeremonyError extends Error { constructor(message) { super(`QingLong 3 Kubernetes deployment ceremony failed: ${message}`); this.name = 'QingLong3KubernetesDeploymentCeremonyError'; this.code = 'QL3_KUBERNETES_DEPLOYMENT_CEREMONY_FAILED'; } } function fail(message) { throw new QingLong3KubernetesDeploymentCeremonyError(message); } function sha256(value) { return `sha256:${crypto.createHash('sha256').update(value).digest('hex')}`; } function canonicalJson(value) { return `${JSON.stringify(value)}\n`; } function isPlainObject(value) { if (value === null || typeof value !== 'object' || Array.isArray(value)) { return false; } const prototype = Object.getPrototypeOf(value); return prototype === Object.prototype || prototype === null; } function exactKeys(value, expected) { return ( isPlainObject(value) && JSON.stringify(Object.keys(value).sort()) === JSON.stringify([...expected].sort()) ); } function resolveCanonicalAbsolute(input, label) { if (typeof input !== 'string' || !path.isAbsolute(input)) { fail(`${label} path must be absolute`); } const resolved = path.resolve(input); if (resolved !== input) fail(`${label} path must be normalized`); return resolved; } function validatePrivateParent(filePath, label, uid = process.getuid()) { const parent = path.dirname(filePath); let status; try { status = fs.lstatSync(parent); } catch { fail(`${label} parent is unavailable`); } if ( !status.isDirectory() || status.isSymbolicLink() || status.uid !== uid || (status.mode & 0o777) !== 0o700 || fs.realpathSync(parent) !== parent ) { fail(`${label} parent must be one current-owner canonical 0700 directory`); } return parent; } function descriptorIdentity(status) { return Object.freeze({ dev: status.dev, ino: status.ino, size: status.size, mtimeMs: status.mtimeMs, ctimeMs: status.ctimeMs, }); } function sameIdentity(left, right) { return ( left.dev === right.dev && left.ino === right.ino && left.size === right.size && left.mtimeMs === right.mtimeMs && left.ctimeMs === right.ctimeMs ); } function readStableFile( input, label, maximumBytes, { privateFile = true, executable = false } = {}, ) { const target = resolveCanonicalAbsolute(input, label); if (privateFile) validatePrivateParent(target, label); let lexical; try { lexical = fs.lstatSync(target); } catch { fail(`${label} is unavailable`); } const uid = process.getuid(); const allowedOwner = privateFile ? lexical.uid === uid : lexical.uid === uid || lexical.uid === 0; const validMode = privateFile ? (lexical.mode & 0o777) === 0o600 : (lexical.mode & 0o022) === 0 && (lexical.mode & 0o111) !== 0; if ( !lexical.isFile() || lexical.isSymbolicLink() || lexical.nlink !== 1 || !allowedOwner || !validMode || lexical.size < 1 || lexical.size > maximumBytes || fs.realpathSync(target) !== target || (executable && (lexical.mode & 0o111) === 0) ) { fail(`${label} file authority is invalid`); } const flags = fs.constants.O_RDONLY | (fs.constants.O_NOFOLLOW ?? 0) | (fs.constants.O_CLOEXEC ?? 0); let descriptor; try { descriptor = fs.openSync(target, flags); } catch { fail(`${label} cannot be opened safely`); } try { const before = fs.fstatSync(descriptor); if ( !before.isFile() || before.dev !== lexical.dev || before.ino !== lexical.ino || before.size !== lexical.size ) { fail(`${label} descriptor identity is invalid`); } const chunks = []; const hash = crypto.createHash('sha256'); let total = 0; while (true) { const chunk = Buffer.allocUnsafe(Math.min(64 * 1024, maximumBytes + 1)); const read = fs.readSync(descriptor, chunk, 0, chunk.length, null); if (read === 0) break; total += read; if (total > maximumBytes) fail(`${label} exceeds its byte limit`); const actual = chunk.subarray(0, read); hash.update(actual); if (!executable) chunks.push(actual); } const after = fs.fstatSync(descriptor); if (!sameIdentity(descriptorIdentity(before), descriptorIdentity(after))) { fail(`${label} changed while it was read`); } const contents = executable ? undefined : Buffer.concat(chunks); return Object.freeze({ path: target, identity: descriptorIdentity(after), bytes: total, digest: `sha256:${hash.digest('hex')}`, ...(contents === undefined ? {} : { contents }), }); } finally { fs.closeSync(descriptor); } } function utf8(file, label) { const contents = file.contents.toString('utf8'); if (!Buffer.from(contents, 'utf8').equals(file.contents)) { fail(`${label} must contain valid UTF-8`); } return contents; } function parseCanonicalJson(contents, label) { let parsed; try { parsed = JSON.parse(contents); } catch { fail(`${label} must contain valid JSON`); } if (canonicalJson(parsed) !== contents) { fail(`${label} must use canonical JSON encoding`); } return parsed; } function readCanonicalJson(input, label, maximumBytes) { const file = readStableFile(input, label, maximumBytes); return Object.freeze({ ...file, value: parseCanonicalJson(utf8(file, label), label), }); } function artifact(value, label) { if ( !exactKeys(value, ['path', 'expectedDigest']) || typeof value.path !== 'string' || !DIGEST_PATTERN.test(value.expectedDigest || '') ) { fail(`${label} authority is invalid`); } return Object.freeze({ path: resolveCanonicalAbsolute(value.path, label), expectedDigest: value.expectedDigest, }); } function verifyExpected(file, expectedDigest, label) { if (file.digest !== expectedDigest) fail(`${label} digest changed`); return file; } function outputTarget(input, label) { const target = resolveCanonicalAbsolute(input, label); validatePrivateParent(target, label); if (fs.existsSync(target)) fail(`${label} must be unused`); return target; } function writeNoReplace(target, value) { fs.writeFileSync(target, canonicalJson(value), { encoding: 'utf8', mode: 0o600, flag: 'wx', }); } function expectedHead(value) { if ( !exactKeys(value, [ 'generation', 'deploymentDigest', 'lockDigest', 'stateDigest', ]) || !Number.isSafeInteger(value.generation) || value.generation < 0 || (value.generation === 0 && (value.deploymentDigest !== null || value.lockDigest !== null || value.stateDigest !== null)) || (value.generation > 0 && (!DIGEST_PATTERN.test(value.deploymentDigest || '') || !DIGEST_PATTERN.test(value.lockDigest || '') || !DIGEST_PATTERN.test(value.stateDigest || ''))) ) { fail('expected deployment head is invalid'); } return Object.freeze({ ...value }); } function transitionRequest(request) { if (!TRANSITION_KINDS.includes(request.transitionKind)) { fail('deployment transition kind is invalid'); } const head = expectedHead(request.expectedHead); if ( (request.transitionKind === 'install' && head.generation !== 0) || (request.transitionKind !== 'install' && head.generation === 0) ) { fail('deployment transition head is inconsistent'); } return Object.freeze({ transitionKind: request.transitionKind, head }); } function retirementTargets(value) { validateResourceInventory(value); if (value.length > MAX_RETIREMENT_COUNT) { fail('deployment retirement target count is invalid'); } for (const target of value) { if ( target.namespace === null || FORBIDDEN_RETIREMENT_IDENTITIES.includes( `${target.apiVersion}\u0000${target.kind}`, ) || (target.apiVersion === 'v1' && target.kind === 'ConfigMap' && target.name === HEAD_NAME) ) { fail('deployment retirement target requires a dedicated ceremony'); } } return Object.freeze(value.map((target) => Object.freeze({ ...target }))); } function retirementRequest(request) { const head = expectedHead(request.expectedHead); if (head.generation === 0) { fail('deployment retirement requires a committed head'); } return Object.freeze({ lockedManifest: artifact(request.lockedManifest, 'locked manifest'), lockReport: artifact(request.lockReport, 'lock report'), kubectl: artifact(request.kubectl, 'kubectl'), curl: artifact(request.curl, 'curl'), kubeconfig: artifact(request.kubeconfig, 'kubeconfig'), context: request.context, expectedClusterUid: request.expectedClusterUid, expectedHead: head, targets: retirementTargets(request.targets), }); } function parseCommand(value) { if ( !exactKeys(value, ['schemaVersion', 'schema', 'operation', 'request']) || value.schemaVersion !== 1 || value.schema !== COMMAND_SCHEMA ) { fail('command shape is invalid'); } const request = value.request; if (value.operation === 'cluster.deployment.preflight') { if ( !exactKeys(request, [ 'preflightId', 'lockedManifest', 'lockReport', 'kubectl', 'kubeconfig', 'context', 'expectedClusterUid', 'transitionKind', 'expectedHead', 'output', ]) || !UUID_PATTERN.test(request.preflightId || '') || !CONTEXT_PATTERN.test(request.context || '') || !UUID_PATTERN.test(request.expectedClusterUid || '') || typeof request.output !== 'string' ) { fail('preflight command is invalid'); } const transition = transitionRequest(request); return Object.freeze({ schemaVersion: 1, schema: COMMAND_SCHEMA, operation: value.operation, request: Object.freeze({ preflightId: request.preflightId, lockedManifest: artifact(request.lockedManifest, 'locked manifest'), lockReport: artifact(request.lockReport, 'lock report'), kubectl: artifact(request.kubectl, 'kubectl'), kubeconfig: artifact(request.kubeconfig, 'kubeconfig'), context: request.context, expectedClusterUid: request.expectedClusterUid, transitionKind: transition.transitionKind, expectedHead: transition.head, output: resolveCanonicalAbsolute(request.output, 'preflight output'), }), }); } if (value.operation === 'cluster.deployment.apply') { if ( !exactKeys(request, [ 'mutationId', 'preflight', 'lockedManifest', 'lockReport', 'kubectl', 'kubeconfig', 'context', 'expectedClusterUid', 'transitionKind', 'expectedHead', 'output', ]) || !UUID_PATTERN.test(request.mutationId || '') || !CONTEXT_PATTERN.test(request.context || '') || !UUID_PATTERN.test(request.expectedClusterUid || '') || typeof request.output !== 'string' ) { fail('apply command is invalid'); } const transition = transitionRequest(request); return Object.freeze({ schemaVersion: 1, schema: COMMAND_SCHEMA, operation: value.operation, request: Object.freeze({ mutationId: request.mutationId, preflight: artifact(request.preflight, 'preflight report'), lockedManifest: artifact(request.lockedManifest, 'locked manifest'), lockReport: artifact(request.lockReport, 'lock report'), kubectl: artifact(request.kubectl, 'kubectl'), kubeconfig: artifact(request.kubeconfig, 'kubeconfig'), context: request.context, expectedClusterUid: request.expectedClusterUid, transitionKind: transition.transitionKind, expectedHead: transition.head, output: resolveCanonicalAbsolute( request.output, 'apply receipt output', ), }), }); } if (value.operation === 'cluster.deployment.retirement.preflight') { if ( !exactKeys(request, [ 'preflightId', 'lockedManifest', 'lockReport', 'kubectl', 'curl', 'kubeconfig', 'context', 'expectedClusterUid', 'expectedHead', 'targets', 'output', ]) || !UUID_PATTERN.test(request.preflightId || '') || !CONTEXT_PATTERN.test(request.context || '') || !UUID_PATTERN.test(request.expectedClusterUid || '') || typeof request.output !== 'string' ) { fail('retirement preflight command is invalid'); } return Object.freeze({ schemaVersion: 1, schema: COMMAND_SCHEMA, operation: value.operation, request: Object.freeze({ preflightId: request.preflightId, ...retirementRequest(request), output: resolveCanonicalAbsolute( request.output, 'retirement preflight output', ), }), }); } if (value.operation === 'cluster.deployment.retirement.apply') { if ( !exactKeys(request, [ 'mutationId', 'preflight', 'lockedManifest', 'lockReport', 'kubectl', 'curl', 'kubeconfig', 'context', 'expectedClusterUid', 'expectedHead', 'targets', 'output', ]) || !UUID_PATTERN.test(request.mutationId || '') || !CONTEXT_PATTERN.test(request.context || '') || !UUID_PATTERN.test(request.expectedClusterUid || '') || typeof request.output !== 'string' ) { fail('retirement apply command is invalid'); } return Object.freeze({ schemaVersion: 1, schema: COMMAND_SCHEMA, operation: value.operation, request: Object.freeze({ mutationId: request.mutationId, preflight: artifact(request.preflight, 'retirement preflight report'), ...retirementRequest(request), output: resolveCanonicalAbsolute( request.output, 'retirement receipt output', ), }), }); } if (value.operation === 'cluster.deployment.retirement.receipt.audit') { if (!exactKeys(request, ['applyCommand', 'receipt'])) { fail('retirement receipt audit command is invalid'); } return Object.freeze({ schemaVersion: 1, schema: COMMAND_SCHEMA, operation: value.operation, request: Object.freeze({ applyCommand: artifact( request.applyCommand, 'retirement apply command', ), receipt: artifact(request.receipt, 'retirement receipt'), }), }); } if (value.operation === 'cluster.deployment.receipt.audit') { if (!exactKeys(request, ['applyCommand', 'receipt'])) { fail('receipt audit command is invalid'); } return Object.freeze({ schemaVersion: 1, schema: COMMAND_SCHEMA, operation: value.operation, request: Object.freeze({ applyCommand: artifact(request.applyCommand, 'apply command'), receipt: artifact(request.receipt, 'deployment receipt'), }), }); } fail('command operation is invalid'); } function readCommand(input) { const commandFile = readCanonicalJson( input, 'deployment command', MAX_COMMAND_BYTES, ); return Object.freeze({ file: commandFile, command: parseCommand(commandFile.value), commandDigest: commandFile.digest, }); } function validateRelease(release) { if ( !exactKeys(release, ['version', 'sourceRevision', 'sourceRef', 'scope']) || !VERSION_PATTERN.test(release.version || '') || !/^[a-f0-9]{40}$/u.test(release.sourceRevision || '') || release.sourceRef !== `refs/tags/v${release.version}` || !['cluster', 'all'].includes(release.scope) ) { fail('deployment lock release identity is invalid'); } } function validateCatalog(catalog, release, releaseSetDigest) { if ( !exactKeys(catalog, [ 'schema', 'sourceRepository', 'workflowIdentity', 'immutableReference', 'manifestDigest', 'consumptionReportDigest', 'releaseSetDigest', 'discoveryTagAuthority', ]) || catalog.schema !== CATALOG_SCHEMA || !REPOSITORY_PATTERN.test(catalog.sourceRepository || '') || catalog.workflowIdentity !== `https://github.com/${catalog.sourceRepository}/.github/workflows/ql3-image-release.yml@${release.sourceRef}` || !DIGEST_PATTERN.test(catalog.manifestDigest || '') || !DIGEST_PATTERN.test(catalog.consumptionReportDigest || '') || catalog.releaseSetDigest !== releaseSetDigest || catalog.discoveryTagAuthority !== 'none' ) { fail('deployment lock catalog authority is invalid'); } const match = new RegExp( `^ghcr\\.io/([a-z0-9](?:[a-z0-9-]{0,37}[a-z0-9])?)/qinglong3-release-catalog@${catalog.manifestDigest}$`, 'u', ).exec(catalog.immutableReference || ''); if (!match) fail('deployment lock catalog reference is invalid'); return match[1]; } function validateLockReport(report) { if ( !exactKeys(report, [ 'schemaVersion', 'schema', 'release', 'releaseSetDigest', 'catalog', 'deploymentFamily', 'requiredImages', 'imageOccurrences', 'manifest', 'verification', 'lockDigest', ]) || report.schemaVersion !== 1 || report.schema !== LOCK_SCHEMA || report.deploymentFamily !== 'cluster' || !DIGEST_PATTERN.test(report.releaseSetDigest || '') || !DIGEST_PATTERN.test(report.lockDigest || '') ) { fail('deployment lock report shape is invalid'); } validateRelease(report.release); const repositoryOwner = validateCatalog( report.catalog, report.release, report.releaseSetDigest, ); if ( !Array.isArray(report.requiredImages) || report.requiredImages.length < 1 || JSON.stringify(report.requiredImages) !== JSON.stringify( ROLE_ORDER.filter((role) => report.requiredImages.includes(role)), ) || !Array.isArray(report.imageOccurrences) || report.imageOccurrences.length !== ROLE_ORDER.length ) { fail('deployment lock role closure is invalid'); } const references = {}; const counts = {}; for (let index = 0; index < ROLE_ORDER.length; index += 1) { const role = ROLE_ORDER[index]; const occurrence = report.imageOccurrences[index]; const repository = IMAGE_NAMES[role]; if ( !exactKeys(occurrence, ['name', 'reference', 'count']) || occurrence.name !== role || !new RegExp( `^ghcr\\.io/${repositoryOwner}/${repository}@sha256:[a-f0-9]{64}$`, 'u', ).test(occurrence.reference || '') || !Number.isSafeInteger(occurrence.count) || occurrence.count < 0 || occurrence.count > MAX_RESOURCE_COUNT * 16 || (report.requiredImages.includes(role) && occurrence.count < 1) ) { fail(`deployment lock image occurrence is invalid: ${role}`); } references[role] = occurrence.reference; counts[role] = occurrence.count; } if ( !exactKeys(report.manifest, [ 'inputDigest', 'outputDigest', 'resources', 'changedResources', 'admissionAuthorityCount', ]) || !DIGEST_PATTERN.test(report.manifest.inputDigest || '') || !DIGEST_PATTERN.test(report.manifest.outputDigest || '') || !Number.isSafeInteger(report.manifest.resources) || report.manifest.resources < 1 || report.manifest.resources > MAX_RESOURCE_COUNT || !Number.isSafeInteger(report.manifest.changedResources) || report.manifest.changedResources < 1 || report.manifest.changedResources > report.manifest.resources || !Number.isSafeInteger(report.manifest.admissionAuthorityCount) || report.manifest.admissionAuthorityCount < 0 || report.manifest.admissionAuthorityCount > report.manifest.resources || !exactKeys(report.verification, [ 'releaseSet', 'sourceRecordsReplayed', 'catalogConsumption', 'externalToolResultsReplayed', 'unknownImageAuthorities', 'mutableQingLongImages', 'networkAccess', 'kubernetesMutation', ]) || JSON.stringify(report.verification) !== JSON.stringify({ releaseSet: 'standalone_structure_identity_and_self_digest', sourceRecordsReplayed: false, catalogConsumption: 'offline_reconstructed', externalToolResultsReplayed: false, unknownImageAuthorities: 0, mutableQingLongImages: 0, networkAccess: false, kubernetesMutation: false, }) ) { fail('deployment lock verification is invalid'); } const { lockDigest, ...unsigned } = report; if (lockDigest !== sha256(JSON.stringify(unsigned))) { fail('deployment lock self digest is invalid'); } return Object.freeze({ report, repositoryOwner, references: Object.freeze(references), counts: Object.freeze(counts), }); } function validateStructure(value, state, depth = 0) { if (depth > MAX_STRUCTURE_DEPTH) fail('locked manifest is too deep'); if (value === null || typeof value !== 'object') return; if (state.seen.has(value)) fail('locked manifest aliases are not allowed'); state.seen.add(value); if (Array.isArray(value)) { for (const entry of value) validateStructure(entry, state, depth + 1); return; } if (!isPlainObject(value)) fail('locked manifest value is invalid'); for (const [key, entry] of Object.entries(value)) { if (!key || /[\u0000-\u001f\u007f]/u.test(key)) { fail('locked manifest key is invalid'); } validateStructure(entry, state, depth + 1); } } function parseManifest(contents) { const resources = []; try { yaml.loadAll( contents, (resource) => { if (resource !== undefined && resource !== null) resources.push(resource); }, { json: false }, ); } catch { fail('locked manifest must be duplicate-free YAML'); } if (resources.length < 1 || resources.length > MAX_RESOURCE_COUNT) { fail('locked manifest resource count is invalid'); } const state = { seen: new WeakSet() }; for (const resource of resources) { if (!isPlainObject(resource)) fail('locked manifest resource is invalid'); validateStructure(resource, state); if ( typeof resource.apiVersion !== 'string' || typeof resource.kind !== 'string' || !isPlainObject(resource.metadata) || typeof resource.metadata.name !== 'string' ) { fail('locked manifest resource identity is invalid'); } } return resources; } function roleFromImage(value) { if (typeof value !== 'string') return null; for (const role of ROLE_ORDER) { if ( new RegExp( `^(?:[A-Za-z0-9][A-Za-z0-9._-]*(?::[0-9]+)?/)*${IMAGE_NAMES[role]}(?::[^@\\s]+|@sha256:[a-f0-9]{64})$`, 'u', ).test(value) ) { return role; } } return null; } function mentionsRoleImageName(value) { return ( typeof value === 'string' && ROLE_ORDER.some((role) => value.includes(IMAGE_NAMES[role])) ); } function podTemplateFor(resource) { if ( ['Deployment', 'StatefulSet', 'DaemonSet', 'ReplicaSet'].includes( resource.kind, ) ) { return resource.spec?.template; } if (resource.kind === 'Job') return resource.spec?.template; if (resource.kind === 'CronJob') { return resource.spec?.jobTemplate?.spec?.template; } return undefined; } function podSpecFor(resource) { if (resource.kind === 'Pod') return resource.spec; return podTemplateFor(resource)?.spec; } function expectedAnnotations(report) { return Object.freeze({ [ANNOTATION_KEYS.releaseSet]: report.releaseSetDigest, [ANNOTATION_KEYS.catalogManifest]: report.catalog.manifestDigest, [ANNOTATION_KEYS.catalogReport]: report.catalog.consumptionReportDigest, [ANNOTATION_KEYS.sourceRevision]: report.release.sourceRevision, [ANNOTATION_KEYS.version]: report.release.version, }); } function assertAnnotations(target, expected, label) { const annotations = target?.metadata?.annotations; if (!isPlainObject(annotations)) fail(`${label} annotations are invalid`); for (const [key, value] of Object.entries(expected)) { if (annotations[key] !== value) fail(`${label} annotations drifted`); } } function inspectContainerList(list, authority, handled, counters) { if (list === undefined) return 0; if (!Array.isArray(list)) fail('locked manifest container list is invalid'); let matched = 0; for (const container of list) { if (!isPlainObject(container)) fail('locked manifest container is invalid'); const role = roleFromImage(container.image); if (role === null) { if (mentionsRoleImageName(container.image)) { fail('locked manifest container image is malformed'); } continue; } if (container.image !== authority.references[role]) { fail(`locked manifest image authority drifted: ${role}`); } handled.add(container); counters[role] += 1; matched += 1; } return matched; } function scanUnhandled(value, authority, handled, parent, parentKey) { if (typeof value === 'string') { const role = roleFromImage(value); if ( role !== null && (value !== authority.references[role] || parentKey !== 'image' || !handled.has(parent)) ) { fail('locked manifest contains an unhandled QingLong image authority'); } if (role === null && mentionsRoleImageName(value)) { fail('locked manifest contains a malformed QingLong image authority'); } return; } if (value === null || typeof value !== 'object') return; if (Array.isArray(value)) { for (let index = 0; index < value.length; index += 1) { scanUnhandled(value[index], authority, handled, value, String(index)); } return; } for (const [key, entry] of Object.entries(value)) { scanUnhandled(entry, authority, handled, value, key); } } function inspectResourceAuthorities(resources, authority, label) { const report = authority.report; const counters = Object.fromEntries(ROLE_ORDER.map((role) => [role, 0])); const handled = new WeakSet(); const annotations = expectedAnnotations(report); let changedResources = 0; let admissionAuthorityCount = 0; for (const resource of resources) { const podSpec = podSpecFor(resource); let matched = 0; if (podSpec !== undefined) { if (!isPlainObject(podSpec)) fail(`${label} Pod spec is invalid`); for (const key of [ 'initContainers', 'containers', 'ephemeralContainers', ]) { matched += inspectContainerList( podSpec[key], authority, handled, counters, ); } } if ( resource.kind === 'ConfigMap' && resource.metadata.name === 'ql3-plugin-package-secret-action-admission' ) { const role = roleFromImage(resource.data?.image); if ( role !== 'admin' || resource.data.image !== authority.references.admin ) { fail('locked manifest admission authority is invalid'); } handled.add(resource.data); counters.admin += 1; matched += 1; admissionAuthorityCount += 1; } if (matched > 0) { if ( typeof resource.metadata.namespace !== 'string' || !CONTEXT_PATTERN.test(resource.metadata.namespace) ) { fail(`${label} resource namespace must be explicit`); } assertAnnotations(resource, annotations, `${label} resource`); const template = podTemplateFor(resource); if (template !== undefined) { assertAnnotations(template, annotations, `${label} Pod template`); } changedResources += 1; } } for (const resource of resources) { scanUnhandled(resource, authority, handled, undefined, undefined); } if ( JSON.stringify(counters) !== JSON.stringify(authority.counts) || changedResources !== report.manifest.changedResources || admissionAuthorityCount !== report.manifest.admissionAuthorityCount ) { fail('locked manifest authority counts changed'); } return Object.freeze({ resourceCount: resources.length, changedResources, admissionAuthorityCount, counts: Object.freeze({ ...counters }), }); } function inspectLockedManifest(contents, authority) { const report = authority.report; if (sha256(Buffer.from(contents, 'utf8')) !== report.manifest.outputDigest) { fail('locked manifest digest changed'); } const resources = parseManifest(contents); if (resources.length !== report.manifest.resources) { fail('locked manifest resource count changed'); } inspectResourceAuthorities(resources, authority, 'locked manifest'); return Object.freeze({ resources: Object.freeze(resources) }); } function resourceIdentity(resource) { const namespace = resource.metadata?.namespace ?? ''; return `${resource.apiVersion}\u0000${resource.kind}\u0000${namespace}\u0000${ resource.metadata?.name ?? '' }`; } function resourceInventory(resources) { const inventory = resources .map((resource) => { const namespace = resource.metadata?.namespace ?? null; const item = { apiVersion: resource.apiVersion, kind: resource.kind, namespace, name: resource.metadata?.name, }; if ( Object.values(item).some( (entry) => entry !== null && (typeof entry !== 'string' || !entry), ) ) { fail('deployment resource inventory is invalid'); } return Object.freeze(item); }) .sort((left, right) => { const leftIdentity = JSON.stringify(left); const rightIdentity = JSON.stringify(right); return leftIdentity < rightIdentity ? -1 : leftIdentity > rightIdentity ? 1 : 0; }); const identities = inventory.map((item) => JSON.stringify(item)); if (new Set(identities).size !== identities.length) { fail('deployment resource inventory is duplicated'); } return Object.freeze(inventory); } function containsExactString(value, expected, depth = 0) { if (depth > MAX_STRUCTURE_DEPTH) fail('locked manifest is too deep'); if (typeof value === 'string') return value === expected; if (value === null || typeof value !== 'object') return false; if (Array.isArray(value)) { return value.some((entry) => containsExactString(entry, expected, depth + 1), ); } return Object.values(value).some((entry) => containsExactString(entry, expected, depth + 1), ); } function deploymentHeadNamespace(resources, authority) { const namespaces = new Set( resources .filter((resource) => containsExactString(resource, authority.references.control), ) .map((resource) => resource.metadata?.namespace) .filter((namespace) => typeof namespace === 'string'), ); if (namespaces.size !== 1) { fail('deployment control namespace authority is invalid'); } return [...namespaces][0]; } function containsQingLongAuthority(value, references) { if (typeof value === 'string') { const role = roleFromImage(value); return role !== null && value === references[role]; } if (value === null || typeof value !== 'object') return false; if (Array.isArray(value)) { return value.some((entry) => containsQingLongAuthority(entry, references)); } return Object.values(value).some((entry) => containsQingLongAuthority(entry, references), ); } function assertDesiredSubset(desired, actual, depth = 0) { if (depth > MAX_STRUCTURE_DEPTH) fail('live Kubernetes object is too deep'); if (desired === null || typeof desired !== 'object') { if (desired !== actual) fail('live Kubernetes object drifted'); return; } if (Array.isArray(desired)) { if (!Array.isArray(actual) || actual.length !== desired.length) { fail('live Kubernetes array drifted'); } for (let index = 0; index < desired.length; index += 1) { assertDesiredSubset(desired[index], actual[index], depth + 1); } return; } if (!isPlainObject(actual)) fail('live Kubernetes object is invalid'); for (const [key, value] of Object.entries(desired)) { if (!Object.hasOwn(actual, key)) fail('live Kubernetes field is missing'); assertDesiredSubset(value, actual[key], depth + 1); } } function inspectConvergenceOutput(contents, inputs) { let value; try { value = JSON.parse(contents); } catch { fail('Kubernetes convergence response is invalid'); } const items = value?.kind === 'List' ? value.items : undefined; if ( !Array.isArray(items) || items.length !== inputs.authority.report.manifest.resources || items.some((item) => !isPlainObject(item)) ) { fail('Kubernetes convergence resource count changed'); } const liveByIdentity = new Map(); for (const item of items) { const identity = resourceIdentity(item); if (liveByIdentity.has(identity)) { fail('Kubernetes convergence identity is duplicated'); } liveByIdentity.set(identity, item); } const desiredResources = inspectLockedManifest( inputs.manifestContents, inputs.authority, ).resources; for (const desired of desiredResources) { const live = liveByIdentity.get(resourceIdentity(desired)); if ( live === undefined || typeof live.metadata?.uid !== 'string' || typeof live.metadata?.resourceVersion !== 'string' ) { fail('Kubernetes convergence identity is unavailable'); } if ( containsQingLongAuthority(desired, inputs.authority.references) && (!Array.isArray(live.metadata.managedFields) || !live.metadata.managedFields.some( (entry) => entry?.manager === FIELD_MANAGER && entry?.operation === 'Apply', )) ) { fail('Kubernetes convergence field authority is unavailable'); } assertDesiredSubset(desired, live); } inspectResourceAuthorities(items, inputs.authority, 'live Kubernetes'); } function validateKubeconfig(contents, context) { let config; try { config = yaml.load(contents, { json: false }); } catch { fail('kubeconfig is invalid'); } if ( !isPlainObject(config) || !Array.isArray(config.contexts) || !config.contexts.some( (entry) => isPlainObject(entry) && entry.name === context && isPlainObject(entry.context) && typeof entry.context.cluster === 'string', ) || !Array.isArray(config.users) ) { fail('kubeconfig context is unavailable'); } for (const entry of config.users) { const user = entry?.user; if ( !isPlainObject(entry) || !isPlainObject(user) || Object.hasOwn(user, 'exec') || Object.hasOwn(user, 'auth-provider') ) { fail('kubeconfig executable authentication is forbidden'); } } } function inspectInputs(request) { const manifest = verifyExpected( readStableFile( request.lockedManifest.path, 'locked manifest', MAX_MANIFEST_BYTES, ), request.lockedManifest.expectedDigest, 'locked manifest', ); const reportFile = readCanonicalJson( request.lockReport.path, 'lock report', MAX_REPORT_BYTES, ); const authority = validateLockReport(reportFile.value); if (authority.report.lockDigest !== request.lockReport.expectedDigest) { fail('lock report digest changed'); } const manifestContents = utf8(manifest, 'locked manifest'); const inspection = inspectLockedManifest(manifestContents, authority); if (manifest.digest !== authority.report.manifest.outputDigest) { fail('locked manifest and report digest differ'); } if ( JSON.stringify(authority.report.requiredImages) !== JSON.stringify(ROLE_ORDER) ) { fail('cluster deployment requires the complete role surface'); } const headNamespace = deploymentHeadNamespace( inspection.resources, authority, ); if ( inspection.resources.some( (resource) => resource.apiVersion === 'v1' && resource.kind === 'ConfigMap' && resource.metadata?.namespace === headNamespace && resource.metadata?.name === HEAD_NAME, ) ) { fail('locked manifest cannot own the deployment head'); } const executable = verifyExpected( readStableFile(request.kubectl.path, 'kubectl', MAX_EXECUTABLE_BYTES, { privateFile: false, executable: true, }), request.kubectl.expectedDigest, 'kubectl', ); const kubeconfig = verifyExpected( readStableFile(request.kubeconfig.path, 'kubeconfig', MAX_KUBECONFIG_BYTES), request.kubeconfig.expectedDigest, 'kubeconfig', ); validateKubeconfig(utf8(kubeconfig, 'kubeconfig'), request.context); return Object.freeze({ manifest, manifestContents, reportFile, authority, resources: inspection.resources, resourceInventory: resourceInventory(inspection.resources), headNamespace, executable, kubeconfig, }); } function inspectRetirementInputs(request) { const inputs = inspectInputs(request); const curl = verifyExpected( readStableFile(request.curl.path, 'curl', MAX_EXECUTABLE_BYTES, { privateFile: false, executable: true, }), request.curl.expectedDigest, 'curl', ); return Object.freeze({ ...inputs, curl }); } function defaultRunProcess(executable, args, input) { const privateHome = fs.mkdtempSync( path.join(os.tmpdir(), 'ql3-kubectl-home-'), ); fs.chmodSync(privateHome, 0o700); try { return spawnSync(executable, args, { input, encoding: 'utf8', timeout: PROCESS_TIMEOUT_MS, maxBuffer: MAX_PROCESS_OUTPUT_BYTES, shell: false, windowsHide: true, env: Object.freeze({ HOME: privateHome, XDG_CACHE_HOME: privateHome, TMPDIR: privateHome, LANG: 'C', LC_ALL: 'C', NO_COLOR: '1', }), }); } finally { fs.rmSync(privateHome, { recursive: true, force: true }); } } function waitForProxySocket(socketPath, child) { const deadline = Date.now() + 5_000; const sleeper = new Int32Array(new SharedArrayBuffer(4)); while (Date.now() < deadline) { try { if (fs.lstatSync(socketPath).isSocket()) return; } catch {} if (child.exitCode !== null) break; Atomics.wait(sleeper, 0, 0, 25); } fail('private Kubernetes API proxy did not become ready'); } function curlUnixRequest(inputs, socketPath, method, requestPath, body) { const args = [ '--silent', '--show-error', '--unix-socket', socketPath, '--request', method, '--header', 'Accept: application/json', '--max-time', '60', '--output', '-', '--write-out', '\n%{http_code}', ]; let input = ''; if (body !== null) { args.push( '--header', 'Content-Type: application/json', '--data-binary', '@-', ); input = JSON.stringify(body); } args.push(`http://localhost${requestPath}`); const result = spawnSync(inputs.curl.path, args, { input, encoding: 'utf8', timeout: PROCESS_TIMEOUT_MS, maxBuffer: MAX_PROCESS_OUTPUT_BYTES, shell: false, windowsHide: true, }); if ( result.error || result.status !== 0 || typeof result.stdout !== 'string' ) { fail('private Kubernetes API request could not complete safely'); } const boundary = result.stdout.lastIndexOf('\n'); const statusText = result.stdout.slice(boundary + 1); const responseBody = result.stdout.slice(0, boundary); if ( boundary < 0 || !/^\d{3}$/u.test(statusText) || Buffer.byteLength(responseBody, 'utf8') > MAX_PROCESS_OUTPUT_BYTES ) { fail('private Kubernetes API response is invalid'); } return Object.freeze({ status: Number(statusText), body: responseBody }); } function defaultRetirementApi(inputs, request, callback) { const privateHome = fs.mkdtempSync( path.join(os.tmpdir(), 'ql3-kubectl-retirement-'), ); fs.chmodSync(privateHome, 0o700); const socketPath = path.join(privateHome, 'api.sock'); const args = [ `--kubeconfig=${inputs.kubeconfig.path}`, `--context=${request.context}`, '--request-timeout=60s', 'proxy', `--unix-socket=${socketPath}`, '--api-prefix=/', '--www=', '--accept-paths=^/(api|apis)/.*$', '--reject-methods=^(POST|PUT|PATCH|CONNECT|OPTIONS|TRACE)$', '--keepalive=0s', ]; let child; try { child = spawn(inputs.executable.path, args, { stdio: 'ignore', shell: false, windowsHide: true, env: Object.freeze({ HOME: privateHome, XDG_CACHE_HOME: privateHome, TMPDIR: privateHome, LANG: 'C', LC_ALL: 'C', NO_COLOR: '1', }), }); waitForProxySocket(socketPath, child); return callback( Object.freeze({ request(method, requestPath, body = null) { return curlUnixRequest(inputs, socketPath, method, requestPath, body); }, }), ); } finally { if (child?.pid) child.kill('SIGTERM'); try { fs.rmSync(privateHome, { recursive: true, force: true }); } catch {} } } function withRetirementApi(inputs, request, dependencies, callback) { if (dependencies.retirementApi !== undefined) { if (typeof dependencies.retirementApi?.request !== 'function') { fail('retirement API dependency is invalid'); } return callback(dependencies.retirementApi); } return defaultRetirementApi(inputs, request, callback); } function recordedApiRequest(records, api, name, method, requestPath, body) { const response = api.request(method, requestPath, body); if ( !Number.isInteger(response?.status) || response.status < 100 || response.status > 599 || typeof response.body !== 'string' || Buffer.byteLength(response.body, 'utf8') > MAX_PROCESS_OUTPUT_BYTES ) { fail('retirement API dependency response is invalid'); } records.push( Object.freeze({ name, method, requestPathDigest: sha256(Buffer.from(requestPath, 'utf8')), requestDigest: sha256( Buffer.from(body === null ? '' : JSON.stringify(body), 'utf8'), ), status: response.status, responseDigest: sha256(Buffer.from(response.body, 'utf8')), responseBytes: Buffer.byteLength(response.body, 'utf8'), }), ); return response; } function aggregateRequestStep(name, records) { const stdoutBytes = records.reduce( (total, record) => total + record.responseBytes, 0, ); if (records.length < 1 || stdoutBytes > MAX_PROCESS_OUTPUT_BYTES) { fail('retirement API transcript exceeds its bound'); } return Object.freeze({ name, status: 0, argvDigest: sha256( Buffer.from( JSON.stringify( records.map((record) => ({ name: record.name, method: record.method, requestPathDigest: record.requestPathDigest, requestDigest: record.requestDigest, status: record.status, })), ), 'utf8', ), ), stdoutDigest: sha256( Buffer.from( JSON.stringify(records.map((record) => record.responseDigest)), 'utf8', ), ), stdoutBytes, stderrDigest: sha256(Buffer.alloc(0)), stderrBytes: 0, }); } function runStep( inputs, request, name, argumentsAfterTarget, runner, input = inputs.manifestContents, ) { const args = [ `--kubeconfig=${inputs.kubeconfig.path}`, `--context=${request.context}`, '--request-timeout=60s', ...argumentsAfterTarget, ]; const result = runner(inputs.executable.path, args, input); const stdout = typeof result.stdout === 'string' ? result.stdout : ''; const stderr = typeof result.stderr === 'string' ? result.stderr : ''; if ( result.error || !Number.isInteger(result.status) || Buffer.byteLength(stdout, 'utf8') > MAX_PROCESS_OUTPUT_BYTES || Buffer.byteLength(stderr, 'utf8') > MAX_PROCESS_OUTPUT_BYTES ) { fail(`kubectl ${name} could not complete safely`); } return Object.freeze({ name, status: result.status, argvDigest: sha256(Buffer.from(JSON.stringify(args), 'utf8')), stdoutDigest: sha256(Buffer.from(stdout, 'utf8')), stdoutBytes: Buffer.byteLength(stdout, 'utf8'), stderrDigest: sha256(Buffer.from(stderr, 'utf8')), stderrBytes: Buffer.byteLength(stderr, 'utf8'), stdout, }); } function identityStep(inputs, request, runner, name) { const step = runStep( inputs, request, name, ['get', 'namespace', 'kube-system', '-o=jsonpath={.metadata.uid}'], runner, ); if (step.status !== 0 || step.stdout.trim() !== request.expectedClusterUid) { fail('Kubernetes cluster identity changed'); } const { stdout, ...record } = step; return Object.freeze(record); } function dryRunStep(inputs, request, runner, name = 'server_side_dry_run') { const step = runStep( inputs, request, name, [ 'apply', '--server-side', '--dry-run=server', `--field-manager=${FIELD_MANAGER}`, '--validate=strict', '-f=-', '-o=name', ], runner, ); if (step.status !== 0) fail('Kubernetes server-side dry-run was rejected'); const { stdout, ...record } = step; return Object.freeze(record); } function convergenceReadStep(inputs, request, runner) { const step = runStep( inputs, request, 'server_side_convergence_read', ['get', '-f=-', '-o=json', '--show-managed-fields=true'], runner, ); if (step.status !== 0) fail('Kubernetes convergence read failed'); inspectConvergenceOutput(step.stdout, inputs); const { stdout, ...record } = step; return Object.freeze(record); } function revalidateStableInputs(inputs, request) { for (const [label, previous, maximumBytes, options] of [ ['locked manifest', inputs.manifest, MAX_MANIFEST_BYTES, {}], ['lock report', inputs.reportFile, MAX_REPORT_BYTES, {}], [ 'kubectl', inputs.executable, MAX_EXECUTABLE_BYTES, { privateFile: false, executable: true }, ], ['kubeconfig', inputs.kubeconfig, MAX_KUBECONFIG_BYTES, {}], ]) { const current = readStableFile(previous.path, label, maximumBytes, options); if ( current.digest !== previous.digest || !sameIdentity(current.identity, previous.identity) ) { fail(`${label} changed during the ceremony`); } } if ( inputs.manifest.digest !== request.lockedManifest.expectedDigest || inputs.authority.report.lockDigest !== request.lockReport.expectedDigest || inputs.executable.digest !== request.kubectl.expectedDigest || inputs.kubeconfig.digest !== request.kubeconfig.expectedDigest ) { fail('deployment input authority changed'); } } function revalidateRetirementInputs(inputs, request) { revalidateStableInputs(inputs, request); const current = readStableFile( inputs.curl.path, 'curl', MAX_EXECUTABLE_BYTES, { privateFile: false, executable: true }, ); if ( current.digest !== inputs.curl.digest || !sameIdentity(current.identity, inputs.curl.identity) || current.digest !== request.curl.expectedDigest ) { fail('curl changed during the retirement ceremony'); } } function lockSummary(inputs) { const report = inputs.authority.report; return Object.freeze({ lockDigest: report.lockDigest, manifestDigest: inputs.manifest.digest, lockReportDigest: inputs.reportFile.digest, releaseSetDigest: report.releaseSetDigest, catalogManifestDigest: report.catalog.manifestDigest, catalogReportDigest: report.catalog.consumptionReportDigest, catalogImmutableReference: report.catalog.immutableReference, release: Object.freeze({ ...report.release }), requiredImages: Object.freeze([...report.requiredImages]), }); } function targetSummary(inputs, request) { return Object.freeze({ context: request.context, clusterUid: request.expectedClusterUid, kubeconfigDigest: inputs.kubeconfig.digest, fieldManager: FIELD_MANAGER, }); } function toolSummary(inputs) { return Object.freeze({ name: 'kubectl', executableDigest: inputs.executable.digest, }); } function retirementToolSummary(inputs) { return Object.freeze({ kubectl: toolSummary(inputs), curl: Object.freeze({ name: 'curl', executableDigest: inputs.curl.digest, }), transport: 'owner_private_unix_socket_proxy', }); } function validateRetirementToolSummary(value) { if ( !exactKeys(value, ['kubectl', 'curl', 'transport']) || value.transport !== 'owner_private_unix_socket_proxy' || !exactKeys(value.curl, ['name', 'executableDigest']) || value.curl.name !== 'curl' || !DIGEST_PATTERN.test(value.curl.executableDigest || '') ) { fail('retirement tool summary is invalid'); } validateToolSummary(value.kubectl); } function inventoryKey(value) { return JSON.stringify({ apiVersion: value.apiVersion, kind: value.kind, namespace: value.namespace ?? value.metadata?.namespace ?? null, name: value.name ?? value.metadata?.name, }); } function retirementPlan(inputs, current, targets) { if (current.state === null || current.state.phase !== 'committed') { fail('deployment retirement requires a committed head'); } const currentDeployment = current.state.deployment; if ( JSON.stringify(currentDeployment.lock) !== JSON.stringify(lockSummary(inputs)) ) { fail('deployment retirement lock is not the active lock'); } const manifestByIdentity = new Map( inputs.resources.map((resource) => [inventoryKey(resource), resource]), ); const activeKeys = new Set( currentDeployment.resources.map((resource) => inventoryKey(resource)), ); if ( activeKeys.size !== currentDeployment.resources.length || [...activeKeys].some((identity) => !manifestByIdentity.has(identity)) ) { fail('active deployment inventory is not backed by its lock'); } const retiredKeys = new Set(targets.map((target) => inventoryKey(target))); if ([...retiredKeys].some((identity) => !activeKeys.has(identity))) { fail('deployment retirement target is not active'); } const survivors = Object.freeze( currentDeployment.resources.filter( (resource) => !retiredKeys.has(inventoryKey(resource)), ), ); if (survivors.length < 1) { fail('deployment retirement cannot empty the active inventory'); } const survivorResources = survivors.map((item) => manifestByIdentity.get(inventoryKey(item)), ); for (const role of ROLE_ORDER) { if ( !survivorResources.some((resource) => containsExactString(resource, inputs.authority.references[role]), ) ) { fail(`deployment retirement would remove the last ${role} authority`); } } return Object.freeze({ active: currentDeployment.resources, survivors, resources: Object.freeze( targets.map((target) => manifestByIdentity.get(inventoryKey(target))), ), }); } function apiVersionBase(apiVersion) { if (apiVersion === 'v1') return '/api/v1'; const segments = apiVersion.split('/'); if ( segments.length !== 2 || segments.some( (segment) => !segment || /[\u0000-\u001f\u007f]/u.test(segment), ) ) { fail('retirement API version is invalid'); } return `/apis/${encodeURIComponent(segments[0])}/${encodeURIComponent( segments[1], )}`; } function apiResourcePath(api, item, cache, records) { const base = apiVersionBase(item.apiVersion); let resources = cache.get(base); if (resources === undefined) { const response = recordedApiRequest( records, api, 'api_discovery', 'GET', base, null, ); if (response.status !== 200) fail('Kubernetes API discovery failed'); let discovery; try { discovery = JSON.parse(response.body); } catch { fail('Kubernetes API discovery response is invalid'); } if (!Array.isArray(discovery?.resources)) { fail('Kubernetes API discovery response is invalid'); } resources = discovery.resources; cache.set(base, resources); } const candidates = resources.filter( (resource) => resource?.kind === item.kind && typeof resource?.name === 'string' && !resource.name.includes('/') && Array.isArray(resource.verbs) && resource.verbs.includes('get') && resource.verbs.includes('delete') && resource.namespaced === (item.namespace !== null), ); if ( candidates.length !== 1 || !/^[a-z0-9](?:[a-z0-9.-]{0,252})$/u.test(candidates[0].name) ) { fail('retirement API resource mapping is ambiguous'); } const collection = item.namespace === null ? `${base}/${candidates[0].name}` : `${base}/namespaces/${encodeURIComponent(item.namespace)}/${ candidates[0].name }`; return `${collection}/${encodeURIComponent(item.name)}`; } function parseRetirementObject(response, desired, expected = null) { let live; try { live = JSON.parse(response.body); } catch { fail('retirement target response is invalid'); } if ( response.status !== 200 || inventoryKey(live) !== inventoryKey(desired) || !UUID_PATTERN.test(live.metadata?.uid || '') || typeof live.metadata?.resourceVersion !== 'string' || !live.metadata.resourceVersion || live.metadata.deletionTimestamp !== undefined || !Array.isArray(live.metadata.managedFields) || !live.metadata.managedFields.some( (entry) => entry?.manager === FIELD_MANAGER && entry?.operation === 'Apply', ) ) { fail('retirement target authority is invalid'); } assertDesiredSubset(desired, live); const observation = Object.freeze({ apiVersion: desired.apiVersion, kind: desired.kind, namespace: desired.metadata.namespace, name: desired.metadata.name, uid: live.metadata.uid, resourceVersion: live.metadata.resourceVersion, }); if ( expected !== null && JSON.stringify(observation) !== JSON.stringify(expected) ) { fail('retirement target changed after preflight'); } return observation; } function readRetirementTargets(api, resources, records) { const cache = new Map(); return Object.freeze( resources.map((desired) => { const item = { apiVersion: desired.apiVersion, kind: desired.kind, namespace: desired.metadata?.namespace ?? null, name: desired.metadata?.name, }; const requestPath = apiResourcePath(api, item, cache, records); const response = recordedApiRequest( records, api, 'retirement_target_get', 'GET', requestPath, null, ); return Object.freeze({ observation: parseRetirementObject(response, desired), requestPath, }); }), ); } function deleteOptions(observation, dryRun) { return Object.freeze({ apiVersion: 'v1', kind: 'DeleteOptions', ...(dryRun ? { dryRun: Object.freeze(['All']) } : {}), propagationPolicy: 'Background', preconditions: Object.freeze({ uid: observation.uid, resourceVersion: observation.resourceVersion, }), }); } function validateResourceInventory(value) { if ( !Array.isArray(value) || value.length < 1 || value.length > MAX_RESOURCE_COUNT ) { fail('deployment resource inventory is invalid'); } const serialized = []; for (const item of value) { if ( !exactKeys(item, ['apiVersion', 'kind', 'namespace', 'name']) || typeof item.apiVersion !== 'string' || !item.apiVersion || typeof item.kind !== 'string' || !item.kind || (item.namespace !== null && (typeof item.namespace !== 'string' || !item.namespace)) || typeof item.name !== 'string' || !item.name ) { fail('deployment resource inventory is invalid'); } serialized.push(JSON.stringify(item)); } if ( new Set(serialized).size !== serialized.length || JSON.stringify([...serialized].sort()) !== JSON.stringify(serialized) ) { fail('deployment resource inventory ordering is invalid'); } } function validateRetirementObservations(value) { if ( !Array.isArray(value) || value.length < 1 || value.length > MAX_RETIREMENT_COUNT ) { fail('deployment retirement observations are invalid'); } const identities = []; for (const item of value) { if ( !exactKeys(item, [ 'apiVersion', 'kind', 'namespace', 'name', 'uid', 'resourceVersion', ]) || item.namespace === null || !UUID_PATTERN.test(item.uid || '') || typeof item.resourceVersion !== 'string' || !item.resourceVersion ) { fail('deployment retirement observation is invalid'); } retirementTargets([ { apiVersion: item.apiVersion, kind: item.kind, namespace: item.namespace, name: item.name, }, ]); identities.push(inventoryKey(item)); } if ( new Set(identities).size !== identities.length || JSON.stringify([...identities].sort()) !== JSON.stringify(identities) ) { fail('deployment retirement observation ordering is invalid'); } } function deploymentPlan(inputs) { return Object.freeze({ lock: lockSummary(inputs), resources: inputs.resourceInventory, }); } function validateDeploymentPlan(value) { if (!exactKeys(value, ['lock', 'resources'])) { fail('deployment plan is invalid'); } validateLockSummary(value.lock); validateResourceInventory(value.resources); } function validateDeploymentSummary(value) { if (!exactKeys(value, ['deploymentDigest', 'lock', 'resources'])) { fail('deployment summary is invalid'); } if (!DIGEST_PATTERN.test(value.deploymentDigest || '')) { fail('deployment summary digest is invalid'); } validateLockSummary(value.lock); validateResourceInventory(value.resources); } function headSnapshot(namespace, state) { if (state === null) { return Object.freeze({ namespace, name: HEAD_NAME, generation: 0, deploymentDigest: null, lockDigest: null, stateDigest: null, }); } if (state.phase !== 'committed') { fail('deployment head is not committed'); } return Object.freeze({ namespace, name: HEAD_NAME, generation: state.generation, deploymentDigest: state.deployment.deploymentDigest, lockDigest: state.deployment.lock.lockDigest, stateDigest: state.stateDigest, }); } function validateHeadSnapshot(value) { if ( !exactKeys(value, [ 'namespace', 'name', 'generation', 'deploymentDigest', 'lockDigest', 'stateDigest', ]) || !CONTEXT_PATTERN.test(value.namespace || '') || value.name !== HEAD_NAME ) { fail('deployment head snapshot is invalid'); } expectedHead({ generation: value.generation, deploymentDigest: value.deploymentDigest, lockDigest: value.lockDigest, stateDigest: value.stateDigest, }); } function expectedSnapshot(request, namespace) { return Object.freeze({ namespace, name: HEAD_NAME, ...request.expectedHead, }); } function validateTransition(current, request, planned) { const expected = expectedSnapshot(request, current.namespace); if (JSON.stringify(expected) !== JSON.stringify(current.snapshot)) { fail('deployment head compare-and-swap authority changed'); } if (request.transitionKind === 'install') { if (current.state !== null) fail('deployment install requires an empty head'); return; } if (current.state === null || current.state.phase !== 'committed') { fail('deployment transition requires a committed head'); } const active = current.state.deployment; const activeVersion = active.lock.release.version; const plannedVersion = planned.lock.release.version; if (request.transitionKind === 'upgrade') { if (!semver.gt(plannedVersion, activeVersion)) { fail('deployment upgrade version must increase'); } const plannedResources = new Set( planned.resources.map((item) => JSON.stringify(item)), ); if ( active.resources.some( (item) => !plannedResources.has(JSON.stringify(item)), ) ) { fail('deployment upgrade cannot retire resources implicitly'); } return; } const previous = current.state.previous; const versionRollback = semver.lt(plannedVersion, activeVersion); const retirementRestore = current.state.transition.kind === 'retire' && plannedVersion === activeVersion && previous !== null && previous.lock.release.version === activeVersion; if ( previous === null || (!versionRollback && !retirementRestore) || JSON.stringify(planned.lock) !== JSON.stringify(previous.lock) || JSON.stringify(planned.resources) !== JSON.stringify(previous.resources) ) { fail('deployment rollback must restore the exact previous resource set'); } } function validateHeadTransition(value) { const retirement = value?.kind === 'retire'; if ( !exactKeys(value, [ 'kind', 'mutationId', 'commandDigest', 'preflightDigest', 'expectedHead', ...(retirement ? ['retirements', 'curlExecutableDigest'] : []), ]) || !HEAD_TRANSITION_KINDS.includes(value.kind) || !UUID_PATTERN.test(value.mutationId || '') || !DIGEST_PATTERN.test(value.commandDigest || '') || !DIGEST_PATTERN.test(value.preflightDigest || '') || (retirement && !DIGEST_PATTERN.test(value.curlExecutableDigest || '')) ) { fail('deployment head transition is invalid'); } validateHeadSnapshot(value.expectedHead); if (retirement) validateRetirementObservations(value.retirements); } function deploymentEvidence(state) { return Object.freeze({ schemaVersion: 1, schema: 'qinglong/kubernetes-deployment-evidence@v1', generation: state.generation, transition: state.transition, previous: state.previous, planned: state.planned, target: state.target, tool: state.tool, steps: state.steps, }); } function validateHeadState(value) { if ( !exactKeys(value, [ 'schemaVersion', 'schema', 'phase', 'generation', 'transition', 'previous', 'planned', 'target', 'tool', 'steps', 'deployment', 'stateDigest', ]) || value.schemaVersion !== 1 || value.schema !== HEAD_SCHEMA || !['applying', 'committed'].includes(value.phase) || !Number.isSafeInteger(value.generation) || value.generation < 1 || !DIGEST_PATTERN.test(value.stateDigest || '') || !Array.isArray(value.steps) ) { fail('deployment head state is invalid'); } validateHeadTransition(value.transition); if (value.previous !== null) validateDeploymentSummary(value.previous); validateDeploymentPlan(value.planned); validateTargetSummary(value.target); validateToolSummary(value.tool); for (const step of value.steps) validateStepRecord(step); if (value.steps.some((step) => step.status !== 0)) { fail('deployment head contains a failed step'); } if (value.phase === 'applying') { if (value.steps.length !== 0 || value.deployment !== null) { fail('applying deployment head is invalid'); } } else { const expectedStepNames = value.transition.kind === 'retire' ? [ 'cluster_identity_before', 'resource_precondition_delete', 'retirement_absence_read', 'cluster_identity_after', ] : [ 'cluster_identity_before', 'server_side_dry_run', 'server_side_apply', 'server_side_convergence_read', 'cluster_identity_after', ]; if ( value.steps.length !== expectedStepNames.length || JSON.stringify(value.steps.map((step) => step.name)) !== JSON.stringify(expectedStepNames) || value.deployment === null || JSON.stringify(value.deployment.lock) !== JSON.stringify(value.planned.lock) || JSON.stringify(value.deployment.resources) !== JSON.stringify(value.planned.resources) ) { fail('committed deployment head is invalid'); } validateDeploymentSummary(value.deployment); const expectedDigest = sha256( Buffer.from(JSON.stringify(deploymentEvidence(value)), 'utf8'), ); if (value.deployment.deploymentDigest !== expectedDigest) { fail('deployment evidence digest is invalid'); } } const { stateDigest, ...unsigned } = value; if (stateDigest !== sha256(Buffer.from(JSON.stringify(unsigned), 'utf8'))) { fail('deployment head self digest is invalid'); } return value; } function headReadStep(inputs, request, runner) { const step = runStep( inputs, request, 'deployment_head_read', [ 'get', 'configmap', HEAD_NAME, `--namespace=${inputs.headNamespace}`, '--ignore-not-found=true', '-o=json', ], runner, '', ); if (step.status !== 0) fail('deployment head read failed'); const { stdout: _stdout, ...record } = step; if (!step.stdout.trim()) { return Object.freeze({ state: null, resourceVersion: null, namespace: inputs.headNamespace, snapshot: headSnapshot(inputs.headNamespace, null), record: Object.freeze(record), }); } let configMap; let state; try { configMap = JSON.parse(step.stdout); state = JSON.parse(configMap?.data?.[HEAD_DATA_KEY]); } catch { fail('deployment head response is invalid'); } if ( configMap?.apiVersion !== 'v1' || configMap?.kind !== 'ConfigMap' || configMap?.metadata?.name !== HEAD_NAME || configMap?.metadata?.namespace !== inputs.headNamespace || typeof configMap?.metadata?.resourceVersion !== 'string' || !configMap.metadata.resourceVersion ) { fail('deployment head identity is invalid'); } validateHeadState(state); return Object.freeze({ state, resourceVersion: configMap.metadata.resourceVersion, namespace: inputs.headNamespace, snapshot: state.phase === 'committed' ? headSnapshot(inputs.headNamespace, state) : null, record: Object.freeze(record), }); } function headConfigMap(inputs, state, resourceVersion) { return JSON.stringify({ apiVersion: 'v1', kind: 'ConfigMap', metadata: { name: HEAD_NAME, namespace: inputs.headNamespace, labels: { 'app.kubernetes.io/managed-by': FIELD_MANAGER }, ...(resourceVersion === null ? {} : { resourceVersion }), }, data: { [HEAD_DATA_KEY]: JSON.stringify(state) }, }); } function writeHead(inputs, request, runner, state, resourceVersion, name) { const operation = resourceVersion === null ? 'create' : 'replace'; const step = runStep( inputs, request, name, [operation, '--validate=strict', '-f=-', '-o=json'], runner, headConfigMap(inputs, state, resourceVersion), ); if (step.status !== 0) fail(`deployment head ${operation} was rejected`); let written; try { written = JSON.parse(step.stdout); } catch { fail('deployment head write response is invalid'); } if ( typeof written?.metadata?.resourceVersion !== 'string' || !written.metadata.resourceVersion || written?.data?.[HEAD_DATA_KEY] !== JSON.stringify(state) ) { fail('deployment head write did not converge'); } const { stdout, ...record } = step; return Object.freeze({ record: Object.freeze(record), resourceVersion: written.metadata.resourceVersion, }); } function applyingHead(commandState, preflight, inputs, current) { const unsigned = { schemaVersion: 1, schema: HEAD_SCHEMA, phase: 'applying', generation: current.state === null ? 1 : current.state.generation + 1, transition: { kind: commandState.command.request.transitionKind, mutationId: commandState.command.request.mutationId, commandDigest: commandState.commandDigest, preflightDigest: preflight.report.preflightDigest, expectedHead: current.snapshot, }, previous: current.state === null ? null : Object.freeze({ ...current.state.deployment }), planned: deploymentPlan(inputs), target: targetSummary(inputs, commandState.command.request), tool: toolSummary(inputs), steps: [], deployment: null, }; return Object.freeze({ ...unsigned, stateDigest: sha256(Buffer.from(JSON.stringify(unsigned), 'utf8')), }); } function committedHead(applying, steps) { const evidenceState = { ...applying, phase: 'committed', steps }; delete evidenceState.stateDigest; const deployment = Object.freeze({ deploymentDigest: sha256( Buffer.from(JSON.stringify(deploymentEvidence(evidenceState)), 'utf8'), ), lock: applying.planned.lock, resources: applying.planned.resources, }); const unsigned = { ...evidenceState, deployment }; return Object.freeze({ ...unsigned, stateDigest: sha256(Buffer.from(JSON.stringify(unsigned), 'utf8')), }); } function validatePreflight(value) { if ( !exactKeys(value, [ 'schemaVersion', 'schema', 'preflightId', 'commandDigest', 'transitionKind', 'lock', 'resourceInventory', 'deploymentHead', 'target', 'tool', 'steps', 'verification', 'preflightDigest', ]) || value.schemaVersion !== 1 || value.schema !== PREFLIGHT_SCHEMA || !UUID_PATTERN.test(value.preflightId || '') || !DIGEST_PATTERN.test(value.commandDigest || '') || !DIGEST_PATTERN.test(value.preflightDigest || '') || !TRANSITION_KINDS.includes(value.transitionKind) || !Array.isArray(value.steps) || value.steps.length !== 3 || value.steps[0]?.name !== 'cluster_identity_before' || value.steps[1]?.name !== 'deployment_head_read' || value.steps[2]?.name !== 'server_side_dry_run' || !exactKeys(value.verification, [ 'catalogBoundLock', 'clusterIdentityBound', 'serverSideDryRun', 'deploymentHeadCompared', 'resourceInventoryClosed', 'networkAccess', 'kubernetesMutation', 'externalResultsReplayed', 'ambientHome', 'ephemeralCache', ]) || JSON.stringify(value.verification) !== JSON.stringify({ catalogBoundLock: true, clusterIdentityBound: true, serverSideDryRun: true, deploymentHeadCompared: true, resourceInventoryClosed: true, networkAccess: true, kubernetesMutation: false, externalResultsReplayed: false, ambientHome: false, ephemeralCache: true, }) ) { fail('deployment preflight report is invalid'); } validateLockSummary(value.lock); validateResourceInventory(value.resourceInventory); validateHeadSnapshot(value.deploymentHead); validateTargetSummary(value.target); validateToolSummary(value.tool); for (const step of value.steps) validateStepRecord(step); if (value.steps.some((step) => step.status !== 0)) { fail('deployment preflight contains a failed step'); } const { preflightDigest, ...unsigned } = value; if ( preflightDigest !== sha256(Buffer.from(JSON.stringify(unsigned), 'utf8')) ) { fail('deployment preflight self digest is invalid'); } return value; } function validateLockSummary(value) { if ( !exactKeys(value, [ 'lockDigest', 'manifestDigest', 'lockReportDigest', 'releaseSetDigest', 'catalogManifestDigest', 'catalogReportDigest', 'catalogImmutableReference', 'release', 'requiredImages', ]) || !DIGEST_PATTERN.test(value.lockDigest || '') || !DIGEST_PATTERN.test(value.manifestDigest || '') || !DIGEST_PATTERN.test(value.lockReportDigest || '') || !DIGEST_PATTERN.test(value.releaseSetDigest || '') || !DIGEST_PATTERN.test(value.catalogManifestDigest || '') || !DIGEST_PATTERN.test(value.catalogReportDigest || '') || !Array.isArray(value.requiredImages) || value.requiredImages.length < 1 || JSON.stringify(value.requiredImages) !== JSON.stringify( ROLE_ORDER.filter((role) => value.requiredImages.includes(role)), ) ) { fail('deployment lock summary is invalid'); } validateRelease(value.release); if ( !new RegExp( `^ghcr\\.io/[a-z0-9](?:[a-z0-9-]{0,37}[a-z0-9])?/qinglong3-release-catalog@${value.catalogManifestDigest}$`, 'u', ).test(value.catalogImmutableReference || '') ) { fail('deployment lock summary catalog reference is invalid'); } } function validateTargetSummary(value) { if ( !exactKeys(value, [ 'context', 'clusterUid', 'kubeconfigDigest', 'fieldManager', ]) || value.fieldManager !== FIELD_MANAGER || !CONTEXT_PATTERN.test(value.context || '') || !UUID_PATTERN.test(value.clusterUid || '') || !DIGEST_PATTERN.test(value.kubeconfigDigest || '') ) { fail('deployment target summary is invalid'); } } function validateToolSummary(value) { if ( !exactKeys(value, ['name', 'executableDigest']) || value.name !== 'kubectl' || !DIGEST_PATTERN.test(value.executableDigest || '') ) { fail('deployment tool summary is invalid'); } } function validateStepRecord(step) { if ( !exactKeys(step, [ 'name', 'status', 'argvDigest', 'stdoutDigest', 'stdoutBytes', 'stderrDigest', 'stderrBytes', ]) || typeof step.name !== 'string' || !Number.isInteger(step.status) || !DIGEST_PATTERN.test(step.argvDigest || '') || !DIGEST_PATTERN.test(step.stdoutDigest || '') || !Number.isSafeInteger(step.stdoutBytes) || step.stdoutBytes < 0 || step.stdoutBytes > MAX_PROCESS_OUTPUT_BYTES || !DIGEST_PATTERN.test(step.stderrDigest || '') || !Number.isSafeInteger(step.stderrBytes) || step.stderrBytes < 0 || step.stderrBytes > MAX_PROCESS_OUTPUT_BYTES ) { fail('deployment command step is invalid'); } } function createPreflight(commandState, dependencies = {}) { const request = commandState.command.request; const output = outputTarget(request.output, 'preflight output'); const inputs = inspectInputs(request); const runner = dependencies.runProcess ?? defaultRunProcess; const identity = identityStep( inputs, request, runner, 'cluster_identity_before', ); const current = headReadStep(inputs, request, runner); if (current.state !== null && current.state.phase !== 'committed') { fail('deployment head contains an unfinished transition'); } validateTransition(current, request, deploymentPlan(inputs)); const steps = [identity, current.record, dryRunStep(inputs, request, runner)]; revalidateStableInputs(inputs, request); const unsigned = { schemaVersion: 1, schema: PREFLIGHT_SCHEMA, preflightId: request.preflightId, commandDigest: commandState.commandDigest, transitionKind: request.transitionKind, lock: lockSummary(inputs), resourceInventory: inputs.resourceInventory, deploymentHead: current.snapshot, target: targetSummary(inputs, request), tool: toolSummary(inputs), steps, verification: { catalogBoundLock: true, clusterIdentityBound: true, serverSideDryRun: true, deploymentHeadCompared: true, resourceInventoryClosed: true, networkAccess: true, kubernetesMutation: false, externalResultsReplayed: false, ambientHome: false, ephemeralCache: true, }, }; const result = Object.freeze({ ...unsigned, preflightDigest: sha256(Buffer.from(JSON.stringify(unsigned), 'utf8')), }); writeNoReplace(output, result); return result; } function readAndValidatePreflight(request, inputs) { const file = readCanonicalJson( request.preflight.path, 'preflight report', MAX_REPORT_BYTES, ); const report = validatePreflight(file.value); if ( report.preflightDigest !== request.preflight.expectedDigest || report.transitionKind !== request.transitionKind || JSON.stringify(report.lock) !== JSON.stringify(lockSummary(inputs)) || JSON.stringify(report.resourceInventory) !== JSON.stringify(inputs.resourceInventory) || JSON.stringify(report.deploymentHead) !== JSON.stringify(expectedSnapshot(request, inputs.headNamespace)) || JSON.stringify(report.target) !== JSON.stringify(targetSummary(inputs, request)) || JSON.stringify(report.tool) !== JSON.stringify(toolSummary(inputs)) ) { fail('deployment preflight binding changed'); } return Object.freeze({ file, report }); } function receiptFromCommitted(state, namespace) { if (state.phase !== 'committed' || state.transition.kind === 'retire') { fail('deployment receipt requires a committed head'); } const unsigned = { schemaVersion: 1, schema: RECEIPT_SCHEMA, mutationId: state.transition.mutationId, commandDigest: state.transition.commandDigest, preflightDigest: state.transition.preflightDigest, transitionKind: state.transition.kind, lock: state.deployment.lock, resourceInventory: state.deployment.resources, deploymentHead: headSnapshot(namespace, state), target: state.target, tool: state.tool, steps: state.steps, verification: { catalogBoundLock: true, clusterIdentityBound: true, serverSideDryRun: true, serverSideApply: true, convergenceRead: true, deploymentHeadCas: true, resourceInventoryClosed: true, networkAccess: true, kubernetesMutation: true, crossResourceAtomicity: false, externalResultsReplayed: false, recovery: 'resume_exact_transition_from_target_head', ambientHome: false, ephemeralCache: true, }, }; return Object.freeze({ ...unsigned, receiptDigest: sha256(Buffer.from(JSON.stringify(unsigned), 'utf8')), }); } function validateReceipt(value, expectedCommandDigest) { if ( !exactKeys(value, [ 'schemaVersion', 'schema', 'mutationId', 'commandDigest', 'preflightDigest', 'transitionKind', 'lock', 'resourceInventory', 'deploymentHead', 'target', 'tool', 'steps', 'verification', 'receiptDigest', ]) || value.schemaVersion !== 1 || value.schema !== RECEIPT_SCHEMA || !UUID_PATTERN.test(value.mutationId || '') || value.commandDigest !== expectedCommandDigest || !DIGEST_PATTERN.test(value.preflightDigest || '') || !DIGEST_PATTERN.test(value.receiptDigest || '') || !TRANSITION_KINDS.includes(value.transitionKind) || !Array.isArray(value.steps) || JSON.stringify(value.steps.map((step) => step.name)) !== JSON.stringify([ 'cluster_identity_before', 'server_side_dry_run', 'server_side_apply', 'server_side_convergence_read', 'cluster_identity_after', ]) || !exactKeys(value.verification, [ 'catalogBoundLock', 'clusterIdentityBound', 'serverSideDryRun', 'serverSideApply', 'convergenceRead', 'deploymentHeadCas', 'resourceInventoryClosed', 'networkAccess', 'kubernetesMutation', 'crossResourceAtomicity', 'externalResultsReplayed', 'recovery', 'ambientHome', 'ephemeralCache', ]) || JSON.stringify(value.verification) !== JSON.stringify({ catalogBoundLock: true, clusterIdentityBound: true, serverSideDryRun: true, serverSideApply: true, convergenceRead: true, deploymentHeadCas: true, resourceInventoryClosed: true, networkAccess: true, kubernetesMutation: true, crossResourceAtomicity: false, externalResultsReplayed: false, recovery: 'resume_exact_transition_from_target_head', ambientHome: false, ephemeralCache: true, }) ) { fail('deployment receipt is invalid'); } validateLockSummary(value.lock); validateResourceInventory(value.resourceInventory); validateHeadSnapshot(value.deploymentHead); validateTargetSummary(value.target); validateToolSummary(value.tool); for (const step of value.steps) validateStepRecord(step); if (value.steps.some((step) => step.status !== 0)) { fail('deployment receipt contains a failed step'); } const { receiptDigest, ...unsigned } = value; if (receiptDigest !== sha256(Buffer.from(JSON.stringify(unsigned), 'utf8'))) { fail('deployment receipt self digest is invalid'); } return value; } function retirementDeleteDryRun(api, targets) { const dryRunRecords = []; for (const target of targets) { const response = recordedApiRequest( dryRunRecords, api, 'retirement_delete_dry_run', 'DELETE', target.requestPath, deleteOptions(target.observation, true), ); if (![200, 201, 202].includes(response.status)) { fail('Kubernetes retirement dry-run was rejected'); } } return aggregateRequestStep('server_side_delete_dry_run', dryRunRecords); } function retirementRecheck(api, resources, targets) { const records = []; const cache = new Map(); for (let index = 0; index < resources.length; index += 1) { const desired = resources[index]; const observation = targets[index].observation; const requestPath = apiResourcePath(api, observation, cache, records); const response = recordedApiRequest( records, api, 'retirement_target_recheck', 'GET', requestPath, null, ); parseRetirementObject(response, desired, observation); } return aggregateRequestStep('retirement_target_recheck', records); } function validateRetirementPreflight(value) { if ( !exactKeys(value, [ 'schemaVersion', 'schema', 'preflightId', 'commandDigest', 'lock', 'activeResourceInventory', 'survivorResourceInventory', 'retirementTargets', 'deploymentHead', 'target', 'tools', 'steps', 'verification', 'preflightDigest', ]) || value.schemaVersion !== 1 || value.schema !== RETIREMENT_PREFLIGHT_SCHEMA || !UUID_PATTERN.test(value.preflightId || '') || !DIGEST_PATTERN.test(value.commandDigest || '') || !DIGEST_PATTERN.test(value.preflightDigest || '') || !Array.isArray(value.steps) || JSON.stringify(value.steps.map((step) => step.name)) !== JSON.stringify([ 'cluster_identity_before', 'deployment_head_read', 'retirement_target_read', 'server_side_delete_dry_run', 'retirement_target_recheck', ]) || !exactKeys(value.verification, [ 'catalogBoundLock', 'clusterIdentityBound', 'deploymentHeadCompared', 'uidResourceVersionObserved', 'serverSideDeleteDryRun', 'survivorRoleClosure', 'resourceInventoryClosed', 'networkAccess', 'kubernetesMutation', 'unixSocketProxy', 'ambientHome', 'ephemeralCache', ]) || JSON.stringify(value.verification) !== JSON.stringify({ catalogBoundLock: true, clusterIdentityBound: true, deploymentHeadCompared: true, uidResourceVersionObserved: true, serverSideDeleteDryRun: true, survivorRoleClosure: true, resourceInventoryClosed: true, networkAccess: true, kubernetesMutation: false, unixSocketProxy: true, ambientHome: false, ephemeralCache: true, }) ) { fail('retirement preflight report is invalid'); } validateLockSummary(value.lock); validateResourceInventory(value.activeResourceInventory); validateResourceInventory(value.survivorResourceInventory); validateRetirementObservations(value.retirementTargets); validateHeadSnapshot(value.deploymentHead); validateTargetSummary(value.target); validateRetirementToolSummary(value.tools); for (const step of value.steps) validateStepRecord(step); if (value.steps.some((step) => step.status !== 0)) { fail('retirement preflight contains a failed step'); } const retired = new Set( value.retirementTargets.map((target) => inventoryKey(target)), ); if ( JSON.stringify( value.activeResourceInventory.filter( (resource) => !retired.has(inventoryKey(resource)), ), ) !== JSON.stringify(value.survivorResourceInventory) ) { fail('retirement preflight inventory closure is invalid'); } const { preflightDigest, ...unsigned } = value; if ( preflightDigest !== sha256(Buffer.from(JSON.stringify(unsigned), 'utf8')) ) { fail('retirement preflight self digest is invalid'); } return value; } function createRetirementPreflight(commandState, dependencies = {}) { const request = commandState.command.request; const output = outputTarget(request.output, 'retirement preflight output'); const inputs = inspectRetirementInputs(request); const runner = dependencies.runProcess ?? defaultRunProcess; const identity = identityStep( inputs, request, runner, 'cluster_identity_before', ); const current = headReadStep(inputs, request, runner); if ( current.state === null || current.state.phase !== 'committed' || JSON.stringify(current.snapshot) !== JSON.stringify(expectedSnapshot(request, inputs.headNamespace)) ) { fail('deployment retirement head authority changed'); } const plan = retirementPlan(inputs, current, request.targets); const apiEvidence = withRetirementApi( inputs, request, dependencies, (api) => { const readRecords = []; const targets = readRetirementTargets(api, plan.resources, readRecords); return Object.freeze({ targets, read: aggregateRequestStep('retirement_target_read', readRecords), dryRun: retirementDeleteDryRun(api, targets), recheck: retirementRecheck(api, plan.resources, targets), }); }, ); revalidateRetirementInputs(inputs, request); const unsigned = { schemaVersion: 1, schema: RETIREMENT_PREFLIGHT_SCHEMA, preflightId: request.preflightId, commandDigest: commandState.commandDigest, lock: lockSummary(inputs), activeResourceInventory: plan.active, survivorResourceInventory: plan.survivors, retirementTargets: Object.freeze( apiEvidence.targets.map((target) => target.observation), ), deploymentHead: current.snapshot, target: targetSummary(inputs, request), tools: retirementToolSummary(inputs), steps: Object.freeze([ identity, current.record, apiEvidence.read, apiEvidence.dryRun, apiEvidence.recheck, ]), verification: { catalogBoundLock: true, clusterIdentityBound: true, deploymentHeadCompared: true, uidResourceVersionObserved: true, serverSideDeleteDryRun: true, survivorRoleClosure: true, resourceInventoryClosed: true, networkAccess: true, kubernetesMutation: false, unixSocketProxy: true, ambientHome: false, ephemeralCache: true, }, }; const result = Object.freeze({ ...unsigned, preflightDigest: sha256(Buffer.from(JSON.stringify(unsigned), 'utf8')), }); writeNoReplace(output, result); return result; } function readAndValidateRetirementPreflight(request, inputs) { const file = readCanonicalJson( request.preflight.path, 'retirement preflight report', MAX_REPORT_BYTES, ); const report = validateRetirementPreflight(file.value); if ( report.preflightDigest !== request.preflight.expectedDigest || JSON.stringify(report.lock) !== JSON.stringify(lockSummary(inputs)) || JSON.stringify(report.deploymentHead) !== JSON.stringify(expectedSnapshot(request, inputs.headNamespace)) || JSON.stringify( report.retirementTargets.map( ({ uid: _uid, resourceVersion: _resourceVersion, ...item }) => item, ), ) !== JSON.stringify(request.targets) || JSON.stringify(report.target) !== JSON.stringify(targetSummary(inputs, request)) || JSON.stringify(report.tools) !== JSON.stringify(retirementToolSummary(inputs)) ) { fail('retirement preflight binding changed'); } return Object.freeze({ file, report }); } function retirementApplyingHead(commandState, preflight, inputs, current) { const unsigned = { schemaVersion: 1, schema: HEAD_SCHEMA, phase: 'applying', generation: current.state.generation + 1, transition: { kind: 'retire', mutationId: commandState.command.request.mutationId, commandDigest: commandState.commandDigest, preflightDigest: preflight.report.preflightDigest, expectedHead: current.snapshot, retirements: preflight.report.retirementTargets, curlExecutableDigest: inputs.curl.digest, }, previous: Object.freeze({ ...current.state.deployment }), planned: Object.freeze({ lock: current.state.deployment.lock, resources: preflight.report.survivorResourceInventory, }), target: targetSummary(inputs, commandState.command.request), tool: toolSummary(inputs), steps: [], deployment: null, }; return Object.freeze({ ...unsigned, stateDigest: sha256(Buffer.from(JSON.stringify(unsigned), 'utf8')), }); } function assertRetirementBinding(state, commandState, preflight, inputs) { const request = commandState.command.request; if ( state.transition.kind !== 'retire' || state.transition.mutationId !== request.mutationId || state.transition.commandDigest !== commandState.commandDigest || state.transition.preflightDigest !== preflight.report.preflightDigest || state.transition.curlExecutableDigest !== inputs.curl.digest || JSON.stringify(state.transition.expectedHead) !== JSON.stringify(preflight.report.deploymentHead) || JSON.stringify(state.transition.retirements) !== JSON.stringify(preflight.report.retirementTargets) || JSON.stringify(state.planned) !== JSON.stringify({ lock: preflight.report.lock, resources: preflight.report.survivorResourceInventory, }) || JSON.stringify(state.target) !== JSON.stringify(targetSummary(inputs, request)) || JSON.stringify(state.tool) !== JSON.stringify(toolSummary(inputs)) ) { fail('retirement head transition binding changed'); } } function retirementObjectState(response, desired, expected) { if (response.status === 404) return Object.freeze({ kind: 'absent' }); if (response.status !== 200) { fail('retirement target inspection failed'); } let live; try { live = JSON.parse(response.body); } catch { fail('retirement target response is invalid'); } if ( inventoryKey(live) !== inventoryKey(expected) || typeof live.metadata?.uid !== 'string' || typeof live.metadata?.resourceVersion !== 'string' ) { fail('retirement target identity changed'); } if (live.metadata.uid !== expected.uid) { fail('retirement target was replaced by a different UID'); } assertDesiredSubset(desired, live); if (live.metadata.deletionTimestamp !== undefined) { return Object.freeze({ kind: 'terminating' }); } if (live.metadata.resourceVersion !== expected.resourceVersion) { fail('retirement target resourceVersion changed'); } return Object.freeze({ kind: 'current' }); } function recoveredDeleteRecord(observation, requestPath) { const body = deleteOptions(observation, false); return Object.freeze({ name: 'retirement_delete_recovered', method: 'DELETE', requestPathDigest: sha256(Buffer.from(requestPath, 'utf8')), requestDigest: sha256(Buffer.from(JSON.stringify(body), 'utf8')), status: 404, responseDigest: sha256(Buffer.from('recovered_absent', 'utf8')), responseBytes: 0, }); } function executeRetirementDeletes(api, inputs, preflight, resumed) { const desiredByIdentity = new Map( inputs.resources.map((resource) => [inventoryKey(resource), resource]), ); const cache = new Map(); const deleteRecords = []; const absenceRecords = []; for (const observation of preflight.retirementTargets) { const desired = desiredByIdentity.get(inventoryKey(observation)); if (desired === undefined) { fail('retirement target is no longer in the locked manifest'); } const requestPath = apiResourcePath(api, observation, cache, deleteRecords); const before = recordedApiRequest( deleteRecords, api, 'retirement_target_before_delete', 'GET', requestPath, null, ); const beforeState = retirementObjectState(before, desired, observation); if (beforeState.kind === 'absent') { if (!resumed) fail('retirement target disappeared before deletion'); deleteRecords.push(recoveredDeleteRecord(observation, requestPath)); } else if (beforeState.kind === 'terminating') { fail('retirement target deletion is not complete'); } else { const response = recordedApiRequest( deleteRecords, api, 'retirement_target_delete', 'DELETE', requestPath, deleteOptions(observation, false), ); if (![200, 201, 202].includes(response.status)) { fail('Kubernetes retirement delete was rejected'); } } const after = recordedApiRequest( absenceRecords, api, 'retirement_target_absence', 'GET', requestPath, null, ); const afterState = retirementObjectState(after, desired, observation); if (afterState.kind === 'terminating') { fail('retirement target deletion is not complete'); } if (afterState.kind !== 'absent') { fail('retirement target remains after deletion'); } } return Object.freeze({ deletion: aggregateRequestStep( 'resource_precondition_delete', deleteRecords, ), absence: aggregateRequestStep('retirement_absence_read', absenceRecords), }); } function retirementReceiptFromCommitted(state, namespace, inputs) { if (state.phase !== 'committed' || state.transition.kind !== 'retire') { fail('retirement receipt requires a committed retirement head'); } const unsigned = { schemaVersion: 1, schema: RETIREMENT_RECEIPT_SCHEMA, mutationId: state.transition.mutationId, commandDigest: state.transition.commandDigest, preflightDigest: state.transition.preflightDigest, lock: state.deployment.lock, retiredResources: state.transition.retirements, resourceInventory: state.deployment.resources, deploymentHead: headSnapshot(namespace, state), target: state.target, tools: retirementToolSummary(inputs), steps: state.steps, verification: { catalogBoundLock: true, clusterIdentityBound: true, uidResourceVersionDeletePreconditions: true, deploymentHeadCas: true, resourceInventoryClosed: true, survivorRoleClosure: true, deletionAbsenceConfirmed: true, networkAccess: true, kubernetesMutation: true, crossResourceAtomicity: false, externalResultsReplayed: false, recovery: 'resume_exact_retirement_from_target_head', unixSocketProxy: true, ambientHome: false, ephemeralCache: true, }, }; return Object.freeze({ ...unsigned, receiptDigest: sha256(Buffer.from(JSON.stringify(unsigned), 'utf8')), }); } function validateRetirementReceipt(value, expectedCommandDigest) { if ( !exactKeys(value, [ 'schemaVersion', 'schema', 'mutationId', 'commandDigest', 'preflightDigest', 'lock', 'retiredResources', 'resourceInventory', 'deploymentHead', 'target', 'tools', 'steps', 'verification', 'receiptDigest', ]) || value.schemaVersion !== 1 || value.schema !== RETIREMENT_RECEIPT_SCHEMA || !UUID_PATTERN.test(value.mutationId || '') || value.commandDigest !== expectedCommandDigest || !DIGEST_PATTERN.test(value.preflightDigest || '') || !DIGEST_PATTERN.test(value.receiptDigest || '') || !Array.isArray(value.steps) || JSON.stringify(value.steps.map((step) => step.name)) !== JSON.stringify([ 'cluster_identity_before', 'resource_precondition_delete', 'retirement_absence_read', 'cluster_identity_after', ]) || !exactKeys(value.verification, [ 'catalogBoundLock', 'clusterIdentityBound', 'uidResourceVersionDeletePreconditions', 'deploymentHeadCas', 'resourceInventoryClosed', 'survivorRoleClosure', 'deletionAbsenceConfirmed', 'networkAccess', 'kubernetesMutation', 'crossResourceAtomicity', 'externalResultsReplayed', 'recovery', 'unixSocketProxy', 'ambientHome', 'ephemeralCache', ]) || JSON.stringify(value.verification) !== JSON.stringify({ catalogBoundLock: true, clusterIdentityBound: true, uidResourceVersionDeletePreconditions: true, deploymentHeadCas: true, resourceInventoryClosed: true, survivorRoleClosure: true, deletionAbsenceConfirmed: true, networkAccess: true, kubernetesMutation: true, crossResourceAtomicity: false, externalResultsReplayed: false, recovery: 'resume_exact_retirement_from_target_head', unixSocketProxy: true, ambientHome: false, ephemeralCache: true, }) ) { fail('retirement receipt is invalid'); } validateLockSummary(value.lock); validateRetirementObservations(value.retiredResources); validateResourceInventory(value.resourceInventory); validateHeadSnapshot(value.deploymentHead); validateTargetSummary(value.target); validateRetirementToolSummary(value.tools); for (const step of value.steps) validateStepRecord(step); if (value.steps.some((step) => step.status !== 0)) { fail('retirement receipt contains a failed step'); } const { receiptDigest, ...unsigned } = value; if (receiptDigest !== sha256(Buffer.from(JSON.stringify(unsigned), 'utf8'))) { fail('retirement receipt self digest is invalid'); } return value; } function existingRetirementReceipt(request, commandDigest) { if (!fs.existsSync(request.output)) return null; const file = readCanonicalJson( request.output, 'existing retirement receipt', MAX_REPORT_BYTES, ); const receipt = validateRetirementReceipt(file.value, commandDigest); if ( receipt.mutationId !== request.mutationId || receipt.preflightDigest !== request.preflight.expectedDigest ) { fail('existing retirement receipt binding changed'); } return receipt; } function applyRetirement(commandState, dependencies = {}) { const request = commandState.command.request; const resumedReceipt = existingRetirementReceipt( request, commandState.commandDigest, ); const output = resumedReceipt === null ? outputTarget(request.output, 'retirement receipt output') : request.output; const inputs = inspectRetirementInputs(request); const preflight = readAndValidateRetirementPreflight(request, inputs); const runner = dependencies.runProcess ?? defaultRunProcess; const current = headReadStep(inputs, request, runner); if (resumedReceipt !== null) { if ( current.state === null || current.state.phase !== 'committed' || resumedReceipt.preflightDigest !== preflight.report.preflightDigest || JSON.stringify(resumedReceipt.lock) !== JSON.stringify(preflight.report.lock) || JSON.stringify(resumedReceipt.resourceInventory) !== JSON.stringify(preflight.report.survivorResourceInventory) || JSON.stringify(resumedReceipt.deploymentHead) !== JSON.stringify(headSnapshot(inputs.headNamespace, current.state)) || JSON.stringify(resumedReceipt.target) !== JSON.stringify(targetSummary(inputs, request)) || JSON.stringify(resumedReceipt.tools) !== JSON.stringify(retirementToolSummary(inputs)) ) { fail('existing retirement receipt authority changed'); } assertRetirementBinding(current.state, commandState, preflight, inputs); revalidateRetirementInputs(inputs, request); return resumedReceipt; } if ( current.state !== null && current.state.phase === 'committed' && current.state.transition.kind === 'retire' && current.state.transition.commandDigest === commandState.commandDigest ) { assertRetirementBinding(current.state, commandState, preflight, inputs); const recovered = retirementReceiptFromCommitted( current.state, inputs.headNamespace, inputs, ); writeNoReplace(output, recovered); revalidateRetirementInputs(inputs, request); return recovered; } let applying; let headResourceVersion; let resumedHead = false; if (current.state !== null && current.state.phase === 'applying') { assertRetirementBinding(current.state, commandState, preflight, inputs); applying = current.state; headResourceVersion = current.resourceVersion; resumedHead = true; } else { if ( JSON.stringify(current.snapshot) !== JSON.stringify(preflight.report.deploymentHead) ) { fail('deployment retirement head changed after preflight'); } const plan = retirementPlan(inputs, current, request.targets); if ( JSON.stringify(plan.active) !== JSON.stringify(preflight.report.activeResourceInventory) || JSON.stringify(plan.survivors) !== JSON.stringify(preflight.report.survivorResourceInventory) ) { fail('deployment retirement inventory changed after preflight'); } withRetirementApi(inputs, request, dependencies, (api) => { const records = []; const observations = readRetirementTargets( api, plan.resources, records, ).map((target) => target.observation); if ( JSON.stringify(observations) !== JSON.stringify(preflight.report.retirementTargets) ) { fail('deployment retirement target changed after preflight'); } }); applying = retirementApplyingHead(commandState, preflight, inputs, current); const acquired = writeHead( inputs, request, runner, applying, current.resourceVersion, 'deployment_head_acquire', ); headResourceVersion = acquired.resourceVersion; } const beforeIdentity = identityStep( inputs, request, runner, 'cluster_identity_before', ); const deletion = withRetirementApi(inputs, request, dependencies, (api) => executeRetirementDeletes(api, inputs, preflight.report, resumedHead), ); const afterIdentity = identityStep( inputs, request, runner, 'cluster_identity_after', ); revalidateRetirementInputs(inputs, request); const committed = committedHead( applying, Object.freeze([ beforeIdentity, deletion.deletion, deletion.absence, afterIdentity, ]), ); writeHead( inputs, request, runner, committed, headResourceVersion, 'deployment_head_commit', ); const receipt = retirementReceiptFromCommitted( committed, inputs.headNamespace, inputs, ); writeNoReplace(output, receipt); return receipt; } function existingReceipt(request, commandDigest) { if (!fs.existsSync(request.output)) return null; const file = readCanonicalJson( request.output, 'existing deployment receipt', MAX_REPORT_BYTES, ); const receipt = validateReceipt(file.value, commandDigest); if ( receipt.mutationId !== request.mutationId || receipt.preflightDigest !== request.preflight.expectedDigest ) { fail('existing deployment receipt binding changed'); } return receipt; } function assertTransitionBinding(state, commandState, preflight, inputs) { const request = commandState.command.request; if ( state.transition.kind !== request.transitionKind || state.transition.mutationId !== request.mutationId || state.transition.commandDigest !== commandState.commandDigest || state.transition.preflightDigest !== preflight.report.preflightDigest || JSON.stringify(state.transition.expectedHead) !== JSON.stringify(preflight.report.deploymentHead) || JSON.stringify(state.planned) !== JSON.stringify(deploymentPlan(inputs)) || JSON.stringify(state.target) !== JSON.stringify(targetSummary(inputs, request)) || JSON.stringify(state.tool) !== JSON.stringify(toolSummary(inputs)) ) { fail('deployment head transition binding changed'); } } function applyDeployment(commandState, dependencies = {}) { const request = commandState.command.request; const resumed = existingReceipt(request, commandState.commandDigest); const output = resumed === null ? outputTarget(request.output, 'apply receipt output') : request.output; const inputs = inspectInputs(request); const preflight = readAndValidatePreflight(request, inputs); const runner = dependencies.runProcess ?? defaultRunProcess; const current = headReadStep(inputs, request, runner); if (resumed !== null) { if ( current.state === null || current.state.phase !== 'committed' || resumed.preflightDigest !== preflight.report.preflightDigest || JSON.stringify(resumed.lock) !== JSON.stringify(lockSummary(inputs)) || JSON.stringify(resumed.resourceInventory) !== JSON.stringify(inputs.resourceInventory) || JSON.stringify(resumed.deploymentHead) !== JSON.stringify(headSnapshot(inputs.headNamespace, current.state)) || JSON.stringify(resumed.target) !== JSON.stringify(targetSummary(inputs, request)) || JSON.stringify(resumed.tool) !== JSON.stringify(toolSummary(inputs)) ) { fail('existing deployment receipt authority changed'); } assertTransitionBinding(current.state, commandState, preflight, inputs); revalidateStableInputs(inputs, request); return resumed; } if ( current.state !== null && current.state.phase === 'committed' && current.state.transition.commandDigest === commandState.commandDigest ) { assertTransitionBinding(current.state, commandState, preflight, inputs); const recovered = receiptFromCommitted(current.state, inputs.headNamespace); writeNoReplace(output, recovered); revalidateStableInputs(inputs, request); return recovered; } let applying; let headResourceVersion; if (current.state !== null && current.state.phase === 'applying') { assertTransitionBinding(current.state, commandState, preflight, inputs); applying = current.state; headResourceVersion = current.resourceVersion; } else { if ( JSON.stringify(current.snapshot) !== JSON.stringify(preflight.report.deploymentHead) ) { fail('deployment head changed after preflight'); } validateTransition(current, request, deploymentPlan(inputs)); applying = applyingHead(commandState, preflight, inputs, current); const acquired = writeHead( inputs, request, runner, applying, current.resourceVersion, 'deployment_head_acquire', ); headResourceVersion = acquired.resourceVersion; } const steps = [ identityStep(inputs, request, runner, 'cluster_identity_before'), dryRunStep(inputs, request, runner), ]; const applied = runStep( inputs, request, 'server_side_apply', [ 'apply', '--server-side', `--field-manager=${FIELD_MANAGER}`, '--validate=strict', '-f=-', '-o=name', ], runner, ); if (applied.status !== 0) fail('Kubernetes server-side apply failed'); const { stdout: _appliedOutput, ...appliedRecord } = applied; steps.push(Object.freeze(appliedRecord)); steps.push(convergenceReadStep(inputs, request, runner)); steps.push(identityStep(inputs, request, runner, 'cluster_identity_after')); revalidateStableInputs(inputs, request); const committed = committedHead(applying, Object.freeze(steps)); writeHead( inputs, request, runner, committed, headResourceVersion, 'deployment_head_commit', ); const receipt = receiptFromCommitted(committed, inputs.headNamespace); writeNoReplace(output, receipt); return receipt; } function auditReceipt(command) { const applyFile = verifyExpected( readCanonicalJson( command.request.applyCommand.path, 'apply command', MAX_COMMAND_BYTES, ), command.request.applyCommand.expectedDigest, 'apply command', ); const applyCommand = parseCommand(applyFile.value); if (applyCommand.operation !== 'cluster.deployment.apply') { fail('receipt audit requires an apply command'); } const receiptFile = readCanonicalJson( command.request.receipt.path, 'deployment receipt', MAX_REPORT_BYTES, ); const receipt = validateReceipt(receiptFile.value, applyFile.digest); if ( receipt.receiptDigest !== command.request.receipt.expectedDigest || receipt.mutationId !== applyCommand.request.mutationId || receipt.preflightDigest !== applyCommand.request.preflight.expectedDigest || receipt.transitionKind !== applyCommand.request.transitionKind || applyCommand.request.output !== command.request.receipt.path ) { fail('deployment receipt command binding changed'); } return Object.freeze({ compatible: true, deploymentFamily: 'cluster', mutationId: receipt.mutationId, receiptDigest: receipt.receiptDigest, preflightDigest: receipt.preflightDigest, transitionKind: receipt.transitionKind, lockDigest: receipt.lock.lockDigest, manifestDigest: receipt.lock.manifestDigest, deploymentGeneration: receipt.deploymentHead.generation, deploymentDigest: receipt.deploymentHead.deploymentDigest, deploymentHeadStateDigest: receipt.deploymentHead.stateDigest, resourceCount: receipt.resourceInventory.length, clusterUid: receipt.target.clusterUid, externalResultsReplayed: false, kubernetesMutation: false, }); } function auditRetirementReceipt(command) { const applyFile = verifyExpected( readCanonicalJson( command.request.applyCommand.path, 'retirement apply command', MAX_COMMAND_BYTES, ), command.request.applyCommand.expectedDigest, 'retirement apply command', ); const applyCommand = parseCommand(applyFile.value); if (applyCommand.operation !== 'cluster.deployment.retirement.apply') { fail('retirement receipt audit requires a retirement apply command'); } const receiptFile = readCanonicalJson( command.request.receipt.path, 'retirement receipt', MAX_REPORT_BYTES, ); const receipt = validateRetirementReceipt( receiptFile.value, applyFile.digest, ); if ( receipt.receiptDigest !== command.request.receipt.expectedDigest || receipt.mutationId !== applyCommand.request.mutationId || receipt.preflightDigest !== applyCommand.request.preflight.expectedDigest || applyCommand.request.output !== command.request.receipt.path ) { fail('retirement receipt command binding changed'); } return Object.freeze({ compatible: true, deploymentFamily: 'cluster', mutationId: receipt.mutationId, receiptDigest: receipt.receiptDigest, preflightDigest: receipt.preflightDigest, transitionKind: 'retire', lockDigest: receipt.lock.lockDigest, manifestDigest: receipt.lock.manifestDigest, deploymentGeneration: receipt.deploymentHead.generation, deploymentDigest: receipt.deploymentHead.deploymentDigest, deploymentHeadStateDigest: receipt.deploymentHead.stateDigest, retiredResourceCount: receipt.retiredResources.length, resourceCount: receipt.resourceInventory.length, clusterUid: receipt.target.clusterUid, externalResultsReplayed: false, kubernetesMutation: false, }); } function executeCommand(commandFile, dependencies = {}) { const commandState = readCommand(commandFile); if (commandState.command.operation === 'cluster.deployment.preflight') { return createPreflight(commandState, dependencies); } if (commandState.command.operation === 'cluster.deployment.apply') { return applyDeployment(commandState, dependencies); } if ( commandState.command.operation === 'cluster.deployment.retirement.preflight' ) { return createRetirementPreflight(commandState, dependencies); } if ( commandState.command.operation === 'cluster.deployment.retirement.apply' ) { return applyRetirement(commandState, dependencies); } if ( commandState.command.operation === 'cluster.deployment.retirement.receipt.audit' ) { return auditRetirementReceipt(commandState.command); } return auditReceipt(commandState.command); } module.exports = Object.freeze({ COMMAND_SCHEMA, FIELD_MANAGER, HEAD_DATA_KEY, HEAD_NAME, HEAD_SCHEMA, LOCK_SCHEMA, PREFLIGHT_SCHEMA, RECEIPT_SCHEMA, RETIREMENT_PREFLIGHT_SCHEMA, RETIREMENT_RECEIPT_SCHEMA, QingLong3KubernetesDeploymentCeremonyError, canonicalJson, executeCommand, parseCommand, validateLockReport, validatePreflight, validateReceipt, validateRetirementPreflight, validateRetirementReceipt, });