import { readFileSync } from "node:fs"; import { rootPath } from "./config"; export type DeployJsonEnvironment = "dev" | "prod"; export interface DeployJsonArtifactContract { kind: "source-build"; repository: string; tag: "commitId"; } export interface DeployJsonConsumerTargetContract { namespace: string; deployment: string; service: string; containerName: string; stableImage: string; manifestRepoPath: string; } export interface DeployJsonConsumerContract { kind: "d601-k3s-managed"; dev?: { enabled: boolean }; prod?: { enabled: boolean }; supportLevel: "reviewed"; targetRef: string; noRuntimeSourceBuild: boolean; target: DeployJsonConsumerTargetContract; } export interface DeployJsonRuntimeContract { containerPort: number; healthPath: string; memory: { request: string; limit: string; }; health?: { deployMetadataRequired: boolean; }; } export interface DeployJsonServiceContract { id: string; repo: string; commitId: string; artifact?: DeployJsonArtifactContract; consumer?: DeployJsonConsumerContract; runtime?: DeployJsonRuntimeContract; } export interface DeployJsonDriftItem { surface: string; path?: string; field: string; expected: unknown; actual: unknown; } export interface DeployJsonExecutorMirror { surface: string; path?: string; artifact?: { kind?: string; repository?: string; tag?: string; }; consumer?: { kind?: string; noRuntimeSourceBuild?: boolean; target?: Partial; }; runtime?: { containerPort?: number; servicePort?: number; healthPath?: string; memory?: { request?: string; limit?: string; }; health?: { deployMetadataRequired?: boolean; }; }; } function asRecord(value: unknown, path: string): Record { if (typeof value !== "object" || value === null || Array.isArray(value)) throw new Error(`${path} must be an object`); return value as Record; } function optionalRecord(value: unknown, path: string): Record | undefined { if (value === undefined) return undefined; return asRecord(value, path); } function stringField(record: Record, key: string, path: string): string { const value = record[key]; if (typeof value !== "string" || value.length === 0) throw new Error(`${path}.${key} must be a non-empty string`); return value; } function numberField(record: Record, key: string, path: string): number { const value = record[key]; if (typeof value !== "number" || !Number.isInteger(value) || value <= 0) throw new Error(`${path}.${key} must be a positive integer`); return value; } function booleanField(record: Record, key: string, path: string): boolean { const value = record[key]; if (typeof value !== "boolean") throw new Error(`${path}.${key} must be a boolean`); return value; } function assertKnownKeys(record: Record, allowed: string[], path: string): void { const unknown = Object.keys(record).filter((key) => !allowed.includes(key)); if (unknown.length > 0) throw new Error(`${path} contains unsupported deploy.json contract key(s): ${unknown.join(", ")}`); } function parseArtifactContract(value: unknown, path: string): DeployJsonArtifactContract | undefined { const artifact = optionalRecord(value, path); if (artifact === undefined) return undefined; assertKnownKeys(artifact, ["kind", "repository", "tag"], path); const kind = stringField(artifact, "kind", path); if (kind !== "source-build") throw new Error(`${path}.kind must be source-build`); const tag = stringField(artifact, "tag", path); if (tag !== "commitId") throw new Error(`${path}.tag must be commitId`); return { kind, repository: stringField(artifact, "repository", path), tag, }; } function parseEnabled(value: unknown, path: string): { enabled: boolean } | undefined { const record = optionalRecord(value, path); if (record === undefined) return undefined; assertKnownKeys(record, ["enabled"], path); return { enabled: booleanField(record, "enabled", path) }; } function parseConsumerTarget(value: unknown, path: string): DeployJsonConsumerTargetContract { const target = asRecord(value, path); assertKnownKeys(target, ["namespace", "deployment", "service", "containerName", "stableImage", "manifestRepoPath"], path); return { namespace: stringField(target, "namespace", path), deployment: stringField(target, "deployment", path), service: stringField(target, "service", path), containerName: stringField(target, "containerName", path), stableImage: stringField(target, "stableImage", path), manifestRepoPath: stringField(target, "manifestRepoPath", path), }; } function parseConsumerContract(value: unknown, path: string): DeployJsonConsumerContract | undefined { const consumer = optionalRecord(value, path); if (consumer === undefined) return undefined; assertKnownKeys(consumer, ["kind", "dev", "prod", "supportLevel", "targetRef", "noRuntimeSourceBuild", "target"], path); const kind = stringField(consumer, "kind", path); if (kind !== "d601-k3s-managed") throw new Error(`${path}.kind must be d601-k3s-managed`); const supportLevel = stringField(consumer, "supportLevel", path); if (supportLevel !== "reviewed") throw new Error(`${path}.supportLevel must be reviewed`); return { kind, dev: parseEnabled(consumer.dev, `${path}.dev`), prod: parseEnabled(consumer.prod, `${path}.prod`), supportLevel, targetRef: stringField(consumer, "targetRef", path), noRuntimeSourceBuild: booleanField(consumer, "noRuntimeSourceBuild", path), target: parseConsumerTarget(consumer.target, `${path}.target`), }; } function parseRuntimeContract(value: unknown, path: string): DeployJsonRuntimeContract | undefined { const runtime = optionalRecord(value, path); if (runtime === undefined) return undefined; assertKnownKeys(runtime, ["containerPort", "healthPath", "memory", "health"], path); const memory = asRecord(runtime.memory, `${path}.memory`); assertKnownKeys(memory, ["request", "limit"], `${path}.memory`); const health = optionalRecord(runtime.health, `${path}.health`); if (health !== undefined) assertKnownKeys(health, ["deployMetadataRequired"], `${path}.health`); return { containerPort: numberField(runtime, "containerPort", path), healthPath: stringField(runtime, "healthPath", path), memory: { request: stringField(memory, "request", `${path}.memory`), limit: stringField(memory, "limit", `${path}.memory`), }, health: health === undefined ? undefined : { deployMetadataRequired: booleanField(health, "deployMetadataRequired", `${path}.health`), }, }; } export function parseDeployJsonServiceContract(value: unknown, path: string): DeployJsonServiceContract { const service = asRecord(value, path); assertKnownKeys(service, ["id", "repo", "commitId", "artifact", "consumer", "runtime"], path); const id = stringField(service, "id", path); const repo = stringField(service, "repo", path); const commitId = stringField(service, "commitId", path).toLowerCase(); if (!/^[0-9a-f]{7,40}$/iu.test(commitId)) throw new Error(`${path}.commitId must be a 7-40 char git SHA`); return { id, repo, commitId, artifact: parseArtifactContract(service.artifact, `${path}.artifact`), consumer: parseConsumerContract(service.consumer, `${path}.consumer`), runtime: parseRuntimeContract(service.runtime, `${path}.runtime`), }; } export function parseDeployJsonServiceContractBase64(raw: string): DeployJsonServiceContract { const text = Buffer.from(raw, "base64").toString("utf8"); return parseDeployJsonServiceContract(JSON.parse(text) as unknown, "deploy service contract"); } export function encodeDeployJsonServiceContract(service: DeployJsonServiceContract): string { return Buffer.from(JSON.stringify(service), "utf8").toString("base64"); } export function hasDeployJsonExecutorContract(service: DeployJsonServiceContract): boolean { return service.artifact !== undefined || service.consumer !== undefined || service.runtime !== undefined; } export function deployJsonCommitImage(stableImage: string, commit: string): string { const lastSlash = stableImage.lastIndexOf("/"); const tagIndex = stableImage.indexOf(":", lastSlash + 1); if (tagIndex === -1) return `${stableImage}:${commit}`; return `${stableImage.slice(0, tagIndex)}:${commit}`; } export function deployJsonSourceOfTruth(service: DeployJsonServiceContract, environment: DeployJsonEnvironment): Record { return { source: "deploy.json", environment, serviceId: service.id, servicePath: `environments.${environment}.services.${service.id}`, ownedFields: [ ...(service.artifact === undefined ? [] : ["artifact.kind", "artifact.repository", "artifact.tag"]), ...(service.consumer === undefined ? [] : [ "consumer.kind", `consumer.${environment}.enabled`, "consumer.supportLevel", "consumer.targetRef", "consumer.noRuntimeSourceBuild", "consumer.target.namespace", "consumer.target.deployment", "consumer.target.service", "consumer.target.containerName", "consumer.target.stableImage", "consumer.target.manifestRepoPath", ]), ...(service.runtime === undefined ? [] : [ "runtime.containerPort", "runtime.healthPath", "runtime.memory.request", "runtime.memory.limit", "runtime.health.deployMetadataRequired", ]), ], }; } function pushDrift(items: DeployJsonDriftItem[], mirror: DeployJsonExecutorMirror, field: string, expected: unknown, actual: unknown): void { if (actual === undefined || expected === actual) return; items.push({ surface: mirror.surface, path: mirror.path, field, expected, actual }); } export function compareDeployJsonExecutorMirrors( service: DeployJsonServiceContract, _environment: DeployJsonEnvironment, mirrors: DeployJsonExecutorMirror[], ): DeployJsonDriftItem[] { const items: DeployJsonDriftItem[] = []; for (const mirror of mirrors) { if (service.artifact !== undefined && mirror.artifact !== undefined) { pushDrift(items, mirror, "artifact.kind", service.artifact.kind, mirror.artifact.kind); pushDrift(items, mirror, "artifact.repository", service.artifact.repository, mirror.artifact.repository); pushDrift(items, mirror, "artifact.tag", service.artifact.tag, mirror.artifact.tag); } if (service.consumer !== undefined && mirror.consumer !== undefined) { pushDrift(items, mirror, "consumer.kind", service.consumer.kind, mirror.consumer.kind); pushDrift(items, mirror, "consumer.noRuntimeSourceBuild", service.consumer.noRuntimeSourceBuild, mirror.consumer.noRuntimeSourceBuild); const expectedTarget = service.consumer.target; const actualTarget = mirror.consumer.target; if (actualTarget !== undefined) { pushDrift(items, mirror, "consumer.target.namespace", expectedTarget.namespace, actualTarget.namespace); pushDrift(items, mirror, "consumer.target.deployment", expectedTarget.deployment, actualTarget.deployment); pushDrift(items, mirror, "consumer.target.service", expectedTarget.service, actualTarget.service); pushDrift(items, mirror, "consumer.target.containerName", expectedTarget.containerName, actualTarget.containerName); pushDrift(items, mirror, "consumer.target.stableImage", expectedTarget.stableImage, actualTarget.stableImage); pushDrift(items, mirror, "consumer.target.manifestRepoPath", expectedTarget.manifestRepoPath, actualTarget.manifestRepoPath); } } if (service.runtime !== undefined && mirror.runtime !== undefined) { pushDrift(items, mirror, "runtime.containerPort", service.runtime.containerPort, mirror.runtime.containerPort); pushDrift(items, mirror, "runtime.servicePort", service.runtime.containerPort, mirror.runtime.servicePort); pushDrift(items, mirror, "runtime.healthPath", service.runtime.healthPath, mirror.runtime.healthPath); pushDrift(items, mirror, "runtime.memory.request", service.runtime.memory.request, mirror.runtime.memory?.request); pushDrift(items, mirror, "runtime.memory.limit", service.runtime.memory.limit, mirror.runtime.memory?.limit); if (service.runtime.health !== undefined) { pushDrift(items, mirror, "runtime.health.deployMetadataRequired", service.runtime.health.deployMetadataRequired, mirror.runtime.health?.deployMetadataRequired); } } } return items; } function yamlDocuments(text: string): string[] { return text.split(/^---\s*$/mu).map((part) => part.trim()).filter((part) => part.length > 0); } function yamlKind(documentText: string): string { return documentText.match(/^kind:\s*("?)([^"\s#]+)\1\s*$/mu)?.[2] ?? ""; } function yamlMetadataField(documentText: string, field: "name" | "namespace"): string { const metadataIndex = documentText.search(/^metadata:\s*$/mu); if (metadataIndex === -1) return ""; const metadata = documentText.slice(metadataIndex); return metadata.match(new RegExp(`^ ${field}:\\s*("?)([^"\\n#]+)\\1\\s*$`, "mu"))?.[2]?.trim() ?? ""; } function yamlDocumentByKindName(documents: string[], kind: string, name: string): string { return documents.find((documentText) => yamlKind(documentText) === kind && yamlMetadataField(documentText, "name") === name) ?? ""; } function firstRegex(text: string, pattern: RegExp): string | undefined { return text.match(pattern)?.[1]; } function containerSegment(deploymentText: string, containerName: string): string { const escaped = containerName.replace(/[.*+?^${}()|[\]\\]/gu, "\\$&"); const match = new RegExp(`^ - name:\\s*${escaped}\\s*$`, "mu").exec(deploymentText); if (match === null) return ""; const start = match.index; const rest = deploymentText.slice(start + match[0].length); const next = /^ - name:\s*\S+\s*$/mu.exec(rest); return deploymentText.slice(start, next === null ? undefined : start + match[0].length + next.index); } export function k3sManifestExecutorMirror(service: DeployJsonServiceContract): DeployJsonExecutorMirror | null { const target = service.consumer?.target; if (service.consumer?.kind !== "d601-k3s-managed" || target === undefined) return null; const path = target.manifestRepoPath; const manifest = readFileSync(rootPath(path), "utf8"); const documents = yamlDocuments(manifest); const deployment = yamlDocumentByKindName(documents, "Deployment", target.deployment); const serviceDoc = yamlDocumentByKindName(documents, "Service", target.service); const container = containerSegment(deployment, target.containerName); const containerPortRaw = firstRegex(container, /^\s+containerPort:\s*(\d+)\s*$/mu); const servicePortRaw = firstRegex(serviceDoc, /^\s+port:\s*(\d+)\s*$/mu); return { surface: "k8s-manifest", path, consumer: { kind: "d601-k3s-managed", noRuntimeSourceBuild: true, target: { namespace: yamlMetadataField(deployment, "namespace") || yamlMetadataField(serviceDoc, "namespace"), deployment: yamlMetadataField(deployment, "name"), service: yamlMetadataField(serviceDoc, "name"), containerName: firstRegex(container, /^ - name:\s*([^\s#]+)\s*$/mu), manifestRepoPath: path, }, }, runtime: { containerPort: containerPortRaw === undefined ? undefined : Number(containerPortRaw), servicePort: servicePortRaw === undefined ? undefined : Number(servicePortRaw), healthPath: firstRegex(container, /^\s+path:\s*([^\s#"]+)\s*$/mu), memory: { request: firstRegex(container, /^\s+requests:\s*$[\s\S]*?^\s+memory:\s*([^\s#"]+)\s*$/mu), limit: firstRegex(container, /^\s+limits:\s*$[\s\S]*?^\s+memory:\s*([^\s#"]+)\s*$/mu), }, health: { deployMetadataRequired: [ "UNIDESK_DEPLOY_SERVICE_ID", "UNIDESK_DEPLOY_REPO", "UNIDESK_DEPLOY_COMMIT", "UNIDESK_DEPLOY_REQUESTED_COMMIT", ].every((name) => container.includes(`name: ${name}`)), }, }, }; } export function deployJsonDriftResult( service: DeployJsonServiceContract, environment: DeployJsonEnvironment, drifts: DeployJsonDriftItem[], ): Record { return { ok: false, supported: false, error: "deploy-json-drift", serviceId: service.id, environment, sourceOfTruth: deployJsonSourceOfTruth(service, environment), drift: { ok: false, count: drifts.length, items: drifts, }, policy: "deploy.json is the only source for this executor contract; mirrored manifest, artifact-registry, CI catalog or config values must be regenerated or corrected before dry-run can proceed.", }; } export function readDeployJsonServiceContractFromFile( environment: DeployJsonEnvironment, serviceId: string, filePath = rootPath("deploy.json"), ): DeployJsonServiceContract | null { const root = asRecord(JSON.parse(readFileSync(filePath, "utf8")) as unknown, "deploy.json"); const environments = asRecord(root.environments, "deploy.json.environments"); const environmentRecord = asRecord(environments[environment], `deploy.json.environments.${environment}`); const services = environmentRecord.services; if (!Array.isArray(services)) throw new Error(`deploy.json.environments.${environment}.services must be an array`); const index = services.findIndex((item) => asRecord(item, `deploy.json.environments.${environment}.services[]`).id === serviceId); if (index === -1) return null; return parseDeployJsonServiceContract(services[index], `deploy.json.environments.${environment}.services[${index}]`); }