fix(cicd): 收紧 PaC admission 更新与探针
This commit is contained in:
@@ -32,6 +32,8 @@ test("admission contract rejects malformed required declarations instead of sile
|
||||
|
||||
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);
|
||||
@@ -50,6 +52,7 @@ test("versioned admission desired state protects controller proof, candidate ide
|
||||
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"]');
|
||||
@@ -58,6 +61,12 @@ test("versioned admission desired state protects controller proof, candidate ide
|
||||
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", () => {
|
||||
@@ -184,6 +193,9 @@ test("AgentRun rendered delivery plan and real TaskRun terminals close the statu
|
||||
collectArtifacts: "succeeded",
|
||||
gitopsPromote: "succeeded",
|
||||
},
|
||||
terminalSources: {
|
||||
planArtifacts: { markerSource: "pac-delivery-plan-structured-log" },
|
||||
},
|
||||
});
|
||||
expect(evaluator.evaluatePacStatus({
|
||||
sourceCommit,
|
||||
@@ -213,6 +225,15 @@ test("history evaluates admission and default RBAC in each consumer namespace",
|
||||
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");
|
||||
});
|
||||
|
||||
|
||||
@@ -25,7 +25,6 @@ export interface PacAdmissionContract {
|
||||
readonly bindingName: string;
|
||||
readonly failurePolicy: "Fail";
|
||||
readonly validationActions: readonly ["Deny"];
|
||||
readonly terminalRoles: readonly string[];
|
||||
readonly child: {
|
||||
readonly enabled: false;
|
||||
readonly parentNameAnnotation: string;
|
||||
@@ -94,7 +93,6 @@ export function parsePacAdmissionContract(source: Record<string, unknown>): PacA
|
||||
bindingName,
|
||||
failurePolicy: literal(admission.failurePolicy, "deliveryProvenance.admission.failurePolicy", "Fail"),
|
||||
validationActions: ["Deny"],
|
||||
terminalRoles: stringArray(provenance.terminalRoles, "deliveryProvenance.terminalRoles"),
|
||||
child: {
|
||||
enabled: literal(child.enabled, "deliveryProvenance.child.enabled", false),
|
||||
parentNameAnnotation: required(child.parentNameAnnotation, "deliveryProvenance.child.parentNameAnnotation"),
|
||||
@@ -172,6 +170,11 @@ export function renderPacAdmissionDesiredFragment(targetId: string): string {
|
||||
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`,
|
||||
|
||||
@@ -278,9 +278,9 @@ admission_provenance_state() {
|
||||
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; do
|
||||
for verb in create patch update delete deletecollection; do
|
||||
for resource in pipelineruns.tekton.dev taskruns.tekton.dev; do
|
||||
if answer=$(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
|
||||
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) ;;
|
||||
@@ -472,12 +472,16 @@ let admissionBinding = null;
|
||||
const admissionRbac = new Map();
|
||||
|
||||
function defaultCanMutate(namespace) {
|
||||
for (const verb of ['create', 'patch', 'update']) {
|
||||
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: 12000,
|
||||
maxBuffer: 64 * 1024,
|
||||
timeout: waitTimeoutSeconds * 1000,
|
||||
});
|
||||
const answer = String(result.stdout || '').trim();
|
||||
if (answer === 'yes') return true;
|
||||
@@ -1424,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}`);
|
||||
|
||||
Reference in New Issue
Block a user