fix: 统一 HWLAB PaC Pipeline provenance 合同
This commit is contained in:
@@ -559,6 +559,12 @@ export interface HwlabRuntimeSourceWorkspaceHostDependenciesSpec {
|
||||
};
|
||||
}
|
||||
|
||||
export interface HwlabRuntimePipelineProvenanceSpec {
|
||||
readonly configRef: string;
|
||||
readonly renderer: "hwlab-runtime-lane";
|
||||
readonly mode: "remote-pipeline-annotation";
|
||||
}
|
||||
|
||||
export interface HwlabRuntimeLaneSpec {
|
||||
readonly lane: HwlabRuntimeLane;
|
||||
readonly nodeId: string;
|
||||
@@ -578,6 +584,7 @@ export interface HwlabRuntimeLaneSpec {
|
||||
readonly pipelineRunPrefix: string;
|
||||
readonly serviceAccountName: string;
|
||||
readonly controlPlaneFieldManager: string;
|
||||
readonly pipelineProvenance?: HwlabRuntimePipelineProvenanceSpec;
|
||||
readonly gitUrl: string;
|
||||
readonly gitReadUrl: string;
|
||||
readonly gitWriteUrl: string;
|
||||
@@ -647,6 +654,7 @@ interface HwlabLaneConfig {
|
||||
readonly pipelineRunPrefix: string;
|
||||
readonly serviceAccountName: string;
|
||||
readonly controlPlaneFieldManager: string;
|
||||
readonly pipelineProvenance?: HwlabRuntimePipelineProvenanceSpec;
|
||||
readonly git: { readonly url: string; readonly readUrl: string; readonly writeUrl: string };
|
||||
readonly argo: { readonly repoURL?: string };
|
||||
readonly gitopsBranch: string;
|
||||
@@ -886,6 +894,7 @@ function laneConfig(id: HwlabRuntimeLane, raw: Record<string, unknown>): HwlabLa
|
||||
pipelineRunPrefix: stringField(raw, "pipelineRunPrefix", `lanes.${id}`),
|
||||
serviceAccountName: stringField(raw, "serviceAccountName", `lanes.${id}`),
|
||||
controlPlaneFieldManager: stringField(raw, "controlPlaneFieldManager", `lanes.${id}`),
|
||||
pipelineProvenance: pipelineProvenanceConfig(raw.pipelineProvenance, `lanes.${id}.pipelineProvenance`),
|
||||
git: {
|
||||
url: stringField(git, "url", `lanes.${id}.git`),
|
||||
readUrl: stringField(git, "readUrl", `lanes.${id}.git`),
|
||||
@@ -928,6 +937,8 @@ function laneConfig(id: HwlabRuntimeLane, raw: Record<string, unknown>): HwlabLa
|
||||
}
|
||||
|
||||
function laneTargetConfig(id: HwlabRuntimeLane, nodeId: string, baseRaw: Record<string, unknown>, targetRaw: Record<string, unknown>): HwlabLaneConfig {
|
||||
const expectedConfigRef = `${HWLAB_NODE_LANE_CONFIG_PATH}#lanes.${id}.targets.${nodeId}`;
|
||||
parseHwlabRuntimePipelineProvenance(targetRaw.pipelineProvenance, `lanes.${id}.targets.${nodeId}.pipelineProvenance`, expectedConfigRef);
|
||||
const merged: Record<string, unknown> = {
|
||||
...baseRaw,
|
||||
...targetRaw,
|
||||
@@ -952,7 +963,34 @@ function laneTargetConfig(id: HwlabRuntimeLane, nodeId: string, baseRaw: Record<
|
||||
};
|
||||
delete merged.targets;
|
||||
delete merged.activeTarget;
|
||||
return laneConfig(id, merged);
|
||||
const config = laneConfig(id, merged);
|
||||
return config;
|
||||
}
|
||||
|
||||
function pipelineProvenanceConfig(value: unknown, path: string): HwlabRuntimePipelineProvenanceSpec | undefined {
|
||||
if (value === undefined) return undefined;
|
||||
return parseHwlabRuntimePipelineProvenance(value, path);
|
||||
}
|
||||
|
||||
export function parseHwlabRuntimePipelineProvenance(
|
||||
value: unknown,
|
||||
path: string,
|
||||
expectedConfigRef?: string,
|
||||
): HwlabRuntimePipelineProvenanceSpec {
|
||||
if (value === undefined) throw new Error(`${path} must be declared by the target owner`);
|
||||
const raw = asRecord(value, path);
|
||||
const configRef = stringField(raw, "configRef", path);
|
||||
if (!/^config\/hwlab-node-lanes\.yaml#lanes\.[A-Za-z0-9._-]+\.targets\.[A-Za-z0-9._-]+$/u.test(configRef)) {
|
||||
throw new Error(`${path}.configRef must select a target owner in ${HWLAB_NODE_LANE_CONFIG_PATH}`);
|
||||
}
|
||||
if (expectedConfigRef !== undefined && configRef !== expectedConfigRef) {
|
||||
throw new Error(`${path}.configRef must equal ${expectedConfigRef}`);
|
||||
}
|
||||
return {
|
||||
configRef,
|
||||
renderer: enumStringField(raw, "renderer", path, ["hwlab-runtime-lane"]),
|
||||
mode: enumStringField(raw, "mode", path, ["remote-pipeline-annotation"]),
|
||||
};
|
||||
}
|
||||
|
||||
function sourceAuthorityConfig(value: unknown, path: string): HwlabRuntimeSourceAuthoritySpec | undefined {
|
||||
@@ -2094,6 +2132,7 @@ function buildRuntimeLaneSpec(config: HwlabLaneConfig): HwlabRuntimeLaneSpec {
|
||||
pipelineRunPrefix: config.pipelineRunPrefix,
|
||||
serviceAccountName: config.serviceAccountName,
|
||||
controlPlaneFieldManager: config.controlPlaneFieldManager,
|
||||
...(config.pipelineProvenance === undefined ? {} : { pipelineProvenance: config.pipelineProvenance }),
|
||||
gitUrl: config.git.url,
|
||||
gitReadUrl: config.git.readUrl,
|
||||
gitWriteUrl: config.git.writeUrl,
|
||||
|
||||
@@ -0,0 +1,29 @@
|
||||
import { type HwlabRuntimeLaneSpec, hwlabRuntimeLaneConfigPath } from "../hwlab-node-lanes";
|
||||
import { pipelineProvenanceAnnotations, type PipelineProvenanceContract } from "../pipeline-provenance";
|
||||
import { stableJsonSha256 } from "../stable-json";
|
||||
|
||||
export interface HwlabRuntimePipelineProvenance extends PipelineProvenanceContract {
|
||||
readonly renderer: "hwlab-runtime-lane";
|
||||
readonly mode: "remote-pipeline-annotation";
|
||||
}
|
||||
|
||||
export function hwlabRuntimePipelineProvenance(spec: HwlabRuntimeLaneSpec): HwlabRuntimePipelineProvenance {
|
||||
const declared = spec.pipelineProvenance;
|
||||
if (declared === undefined) {
|
||||
throw new Error(`${hwlabRuntimeLaneConfigPath()}#lanes.${spec.lane}.targets.${spec.nodeId}.pipelineProvenance is required`);
|
||||
}
|
||||
const expectedConfigRef = `${hwlabRuntimeLaneConfigPath()}#lanes.${spec.lane}.targets.${spec.nodeId}`;
|
||||
if (declared.configRef !== expectedConfigRef) {
|
||||
throw new Error(`pipelineProvenance.configRef must equal selected target owner ${expectedConfigRef}`);
|
||||
}
|
||||
return {
|
||||
configRef: declared.configRef,
|
||||
effectiveConfigSha256: stableJsonSha256(spec),
|
||||
renderer: declared.renderer,
|
||||
mode: declared.mode,
|
||||
};
|
||||
}
|
||||
|
||||
export function hwlabRuntimePipelineProvenanceAnnotations(spec: HwlabRuntimeLaneSpec): Record<string, string> {
|
||||
return pipelineProvenanceAnnotations(hwlabRuntimePipelineProvenance(spec));
|
||||
}
|
||||
@@ -41,11 +41,13 @@ import { webObserveShort, webObserveText } from "./web-probe-observe";
|
||||
import { readNodeRuntimeStatusPolicy, runNodeRuntimeStatusProbe, skippedNodeRuntimeStatusCommand, type NodeRuntimeStatusPolicy, type NodeRuntimeStatusWorkerEvent } from "./control-plane-status-observed";
|
||||
import { hwlabRuntimeActiveExternalPostgres } from "../hwlab-node-lanes";
|
||||
import { nodeRuntimeDeployYamlOverlayShellScript } from "./deploy-overlay";
|
||||
import { pipelineProvenanceAnnotationKeys } from "../pipeline-provenance";
|
||||
|
||||
const runtimeGitopsObservabilityNativeScript = readFileSync(rootPath("scripts/native/hwlab/runtime-gitops-observability.mjs"), "utf8").trimEnd();
|
||||
const runtimeGitopsPipelineGuardNativeScript = readFileSync(rootPath("scripts/native/hwlab/runtime-gitops-pipeline-guard.mjs"), "utf8").trimEnd();
|
||||
const runtimeGitopsPostprocessNativeScript = readFileSync(rootPath("scripts/native/hwlab/runtime-gitops-postprocess.mjs"), "utf8").trimEnd();
|
||||
const runtimeGitopsVerifyNativeScript = readFileSync(rootPath("scripts/native/hwlab/runtime-gitops-verify.mjs"), "utf8").trimEnd();
|
||||
const runtimePipelineProvenanceNativeScript = readFileSync(rootPath("scripts/native/hwlab/runtime-pipeline-provenance.mjs"), "utf8").trimEnd();
|
||||
|
||||
export function nodeRuntimeGitMirrorJobName(mirror: NodeRuntimeGitMirrorTargetSpec, action: "sync" | "flush"): string {
|
||||
const prefix = action === "sync" ? mirror.syncJobPrefix : mirror.flushJobPrefix;
|
||||
@@ -2521,11 +2523,6 @@ export function nodeRuntimePipelinePostprocessScript(): string[] {
|
||||
" doc.metadata = doc.metadata || {};",
|
||||
" if (typeof overlay.pipelineName === 'string' && overlay.pipelineName.length > 0) doc.metadata.name = overlay.pipelineName;",
|
||||
" doc.metadata.annotations = doc.metadata.annotations || {};",
|
||||
" const provenance = overlay.sourceArtifactProvenance || {};",
|
||||
" if (provenance.configRef) doc.metadata.annotations['unidesk.ai/owning-config-ref'] = provenance.configRef;",
|
||||
" if (provenance.effectiveConfigSha256) doc.metadata.annotations['unidesk.ai/effective-config-sha256'] = provenance.effectiveConfigSha256;",
|
||||
" if (provenance.renderer) doc.metadata.annotations['unidesk.ai/source-artifact-renderer'] = provenance.renderer;",
|
||||
" if (provenance.mode) doc.metadata.annotations['unidesk.ai/source-artifact-mode'] = provenance.mode;",
|
||||
" doc.metadata.annotations['hwlab.pikastech.local/download-profile'] = overlay.downloadProfileId;",
|
||||
" doc.metadata.annotations['hwlab.pikastech.local/network-profile'] = overlay.networkProfileId;",
|
||||
" for (const task of doc.spec?.tasks || []) {",
|
||||
@@ -2836,10 +2833,26 @@ export function nodeRuntimePipelinePostprocessScript(): string[] {
|
||||
"patchArgoYaml(path.join(renderDir, 'argocd', 'project.yaml'));",
|
||||
"patchArgoYaml(path.join(renderDir, 'argocd', overlay.argoApplicationFile));",
|
||||
"NODE",
|
||||
...nodeRuntimePipelineProvenancePostprocessScript(),
|
||||
...runtimeGitopsPipelineGuardScript(),
|
||||
];
|
||||
}
|
||||
|
||||
export function nodeRuntimePipelineProvenancePostprocessScript(): string[] {
|
||||
const expectedKeys = Buffer.from(JSON.stringify(Object.values(pipelineProvenanceAnnotationKeys).sort()), "utf8").toString("base64");
|
||||
return [
|
||||
"runtime_gitops_guard_dir=\"$render_dir/.unidesk-runtime-gitops\"",
|
||||
"mkdir -p \"$runtime_gitops_guard_dir\"",
|
||||
...writeRuntimeGitopsNativeScript("runtime-pipeline-provenance.mjs", runtimePipelineProvenanceNativeScript, "UNIDESK_RUNTIME_PIPELINE_PROVENANCE_MJS"),
|
||||
[
|
||||
"UNIDESK_RUNTIME_GITOPS_OVERLAY_B64=\"$overlay_b64\"",
|
||||
`UNIDESK_PIPELINE_PROVENANCE_KEYS_B64=${shellQuote(expectedKeys)}`,
|
||||
"node \"$runtime_gitops_guard_dir/runtime-pipeline-provenance.mjs\"",
|
||||
"--pipeline \"$render_dir/$(node -e 'const o=JSON.parse(Buffer.from(process.argv[1],\"base64\").toString(\"utf8\")); process.stdout.write(o.tektonDir)' \"$overlay_b64\")/pipeline.yaml\"",
|
||||
].join(" "),
|
||||
];
|
||||
}
|
||||
|
||||
function runtimeGitopsPipelineGuardScript(): string[] {
|
||||
return [
|
||||
"runtime_gitops_guard_dir=\"$render_dir/.unidesk-runtime-gitops\"",
|
||||
|
||||
@@ -13,7 +13,7 @@ import { runCommand, type CommandResult } from "../command";
|
||||
import { startJob } from "../jobs";
|
||||
import { classifySshTcpPoolFailure } from "../ssh";
|
||||
import { HWLAB_NODE_CONTROL_PLANE_CONFIG_PATH, hwlabNodeControlPlaneCiGitWorkspaceSecret, hwlabNodeControlPlaneInfraHelp, runHwlabNodeControlPlaneInfra } from "../hwlab-node-control-plane";
|
||||
import { hwlabRuntimeLaneConfigPath, hwlabRuntimeLaneIds, hwlabRuntimeLaneSpec, hwlabRuntimeLaneSpecForNode, hwlabRuntimeNodeIds, isHwlabRuntimeLane, type HwlabRuntimeLane, type HwlabRuntimeLaneSpec, type HwlabRuntimeObservabilityRecordingRuleSpec, type HwlabRuntimeObservabilitySpec, type HwlabRuntimeObservabilityWarningAlertSpec, type HwlabRuntimePublicExposureSpec, type HwlabRuntimeWebProbeAlertThresholdsSpec, type HwlabRuntimeWebProbeProjectManagementSpec } from "../hwlab-node-lanes";
|
||||
import { hwlabRuntimeLaneIds, hwlabRuntimeLaneSpec, hwlabRuntimeLaneSpecForNode, hwlabRuntimeNodeIds, isHwlabRuntimeLane, type HwlabRuntimeLane, type HwlabRuntimeLaneSpec, type HwlabRuntimeObservabilityRecordingRuleSpec, type HwlabRuntimeObservabilitySpec, type HwlabRuntimeObservabilityWarningAlertSpec, type HwlabRuntimePublicExposureSpec, type HwlabRuntimeWebProbeAlertThresholdsSpec, type HwlabRuntimeWebProbeProjectManagementSpec } from "../hwlab-node-lanes";
|
||||
import { nodeWebProbeScriptRunnerSource } from "../hwlab-node-web-probe-runner-source";
|
||||
import { nodeWebObserveAnalyzerSource } from "../hwlab-node-web-observe-analyzer-source";
|
||||
import { nodeWebObserveRunnerSource } from "../hwlab-node-web-observe-runner-source";
|
||||
@@ -40,7 +40,7 @@ import { assertKnownOptions, nodeWebProbeAutoCommandTimeoutSeconds, normalizeNod
|
||||
import { resolveNodeWebProbeCliOrigin } from "./web-probe-origin";
|
||||
import { hwlabRuntimeActiveExternalPostgres } from "../hwlab-node-lanes";
|
||||
import { resolveEgressProxySourceRef } from "../egress-proxy-sources";
|
||||
import { stableJsonSha256 } from "../stable-json";
|
||||
import { hwlabRuntimePipelineProvenanceAnnotations } from "./pipeline-provenance";
|
||||
|
||||
export function nodeRuntimeRenderOverlay(spec: HwlabRuntimeLaneSpec): Record<string, unknown> {
|
||||
const gitSshProxy = httpProxyEndpoint(spec.networkProfile.proxy.http);
|
||||
@@ -75,12 +75,7 @@ export function nodeRuntimeRenderOverlay(spec: HwlabRuntimeLaneSpec): Record<str
|
||||
},
|
||||
};
|
||||
return {
|
||||
sourceArtifactProvenance: {
|
||||
configRef: `${hwlabRuntimeLaneConfigPath}#lanes.${spec.lane}.targets.${spec.nodeId}`,
|
||||
effectiveConfigSha256: stableJsonSha256(spec),
|
||||
renderer: "hwlab-runtime-lane",
|
||||
mode: "remote-pipeline-annotation",
|
||||
},
|
||||
pipelineProvenanceAnnotations: hwlabRuntimePipelineProvenanceAnnotations(spec),
|
||||
nodeId: spec.nodeId,
|
||||
lane: spec.lane,
|
||||
sourceBranch: spec.sourceBranch,
|
||||
|
||||
@@ -0,0 +1,53 @@
|
||||
export const pipelineProvenanceAnnotationKeys = {
|
||||
configRef: "unidesk.ai/owning-config-ref",
|
||||
effectiveConfigSha256: "unidesk.ai/effective-config-sha256",
|
||||
renderer: "unidesk.ai/source-artifact-renderer",
|
||||
mode: "unidesk.ai/source-artifact-mode",
|
||||
} as const;
|
||||
|
||||
export interface PipelineProvenanceContract {
|
||||
readonly configRef: string;
|
||||
readonly effectiveConfigSha256: string;
|
||||
readonly renderer: string;
|
||||
readonly mode: string;
|
||||
}
|
||||
|
||||
export function pipelineProvenanceAnnotations(provenance: PipelineProvenanceContract): Record<string, string> {
|
||||
return {
|
||||
[pipelineProvenanceAnnotationKeys.configRef]: provenance.configRef,
|
||||
[pipelineProvenanceAnnotationKeys.effectiveConfigSha256]: provenance.effectiveConfigSha256,
|
||||
[pipelineProvenanceAnnotationKeys.renderer]: provenance.renderer,
|
||||
[pipelineProvenanceAnnotationKeys.mode]: provenance.mode,
|
||||
};
|
||||
}
|
||||
|
||||
export function withPipelineProvenanceAnnotations(
|
||||
manifest: Record<string, unknown>,
|
||||
provenance: PipelineProvenanceContract,
|
||||
): Record<string, unknown> {
|
||||
const next = structuredClone(manifest);
|
||||
const metadata = asRecord(next.metadata, "manifest.metadata");
|
||||
const annotations = metadata.annotations === undefined ? {} : asRecord(metadata.annotations, "manifest.metadata.annotations");
|
||||
metadata.annotations = { ...annotations, ...pipelineProvenanceAnnotations(provenance) };
|
||||
return next;
|
||||
}
|
||||
|
||||
export function pipelineProvenanceFromManifest(manifest: Record<string, unknown>): PipelineProvenanceContract | null {
|
||||
const metadata = isRecord(manifest.metadata) ? manifest.metadata : {};
|
||||
const annotations = isRecord(metadata.annotations) ? metadata.annotations : {};
|
||||
const configRef = annotations[pipelineProvenanceAnnotationKeys.configRef];
|
||||
const effectiveConfigSha256 = annotations[pipelineProvenanceAnnotationKeys.effectiveConfigSha256];
|
||||
const renderer = annotations[pipelineProvenanceAnnotationKeys.renderer];
|
||||
const mode = annotations[pipelineProvenanceAnnotationKeys.mode];
|
||||
if ([configRef, effectiveConfigSha256, renderer, mode].some((value) => typeof value !== "string" || value.length === 0)) return null;
|
||||
return { configRef, effectiveConfigSha256, renderer, mode } as PipelineProvenanceContract;
|
||||
}
|
||||
|
||||
function asRecord(value: unknown, path: string): Record<string, unknown> {
|
||||
if (!isRecord(value)) throw new Error(`${path} must be an object`);
|
||||
return value;
|
||||
}
|
||||
|
||||
function isRecord(value: unknown): value is Record<string, unknown> {
|
||||
return value !== null && typeof value === "object" && !Array.isArray(value);
|
||||
}
|
||||
@@ -6,6 +6,11 @@ import { resolve } from "node:path";
|
||||
|
||||
import { rootPath } from "./config";
|
||||
import { applyNodeRuntimeDeployYamlOverlay, nodeRuntimeDeployYamlOverlayShellScript } from "./hwlab-node/deploy-overlay";
|
||||
import { hwlabRuntimePipelineProvenance } from "./hwlab-node/pipeline-provenance";
|
||||
import { nodeRuntimePipelineProvenancePostprocessScript } from "./hwlab-node/render";
|
||||
import { nodeRuntimeRenderOverlay } from "./hwlab-node/web-probe";
|
||||
import { hwlabRuntimeLaneSpecForNode, parseHwlabRuntimePipelineProvenance } from "./hwlab-node-lanes";
|
||||
import { pipelineProvenanceAnnotationKeys, pipelineProvenanceAnnotations } from "./pipeline-provenance";
|
||||
import {
|
||||
assertPacSourceArtifactVerifyWorktree,
|
||||
canonicalSha256,
|
||||
@@ -206,6 +211,74 @@ describe("PaC source artifact CLI contract", () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe("HWLAB YAML-owned Pipeline provenance", () => {
|
||||
const ownerPath = "lanes.v03.targets.NC01.pipelineProvenance";
|
||||
const configRef = "config/hwlab-node-lanes.yaml#lanes.v03.targets.NC01";
|
||||
|
||||
test("target owner parser fails closed for missing or inconsistent contracts", () => {
|
||||
expect(() => parseHwlabRuntimePipelineProvenance(undefined, ownerPath, configRef)).toThrow("must be declared by the target owner");
|
||||
expect(() => parseHwlabRuntimePipelineProvenance({
|
||||
configRef: "config/hwlab-node-lanes.yaml#lanes.v03.targets.JD01",
|
||||
renderer: "hwlab-runtime-lane",
|
||||
mode: "remote-pipeline-annotation",
|
||||
}, ownerPath, configRef)).toThrow(`must equal ${configRef}`);
|
||||
expect(() => parseHwlabRuntimePipelineProvenance({
|
||||
configRef,
|
||||
renderer: "another-renderer",
|
||||
mode: "remote-pipeline-annotation",
|
||||
}, ownerPath, configRef)).toThrow("renderer must be one of hwlab-runtime-lane");
|
||||
expect(() => parseHwlabRuntimePipelineProvenance({
|
||||
configRef,
|
||||
renderer: "hwlab-runtime-lane",
|
||||
mode: "embedded-pipeline-spec",
|
||||
}, ownerPath, configRef)).toThrow("mode must be one of remote-pipeline-annotation");
|
||||
});
|
||||
|
||||
test("normal control-plane postprocess writes exactly the shared four provenance fields", () => {
|
||||
const temporary = mkdtempSync(resolve(tmpdir(), "unidesk-hwlab-pipeline-provenance-"));
|
||||
try {
|
||||
const spec = hwlabRuntimeLaneSpecForNode("v03", "NC01");
|
||||
const overlay = nodeRuntimeRenderOverlay(spec);
|
||||
const pipelineDir = resolve(temporary, spec.tektonDir);
|
||||
const pipelinePath = resolve(pipelineDir, "pipeline.yaml");
|
||||
mkdirSync(pipelineDir, { recursive: true });
|
||||
writeFileSync(pipelinePath, Bun.YAML.stringify({
|
||||
apiVersion: "tekton.dev/v1",
|
||||
kind: "Pipeline",
|
||||
metadata: { name: spec.pipeline, namespace: "hwlab-ci", annotations: { "fixture.example/keep": "true" } },
|
||||
spec: { params: [], tasks: [] },
|
||||
}));
|
||||
const overlayBase64 = Buffer.from(JSON.stringify(overlay), "utf8").toString("base64");
|
||||
const shell = [
|
||||
`render_dir=${JSON.stringify(temporary)}`,
|
||||
`overlay_b64=${JSON.stringify(overlayBase64)}`,
|
||||
...nodeRuntimePipelineProvenancePostprocessScript(),
|
||||
].join("\n");
|
||||
const result = spawnSync("sh", [], { cwd: rootPath(), input: shell, encoding: "utf8", timeout: 30_000 });
|
||||
expect(result.status).toBe(0);
|
||||
const rendered = Bun.YAML.parse(readFileSync(pipelinePath, "utf8")) as Record<string, any>;
|
||||
const annotations = rendered.metadata.annotations as Record<string, string>;
|
||||
const provenanceKeySet = new Set<string>(Object.values(pipelineProvenanceAnnotationKeys));
|
||||
const renderedProvenance = Object.fromEntries(Object.entries(annotations).filter(([key]) => provenanceKeySet.has(key)));
|
||||
expect(renderedProvenance).toEqual(overlay.pipelineProvenanceAnnotations);
|
||||
expect(Object.keys(renderedProvenance).sort()).toEqual(Object.values(pipelineProvenanceAnnotationKeys).sort());
|
||||
expect(annotations["fixture.example/keep"]).toBe("true");
|
||||
|
||||
const invalidOverlay = { ...overlay, pipelineProvenanceAnnotations: { a: "1", b: "2", c: "3", d: "4" } };
|
||||
const invalidShell = [
|
||||
`render_dir=${JSON.stringify(temporary)}`,
|
||||
`overlay_b64=${JSON.stringify(Buffer.from(JSON.stringify(invalidOverlay), "utf8").toString("base64"))}`,
|
||||
...nodeRuntimePipelineProvenancePostprocessScript(),
|
||||
].join("\n");
|
||||
const invalid = spawnSync("sh", [], { cwd: rootPath(), input: invalidShell, encoding: "utf8", timeout: 30_000 });
|
||||
expect(invalid.status).not.toBe(0);
|
||||
expect(`${invalid.stdout}\n${invalid.stderr}`).toContain("shared four-key contract");
|
||||
} finally {
|
||||
rmSync(temporary, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
describe("Tekton admission canonicalization", () => {
|
||||
const admitted = {
|
||||
tasks: [{
|
||||
@@ -381,11 +454,19 @@ describe("runtime source-commit selection", () => {
|
||||
});
|
||||
|
||||
test("remote mode validates Pipeline spec live and wrapper on the exact-commit Run", () => {
|
||||
const remoteProvenance = { ...provenance, mode: "remote-pipeline-annotation" };
|
||||
const remoteAnnotations = {
|
||||
...manifestAnnotations(),
|
||||
"unidesk.ai/source-artifact-mode": "remote-pipeline-annotation",
|
||||
};
|
||||
const spec = hwlabRuntimeLaneSpecForNode("v03", "NC01");
|
||||
const remoteProvenance = hwlabRuntimePipelineProvenance(spec);
|
||||
const overlay = nodeRuntimeRenderOverlay(spec);
|
||||
const remoteAnnotations = overlay.pipelineProvenanceAnnotations as Record<string, string>;
|
||||
expect(spec.pipelineProvenance).toEqual({
|
||||
configRef: "config/hwlab-node-lanes.yaml#lanes.v03.targets.NC01",
|
||||
renderer: "hwlab-runtime-lane",
|
||||
mode: "remote-pipeline-annotation",
|
||||
});
|
||||
expect(remoteAnnotations).toEqual(pipelineProvenanceAnnotations(remoteProvenance));
|
||||
expect(Object.keys(remoteAnnotations).sort()).toEqual(Object.values(pipelineProvenanceAnnotationKeys).sort());
|
||||
expect(readFileSync(rootPath("scripts", "src", "hwlab-node", "render.ts"), "utf8")).not.toContain("overlay.sourceArtifactProvenance");
|
||||
expect(readFileSync(rootPath("scripts", "src", "platform-infra-pipelines-as-code-source-artifact.ts"), "utf8")).not.toContain('"unidesk.ai/owning-config-ref"');
|
||||
const wrapper = {
|
||||
mode: "remote-pipeline-annotation",
|
||||
executionMode: "pipeline-spec",
|
||||
@@ -411,7 +492,9 @@ describe("runtime source-commit selection", () => {
|
||||
? { kind: "ok", value: { metadata: { name: input.pipeline, annotations: remoteAnnotations }, spec: desiredSpec } }
|
||||
: { kind: "ok", value: { items: [run] } });
|
||||
expect(observed.live.status).toBe("aligned");
|
||||
expect(observed.live.provenanceAligned).toBe(true);
|
||||
expect(observed.embedded.status).toBe("aligned");
|
||||
expect(observed.embedded.provenanceAligned).toBe(true);
|
||||
expect(observed.embedded.firstDrift).toBeNull();
|
||||
});
|
||||
|
||||
|
||||
@@ -11,10 +11,12 @@ import { hwlabRuntimeLaneSpecForNode, isHwlabRuntimeLane, type HwlabRuntimeLaneS
|
||||
import { nodeRuntimeGitopsRoot } from "./hwlab-node/cleanup";
|
||||
import { nodeRuntimePipelinePostprocessScript } from "./hwlab-node/render";
|
||||
import { nodeRuntimeRenderOverlay } from "./hwlab-node/web-probe";
|
||||
import { hwlabRuntimePipelineProvenance } from "./hwlab-node/pipeline-provenance";
|
||||
import { applyNodeRuntimeDeployYamlOverlay } from "./hwlab-node/deploy-overlay";
|
||||
import type { RenderedCliResult } from "./output";
|
||||
import { renderedCliResult } from "./agentrun/render";
|
||||
import { stableJsonSha256, stableJsonValue } from "./stable-json";
|
||||
import { pipelineProvenanceAnnotations, pipelineProvenanceFromManifest, withPipelineProvenanceAnnotations } from "./pipeline-provenance";
|
||||
|
||||
export type PacSourceArtifactMode = "embedded-pipeline-spec" | "remote-pipeline-annotation";
|
||||
export type PacSourceArtifactRenderer = "agentrun-control-plane" | "hwlab-runtime-lane";
|
||||
@@ -167,13 +169,6 @@ export function pacSourceArtifactSafeError(error: unknown): PacSourceArtifactSaf
|
||||
};
|
||||
}
|
||||
|
||||
const provenanceKeys = {
|
||||
configRef: "unidesk.ai/owning-config-ref",
|
||||
effectiveConfigSha256: "unidesk.ai/effective-config-sha256",
|
||||
renderer: "unidesk.ai/source-artifact-renderer",
|
||||
mode: "unidesk.ai/source-artifact-mode",
|
||||
} as const;
|
||||
|
||||
const knownDriftPathKeys = new Set([
|
||||
"accessModes", "annotations", "apiVersion", "args", "command", "computeResources", "default", "description", "dnsPolicy", "env", "envFrom",
|
||||
"executionMode", "finally", "fsGroup", "generateName", "hostNetwork", "image", "kind", "labels", "length", "metadata", "mode", "mountPath", "name", "namespace", "onError", "operator",
|
||||
@@ -442,7 +437,12 @@ function renderDesiredArtifact(binding: PacSourceArtifactBinding, sourceWorktree
|
||||
const rendered = sourceArtifact.renderer === "agentrun-control-plane"
|
||||
? renderAgentRunDesiredPipeline(binding)
|
||||
: renderHwlabDesiredPipeline(binding, sourceWorktree);
|
||||
const pipeline = withPipelineProvenance(rendered.pipeline, rendered.provenance);
|
||||
const pipeline = sourceArtifact.renderer === "hwlab-runtime-lane"
|
||||
? rendered.pipeline
|
||||
: withPipelineProvenanceAnnotations(rendered.pipeline, rendered.provenance);
|
||||
if (!provenanceEquals(provenanceFromManifest(pipeline), rendered.provenance)) {
|
||||
throw new Error(`${sourceArtifact.renderer} did not render the declared Pipeline provenance contract`);
|
||||
}
|
||||
const desiredSpec = requiredRecord(pipeline.spec, "rendered Pipeline.spec");
|
||||
assertPipelineIdentity(pipeline, binding);
|
||||
const pipelineRun = sourceArtifact.mode === "embedded-pipeline-spec"
|
||||
@@ -485,8 +485,11 @@ function renderAgentRunDesiredPipeline(binding: PacSourceArtifactBinding): { pip
|
||||
function renderHwlabDesiredPipeline(binding: PacSourceArtifactBinding, sourceWorktree: string): { pipeline: Record<string, unknown>; provenance: PacSourceArtifactProvenance } {
|
||||
if (!isHwlabRuntimeLane(binding.consumer.lane)) throw new Error(`HWLAB source artifact lane ${binding.consumer.lane} is not declared`);
|
||||
const spec = hwlabRuntimeLaneSpecForNode(binding.consumer.lane, binding.consumer.node);
|
||||
const expectedConfigRef = `config/hwlab-node-lanes.yaml#lanes.${spec.lane}.targets.${spec.nodeId}`;
|
||||
assertConfigRef(binding.consumer.sourceArtifact.configRef, expectedConfigRef);
|
||||
const provenance = hwlabRuntimePipelineProvenance(spec);
|
||||
if (binding.consumer.sourceArtifact.renderer !== provenance.renderer || binding.consumer.sourceArtifact.mode !== provenance.mode) {
|
||||
throw new Error(`sourceArtifact renderer/mode must match ${provenance.configRef}.pipelineProvenance`);
|
||||
}
|
||||
assertConfigRef(binding.consumer.sourceArtifact.configRef, provenance.configRef);
|
||||
assertBindingIdentity(binding, {
|
||||
node: spec.nodeId,
|
||||
lane: spec.lane,
|
||||
@@ -500,12 +503,7 @@ function renderHwlabDesiredPipeline(binding: PacSourceArtifactBinding, sourceWor
|
||||
const pipeline = renderHwlabPipelineFromOwningYaml(spec, sourceWorktree);
|
||||
return {
|
||||
pipeline,
|
||||
provenance: {
|
||||
configRef: expectedConfigRef,
|
||||
effectiveConfigSha256: stableJsonSha256(spec),
|
||||
renderer: "hwlab-runtime-lane",
|
||||
mode: binding.consumer.sourceArtifact.mode,
|
||||
},
|
||||
provenance,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -547,7 +545,7 @@ function renderHwlabPipelineFromOwningYaml(spec: HwlabRuntimeLaneSpec, sourceWor
|
||||
`overlay_b64=${shellQuote(overlay)}`,
|
||||
...nodeRuntimePipelinePostprocessScript(),
|
||||
].join("\n");
|
||||
runChecked("sh", ["-c", postprocess], temporarySource, 120_000, "HWLAB UniDesk domain postprocess/verify renderer");
|
||||
runChecked("sh", [], temporarySource, 120_000, "HWLAB UniDesk domain postprocess/verify renderer", postprocess);
|
||||
const path = resolve(output, spec.tektonDir, "pipeline.yaml");
|
||||
if (!existsSync(path)) throw new Error(`HWLAB domain renderer did not produce ${path}`);
|
||||
return parseYamlRecord(readFileSync(path, "utf8"), path);
|
||||
@@ -637,10 +635,7 @@ function pipelineRunAnnotations(binding: PacSourceArtifactBinding, provenance: P
|
||||
"pipelinesascode.tekton.dev/on-target-branch": `[${branch}]`,
|
||||
"pipelinesascode.tekton.dev/on-cel-expression": `event == 'push' && target_branch == '${branch}' && node == '${binding.consumer.node}'`,
|
||||
"pipelinesascode.tekton.dev/max-keep-runs": String(binding.consumer.sourceArtifact.maxKeepRuns),
|
||||
[provenanceKeys.configRef]: provenance.configRef,
|
||||
[provenanceKeys.effectiveConfigSha256]: provenance.effectiveConfigSha256,
|
||||
[provenanceKeys.renderer]: provenance.renderer,
|
||||
[provenanceKeys.mode]: binding.consumer.sourceArtifact.mode,
|
||||
...pipelineProvenanceAnnotations(provenance),
|
||||
};
|
||||
}
|
||||
|
||||
@@ -745,20 +740,6 @@ function pipelineRunWorkspaces(binding: PacSourceArtifactBinding, desiredSpec: R
|
||||
});
|
||||
}
|
||||
|
||||
function withPipelineProvenance(pipeline: Record<string, unknown>, provenance: PacSourceArtifactProvenance): Record<string, unknown> {
|
||||
const next = structuredClone(pipeline);
|
||||
const metadata = requiredRecord(next.metadata, "rendered Pipeline.metadata");
|
||||
const annotations = isRecord(metadata.annotations) ? metadata.annotations : {};
|
||||
metadata.annotations = {
|
||||
...annotations,
|
||||
[provenanceKeys.configRef]: provenance.configRef,
|
||||
[provenanceKeys.effectiveConfigSha256]: provenance.effectiveConfigSha256,
|
||||
[provenanceKeys.renderer]: provenance.renderer,
|
||||
[provenanceKeys.mode]: provenance.mode,
|
||||
};
|
||||
return next;
|
||||
}
|
||||
|
||||
function inspectSourceArtifact(sourceWorktree: string, binding: PacSourceArtifactBinding, desired: DesiredArtifact): SourceInspection {
|
||||
const sourceArtifact = binding.consumer.sourceArtifact;
|
||||
const pipelineRunPath = safeArtifactPath(sourceWorktree, sourceArtifact.pipelineRunPath);
|
||||
@@ -932,14 +913,11 @@ function assertPipelineIdentity(pipeline: Record<string, unknown>, binding: PacS
|
||||
}
|
||||
|
||||
function provenanceFromManifest(manifest: Record<string, unknown>): PacSourceArtifactProvenance | null {
|
||||
const metadata = isRecord(manifest.metadata) ? manifest.metadata : {};
|
||||
const annotations = isRecord(metadata.annotations) ? metadata.annotations : {};
|
||||
const configRef = annotations[provenanceKeys.configRef];
|
||||
const effectiveConfigSha256 = annotations[provenanceKeys.effectiveConfigSha256];
|
||||
const renderer = annotations[provenanceKeys.renderer];
|
||||
const mode = annotations[provenanceKeys.mode];
|
||||
if (typeof configRef !== "string" || typeof effectiveConfigSha256 !== "string" || (renderer !== "agentrun-control-plane" && renderer !== "hwlab-runtime-lane") || (mode !== "embedded-pipeline-spec" && mode !== "remote-pipeline-annotation")) return null;
|
||||
return { configRef, effectiveConfigSha256, renderer, mode };
|
||||
const provenance = pipelineProvenanceFromManifest(manifest);
|
||||
if (provenance === null) return null;
|
||||
if (provenance.renderer !== "agentrun-control-plane" && provenance.renderer !== "hwlab-runtime-lane") return null;
|
||||
if (provenance.mode !== "embedded-pipeline-spec" && provenance.mode !== "remote-pipeline-annotation") return null;
|
||||
return provenance as PacSourceArtifactProvenance;
|
||||
}
|
||||
|
||||
function provenanceEquals(actual: PacSourceArtifactProvenance | null, expected: PacSourceArtifactProvenance): boolean {
|
||||
@@ -1098,9 +1076,13 @@ function git(worktree: string, args: readonly string[]): string {
|
||||
return result.stdout.trim();
|
||||
}
|
||||
|
||||
function runChecked(command: string, args: readonly string[], cwd: string, timeout: number, label: string): void {
|
||||
const result = spawnSync(command, [...args], { cwd, encoding: "utf8", timeout, maxBuffer: 32 * 1024 * 1024 });
|
||||
if (result.status !== 0) throw new Error(`${label} failed: ${(result.stderr || result.stdout).trim().slice(-4000)}`);
|
||||
function runChecked(command: string, args: readonly string[], cwd: string, timeout: number, label: string, input?: string): void {
|
||||
const result = spawnSync(command, [...args], { cwd, encoding: "utf8", timeout, maxBuffer: 32 * 1024 * 1024, input });
|
||||
if (result.status !== 0) {
|
||||
const output = [result.stderr, result.stdout].find((value) => typeof value === "string" && value.trim().length > 0)?.trim();
|
||||
const reason = output ?? result.error?.message ?? `exit=${String(result.status)} signal=${String(result.signal)}`;
|
||||
throw new Error(`${label} failed: ${reason.slice(-4000)}`);
|
||||
}
|
||||
}
|
||||
|
||||
function shellQuote(value: string): string {
|
||||
|
||||
Reference in New Issue
Block a user