Files
qinglong/scripts/ql3-kubernetes-deployment-live-contract.cjs
T

378 lines
11 KiB
JavaScript

#!/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,
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 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,
'/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('d341-live-release-set');
const catalogManifestDigest = digest('d341-live-catalog-manifest');
const catalogReportDigest = digest('d341-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 },
},
];
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('d341-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 writeCommand(directory, name, operation, request) {
return privateFile(
directory,
name,
canonicalJson({
schemaVersion: 1,
schema: COMMAND_SCHEMA,
operation,
request,
}),
);
}
async function main() {
const fixture = new K3sDockerLiveFixture({
prefix: 'ql3-deploy-live',
kubectl: process.env.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 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 = lockedArtifacts();
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,
};
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 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 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,
},
preflightDigest: preflight.preflightDigest,
receiptDigest: receipt.receiptDigest,
receiptAuditCompatible: audit.compatible,
serverSideDryRun: preflight.verification.serverSideDryRun,
serverSideApply: receipt.verification.serverSideApply,
convergenceRead: receipt.verification.convergenceRead,
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({ lockedArtifacts, main });