#!/usr/bin/env node 'use strict'; const crypto = require('node:crypto'); const fs = require('node:fs'); const path = require('node:path'); const { COMMAND_SCHEMA, FIELD_MANAGER, HEAD_DATA_KEY, HEAD_NAME, canonicalJson, executeCommand, } = require('./lib/ql3-kubernetes-deployment-ceremony.cjs'); const { K3sDockerLiveFixture } = require('./lib/ql3-k3s-docker-live.cjs'); const { readReleaseIdentity } = require('./lib/ql3-release-identity.cjs'); const { createKubernetesLock } = require('./ql3-deployment-lock-contract.cjs'); const { auditCeremonyBundle, } = require('./ql3-release-catalog-consumption-ceremony.cjs'); const ROOT = path.resolve(__dirname, '..'); const VERSION = readReleaseIdentity(ROOT).version; const SOURCE_REVISION = 'd'.repeat(40); const NAMESPACE = 'ql3-deployment-live'; const CONTEXT = 'default'; const OWNER = 'qinglong-release'; const ROLE_ORDER = Object.freeze(['control', 'control-ai', 'admin', 'worker']); const IMAGE_NAMES = Object.freeze({ control: 'qinglong3-cluster-control', 'control-ai': 'qinglong3-cluster-control-ai', admin: 'qinglong3-cluster-admin', worker: 'qinglong3-worker', }); function fail(message) { throw new Error( `QingLong Kubernetes deployment live contract failed: ${message}`, ); } function digest(value) { return `sha256:${crypto.createHash('sha256').update(value).digest('hex')}`; } function privateFile(directory, name, contents) { const target = path.join(directory, name); fs.writeFileSync(target, contents, { mode: 0o600, flag: 'wx' }); return target; } function executablePath(input) { const candidates = [ input, ...(input === 'kubectl' ? ['/Applications/Docker.app/Contents/Resources/bin/kubectl'] : []), ...(process.env.PATH ?? '') .split(path.delimiter) .filter(Boolean) .map((directory) => path.join(directory, input)), ]; for (const candidate of candidates) { try { const resolved = fs.realpathSync(candidate); if (fs.lstatSync(resolved).isFile()) return resolved; } catch {} } fail('kubectl executable is unavailable'); } function references() { return Object.fromEntries( ROLE_ORDER.map((role, index) => [ role, `ghcr.io/${OWNER}/${IMAGE_NAMES[role]}@sha256:${String(index + 1).repeat( 64, )}`, ]), ); } function lockedArtifacts() { const releaseSetDigest = digest('d343-live-release-set'); const catalogManifestDigest = digest('d343-live-catalog-manifest'); const catalogReportDigest = digest('d343-live-catalog-report'); const imageReferences = references(); const annotations = { 'qinglong.io/release-set-digest': releaseSetDigest, 'qinglong.io/release-catalog-manifest-digest': catalogManifestDigest, 'qinglong.io/release-catalog-report-digest': catalogReportDigest, 'qinglong.io/release-source-revision': SOURCE_REVISION, 'qinglong.io/release-version': VERSION, }; const deployment = (role) => ({ apiVersion: 'apps/v1', kind: 'Deployment', metadata: { name: `ql3-${role.replace('control-ai', 'control-ai')}`, namespace: NAMESPACE, annotations, }, spec: { replicas: 0, selector: { matchLabels: { 'app.kubernetes.io/name': `ql3-${role}` } }, template: { metadata: { labels: { 'app.kubernetes.io/name': `ql3-${role}` }, annotations, }, spec: { containers: [ { name: role, image: imageReferences[role], command: ['/bin/false'], }, ], }, }, }, }); const resources = [ { apiVersion: 'v1', kind: 'Namespace', metadata: { name: NAMESPACE }, }, ...ROLE_ORDER.map(deployment), { apiVersion: 'v1', kind: 'ConfigMap', metadata: { name: 'ql3-plugin-package-secret-action-admission', namespace: NAMESPACE, annotations, }, data: { image: imageReferences.admin }, }, { apiVersion: 'v1', kind: 'ConfigMap', metadata: { name: 'ql3-retirement-live-target', namespace: NAMESPACE, annotations, }, data: { purpose: 'uid-resource-version-retirement-live-contract' }, }, ]; const manifest = `${resources .map((resource) => JSON.stringify(resource)) .join('\n---\n')}\n`; const unsigned = { schemaVersion: 1, schema: 'qinglong/kubernetes-deployment-lock@v2', release: { version: VERSION, sourceRevision: SOURCE_REVISION, sourceRef: `refs/tags/v${VERSION}`, scope: 'cluster', }, releaseSetDigest, catalog: { schema: 'qinglong/release-catalog-consumption-ceremony@v1', sourceRepository: `${OWNER}/qinglong`, workflowIdentity: `https://github.com/${OWNER}/qinglong/.github/workflows/ql3-image-release.yml@refs/tags/v${VERSION}`, immutableReference: `ghcr.io/${OWNER}/qinglong3-release-catalog@${catalogManifestDigest}`, manifestDigest: catalogManifestDigest, consumptionReportDigest: catalogReportDigest, releaseSetDigest, discoveryTagAuthority: 'none', }, deploymentFamily: 'cluster', requiredImages: [...ROLE_ORDER], imageOccurrences: ROLE_ORDER.map((role) => ({ name: role, reference: imageReferences[role], count: role === 'admin' ? 2 : 1, })), manifest: { inputDigest: digest('d343-live-source-render'), outputDigest: digest(manifest), resources: resources.length, changedResources: 5, admissionAuthorityCount: 1, }, verification: { releaseSet: 'standalone_structure_identity_and_self_digest', sourceRecordsReplayed: false, catalogConsumption: 'offline_reconstructed', externalToolResultsReplayed: false, unknownImageAuthorities: 0, mutableQingLongImages: 0, networkAccess: false, kubernetesMutation: false, }, }; return Object.freeze({ manifest, report: Object.freeze({ ...unsigned, lockDigest: digest(JSON.stringify(unsigned)), }), imageReferences: Object.freeze(imageReferences), }); } function catalogSourceManifest(imageReferences) { const annotations = { 'qinglong.io/live-contract': 'verified-release-catalog', }; const deployment = (role) => ({ apiVersion: 'apps/v1', kind: 'Deployment', metadata: { name: `ql3-${role}`, namespace: NAMESPACE, annotations, }, spec: { replicas: 0, selector: { matchLabels: { 'app.kubernetes.io/name': `ql3-${role}` } }, template: { metadata: { labels: { 'app.kubernetes.io/name': `ql3-${role}` }, annotations, }, spec: { containers: [ { name: role, image: imageReferences[role], command: ['/bin/false'], }, ], }, }, }, }); return `${[ { apiVersion: 'v1', kind: 'Namespace', metadata: { name: NAMESPACE }, }, ...ROLE_ORDER.map(deployment), { apiVersion: 'v1', kind: 'ConfigMap', metadata: { name: 'ql3-plugin-package-secret-action-admission', namespace: NAMESPACE, annotations, }, data: { image: imageReferences.admin }, }, { apiVersion: 'v1', kind: 'ConfigMap', metadata: { name: 'ql3-retirement-live-target', namespace: NAMESPACE, annotations, }, data: { purpose: 'catalog-bound-retirement-live-contract' }, }, ] .map((resource) => JSON.stringify(resource)) .join('\n---\n')}\n`; } function catalogLockedArtifacts(options, dependencies = {}) { const audit = dependencies.auditCeremonyBundle ?? auditCeremonyBundle; const materialize = dependencies.createKubernetesLock ?? createKubernetesLock; const identity = Object.freeze({ version: VERSION, sourceRevision: options?.sourceRevision, sourceRef: options?.sourceRef, releaseScope: options?.releaseScope, repositoryOwner: options?.repositoryOwner, sourceRepository: options?.sourceRepository, }); if (!['cluster', 'all'].includes(identity.releaseScope)) { fail('release catalog live deployment requires cluster or all scope'); } const consumption = audit({ ...identity, outputDirectory: options?.consumptionBundle, }); const imageReferences = Object.freeze( Object.fromEntries( ROLE_ORDER.map((role) => { const images = consumption.releaseSet?.images?.filter( (image) => image.name === role, ); if (images?.length !== 1) { fail(`verified release catalog is missing one ${role} image`); } return [role, images[0].reference]; }), ), ); const sourceManifest = catalogSourceManifest(imageReferences); const created = materialize(consumption.releaseSet, sourceManifest, { ...identity, consumption, requiredImages: ROLE_ORDER.join(','), }); return Object.freeze({ manifest: created.outputManifest, report: created.report, imageReferences, releaseAuthority: Object.freeze({ mode: 'verified_release_catalog', version: VERSION, sourceRevision: identity.sourceRevision, sourceRef: identity.sourceRef, scope: identity.releaseScope, releaseSetDigest: consumption.releaseSetDigest, catalogManifestDigest: consumption.catalogManifestDigest, catalogConsumptionDigest: consumption.contentDigest, immutableReference: consumption.immutableReference, }), }); } function deploymentArtifacts(environment = process.env, dependencies = {}) { const configuration = { consumptionBundle: environment.QL3_RELEASE_CATALOG_CONSUMPTION_BUNDLE, sourceRevision: environment.QL3_RELEASE_SOURCE_REVISION, sourceRef: environment.QL3_RELEASE_SOURCE_REF, releaseScope: environment.QL3_RELEASE_SCOPE, repositoryOwner: environment.QL3_RELEASE_REPOSITORY_OWNER, sourceRepository: environment.QL3_RELEASE_SOURCE_REPOSITORY, }; const values = Object.values(configuration); const configured = values.filter( (value) => typeof value === 'string' && value.length > 0, ).length; if (configured === 0) { const artifacts = lockedArtifacts(); return Object.freeze({ ...artifacts, releaseAuthority: Object.freeze({ mode: 'synthetic_live_fixture', version: artifacts.report.release.version, sourceRevision: artifacts.report.release.sourceRevision, sourceRef: artifacts.report.release.sourceRef, scope: artifacts.report.release.scope, releaseSetDigest: artifacts.report.releaseSetDigest, catalogManifestDigest: artifacts.report.catalog.manifestDigest, catalogConsumptionDigest: artifacts.report.catalog.consumptionReportDigest, immutableReference: artifacts.report.catalog.immutableReference, }), }); } if (configured !== values.length) { fail('release catalog live deployment configuration is incomplete'); } return catalogLockedArtifacts(configuration, dependencies); } function writeCommand(directory, name, operation, request) { return privateFile( directory, name, canonicalJson({ schemaVersion: 1, schema: COMMAND_SCHEMA, operation, request, }), ); } async function main(environment = process.env) { const fixture = new K3sDockerLiveFixture({ prefix: 'ql3-deploy-live', kubectl: environment.QL3_KUBECTL_BIN, }); let evidence; let cleanupComplete = false; try { const nodes = await fixture.start(); fs.chmodSync(fixture.temporary, 0o700); const ceremonyDirectory = fs.realpathSync(fixture.temporary); const kubeconfig = fs.realpathSync(fixture.kubeconfig); const kubectl = executablePath(fixture.kubectlBinary); const curl = executablePath('curl'); const clusterUid = fixture .kubectl( ['get', 'namespace', 'kube-system', '-o=jsonpath={.metadata.uid}'], { capture: true, quiet: true }, ) .stdout.trim(); fixture.apply({ apiVersion: 'v1', kind: 'Namespace', metadata: { name: NAMESPACE }, }); const artifacts = deploymentArtifacts(environment); const manifestPath = privateFile( ceremonyDirectory, 'locked.yaml', artifacts.manifest, ); const reportPath = privateFile( ceremonyDirectory, 'lock.json', canonicalJson(artifacts.report), ); const common = { lockedManifest: { path: manifestPath, expectedDigest: artifacts.report.manifest.outputDigest, }, lockReport: { path: reportPath, expectedDigest: artifacts.report.lockDigest, }, kubectl: { path: kubectl, expectedDigest: digest(fs.readFileSync(kubectl)), }, kubeconfig: { path: kubeconfig, expectedDigest: digest(fs.readFileSync(kubeconfig)), }, context: CONTEXT, expectedClusterUid: clusterUid, transitionKind: 'install', expectedHead: { generation: 0, deploymentDigest: null, lockDigest: null, stateDigest: null, }, }; const preflightPath = path.join(ceremonyDirectory, 'preflight.json'); const preflightCommand = writeCommand( ceremonyDirectory, 'preflight-command.json', 'cluster.deployment.preflight', { preflightId: crypto.randomUUID(), ...common, output: preflightPath, }, ); const preflight = executeCommand(preflightCommand); const receiptPath = path.join(ceremonyDirectory, 'receipt.json'); const applyCommand = writeCommand( ceremonyDirectory, 'apply-command.json', 'cluster.deployment.apply', { mutationId: crypto.randomUUID(), preflight: { path: preflightPath, expectedDigest: preflight.preflightDigest, }, ...common, output: receiptPath, }, ); const receipt = executeCommand(applyCommand); const auditCommand = writeCommand( ceremonyDirectory, 'audit-command.json', 'cluster.deployment.receipt.audit', { applyCommand: { path: applyCommand, expectedDigest: digest(fs.readFileSync(applyCommand)), }, receipt: { path: receiptPath, expectedDigest: receipt.receiptDigest }, }, ); const audit = executeCommand(auditCommand); const installedHeadConfigMap = fixture.kubectlJson([ 'get', 'configmap', HEAD_NAME, '-n', NAMESPACE, ]); const installedHead = JSON.parse( installedHeadConfigMap.data?.[HEAD_DATA_KEY], ); if ( installedHead.phase !== 'committed' || installedHead.generation !== 1 || installedHead.stateDigest !== receipt.deploymentHead.stateDigest || installedHead.deployment?.deploymentDigest !== receipt.deploymentHead.deploymentDigest ) { fail('committed deployment head is invalid'); } const deployments = fixture.kubectlJson([ 'get', 'deployments', '-n', NAMESPACE, '--show-managed-fields=true', ]).items; if (deployments.length !== ROLE_ORDER.length) { fail('applied deployment count is invalid'); } for (const deployment of deployments) { const container = deployment.spec?.template?.spec?.containers?.[0]; if ( !Object.values(artifacts.imageReferences).includes(container?.image) ) { fail('applied immutable image authority is invalid'); } if ( !deployment.metadata?.managedFields?.some( (entry) => entry.manager === FIELD_MANAGER, ) ) { fail('server-side apply field manager is unavailable'); } } const retirementTarget = { apiVersion: 'v1', kind: 'ConfigMap', namespace: NAMESPACE, name: 'ql3-retirement-live-target', }; const retirementCommon = { lockedManifest: common.lockedManifest, lockReport: common.lockReport, kubectl: common.kubectl, curl: { path: curl, expectedDigest: digest(fs.readFileSync(curl)), }, kubeconfig: common.kubeconfig, context: CONTEXT, expectedClusterUid: clusterUid, expectedHead: { generation: receipt.deploymentHead.generation, deploymentDigest: receipt.deploymentHead.deploymentDigest, lockDigest: receipt.deploymentHead.lockDigest, stateDigest: receipt.deploymentHead.stateDigest, }, targets: [retirementTarget], }; const retirementPreflightPath = path.join( ceremonyDirectory, 'retirement-preflight.json', ); const retirementPreflightCommand = writeCommand( ceremonyDirectory, 'retirement-preflight-command.json', 'cluster.deployment.retirement.preflight', { preflightId: crypto.randomUUID(), ...retirementCommon, output: retirementPreflightPath, }, ); const retirementPreflight = executeCommand(retirementPreflightCommand); const retirementReceiptPath = path.join( ceremonyDirectory, 'retirement-receipt.json', ); const retirementApplyCommand = writeCommand( ceremonyDirectory, 'retirement-apply-command.json', 'cluster.deployment.retirement.apply', { mutationId: crypto.randomUUID(), preflight: { path: retirementPreflightPath, expectedDigest: retirementPreflight.preflightDigest, }, ...retirementCommon, output: retirementReceiptPath, }, ); const retirementReceipt = executeCommand(retirementApplyCommand); const retirementAuditCommand = writeCommand( ceremonyDirectory, 'retirement-audit-command.json', 'cluster.deployment.retirement.receipt.audit', { applyCommand: { path: retirementApplyCommand, expectedDigest: digest(fs.readFileSync(retirementApplyCommand)), }, receipt: { path: retirementReceiptPath, expectedDigest: retirementReceipt.receiptDigest, }, }, ); const retirementAudit = executeCommand(retirementAuditCommand); const retiredName = fixture .kubectl( [ 'get', 'configmap', retirementTarget.name, '-n', NAMESPACE, '--ignore-not-found=true', '-o=name', ], { capture: true, quiet: true }, ) .stdout.trim(); const deploymentHeadConfigMap = fixture.kubectlJson([ 'get', 'configmap', HEAD_NAME, '-n', NAMESPACE, ]); const deploymentHead = JSON.parse( deploymentHeadConfigMap.data?.[HEAD_DATA_KEY], ); if ( retiredName !== '' || deploymentHead.phase !== 'committed' || deploymentHead.generation !== 2 || deploymentHead.transition.kind !== 'retire' || deploymentHead.stateDigest !== retirementReceipt.deploymentHead.stateDigest || deploymentHead.deployment.resources.length !== artifacts.report.manifest.resources - 1 ) { fail('committed resource retirement head is invalid'); } const version = JSON.parse( fixture.kubectl(['version', '-o=json'], { capture: true, quiet: true, }).stdout, ); evidence = Object.freeze({ schemaVersion: 1, schema: 'qinglong/kubernetes-deployment-live-evidence@v1', kubernetes: { serverVersion: version.serverVersion.gitVersion, architecture: version.serverVersion.platform, nodeCount: nodes.length, clusterUid, }, deployment: { namespace: NAMESPACE, resourceCount: artifacts.report.manifest.resources, deploymentCount: deployments.length, replicas: 0, fieldManager: FIELD_MANAGER, immutableImages: true, headName: HEAD_NAME, headPhase: deploymentHead.phase, headGeneration: deploymentHead.generation, deploymentDigest: deploymentHead.deployment.deploymentDigest, resourceInventoryCount: deploymentHead.deployment.resources.length, }, releaseAuthority: artifacts.releaseAuthority, preflightDigest: preflight.preflightDigest, receiptDigest: receipt.receiptDigest, receiptAuditCompatible: audit.compatible, retirement: { preflightDigest: retirementPreflight.preflightDigest, receiptDigest: retirementReceipt.receiptDigest, receiptAuditCompatible: retirementAudit.compatible, targetCount: retirementReceipt.retiredResources.length, targetAbsent: retiredName === '', uidResourceVersionDeletePreconditions: retirementReceipt.verification.uidResourceVersionDeletePreconditions, deploymentHeadCas: retirementReceipt.verification.deploymentHeadCas, inventoryCount: retirementReceipt.resourceInventory.length, unixSocketProxy: retirementReceipt.verification.unixSocketProxy, }, serverSideDryRun: preflight.verification.serverSideDryRun, serverSideApply: receipt.verification.serverSideApply, convergenceRead: receipt.verification.convergenceRead, deploymentHeadCas: receipt.verification.deploymentHeadCas, resourceInventoryClosed: receipt.verification.resourceInventoryClosed, crossResourceAtomicity: receipt.verification.crossResourceAtomicity, }); } finally { await fixture.cleanup().catch(() => undefined); cleanupComplete = true; } process.stdout.write(canonicalJson({ ...evidence, cleanupComplete })); } if (require.main === module) { main().catch((error) => { process.stderr.write( `${ error instanceof Error ? error.message : 'deployment live contract failed' }\n`, ); process.exitCode = 1; }); } module.exports = Object.freeze({ catalogLockedArtifacts, deploymentArtifacts, lockedArtifacts, main, });