Merge pull request #1808 from pikasTech/fix/1802-pac-delivery-provenance
Pipelines as Code CI / hwlab-web-probe-sentinel-nc01- Success
Pipelines as Code CI / platform-infra-gitea-nc01- Success
Pipelines as Code CI / unidesk-host- Success

feat(cicd): 固化 PaC admission 交付溯源
This commit is contained in:
Lyon
2026-07-12 06:05:53 +08:00
committed by GitHub
12 changed files with 1311 additions and 109 deletions
@@ -11,6 +11,8 @@ metadata:
- 1560
- 1649
- 1760
- 1769
- 1802
defaults:
targetId: NC01
consumerId: hwlab-nc01-v03
@@ -23,6 +25,22 @@ release:
controllerServiceName: pipelines-as-code-controller
controllerServicePort: 8080
waitTimeoutSeconds: 55
deliveryProvenance:
version: admission-pac-v1
markerAnnotation: unidesk.ai/pac-admission-provenance
child:
enabled: false
parentNameAnnotation: unidesk.ai/pac-parent-pipelinerun
parentUidAnnotation: unidesk.ai/pac-parent-pipelinerun-uid
controller:
namespace: pipelines-as-code
serviceAccountName: pipelines-as-code-controller
admission:
policyName: unidesk-pac-delivery-provenance-v1
bindingName: unidesk-pac-delivery-provenance-v1
failurePolicy: Fail
validationActions:
- Deny
capabilities:
sentinelInternalPublish:
enabled: false
@@ -110,6 +128,13 @@ consumers:
variables:
NODE: NC01
LANE: v02
deliveryProvenance:
required: true
markerValue: admission-pac-v1:agentrun-nc01-v02
executionServiceAccountName: agentrun-nc01-v02-tekton-runner
gitOps:
repoUrl: http://git-mirror-http.devops-infra.svc.cluster.local:8080/pikasTech/agentrun.git
targetRevision: nc01-v0.2-gitops
sourceArtifact:
mode: embedded-pipeline-spec
renderer: agentrun-control-plane
+267 -9
View File
@@ -1,5 +1,7 @@
"use strict";
const { createHash } = require("node:crypto");
function record(value) {
return typeof value === "object" && value !== null && !Array.isArray(value) ? value : {};
}
@@ -37,12 +39,146 @@ function pipelineRunMatchesConsumer(consumerInput, itemInput) {
const item = record(itemInput);
const metadata = record(item.metadata);
const labels = record(metadata.labels);
const spec = record(item.spec);
const pipelineRef = record(spec.pipelineRef);
const name = stringOrNull(metadata.name) || "";
const prefix = stringOrNull(consumer.pipelineRunPrefix) || "";
const pipeline = stringOrNull(consumer.pipeline) || "";
return (prefix.length > 0 && name.startsWith(prefix))
|| (pipeline.length > 0 && labels["tekton.dev/pipeline"] === pipeline)
|| (pipeline.length > 0 && labels["tekton.dev/pipelineName"] === pipeline);
|| (pipeline.length > 0 && labels["tekton.dev/pipelineName"] === pipeline)
|| (pipeline.length > 0 && pipelineRef.name === pipeline);
}
function stableCanonicalJson(value) {
if (Array.isArray(value)) return `[${value.map(stableCanonicalJson).join(",")}]`;
if (typeof value === "object" && value !== null) {
return `{${Object.entries(value).sort(([left], [right]) => left.localeCompare(right)).map(([key, item]) => `${JSON.stringify(key)}:${stableCanonicalJson(item)}`).join(",")}}`;
}
return JSON.stringify(value);
}
function liveAdmissionSpecSha256(targetId, policyName, bindingName, policySpecInput, bindingSpecInput) {
const policySpec = normalizeAdmissionPolicySpec(policySpecInput);
const bindingSpec = normalizeAdmissionBindingSpec(bindingSpecInput);
const normalized = {
targetId,
policyName,
bindingName,
policySpec,
bindingSpec,
};
return `sha256:${createHash("sha256").update(stableCanonicalJson(normalized)).digest("hex")}`;
}
function normalizeAdmissionPolicySpec(value) {
const spec = { ...record(value) };
if (Object.prototype.hasOwnProperty.call(spec, "matchConstraints")) {
const matchConstraints = { ...record(spec.matchConstraints) };
if (!Object.prototype.hasOwnProperty.call(matchConstraints, "matchPolicy")) matchConstraints.matchPolicy = "Equivalent";
spec.matchConstraints = matchConstraints;
}
return spec;
}
function normalizeAdmissionBindingSpec(value) {
const spec = { ...record(value) };
if (Object.prototype.hasOwnProperty.call(spec, "matchResources")) {
const matchResources = { ...record(spec.matchResources) };
if (!Object.prototype.hasOwnProperty.call(matchResources, "matchPolicy")) matchResources.matchPolicy = "Equivalent";
spec.matchResources = matchResources;
}
return spec;
}
function evaluatePacAdmissionState(inputValue) {
const input = record(inputValue);
const policy = record(input.policy);
const binding = record(input.binding);
const role = record(input.role);
const roleBinding = record(input.roleBinding);
const expected = record(input.expected);
const namespace = stringOrNull(input.namespace);
const policyMetadata = record(policy.metadata);
const bindingMetadata = record(binding.metadata);
const roleMetadata = record(role.metadata);
const roleBindingMetadata = record(roleBinding.metadata);
const policyAnnotations = record(policyMetadata.annotations);
const bindingAnnotations = record(bindingMetadata.annotations);
const roleAnnotations = record(roleMetadata.annotations);
const roleBindingAnnotations = record(roleBindingMetadata.annotations);
const policySpec = record(policy.spec);
const bindingSpec = record(binding.spec);
const policyStatus = record(policy.status);
const typeChecking = record(policyStatus.typeChecking);
const warnings = Array.isArray(typeChecking.expressionWarnings) ? typeChecking.expressionWarnings : [];
const reasons = [];
const targetId = stringOrNull(expected.targetId);
const liveSpecSha256 = targetId === null
? null
: liveAdmissionSpecSha256(targetId, expected.policyName, expected.bindingName, policySpec, bindingSpec);
if (targetId === null) reasons.push("admission-target-id-missing");
if (liveSpecSha256 !== expected.configSha256) reasons.push("admission-live-spec-sha256-mismatch");
if (policyMetadata.name !== expected.policyName) reasons.push("policy-missing");
if (bindingMetadata.name !== expected.bindingName) reasons.push("binding-missing");
for (const [kind, annotations] of [["policy", policyAnnotations], ["binding", bindingAnnotations]]) {
if (annotations["unidesk.ai/pac-provenance-version"] !== expected.version) reasons.push(`${kind}-version-mismatch`);
if (annotations["unidesk.ai/pac-controller-identity"] !== expected.controllerIdentity) reasons.push(`${kind}-controller-identity-mismatch`);
if (annotations["unidesk.ai/effective-config-sha256"] !== expected.configSha256) reasons.push(`${kind}-config-sha256-mismatch`);
}
if (policySpec.failurePolicy !== "Fail") reasons.push("policy-failure-policy-not-fail");
if (bindingSpec.policyName !== expected.policyName) reasons.push("binding-policy-name-mismatch");
if (!Array.isArray(bindingSpec.validationActions) || bindingSpec.validationActions.length !== 1 || bindingSpec.validationActions[0] !== "Deny") reasons.push("binding-validation-actions-not-deny");
if (!Number.isInteger(policyMetadata.generation) || !Number.isInteger(policyStatus.observedGeneration) || policyStatus.observedGeneration < policyMetadata.generation) reasons.push("policy-generation-not-observed");
if (warnings.length > 0) reasons.push("policy-expression-warning");
if (roleMetadata.name !== "unidesk-pac-consumer-runner") reasons.push("default-role-missing");
if (roleBindingMetadata.name !== "unidesk-pac-consumer-runner-default") reasons.push("default-rolebinding-missing");
if (roleAnnotations["unidesk.ai/effective-config-sha256"] !== expected.rbacConfigSha256) reasons.push("default-role-config-sha256-mismatch");
if (roleBindingAnnotations["unidesk.ai/effective-config-sha256"] !== expected.rbacConfigSha256) reasons.push("default-rolebinding-config-sha256-mismatch");
const forbiddenVerbs = new Set(["create", "patch", "update", "delete", "deletecollection"]);
const roleRules = Array.isArray(role.rules) ? role.rules : [];
const roleHasMutation = roleRules.some((ruleValue) => {
const rule = record(ruleValue);
return (Array.isArray(rule.verbs) ? rule.verbs : []).some((verb) => forbiddenVerbs.has(verb));
});
if (roleHasMutation) reasons.push("default-role-declares-mutation");
const expectedRoleRules = [
{ apiGroups: ["tekton.dev"], resources: ["pipelineruns", "taskruns"], verbs: ["get", "list", "watch"] },
{ apiGroups: [""], resources: ["pods", "pods/log"], verbs: ["get", "list", "watch"] },
];
if (stableCanonicalJson(roleRules) !== stableCanonicalJson(expectedRoleRules)) reasons.push("default-role-rules-mismatch");
const roleRef = record(roleBinding.roleRef);
const subjects = Array.isArray(roleBinding.subjects) ? roleBinding.subjects.map(record) : [];
if (roleRef.apiGroup !== "rbac.authorization.k8s.io" || roleRef.kind !== "Role" || roleRef.name !== "unidesk-pac-consumer-runner") reasons.push("default-rolebinding-role-ref-mismatch");
if (namespace === null || subjects.length !== 1 || subjects[0].kind !== "ServiceAccount" || subjects[0].name !== "default" || subjects[0].namespace !== namespace) reasons.push("default-rolebinding-subject-mismatch");
if (typeof input.defaultCanMutate !== "boolean") reasons.push("default-serviceaccount-mutation-probe-unavailable");
if (input.defaultCanMutate === true) reasons.push("default-serviceaccount-can-mutate-tekton-runs");
const timestamps = [policyMetadata.creationTimestamp, bindingMetadata.creationTimestamp]
.filter((value) => typeof value === "string" && Number.isFinite(Date.parse(value)))
.sort((left, right) => Date.parse(left) - Date.parse(right));
const activeAfter = timestamps.length === 2 ? timestamps[1] : null;
if (activeAfter === null) reasons.push("admission-creation-time-missing");
return {
configured: true,
required: true,
ready: reasons.length === 0,
source: "live-admissionregistration-v1",
policyName: stringOrNull(policyMetadata.name),
bindingName: stringOrNull(bindingMetadata.name),
version: stringOrNull(policyAnnotations["unidesk.ai/pac-provenance-version"]),
controllerIdentity: stringOrNull(policyAnnotations["unidesk.ai/pac-controller-identity"]),
configSha256: stringOrNull(policyAnnotations["unidesk.ai/effective-config-sha256"]),
liveSpecSha256,
policyUid: stringOrNull(policyMetadata.uid),
bindingUid: stringOrNull(bindingMetadata.uid),
policyGeneration: Number.isInteger(policyMetadata.generation) ? policyMetadata.generation : null,
observedGeneration: Number.isInteger(policyStatus.observedGeneration) ? policyStatus.observedGeneration : null,
rbacConfigSha256: stringOrNull(roleAnnotations["unidesk.ai/effective-config-sha256"]),
defaultCanMutate: typeof input.defaultCanMutate === "boolean" ? input.defaultCanMutate : null,
activeAfter,
reasons,
valuesPrinted: false,
};
}
function classifyPacPipelineRun(consumerInput, itemInput) {
@@ -63,6 +199,8 @@ function classifyPacPipelineRun(consumerInput, itemInput) {
const managedBy = stringOrNull(labels["app.kubernetes.io/managed-by"]);
const provider = stringOrNull(annotations["pipelinesascode.tekton.dev/git-provider"]);
const expectedRepository = stringOrNull(consumer.repository);
const provenance = record(consumer.deliveryProvenance);
const admissionState = record(consumer.admissionState);
const outer = candidate
&& eventType === "push"
&& expectedRepository !== null
@@ -75,6 +213,33 @@ function classifyPacPipelineRun(consumerInput, itemInput) {
&& labels["unidesk.ai/ci-system"] === "tekton"
&& annotations["unidesk.ai/source-authority"] === "gitea-snapshot"
&& typeof annotations["unidesk.ai/publish-gitops"] === "string";
const provenanceRequired = provenance.required === true;
const markerAnnotation = stringOrNull(provenance.markerAnnotation);
const markerValue = stringOrNull(provenance.markerValue);
const executionServiceAccountName = stringOrNull(provenance.executionServiceAccountName);
const taskRunTemplate = record(record(item.spec).taskRunTemplate);
const createdAt = stringOrNull(metadata.creationTimestamp);
const activeAfter = stringOrNull(admissionState.activeAfter);
const createdAfterAdmission = createdAt !== null
&& activeAfter !== null
&& Number.isFinite(Date.parse(createdAt))
&& Number.isFinite(Date.parse(activeAfter))
&& Date.parse(createdAt) >= Date.parse(activeAfter);
const admissionProvenanceVerified = outer
&& provenanceRequired
&& admissionState.ready === true
&& admissionState.source === "live-admissionregistration-v1"
&& admissionState.policyName === provenance.policyName
&& admissionState.bindingName === provenance.bindingName
&& admissionState.version === provenance.version
&& admissionState.controllerIdentity === provenance.controllerIdentity
&& admissionState.configSha256 === provenance.configSha256
&& markerAnnotation !== null
&& markerValue !== null
&& annotations[markerAnnotation] === markerValue
&& executionServiceAccountName !== null
&& taskRunTemplate.serviceAccountName === executionServiceAccountName
&& createdAfterAdmission;
const deliveryClass = outer
? "outer-pac-event"
@@ -82,7 +247,11 @@ function classifyPacPipelineRun(consumerInput, itemInput) {
? "inner-deterministic-publish"
: "unknown";
const reason = outer
? "observed-pac-push-metadata-matches-consumer"
? admissionProvenanceVerified
? "admission-owned-pac-push-provenance-verified"
: provenanceRequired
? "pac-push-metadata-observed-but-admission-provenance-unverified"
: "observed-pac-push-metadata-matches-consumer"
: inner
? "deterministic-sentinel-publish-without-proven-parent"
: candidate
@@ -92,13 +261,19 @@ function classifyPacPipelineRun(consumerInput, itemInput) {
deliveryClass,
reason,
candidate,
deliveryAuthorityEligible: outer,
deliveryOwner: outer ? "pac-controller-observed" : null,
deliveryAuthorityEligible: outer && (!provenanceRequired || admissionProvenanceVerified),
deliveryOwner: outer ? admissionProvenanceVerified ? "pac-controller-admission" : "pac-controller-observed" : null,
executionOwner: inner ? "tekton-child-unattached" : null,
parentPipelineRun: null,
parentRelation: inner ? "unproven" : null,
metadataObservationOnly: outer,
admissionProvenanceVerified: false,
metadataObservationOnly: outer && !admissionProvenanceVerified,
admissionProvenanceVerified,
admissionPolicyReady: admissionState.ready === true,
admissionPolicyName: stringOrNull(admissionState.policyName),
admissionBindingName: stringOrNull(admissionState.bindingName),
admissionConfigSha256: stringOrNull(admissionState.configSha256),
admissionActiveAfter: activeAfter,
createdAfterAdmission,
valuesPrinted: false,
};
}
@@ -173,7 +348,7 @@ function terminalSource(task, marker, markerStatus = "skipped", markerSource = "
function extractPacSourceObservation(recordsInput) {
const records = Array.isArray(recordsInput) ? recordsInput.map(record) : [];
const plan = [...records].reverse().find((item) => item.event === "g14-ci-plan") || null;
const plan = [...records].reverse().find((item) => item.event === "pac-delivery-plan" || item.event === "g14-ci-plan") || null;
const collectMarker = exactSkipMarker(records, "collect-artifacts");
const promoteMarker = exactSkipMarker(records, "gitops-promote");
const planTask = exactTaskTerminal(records, "plan-artifacts");
@@ -225,9 +400,14 @@ function extractPacSourceObservation(recordsInput) {
reason = "delivery-terminal-evidence-incomplete";
}
const planMarkerSource = plan?.event === "pac-delivery-plan"
? "pac-delivery-plan-structured-log"
: plan?.event === "g14-ci-plan"
? "g14-ci-plan-structured-log"
: "delivery-plan-structured-log";
return {
schemaVersion: "v1",
contract: "hwlab-g14-ci-plan",
contract: plan?.event === "pac-delivery-plan" ? "pac-delivery-plan-v1" : "hwlab-g14-ci-plan",
mode,
valid,
reason,
@@ -244,7 +424,7 @@ function extractPacSourceObservation(recordsInput) {
gitopsPromote: promoteMarker !== null ? "no-build-no-rollout-plan" : promoteTask?.status || "missing",
},
terminalSources: {
planArtifacts: terminalSource(planTask, plan, "observed", "g14-ci-plan-structured-log"),
planArtifacts: terminalSource(planTask, plan, "observed", planMarkerSource),
collectArtifacts: terminalSource(collectTask, collectMarker),
gitopsPromote: terminalSource(promoteTask, promoteMarker),
},
@@ -721,6 +901,18 @@ function runPacStatusFixtureChecks() {
expectedCode: "pac-ready-gitops-exact",
input: fixtureInput({ artifact: { imageStatus: "built", digest: digestA, digests: [digestA], gitopsCommit: revision, sourceObservation: delivery, catalog: publishedCatalog() }, argo: exactArgo, runtime: { image: `registry/service@${digestA}`, digest: digestA, replicas: 1, readyReplicas: 1 } }),
},
{
id: "delivery-ready-descendant",
expectedOk: true,
expectedCode: "pac-ready-gitops-descendant",
input: fixtureInput({ artifact: { imageStatus: "built", digest: digestA, digests: [digestA], gitopsCommit: revision, sourceObservation: delivery, catalog: publishedCatalog() }, argo: { ...exactArgo, revision: "d".repeat(40), revisionRelation: { relation: "descendant", expectedRevision: revision, observedRevision: "d".repeat(40) } }, runtime: { image: `registry/service@${digestA}`, digest: digestA, replicas: 1, readyReplicas: 1 } }),
},
{
id: "delivery-stale-gitops-fails-closed",
expectedOk: false,
expectedCode: "pac-argo-revision-stale",
input: fixtureInput({ artifact: { imageStatus: "built", digest: digestA, digests: [digestA], gitopsCommit: revision, sourceObservation: delivery, catalog: publishedCatalog() }, argo: { ...exactArgo, revision: "d".repeat(40), revisionRelation: { relation: "stale", expectedRevision: revision, observedRevision: "d".repeat(40) } }, runtime: { image: `registry/service@${digestA}`, digest: digestA, replicas: 1, readyReplicas: 1 } }),
},
{
id: "delivery-gitops-missing",
expectedOk: false,
@@ -802,11 +994,77 @@ function runPacStatusFixtureChecks() {
actualCode: result.code,
};
});
const provenance = {
required: true,
version: "admission-pac-v1",
controllerIdentity: "system:serviceaccount:pipelines-as-code:pipelines-as-code-controller",
policyName: "unidesk-pac-delivery-provenance-v1",
bindingName: "unidesk-pac-delivery-provenance-v1",
configSha256: `sha256:${"1".repeat(64)}`,
markerAnnotation: "unidesk.ai/pac-admission-provenance",
markerValue: "admission-pac-v1:fixture",
executionServiceAccountName: "fixture-pac-runner",
};
const admissionState = {
ready: true,
source: "live-admissionregistration-v1",
policyName: provenance.policyName,
bindingName: provenance.bindingName,
version: provenance.version,
controllerIdentity: provenance.controllerIdentity,
configSha256: provenance.configSha256,
activeAfter: "2026-01-01T00:00:00Z",
};
const consumer = {
id: "fixture",
repository: "fixture-repository",
pipeline: "fixture-pipeline",
pipelineRunPrefix: "fixture-run-",
deliveryProvenance: provenance,
admissionState,
};
const verifiedRun = {
metadata: {
name: "fixture-run-verified",
creationTimestamp: "2026-01-01T00:00:01Z",
labels: {
"app.kubernetes.io/managed-by": "pipelinesascode.tekton.dev",
"pipelinesascode.tekton.dev/event-type": "push",
"pipelinesascode.tekton.dev/repository": "fixture-repository",
},
annotations: {
"pipelinesascode.tekton.dev/git-provider": "gitea",
[provenance.markerAnnotation]: provenance.markerValue,
},
},
spec: { taskRunTemplate: { serviceAccountName: provenance.executionServiceAccountName } },
};
const classificationCases = [
{ id: "admission-provenance-verified", expected: true, item: verifiedRun, state: admissionState },
{ id: "pre-bootstrap-run-fails-closed", expected: false, item: { ...verifiedRun, metadata: { ...verifiedRun.metadata, creationTimestamp: "2025-12-31T23:59:59Z" } }, state: admissionState },
{ id: "forged-name-candidate-fails-closed", expected: false, item: { ...verifiedRun, metadata: { ...verifiedRun.metadata, annotations: { ...verifiedRun.metadata.annotations, [provenance.markerAnnotation]: "forged" } } }, state: admissionState },
{ id: "forged-label-candidate-fails-closed", expected: false, item: { ...verifiedRun, metadata: { ...verifiedRun.metadata, name: "unrelated", labels: { ...verifiedRun.metadata.labels, "tekton.dev/pipeline": consumer.pipeline }, annotations: { ...verifiedRun.metadata.annotations, [provenance.markerAnnotation]: "forged" } } }, state: admissionState },
{ id: "forged-pipeline-ref-candidate-fails-closed", expected: false, item: { ...verifiedRun, metadata: { ...verifiedRun.metadata, name: "unrelated", annotations: { ...verifiedRun.metadata.annotations, [provenance.markerAnnotation]: "forged" } }, spec: { ...verifiedRun.spec, pipelineRef: { name: consumer.pipeline } } }, state: admissionState },
{ id: "wrong-service-account-fails-closed", expected: false, item: { ...verifiedRun, spec: { taskRunTemplate: { serviceAccountName: "default" } } }, state: admissionState },
{ id: "policy-identity-mismatch-fails-closed", expected: false, item: verifiedRun, state: { ...admissionState, configSha256: `sha256:${"2".repeat(64)}` } },
];
for (const item of classificationCases) {
const result = classifyPacPipelineRun({ ...consumer, admissionState: item.state }, item.item);
checks.push({
id: item.id,
ok: result.admissionProvenanceVerified === item.expected && result.deliveryAuthorityEligible === item.expected,
expectedOk: item.expected,
actualOk: result.deliveryAuthorityEligible,
expectedCode: item.expected ? "admission-owned-pac-push-provenance-verified" : "pac-push-metadata-observed-but-admission-provenance-unverified",
actualCode: result.reason,
});
}
return { ok: checks.every((item) => item.ok), checks, valuesPrinted: false };
}
module.exports = {
classifyPacPipelineRun,
evaluatePacAdmissionState,
evaluatePacStatus,
extractPacArtifactEvidence,
extractPacSourceObservation,
+43 -75
View File
@@ -218,83 +218,13 @@ export function renderAgentRunPipelineManifest(spec: AgentRunLaneSpec): Record<s
{ name: "artifact-catalog", type: "string", default: spec.deployment.artifactCatalogPath },
],
workspaces: [{ name: "source" }],
tasks: [
agentRunBuildPublishTask(spec),
],
tasks: agentRunBuildPublishTasks(spec),
},
};
}
function agentRunBuildPublishTask(spec: AgentRunLaneSpec): Record<string, unknown> {
return {
name: "build-publish",
workspaces: [{ name: "source", workspace: "source" }],
taskSpec: {
params: [
{ name: "git-read-url" },
{ name: "git-write-url" },
{ name: "source-branch" },
{ name: "gitops-branch" },
{ name: "revision" },
{ name: "source-stage-ref" },
{ name: "registry-prefix" },
{ name: "tools-image" },
{ name: "buildkit-image" },
{ name: "containerfile" },
{ name: "context-dir" },
{ name: "image-repository" },
{ name: "build-network" },
{ name: "build-args-json" },
{ name: "build-http-proxy" },
{ name: "build-https-proxy" },
{ name: "build-no-proxy" },
{ name: "container-http-proxy" },
{ name: "container-https-proxy" },
{ name: "container-no-proxy" },
{ name: "env-identity-files-json" },
{ name: "gitops-root" },
{ name: "artifact-catalog" },
],
workspaces: [{ name: "source" }],
steps: [
{
name: "source-checkout",
image: "$(params.tools-image)",
script: agentRunTektonSourceCheckoutScript(),
},
{
name: "probe-env-image",
image: "$(params.tools-image)",
script: agentRunTektonProbeImageScript(),
},
{
name: "build-env-image",
image: "$(params.buildkit-image)",
env: [
{ name: "BUILDKITD_FLAGS", value: "--oci-worker-no-process-sandbox --oci-worker-net=host --allow-insecure-entitlement network.host" },
{ name: "HTTP_PROXY", value: "$(params.build-http-proxy)" },
{ name: "http_proxy", value: "$(params.build-http-proxy)" },
{ name: "HTTPS_PROXY", value: "$(params.build-https-proxy)" },
{ name: "https_proxy", value: "$(params.build-https-proxy)" },
{ name: "ALL_PROXY", value: "$(params.build-https-proxy)" },
{ name: "all_proxy", value: "$(params.build-https-proxy)" },
{ name: "NO_PROXY", value: "$(params.build-no-proxy)" },
{ name: "no_proxy", value: "$(params.build-no-proxy)" },
],
securityContext: { privileged: true, runAsUser: 1000, runAsGroup: 1000 },
script: agentRunTektonBuildImageScript(),
},
{
name: "publish-gitops",
image: "$(params.tools-image)",
env: [
{ name: "GITEA_TOKEN", valueFrom: { secretKeyRef: { name: agentRunPacGiteaSecretName(spec), key: "token", optional: true } } },
],
script: agentRunTektonGitopsPublishScript(spec),
},
],
},
params: [
function agentRunBuildPublishTasks(spec: AgentRunLaneSpec): readonly Record<string, unknown>[] {
const params = [
{ name: "git-read-url", value: "$(params.git-read-url)" },
{ name: "git-write-url", value: "$(params.git-write-url)" },
{ name: "source-branch", value: "$(params.source-branch)" },
@@ -318,9 +248,45 @@ function agentRunBuildPublishTask(spec: AgentRunLaneSpec): Record<string, unknow
{ name: "env-identity-files-json", value: "$(params.env-identity-files-json)" },
{ name: "gitops-root", value: "$(params.gitops-root)" },
{ name: "artifact-catalog", value: "$(params.artifact-catalog)" },
],
];
const taskParams = params.map(({ name }) => ({ name }));
const task = (name: string, steps: readonly Record<string, unknown>[], runAfter: readonly string[] = []): Record<string, unknown> => ({
name,
...(runAfter.length === 0 ? {} : { runAfter }),
workspaces: [{ name: "source", workspace: "source" }],
taskSpec: { params: taskParams, workspaces: [{ name: "source" }], steps },
params,
when: [{ input: spec.deployment.format, operator: "in", values: ["unidesk-yaml-only"] }],
};
});
return [
task("plan-artifacts", [
{ name: "source-checkout", image: "$(params.tools-image)", script: agentRunTektonSourceCheckoutScript() },
{ name: "probe-env-image", image: "$(params.tools-image)", script: agentRunTektonProbeImageScript() },
]),
task("collect-artifacts", [{
name: "build-env-image",
image: "$(params.buildkit-image)",
env: [
{ name: "BUILDKITD_FLAGS", value: "--oci-worker-no-process-sandbox --oci-worker-net=host --allow-insecure-entitlement network.host" },
{ name: "HTTP_PROXY", value: "$(params.build-http-proxy)" },
{ name: "http_proxy", value: "$(params.build-http-proxy)" },
{ name: "HTTPS_PROXY", value: "$(params.build-https-proxy)" },
{ name: "https_proxy", value: "$(params.build-https-proxy)" },
{ name: "ALL_PROXY", value: "$(params.build-https-proxy)" },
{ name: "all_proxy", value: "$(params.build-https-proxy)" },
{ name: "NO_PROXY", value: "$(params.build-no-proxy)" },
{ name: "no_proxy", value: "$(params.build-no-proxy)" },
],
securityContext: { privileged: true, runAsUser: 1000, runAsGroup: 1000 },
script: agentRunTektonBuildImageScript(),
}], ["plan-artifacts"]),
task("gitops-promote", [{
name: "publish-gitops",
image: "$(params.tools-image)",
env: [{ name: "GITEA_TOKEN", valueFrom: { secretKeyRef: { name: agentRunPacGiteaSecretName(spec), key: "token", optional: true } } }],
script: agentRunTektonGitopsPublishScript(spec),
}], ["collect-artifacts"]),
];
}
function agentRunTektonSourceCheckoutScript(): string {
@@ -399,6 +365,8 @@ function agentRunTektonProbeImageScript(): string {
"else",
" printf '{\"ok\":false,\"status\":\"cache-miss\",\"sourceCommit\":\"%s\",\"envIdentity\":\"%s\",\"valuesPrinted\":false}\\n' \"$(params.revision)\" \"$env_identity\" > \"$root/build-result.json\"",
"fi",
"if [ -f \"$root/skip-build\" ]; then build_services='[]'; reused_services='[\"agentrun-mgr\"]'; else build_services='[\"agentrun-mgr\"]'; reused_services='[]'; fi",
"printf '{\"event\":\"pac-delivery-plan\",\"schemaVersion\":\"v1\",\"sourceCommitId\":\"%s\",\"affectedServices\":[\"agentrun-mgr\"],\"rolloutServices\":[\"agentrun-mgr\"],\"buildServices\":%s,\"reusedServices\":%s,\"valuesPrinted\":false}\\n' \"$(params.revision)\" \"$build_services\" \"$reused_services\"",
"chmod a+rw \"$root/build-result.json\"",
"cat \"$root/build-result.json\"",
].join("\n");
@@ -0,0 +1,13 @@
import { renderPacAdmissionDesiredFragment } from "./platform-infra-pac-provenance";
import { renderPacConsumerRbacDesiredFragment } from "./platform-infra-pac-consumer-rbac";
export function renderPlatformInfraGiteaDesiredFragments(targetId: string): string {
return [
renderPacConsumerRbacDesiredFragment(targetId),
renderPacAdmissionDesiredFragment(targetId),
].map(normalizeFragment).filter((item) => item.length > 0).join("\n---\n");
}
function normalizeFragment(value: string): string {
return value.trim().replace(/^(?:---\s*)+|(?:\s*---)+$/gu, "");
}
@@ -67,5 +67,17 @@ test("owning YAML renders one child Application and the complete durable bridge
expect(manifest).toContain("name: candidate-inbox\n emptyDir: {}");
expect(manifest).toContain('gate: "gitea-github-sync-candidate-startup"');
const documents = manifest.split(/^---\s*$/mu).map((item) => item.trim()).filter(Boolean).map((item) => Bun.YAML.parse(item) as any);
expect(documents.map((item) => item.kind)).toEqual(["ConfigMap", "Job", "ServiceAccount", "ConfigMap", "PersistentVolumeClaim", "Service", "Deployment"]);
expect(documents.map((item) => item.kind)).toEqual([
"ConfigMap",
"Job",
"ServiceAccount",
"ConfigMap",
"PersistentVolumeClaim",
"Service",
"Deployment",
"Role",
"RoleBinding",
"ValidatingAdmissionPolicy",
"ValidatingAdmissionPolicyBinding",
]);
});
+5 -1
View File
@@ -21,6 +21,7 @@ import type { GiteaWebhookIngressRetry } from "./platform-infra-gitea-caddy";
import { renderGiteaRemotePayloadMaterializer, summarizeGiteaRemotePayloads } from "./platform-infra-gitea-payload";
import { parseEnvFile, readTextFile, redactRepoPath, requiredEnvValue } from "./secrets";
import { PAC_AUTOMATIC_DELIVERY_REFERENCE } from "./cicd-delivery-authority";
import { renderPlatformInfraGiteaDesiredFragments } from "./platform-infra-gitea-desired-fragments";
const configFile = rootPath("config", "platform-infra", "gitea.yaml");
const configLabel = "config/platform-infra/gitea.yaml";
@@ -1628,7 +1629,10 @@ export function renderGiteaWebhookSyncDesiredManifest(targetId: string): string
const target = resolveTarget(gitea, targetId);
if (!delivery.enabled) throw new Error(`${configLabel}.sourceAuthority.webhookSync.gitOpsDelivery.enabled must be true`);
if (target.id !== delivery.targetId) throw new Error(`GitOps delivery target ${delivery.targetId} does not match requested target ${target.id}`);
return renderGithubSyncManifest(gitea, target);
return [
renderGithubSyncManifest(gitea, target).trim(),
renderPlatformInfraGiteaDesiredFragments(target.id).trim(),
].filter((item) => item.length > 0).join("\n---\n") + "\n";
}
export function readGiteaWebhookGitOpsDelivery(): GiteaWebhookGitOpsDelivery {
@@ -0,0 +1,67 @@
import { createHash } from "node:crypto";
import { readPacAdmissionContract } from "./platform-infra-pac-provenance";
export interface PacConsumerRbacDesiredIdentity {
readonly configSha256: string;
readonly namespaces: readonly string[];
}
const pacConsumerReadOnlyRules = [
{ apiGroups: ["tekton.dev"], resources: ["pipelineruns", "taskruns"], verbs: ["get", "list", "watch"] },
{ apiGroups: [""], resources: ["pods", "pods/log"], verbs: ["get", "list", "watch"] },
] as const;
const pacConsumerDefaultRoleRef = { apiGroup: "rbac.authorization.k8s.io", kind: "Role", name: "unidesk-pac-consumer-runner" } as const;
export function renderPacConsumerRbacDesiredFragment(targetId: string): string {
const consumers = readPacAdmissionContract().consumers.filter((consumer) => consumer.node.toLowerCase() === targetId.toLowerCase());
const namespaces = [...new Set(consumers.map((consumer) => consumer.namespace))];
if (namespaces.length === 0) return "";
const configSha256 = pacConsumerRbacDesiredIdentity(targetId).configSha256;
const objects = namespaces.flatMap((namespace) => {
const labels = {
"app.kubernetes.io/managed-by": "unidesk",
"app.kubernetes.io/part-of": "platform-infra",
"unidesk.ai/pac-rbac-contract": "admission-provenance-required",
};
const annotations = {
"argocd.argoproj.io/sync-wave": "-1",
"unidesk.ai/effective-config-sha256": configSha256,
};
return [
{
apiVersion: "rbac.authorization.k8s.io/v1",
kind: "Role",
metadata: { name: "unidesk-pac-consumer-runner", namespace, labels, annotations },
rules: pacConsumerReadOnlyRules,
},
{
apiVersion: "rbac.authorization.k8s.io/v1",
kind: "RoleBinding",
metadata: { name: "unidesk-pac-consumer-runner-default", namespace, labels, annotations },
subjects: [{ kind: "ServiceAccount", name: "default", namespace }],
roleRef: pacConsumerDefaultRoleRef,
},
];
});
return `${objects.map((item) => Bun.YAML.stringify(item).trim()).join("\n---\n")}\n`;
}
export function pacConsumerRbacDesiredIdentity(targetId: string): PacConsumerRbacDesiredIdentity {
const consumers = readPacAdmissionContract().consumers.filter((consumer) => consumer.node.toLowerCase() === targetId.toLowerCase());
const namespaces = [...new Set(consumers.map((consumer) => consumer.namespace))].sort();
const desiredSpec = namespaces.map((namespace) => ({
namespace,
role: { name: "unidesk-pac-consumer-runner", rules: pacConsumerReadOnlyRules },
roleBinding: {
name: "unidesk-pac-consumer-runner-default",
subjects: [{ kind: "ServiceAccount", name: "default", namespace }],
roleRef: pacConsumerDefaultRoleRef,
},
}));
return {
configSha256: `sha256:${createHash("sha256").update(JSON.stringify({ targetId, desiredSpec, contract: "admission-provenance-required-v1" })).digest("hex")}`,
namespaces,
};
}
@@ -0,0 +1,249 @@
import { expect, test } from "bun:test";
import { readFileSync } from "node:fs";
import { createRequire } from "node:module";
import { resolve } from "node:path";
import { resolveAgentRunLaneTarget } from "./agentrun-lanes";
import { renderAgentRunPipelineManifest } from "./agentrun-manifests";
import { renderPlatformInfraGiteaDesiredFragments } from "./platform-infra-gitea-desired-fragments";
import { pacConsumerRbacDesiredIdentity, renderPacConsumerRbacDesiredFragment } from "./platform-infra-pac-consumer-rbac";
import { parsePacAdmissionContract, readPacAdmissionContract, renderPacAdmissionDesiredFragment } from "./platform-infra-pac-provenance";
import { pacDebugFixtureDisclosure } from "./platform-infra-pipelines-as-code";
const root = resolve(import.meta.dir, "../..");
const evaluator = createRequire(import.meta.url)("../native/cicd/pac-status-evaluator.cjs") as {
evaluatePacAdmissionState: (input: Record<string, unknown>) => Record<string, any>;
evaluatePacStatus: (input: Record<string, unknown>) => Record<string, any>;
extractPacArtifactEvidence: (records: unknown[], logText: string) => Record<string, any>;
taskTerminalRecord: (taskRun: Record<string, unknown>) => Record<string, any> | null;
runPacStatusFixtureChecks: () => { ok: boolean; checks: Array<{ id: string; ok: boolean }> };
};
function documents(value: string): any[] {
return value.split(/^---\s*$/mu).map((item) => item.trim()).filter(Boolean).map((item) => Bun.YAML.parse(item));
}
test("admission contract rejects malformed required declarations instead of silently downgrading", () => {
const source = Bun.YAML.parse(readFileSync(resolve(root, "config/platform-infra/pipelines-as-code.yaml"), "utf8")) as any;
const consumer = source.consumers.find((item: any) => item.deliveryProvenance !== undefined);
consumer.deliveryProvenance.required = "true";
expect(() => parsePacAdmissionContract(source)).toThrow("deliveryProvenance.required must be boolean true or false");
});
test("versioned admission desired state protects controller proof, candidate identity, params, and execution SA", () => {
const contract = readPacAdmissionContract();
const owningYaml = Bun.YAML.parse(readFileSync(resolve(root, "config/platform-infra/pipelines-as-code.yaml"), "utf8")) as any;
expect(owningYaml.deliveryProvenance.terminalRoles).toBeUndefined();
expect(contract.policyName).toEndWith("-v1");
expect(contract.bindingName).toEndWith("-v1");
expect(contract.child.enabled).toBe(false);
expect(contract.consumers.map((item) => item.id)).toEqual(["agentrun-nc01-v02"]);
const items = documents(renderPacAdmissionDesiredFragment("NC01"));
expect(items.map((item) => item.kind)).toEqual(["ValidatingAdmissionPolicy", "ValidatingAdmissionPolicyBinding"]);
const policy = items[0];
const expressions = policy.spec.validations.map((item: any) => item.expression).join("\n");
const messages = policy.spec.validations.map((item: any) => item.message).join("\n");
expect(policy.spec.failurePolicy).toBe("Fail");
expect(policy.spec.matchConstraints.matchPolicy).toBe("Equivalent");
expect(items[1].spec.validationActions).toEqual(["Deny"]);
expect(policy.metadata.annotations["argocd.argoproj.io/sync-wave"]).toBe("0");
expect(items[1].metadata.annotations["argocd.argoproj.io/sync-wave"]).toBe("1");
expect(expressions).toContain("request.userInfo.username");
expect(expressions).toContain("spec.pipelineRef.name");
expect(expressions).toContain("spec.params == oldObject.spec.params");
expect(expressions).toContain("object.spec.pipelineRef == oldObject.spec.pipelineRef");
expect(expressions).toContain("object.spec == oldObject.spec");
expect(expressions).toContain("taskRunTemplate.serviceAccountName == oldObject.spec.taskRunTemplate.serviceAccountName");
expect(expressions).toContain("object.metadata.name.startsWith");
expect(expressions).toContain('object.metadata.labels["tekton.dev/pipeline"]');
expect(expressions).toContain("agentrun-nc01-v02-tekton-runner");
expect(messages).toContain("execution ServiceAccount");
expect(expressions).toContain(contract.child.parentUidAnnotation);
expect(expressions).toContain("oldObject");
expect(JSON.stringify(items)).not.toContain('"kind":"ServiceAccount"');
const specGuard = policy.spec.validations.find((item: any) => item.message.includes("PipelineRun spec is immutable"));
const oldRun = { spec: { pipelineSpec: { tasks: [{ name: "plan-artifacts" }] }, workspaces: [], timeouts: { pipeline: "1h" } } };
const mutatedRun = structuredClone(oldRun);
mutatedRun.spec.pipelineSpec.tasks[0].name = "forged-task";
expect(specGuard.expression).toMatch(/object\.spec == oldObject\.spec/u);
expect(mutatedRun.spec).not.toEqual(oldRun.spec);
});
test("required consumer default RBAC has one automatic desired owner and no Tekton mutation verbs", () => {
const rbac = documents(renderPacConsumerRbacDesiredFragment("NC01"));
expect(rbac.map((item) => item.kind)).toEqual(["Role", "RoleBinding"]);
const role = rbac[0];
expect(role.metadata.annotations["argocd.argoproj.io/sync-wave"]).toBe("-1");
expect(role.metadata.annotations["unidesk.ai/effective-config-sha256"]).toBe(pacConsumerRbacDesiredIdentity("NC01").configSha256);
const verbs = role.rules.flatMap((rule: any) => rule.verbs);
for (const verb of ["create", "patch", "update", "delete", "deletecollection"]) expect(verbs).not.toContain(verb);
const desiredKinds = documents(renderPlatformInfraGiteaDesiredFragments("NC01")).map((item) => item.kind);
expect(desiredKinds).toEqual(["Role", "RoleBinding", "ValidatingAdmissionPolicy", "ValidatingAdmissionPolicyBinding"]);
});
test("live admission evaluator requires exact desired hashes, observed generation, default-SA loss, and a new resource epoch", () => {
const contract = readPacAdmissionContract();
const admission = documents(renderPacAdmissionDesiredFragment("NC01"));
const rbac = documents(renderPacConsumerRbacDesiredFragment("NC01"));
const policy = { ...admission[0], metadata: { ...admission[0].metadata, uid: "policy-uid", generation: 2, creationTimestamp: "2026-01-01T00:00:00Z" }, status: { observedGeneration: 2, typeChecking: { expressionWarnings: [] } } };
const binding = { ...admission[1], metadata: { ...admission[1].metadata, uid: "binding-uid", creationTimestamp: "2026-01-01T00:00:01Z" } };
const expected = {
targetId: "NC01",
policyName: contract.policyName,
bindingName: contract.bindingName,
version: contract.version,
controllerIdentity: contract.controllerUsername,
configSha256: policy.metadata.annotations["unidesk.ai/effective-config-sha256"],
rbacConfigSha256: rbac[0].metadata.annotations["unidesk.ai/effective-config-sha256"],
};
const input = { policy, binding, role: rbac[0], roleBinding: rbac[1], namespace: "agentrun-ci", defaultCanMutate: false, expected };
expect(evaluator.evaluatePacAdmissionState(input)).toMatchObject({ ready: true, activeAfter: "2026-01-01T00:00:01Z", defaultCanMutate: false });
expect(evaluator.evaluatePacAdmissionState({ ...input, defaultCanMutate: true })).toMatchObject({ ready: false, reasons: expect.arrayContaining(["default-serviceaccount-can-mutate-tekton-runs"]) });
expect(evaluator.evaluatePacAdmissionState({ ...input, defaultCanMutate: null })).toMatchObject({ ready: false, defaultCanMutate: null, reasons: expect.arrayContaining(["default-serviceaccount-mutation-probe-unavailable"]) });
expect(evaluator.evaluatePacAdmissionState({ ...input, role: { ...rbac[0], rules: [rbac[0].rules[0]] } })).toMatchObject({ ready: false, reasons: expect.arrayContaining(["default-role-rules-mismatch"]) });
expect(evaluator.evaluatePacAdmissionState({ ...input, expected: { ...expected, configSha256: `sha256:${"0".repeat(64)}` } })).toMatchObject({ ready: false, reasons: expect.arrayContaining(["policy-config-sha256-mismatch"]) });
const mutatedPolicy = {
...policy,
spec: {
...policy.spec,
validations: policy.spec.validations.map((item: any, index: number) => index === 0 ? { ...item, expression: "true" } : item),
},
};
expect(evaluator.evaluatePacAdmissionState({ ...input, policy: mutatedPolicy })).toMatchObject({
ready: false,
reasons: expect.arrayContaining(["admission-live-spec-sha256-mismatch"]),
});
expect(evaluator.evaluatePacAdmissionState({
...input,
policy: { ...policy, spec: { ...policy.spec, matchConditions: [{ name: "disable-enforcement", expression: "false" }] } },
})).toMatchObject({ ready: false, reasons: expect.arrayContaining(["admission-live-spec-sha256-mismatch"]) });
expect(evaluator.evaluatePacAdmissionState({
...input,
binding: { ...binding, spec: { ...binding.spec, matchResources: { namespaceSelector: { matchLabels: { disabled: "true" } } } } },
})).toMatchObject({ ready: false, reasons: expect.arrayContaining(["admission-live-spec-sha256-mismatch"]) });
});
test("AgentRun owning renderer emits ordered terminal roles over the shared workspace without changing build and publish steps", () => {
const spec = resolveAgentRunLaneTarget({ node: "NC01", lane: "nc01-v02" }).spec;
const pipeline = renderAgentRunPipelineManifest(spec) as any;
const tasks = pipeline.spec.tasks;
expect(tasks.map((item: any) => item.name)).toEqual(["plan-artifacts", "collect-artifacts", "gitops-promote"]);
expect(tasks[0].runAfter).toBeUndefined();
expect(tasks[1].runAfter).toEqual(["plan-artifacts"]);
expect(tasks[2].runAfter).toEqual(["collect-artifacts"]);
for (const task of tasks) expect(task.workspaces).toEqual([{ name: "source", workspace: "source" }]);
expect(tasks[0].taskSpec.steps.map((item: any) => item.name)).toEqual(["source-checkout", "probe-env-image"]);
expect(tasks[1].taskSpec.steps.map((item: any) => item.name)).toEqual(["build-env-image"]);
expect(tasks[2].taskSpec.steps.map((item: any) => item.name)).toEqual(["publish-gitops"]);
expect(tasks[0].taskSpec.steps[1].script).toContain('"event":"pac-delivery-plan"');
expect(tasks[1].taskSpec.steps[0].script).toContain("buildctl-daemonless.sh");
expect(tasks[2].taskSpec.steps[0].script).toContain("gitops-publish");
});
test("AgentRun rendered delivery plan and real TaskRun terminals close the status evaluator gate", () => {
const spec = resolveAgentRunLaneTarget({ node: "NC01", lane: "nc01-v02" }).spec;
const pipeline = renderAgentRunPipelineManifest(spec) as any;
const tasks = pipeline.spec.tasks as any[];
const sourceCommit = "c".repeat(40);
const gitopsCommit = "b".repeat(40);
const digest = `sha256:${"a".repeat(64)}`;
const planPrintf = String(tasks[0].taskSpec.steps[1].script)
.split("\n")
.find((line) => line.startsWith("printf '{\"event\":\"pac-delivery-plan\""));
expect(planPrintf).toBeDefined();
const planResult = Bun.spawnSync({
cmd: ["sh", "-c", `build_services='["agentrun-mgr"]'; reused_services='[]'; ${planPrintf!.replaceAll("$(params.revision)", sourceCommit)}`],
stdout: "pipe",
stderr: "pipe",
});
expect(planResult.exitCode).toBe(0);
const plan = JSON.parse(planResult.stdout.toString().trim());
expect(plan).toMatchObject({ event: "pac-delivery-plan", sourceCommitId: sourceCommit, buildServices: ["agentrun-mgr"] });
const terminals = tasks.map((task) => evaluator.taskTerminalRecord({
apiVersion: "tekton.dev/v1",
kind: "TaskRun",
metadata: {
name: `fixture-${task.name}`,
labels: { "tekton.dev/pipelineTask": task.name },
},
status: {
conditions: [{ type: "Succeeded", status: "True", reason: "Succeeded" }],
results: [],
},
}));
expect(terminals).not.toContain(null);
const publish = {
ok: true,
status: "succeeded",
phase: "gitops-publish",
gitopsCommit,
sourceCommit,
imageStatus: "built",
digest,
valuesPrinted: false,
};
const artifact = evaluator.extractPacArtifactEvidence([plan, ...terminals, publish], "");
expect(artifact.sourceObservation).toMatchObject({
contract: "pac-delivery-plan-v1",
mode: "delivery",
valid: true,
terminal: {
planArtifacts: "succeeded",
collectArtifacts: "succeeded",
gitopsPromote: "succeeded",
},
terminalSources: {
planArtifacts: { markerSource: "pac-delivery-plan-structured-log" },
},
});
expect(evaluator.evaluatePacStatus({
sourceCommit,
artifact,
registryPresent: true,
argo: {
sync: "Synced",
health: "Healthy",
revision: gitopsCommit,
revisionRelation: { relation: "exact", expectedRevision: gitopsCommit, observedRevision: gitopsCommit },
},
runtime: { image: `registry/agentrun-mgr@${digest}`, digest, replicas: 1, readyReplicas: 1 },
})).toMatchObject({ ok: true, code: "pac-ready-gitops-exact" });
});
test("provenance fixtures fail closed for pre-bootstrap, forged marker, wrong SA, and policy mismatch", () => {
const result = evaluator.runPacStatusFixtureChecks();
expect(result.ok).toBe(true);
for (const id of ["admission-provenance-verified", "pre-bootstrap-run-fails-closed", "forged-name-candidate-fails-closed", "forged-label-candidate-fails-closed", "forged-pipeline-ref-candidate-fails-closed", "wrong-service-account-fails-closed", "policy-identity-mismatch-fails-closed"]) {
expect(result.checks.find((item) => item.id === id)?.ok).toBe(true);
}
});
test("history evaluates admission and default RBAC in each consumer namespace", () => {
const remote = readFileSync(resolve(root, "scripts/src/platform-infra-pipelines-as-code-remote.sh"), "utf8");
expect(remote).toContain("consumer.admissionState = admissionStateForConsumer(consumer)");
expect(remote).toContain("kubectlJson(['-n', consumer.namespace, 'get', 'role', 'unidesk-pac-consumer-runner'");
expect(remote).toContain("kubectlJson(['-n', consumer.namespace, 'get', 'rolebinding', 'unidesk-pac-consumer-runner-default'");
expect(remote).toContain("defaultCanMutate(consumer.namespace)");
expect(remote).toContain("for verb in create patch update delete deletecollection; do");
expect(remote).toContain('timeout "$UNIDESK_PAC_WAIT_TIMEOUT_SECONDS" kubectl auth can-i');
expect(remote).toContain("for (const verb of ['create', 'patch', 'update', 'delete', 'deletecollection'])");
const defaultMutationProbe = remote.slice(remote.indexOf("function defaultCanMutate(namespace)"), remote.indexOf("function admissionStateForConsumer(consumer)"));
expect(defaultMutationProbe).toContain("timeout: waitTimeoutSeconds * 1000");
expect(defaultMutationProbe).not.toContain("timeout: 12000");
expect(remote).toContain("'delivery plan structured evidence is missing'");
expect(remote).toContain("'delivery plan source commit does not match the selected PipelineRun'");
expect(remote).not.toContain("'g14-ci-plan structured evidence is missing'");
expect(remote).not.toContain("for (const consumer of consumers) consumer.admissionState = admissionState");
});
test("id-specific PaC debug keeps fixture failures but does not dump every passing check", () => {
const passing = Array.from({ length: 23 }, (_, index) => ({ id: `pass-${index}`, ok: true }));
expect(pacDebugFixtureDisclosure(passing, "agentrun-run")).toMatchObject({
fixtureGate: { ok: true, total: 23, passed: 23, failed: 0, disclosure: "failed-checks-only-for-id-specific-debug" },
checks: [],
});
const withFailure = [...passing, { id: "failed", ok: false }];
expect(pacDebugFixtureDisclosure(withFailure, "agentrun-run").checks).toEqual([{ id: "failed", ok: false }]);
expect(pacDebugFixtureDisclosure(passing, null).checks).toHaveLength(23);
});
@@ -0,0 +1,310 @@
import { createHash } from "node:crypto";
import { rootPath } from "./config";
import { readYamlRecord } from "./platform-infra-ops-library";
import { materializeYamlComposition } from "./yaml-composition";
const configPath = "config/platform-infra/pipelines-as-code.yaml";
export interface PacAdmissionConsumerContract {
readonly id: string;
readonly node: string;
readonly namespace: string;
readonly pipeline: string;
readonly pipelineRunPrefix: string;
readonly markerValue: string;
readonly executionServiceAccountName: string;
readonly gitOps: { readonly repoUrl: string; readonly targetRevision: string };
}
export interface PacAdmissionContract {
readonly version: string;
readonly markerAnnotation: string;
readonly controllerUsername: string;
readonly policyName: string;
readonly bindingName: string;
readonly failurePolicy: "Fail";
readonly validationActions: readonly ["Deny"];
readonly child: {
readonly enabled: false;
readonly parentNameAnnotation: string;
readonly parentUidAnnotation: string;
};
readonly consumers: readonly PacAdmissionConsumerContract[];
}
export interface PacAdmissionDesiredIdentity {
readonly configSha256: string;
readonly policyName: string;
readonly bindingName: string;
readonly version: string;
readonly controllerIdentity: string;
}
export function readPacAdmissionContract(): PacAdmissionContract {
const source = readYamlRecord<Record<string, unknown>>(rootPath("config", "platform-infra", "pipelines-as-code.yaml"), "platform-infra-pipelines-as-code");
return parsePacAdmissionContract(source);
}
export function parsePacAdmissionContract(source: Record<string, unknown>): PacAdmissionContract {
const root = record(materializeYamlComposition(source, { label: configPath }).value, configPath);
const provenance = record(root.deliveryProvenance, `${configPath}#deliveryProvenance`);
const child = record(provenance.child, `${configPath}#deliveryProvenance.child`);
const controller = record(provenance.controller, `${configPath}#deliveryProvenance.controller`);
const admission = record(provenance.admission, `${configPath}#deliveryProvenance.admission`);
const controllerNamespace = required(controller.namespace, "deliveryProvenance.controller.namespace");
const controllerServiceAccount = required(controller.serviceAccountName, "deliveryProvenance.controller.serviceAccountName");
const validationActions = stringArray(admission.validationActions, "deliveryProvenance.admission.validationActions");
if (validationActions.length !== 1 || validationActions[0] !== "Deny") throw new Error("deliveryProvenance.admission.validationActions must be exactly [Deny]");
const consumers = recordArray(root.consumers, "consumers").flatMap((consumer, index) => {
if (consumer.deliveryProvenance === undefined) return [];
const value = record(consumer.deliveryProvenance, `consumers[${index}].deliveryProvenance`);
if (value.required === false) return [];
if (value.required !== true) throw new Error(`consumers[${index}].deliveryProvenance.required must be boolean true or false`);
const gitOps = record(value.gitOps, `consumers[${index}].deliveryProvenance.gitOps`);
return [{
id: required(consumer.id, `consumers[${index}].id`),
node: required(consumer.node, `consumers[${index}].node`),
namespace: required(consumer.namespace, `consumers[${index}].namespace`),
pipeline: required(consumer.pipeline, `consumers[${index}].pipeline`),
pipelineRunPrefix: required(consumer.pipelineRunPrefix, `consumers[${index}].pipelineRunPrefix`),
markerValue: required(value.markerValue, `consumers[${index}].deliveryProvenance.markerValue`),
executionServiceAccountName: required(value.executionServiceAccountName, `consumers[${index}].deliveryProvenance.executionServiceAccountName`),
gitOps: {
repoUrl: required(gitOps.repoUrl, `consumers[${index}].deliveryProvenance.gitOps.repoUrl`),
targetRevision: required(gitOps.targetRevision, `consumers[${index}].deliveryProvenance.gitOps.targetRevision`),
},
}];
});
const markerValues = new Set(consumers.map((consumer) => consumer.markerValue));
if (markerValues.size !== consumers.length) throw new Error("deliveryProvenance markerValue must be unique per consumer");
const version = required(provenance.version, "deliveryProvenance.version");
const versionMatch = version.match(/-v([1-9][0-9]*)$/u);
if (versionMatch === null) throw new Error("deliveryProvenance.version must end with a positive -vN resource epoch");
const resourceEpoch = `-v${versionMatch[1]}`;
const policyName = required(admission.policyName, "deliveryProvenance.admission.policyName");
const bindingName = required(admission.bindingName, "deliveryProvenance.admission.bindingName");
if (!policyName.endsWith(resourceEpoch) || !bindingName.endsWith(resourceEpoch)) throw new Error(`deliveryProvenance admission resource names must end with ${resourceEpoch}`);
return {
version,
markerAnnotation: required(provenance.markerAnnotation, "deliveryProvenance.markerAnnotation"),
controllerUsername: `system:serviceaccount:${controllerNamespace}:${controllerServiceAccount}`,
policyName,
bindingName,
failurePolicy: literal(admission.failurePolicy, "deliveryProvenance.admission.failurePolicy", "Fail"),
validationActions: ["Deny"],
child: {
enabled: literal(child.enabled, "deliveryProvenance.child.enabled", false),
parentNameAnnotation: required(child.parentNameAnnotation, "deliveryProvenance.child.parentNameAnnotation"),
parentUidAnnotation: required(child.parentUidAnnotation, "deliveryProvenance.child.parentUidAnnotation"),
},
consumers,
};
}
export function pacAdmissionConsumer(consumerId: string): PacAdmissionConsumerContract | null {
return readPacAdmissionContract().consumers.find((consumer) => consumer.id === consumerId) ?? null;
}
export function renderPacAdmissionDesiredFragment(targetId: string): string {
const contract = readPacAdmissionContract();
const consumers = contract.consumers.filter((consumer) => consumer.node.toLowerCase() === targetId.toLowerCase());
if (consumers.length === 0) return "";
const annotation = contract.markerAnnotation;
const markerPresent = `has(object.metadata.annotations) && ${cel(annotation)} in object.metadata.annotations`;
const oldMarkerPresent = `has(oldObject.metadata.annotations) && ${cel(annotation)} in oldObject.metadata.annotations`;
const protectedUpdate = [markerPresent, oldMarkerPresent, ...consumers.flatMap((consumer) => [candidateExpression(consumer, "object"), candidateExpression(consumer, "oldObject")])].map((item) => `(${item})`).join(" || ");
const immutableProofKeys = [
["labels", "app.kubernetes.io/managed-by"],
["labels", "pipelinesascode.tekton.dev/event-type"],
["labels", "pipelinesascode.tekton.dev/repository"],
["labels", "pipelinesascode.tekton.dev/sha"],
["labels", "pipelinesascode.tekton.dev/commit"],
["labels", "pipelinesascode.tekton.dev/branch"],
["labels", "pipelinesascode.tekton.dev/sender"],
["annotations", "pipelinesascode.tekton.dev/controller-info"],
["annotations", "pipelinesascode.tekton.dev/event-type"],
["annotations", "pipelinesascode.tekton.dev/git-provider"],
["annotations", "pipelinesascode.tekton.dev/repository"],
["annotations", "pipelinesascode.tekton.dev/sha"],
["annotations", "pipelinesascode.tekton.dev/commit"],
["annotations", "pipelinesascode.tekton.dev/branch"],
["annotations", "pipelinesascode.tekton.dev/source-branch"],
["annotations", "pipelinesascode.tekton.dev/sender"],
["annotations", contract.child.parentNameAnnotation],
["annotations", contract.child.parentUidAnnotation],
] as const;
const validations: Record<string, unknown>[] = [
{
expression: `request.operation != 'CREATE' || !(${markerPresent}) || request.userInfo.username == ${cel(contract.controllerUsername)}`,
message: "reserved PaC provenance marker may only be created by the authenticated PaC controller ServiceAccount",
reason: "Forbidden",
},
{
expression: `request.operation != 'UPDATE' || ((${markerPresent}) == (${oldMarkerPresent}) && (!(${markerPresent}) || object.metadata.annotations[${cel(annotation)}] == oldObject.metadata.annotations[${cel(annotation)}]))`,
message: "reserved PaC provenance marker is immutable",
reason: "Invalid",
},
{
expression: `request.operation != 'CREATE' || ((!has(object.metadata.annotations) || !(${cel(contract.child.parentNameAnnotation)} in object.metadata.annotations)) && (!has(object.metadata.annotations) || !(${cel(contract.child.parentUidAnnotation)} in object.metadata.annotations)))`,
message: "child PipelineRun provenance is disabled until an admission-verified parent name and UID contract is enabled",
reason: "Forbidden",
},
{
expression: `request.operation != 'UPDATE' || !(${protectedUpdate}) || (${immutableProofKeys.map(([map, key]) => metadataEntryEqual(map, key)).join(" && ")})`,
message: "PaC repository, branch, revision, event, controller, sender, and parent identity proof fields are immutable",
reason: "Invalid",
},
{
expression: `request.operation != 'UPDATE' || !(${protectedUpdate}) || ((!has(object.spec.params) && !has(oldObject.spec.params)) || (has(object.spec.params) && has(oldObject.spec.params) && object.spec.params == oldObject.spec.params))`,
message: "PaC source revision parameters are immutable",
reason: "Invalid",
},
{
expression: `request.operation != 'UPDATE' || !(${protectedUpdate}) || (${serviceAccountEqual()})`,
message: "PaC execution ServiceAccount is immutable",
reason: "Invalid",
},
{
expression: `request.operation != 'UPDATE' || !(${protectedUpdate}) || (${pipelineRefEqual()})`,
message: "PaC referenced Pipeline identity is immutable",
reason: "Invalid",
},
{
expression: `request.operation != 'UPDATE' || !(${protectedUpdate}) || object.spec == oldObject.spec`,
message: "admission-proven PaC PipelineRun spec is immutable, including embedded pipelineSpec, workspaces, and timeouts",
reason: "Invalid",
},
...consumers.flatMap((consumer) => [{
expression: `request.operation != 'CREATE' || !(${candidateExpression(consumer, "object")}) || (request.userInfo.username == ${cel(contract.controllerUsername)} && (${markerPresent}) && object.metadata.annotations[${cel(annotation)}] == ${cel(consumer.markerValue)} && ${usesServiceAccount(consumer, "object")})`,
message: `PipelineRun candidates for ${consumer.id} require its exact admission marker and execution ServiceAccount`,
reason: "Invalid",
}, {
expression: `!(${usesServiceAccount(consumer, "object")}) || ((${candidateExpression(consumer, "object")}) && (${markerPresent}) && object.metadata.annotations[${cel(annotation)}] == ${cel(consumer.markerValue)})`,
message: `execution ServiceAccount for ${consumer.id} is reserved to its admission-proven PipelineRun candidates`,
reason: "Forbidden",
}]),
];
const policySpec = {
failurePolicy: contract.failurePolicy,
matchConstraints: {
matchPolicy: "Equivalent",
resourceRules: [{ apiGroups: ["tekton.dev"], apiVersions: ["v1"], operations: ["CREATE", "UPDATE"], resources: ["pipelineruns"] }],
},
validations,
};
const bindingSpec = { policyName: contract.policyName, validationActions: contract.validationActions };
const policyHash = admissionSpecSha256(targetId, contract.policyName, contract.bindingName, policySpec, bindingSpec);
const metadata = {
labels: { "app.kubernetes.io/managed-by": "unidesk", "app.kubernetes.io/part-of": "platform-infra" },
annotations: {
"argocd.argoproj.io/sync-wave": "0",
"unidesk.ai/pac-provenance-version": contract.version,
"unidesk.ai/pac-controller-identity": contract.controllerUsername,
"unidesk.ai/owning-config-ref": `${configPath}#deliveryProvenance`,
"unidesk.ai/effective-config-sha256": policyHash,
},
};
const policy = {
apiVersion: "admissionregistration.k8s.io/v1",
kind: "ValidatingAdmissionPolicy",
metadata: { name: contract.policyName, ...metadata },
spec: policySpec,
};
const binding = {
apiVersion: "admissionregistration.k8s.io/v1",
kind: "ValidatingAdmissionPolicyBinding",
metadata: {
name: contract.bindingName,
...metadata,
annotations: { ...metadata.annotations, "argocd.argoproj.io/sync-wave": "1" },
},
spec: bindingSpec,
};
return [policy, binding].map((item) => Bun.YAML.stringify(item).trim()).join("\n---\n") + "\n";
}
export function pacAdmissionDesiredIdentity(targetId: string): PacAdmissionDesiredIdentity {
const contract = readPacAdmissionContract();
const manifest = renderPacAdmissionDesiredFragment(targetId);
const policy = manifest.length === 0 ? null : record(Bun.YAML.parse(manifest.split(/^---\s*$/mu)[0] ?? ""), "rendered admission policy");
const policyMetadata = policy === null ? {} : record(policy.metadata, "rendered admission policy metadata");
const annotations = policy === null ? {} : record(policyMetadata.annotations, "rendered admission policy annotations");
return {
configSha256: policy === null
? `sha256:${createHash("sha256").update(stableCanonicalJson({ targetId, disabled: true })).digest("hex")}`
: required(annotations["unidesk.ai/effective-config-sha256"], "rendered admission policy effective config sha256"),
policyName: contract.policyName,
bindingName: contract.bindingName,
version: contract.version,
controllerIdentity: contract.controllerUsername,
};
}
function admissionSpecSha256(targetId: string, policyName: string, bindingName: string, policySpec: Record<string, unknown>, bindingSpec: Record<string, unknown>): string {
const input = { targetId, policyName, bindingName, policySpec, bindingSpec };
return `sha256:${createHash("sha256").update(stableCanonicalJson(input)).digest("hex")}`;
}
function stableCanonicalJson(value: unknown): string {
if (Array.isArray(value)) return `[${value.map(stableCanonicalJson).join(",")}]`;
if (typeof value === "object" && value !== null) {
return `{${Object.entries(value as Record<string, unknown>).sort(([left], [right]) => left.localeCompare(right)).map(([key, item]) => `${JSON.stringify(key)}:${stableCanonicalJson(item)}`).join(",")}}`;
}
return JSON.stringify(value);
}
function candidateExpression(consumer: PacAdmissionConsumerContract, root: "object" | "oldObject"): string {
return `${root}.metadata.namespace == ${cel(consumer.namespace)} && (${root}.metadata.name.startsWith(${cel(consumer.pipelineRunPrefix)}) || (has(${root}.metadata.labels) && ((${cel("tekton.dev/pipeline")} in ${root}.metadata.labels && ${root}.metadata.labels[${cel("tekton.dev/pipeline")}] == ${cel(consumer.pipeline)}) || (${cel("tekton.dev/pipelineName")} in ${root}.metadata.labels && ${root}.metadata.labels[${cel("tekton.dev/pipelineName")}] == ${cel(consumer.pipeline)}))) || (has(${root}.spec.pipelineRef) && has(${root}.spec.pipelineRef.name) && ${root}.spec.pipelineRef.name == ${cel(consumer.pipeline)}))`;
}
function usesServiceAccount(consumer: PacAdmissionConsumerContract, root: "object" | "oldObject"): string {
return `has(${root}.spec.taskRunTemplate) && has(${root}.spec.taskRunTemplate.serviceAccountName) && ${root}.spec.taskRunTemplate.serviceAccountName == ${cel(consumer.executionServiceAccountName)}`;
}
function metadataEntryEqual(map: "annotations" | "labels", key: string): string {
const current = `has(object.metadata.${map}) && ${cel(key)} in object.metadata.${map}`;
const previous = `has(oldObject.metadata.${map}) && ${cel(key)} in oldObject.metadata.${map}`;
return `((${current}) == (${previous}) && (!(${current}) || object.metadata.${map}[${cel(key)}] == oldObject.metadata.${map}[${cel(key)}]))`;
}
function serviceAccountEqual(): string {
const current = "has(object.spec.taskRunTemplate) && has(object.spec.taskRunTemplate.serviceAccountName)";
const previous = "has(oldObject.spec.taskRunTemplate) && has(oldObject.spec.taskRunTemplate.serviceAccountName)";
return `((${current}) == (${previous}) && (!(${current}) || object.spec.taskRunTemplate.serviceAccountName == oldObject.spec.taskRunTemplate.serviceAccountName))`;
}
function pipelineRefEqual(): string {
const current = "has(object.spec.pipelineRef)";
const previous = "has(oldObject.spec.pipelineRef)";
return `((${current}) == (${previous}) && (!(${current}) || object.spec.pipelineRef == oldObject.spec.pipelineRef))`;
}
function cel(value: string): string {
return JSON.stringify(value);
}
function record(value: unknown, path: string): Record<string, unknown> {
if (typeof value !== "object" || value === null || Array.isArray(value)) throw new Error(`${path} must be an object`);
return value as Record<string, unknown>;
}
function recordArray(value: unknown, path: string): Record<string, unknown>[] {
if (!Array.isArray(value)) throw new Error(`${path} must be an array`);
return value.map((item, index) => record(item, `${path}[${index}]`));
}
function required(value: unknown, path: string): string {
if (typeof value !== "string" || value.length === 0 || value.includes("\n")) throw new Error(`${path} must be a non-empty single-line string`);
return value;
}
function stringArray(value: unknown, path: string): string[] {
if (!Array.isArray(value) || value.length === 0 || value.some((item) => typeof item !== "string" || item.length === 0)) throw new Error(`${path} must be a non-empty string array`);
return value as string[];
}
function literal<T extends string | boolean>(value: unknown, path: string, expected: T): T {
if (value !== expected) throw new Error(`${path} must be ${expected}`);
return expected;
}
@@ -163,6 +163,7 @@ EOF
}
consumer_rbac_manifest() {
pipeline_verbs='["get", "list", "watch", "create", "patch", "update"]'
cat <<EOF
apiVersion: rbac.authorization.k8s.io/v1
kind: Role
@@ -176,7 +177,7 @@ metadata:
rules:
- apiGroups: ["tekton.dev"]
resources: ["pipelineruns", "taskruns"]
verbs: ["get", "list", "watch", "create", "patch", "update"]
verbs: $pipeline_verbs
- apiGroups: [""]
resources: ["pods", "pods/log"]
verbs: ["get", "list", "watch"]
@@ -229,7 +230,14 @@ apply_action() {
fi
apply_release
kubectl create ns "$UNIDESK_PAC_TARGET_NAMESPACE" --dry-run=client -o yaml | kubectl apply -f - >/dev/null
consumer_rbac_manifest | kubectl apply --server-side --force-conflicts --field-manager="$UNIDESK_PAC_FIELD_MANAGER" -f - >/dev/null
if [ "${UNIDESK_PAC_DELIVERY_PROVENANCE_REQUIRED:-0}" = "1" ]; then
rbac_tmp=$(mktemp)
printf '%s' "$UNIDESK_PAC_CONSUMER_RBAC_MANIFEST_B64" | base64 -d >"$rbac_tmp"
kubectl apply --server-side --force-conflicts --field-manager="$UNIDESK_PAC_FIELD_MANAGER-compatibility-bootstrap" -f "$rbac_tmp" >/dev/null
rm -f "$rbac_tmp"
else
consumer_rbac_manifest | kubectl apply --server-side --force-conflicts --field-manager="$UNIDESK_PAC_FIELD_MANAGER" -f - >/dev/null
fi
token=$(ensure_token)
test -n "$token"
kubectl -n "$UNIDESK_PAC_TARGET_NAMESPACE" create secret generic "$UNIDESK_PAC_SECRET_NAME" \
@@ -256,6 +264,63 @@ condition_status() {
kubectl -n "$1" get "$2" "$3" -o jsonpath='{range .status.conditions[*]}{.type}={.status}:{.reason}{";"}{end}' 2>/dev/null || true
}
admission_provenance_state() {
if [ "${UNIDESK_PAC_DELIVERY_PROVENANCE_REQUIRED:-0}" != "1" ]; then
printf '{"configured":false,"required":false,"ready":null,"reasons":["consumer-admission-provenance-not-required"],"valuesPrinted":false}'
return
fi
policy_file=$(mktemp)
binding_file=$(mktemp)
role_file=$(mktemp)
role_binding_file=$(mktemp)
kubectl get validatingadmissionpolicy "$UNIDESK_PAC_ADMISSION_POLICY_NAME" -o json >"$policy_file" 2>/dev/null || printf '{}' >"$policy_file"
kubectl get validatingadmissionpolicybinding "$UNIDESK_PAC_ADMISSION_BINDING_NAME" -o json >"$binding_file" 2>/dev/null || printf '{}' >"$binding_file"
kubectl -n "$UNIDESK_PAC_TARGET_NAMESPACE" get role unidesk-pac-consumer-runner -o json >"$role_file" 2>/dev/null || printf '{}' >"$role_file"
kubectl -n "$UNIDESK_PAC_TARGET_NAMESPACE" get rolebinding unidesk-pac-consumer-runner-default -o json >"$role_binding_file" 2>/dev/null || printf '{}' >"$role_binding_file"
default_can_mutate=false
for verb in create patch update delete deletecollection; do
for resource in pipelineruns.tekton.dev taskruns.tekton.dev; do
if answer=$(timeout "$UNIDESK_PAC_WAIT_TIMEOUT_SECONDS" kubectl auth can-i --as="system:serviceaccount:$UNIDESK_PAC_TARGET_NAMESPACE:default" "$verb" "$resource" -n "$UNIDESK_PAC_TARGET_NAMESPACE" 2>/dev/null); then :; else :; fi
case "$answer" in
yes) default_can_mutate=true ;;
no) ;;
*) [ "$default_can_mutate" = true ] || default_can_mutate=unknown ;;
esac
done
done
export UNIDESK_PAC_DEFAULT_CAN_MUTATE="$default_can_mutate"
node - "$policy_file" "$binding_file" "$role_file" "$role_binding_file" <<'NODE'
const fs = require('node:fs');
const { evaluatePacAdmissionState } = require(process.env.UNIDESK_PAC_EVALUATOR_PATH);
function read(path) {
try { return JSON.parse(fs.readFileSync(path, 'utf8') || '{}'); } catch { return {}; }
}
process.stdout.write(JSON.stringify(evaluatePacAdmissionState({
policy: read(process.argv[2]),
binding: read(process.argv[3]),
role: read(process.argv[4]),
roleBinding: read(process.argv[5]),
namespace: process.env.UNIDESK_PAC_TARGET_NAMESPACE || null,
defaultCanMutate: process.env.UNIDESK_PAC_DEFAULT_CAN_MUTATE === 'unknown' ? null : process.env.UNIDESK_PAC_DEFAULT_CAN_MUTATE === 'true',
expected: {
targetId: process.env.UNIDESK_PAC_TARGET_ID || '',
policyName: process.env.UNIDESK_PAC_ADMISSION_POLICY_NAME || '',
bindingName: process.env.UNIDESK_PAC_ADMISSION_BINDING_NAME || '',
version: process.env.UNIDESK_PAC_ADMISSION_VERSION || '',
controllerIdentity: process.env.UNIDESK_PAC_ADMISSION_CONTROLLER_IDENTITY || '',
configSha256: process.env.UNIDESK_PAC_ADMISSION_CONFIG_SHA256 || '',
rbacConfigSha256: process.env.UNIDESK_PAC_CONSUMER_RBAC_CONFIG_SHA256 || '',
},
})));
NODE
rm -f "$policy_file" "$binding_file" "$role_file" "$role_binding_file"
}
prepare_admission_provenance_state() {
UNIDESK_PAC_ADMISSION_STATE_JSON=$(admission_provenance_state)
export UNIDESK_PAC_ADMISSION_STATE_JSON
}
pipeline_rows() {
payload_file=$(mktemp)
kubectl -n "$UNIDESK_PAC_TARGET_NAMESPACE" get pipelinerun -o json >"$payload_file" 2>/dev/null || printf '{"items":[]}' >"$payload_file"
@@ -310,14 +375,8 @@ function durationSeconds(item) {
if (!start) return null;
return Math.max(0, Math.round(((done ? Date.parse(done) : Date.now()) - Date.parse(start)) / 1000));
}
const prefix = process.env.UNIDESK_PAC_PIPELINE_RUN_PREFIX;
const pipeline = process.env.UNIDESK_PAC_PIPELINE_NAME;
const consumer = {
id: process.env.UNIDESK_PAC_CONSUMER_ID,
repository: process.env.UNIDESK_PAC_REPOSITORY_NAME,
pipelineRunPrefix: prefix,
pipeline,
};
const consumer = JSON.parse(Buffer.from(process.env.UNIDESK_PAC_CONSUMER_CONFIG_B64 || 'e30=', 'base64').toString('utf8'));
consumer.admissionState = JSON.parse(process.env.UNIDESK_PAC_ADMISSION_STATE_JSON || '{}');
const rows = (data.items || [])
.filter((item) => pipelineRunMatchesConsumer(consumer, item))
.map((item) => ({ item, classification: classifyPacPipelineRun(consumer, item) }))
@@ -350,7 +409,7 @@ history_rows() {
node <<'NODE'
const fs = require('node:fs');
const cp = require('node:child_process');
const { classifyPacPipelineRun, extractPacArtifactEvidence, parsePacLogRecords, pipelineRunMatchesConsumer, taskTerminalRecord } = require(process.env.UNIDESK_PAC_EVALUATOR_PATH);
const { classifyPacPipelineRun, evaluatePacAdmissionState, extractPacArtifactEvidence, parsePacLogRecords, pipelineRunMatchesConsumer, taskTerminalRecord } = require(process.env.UNIDESK_PAC_EVALUATOR_PATH);
const limit = Math.max(1, Math.min(50, Number.parseInt(process.env.UNIDESK_PAC_HISTORY_LIMIT || '5', 10) || 5));
const detailId = process.env.UNIDESK_PAC_HISTORY_ID || '';
const consumers = JSON.parse(Buffer.from(process.env.UNIDESK_PAC_HISTORY_CONSUMERS_B64 || 'W10=', 'base64').toString('utf8'));
@@ -408,6 +467,65 @@ function kubectlJson(args, fallback, context) {
}
}
let admissionPolicy = null;
let admissionBinding = null;
const admissionRbac = new Map();
function defaultCanMutate(namespace) {
const waitTimeoutSeconds = Number.parseInt(process.env.UNIDESK_PAC_WAIT_TIMEOUT_SECONDS || '', 10);
if (!Number.isInteger(waitTimeoutSeconds) || waitTimeoutSeconds <= 0) {
kubectlJsonErrors.push({ context: `${namespace}:default-can-mutation`, status: null, error: 'invalid-yaml-wait-timeout', stderr: '' });
return null;
}
for (const verb of ['create', 'patch', 'update', 'delete', 'deletecollection']) {
for (const resource of ['pipelineruns.tekton.dev', 'taskruns.tekton.dev']) {
const result = cp.spawnSync('kubectl', ['auth', 'can-i', `--as=system:serviceaccount:${namespace}:default`, verb, resource, '-n', namespace], {
encoding: 'utf8',
timeout: waitTimeoutSeconds * 1000,
});
const answer = String(result.stdout || '').trim();
if (answer === 'yes') return true;
if (answer !== 'no' || result.error || (result.status !== 0 && result.status !== 1)) {
kubectlJsonErrors.push({ context: `${namespace}:default-can-${verb}-${resource}`, status: result.status, error: result.error ? String(result.error.message || result.error) : null, stderr: String(result.stderr || '').slice(0, 1000) });
return null;
}
}
}
return false;
}
function admissionStateForConsumer(consumer) {
const expected = consumer.deliveryProvenance;
if (!expected || expected.required !== true) return { configured: false, required: false, ready: null, reasons: ['consumer-admission-provenance-not-required'], valuesPrinted: false };
admissionPolicy ||= kubectlJson(['get', 'validatingadmissionpolicy', expected.policyName, '-o', 'json'], {}, 'admission-policy');
admissionBinding ||= kubectlJson(['get', 'validatingadmissionpolicybinding', expected.bindingName, '-o', 'json'], {}, 'admission-binding');
if (!admissionRbac.has(consumer.namespace)) {
admissionRbac.set(consumer.namespace, {
role: kubectlJson(['-n', consumer.namespace, 'get', 'role', 'unidesk-pac-consumer-runner', '-o', 'json'], {}, `${consumer.id}:default-role`),
roleBinding: kubectlJson(['-n', consumer.namespace, 'get', 'rolebinding', 'unidesk-pac-consumer-runner-default', '-o', 'json'], {}, `${consumer.id}:default-rolebinding`),
defaultCanMutate: defaultCanMutate(consumer.namespace),
});
}
const rbac = admissionRbac.get(consumer.namespace);
return evaluatePacAdmissionState({
policy: admissionPolicy,
binding: admissionBinding,
role: rbac.role,
roleBinding: rbac.roleBinding,
namespace: consumer.namespace,
defaultCanMutate: rbac.defaultCanMutate,
expected: {
targetId: expected.targetId,
policyName: expected.policyName,
bindingName: expected.bindingName,
version: expected.version,
controllerIdentity: expected.controllerIdentity,
configSha256: expected.configSha256,
rbacConfigSha256: expected.rbacConfigSha256,
},
});
}
function condition(item) {
const c = (item.status?.conditions || []).find((x) => x.type === 'Succeeded') || {};
return { status: c.status || '', reason: c.reason || '', message: c.message || '' };
@@ -671,6 +789,7 @@ function rowFor(consumer, item, taskRuns) {
const rows = [];
const consumerSummaries = [];
for (const consumer of consumers) {
consumer.admissionState = admissionStateForConsumer(consumer);
const pipelineRuns = kubectlJson(['-n', consumer.namespace, 'get', 'pipelinerun', '-o', 'json'], { items: [] }, `${consumer.id}:pipelinerun`);
const taskRuns = kubectlJson(['-n', consumer.namespace, 'get', 'taskrun', '-o', 'json'], { items: [] }, `${consumer.id}:taskrun`);
let matches = (pipelineRuns.items || []).filter((item) => {
@@ -914,25 +1033,35 @@ NODE
observed=$(printf '%s\n' "$fields" | sed -n 's/^observed=//p' | base64 -d 2>/dev/null || true)
repo_url=$(printf '%s\n' "$fields" | sed -n 's/^repoUrl=//p' | base64 -d 2>/dev/null || true)
target_revision=$(printf '%s\n' "$fields" | sed -n 's/^targetRevision=//p' | base64 -d 2>/dev/null || true)
expected_repo_url="${UNIDESK_PAC_GITOPS_EXPECTED_REPO_URL:-}"
expected_target_revision="${UNIDESK_PAC_GITOPS_EXPECTED_TARGET_REVISION:-}"
[ -n "$expected_repo_url" ] || expected_repo_url="$repo_url"
[ -n "$expected_target_revision" ] || expected_target_revision="$target_revision"
normalized_expected_repo=$(printf '%s' "$expected_repo_url" | sed 's#/*$##')
normalized_observed_repo=$(printf '%s' "$repo_url" | sed 's#/*$##')
retained=$(printf '%s\n' "$fields" | sed -n 's/^retained=//p' | base64 -d 2>/dev/null || true)
relation=unknown
proof=unavailable
reason=missing-revision-context
fetch_ok=false
if [ "$retained" = true ] && printf '%s' "$observed" | grep -Eq '^([0-9a-fA-F]{40}|[0-9a-fA-F]{64})$'; then
if [ "$normalized_expected_repo" != "$normalized_observed_repo" ]; then
reason=repository-mismatch
elif [ "$expected_target_revision" != "$target_revision" ]; then
reason=target-revision-mismatch
elif [ "$retained" = true ] && printf '%s' "$observed" | grep -Eq '^([0-9a-fA-F]{40}|[0-9a-fA-F]{64})$'; then
relation=retained
proof=structured-no-runtime-change-plan
reason=source-observation-retains-current-revision
elif printf '%s\n%s\n' "$expected" "$observed" | grep -Eqv '^[0-9a-fA-F]{7,64}$'; then
reason=invalid-or-missing-revision
elif [ -z "$repo_url" ] \
|| ! git check-ref-format "refs/heads/$target_revision" >/dev/null 2>&1; then
elif [ -z "$expected_repo_url" ] \
|| ! git check-ref-format "refs/heads/$expected_target_revision" >/dev/null 2>&1; then
reason=invalid-or-missing-git-context
else
repo_dir=$(mktemp -d)
if resolved_url=$(resolve_git_service_url "$repo_url") \
if resolved_url=$(resolve_git_service_url "$expected_repo_url") \
&& git init --bare -q "$repo_dir/repo.git" \
&& timeout "$UNIDESK_PAC_WAIT_TIMEOUT_SECONDS" git --git-dir="$repo_dir/repo.git" fetch -q --no-tags "$resolved_url" "+refs/heads/$target_revision:refs/remotes/origin/$target_revision" >/dev/null 2>&1; then
&& timeout "$UNIDESK_PAC_WAIT_TIMEOUT_SECONDS" git --git-dir="$repo_dir/repo.git" fetch -q --no-tags "$resolved_url" "+refs/heads/$expected_target_revision:refs/remotes/origin/$expected_target_revision" >/dev/null 2>&1; then
fetch_ok=true
if git --git-dir="$repo_dir/repo.git" cat-file -e "$expected^{commit}" 2>/dev/null \
&& git --git-dir="$repo_dir/repo.git" cat-file -e "$observed^{commit}" 2>/dev/null; then
@@ -964,6 +1093,8 @@ NODE
UNIDESK_PAC_GITOPS_OBSERVED="$observed" \
UNIDESK_PAC_GITOPS_REPO_URL="$repo_url" \
UNIDESK_PAC_GITOPS_TARGET_REVISION="$target_revision" \
UNIDESK_PAC_GITOPS_EXPECTED_REPO_URL_OBSERVED="$expected_repo_url" \
UNIDESK_PAC_GITOPS_EXPECTED_TARGET_REVISION_OBSERVED="$expected_target_revision" \
UNIDESK_PAC_GITOPS_RELATION="$relation" \
UNIDESK_PAC_GITOPS_PROOF="$proof" \
UNIDESK_PAC_GITOPS_REASON="$reason" \
@@ -974,6 +1105,10 @@ process.stdout.write(JSON.stringify({
observedRevision: process.env.UNIDESK_PAC_GITOPS_OBSERVED || null,
repoURL: process.env.UNIDESK_PAC_GITOPS_REPO_URL || null,
targetRevision: process.env.UNIDESK_PAC_GITOPS_TARGET_REVISION || null,
expectedRepoURL: process.env.UNIDESK_PAC_GITOPS_EXPECTED_REPO_URL_OBSERVED || null,
expectedTargetRevision: process.env.UNIDESK_PAC_GITOPS_EXPECTED_TARGET_REVISION_OBSERVED || null,
observedRepoURL: process.env.UNIDESK_PAC_GITOPS_REPO_URL || null,
observedTargetRevision: process.env.UNIDESK_PAC_GITOPS_TARGET_REVISION || null,
proof: process.env.UNIDESK_PAC_GITOPS_PROOF || 'unavailable',
reason: process.env.UNIDESK_PAC_GITOPS_REASON || null,
fetchOk: process.env.UNIDESK_PAC_GITOPS_FETCH_OK === 'true',
@@ -1156,6 +1291,7 @@ status_action() {
crd=$(kubectl get crd repositories.pipelinesascode.tekton.dev -o name 2>/dev/null || true)
controller_ready=$(kubectl -n "$UNIDESK_PAC_RELEASE_NAMESPACE" get deploy "$UNIDESK_PAC_CONTROLLER_SERVICE_NAME" -o jsonpath='{.status.readyReplicas}/{.spec.replicas}' 2>/dev/null || echo "0/0")
repository_condition=$(condition_status "$UNIDESK_PAC_TARGET_NAMESPACE" repository "$UNIDESK_PAC_REPOSITORY_NAME")
prepare_admission_provenance_state
pipelines=$(pipeline_rows)
latest=$(printf '%s' "$pipelines" | node -e 'const fs=require("fs"); const a=JSON.parse(fs.readFileSync(0,"utf8")||"[]"); process.stdout.write(a[0]?.name||"")')
export UNIDESK_PAC_TARGET_PIPELINERUN="$latest"
@@ -1179,10 +1315,30 @@ NODE
)
runtime=$(json_normalize "$(runtime_summary)")
diagnostics=$(cicd_diagnostics "$pipelines" "$artifact" "$argo" "$runtime")
printf '{"ok":%s,"crdPresent":%s,"controllerReady":"%s","repository":{"name":"%s","repo":"%s/%s","url":"%s","condition":"%s"},"repositoryCondition":"%s","webhooks":%s,"pipelineRuns":%s,"taskRuns":%s,"artifact":%s,"argo":%s,"runtime":%s,"diagnostics":%s,"valuesPrinted":false}\n' \
"$( [ -n "$crd" ] && [ "$controller_ready" != "0/0" ] && echo true || echo false )" \
admission_ready=$(UNIDESK_PAC_STATE="$UNIDESK_PAC_ADMISSION_STATE_JSON" node -e 'const s=JSON.parse(process.env.UNIDESK_PAC_STATE||"{}"); process.stdout.write(s.ready===true?"true":"false")')
diagnostics=$(UNIDESK_PAC_DIAGNOSTICS="$diagnostics" UNIDESK_PAC_STATE="$UNIDESK_PAC_ADMISSION_STATE_JSON" node <<'NODE'
const diagnostics = JSON.parse(process.env.UNIDESK_PAC_DIAGNOSTICS || '{}');
const admissionProvenance = JSON.parse(process.env.UNIDESK_PAC_STATE || '{}');
if (process.env.UNIDESK_PAC_DELIVERY_PROVENANCE_REQUIRED === '1' && admissionProvenance.ready !== true) {
process.stdout.write(JSON.stringify({
...diagnostics,
ok: false,
code: 'pac-admission-provenance-not-ready',
phase: 'admission-provenance-not-ready',
hint: 'required PaC admission policy, binding, desired RBAC, or live default-SA mutation gate is not ready',
admissionProvenance,
valuesPrinted: false,
}));
} else {
process.stdout.write(JSON.stringify({ ...diagnostics, admissionProvenance, valuesPrinted: false }));
}
NODE
)
printf '{"ok":%s,"crdPresent":%s,"controllerReady":"%s","admissionProvenance":%s,"repository":{"name":"%s","repo":"%s/%s","url":"%s","condition":"%s"},"repositoryCondition":"%s","webhooks":%s,"pipelineRuns":%s,"taskRuns":%s,"artifact":%s,"argo":%s,"runtime":%s,"diagnostics":%s,"valuesPrinted":false}\n' \
"$( [ -n "$crd" ] && [ "$controller_ready" != "0/0" ] && { [ "${UNIDESK_PAC_DELIVERY_PROVENANCE_REQUIRED:-0}" != "1" ] || [ "$admission_ready" = "true" ]; } && echo true || echo false )" \
"$( [ -n "$crd" ] && echo true || echo false )" \
"$(json_string "$controller_ready")" \
"$UNIDESK_PAC_ADMISSION_STATE_JSON" \
"$(json_string "$UNIDESK_PAC_REPOSITORY_NAME")" \
"$(json_string "$UNIDESK_PAC_GITEA_OWNER")" \
"$(json_string "$UNIDESK_PAC_GITEA_REPO")" \
@@ -1197,14 +1353,16 @@ history_action() {
controller_ready=$(kubectl -n "$UNIDESK_PAC_RELEASE_NAMESPACE" get deploy "$UNIDESK_PAC_CONTROLLER_SERVICE_NAME" -o jsonpath='{.status.readyReplicas}/{.spec.replicas}' 2>/dev/null || echo "0/0")
repository_condition=$(condition_status "$UNIDESK_PAC_TARGET_NAMESPACE" repository "$UNIDESK_PAC_REPOSITORY_NAME")
hooks=$(hook_summary)
prepare_admission_provenance_state
history=$(history_rows)
rows=$(printf '%s' "$history" | node -e 'const fs=require("fs"); const h=JSON.parse(fs.readFileSync(0,"utf8")||"{}"); process.stdout.write(JSON.stringify(h.rows||[]));')
consumers=$(printf '%s' "$history" | node -e 'const fs=require("fs"); const h=JSON.parse(fs.readFileSync(0,"utf8")||"{}"); process.stdout.write(JSON.stringify(h.consumers||[]));')
errors=$(printf '%s' "$history" | node -e 'const fs=require("fs"); const h=JSON.parse(fs.readFileSync(0,"utf8")||"{}"); process.stdout.write(JSON.stringify(h.errors||[]));')
printf '{"ok":%s,"crdPresent":%s,"controllerReady":"%s","repositoryCondition":"%s","repository":{"name":"%s","repo":"%s/%s","url":"%s"},"consumer":"%s","display":{"timeZone":"%s"},"consumerRows":%s,"rows":%s,"historyErrors":%s,"webhooks":%s,"source":"gitea-pac-tekton-live","historyStore":"none","valuesPrinted":false}\n' \
printf '{"ok":%s,"crdPresent":%s,"controllerReady":"%s","admissionProvenance":%s,"repositoryCondition":"%s","repository":{"name":"%s","repo":"%s/%s","url":"%s"},"consumer":"%s","display":{"timeZone":"%s"},"consumerRows":%s,"rows":%s,"historyErrors":%s,"webhooks":%s,"source":"gitea-pac-tekton-live","historyStore":"none","valuesPrinted":false}\n' \
"$( [ -n "$crd" ] && [ "$controller_ready" != "0/0" ] && echo true || echo false )" \
"$( [ -n "$crd" ] && echo true || echo false )" \
"$(json_string "$controller_ready")" \
"$UNIDESK_PAC_ADMISSION_STATE_JSON" \
"$(json_string "$repository_condition")" \
"$(json_string "$UNIDESK_PAC_REPOSITORY_NAME")" \
"$(json_string "$UNIDESK_PAC_GITEA_OWNER")" \
@@ -1227,6 +1385,7 @@ NODE
FIXTURES_JSON="$fixtures" node -e 'const result=JSON.parse(process.env.FIXTURES_JSON||"{}"); if(!result.ok) process.exitCode=1'
return
fi
prepare_admission_provenance_state
history=$(history_rows)
UNIDESK_PAC_DEBUG_FIXTURES_JSON="$fixtures" UNIDESK_PAC_DEBUG_HISTORY_JSON="$history" node <<'NODE'
function record(value) {
@@ -1269,8 +1428,8 @@ if (historyErrors.length > 0) breakAt('target-read-failed', 'target-read', histo
if (Object.keys(row).length === 0) breakAt('pipeline-run-not-found', 'pipeline-run', `PipelineRun ${process.env.UNIDESK_PAC_HISTORY_ID || '-'} was not found for the selected consumer`);
if (Object.keys(row).length > 0 && row.deliveryAuthorityEligible !== true) breakAt('pipeline-run-not-delivery-authority', 'pipeline-run', `PipelineRun is classified as ${row.deliveryClass || 'unknown'}; only an outer PaC event can drive delivery closeout`);
if (Object.keys(row).length > 0 && row.status !== 'True') breakAt('pipeline-run-not-succeeded', 'pipeline-run', `PipelineRun terminal status is ${row.status || row.reason || 'unknown'}`);
if (Object.keys(row).length > 0 && Object.keys(sourceObservation).length === 0) breakAt('source-observation-missing', 'plan-artifacts', 'g14-ci-plan structured evidence is missing');
if (sourceObservation.sourceMatched !== true) breakAt('source-observation-mismatch', 'plan-artifacts', 'g14-ci-plan source commit does not match the selected PipelineRun');
if (Object.keys(row).length > 0 && Object.keys(sourceObservation).length === 0) breakAt('source-observation-missing', 'plan-artifacts', 'delivery plan structured evidence is missing');
if (sourceObservation.sourceMatched !== true) breakAt('source-observation-mismatch', 'plan-artifacts', 'delivery plan source commit does not match the selected PipelineRun');
if (sourceObservation.valid !== true) breakAt('source-observation-invalid', 'plan-artifacts', sourceObservation.reason || 'delivery plan is incomplete or contradictory');
for (const item of terminal) {
if (item.present !== true || item.status !== 'succeeded') breakAt('terminal-evidence-incomplete', item.step, `${item.step} TaskRun terminal status is ${item.status}`);
@@ -48,6 +48,11 @@ export interface PacSourceArtifactBinding {
readonly namespace: string;
readonly pipeline: string;
readonly pipelineRunPrefix: string;
readonly deliveryProvenance?: {
readonly markerAnnotation: string;
readonly markerValue: string;
readonly executionServiceAccountName: string;
} | null;
readonly sourceArtifact: PacSourceArtifactSpec;
};
readonly repository: {
@@ -635,6 +640,9 @@ 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),
...(binding.consumer.deliveryProvenance == null ? {} : {
[binding.consumer.deliveryProvenance.markerAnnotation]: binding.consumer.deliveryProvenance.markerValue,
}),
...pipelineProvenanceAnnotations(provenance),
};
}
@@ -661,7 +669,7 @@ function pipelineRunLabels(binding: PacSourceArtifactBinding, partOf: "agentrun"
function taskRunTemplate(binding: PacSourceArtifactBinding): Record<string, unknown> {
const declared = binding.consumer.sourceArtifact.taskRunTemplate;
return {
serviceAccountName: `{{ service_account }}`,
serviceAccountName: binding.consumer.deliveryProvenance?.executionServiceAccountName ?? `{{ service_account }}`,
podTemplate: {
hostNetwork: declared.hostNetwork,
dnsPolicy: declared.dnsPolicy,
+130 -1
View File
@@ -26,6 +26,8 @@ import {
type PacSourceArtifactRuntimeObservation,
type PacSourceArtifactSpec,
} from "./platform-infra-pipelines-as-code-source-artifact";
import { pacAdmissionDesiredIdentity } from "./platform-infra-pac-provenance";
import { pacConsumerRbacDesiredIdentity, renderPacConsumerRbacDesiredFragment } from "./platform-infra-pac-consumer-rbac";
const configFile = rootPath("config", "platform-infra", "pipelines-as-code.yaml");
const configLabel = "config/platform-infra/pipelines-as-code.yaml";
@@ -76,6 +78,13 @@ interface PacConfig {
controllerServicePort: number;
waitTimeoutSeconds: number;
};
deliveryProvenance: {
version: string;
markerAnnotation: string;
controllerIdentity: string;
policyName: string;
bindingName: string;
};
capabilities: {
sentinelInternalPublish: {
enabled: boolean;
@@ -146,6 +155,12 @@ interface PacConsumer {
destinationNamespace: string;
automated: boolean;
} | null;
deliveryProvenance: {
required: true;
markerValue: string;
executionServiceAccountName: string;
gitOps: { repoUrl: string; targetRevision: string };
} | null;
repositoryRef: string;
closeoutGitOpsMirrorFlush: boolean;
closeoutGitOpsMirrorLane: "v02" | "v03" | null;
@@ -256,6 +271,11 @@ export async function runPlatformInfraPipelinesAsCodeCommand(config: UniDeskConf
namespace: consumer.namespace,
pipeline: consumer.pipeline,
pipelineRunPrefix: consumer.pipelineRunPrefix,
deliveryProvenance: consumer.deliveryProvenance === null ? null : {
markerAnnotation: pac.deliveryProvenance.markerAnnotation,
markerValue: consumer.deliveryProvenance.markerValue,
executionServiceAccountName: consumer.deliveryProvenance.executionServiceAccountName,
},
sourceArtifact: consumer.sourceArtifact,
},
repository: {
@@ -363,6 +383,9 @@ function help(scope: string | null): Record<string, unknown> {
function readPacConfig(): PacConfig {
const root = materializeYamlComposition(readYamlRecord<Record<string, unknown>>(configFile, "platform-infra-pipelines-as-code"), { label: configLabel }).value;
const release = y.objectField(root, "release", "");
const deliveryProvenance = y.objectField(root, "deliveryProvenance", "");
const provenanceController = y.objectField(deliveryProvenance, "controller", "deliveryProvenance");
const provenanceAdmission = y.objectField(deliveryProvenance, "admission", "deliveryProvenance");
const capabilities = y.objectField(root, "capabilities", "");
const sentinelInternalPublish = y.objectField(capabilities, "sentinelInternalPublish", "capabilities");
const closeout = y.objectField(root, "closeout", "");
@@ -402,6 +425,13 @@ function readPacConfig(): PacConfig {
controllerServicePort: y.portField(release, "controllerServicePort", "release"),
waitTimeoutSeconds: positiveInteger(release, "waitTimeoutSeconds", "release"),
},
deliveryProvenance: {
version: y.stringField(deliveryProvenance, "version", "deliveryProvenance"),
markerAnnotation: y.stringField(deliveryProvenance, "markerAnnotation", "deliveryProvenance"),
controllerIdentity: `system:serviceaccount:${y.kubernetesNameField(provenanceController, "namespace", "deliveryProvenance.controller")}:${y.kubernetesNameField(provenanceController, "serviceAccountName", "deliveryProvenance.controller")}`,
policyName: y.kubernetesNameField(provenanceAdmission, "policyName", "deliveryProvenance.admission"),
bindingName: y.kubernetesNameField(provenanceAdmission, "bindingName", "deliveryProvenance.admission"),
},
capabilities: {
sentinelInternalPublish: {
enabled: y.booleanField(sentinelInternalPublish, "enabled", "capabilities.sentinelInternalPublish"),
@@ -464,6 +494,7 @@ function parseRepository(repository: Record<string, unknown>, path: string): Pac
function parseConsumer(consumer: Record<string, unknown>, path: string, defaultRepositoryRef: string): PacConsumer {
const id = typeof consumer.id === "string" && consumer.id.length > 0 ? consumer.id : `${y.stringField(consumer, "node", path)}-${y.stringField(consumer, "lane", path)}`;
const argoBootstrap = consumer.argoBootstrap === undefined ? null : y.objectField(consumer, "argoBootstrap", path);
const deliveryProvenance = consumer.deliveryProvenance === undefined ? null : parseConsumerDeliveryProvenance(y.objectField(consumer, "deliveryProvenance", path), `${path}.deliveryProvenance`);
return {
id,
node: y.stringField(consumer, "node", path),
@@ -481,6 +512,7 @@ function parseConsumer(consumer: Record<string, unknown>, path: string, defaultR
destinationNamespace: y.kubernetesNameField(argoBootstrap, "destinationNamespace", `${path}.argoBootstrap`),
automated: y.booleanField(argoBootstrap, "automated", `${path}.argoBootstrap`),
},
deliveryProvenance,
repositoryRef: typeof consumer.repositoryRef === "string" && consumer.repositoryRef.length > 0 ? consumer.repositoryRef : defaultRepositoryRef,
closeoutGitOpsMirrorFlush: y.booleanField(consumer, "closeoutGitOpsMirrorFlush", path),
closeoutGitOpsMirrorLane: consumer.closeoutGitOpsMirrorLane === undefined ? null : y.enumField(consumer, "closeoutGitOpsMirrorLane", path, ["v02", "v03"] as const),
@@ -488,6 +520,21 @@ function parseConsumer(consumer: Record<string, unknown>, path: string, defaultR
};
}
function parseConsumerDeliveryProvenance(value: Record<string, unknown>, path: string): PacConsumer["deliveryProvenance"] {
const required = y.booleanField(value, "required", path);
if (!required) return null;
const gitOps = y.objectField(value, "gitOps", path);
return {
required: true,
markerValue: y.stringField(value, "markerValue", path),
executionServiceAccountName: y.kubernetesNameField(value, "executionServiceAccountName", path),
gitOps: {
repoUrl: urlField(gitOps, "repoUrl", `${path}.gitOps`),
targetRevision: y.stringField(gitOps, "targetRevision", `${path}.gitOps`),
},
};
}
function parseSourceArtifact(value: Record<string, unknown>, path: string): PacSourceArtifactSpec {
const mode = y.enumField(value, "mode", path, ["embedded-pipeline-spec", "remote-pipeline-annotation"] as const);
const renderer = y.enumField(value, "renderer", path, ["agentrun-control-plane", "hwlab-runtime-lane"] as const);
@@ -707,11 +754,25 @@ function validateConfig(config: PacConfig): void {
if (new URL(repository.cloneUrl).origin !== new URL(config.gitea.internalBaseUrl).origin) throw new Error(`${configLabel}.repositories.${repository.id}.cloneUrl must use the configured internal Gitea base URL`);
}
const consumerIds = new Set<string>();
const markerValues = new Set<string>();
for (const consumer of config.consumers) {
if (consumerIds.has(consumer.id)) throw new Error(`${configLabel}.consumers id must be unique: ${consumer.id}`);
consumerIds.add(consumer.id);
const repository = resolveRepository(config, consumer.repositoryRef);
if (repository.namespace !== consumer.namespace) throw new Error(`${configLabel}.consumers.${consumer.id}.namespace must match repository namespace`);
if (consumer.deliveryProvenance !== null) {
if (consumer.sourceArtifact === null) throw new Error(`${configLabel}.consumers.${consumer.id}.deliveryProvenance requires sourceArtifact ownership`);
if (markerValues.has(consumer.deliveryProvenance.markerValue)) throw new Error(`${configLabel}.consumers deliveryProvenance.markerValue must be unique: ${consumer.deliveryProvenance.markerValue}`);
markerValues.add(consumer.deliveryProvenance.markerValue);
if (repository.params.service_account !== consumer.deliveryProvenance.executionServiceAccountName) {
throw new Error(`${configLabel}.consumers.${consumer.id}.deliveryProvenance.executionServiceAccountName must match repository params.service_account`);
}
if (consumer.argoBootstrap !== null
&& (consumer.argoBootstrap.repoUrl !== consumer.deliveryProvenance.gitOps.repoUrl
|| consumer.argoBootstrap.targetRevision !== consumer.deliveryProvenance.gitOps.targetRevision)) {
throw new Error(`${configLabel}.consumers.${consumer.id}.deliveryProvenance.gitOps must match argoBootstrap source identity`);
}
}
}
if (!config.gitea.webhook.events.includes("push")) throw new Error(`${configLabel}.gitea.webhook.events must include push`);
validateTimeZone(config.display.timeZone, `${configLabel}.display.timeZone`);
@@ -1029,6 +1090,7 @@ async function history(config: UniDeskConfig, options: HistoryOptions): Promise<
repositoryCondition: parsed.repositoryCondition,
repository: record(parsed.repository),
consumerRows: arrayRecords(parsed.consumerRows),
admissionProvenance: record(parsed.admissionProvenance),
webhookCount: arrayRecords(parsed.webhooks).length,
valuesPrinted: false,
},
@@ -1083,6 +1145,8 @@ async function debugStep(config: UniDeskConfig, options: HistoryOptions): Promis
const repository = resolveRepository(pac, consumer.repositoryRef);
const result = await capture(config, target.route, ["sh"], remoteScript("debug-step", pac, target, repository, consumer, { ...options, confirm: false, dryRun: true, wait: false }, emptySecretMaterial(), ""));
const parsed = parseJsonOutput(result.stdout);
const fixtureChecks = parsed === null ? [] : arrayRecords(parsed.checks);
const fixtureDisclosure = pacDebugFixtureDisclosure(fixtureChecks, options.detailId);
return {
ok: result.exitCode === 0 && parsed?.ok === true,
action: "platform-infra-pipelines-as-code-debug-step",
@@ -1091,7 +1155,8 @@ async function debugStep(config: UniDeskConfig, options: HistoryOptions): Promis
consumer: consumer.id,
detailId: options.detailId,
evaluator: "scripts/native/cicd/pac-status-evaluator.cjs",
checks: parsed === null ? [] : arrayRecords(parsed.checks),
fixtureGate: { ...fixtureDisclosure.fixtureGate, ok: parsed !== null && fixtureDisclosure.fixtureGate.ok === true },
checks: fixtureDisclosure.checks,
realRun: parsed === null ? null : parsed.realRun ?? null,
remote: parsed === null || options.raw ? parsed ?? compactCapture(result, { full: true }) : undefined,
next: nextCommands(target.id, consumer.id, pac.defaults.consumerId),
@@ -1099,6 +1164,24 @@ async function debugStep(config: UniDeskConfig, options: HistoryOptions): Promis
};
}
export function pacDebugFixtureDisclosure(fixtureChecks: readonly Record<string, unknown>[], detailId: string | null): {
readonly fixtureGate: Record<string, unknown>;
readonly checks: readonly Record<string, unknown>[];
} {
const failedChecks = fixtureChecks.filter((item) => item.ok !== true);
return {
fixtureGate: {
ok: failedChecks.length === 0,
total: fixtureChecks.length,
passed: fixtureChecks.length - failedChecks.length,
failed: failedChecks.length,
disclosure: detailId === null ? "all-checks" : "failed-checks-only-for-id-specific-debug",
valuesPrinted: false,
},
checks: detailId === null ? fixtureChecks : failedChecks,
};
}
async function fetchReleaseManifest(pac: PacConfig): Promise<string> {
const response = await fetch(pac.release.manifestUrl);
if (!response.ok) throw new Error(`failed to fetch ${pac.release.manifestUrl}: HTTP ${response.status}`);
@@ -1109,6 +1192,9 @@ async function fetchReleaseManifest(pac: PacConfig): Promise<string> {
function remoteScript(action: "apply" | "status" | "history" | "debug-step", pac: PacConfig, target: PacTarget, repository: PacRepository, consumer: PacConsumer, options: ApplyOptions | HistoryOptions, secrets: SecretMaterial, releaseManifest: string, historyConsumers: PacConsumer[] = [consumer]): string {
const webhookUrl = `${pac.gitea.internalBaseUrl.replace(/\/+$/u, "").replace(/gitea-http\.[^.]+\.svc\.cluster\.local:3000/u, `${pac.release.controllerServiceName}.${pac.release.namespace}.svc.cluster.local:${pac.release.controllerServicePort}`)}`;
const admissionIdentity = pacAdmissionDesiredIdentity(target.id);
const rbacIdentity = pacConsumerRbacDesiredIdentity(target.id);
const consumerConfig = historyConsumerConfig(pac, consumer);
const env: Record<string, string> = {
UNIDESK_PAC_ACTION: action,
UNIDESK_PAC_EVALUATOR_B64: Buffer.from(readFileSync(evaluatorFile, "utf8"), "utf8").toString("base64"),
@@ -1138,6 +1224,17 @@ function remoteScript(action: "apply" | "status" | "history" | "debug-step", pac
UNIDESK_PAC_PARAMS_JSON: JSON.stringify(repository.params),
UNIDESK_PAC_PIPELINE_NAME: consumer.pipeline,
UNIDESK_PAC_PIPELINE_RUN_PREFIX: consumer.pipelineRunPrefix,
UNIDESK_PAC_CONSUMER_CONFIG_B64: Buffer.from(JSON.stringify(consumerConfig), "utf8").toString("base64"),
UNIDESK_PAC_DELIVERY_PROVENANCE_REQUIRED: consumer.deliveryProvenance?.required === true ? "1" : "0",
UNIDESK_PAC_CONSUMER_RBAC_MANIFEST_B64: Buffer.from(renderPacConsumerRbacDesiredFragment(target.id), "utf8").toString("base64"),
UNIDESK_PAC_CONSUMER_RBAC_CONFIG_SHA256: rbacIdentity.configSha256,
UNIDESK_PAC_ADMISSION_POLICY_NAME: admissionIdentity.policyName,
UNIDESK_PAC_ADMISSION_BINDING_NAME: admissionIdentity.bindingName,
UNIDESK_PAC_ADMISSION_VERSION: admissionIdentity.version,
UNIDESK_PAC_ADMISSION_CONTROLLER_IDENTITY: admissionIdentity.controllerIdentity,
UNIDESK_PAC_ADMISSION_CONFIG_SHA256: admissionIdentity.configSha256,
UNIDESK_PAC_GITOPS_EXPECTED_REPO_URL: consumer.deliveryProvenance?.gitOps.repoUrl ?? "",
UNIDESK_PAC_GITOPS_EXPECTED_TARGET_REVISION: consumer.deliveryProvenance?.gitOps.targetRevision ?? "",
UNIDESK_PAC_HISTORY_LIMIT: "limit" in options ? String(options.limit) : "5",
UNIDESK_PAC_HISTORY_ID: "detailId" in options && options.detailId !== null ? options.detailId : "",
UNIDESK_PAC_HISTORY_CONSUMERS_B64: Buffer.from(JSON.stringify(historyConsumers.map((item) => historyConsumerConfig(pac, item))), "utf8").toString("base64"),
@@ -1154,14 +1251,31 @@ function remoteScript(action: "apply" | "status" | "history" | "debug-step", pac
function historyConsumerConfig(pac: PacConfig, consumer: PacConsumer): Record<string, unknown> {
const repository = resolveRepository(pac, consumer.repositoryRef);
const admissionIdentity = pacAdmissionDesiredIdentity(consumer.node);
const rbacIdentity = pacConsumerRbacDesiredIdentity(consumer.node);
return {
id: consumer.id,
node: consumer.node,
namespace: consumer.namespace,
pipeline: consumer.pipeline,
pipelineRunPrefix: consumer.pipelineRunPrefix,
repository: repository.name,
repo: `${repository.owner}/${repository.repo}`,
repoUrl: repository.url,
deliveryProvenance: consumer.deliveryProvenance === null ? null : {
required: true,
targetId: consumer.node,
version: admissionIdentity.version,
controllerIdentity: admissionIdentity.controllerIdentity,
policyName: admissionIdentity.policyName,
bindingName: admissionIdentity.bindingName,
configSha256: admissionIdentity.configSha256,
rbacConfigSha256: rbacIdentity.configSha256,
markerAnnotation: pac.deliveryProvenance.markerAnnotation,
markerValue: consumer.deliveryProvenance.markerValue,
executionServiceAccountName: consumer.deliveryProvenance.executionServiceAccountName,
gitOps: consumer.deliveryProvenance.gitOps,
},
};
}
@@ -1296,6 +1410,7 @@ function statusSummary(payload: Record<string, unknown>): Record<string, unknown
const argo = record(payload.argo);
const runtime = record(payload.runtime);
const diagnostics = record(payload.diagnostics);
const admissionProvenance = record(payload.admissionProvenance);
const webhooks = arrayRecords(payload.webhooks);
const rawArtifact = record(payload.artifact);
const sourceObservation = record(rawArtifact.sourceObservation);
@@ -1328,6 +1443,7 @@ function statusSummary(payload: Record<string, unknown>): Record<string, unknown
argo,
runtime,
diagnostics,
admissionProvenance,
valuesPrinted: false,
};
}
@@ -1612,6 +1728,7 @@ function compactStatusSummary(summary: Record<string, unknown>): Record<string,
},
},
pipelineRunGate: summary.pipelineRunGate,
admissionProvenance: summary.admissionProvenance,
sourceObservation: summary.sourceObservation,
artifact: {
imageStatus: artifact.imageStatus,
@@ -1709,6 +1826,11 @@ function compactHistoryJson(result: Record<string, unknown>, includeTaskDetails
reason: row.reason,
commit: row.commit,
branch: row.branch,
deliveryClass: row.deliveryClass,
deliveryAuthorityEligible: row.deliveryAuthorityEligible === true,
deliveryOwner: row.deliveryOwner,
parentRelation: row.parentRelation,
classification: row.classification,
envReuse: row.envReuse,
sourceObservation: row.sourceObservation,
artifact: detailRequested ? {
@@ -1827,6 +1949,7 @@ function renderStatus(result: Record<string, unknown>): RenderedCliResult {
const artifact = record(summary.artifact);
const argo = record(summary.argo);
const diagnostics = record(summary.diagnostics);
const admissionProvenance = record(summary.admissionProvenance);
const sourceObservation = record(summary.sourceObservation);
const deliveryPlan = record(sourceObservation.plan);
const repository = record(summary.repository);
@@ -1841,6 +1964,12 @@ function renderStatus(result: Record<string, unknown>): RenderedCliResult {
...table(["CONSUMER", "READY", "AUTHORITY", "CRD", "CONTROLLER", "WEBHOOKS", "REPO_URL"], [[stringValue(consumer.id), boolText(summary.ready), stringValue(deliveryAuthority.kind), boolText(summary.crdPresent), stringValue(summary.controllerReady), stringValue(summary.webhookCount), short(stringValue(repository.url), 56)]]),
` repository-condition: ${compactLine(stringValue(summary.repositoryCondition))}`,
` sentinel-internal-publish: enabled=${boolText(internalPublish.enabled)} provenance=${stringValue(internalPublish.admissionProvenance)} tracking=${stringValue(internalPublish.trackingIssue)}`,
...(admissionProvenance.configured === false
? [" admission-provenance: not-required"]
: [
` admission-provenance: ready=${boolText(admissionProvenance.ready)} policy=${stringValue(admissionProvenance.policyName)} binding=${stringValue(admissionProvenance.bindingName)} active-after=${stringValue(admissionProvenance.activeAfter)} default-can-mutate=${boolText(admissionProvenance.defaultCanMutate)}`,
` admission-reasons: ${Array.isArray(admissionProvenance.reasons) ? admissionProvenance.reasons.join(",") || "-" : "-"}`,
]),
"",
"GITEA HOOKS",
...(webhooks.length === 0 ? ["-"] : table(["HOOK", "ACTIVE", "EVENTS", "URL"], webhooks.map((item) => [stringValue(item.id), boolText(item.active), Array.isArray(item.events) ? item.events.join(",") : stringValue(item.events), short(stringValue(item.url), 56)]))),