Files
pikasTech-unidesk/scripts/native/cicd/pac-source-artifact-runtime.mjs
T

239 lines
11 KiB
JavaScript

import { createHash } from "node:crypto";
import { execFileSync } from "node:child_process";
import { readFileSync } from "node:fs";
const provenanceKeys = {
configRef: "unidesk.ai/owning-config-ref",
effectiveConfigSha256: "unidesk.ai/effective-config-sha256",
renderer: "unidesk.ai/source-artifact-renderer",
};
const sourceCommitKeys = [
"pipelinesascode.tekton.dev/sha",
"pipelinesascode.tekton.dev/commit",
"agentrun.pikastech.local/source-commit",
"unidesk.ai/source-commit",
"hwlab.pikastech.local/source-commit",
];
function record(value) {
return value !== null && typeof value === "object" && !Array.isArray(value) ? value : {};
}
function compact(value) {
const raw = JSON.stringify(value);
if (raw === undefined) return String(value);
return raw.length <= 160 ? raw : `${raw.slice(0, 157)}...`;
}
function unavailable(reason, sourceCommit = null, candidateCount = 0) {
return {
status: "unavailable",
name: null,
canonicalSha256: null,
firstDrift: null,
provenanceAligned: false,
configRef: null,
effectiveConfigSha256: null,
sourceCommit,
reason,
candidateCount,
};
}
function missing(reason, sourceCommit = null) {
return { ...unavailable(reason, sourceCommit, 0), status: "missing" };
}
function isAdmissionDefault(path, value) {
const normalized = path.map((segment) => typeof segment === "number" ? "*" : segment).join(".");
const emptyRecord = value !== null && typeof value === "object" && !Array.isArray(value) && Object.keys(value).length === 0;
const atTaskSpec = (suffix) => [
`tasks.*.taskSpec.${suffix}`,
`spec.tasks.*.taskSpec.${suffix}`,
`spec.pipelineSpec.tasks.*.taskSpec.${suffix}`,
].includes(normalized);
if (atTaskSpec("metadata")) return emptyRecord;
if (atTaskSpec("spec")) return value === null;
if (atTaskSpec("params.*.type")) return value === "string";
if (atTaskSpec("results.*.type")) return value === "string";
if (atTaskSpec("steps.*.computeResources")) return emptyRecord;
if (atTaskSpec("sidecars.*.computeResources")) return emptyRecord;
return false;
}
export function canonicalizePacPipelineSpec(value, path = []) {
if (Array.isArray(value)) return value.map((item, index) => canonicalizePacPipelineSpec(item, [...path, index]));
if (value === null || typeof value !== "object") return value;
const output = {};
for (const key of Object.keys(value).sort()) {
const child = value[key];
const childPath = [...path, key];
if (isAdmissionDefault(childPath, child)) continue;
output[key] = canonicalizePacPipelineSpec(child, childPath);
}
return output;
}
export function canonicalPacPipelineSha256(value) {
return `sha256:${createHash("sha256").update(JSON.stringify(canonicalizePacPipelineSpec(value))).digest("hex")}`;
}
function firstCanonicalDrift(expected, actual, path = "$") {
if (Object.is(expected, actual)) return null;
if (Array.isArray(expected) || Array.isArray(actual)) {
if (!Array.isArray(expected) || !Array.isArray(actual)) return { path, expected: compact(expected), actual: compact(actual) };
if (expected.length !== actual.length) return { path: `${path}.length`, expected: String(expected.length), actual: String(actual.length) };
for (let index = 0; index < expected.length; index += 1) {
const drift = firstCanonicalDrift(expected[index], actual[index], `${path}[${index}]`);
if (drift !== null) return drift;
}
return null;
}
const expectedRecord = expected !== null && typeof expected === "object";
const actualRecord = actual !== null && typeof actual === "object";
if (expectedRecord || actualRecord) {
if (!expectedRecord || !actualRecord) return { path, expected: compact(expected), actual: compact(actual) };
const keys = [...new Set([...Object.keys(expected), ...Object.keys(actual)])].sort();
for (const key of keys) {
if (!Object.prototype.hasOwnProperty.call(expected, key) || !Object.prototype.hasOwnProperty.call(actual, key)) {
return { path: `${path}.${key}`, expected: compact(expected[key]), actual: compact(actual[key]) };
}
const drift = firstCanonicalDrift(expected[key], actual[key], `${path}.${key}`);
if (drift !== null) return drift;
}
return null;
}
return { path, expected: compact(expected), actual: compact(actual) };
}
function firstDrift(expected, actual) {
return firstCanonicalDrift(canonicalizePacPipelineSpec(expected), canonicalizePacPipelineSpec(actual));
}
function kubectlJson(args, label) {
try {
return {
kind: "ok",
value: JSON.parse(execFileSync("kubectl", args, { encoding: "utf8", timeout: 30_000, maxBuffer: 64 * 1024 * 1024 })),
};
} catch (error) {
const stderr = typeof error?.stderr === "string" ? error.stderr : Buffer.isBuffer(error?.stderr) ? error.stderr.toString("utf8") : "";
const message = stderr.trim().split(/\r?\n/u)[0] || String(error?.message ?? "command failed");
if (/\bNotFound\b|\bnot found\b/u.test(message)) return { kind: "not-found", reason: `${label}-not-found` };
const status = Number.isInteger(error?.status) ? error.status : "unknown";
return { kind: "unavailable", reason: `${label}-command-failed:${status}:${compact(message)}` };
}
}
function manifestProvenance(manifest) {
const annotations = record(record(manifest?.metadata).annotations);
return {
configRef: typeof annotations[provenanceKeys.configRef] === "string" ? annotations[provenanceKeys.configRef] : null,
effectiveConfigSha256: typeof annotations[provenanceKeys.effectiveConfigSha256] === "string" ? annotations[provenanceKeys.effectiveConfigSha256] : null,
renderer: typeof annotations[provenanceKeys.renderer] === "string" ? annotations[provenanceKeys.renderer] : null,
};
}
function manifestSourceCommits(manifest) {
const metadata = record(manifest?.metadata);
const labels = record(metadata.labels);
const annotations = record(metadata.annotations);
return [...new Set(sourceCommitKeys.flatMap((key) => [labels[key], annotations[key]])
.filter((value) => typeof value === "string" && /^[0-9a-f]{40}$/iu.test(value))
.map((value) => value.toLowerCase()))];
}
function observedItem(input, manifest, spec, sourceCommit = null, candidateCount = 0) {
const observedProvenance = manifestProvenance(manifest);
const provenanceAligned = observedProvenance.configRef === input.provenance.configRef
&& observedProvenance.effectiveConfigSha256 === input.provenance.effectiveConfigSha256
&& observedProvenance.renderer === input.provenance.renderer;
const drift = firstDrift(input.desiredSpec, spec);
return {
status: drift === null ? "aligned" : "drifted",
name: typeof record(manifest.metadata).name === "string" ? record(manifest.metadata).name : null,
canonicalSha256: canonicalPacPipelineSha256(spec),
firstDrift: drift,
provenanceAligned,
configRef: observedProvenance.configRef,
effectiveConfigSha256: observedProvenance.effectiveConfigSha256,
sourceCommit,
reason: null,
candidateCount,
};
}
export function selectPipelineRunBySourceCommit(items, pipelineRunPrefix, sourceCommit) {
if (sourceCommit === null) return { kind: "unavailable", reason: "source-commit-not-specified", run: null, candidates: [] };
const candidates = items.filter((run) => {
const name = String(record(run?.metadata).name ?? "");
return name.startsWith(pipelineRunPrefix) && manifestSourceCommits(run).includes(sourceCommit);
});
if (candidates.length === 0) return { kind: "missing", reason: "source-commit-run-not-found", run: null, candidates: [] };
if (candidates.some((run) => {
const commits = manifestSourceCommits(run);
return commits.length !== 1 || commits[0] !== sourceCommit;
})) return { kind: "unavailable", reason: "source-commit-identity-conflict", run: null, candidates };
const ordered = [...candidates].sort((left, right) => {
const time = String(record(right?.metadata).creationTimestamp ?? "").localeCompare(String(record(left?.metadata).creationTimestamp ?? ""));
return time !== 0 ? time : String(record(right?.metadata).name ?? "").localeCompare(String(record(left?.metadata).name ?? ""));
});
return { kind: "ok", reason: null, run: ordered[0], candidates: ordered };
}
export function observePacSourceArtifactRuntime(input, readKubectlJson = kubectlJson) {
const liveResult = readKubectlJson(["get", "pipeline", input.pipeline, "-n", input.namespace, "-o", "json"], "pipeline-get");
const live = liveResult.kind === "ok"
? observedItem(input, liveResult.value, record(liveResult.value.spec))
: liveResult.kind === "not-found"
? missing(liveResult.reason)
: unavailable(liveResult.reason);
let embedded;
if (input.sourceCommit === null) {
embedded = unavailable("source-commit-not-specified");
} else {
const runsResult = readKubectlJson(["get", "pipelinerun", "-n", input.namespace, "-o", "json"], "pipelinerun-list");
if (runsResult.kind !== "ok") {
embedded = runsResult.kind === "not-found" ? missing(runsResult.reason, input.sourceCommit) : unavailable(runsResult.reason, input.sourceCommit);
} else {
const items = Array.isArray(runsResult.value?.items) ? runsResult.value.items : null;
if (items === null) {
embedded = unavailable("pipelinerun-list-invalid-shape", input.sourceCommit);
} else {
const selected = selectPipelineRunBySourceCommit(items, input.pipelineRunPrefix, input.sourceCommit);
if (selected.kind === "ok") {
const signatures = selected.candidates.map((run) => JSON.stringify({
canonicalSha256: canonicalPacPipelineSha256(record(record(run.spec).pipelineSpec)),
provenance: manifestProvenance(run),
}));
embedded = new Set(signatures).size === 1
? observedItem(input, selected.run, record(record(selected.run.spec).pipelineSpec), input.sourceCommit, selected.candidates.length)
: unavailable(`source-commit-run-conflict:${selected.candidates.length}`, input.sourceCommit, selected.candidates.length);
} else {
embedded = selected.kind === "missing"
? missing(selected.reason, input.sourceCommit)
: unavailable(selected.reason, input.sourceCommit, selected.candidates.length);
}
}
}
}
return { live, embedded };
}
if (typeof process.env.PAC_SOURCE_ARTIFACT_INPUT_FILE === "string" || typeof process.env.PAC_SOURCE_ARTIFACT_INPUT === "string") {
try {
const inputText = typeof process.env.PAC_SOURCE_ARTIFACT_INPUT_FILE === "string"
? readFileSync(process.env.PAC_SOURCE_ARTIFACT_INPUT_FILE, "utf8")
: Buffer.from(process.env.PAC_SOURCE_ARTIFACT_INPUT, "base64").toString("utf8");
const input = JSON.parse(inputText);
process.stdout.write(JSON.stringify(observePacSourceArtifactRuntime(input)));
} catch (error) {
process.stdout.write(JSON.stringify({
live: unavailable(`runtime-observer-input-error:${compact(String(error?.message ?? error))}`),
embedded: unavailable(`runtime-observer-input-error:${compact(String(error?.message ?? error))}`),
}));
}
}