fix(cicd): 回退 PaC admission 业务阻断
This commit is contained in:
@@ -65,9 +65,10 @@ deliveryProvenance:
|
||||
admission:
|
||||
policyName: unidesk-pac-delivery-provenance-v2
|
||||
bindingName: unidesk-pac-delivery-provenance-v2
|
||||
failurePolicy: Fail
|
||||
failurePolicy: Ignore
|
||||
validationActions:
|
||||
- Deny
|
||||
- Warn
|
||||
- Audit
|
||||
capabilities:
|
||||
sentinelInternalPublish:
|
||||
enabled: false
|
||||
|
||||
@@ -0,0 +1,81 @@
|
||||
# R4.1.1 任务报告
|
||||
|
||||
## 状态
|
||||
|
||||
- MDTODO:`R4.1.1`
|
||||
- GitHub issue:`pikasTech/unidesk#2039`
|
||||
- 状态:`in_progress`
|
||||
- 原因:源码修复与定向验证已完成,尚未合并 PR,也未执行合并后的 NC01 原入口运行面验收。
|
||||
|
||||
## 问题事实
|
||||
|
||||
- PR #1808 引入同名 `ValidatingAdmissionPolicy` 与 binding:
|
||||
- 使用 `failurePolicy: Fail`;
|
||||
- 使用 `validationActions: [Deny]`;
|
||||
- 阻断不满足 provenance CEL 的 PipelineRun 创建或更新。
|
||||
- PR #1812 继续收紧 watcher 的 queue transition:
|
||||
- 假定同一次 UPDATE 完成 `spec.status: PipelineRunPending -> absent`;
|
||||
- 同时完成 `state: queued -> started`。
|
||||
- NC01 官方 `pipelines-as-code-watcher` 的真实更新不满足该原子假定:
|
||||
- live `unidesk-pac-delivery-provenance-v2` 拒绝 watcher;
|
||||
- HWLAB `df925768` 与 AgentRun 自动队列停在 Pending。
|
||||
|
||||
## 本次语义回退
|
||||
|
||||
- owning YAML 将同名 v2 policy 改为 Kubernetes 原生 warning-only:
|
||||
- `failurePolicy: Ignore`;
|
||||
- `validationActions: [Warn, Audit]`。
|
||||
- 保留现有观察项的计算和披露:
|
||||
- CEL、marker、creator 与 ServiceAccount;
|
||||
- queue transition、spec/config SHA、版本与 resource epoch;
|
||||
- 默认 RBAC。
|
||||
- 上述观察项不再作为以下入口的阻断条件:
|
||||
- 业务 admission 与 bootstrap;
|
||||
- status、history 与 debug;
|
||||
- 外层 PaC delivery eligibility。
|
||||
- admission 与 consumer bootstrap 漂移统一输出结构化字段:
|
||||
- `blocking: false`;
|
||||
- `warning: true|false`;
|
||||
- `warnings` / `reasons`。
|
||||
- PaC bootstrap 的 `pac-admission` 阶段在 live identity 或 generation 尚未精确收敛时显示 `state: warning`,整体结果仍可成功。
|
||||
- 外层 GitHub/Gitea PaC push event 继续作为:
|
||||
- 唯一流水线触发;
|
||||
- 唯一 delivery authority。
|
||||
- admission exact identity 仅决定 `admissionProvenanceVerified` 与 warning,不再让已观察到的 outer event 失去 eligibility。
|
||||
|
||||
## 明确保留
|
||||
|
||||
- PR #1808 的 source artifact、三阶段 terminal、GitOps/status/history/debug 可观测性与无关后续改动。
|
||||
- YAML-owned policy/binding 名称 `unidesk-pac-delivery-provenance-v2`,以便受控 bootstrap 通过 server-side apply 原位收敛现有 live v2。
|
||||
- provenance、creator、marker、ServiceAccount、spec/config SHA、版本、epoch、RBAC 与 drift 的只读计算和结构化披露。
|
||||
- GitHub PR merge 继续作为唯一正式交付触发。
|
||||
- 未新增以下入口或机制:
|
||||
- PipelineRun、mirror、trigger、sync、flush、Argo sync 或 runtime patch;
|
||||
- 数据库迁移、合同、锁、租约、第二 authority 或 fallback。
|
||||
|
||||
## 定向验证
|
||||
|
||||
- `sh -n scripts/src/platform-infra-pipelines-as-code-remote.sh`
|
||||
- `bun test scripts/src/platform-infra-pac-provenance.test.ts scripts/src/platform-infra-pipelines-as-code-bootstrap.test.ts`
|
||||
- 结果:`23 pass, 0 fail, 234 expect()`。
|
||||
- `node` 调用 `runPacStatusFixtureChecks()`:
|
||||
- 结果:`ok=true, checks=34`。
|
||||
- `bun scripts/cli.ts platform-infra pipelines-as-code plan --target NC01`
|
||||
- 结果:YAML source-of-truth、single-path、Gitea source 与 runtime-zero-docker 策略均为 `true`。
|
||||
- `git diff --check`
|
||||
- 结果:通过。
|
||||
|
||||
## 合并后受控收敛
|
||||
|
||||
- 主代理在 PR 合并后先执行只读计划:
|
||||
- `bun scripts/cli.ts platform-infra pipelines-as-code bootstrap --target NC01 --consumer agentrun-nc01-v02 --dry-run`
|
||||
- 确认计划只体现 owning YAML 的同名 v2 warning-only 收敛后,再执行:
|
||||
- `bun scripts/cli.ts platform-infra pipelines-as-code bootstrap --target NC01 --consumer agentrun-nc01-v02 --confirm`
|
||||
- 不需要 prune:policy 与 binding 名称保持不变,bootstrap 使用 server-side apply 原位把 live `Fail/Deny` 更新为 `Ignore` 与 `Warn/Audit`。
|
||||
- bootstrap 不是流水线补跑入口;禁止随后人工执行 PipelineRun、mirror、trigger、sync、flush、Argo sync 或 runtime patch。
|
||||
- 运行面验收由主代理观察既有 merge 事件自动恢复,并通过原 `status`、`history` 与受影响 HWLAB/AgentRun 原入口完成。
|
||||
|
||||
## 当前阻塞
|
||||
|
||||
- 无源码或定向验证阻塞。
|
||||
- 运行面验收仍等待 PR 合并及主代理执行受控 bootstrap,因此任务保持 `in_progress`。
|
||||
@@ -222,9 +222,9 @@ function evaluatePacAdmissionState(inputValue) {
|
||||
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 (policySpec.failurePolicy !== "Ignore") reasons.push("policy-failure-policy-not-ignore");
|
||||
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 (!Array.isArray(bindingSpec.validationActions) || stableCanonicalJson(bindingSpec.validationActions) !== stableCanonicalJson(["Warn", "Audit"])) reasons.push("binding-validation-actions-not-warning-only");
|
||||
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");
|
||||
@@ -258,6 +258,9 @@ function evaluatePacAdmissionState(inputValue) {
|
||||
configured: true,
|
||||
required: true,
|
||||
ready: reasons.length === 0,
|
||||
blocking: false,
|
||||
warning: reasons.length > 0,
|
||||
warnings: reasons,
|
||||
source: "live-admissionregistration-v1",
|
||||
policyName: stringOrNull(policyMetadata.name),
|
||||
bindingName: stringOrNull(bindingMetadata.name),
|
||||
@@ -357,13 +360,18 @@ function classifyPacPipelineRun(consumerInput, itemInput) {
|
||||
deliveryClass,
|
||||
reason,
|
||||
candidate,
|
||||
deliveryAuthorityEligible: outer && (!provenanceRequired || admissionProvenanceVerified),
|
||||
deliveryAuthorityEligible: outer,
|
||||
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,
|
||||
admissionProvenanceVerified,
|
||||
admissionWarning: outer && !admissionProvenanceVerified ? {
|
||||
code: "pac-admission-provenance-unverified",
|
||||
blocking: false,
|
||||
reasons: Array.isArray(admissionState.reasons) ? admissionState.reasons : [],
|
||||
} : null,
|
||||
admissionPolicyReady: admissionState.ready === true,
|
||||
admissionPolicyName: stringOrNull(admissionState.policyName),
|
||||
admissionBindingName: stringOrNull(admissionState.bindingName),
|
||||
@@ -1285,19 +1293,19 @@ function runPacStatusFixtureChecks() {
|
||||
};
|
||||
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)}` } },
|
||||
{ id: "pre-bootstrap-run-warning-only", expected: false, item: { ...verifiedRun, metadata: { ...verifiedRun.metadata, creationTimestamp: "2025-12-31T23:59:59Z" } }, state: admissionState },
|
||||
{ id: "marker-mismatch-warning-only", expected: false, item: { ...verifiedRun, metadata: { ...verifiedRun.metadata, annotations: { ...verifiedRun.metadata.annotations, [provenance.markerAnnotation]: "forged" } } }, state: admissionState },
|
||||
{ id: "candidate-label-warning-only", 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: "candidate-pipeline-ref-warning-only", 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: "service-account-mismatch-warning-only", expected: false, item: { ...verifiedRun, spec: { taskRunTemplate: { serviceAccountName: "default" } } }, state: admissionState },
|
||||
{ id: "policy-identity-mismatch-warning-only", 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,
|
||||
ok: result.admissionProvenanceVerified === item.expected && result.deliveryAuthorityEligible === true,
|
||||
expectedOk: true,
|
||||
actualOk: result.deliveryAuthorityEligible,
|
||||
expectedCode: item.expected ? "admission-owned-pac-push-provenance-verified" : "pac-push-metadata-observed-but-admission-provenance-unverified",
|
||||
actualCode: result.reason,
|
||||
|
||||
@@ -38,6 +38,11 @@ test("admission contract rejects malformed required declarations instead of sile
|
||||
const duplicatedRequiredConsumers = duplicatedServiceAccount.consumers.filter((item: any) => item.deliveryProvenance !== undefined);
|
||||
duplicatedRequiredConsumers[1].deliveryProvenance.executionServiceAccountName = duplicatedRequiredConsumers[0].deliveryProvenance.executionServiceAccountName;
|
||||
expect(() => parsePacAdmissionContract(duplicatedServiceAccount)).toThrow("executionServiceAccountName must be unique per target");
|
||||
|
||||
const denyingAdmission = structuredClone(source);
|
||||
denyingAdmission.deliveryProvenance.admission.failurePolicy = "Fail";
|
||||
denyingAdmission.deliveryProvenance.admission.validationActions = ["Deny"];
|
||||
expect(() => parsePacAdmissionContract(denyingAdmission)).toThrow("validationActions must be exactly [Warn, Audit]");
|
||||
});
|
||||
|
||||
test("admission queue transition contract rejects absent sources, unknown Tekton states, and duplicate transitions", () => {
|
||||
@@ -55,7 +60,7 @@ test("admission queue transition contract rejects absent sources, unknown Tekton
|
||||
expect(() => parsePacAdmissionContract(duplicate)).toThrow("deliveryProvenance.queueTransition.allowed must be unique");
|
||||
});
|
||||
|
||||
test("versioned admission desired state protects controller proof, candidate identity, params, and execution SA", () => {
|
||||
test("versioned admission desired state reports controller proof and candidate drift without denying delivery", () => {
|
||||
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();
|
||||
@@ -69,15 +74,15 @@ test("versioned admission desired state protects controller proof, candidate ide
|
||||
toState: "started",
|
||||
}]);
|
||||
expect(contract.child.enabled).toBe(false);
|
||||
expect(contract.consumers.map((item) => item.id)).toEqual(["agentrun-nc01-v02", "platform-infra-sub2rank-nc01", "selfmedia-nc01"]);
|
||||
expect(contract.consumers.map((item) => item.id)).toEqual(["agentrun-nc01-v02", "platform-infra-sub2rank-nc01", "selfmedia-nc01", "selfmedia-production-nc01", "pikaoa-test-nc01"]);
|
||||
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.failurePolicy).toBe("Ignore");
|
||||
expect(policy.spec.matchConstraints.matchPolicy).toBe("Equivalent");
|
||||
expect(items[1].spec.validationActions).toEqual(["Deny"]);
|
||||
expect(items[1].spec.validationActions).toEqual(["Warn", "Audit"]);
|
||||
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");
|
||||
@@ -134,8 +139,8 @@ test("versioned admission desired state protects controller proof, candidate ide
|
||||
|
||||
test("required consumer default RBAC has one automatic desired owner per namespace and no Tekton mutation verbs", () => {
|
||||
const rbac = documents(renderPacConsumerRbacDesiredFragment("NC01"));
|
||||
expect(rbac.map((item) => item.kind)).toEqual(["Role", "RoleBinding", "Role", "RoleBinding", "Role", "RoleBinding"]);
|
||||
expect(rbac.filter((item) => item.kind === "Role").map((item) => item.metadata.namespace)).toEqual(["agentrun-ci", "devops-infra", "selfmedia-ci"]);
|
||||
expect(rbac.map((item) => item.kind)).toEqual(["Role", "RoleBinding", "Role", "RoleBinding", "Role", "RoleBinding", "Role", "RoleBinding"]);
|
||||
expect(rbac.filter((item) => item.kind === "Role").map((item) => item.metadata.namespace)).toEqual(["agentrun-ci", "devops-infra", "selfmedia-ci", "pikaoa-ci"]);
|
||||
for (const role of rbac.filter((item) => item.kind === "Role")) {
|
||||
expect(role.metadata.annotations["argocd.argoproj.io/sync-wave"]).toBe("-1");
|
||||
expect(role.metadata.annotations["unidesk.ai/effective-config-sha256"]).toBe(pacConsumerRbacDesiredIdentity("NC01").configSha256);
|
||||
@@ -150,6 +155,8 @@ test("required consumer default RBAC has one automatic desired owner per namespa
|
||||
"RoleBinding",
|
||||
"Role",
|
||||
"RoleBinding",
|
||||
"Role",
|
||||
"RoleBinding",
|
||||
"ValidatingAdmissionPolicy",
|
||||
"ValidatingAdmissionPolicyBinding",
|
||||
"ServiceAccount",
|
||||
@@ -158,7 +165,7 @@ test("required consumer default RBAC has one automatic desired owner per namespa
|
||||
]);
|
||||
});
|
||||
|
||||
test("live admission evaluator requires exact desired hashes, observed generation, default-SA loss, and a new resource epoch", () => {
|
||||
test("live admission evaluator reports exact desired drift as non-blocking warnings", () => {
|
||||
const contract = readPacAdmissionContract();
|
||||
const admission = documents(renderPacAdmissionDesiredFragment("NC01"));
|
||||
const rbac = documents(renderPacConsumerRbacDesiredFragment("NC01"));
|
||||
@@ -174,7 +181,7 @@ test("live admission evaluator requires exact desired hashes, observed generatio
|
||||
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)).toMatchObject({ ready: true, blocking: false, warning: false, activeAfter: "2026-01-01T00:00:01Z", defaultCanMutate: false });
|
||||
expect(evaluator.evaluatePacAdmissionState({
|
||||
...input,
|
||||
policy: {
|
||||
@@ -189,7 +196,7 @@ test("live admission evaluator requires exact desired hashes, observed generatio
|
||||
},
|
||||
},
|
||||
},
|
||||
})).toMatchObject({ ready: false, reasons: expect.arrayContaining(["policy-expression-warning"]) });
|
||||
})).toMatchObject({ ready: false, blocking: false, warning: true, reasons: expect.arrayContaining(["policy-expression-warning"]) });
|
||||
const apiDefaultedPolicy = {
|
||||
...policy,
|
||||
spec: {
|
||||
@@ -206,10 +213,10 @@ test("live admission evaluator requires exact desired hashes, observed generatio
|
||||
ready: true,
|
||||
liveSpecSha256: expected.configSha256,
|
||||
});
|
||||
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"]) });
|
||||
expect(evaluator.evaluatePacAdmissionState({ ...input, defaultCanMutate: true })).toMatchObject({ ready: false, blocking: false, warning: true, reasons: expect.arrayContaining(["default-serviceaccount-can-mutate-tekton-runs"]) });
|
||||
expect(evaluator.evaluatePacAdmissionState({ ...input, defaultCanMutate: null })).toMatchObject({ ready: false, blocking: false, warning: true, 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, blocking: false, warning: true, reasons: expect.arrayContaining(["default-role-rules-mismatch"]) });
|
||||
expect(evaluator.evaluatePacAdmissionState({ ...input, expected: { ...expected, configSha256: `sha256:${"0".repeat(64)}` } })).toMatchObject({ ready: false, blocking: false, warning: true, reasons: expect.arrayContaining(["policy-config-sha256-mismatch"]) });
|
||||
const mutatedPolicy = {
|
||||
...policy,
|
||||
spec: {
|
||||
@@ -345,10 +352,10 @@ test("AgentRun rendered delivery plan and real TaskRun terminals close the statu
|
||||
})).toMatchObject({ ok: true, code: "pac-ready-gitops-exact" });
|
||||
});
|
||||
|
||||
test("provenance fixtures fail closed for pre-bootstrap, forged marker, wrong SA, and policy mismatch", () => {
|
||||
test("provenance fixtures keep outer PaC delivery eligible while reporting admission warnings", () => {
|
||||
const result = evaluator.runPacStatusFixtureChecks();
|
||||
expect(result.ok).toBe(true);
|
||||
for (const id of ["gitops-only-registry-not-configured", "image-registry-missing-fails-closed", "equal-revisions-ignore-owning-git-fetch-failure", "different-revisions-fetch-failure-remains-unknown", "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"]) {
|
||||
for (const id of ["gitops-only-registry-not-configured", "image-registry-missing-fails-closed", "equal-revisions-ignore-owning-git-fetch-failure", "different-revisions-fetch-failure-remains-unknown", "admission-provenance-verified", "pre-bootstrap-run-warning-only", "marker-mismatch-warning-only", "candidate-label-warning-only", "candidate-pipeline-ref-warning-only", "service-account-mismatch-warning-only", "policy-identity-mismatch-warning-only"]) {
|
||||
expect(result.checks.find((item) => item.id === id)?.ok).toBe(true);
|
||||
}
|
||||
});
|
||||
|
||||
@@ -33,8 +33,8 @@ export interface PacAdmissionContract {
|
||||
readonly allowedQueueTransitions: readonly PacAdmissionQueueTransitionContract[];
|
||||
readonly policyName: string;
|
||||
readonly bindingName: string;
|
||||
readonly failurePolicy: "Fail";
|
||||
readonly validationActions: readonly ["Deny"];
|
||||
readonly failurePolicy: "Ignore";
|
||||
readonly validationActions: readonly ["Warn", "Audit"];
|
||||
readonly child: {
|
||||
readonly enabled: false;
|
||||
readonly parentNameAnnotation: string;
|
||||
@@ -83,7 +83,9 @@ export function parsePacAdmissionContract(source: Record<string, unknown>): PacA
|
||||
const transitionKeys = new Set(allowedQueueTransitions.map((transition) => [transition.fromSpecStatus, transition.toSpecStatus, transition.fromState, transition.toState].join("\u0000")));
|
||||
if (transitionKeys.size !== allowedQueueTransitions.length) throw new Error("deliveryProvenance.queueTransition.allowed must be unique");
|
||||
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]");
|
||||
if (validationActions.length !== 2 || validationActions[0] !== "Warn" || validationActions[1] !== "Audit") {
|
||||
throw new Error("deliveryProvenance.admission.validationActions must be exactly [Warn, Audit]");
|
||||
}
|
||||
const consumers = recordArray(root.consumers, "consumers").flatMap((consumer, index) => {
|
||||
if (consumer.deliveryProvenance === undefined) return [];
|
||||
const value = record(consumer.deliveryProvenance, `consumers[${index}].deliveryProvenance`);
|
||||
@@ -127,8 +129,8 @@ export function parsePacAdmissionContract(source: Record<string, unknown>): PacA
|
||||
allowedQueueTransitions,
|
||||
policyName,
|
||||
bindingName,
|
||||
failurePolicy: literal(admission.failurePolicy, "deliveryProvenance.admission.failurePolicy", "Fail"),
|
||||
validationActions: ["Deny"],
|
||||
failurePolicy: literal(admission.failurePolicy, "deliveryProvenance.admission.failurePolicy", "Ignore"),
|
||||
validationActions: ["Warn", "Audit"],
|
||||
child: {
|
||||
enabled: literal(child.enabled, "deliveryProvenance.child.enabled", false),
|
||||
parentNameAnnotation: required(child.parentNameAnnotation, "deliveryProvenance.child.parentNameAnnotation"),
|
||||
|
||||
@@ -210,6 +210,35 @@ test("PaC bootstrap projects typed stages and compact JSON", () => {
|
||||
expect(rendered).toContain("confirm:");
|
||||
});
|
||||
|
||||
test("PaC bootstrap keeps admission drift as a non-blocking warning", () => {
|
||||
const result = {
|
||||
ok: true,
|
||||
action: "platform-infra-pipelines-as-code-bootstrap",
|
||||
mutation: true,
|
||||
mode: "confirmed",
|
||||
target: { id: "NC01" },
|
||||
consumer: { id: "widgets", node: "NC01", lane: "v01" },
|
||||
repository: { id: "widgets", namespace: "ci" },
|
||||
mirrorRepository: { key: mirror.key, upstreamRepository: mirror.upstream.repository, upstreamBranch: mirror.upstream.branch, owner: mirror.gitea.owner, repo: mirror.gitea.name },
|
||||
githubPreflight: { ok: true, mutation: false, repositories: [{ key: mirror.key, code: "github-upstream-available", ok: true }] },
|
||||
giteaBootstrap: { ok: true, mutation: false, mode: "skipped", blockers: [] },
|
||||
pipelinesAsCodeApply: {
|
||||
ok: true,
|
||||
mutation: true,
|
||||
mode: "confirmed",
|
||||
remote: {
|
||||
ok: true,
|
||||
admission: { applied: true, ready: false },
|
||||
admissionProvenance: { policyName: "unidesk-pac-delivery-provenance-v2", reasons: ["policy-generation-not-observed"] },
|
||||
},
|
||||
},
|
||||
};
|
||||
const projection = projectPacBootstrapResult(result);
|
||||
expect(projection.ok).toBe(true);
|
||||
expect((projection.stages as Record<string, unknown>[]).find((item) => item.id === "pac-admission")).toMatchObject({ ok: true, state: "warning", blocking: false });
|
||||
expect(projection.warnings).toEqual([expect.objectContaining({ code: "pac-admission-provenance-not-ready", blocking: false })]);
|
||||
});
|
||||
|
||||
test("PaC bootstrap returns fix-yaml next for zero YAML matches", () => {
|
||||
const result = pacBootstrapYamlMatchFailure("NC01", "widgets", new Error("cannot resolve repository: no exact match"));
|
||||
expect(projectPacBootstrapResult(result)).toBe(result);
|
||||
|
||||
@@ -70,6 +70,7 @@ export function projectPacBootstrapResult(result: Record<string, unknown>): Reco
|
||||
const apply = record(result.pipelinesAsCodeApply);
|
||||
const remote = record(apply.remote);
|
||||
const admission = record(remote.admission);
|
||||
const admissionProvenance = record(remote.admissionProvenance);
|
||||
const blockers = bootstrapBlockers(githubPreflight, gitea, apply, remote);
|
||||
const dryRun = result.mode === "dry-run";
|
||||
const repositoryMissing = remote.error === "gitea-repository-missing";
|
||||
@@ -78,6 +79,16 @@ export function projectPacBootstrapResult(result: Record<string, unknown>): Reco
|
||||
const giteaSkipped = gitea.mode === "skipped";
|
||||
const applySkipped = apply.mode === "skipped";
|
||||
const applyReady = apply.ok === true || (dryRun && repositoryMissing);
|
||||
const warnings = [
|
||||
...records(result.warnings),
|
||||
...(applySkipped || dryRun || repositoryMissing || admission.ready === true ? [] : [{
|
||||
object: { kind: "ValidatingAdmissionPolicy", id: stringValue(admissionProvenance.policyName, "PaC admission") },
|
||||
blocking: false,
|
||||
code: "pac-admission-provenance-not-ready",
|
||||
configPath: "config/platform-infra/pipelines-as-code.yaml#deliveryProvenance.admission",
|
||||
reasons: Array.isArray(admissionProvenance.reasons) ? admissionProvenance.reasons : [],
|
||||
}]),
|
||||
];
|
||||
const next = bootstrapNext(result, blockers);
|
||||
return {
|
||||
ok: blockers.length === 0 && giteaReady && applyReady,
|
||||
@@ -102,12 +113,12 @@ export function projectPacBootstrapResult(result: Record<string, unknown>): Reco
|
||||
stages: [
|
||||
{ id: "gitea-init", ok: giteaSkipped ? null : giteaReady, state: giteaSkipped ? "skipped" : giteaReady ? (dryRun ? "planned" : "ready") : "failed", mutation: gitea.mutation === true },
|
||||
{ id: "pac-controller", ok: applySkipped ? null : applyReady, state: applySkipped ? "skipped" : repositoryMissing ? "waiting-for-gitea-confirm" : applyReady ? (dryRun ? "planned" : "ready") : "failed", mutation: apply.mutation === true },
|
||||
{ id: "pac-admission", ok: applySkipped ? null : dryRun ? true : admission.ready === true, state: applySkipped ? "skipped" : repositoryMissing ? "waiting-for-gitea-confirm" : dryRun ? "planned" : admission.ready === true ? "ready" : "failed", mutation: admission.applied === true },
|
||||
{ id: "pac-admission", ok: applySkipped ? null : true, state: applySkipped ? "skipped" : repositoryMissing ? "waiting-for-gitea-confirm" : dryRun ? "planned" : admission.ready === true ? "ready" : "warning", blocking: false, mutation: admission.applied === true },
|
||||
{ id: "tekton-consumer", ok: applySkipped ? null : applyReady, state: applySkipped ? "skipped" : repositoryMissing ? "waiting-for-gitea-confirm" : applyReady ? (dryRun ? "planned" : "ready") : "failed", mutation: apply.mutation === true },
|
||||
{ id: "gitops-argo", ok: applySkipped ? null : applyReady, state: applySkipped ? "skipped" : repositoryMissing ? "waiting-for-gitea-confirm" : applyReady ? (dryRun ? "planned" : "ready") : "failed", mutation: apply.mutation === true },
|
||||
],
|
||||
repository: { id: repository.id, namespace: repository.namespace },
|
||||
warnings: result.warnings,
|
||||
warnings,
|
||||
blockers,
|
||||
next,
|
||||
valuesPrinted: false,
|
||||
|
||||
@@ -363,15 +363,13 @@ apply_action() {
|
||||
hook_id=$(ensure_webhook)
|
||||
ready=false
|
||||
if wait_ready; then ready=true; fi
|
||||
prepare_admission_provenance_state
|
||||
admission_ready=true
|
||||
if [ "${UNIDESK_PAC_DELIVERY_PROVENANCE_REQUIRED:-0}" = "1" ]; then
|
||||
admission_ready=false
|
||||
if wait_admission_ready; then admission_ready=true; fi
|
||||
else
|
||||
prepare_admission_provenance_state
|
||||
admission_ready=$(printf '%s' "$UNIDESK_PAC_ADMISSION_STATE_JSON" | node -e 'let input="";process.stdin.on("data",chunk=>input+=chunk);process.stdin.on("end",()=>process.stdout.write(JSON.parse(input).ready===true?"true":"false"));')
|
||||
fi
|
||||
ok=false
|
||||
if [ "$ready" = true ] && [ "$admission_ready" = true ]; then ok=true; fi
|
||||
if [ "$ready" = true ]; then ok=true; fi
|
||||
printf '{"ok":%s,"mode":"confirmed","mutation":true,"controllerReady":%s,"admission":{"applied":%s,"ready":%s},"admissionProvenance":%s,"argoBootstrap":%s,"preflight":{"repositoryExists":true,"httpStatus":"2xx","repository":"%s/%s","valuesPrinted":false},"webhook":{"present":true,"id":"%s","url":"%s"},"repository":"%s","namespace":"%s","valuesPrinted":false}\n' "$ok" "$ready" "$admission_applied" "$admission_ready" "$UNIDESK_PAC_ADMISSION_STATE_JSON" "$argo_bootstrap" "$(json_string "$UNIDESK_PAC_GITEA_OWNER")" "$(json_string "$UNIDESK_PAC_GITEA_REPO")" "$(json_string "$hook_id")" "$(json_string "$UNIDESK_PAC_WEBHOOK_URL")" "$(json_string "$UNIDESK_PAC_REPOSITORY_NAME")" "$(json_string "$UNIDESK_PAC_TARGET_NAMESPACE")"
|
||||
}
|
||||
|
||||
@@ -436,19 +434,6 @@ prepare_admission_provenance_state() {
|
||||
export UNIDESK_PAC_ADMISSION_STATE_JSON
|
||||
}
|
||||
|
||||
wait_admission_ready() {
|
||||
timeout="${UNIDESK_PAC_WAIT_TIMEOUT_SECONDS:-55}"
|
||||
end=$(( $(now_epoch) + timeout ))
|
||||
while :; do
|
||||
prepare_admission_provenance_state
|
||||
if printf '%s' "$UNIDESK_PAC_ADMISSION_STATE_JSON" | node -e 'let input="";process.stdin.on("data",chunk=>input+=chunk);process.stdin.on("end",()=>process.exit(JSON.parse(input).ready===true?0:1));'; then
|
||||
return 0
|
||||
fi
|
||||
if [ "$(now_epoch)" -ge "$end" ]; then return 1; fi
|
||||
sleep 2
|
||||
done
|
||||
}
|
||||
|
||||
pipeline_rows() {
|
||||
payload_file=$(mktemp)
|
||||
kubectl -n "$UNIDESK_PAC_TARGET_NAMESPACE" get pipelinerun -o json >"$payload_file" 2>/dev/null || printf '{"items":[]}' >"$payload_file"
|
||||
@@ -624,7 +609,7 @@ function defaultCanMutate(namespace) {
|
||||
|
||||
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 };
|
||||
if (!expected || expected.required !== true) return { configured: false, required: false, ready: null, blocking: false, warning: false, warnings: [], 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)) {
|
||||
@@ -1561,6 +1546,9 @@ if (!passwordPresent) reasons.push('argocd-repository-secret-password-missing');
|
||||
process.stdout.write(JSON.stringify({
|
||||
configured: runnerConfigured || argoConfigured,
|
||||
ready: reasons.length === 0,
|
||||
blocking: false,
|
||||
warning: reasons.length > 0,
|
||||
warnings: reasons,
|
||||
runner: {
|
||||
configured: runnerConfigured,
|
||||
serviceAccount: runnerName || null,
|
||||
@@ -1597,7 +1585,6 @@ status_action() {
|
||||
repository_condition=$(condition_status "$UNIDESK_PAC_TARGET_NAMESPACE" repository "$UNIDESK_PAC_REPOSITORY_NAME")
|
||||
prepare_admission_provenance_state
|
||||
consumer_bootstrap=$(consumer_bootstrap_state)
|
||||
bootstrap_ready=$(UNIDESK_PAC_BOOTSTRAP_STATE="$consumer_bootstrap" node -e 'const s=JSON.parse(process.env.UNIDESK_PAC_BOOTSTRAP_STATE||"{}"); process.stdout.write(s.configured===true&&s.ready!==true?"false":"true")')
|
||||
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"
|
||||
@@ -1621,7 +1608,6 @@ NODE
|
||||
)
|
||||
runtime=$(json_normalize "$(runtime_summary)")
|
||||
diagnostics=$(cicd_diagnostics "$pipelines" "$artifact" "$argo" "$runtime")
|
||||
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" UNIDESK_PAC_BOOTSTRAP_STATE="$consumer_bootstrap" node <<'NODE'
|
||||
const diagnostics = JSON.parse(process.env.UNIDESK_PAC_DIAGNOSTICS || '{}');
|
||||
const admissionProvenance = JSON.parse(process.env.UNIDESK_PAC_STATE || '{}');
|
||||
@@ -1629,20 +1615,23 @@ const consumerBootstrap = JSON.parse(process.env.UNIDESK_PAC_BOOTSTRAP_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',
|
||||
warnings: [...(Array.isArray(diagnostics.warnings) ? diagnostics.warnings : []), {
|
||||
code: 'pac-admission-provenance-not-ready',
|
||||
blocking: false,
|
||||
hint: 'PaC admission policy, binding, desired RBAC, or live default-SA observation is drifted; delivery continues',
|
||||
reasons: Array.isArray(admissionProvenance.reasons) ? admissionProvenance.reasons : [],
|
||||
}],
|
||||
admissionProvenance,
|
||||
valuesPrinted: false,
|
||||
}));
|
||||
} else if (consumerBootstrap.configured === true && consumerBootstrap.ready !== true) {
|
||||
process.stdout.write(JSON.stringify({
|
||||
...diagnostics,
|
||||
ok: false,
|
||||
code: 'pac-consumer-bootstrap-not-ready',
|
||||
phase: 'consumer-bootstrap-not-ready',
|
||||
hint: 'declared runner ServiceAccount/RoleBinding or Argo repository credential is missing or drifted',
|
||||
warnings: [...(Array.isArray(diagnostics.warnings) ? diagnostics.warnings : []), {
|
||||
code: 'pac-consumer-bootstrap-not-ready',
|
||||
blocking: false,
|
||||
hint: 'declared runner ServiceAccount/RoleBinding or Argo repository credential is missing or drifted; status remains observable',
|
||||
}],
|
||||
admissionProvenance,
|
||||
consumerBootstrap,
|
||||
valuesPrinted: false,
|
||||
@@ -1653,7 +1642,7 @@ if (process.env.UNIDESK_PAC_DELIVERY_PROVENANCE_REQUIRED === '1' && admissionPro
|
||||
NODE
|
||||
)
|
||||
printf '{"ok":%s,"crdPresent":%s,"controllerReady":"%s","admissionProvenance":%s,"consumerBootstrap":%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" ] && [ "$bootstrap_ready" = "true" ] && { [ "${UNIDESK_PAC_DELIVERY_PROVENANCE_REQUIRED:-0}" != "1" ] || [ "$admission_ready" = "true" ]; } && echo true || echo false )" \
|
||||
"$( [ -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" \
|
||||
|
||||
Reference in New Issue
Block a user