From 2ded15966515178c99c8a6ec13f02fd68b93530d Mon Sep 17 00:00:00 2001 From: Barrett LaFrance Date: Mon, 16 Dec 2024 10:15:50 -0600 Subject: [PATCH 1/4] wip: initial --- src/lib/filter/filterNoMatchReason.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/lib/filter/filterNoMatchReason.ts b/src/lib/filter/filterNoMatchReason.ts index e9f64133d..04329ea29 100644 --- a/src/lib/filter/filterNoMatchReason.ts +++ b/src/lib/filter/filterNoMatchReason.ts @@ -28,7 +28,7 @@ import { Binding } from "../types"; /** * Decide to run callback after the event comes back from API Server **/ - +//TODO export function filterNoMatchReason( binding: Binding, kubernetesObject: Partial, From bed78cb25195078e953c93bcb190751278672251 Mon Sep 17 00:00:00 2001 From: Barrett LaFrance Date: Mon, 16 Dec 2024 16:47:43 -0600 Subject: [PATCH 2/4] wip: saving progress --- src/lib/filter/filter.test.ts | 1643 +++++++++++++--------- src/lib/filter/filter.ts | 88 +- src/lib/filter/filterNoMatchReason.ts | 108 -- src/lib/filter/shouldSkipRequest.test.ts | 613 -------- src/lib/helpers.test.ts | 357 +---- src/lib/processors/watch-processor.ts | 2 +- 6 files changed, 1090 insertions(+), 1721 deletions(-) delete mode 100644 src/lib/filter/filterNoMatchReason.ts delete mode 100644 src/lib/filter/shouldSkipRequest.test.ts diff --git a/src/lib/filter/filter.test.ts b/src/lib/filter/filter.test.ts index 4cd08970d..e895b86b2 100644 --- a/src/lib/filter/filter.test.ts +++ b/src/lib/filter/filter.test.ts @@ -2,712 +2,1071 @@ // SPDX-FileCopyrightText: 2023-Present The Pepr Authors import { expect, it, describe } from "@jest/globals"; -import { kind, modelToGroupVersionKind } from "kubernetes-fluent-client"; +import { kind, KubernetesObject, modelToGroupVersionKind } from "kubernetes-fluent-client"; import * as fc from "fast-check"; +import { clone } from "ramda"; import { AdmissionRequestCreatePod, AdmissionRequestDeletePod } from "../../fixtures/loader"; import { - shouldSkipRequest, + adjudicateCarriesIgnoredNamespace, adjudicateMisboundDeleteWithDeletionTimestamp, + adjudicateMisboundNamespace, + adjudicateMismatchedAnnotations, adjudicateMismatchedDeletionTimestamp, adjudicateMismatchedEvent, - adjudicateMismatchedNameRegex, - adjudicateMismatchedName, adjudicateMismatchedGroup, - adjudicateMismatchedVersion, adjudicateMismatchedKind, - adjudicateUnbindableNamespaces, - adjudicateUncarryableNamespace, - adjudicateMismatchedNamespace, adjudicateMismatchedLabels, - adjudicateMismatchedAnnotations, + adjudicateMismatchedName, + adjudicateMismatchedNameRegex, + adjudicateMismatchedNamespace, adjudicateMismatchedNamespaceRegex, - adjudicateCarriesIgnoredNamespace, + adjudicateMismatchedVersion, adjudicateMissingCarriableNamespace, + adjudicateUnbindableNamespaces, + adjudicateUncarryableNamespace, + filterNoMatchReason, + shouldSkipRequest, } from "./filter"; import { AdmissionRequest, Binding } from "../types"; import { Event, Operation } from "../enums"; -import { clusterScopedBinding } from "../helpers.test"; -import { defaultAdmissionRequest, defaultBinding } from "./adjudicators/defaultTestObjects"; +import { + defaultAdmissionRequest, + defaultBinding, + defaultFilters, + defaultKubernetesObject, +} from "./adjudicators/defaultTestObjects"; + const callback = () => undefined; -const podKind = modelToGroupVersionKind(kind.Pod.name); - -describe("Fuzzing shouldSkipRequest", () => { - it("should handle random inputs without crashing", () => { - fc.assert( - fc.property( - fc.record({ - event: fc.constantFrom("CREATE", "UPDATE", "DELETE", "ANY"), - kind: fc.record({ - group: fc.string(), - version: fc.string(), - kind: fc.string(), +describe("shouldSkipRequest", () => { + describe("Fuzzing shouldSkipRequest", () => { + it("should handle random inputs without crashing", () => { + fc.assert( + fc.property( + fc.record({ + event: fc.constantFrom("CREATE", "UPDATE", "DELETE", "ANY"), + kind: fc.record({ + group: fc.string(), + version: fc.string(), + kind: fc.string(), + }), + filters: fc.record({ + name: fc.string(), + namespaces: fc.array(fc.string()), + labels: fc.dictionary(fc.string(), fc.string()), + annotations: fc.dictionary(fc.string(), fc.string()), + deletionTimestamp: fc.boolean(), + }), }), - filters: fc.record({ + fc.record({ + operation: fc.string(), + uid: fc.string(), name: fc.string(), - namespaces: fc.array(fc.string()), - labels: fc.dictionary(fc.string(), fc.string()), - annotations: fc.dictionary(fc.string(), fc.string()), - deletionTimestamp: fc.boolean(), - }), - }), - fc.record({ - operation: fc.string(), - uid: fc.string(), - name: fc.string(), - namespace: fc.string(), - kind: fc.record({ - group: fc.string(), - version: fc.string(), - kind: fc.string(), - }), - object: fc.record({ - metadata: fc.record({ - deletionTimestamp: fc.option(fc.date()), + namespace: fc.string(), + kind: fc.record({ + group: fc.string(), + version: fc.string(), + kind: fc.string(), + }), + object: fc.record({ + metadata: fc.record({ + deletionTimestamp: fc.option(fc.date()), + }), }), }), - }), - fc.array(fc.string()), - (binding, req, capabilityNamespaces) => { - expect(() => - shouldSkipRequest(binding as Binding, req as AdmissionRequest, capabilityNamespaces), - ).not.toThrow(); - }, - ), - { numRuns: 100 }, - ); + fc.array(fc.string()), + (binding, req, capabilityNamespaces) => { + expect(() => + shouldSkipRequest(binding as Binding, req as AdmissionRequest, capabilityNamespaces), + ).not.toThrow(); + }, + ), + { numRuns: 100 }, + ); + }); }); -}); -describe("Property-Based Testing shouldSkipRequest", () => { - it("should only skip requests that do not match the binding criteria", () => { - fc.assert( - fc.property( - fc.record({ - event: fc.constantFrom("CREATE", "UPDATE", "DELETE", "ANY"), - kind: fc.record({ - group: fc.string(), - version: fc.string(), - kind: fc.string(), + describe("Property-Based Testing shouldSkipRequest", () => { + it("should only skip requests that do not match the binding criteria", () => { + fc.assert( + fc.property( + fc.record({ + event: fc.constantFrom("CREATE", "UPDATE", "DELETE", "ANY"), + kind: fc.record({ + group: fc.string(), + version: fc.string(), + kind: fc.string(), + }), + filters: fc.record({ + name: fc.string(), + namespaces: fc.array(fc.string()), + labels: fc.dictionary(fc.string(), fc.string()), + annotations: fc.dictionary(fc.string(), fc.string()), + deletionTimestamp: fc.boolean(), + }), }), - filters: fc.record({ + fc.record({ + operation: fc.string(), + uid: fc.string(), name: fc.string(), - namespaces: fc.array(fc.string()), - labels: fc.dictionary(fc.string(), fc.string()), - annotations: fc.dictionary(fc.string(), fc.string()), - deletionTimestamp: fc.boolean(), - }), - }), - fc.record({ - operation: fc.string(), - uid: fc.string(), - name: fc.string(), - namespace: fc.string(), - kind: fc.record({ - group: fc.string(), - version: fc.string(), - kind: fc.string(), - }), - object: fc.record({ - metadata: fc.record({ - deletionTimestamp: fc.option(fc.date()), + namespace: fc.string(), + kind: fc.record({ + group: fc.string(), + version: fc.string(), + kind: fc.string(), + }), + object: fc.record({ + metadata: fc.record({ + deletionTimestamp: fc.option(fc.date()), + }), }), }), - }), - fc.array(fc.string()), - (binding, req, capabilityNamespaces) => { - const shouldSkip = shouldSkipRequest(binding as Binding, req as AdmissionRequest, capabilityNamespaces); - expect(typeof shouldSkip).toBe("string"); - }, - ), - { numRuns: 100 }, + fc.array(fc.string()), + (binding, req, capabilityNamespaces) => { + const shouldSkip = shouldSkipRequest(binding as Binding, req as AdmissionRequest, capabilityNamespaces); + expect(typeof shouldSkip).toBe("string"); + }, + ), + { numRuns: 100 }, + ); + }); + }); + + it("create: should reject when regex name does not match", () => { + const binding = { + model: kind.Pod, + event: Event.ANY, + kind: podKind, + filters: { + name: "", + namespaces: [], + regexNamespaces: [], + regexName: "^default$", + labels: {}, + annotations: {}, + deletionTimestamp: false, + }, + callback, + }; + const pod = AdmissionRequestCreatePod(); + expect(shouldSkipRequest(binding, pod, [])).toMatch( + /Ignoring Admission Callback: Binding defines name regex '.*' but Object carries '.*'./, ); }); -}); -it("create: should reject when regex name does not match", () => { - const binding = { - model: kind.Pod, - event: Event.ANY, - kind: podKind, - filters: { - name: "", - namespaces: [], - regexNamespaces: [], - regexName: "^default$", - labels: {}, - annotations: {}, - deletionTimestamp: false, - }, - callback, - }; - const pod = AdmissionRequestCreatePod(); - expect(shouldSkipRequest(binding, pod, [])).toMatch( - /Ignoring Admission Callback: Binding defines name regex '.*' but Object carries '.*'./, - ); -}); + it("create: should not reject when regex name does match", () => { + const binding = { + model: kind.Pod, + event: Event.ANY, + kind: podKind, + filters: { + name: "", + namespaces: [], + regexNamespaces: [], + regexName: "^cool", + labels: {}, + annotations: {}, + deletionTimestamp: false, + }, + callback, + }; + const pod = AdmissionRequestCreatePod(); + expect(shouldSkipRequest(binding, pod, [])).toBe(""); + }); -it("create: should not reject when regex name does match", () => { - const binding = { - model: kind.Pod, - event: Event.ANY, - kind: podKind, - filters: { - name: "", - namespaces: [], - regexNamespaces: [], - regexName: "^cool", - labels: {}, - annotations: {}, - deletionTimestamp: false, - }, - callback, - }; - const pod = AdmissionRequestCreatePod(); - expect(shouldSkipRequest(binding, pod, [])).toBe(""); -}); + it("delete: should reject when regex name does not match", () => { + const binding = { + model: kind.Pod, + event: Event.ANY, + kind: podKind, + filters: { + name: "", + namespaces: [], + regexNamespaces: [], + regexName: "^default$", + labels: {}, + annotations: {}, + deletionTimestamp: false, + }, + callback, + }; + const pod = AdmissionRequestDeletePod(); + expect(shouldSkipRequest(binding, pod, [])).toMatch( + /Ignoring Admission Callback: Binding defines name regex '.*' but Object carries '.*'./, + ); + }); -it("delete: should reject when regex name does not match", () => { - const binding = { - model: kind.Pod, - event: Event.ANY, - kind: podKind, - filters: { - name: "", - namespaces: [], - regexNamespaces: [], - regexName: "^default$", - labels: {}, - annotations: {}, - deletionTimestamp: false, - }, - callback, - }; - const pod = AdmissionRequestDeletePod(); - expect(shouldSkipRequest(binding, pod, [])).toMatch( - /Ignoring Admission Callback: Binding defines name regex '.*' but Object carries '.*'./, - ); -}); + it("delete: should not reject when regex name does match", () => { + const binding = { + model: kind.Pod, + event: Event.ANY, + kind: podKind, + filters: { + name: "", + namespaces: [], + regexNamespaces: [], + regexName: "^cool", + labels: {}, + annotations: {}, + deletionTimestamp: false, + }, + callback, + }; + const pod = AdmissionRequestDeletePod(); + expect(shouldSkipRequest(binding, pod, [])).toBe(""); + }); -it("delete: should not reject when regex name does match", () => { - const binding = { - model: kind.Pod, - event: Event.ANY, - kind: podKind, - filters: { - name: "", - namespaces: [], - regexNamespaces: [], - regexName: "^cool", - labels: {}, - annotations: {}, - deletionTimestamp: false, - }, - callback, - }; - const pod = AdmissionRequestDeletePod(); - expect(shouldSkipRequest(binding, pod, [])).toBe(""); -}); + it("create: should not reject when regex namespace does match", () => { + const binding = { + model: kind.Pod, + event: Event.ANY, + kind: podKind, + filters: { + name: "", + namespaces: [], + regexNamespaces: [new RegExp("^helm").source], + regexName: "", + labels: {}, + annotations: {}, + deletionTimestamp: false, + }, + callback, + }; + const pod = AdmissionRequestCreatePod(); + expect(shouldSkipRequest(binding, pod, [])).toBe(""); + }); -it("create: should not reject when regex namespace does match", () => { - const binding = { - model: kind.Pod, - event: Event.ANY, - kind: podKind, - filters: { - name: "", - namespaces: [], - regexNamespaces: [new RegExp("^helm").source], - regexName: "", - labels: {}, - annotations: {}, - deletionTimestamp: false, - }, - callback, - }; - const pod = AdmissionRequestCreatePod(); - expect(shouldSkipRequest(binding, pod, [])).toBe(""); -}); + it("create: should reject when regex namespace does not match", () => { + const binding = { + model: kind.Pod, + event: Event.ANY, + kind: podKind, + filters: { + name: "", + namespaces: [], + regexNamespaces: [new RegExp("^argo").source], + regexName: "", + labels: {}, + annotations: {}, + deletionTimestamp: false, + }, + callback, + }; + const pod = AdmissionRequestCreatePod(); + expect(shouldSkipRequest(binding, pod, [])).toMatch( + /Ignoring Admission Callback: Binding defines namespace regexes '.*' but Object carries '.*'./, + ); + }); -it("create: should reject when regex namespace does not match", () => { - const binding = { - model: kind.Pod, - event: Event.ANY, - kind: podKind, - filters: { - name: "", - namespaces: [], - regexNamespaces: [new RegExp("^argo").source], - regexName: "", - labels: {}, - annotations: {}, - deletionTimestamp: false, - }, - callback, - }; - const pod = AdmissionRequestCreatePod(); - expect(shouldSkipRequest(binding, pod, [])).toMatch( - /Ignoring Admission Callback: Binding defines namespace regexes '.*' but Object carries '.*'./, - ); -}); + it("delete: should reject when regex namespace does not match", () => { + const binding = { + model: kind.Pod, + event: Event.ANY, + kind: podKind, + filters: { + name: "", + namespaces: [], + regexNamespaces: [new RegExp("^argo").source], + regexName: "", + labels: {}, + annotations: {}, + deletionTimestamp: false, + }, + callback, + }; + const pod = AdmissionRequestDeletePod(); + expect(shouldSkipRequest(binding, pod, [])).toMatch( + /Ignoring Admission Callback: Binding defines namespace regexes '.*' but Object carries '.*'./, + ); + }); -it("delete: should reject when regex namespace does not match", () => { - const binding = { - model: kind.Pod, - event: Event.ANY, - kind: podKind, - filters: { - name: "", - namespaces: [], - regexNamespaces: [new RegExp("^argo").source], - regexName: "", - labels: {}, - annotations: {}, - deletionTimestamp: false, - }, - callback, - }; - const pod = AdmissionRequestDeletePod(); - expect(shouldSkipRequest(binding, pod, [])).toMatch( - /Ignoring Admission Callback: Binding defines namespace regexes '.*' but Object carries '.*'./, - ); -}); + it("delete: should not reject when regex namespace does match", () => { + const binding = { + model: kind.Pod, + event: Event.ANY, + kind: podKind, + filters: { + name: "", + namespaces: [], + regexNamespaces: [new RegExp("^helm").source], + regexName: "", + labels: {}, + annotations: {}, + deletionTimestamp: false, + }, + callback, + }; + const pod = AdmissionRequestDeletePod(); + expect(shouldSkipRequest(binding, pod, [])).toBe(""); + }); -it("delete: should not reject when regex namespace does match", () => { - const binding = { - model: kind.Pod, - event: Event.ANY, - kind: podKind, - filters: { - name: "", - namespaces: [], - regexNamespaces: [new RegExp("^helm").source], - regexName: "", - labels: {}, - annotations: {}, - deletionTimestamp: false, - }, - callback, - }; - const pod = AdmissionRequestDeletePod(); - expect(shouldSkipRequest(binding, pod, [])).toBe(""); -}); + it("delete: should reject when name does not match", () => { + const binding = { + model: kind.Pod, + event: Event.ANY, + kind: podKind, + filters: { + name: "bleh", + namespaces: [], + regexNamespaces: [], + regexName: "^not-cool", + labels: {}, + annotations: {}, + deletionTimestamp: false, + }, + callback, + }; + const pod = AdmissionRequestDeletePod(); + expect(shouldSkipRequest(binding, pod, [])).toMatch( + /Ignoring Admission Callback: Binding defines name '.*' but Object carries '.*'./, + ); + }); -it("delete: should reject when name does not match", () => { - const binding = { - model: kind.Pod, - event: Event.ANY, - kind: podKind, - filters: { - name: "bleh", - namespaces: [], - regexNamespaces: [], - regexName: "^not-cool", - labels: {}, - annotations: {}, - deletionTimestamp: false, - }, - callback, - }; - const pod = AdmissionRequestDeletePod(); - expect(shouldSkipRequest(binding, pod, [])).toMatch( - /Ignoring Admission Callback: Binding defines name '.*' but Object carries '.*'./, - ); -}); + it("should reject when kind does not match", () => { + const binding = { + model: kind.Pod, + event: Event.ANY, + kind: { + group: "", + version: "v1", + kind: "Nope", + }, + filters: { + name: "", + namespaces: [], + regexNamespaces: [], + regexName: "", + labels: {}, + annotations: {}, + deletionTimestamp: false, + }, + callback, + }; + const pod = AdmissionRequestCreatePod(); -it("should reject when kind does not match", () => { - const binding = { - model: kind.Pod, - event: Event.ANY, - kind: { - group: "", - version: "v1", - kind: "Nope", - }, - filters: { - name: "", - namespaces: [], - regexNamespaces: [], - regexName: "", - labels: {}, - annotations: {}, - deletionTimestamp: false, - }, - callback, - }; - const pod = AdmissionRequestCreatePod(); + expect(shouldSkipRequest(binding, pod, [])).toMatch( + /Ignoring Admission Callback: Binding defines kind '.*' but Request declares '.*'./, + ); + }); - expect(shouldSkipRequest(binding, pod, [])).toMatch( - /Ignoring Admission Callback: Binding defines kind '.*' but Request declares '.*'./, - ); -}); + it("should reject when group does not match", () => { + const binding = { + model: kind.Pod, + event: Event.ANY, + kind: { + group: "Nope", + version: "v1", + kind: "Pod", + }, + filters: { + name: "", + namespaces: [], + labels: {}, + annotations: {}, + deletionTimestamp: false, + regexNamespaces: [], + regexName: "", + }, + callback, + }; + const pod = AdmissionRequestCreatePod(); -it("should reject when group does not match", () => { - const binding = { - model: kind.Pod, - event: Event.ANY, - kind: { - group: "Nope", - version: "v1", - kind: "Pod", - }, - filters: { - name: "", - namespaces: [], - labels: {}, - annotations: {}, - deletionTimestamp: false, - regexNamespaces: [], - regexName: "", - }, - callback, - }; - const pod = AdmissionRequestCreatePod(); + expect(shouldSkipRequest(binding, pod, [])).toMatch( + /Ignoring Admission Callback: Binding defines group '.*' but Request declares '.*'./, + ); + }); - expect(shouldSkipRequest(binding, pod, [])).toMatch( - /Ignoring Admission Callback: Binding defines group '.*' but Request declares '.*'./, - ); -}); + it("should reject when version does not match", () => { + const binding = { + model: kind.Pod, + event: Event.ANY, + kind: { + group: "", + version: "Nope", + kind: "Pod", + }, + filters: { + name: "", + namespaces: [], + labels: {}, + annotations: {}, + deletionTimestamp: false, + regexNamespaces: [], + regexName: "", + }, + callback, + }; + const pod = AdmissionRequestCreatePod(); -it("should reject when version does not match", () => { - const binding = { - model: kind.Pod, - event: Event.ANY, - kind: { - group: "", - version: "Nope", - kind: "Pod", - }, - filters: { - name: "", - namespaces: [], - labels: {}, - annotations: {}, - deletionTimestamp: false, - regexNamespaces: [], - regexName: "", - }, - callback, - }; - const pod = AdmissionRequestCreatePod(); + expect(shouldSkipRequest(binding, pod, [])).toMatch( + /Ignoring Admission Callback: Binding defines version '.*' but Request declares '.*'./, + ); + }); - expect(shouldSkipRequest(binding, pod, [])).toMatch( - /Ignoring Admission Callback: Binding defines version '.*' but Request declares '.*'./, - ); -}); + it("should allow when group, version, and kind match", () => { + const binding = { + model: kind.Pod, + event: Event.ANY, + kind: podKind, + filters: { + name: "", + namespaces: [], + labels: {}, + annotations: {}, + deletionTimestamp: false, + regexNamespaces: [], + regexName: "", + }, + callback, + }; + const pod = AdmissionRequestCreatePod(); -it("should allow when group, version, and kind match", () => { - const binding = { - model: kind.Pod, - event: Event.ANY, - kind: podKind, - filters: { - name: "", - namespaces: [], - labels: {}, - annotations: {}, - deletionTimestamp: false, - regexNamespaces: [], - regexName: "", - }, - callback, - }; - const pod = AdmissionRequestCreatePod(); + expect(shouldSkipRequest(binding, pod, [])).toBe(""); + }); - expect(shouldSkipRequest(binding, pod, [])).toBe(""); -}); + it("should allow when kind match and others are empty", () => { + const binding = { + model: kind.Pod, + event: Event.ANY, + kind: { + group: "", + version: "", + kind: "Pod", + }, + filters: { + name: "", + namespaces: [], + labels: {}, + annotations: {}, + deletionTimestamp: false, + regexNamespaces: [], + regexName: "", + }, + callback, + }; + const pod = AdmissionRequestCreatePod(); -it("should allow when kind match and others are empty", () => { - const binding = { - model: kind.Pod, - event: Event.ANY, - kind: { - group: "", - version: "", - kind: "Pod", - }, - filters: { - name: "", - namespaces: [], - labels: {}, - annotations: {}, - deletionTimestamp: false, - regexNamespaces: [], - regexName: "", - }, - callback, - }; - const pod = AdmissionRequestCreatePod(); + expect(shouldSkipRequest(binding, pod, [])).toBe(""); + }); - expect(shouldSkipRequest(binding, pod, [])).toBe(""); -}); + it("should reject when the capability namespace does not match", () => { + const binding = { + model: kind.Pod, + event: Event.ANY, + kind: podKind, + filters: { + name: "", + namespaces: [], + labels: {}, + annotations: {}, + deletionTimestamp: false, + regexNamespaces: [], + regexName: "", + }, + callback, + }; + const pod = AdmissionRequestCreatePod(); -it("should reject when the capability namespace does not match", () => { - const binding = { - model: kind.Pod, - event: Event.ANY, - kind: podKind, - filters: { - name: "", - namespaces: [], - labels: {}, - annotations: {}, - deletionTimestamp: false, - regexNamespaces: [], - regexName: "", - }, - callback, - }; - const pod = AdmissionRequestCreatePod(); + expect(shouldSkipRequest(binding, pod, ["bleh", "bleh2"])).toMatch( + /Ignoring Admission Callback: Object carries namespace '.*' but namespaces allowed by Capability are '.*'./, + ); + }); - expect(shouldSkipRequest(binding, pod, ["bleh", "bleh2"])).toMatch( - /Ignoring Admission Callback: Object carries namespace '.*' but namespaces allowed by Capability are '.*'./, - ); -}); + it("should reject when namespace does not match", () => { + const binding = { + model: kind.Pod, + event: Event.ANY, + kind: podKind, + filters: { + name: "", + namespaces: ["bleh"], + labels: {}, + annotations: {}, + deletionTimestamp: false, + regexNamespaces: [], + regexName: "", + }, + callback, + }; + const pod = AdmissionRequestCreatePod(); -it("should reject when namespace does not match", () => { - const binding = { - model: kind.Pod, - event: Event.ANY, - kind: podKind, - filters: { - name: "", - namespaces: ["bleh"], - labels: {}, - annotations: {}, - deletionTimestamp: false, - regexNamespaces: [], - regexName: "", - }, - callback, - }; - const pod = AdmissionRequestCreatePod(); + expect(shouldSkipRequest(binding, pod, [])).toMatch( + /Ignoring Admission Callback: Binding defines namespaces '.*' but Object carries '.*'./, + ); + }); - expect(shouldSkipRequest(binding, pod, [])).toMatch( - /Ignoring Admission Callback: Binding defines namespaces '.*' but Object carries '.*'./, - ); -}); + it("should allow when namespace is match", () => { + const binding = { + model: kind.Pod, + event: Event.ANY, + kind: podKind, + filters: { + name: "", + namespaces: ["helm-releasename", "unicorn", "things"], + labels: {}, + annotations: {}, + deletionTimestamp: false, + regexNamespaces: [], + regexName: "", + }, + callback, + }; + const pod = AdmissionRequestCreatePod(); -it("should allow when namespace is match", () => { - const binding = { - model: kind.Pod, - event: Event.ANY, - kind: podKind, - filters: { - name: "", - namespaces: ["helm-releasename", "unicorn", "things"], - labels: {}, - annotations: {}, - deletionTimestamp: false, - regexNamespaces: [], - regexName: "", - }, - callback, - }; - const pod = AdmissionRequestCreatePod(); + expect(shouldSkipRequest(binding, pod, [])).toBe(""); + }); - expect(shouldSkipRequest(binding, pod, [])).toBe(""); -}); + it("should reject when label does not match", () => { + const binding = { + model: kind.Pod, + event: Event.ANY, + kind: podKind, + filters: { + name: "", + namespaces: [], + labels: { + foo: "bar", + }, + annotations: {}, + deletionTimestamp: false, + regexNamespaces: [], + regexName: "", + }, + callback, + }; + const pod = AdmissionRequestCreatePod(); -it("should reject when label does not match", () => { - const binding = { - model: kind.Pod, - event: Event.ANY, - kind: podKind, - filters: { - name: "", - namespaces: [], - labels: { - foo: "bar", + expect(shouldSkipRequest(binding, pod, [])).toMatch( + /Ignoring Admission Callback: Binding defines labels '.*' but Object carries '.*'./, + ); + }); + + it("should allow when label is match", () => { + const binding = { + model: kind.Pod, + event: Event.ANY, + kind: podKind, + filters: { + name: "", + deletionTimestamp: false, + namespaces: [], + regexNamespaces: [], + regexName: "", + labels: { + foo: "bar", + test: "test1", + }, + annotations: {}, }, - annotations: {}, - deletionTimestamp: false, - regexNamespaces: [], - regexName: "", - }, - callback, - }; - const pod = AdmissionRequestCreatePod(); + callback, + }; + + const pod = AdmissionRequestCreatePod(); + pod.object.metadata = pod.object.metadata || {}; + pod.object.metadata.labels = { + foo: "bar", + test: "test1", + test2: "test2", + }; + + expect(shouldSkipRequest(binding, pod, [])).toBe(""); + }); - expect(shouldSkipRequest(binding, pod, [])).toMatch( - /Ignoring Admission Callback: Binding defines labels '.*' but Object carries '.*'./, - ); -}); + it("should reject when annotation does not match", () => { + const binding = { + model: kind.Pod, + event: Event.ANY, + kind: podKind, + filters: { + name: "", + namespaces: [], + labels: {}, + annotations: { + foo: "bar", + }, + deletionTimestamp: false, + regexNamespaces: [], + regexName: "", + }, + callback, + }; + const pod = AdmissionRequestCreatePod(); -it("should allow when label is match", () => { - const binding = { - model: kind.Pod, - event: Event.ANY, - kind: podKind, - filters: { - name: "", - deletionTimestamp: false, - namespaces: [], - regexNamespaces: [], - regexName: "", - labels: { - foo: "bar", - test: "test1", + expect(shouldSkipRequest(binding, pod, [])).toMatch( + /Ignoring Admission Callback: Binding defines annotations '.*' but Object carries '.*'./, + ); + }); + + it("should allow when annotation is match", () => { + const binding = { + model: kind.Pod, + event: Event.ANY, + kind: podKind, + filters: { + name: "", + namespaces: [], + labels: {}, + annotations: { + foo: "bar", + test: "test1", + }, + deletionTimestamp: false, + regexNamespaces: [], + regexName: "", }, - annotations: {}, - }, - callback, - }; + callback, + }; + + const pod = AdmissionRequestCreatePod(); + pod.object.metadata = pod.object.metadata || {}; + pod.object.metadata.annotations = { + foo: "bar", + test: "test1", + test2: "test2", + }; + + expect(shouldSkipRequest(binding, pod, [])).toBe(""); + }); - const pod = AdmissionRequestCreatePod(); - pod.object.metadata = pod.object.metadata || {}; - pod.object.metadata.labels = { - foo: "bar", - test: "test1", - test2: "test2", - }; + it("should use `oldObject` when the operation is `DELETE`", () => { + const binding = { + model: kind.Pod, + event: Event.DELETE, + kind: podKind, + filters: { + name: "", + namespaces: [], + regexNamespaces: [], + regexName: "", + deletionTimestamp: false, + labels: { + "test-op": "delete", + }, + annotations: {}, + }, + callback, + }; - expect(shouldSkipRequest(binding, pod, [])).toBe(""); -}); + const pod = AdmissionRequestDeletePod(); -it("should reject when annotation does not match", () => { - const binding = { - model: kind.Pod, - event: Event.ANY, - kind: podKind, - filters: { - name: "", - namespaces: [], - labels: {}, - annotations: { - foo: "bar", + expect(shouldSkipRequest(binding, pod, [])).toBe(""); + }); + + it("should allow when deletionTimestamp is present on pod", () => { + const binding = { + model: kind.Pod, + event: Event.ANY, + kind: podKind, + filters: { + name: "", + namespaces: [], + labels: {}, + regexNamespaces: [], + regexName: "", + annotations: { + foo: "bar", + test: "test1", + }, + deletionTimestamp: true, }, - deletionTimestamp: false, - regexNamespaces: [], - regexName: "", - }, - callback, - }; - const pod = AdmissionRequestCreatePod(); + callback, + }; + + const pod = AdmissionRequestCreatePod(); + pod.object.metadata = pod.object.metadata || {}; + pod.object.metadata!.deletionTimestamp = new Date("2021-09-01T00:00:00Z"); + pod.object.metadata.annotations = { + foo: "bar", + test: "test1", + test2: "test2", + }; + + expect(shouldSkipRequest(binding, pod, [])).toBe(""); + }); - expect(shouldSkipRequest(binding, pod, [])).toMatch( - /Ignoring Admission Callback: Binding defines annotations '.*' but Object carries '.*'./, - ); + it("should reject when deletionTimestamp is not present on pod", () => { + const binding = { + model: kind.Pod, + event: Event.ANY, + kind: podKind, + filters: { + name: "", + namespaces: [], + labels: {}, + regexNamespaces: [], + regexName: "", + annotations: { + foo: "bar", + test: "test1", + }, + deletionTimestamp: true, + }, + callback, + }; + + const pod = AdmissionRequestCreatePod(); + pod.object.metadata = pod.object.metadata || {}; + pod.object.metadata.annotations = { + foo: "bar", + test: "test1", + test2: "test2", + }; + + expect(shouldSkipRequest(binding, pod, [])).toMatch( + /Ignoring Admission Callback: Binding defines deletionTimestamp but Object does not carry it./, + ); + }); }); -it("should allow when annotation is match", () => { - const binding = { - model: kind.Pod, - event: Event.ANY, - kind: podKind, - filters: { - name: "", - namespaces: [], - labels: {}, - annotations: { - foo: "bar", - test: "test1", - }, - deletionTimestamp: false, - regexNamespaces: [], - regexName: "", +export const podKind = modelToGroupVersionKind(kind.Pod.name); +export const deploymentKind = modelToGroupVersionKind(kind.Deployment.name); +export const clusterRoleKind = modelToGroupVersionKind(kind.ClusterRole.name); + +export const groupBinding: Binding = { + event: Event.CREATE, + filters: defaultFilters, + kind: deploymentKind, + model: kind.Deployment, +}; + +export const clusterScopedBinding: Binding = { + event: Event.DELETE, + filters: defaultFilters, + kind: clusterRoleKind, + model: kind.ClusterRole, +}; + +describe("filterNoMatchReason", () => { + it.each([ + [{}], + [{ metadata: { namespace: "pepr-uds" } }], + [{ metadata: { namespace: "pepr-core" } }], + [{ metadata: { namespace: "uds-ns" } }], + [{ metadata: { namespace: "uds" } }], + ])( + "given %j, it returns regex namespace filter error for Pods whose namespace does not match the regex", + (obj: KubernetesObject) => { + const kubernetesObject: KubernetesObject = obj.metadata + ? { + ...defaultKubernetesObject, + metadata: { ...defaultKubernetesObject.metadata, namespace: obj.metadata.namespace }, + } + : { ...defaultKubernetesObject, metadata: obj as unknown as undefined }; + const binding: Binding = { + ...defaultBinding, + kind: { kind: "Pod", group: "some-group" }, + filters: { ...defaultFilters, regexNamespaces: [new RegExp("(.*)-system").source] }, + }; + + const capabilityNamespaces: string[] = []; + const expectedErrorMessage = `Ignoring Watch Callback: Binding defines namespace regexes '["(.*)-system"]' but Object carries`; + const result = filterNoMatchReason(binding, kubernetesObject, capabilityNamespaces); + expect(result).toEqual( + typeof kubernetesObject.metadata === "object" && obj !== null && Object.keys(obj).length > 0 + ? `${expectedErrorMessage} '${kubernetesObject.metadata.namespace}'.` + : `${expectedErrorMessage} ''.`, + ); }, - callback, - }; + ); - const pod = AdmissionRequestCreatePod(); - pod.object.metadata = pod.object.metadata || {}; - pod.object.metadata.annotations = { - foo: "bar", - test: "test1", - test2: "test2", - }; + describe("when pod namespace matches the namespace regex", () => { + it.each([["pepr-system"], ["pepr-uds-system"], ["uds-system"], ["some-thing-that-is-a-system"], ["your-system"]])( + "should not return an error message (namespace: '%s')", + namespace => { + const binding: Binding = { + ...defaultBinding, + kind: { kind: "Pod", group: "some-group" }, + filters: { + ...defaultFilters, + regexName: "", + regexNamespaces: [new RegExp("(.*)-system").source], + namespaces: [], + }, + }; + const kubernetesObject: KubernetesObject = { ...defaultKubernetesObject, metadata: { namespace: namespace } }; + const capabilityNamespaces: string[] = []; + const result = filterNoMatchReason(binding, kubernetesObject, capabilityNamespaces); + expect(result).toEqual(""); + }, + ); + }); - expect(shouldSkipRequest(binding, pod, [])).toBe(""); -}); + // Names Fail + it("returns regex name filter error for Pods whos name does not match the regex", () => { + const binding: Binding = { + ...defaultBinding, + kind: { kind: "Pod", group: "some-group" }, + filters: { ...defaultFilters, regexName: "^system", namespaces: [] }, + }; + const obj = { metadata: { name: "pepr-demo" } }; + const objArray = [ + { ...obj }, + { ...obj, metadata: { name: "pepr-uds" } }, + { ...obj, metadata: { name: "pepr-core" } }, + { ...obj, metadata: { name: "uds-ns" } }, + { ...obj, metadata: { name: "uds" } }, + ]; + const capabilityNamespaces: string[] = []; + objArray.map(object => { + const result = filterNoMatchReason(binding, object as unknown as Partial, capabilityNamespaces); + expect(result).toEqual( + `Ignoring Watch Callback: Binding defines name regex '^system' but Object carries '${object?.metadata?.name}'.`, + ); + }); + }); -it("should use `oldObject` when the operation is `DELETE`", () => { - const binding = { - model: kind.Pod, - event: Event.DELETE, - kind: podKind, - filters: { - name: "", - namespaces: [], - regexNamespaces: [], - regexName: "", - deletionTimestamp: false, - labels: { - "test-op": "delete", + // Names Pass + it("returns no regex name filter error for Pods whos name does match the regex", () => { + const binding: Binding = { + ...defaultBinding, + kind: { kind: "Pod", group: "some-group" }, + filters: { ...defaultFilters, regexName: "^system" }, + }; + const obj = { metadata: { name: "pepr-demo" } }; + const objArray = [ + { ...obj, metadata: { name: "systemd" } }, + { ...obj, metadata: { name: "systemic" } }, + { ...obj, metadata: { name: "system-of-kube-apiserver" } }, + { ...obj, metadata: { name: "system" } }, + { ...obj, metadata: { name: "system-uds" } }, + ]; + const capabilityNamespaces: string[] = []; + objArray.map(object => { + const result = filterNoMatchReason(binding, object as unknown as Partial, capabilityNamespaces); + expect(result).toEqual(``); + }); + }); + + describe("when capability namespaces are present", () => { + it("should return missingCarriableNamespace filter error for cluster-scoped objects", () => { + const binding: Binding = { + ...defaultBinding, + filters: { ...defaultFilters, regexName: "" }, + kind: { kind: "ClusterRole", group: "some-group" }, + }; + const obj: KubernetesObject = { + kind: "ClusterRole", + apiVersion: "rbac.authorization.k8s.io/v1", + metadata: { name: "clusterrole1" }, + }; + const capabilityNamespaces: string[] = ["monitoring"]; + const result = filterNoMatchReason(binding, obj, capabilityNamespaces); + expect(result).toEqual( + "Ignoring Watch Callback: Object does not carry a namespace but namespaces allowed by Capability are '[\"monitoring\"]'.", + ); + }); + }); + + it("returns mismatchedNamespace filter error for clusterScoped objects with namespace filters", () => { + const binding: Binding = { + ...defaultBinding, + kind: { kind: "ClusterRole", group: "some-group" }, + filters: { ...defaultFilters, namespaces: ["ns1"] }, + }; + const obj = { + kind: "ClusterRole", + apiVersion: "rbac.authorization.k8s.io/v1", + metadata: { name: "clusterrole1" }, + }; + const capabilityNamespaces: string[] = []; + const result = filterNoMatchReason(binding, obj as unknown as Partial, capabilityNamespaces); + expect(result).toEqual("Ignoring Watch Callback: Binding defines namespaces '[\"ns1\"]' but Object carries ''."); + }); + + it("returns namespace filter error for namespace objects with namespace filters", () => { + const binding: Binding = { + ...defaultBinding, + kind: { kind: "Namespace", group: "some-group" }, + filters: { ...defaultFilters, namespaces: ["ns1"] }, + }; + const obj = {}; + const capabilityNamespaces: string[] = []; + const result = filterNoMatchReason(binding, obj as unknown as Partial, capabilityNamespaces); + expect(result).toEqual("Ignoring Watch Callback: Cannot use namespace filter on a namespace object."); + }); + + it("return an Ignoring Watch Callback string if the binding name and object name are different", () => { + const binding: Binding = { + ...defaultBinding, + filters: { ...defaultFilters, name: "pepr" }, + }; + const obj = { + metadata: { + name: "not-pepr", }, - annotations: {}, - }, - callback, - }; + }; + const capabilityNamespaces: string[] = []; + const result = filterNoMatchReason(binding, obj as unknown as Partial, capabilityNamespaces); + expect(result).toEqual(`Ignoring Watch Callback: Binding defines name 'pepr' but Object carries 'not-pepr'.`); + }); - const pod = AdmissionRequestDeletePod(); + describe("when the binding name and KubernetesObject name are the same", () => { + it("should not return an Ignoring Watch Callback message", () => { + const binding: Binding = { + ...defaultBinding, + filters: { ...defaultFilters, regexName: "", name: "pepr" }, + }; + const obj: KubernetesObject = { + metadata: { name: "pepr" }, + }; + const capabilityNamespaces: string[] = []; + const result = filterNoMatchReason(binding, obj, capabilityNamespaces); + expect(result).toEqual(""); + }); + }); - expect(shouldSkipRequest(binding, pod, [])).toBe(""); -}); + it("return deletionTimestamp error when there is no deletionTimestamp in the object", () => { + const binding: Binding = { + ...defaultBinding, + filters: { ...defaultFilters, deletionTimestamp: true }, + }; + const obj = { + metadata: {}, + }; + const capabilityNamespaces: string[] = []; + const result = filterNoMatchReason(binding, obj as unknown as Partial, capabilityNamespaces); + expect(result).toEqual("Ignoring Watch Callback: Binding defines deletionTimestamp but Object does not carry it."); + }); -it("should allow when deletionTimestamp is present on pod", () => { - const binding = { - model: kind.Pod, - event: Event.ANY, - kind: podKind, - filters: { - name: "", - namespaces: [], - labels: {}, - regexNamespaces: [], - regexName: "", - annotations: { - foo: "bar", - test: "test1", + it("return no deletionTimestamp error when there is a deletionTimestamp in the object", () => { + const binding: Binding = { + ...defaultBinding, + filters: { ...defaultFilters, deletionTimestamp: true }, + }; + const obj = { + metadata: { + deletionTimestamp: "2021-01-01T00:00:00Z", }, - deletionTimestamp: true, - }, - callback, - }; + }; + const capabilityNamespaces: string[] = []; + const result = filterNoMatchReason(binding, obj as unknown as Partial, capabilityNamespaces); + expect(result).not.toEqual("Ignoring Watch Callback: Binding defines deletionTimestamp Object does not carry it."); + }); - const pod = AdmissionRequestCreatePod(); - pod.object.metadata = pod.object.metadata || {}; - pod.object.metadata!.deletionTimestamp = new Date("2021-09-01T00:00:00Z"); - pod.object.metadata.annotations = { - foo: "bar", - test: "test1", - test2: "test2", - }; + it("returns label overlap error when there is no overlap between binding and object labels", () => { + const binding: Binding = { + ...defaultBinding, + filters: { ...defaultFilters, labels: { key: "value" } }, + }; + const obj = { + metadata: { labels: { anotherKey: "anotherValue" } }, + }; + const capabilityNamespaces: string[] = []; + const result = filterNoMatchReason(binding, obj as unknown as Partial, capabilityNamespaces); + expect(result).toEqual( + `Ignoring Watch Callback: Binding defines labels '{"key":"value"}' but Object carries '{"anotherKey":"anotherValue"}'.`, + ); + }); - expect(shouldSkipRequest(binding, pod, [])).toBe(""); -}); + it("returns annotation overlap error when there is no overlap between binding and object annotations", () => { + const binding: Binding = { + ...defaultBinding, + filters: { ...defaultFilters, annotations: { key: "value" } }, + }; + const obj = { + metadata: { annotations: { anotherKey: "anotherValue" } }, + }; + const capabilityNamespaces: string[] = []; + const result = filterNoMatchReason(binding, obj as unknown as Partial, capabilityNamespaces); + expect(result).toEqual( + `Ignoring Watch Callback: Binding defines annotations '{"key":"value"}' but Object carries '{"anotherKey":"anotherValue"}'.`, + ); + }); -it("should reject when deletionTimestamp is not present on pod", () => { - const binding = { - model: kind.Pod, - event: Event.ANY, - kind: podKind, - filters: { - name: "", - namespaces: [], - labels: {}, - regexNamespaces: [], - regexName: "", - annotations: { - foo: "bar", - test: "test1", + it("returns capability namespace error when object is not in capability namespaces", () => { + const binding: Binding = { + model: kind.Pod, + event: Event.ANY, + kind: { + group: "", + version: "v1", + kind: "Pod", }, - deletionTimestamp: true, - }, - callback, - }; + filters: { + name: "bleh", + namespaces: [], + regexNamespaces: [], + regexName: "", + labels: {}, + annotations: {}, + deletionTimestamp: false, + }, + watchCallback: callback, + }; + + const obj = { + metadata: { namespace: "ns2", name: "bleh" }, + }; + const capabilityNamespaces = ["ns1"]; + const result = filterNoMatchReason(binding, obj as unknown as Partial, capabilityNamespaces); + expect(result).toEqual( + `Ignoring Watch Callback: Object carries namespace 'ns2' but namespaces allowed by Capability are '["ns1"]'.`, + ); + }); - const pod = AdmissionRequestCreatePod(); - pod.object.metadata = pod.object.metadata || {}; - pod.object.metadata.annotations = { - foo: "bar", - test: "test1", - test2: "test2", - }; + it("returns binding namespace error when filter namespace is not part of capability namespaces", () => { + const binding: Binding = { + ...defaultBinding, + filters: { ...defaultFilters, namespaces: ["ns3"], regexNamespaces: [] }, + }; + const obj = {}; + const capabilityNamespaces = ["ns1", "ns2"]; + const result = filterNoMatchReason(binding, obj as unknown as Partial, capabilityNamespaces); + expect(result).toEqual( + `Ignoring Watch Callback: Binding defines namespaces ["ns3"] but namespaces allowed by Capability are '["ns1","ns2"]'.`, + ); + }); - expect(shouldSkipRequest(binding, pod, [])).toMatch( - /Ignoring Admission Callback: Binding defines deletionTimestamp but Object does not carry it./, - ); + it("returns binding and object namespace error when they do not overlap", () => { + const binding: Binding = { + ...defaultBinding, + filters: { ...defaultFilters, namespaces: ["ns1"], regexNamespaces: [] }, + }; + const obj = { + metadata: { namespace: "ns2" }, + }; + const capabilityNamespaces = ["ns1", "ns2"]; + const result = filterNoMatchReason(binding, obj as unknown as Partial, capabilityNamespaces); + expect(result).toEqual(`Ignoring Watch Callback: Binding defines namespaces '["ns1"]' but Object carries 'ns2'.`); + }); + + describe("when a KubernetesObject is in an ingnored namespace", () => { + it("should return a watch violation message", () => { + const binding: Binding = { + ...defaultBinding, + filters: { ...defaultFilters, regexName: "", namespaces: ["ns3"] }, + }; + const kubernetesObject: KubernetesObject = { + ...defaultKubernetesObject, + metadata: { namespace: "ns3" }, + }; + const capabilityNamespaces = ["ns3"]; + const ignoredNamespaces = ["ns3"]; + const result = filterNoMatchReason(binding, kubernetesObject, capabilityNamespaces, ignoredNamespaces); + expect(result).toEqual( + `Ignoring Watch Callback: Object carries namespace 'ns3' but ignored namespaces include '["ns3"]'.`, + ); + }); + }); + + it("returns empty string when all checks pass", () => { + const binding: Binding = { + ...defaultBinding, + filters: { + ...defaultFilters, + regexName: "", + namespaces: ["ns1"], + labels: { key: "value" }, + annotations: { key: "value" }, + }, + }; + const obj = { + metadata: { namespace: "ns1", labels: { key: "value" }, annotations: { key: "value" } }, + }; + const capabilityNamespaces = ["ns1"]; + const result = filterNoMatchReason(binding, obj as unknown as Partial, capabilityNamespaces); + expect(result).toEqual(""); + }); }); describe("adjudicateMisboundDeleteWithDeletionTimestamp", () => { @@ -995,3 +1354,35 @@ describe("adjudicateMismatchedNamespaceRegex", () => { expect(result).toBe(null); }); }); + +describe("adjudicateMisboundNamespace", () => { + const defaultBinding: Binding = { + event: Event.CREATE, + filters: { + annotations: {}, + deletionTimestamp: false, + labels: {}, + name: "", + namespaces: [], + regexName: "^default$", + regexNamespaces: [], + }, + kind: { + group: "v1", + kind: "Namespace", + }, + model: kind.Namespace, + }; + + it("should return nothing when binding is correct", () => { + const result = adjudicateMisboundNamespace(defaultBinding); + expect(result).toBe(null); + }); + + it("should return reason when binding is incorrect", () => { + const testBinding = clone(defaultBinding); + testBinding.filters.namespaces = ["oof"]; + const result = adjudicateMisboundNamespace(testBinding); + expect(result).toBe(`Cannot use namespace filter on a namespace object.`); + }); +}); diff --git a/src/lib/filter/filter.ts b/src/lib/filter/filter.ts index 62f10548c..366591d34 100644 --- a/src/lib/filter/filter.ts +++ b/src/lib/filter/filter.ts @@ -5,37 +5,38 @@ import { AdmissionRequest, Binding } from "../types"; import { Operation } from "../enums"; import { KubernetesObject } from "kubernetes-fluent-client"; import { - carriesIgnoredNamespace, + carriedAnnotations, + carriedLabels, carriedName, - definedEvent, - declaredOperation, - definedName, - definedGroup, + carriedNamespace, + carriesIgnoredNamespace, declaredGroup, - definedVersion, + declaredKind, + declaredOperation, declaredVersion, + definedAnnotations, + definedEvent, + definedGroup, definedKind, - declaredKind, - definedNamespaces, - carriedNamespace, definedLabels, - carriedLabels, - definedAnnotations, - carriedAnnotations, - definedNamespaceRegexes, + definedName, definedNameRegex, + definedNamespaceRegexes, + definedNamespaces, + definedVersion, misboundDeleteWithDeletionTimestamp, - mismatchedDeletionTimestamp, + misboundNamespace, mismatchedAnnotations, + mismatchedDeletionTimestamp, + mismatchedEvent, + mismatchedGroup, + mismatchedKind, mismatchedLabels, mismatchedName, mismatchedNameRegex, mismatchedNamespace, mismatchedNamespaceRegex, - mismatchedEvent, - mismatchedGroup, mismatchedVersion, - mismatchedKind, missingCarriableNamespace, unbindableNamespaces, uncarryableNamespace, @@ -43,10 +44,12 @@ import { type AdjudicationResult = string | null; /** - * shouldSkipRequest determines if a request should be skipped based on the binding filters. + * shouldSkipRequest determines if an admission request should be skipped based on the binding filters. * * @param binding the action binding * @param req the incoming request + * @param capabilityNamespaces the namespaces allowed by capability + * @param ignoredNamespaces the namespaces ignored by module config * @returns */ export function shouldSkipRequest( @@ -87,6 +90,55 @@ export function shouldSkipRequest( return ""; } +/** + * filterNoMatchReason determines whether a callback should be skipped after + * receiving an update event from the API server, based on the binding filters. + * + * @param binding the action binding + * @param kubernetesObject the incoming kubernetes object + * @param capabilityNamespaces the namespaces allowed by capability + * @param ignoredNamespaces the namespaces ignored by module config + */ +/** + * Decide to run callback after the event comes back from API Server + **/ +export function filterNoMatchReason( + binding: Binding, + obj: Partial, + capabilityNamespaces: string[], + ignoredNamespaces?: string[], +): string { + const prefix = "Ignoring Watch Callback:"; + + const adjudicators: Array<() => AdjudicationResult> = [ + (): AdjudicationResult => adjudicateMismatchedDeletionTimestamp(binding, obj), + (): AdjudicationResult => adjudicateMismatchedName(binding, obj), + (): AdjudicationResult => adjudicateMisboundNamespace(binding), + (): AdjudicationResult => adjudicateMismatchedLabels(binding, obj), + (): AdjudicationResult => adjudicateMismatchedAnnotations(binding, obj), + (): AdjudicationResult => adjudicateUncarryableNamespace(capabilityNamespaces, obj), + (): AdjudicationResult => adjudicateUnbindableNamespaces(capabilityNamespaces, binding), + (): AdjudicationResult => adjudicateMismatchedNamespace(binding, obj), + (): AdjudicationResult => adjudicateMismatchedNamespaceRegex(binding, obj), + (): AdjudicationResult => adjudicateMismatchedNameRegex(binding, obj), + (): AdjudicationResult => adjudicateCarriesIgnoredNamespace(ignoredNamespaces, obj), + (): AdjudicationResult => adjudicateMissingCarriableNamespace(capabilityNamespaces, obj), + ]; + + for (const adjudicate of adjudicators) { + const result = adjudicate(); + if (result) { + return `${prefix} ${result}`; + } + } + + return ""; +} + +export function adjudicateMisboundNamespace(binding: Binding): AdjudicationResult { + return misboundNamespace(binding) ? "Cannot use namespace filter on a namespace object." : null; +} + export function adjudicateMisboundDeleteWithDeletionTimestamp(binding: Binding): AdjudicationResult { return misboundDeleteWithDeletionTimestamp(binding) ? "Cannot use deletionTimestamp filter on a DELETE operation." diff --git a/src/lib/filter/filterNoMatchReason.ts b/src/lib/filter/filterNoMatchReason.ts deleted file mode 100644 index 04329ea29..000000000 --- a/src/lib/filter/filterNoMatchReason.ts +++ /dev/null @@ -1,108 +0,0 @@ -import { KubernetesObject } from "kubernetes-fluent-client"; -import { - mismatchedDeletionTimestamp, - mismatchedName, - definedName, - carriedName, - misboundNamespace, - mismatchedLabels, - definedLabels, - carriedLabels, - mismatchedAnnotations, - definedAnnotations, - carriedAnnotations, - uncarryableNamespace, - carriedNamespace, - unbindableNamespaces, - definedNamespaces, - mismatchedNamespace, - mismatchedNamespaceRegex, - definedNamespaceRegexes, - mismatchedNameRegex, - definedNameRegex, - carriesIgnoredNamespace, - missingCarriableNamespace, -} from "./adjudicators/adjudicators"; -import { Binding } from "../types"; - -/** - * Decide to run callback after the event comes back from API Server - **/ -//TODO -export function filterNoMatchReason( - binding: Binding, - kubernetesObject: Partial, - capabilityNamespaces: string[], - ignoredNamespaces?: string[], -): string { - const prefix = "Ignoring Watch Callback:"; - - // prettier-ignore - return ( - mismatchedDeletionTimestamp(binding, kubernetesObject) ? - `${prefix} Binding defines deletionTimestamp but Object does not carry it.` : - - mismatchedName(binding, kubernetesObject) ? - `${prefix} Binding defines name '${definedName(binding)}' but Object carries '${carriedName(kubernetesObject)}'.` : - - misboundNamespace(binding) ? - `${prefix} Cannot use namespace filter on a namespace object.` : - - mismatchedLabels(binding, kubernetesObject) ? - ( - `${prefix} Binding defines labels '${JSON.stringify(definedLabels(binding))}' ` + - `but Object carries '${JSON.stringify(carriedLabels(kubernetesObject))}'.` - ) : - - mismatchedAnnotations(binding, kubernetesObject) ? - ( - `${prefix} Binding defines annotations '${JSON.stringify(definedAnnotations(binding))}' ` + - `but Object carries '${JSON.stringify(carriedAnnotations(kubernetesObject))}'.` - ) : - - uncarryableNamespace(capabilityNamespaces, kubernetesObject) ? - ( - `${prefix} Object carries namespace '${carriedNamespace(kubernetesObject)}' ` + - `but namespaces allowed by Capability are '${JSON.stringify(capabilityNamespaces)}'.` - ) : - - unbindableNamespaces(capabilityNamespaces, binding) ? - ( - `${prefix} Binding defines namespaces ${JSON.stringify(definedNamespaces(binding))} ` + - `but namespaces allowed by Capability are '${JSON.stringify(capabilityNamespaces)}'.` - ) : - - mismatchedNamespace(binding, kubernetesObject) ? - ( - `${prefix} Binding defines namespaces '${JSON.stringify(definedNamespaces(binding))}' ` + - `but Object carries '${carriedNamespace(kubernetesObject)}'.` - ) : - - mismatchedNamespaceRegex(binding, kubernetesObject) ? - ( - `${prefix} Binding defines namespace regexes ` + - `'${JSON.stringify(definedNamespaceRegexes(binding))}' ` + - `but Object carries '${carriedNamespace(kubernetesObject)}'.` - ) : - - mismatchedNameRegex(binding, kubernetesObject) ? - ( - `${prefix} Binding defines name regex '${definedNameRegex(binding)}' ` + - `but Object carries '${carriedName(kubernetesObject)}'.` - ) : - - carriesIgnoredNamespace(ignoredNamespaces, kubernetesObject) ? - ( - `${prefix} Object carries namespace '${carriedNamespace(kubernetesObject)}' ` + - `but ignored namespaces include '${JSON.stringify(ignoredNamespaces)}'.` - ) : - - missingCarriableNamespace(capabilityNamespaces, kubernetesObject) ? - ( - `${prefix} Object does not carry a namespace ` + - `but namespaces allowed by Capability are '${JSON.stringify(capabilityNamespaces)}'.` - ) : - - "" - ); -} diff --git a/src/lib/filter/shouldSkipRequest.test.ts b/src/lib/filter/shouldSkipRequest.test.ts deleted file mode 100644 index 8cac7f733..000000000 --- a/src/lib/filter/shouldSkipRequest.test.ts +++ /dev/null @@ -1,613 +0,0 @@ -// SPDX-License-Identifier: Apache-2.0 -// SPDX-FileCopyrightText: 2023-Present The Pepr Authors - -import { expect, describe, it } from "@jest/globals"; -import { kind } from "kubernetes-fluent-client"; -import * as fc from "fast-check"; -import { - AdmissionRequestCreateClusterRole, - AdmissionRequestCreateDeployment, - AdmissionRequestCreatePod, - AdmissionRequestDeletePod, -} from "../../fixtures/loader"; -import { shouldSkipRequest } from "./filter"; -import { AdmissionRequest, Binding, Filters } from "../types"; -import { Event } from "../enums"; -import { defaultFilters } from "./adjudicators/defaultTestObjects"; -import { clusterScopedBinding, groupBinding, podKind } from "../helpers.test"; - -const defaultBinding: Binding = { - event: Event.ANY, - filters: defaultFilters, - kind: podKind, - model: kind.Pod, -}; - -describe("when fuzzing shouldSkipRequest", () => { - it("should handle random inputs without crashing", () => { - fc.assert( - fc.property( - fc.record({ - event: fc.constantFrom("CREATE", "UPDATE", "DELETE", "ANY"), - kind: fc.record({ - group: fc.string(), - version: fc.string(), - kind: fc.string(), - }), - filters: fc.record({ - name: fc.string(), - namespaces: fc.array(fc.string()), - labels: fc.dictionary(fc.string(), fc.string()), - annotations: fc.dictionary(fc.string(), fc.string()), - deletionTimestamp: fc.boolean(), - }), - }), - fc.record({ - operation: fc.string(), - uid: fc.string(), - name: fc.string(), - namespace: fc.string(), - kind: fc.record({ - group: fc.string(), - version: fc.string(), - kind: fc.string(), - }), - object: fc.record({ - metadata: fc.record({ - deletionTimestamp: fc.option(fc.date()), - }), - }), - }), - fc.array(fc.string()), - (binding, req, capabilityNamespaces) => { - expect(() => - shouldSkipRequest(binding as Binding, req as AdmissionRequest, capabilityNamespaces), - ).not.toThrow(); - }, - ), - { numRuns: 100 }, - ); - }); - - it("should only skip requests that do not match the binding criteria", () => { - fc.assert( - fc.property( - fc.record({ - event: fc.constantFrom("CREATE", "UPDATE", "DELETE", "ANY"), - kind: fc.record({ - group: fc.string(), - version: fc.string(), - kind: fc.string(), - }), - filters: fc.record({ - name: fc.string(), - namespaces: fc.array(fc.string()), - labels: fc.dictionary(fc.string(), fc.string()), - annotations: fc.dictionary(fc.string(), fc.string()), - deletionTimestamp: fc.boolean(), - }), - }), - fc.record({ - operation: fc.string(), - uid: fc.string(), - name: fc.string(), - namespace: fc.string(), - kind: fc.record({ - group: fc.string(), - version: fc.string(), - kind: fc.string(), - }), - object: fc.record({ - metadata: fc.record({ - deletionTimestamp: fc.option(fc.date()), - }), - }), - }), - fc.array(fc.string()), - (binding, req, capabilityNamespaces) => { - const shouldSkip = shouldSkipRequest(binding as Binding, req as AdmissionRequest, capabilityNamespaces); - expect(typeof shouldSkip).toBe("string"); - }, - ), - { numRuns: 100 }, - ); - }); -}); - -describe("when checking specific properties of shouldSkipRequest()", () => {}); - -describe("when a binding contains a group scoped object", () => { - const admissionRequestDeployment = AdmissionRequestCreateDeployment(); - const admissionRequestPod = AdmissionRequestCreatePod(); - it("should skip request when the group is different", () => { - expect(shouldSkipRequest(groupBinding, admissionRequestPod, [])).toMatch( - /Ignoring Admission Callback: Binding defines group '.+' but Request declares ''./, - ); - }); - it("should not skip request when the group is the same", () => { - const groupBindingNoRegex: Binding = { - ...groupBinding, - filters: { - ...groupBinding.filters, - regexName: "", - }, - }; - expect(shouldSkipRequest(groupBindingNoRegex, admissionRequestDeployment, [])).toMatch(""); - }); -}); - -describe("when a capability defines namespaces and the admission request object is cluster-scoped", () => { - const capabilityNamespaces = ["monitoring"]; - const admissionRequestCreateClusterRole = AdmissionRequestCreateClusterRole(); - it("should skip request when the capability namespace does not exist on the object", () => { - const binding: Binding = { - ...clusterScopedBinding, - event: Event.CREATE, - filters: { - ...clusterScopedBinding.filters, - regexName: "", - }, - }; - - expect(shouldSkipRequest(binding, admissionRequestCreateClusterRole, capabilityNamespaces)).toMatch( - /Ignoring Admission Callback: Object does not carry a namespace but namespaces allowed by Capability are '.+'./, - ); - }); -}); -describe("when a binding contains a cluster scoped object", () => { - const admissionRequestCreateClusterRole = AdmissionRequestCreateClusterRole(); - - it("should skip request when the binding defines a namespace on a cluster scoped object", () => { - const clusterScopedBindingWithNamespace: Binding = { - ...clusterScopedBinding, - event: Event.CREATE, - filters: { - ...clusterScopedBinding.filters, - namespaces: ["namespace"], - }, - }; - expect(shouldSkipRequest(clusterScopedBindingWithNamespace, admissionRequestCreateClusterRole, [])).toMatch( - /Ignoring Admission Callback: Binding defines namespaces '.+' but Object carries ''./, - ); - }); -}); - -describe("when a pod is created", () => { - it("should reject when regex name does not match", () => { - const pod = AdmissionRequestCreatePod(); - expect(shouldSkipRequest(defaultBinding, pod, [])).toMatch( - /Ignoring Admission Callback: Binding defines name regex '.+' but Object carries '.+'./, - ); - }); - - it("should not reject when regex name does match", () => { - const filters: Filters = { ...defaultFilters, regexName: "^cool" }; - const binding: Binding = { ...defaultBinding, filters }; - const pod = AdmissionRequestCreatePod(); - expect(shouldSkipRequest(binding, pod, [])).toBe(""); - }); - - it("should not reject when regex namespace does match", () => { - const filters: Filters = { - ...defaultFilters, - regexNamespaces: [new RegExp("^helm").source], - regexName: "", - }; - - const binding: Binding = { ...defaultBinding, filters }; - const pod = AdmissionRequestCreatePod(); - expect(shouldSkipRequest(binding, pod, [])).toBe(""); - }); - - it("should reject when regex namespace does not match", () => { - const filters: Filters = { ...defaultFilters, regexNamespaces: [new RegExp("^argo").source] }; - const binding: Binding = { ...defaultBinding, filters }; - const pod = AdmissionRequestCreatePod(); - expect(shouldSkipRequest(binding, pod, [])).toMatch( - /Ignoring Admission Callback: Binding defines namespace regexes '.+' but Object carries '.+'./, - ); - }); - it("should not reject when namespace is not ignored", () => { - const filters: Filters = { ...defaultFilters, regexName: "" }; - const binding: Binding = { ...defaultBinding, filters }; - const pod = AdmissionRequestCreatePod(); - expect(shouldSkipRequest(binding, pod, [])).toMatch(""); - }); - it("should reject when namespace is ignored", () => { - const filters: Filters = { ...defaultFilters, regexName: "" }; - const binding: Binding = { ...defaultBinding, filters }; - const pod = AdmissionRequestCreatePod(); - expect(shouldSkipRequest(binding, pod, [], ["helm-releasename"])).toMatch( - /Ignoring Admission Callback: Object carries namespace '.+' but ignored namespaces include '.+'./, - ); - }); -}); - -describe("when a pod is deleted", () => { - it("should reject when regex name does not match", () => { - const filters: Filters = { ...defaultFilters, regexName: "^default$" }; - const binding: Binding = { ...defaultBinding, filters }; - const pod = AdmissionRequestDeletePod(); - expect(shouldSkipRequest(binding, pod, [])).toMatch( - /Ignoring Admission Callback: Binding defines name regex '.+' but Object carries '.+'./, - ); - }); - - it("should not reject when regex name does match", () => { - const filters: Filters = { ...defaultFilters, regexName: "^cool" }; - const binding: Binding = { ...defaultBinding, filters }; - const pod = AdmissionRequestDeletePod(); - expect(shouldSkipRequest(binding, pod, [])).toBe(""); - }); - - it("should reject when regex namespace does not match", () => { - const filters: Filters = { ...defaultFilters, regexNamespaces: [new RegExp("eargo").source] }; - const binding: Binding = { - ...defaultBinding, - filters, - }; - const pod = AdmissionRequestDeletePod(); - expect(shouldSkipRequest(binding, pod, [])).toMatch( - /Ignoring Admission Callback: Binding defines namespace regexes '.+' but Object carries '.+'./, - ); - }); - - it("should not reject when regex namespace does match", () => { - const filters: Filters = { - ...defaultFilters, - regexNamespaces: [new RegExp("^helm").source], - regexName: "", - labels: {}, - annotations: {}, - deletionTimestamp: false, - }; - const binding: Binding = { - ...defaultBinding, - filters, - }; - const pod = AdmissionRequestDeletePod(); - expect(shouldSkipRequest(binding, pod, [])).toBe(""); - }); - - it("should reject when name does not match", () => { - const filters: Filters = { ...defaultFilters, name: "bleh", regexName: "^not-cool" }; - const binding: Binding = { - ...defaultBinding, - filters, - }; - const pod = AdmissionRequestDeletePod(); - expect(shouldSkipRequest(binding, pod, [])).toMatch( - /Ignoring Admission Callback: Binding defines name '.+' but Object carries '.+'./, - ); - }); - - it("should reject when namespace is ignored", () => { - const filters: Filters = { ...defaultFilters, regexName: "", namespaces: [] }; - const binding: Binding = { - ...defaultBinding, - filters, - }; - const pod = AdmissionRequestDeletePod(); - expect(shouldSkipRequest(binding, pod, [], ["helm-releasename"])).toMatch( - /Ignoring Admission Callback: Object carries namespace '.+' but ignored namespaces include '.+'./, - ); - }); - - it("should not reject when namespace is not ignored", () => { - const filters: Filters = { ...defaultFilters, regexName: "" }; - const binding: Binding = { - ...defaultBinding, - filters, - }; - const pod = AdmissionRequestDeletePod(); - expect(shouldSkipRequest(binding, pod, [])).toMatch(""); - }); -}); - -it("should reject when kind does not match", () => { - const filters: Filters = { ...defaultFilters, regexName: "" }; - const binding: Binding = { - ...defaultBinding, - kind: { - group: "", - version: "v1", - kind: "Nope", - }, - filters, - }; - const pod = AdmissionRequestCreatePod(); - - expect(shouldSkipRequest(binding, pod, [])).toMatch( - /Ignoring Admission Callback: Binding defines kind '.+' but Request declares 'Pod'./, - ); -}); - -it("should reject when group does not match", () => { - const filters: Filters = { ...defaultFilters, regexName: "" }; - const binding: Binding = { - ...defaultBinding, - kind: { - group: "Nope", - version: "v1", - kind: "Pod", - }, - filters, - }; - const pod = AdmissionRequestCreatePod(); - - expect(shouldSkipRequest(binding, pod, [])).toMatch( - /Ignoring Admission Callback: Binding defines group '.+' but Request declares ''./, - ); -}); - -it("should reject when version does not match", () => { - const filters: Filters = { ...defaultFilters, regexName: "" }; - const binding: Binding = { - ...defaultBinding, - kind: { - group: "", - version: "Nope", - kind: "Pod", - }, - filters, - }; - const pod = AdmissionRequestCreatePod(); - - expect(shouldSkipRequest(binding, pod, [])).toMatch( - /Ignoring Admission Callback: Binding defines version '.+' but Request declares '.+'./, - ); -}); - -it("should allow when group, version, and kind match", () => { - const filters: Filters = { ...defaultFilters, regexName: "" }; - const binding: Binding = { ...defaultBinding, filters }; - const pod = AdmissionRequestCreatePod(); - - expect(shouldSkipRequest(binding, pod, [])).toBe(""); -}); - -it("should allow when kind match and others are empty", () => { - const filters: Filters = { ...defaultFilters, regexName: "" }; - - const binding: Binding = { ...defaultBinding, filters }; - const pod = AdmissionRequestCreatePod(); - - expect(shouldSkipRequest(binding, pod, [])).toBe(""); -}); - -it("should reject when the capability namespace does not match", () => { - const filters: Filters = { ...defaultFilters }; - const binding: Binding = { - ...defaultBinding, - filters, - }; - const pod = AdmissionRequestCreatePod(); - - expect(shouldSkipRequest(binding, pod, ["bleh", "bleh2"])).toMatch( - /Ignoring Admission Callback: Object carries namespace '.+' but namespaces allowed by Capability are '.+'./, - ); -}); - -it("should reject when namespace does not match", () => { - const filters: Filters = { ...defaultFilters, namespaces: ["bleh"] }; - const binding: Binding = { ...defaultBinding, filters }; - const pod = AdmissionRequestCreatePod(); - - expect(shouldSkipRequest(binding, pod, [])).toMatch( - /Ignoring Admission Callback: Binding defines namespaces '.+' but Object carries '.+'./, - ); -}); - -it("should allow when namespace is match", () => { - const filters: Filters = { - ...defaultFilters, - namespaces: ["helm-releasename", "unicorn", "things"], - labels: {}, - annotations: {}, - deletionTimestamp: false, - regexNamespaces: [], - regexName: "", - }; - const binding: Binding = { - ...defaultBinding, - filters, - }; - const pod = AdmissionRequestCreatePod(); - - expect(shouldSkipRequest(binding, pod, [])).toBe(""); -}); - -it("should reject when label does not match", () => { - const filters: Filters = { - ...defaultFilters, - labels: { - foo: "bar", - }, - }; - const binding: Binding = { - ...defaultBinding, - filters, - }; - const pod = AdmissionRequestCreatePod(); - - expect(shouldSkipRequest(binding, pod, [])).toMatch( - /Ignoring Admission Callback: Binding defines labels '.+' but Object carries '.+'./, - ); -}); - -it("should allow when label is match", () => { - const filters: Filters = { - ...defaultFilters, - regexName: "", - labels: { - foo: "bar", - test: "test1", - }, - annotations: {}, - }; - const binding: Binding = { - ...defaultBinding, - filters, - }; - - const pod = AdmissionRequestCreatePod(); - pod.object.metadata = pod.object.metadata || {}; - pod.object.metadata.labels = { - foo: "bar", - test: "test1", - test2: "test2", - }; - - expect(shouldSkipRequest(binding, pod, [])).toBe(""); -}); - -it("should reject when annotation does not match", () => { - const filters: Filters = { - ...defaultFilters, - annotations: { - foo: "bar", - }, - }; - const binding: Binding = { - ...defaultBinding, - filters, - }; - const pod = AdmissionRequestCreatePod(); - - expect(shouldSkipRequest(binding, pod, [])).toMatch( - /Ignoring Admission Callback: Binding defines annotations '.+' but Object carries '.+'./, - ); -}); - -it("should allow when annotation is match", () => { - const filters: Filters = { - name: "", - namespaces: [], - labels: {}, - annotations: { - foo: "bar", - test: "test1", - }, - deletionTimestamp: false, - regexNamespaces: [], - regexName: "", - }; - const binding: Binding = { - ...defaultBinding, - filters, - }; - - const pod = AdmissionRequestCreatePod(); - pod.object.metadata = pod.object.metadata || {}; - pod.object.metadata.annotations = { - foo: "bar", - test: "test1", - test2: "test2", - }; - - expect(shouldSkipRequest(binding, pod, [])).toBe(""); -}); - -it("should use `oldObject` when the operation is `DELETE`", () => { - const filters: Filters = { - ...defaultFilters, - regexNamespaces: [], - regexName: "", - deletionTimestamp: false, - labels: { - "test-op": "delete", - }, - annotations: {}, - }; - const binding: Binding = { - ...defaultBinding, - filters, - }; - - const pod = AdmissionRequestDeletePod(); - - expect(shouldSkipRequest(binding, pod, [])).toBe(""); -}); - -it("should allow when deletionTimestamp is present on pod", () => { - const filters: Filters = { - name: "", - namespaces: [], - labels: {}, - regexNamespaces: [], - regexName: "", - annotations: { - foo: "bar", - test: "test1", - }, - deletionTimestamp: true, - }; - const binding: Binding = { - ...defaultBinding, - filters, - }; - - const pod = AdmissionRequestCreatePod(); - pod.object.metadata = pod.object.metadata || {}; - pod.object.metadata!.deletionTimestamp = new Date("2021-09-01T00:00:00Z"); - pod.object.metadata.annotations = { - foo: "bar", - test: "test1", - test2: "test2", - }; - - expect(shouldSkipRequest(binding, pod, [])).toBe(""); -}); - -it("should reject when deletionTimestamp is not present on pod", () => { - const filters: Filters = { - ...defaultFilters, - regexName: "", - annotations: { - foo: "bar", - test: "test1", - }, - deletionTimestamp: true, - }; - const binding: Binding = { ...defaultBinding, filters }; - - const pod = AdmissionRequestCreatePod(); - pod.object.metadata = pod.object.metadata || {}; - pod.object.metadata.annotations = { - foo: "bar", - test: "test1", - test2: "test2", - }; - - expect(shouldSkipRequest(binding, pod, [])).toMatch( - /Ignoring Admission Callback: Binding defines deletionTimestamp but Object does not carry it./, - ); -}); - -describe("when multiple filters are triggered", () => { - const filters: Filters = { - ...defaultFilters, - regexName: "asdf", - name: "not-a-match", - namespaces: ["not-allowed", "also-not-matching"], - }; - const binding: Binding = { ...defaultBinding, filters }; - it("should display the failure message for the first matching filter", () => { - const pod = AdmissionRequestCreatePod(); - expect(shouldSkipRequest(binding, pod, [])).toMatch( - /Ignoring Admission Callback: Binding defines name 'not-a-match' but Object carries '.+'./, - ); - }); - it("should NOT display the failure message for the second matching filter", () => { - const pod = AdmissionRequestCreatePod(); - expect(shouldSkipRequest(binding, pod, [])).not.toMatch( - /Ignoring Admission Callback: Binding defines namespaces 'not-allowed,also-not-matching' but Object carries '.+'./, - ); - }); - it("should NOT display the failure message for the third matching filter", () => { - const pod = AdmissionRequestCreatePod(); - expect(shouldSkipRequest(binding, pod, [])).not.toMatch( - /Ignoring Admission Callback: Binding defines name regex 'asdf' but Object carries '.*./, - ); - }); -}); diff --git a/src/lib/helpers.test.ts b/src/lib/helpers.test.ts index 78ce1203a..eaac6a8dc 100644 --- a/src/lib/helpers.test.ts +++ b/src/lib/helpers.test.ts @@ -1,7 +1,7 @@ // SPDX-License-Identifier: Apache-2.0 // SPDX-FileCopyrightText: 2023-Present The Pepr Authors -import { Binding, CapabilityExport } from "./types"; +import { CapabilityExport } from "./types"; import { Event } from "./enums"; import { bindingAndCapabilityNSConflict, @@ -20,34 +20,14 @@ import { validateCapabilityNames, ValidationError, } from "./helpers"; -import { filterNoMatchReason } from "./filter/filterNoMatchReason"; import { sanitizeResourceName } from "../sdk/sdk"; import * as fc from "fast-check"; import { expect, describe, jest, beforeEach, afterEach, it } from "@jest/globals"; import { SpiedFunction } from "jest-mock"; -import { KubernetesObject, kind, modelToGroupVersionKind } from "kubernetes-fluent-client"; -import { defaultFilters, defaultKubernetesObject, defaultBinding } from "./filter/adjudicators/defaultTestObjects"; +import { kind } from "kubernetes-fluent-client"; export const callback = () => undefined; -export const podKind = modelToGroupVersionKind(kind.Pod.name); -export const deploymentKind = modelToGroupVersionKind(kind.Deployment.name); -export const clusterRoleKind = modelToGroupVersionKind(kind.ClusterRole.name); - -export const groupBinding: Binding = { - event: Event.CREATE, - filters: defaultFilters, - kind: deploymentKind, - model: kind.Deployment, -}; - -export const clusterScopedBinding: Binding = { - event: Event.DELETE, - filters: defaultFilters, - kind: clusterRoleKind, - model: kind.ClusterRole, -}; - const mockCapabilities: CapabilityExport[] = JSON.parse(`[ { "name": "hello-pepr", @@ -780,339 +760,6 @@ describe("replaceString", () => { }); }); -describe("filterNoMatchReason", () => { - it.each([ - [{}], - [{ metadata: { namespace: "pepr-uds" } }], - [{ metadata: { namespace: "pepr-core" } }], - [{ metadata: { namespace: "uds-ns" } }], - [{ metadata: { namespace: "uds" } }], - ])( - "given %j, it returns regex namespace filter error for Pods whose namespace does not match the regex", - (obj: KubernetesObject) => { - const kubernetesObject: KubernetesObject = obj.metadata - ? { - ...defaultKubernetesObject, - metadata: { ...defaultKubernetesObject.metadata, namespace: obj.metadata.namespace }, - } - : { ...defaultKubernetesObject, metadata: obj as unknown as undefined }; - const binding: Binding = { - ...defaultBinding, - kind: { kind: "Pod", group: "some-group" }, - filters: { ...defaultFilters, regexNamespaces: [new RegExp("(.*)-system").source] }, - }; - - const capabilityNamespaces: string[] = []; - const expectedErrorMessage = `Ignoring Watch Callback: Binding defines namespace regexes '["(.*)-system"]' but Object carries`; - const result = filterNoMatchReason(binding, kubernetesObject, capabilityNamespaces); - expect(result).toEqual( - typeof kubernetesObject.metadata === "object" && obj !== null && Object.keys(obj).length > 0 - ? `${expectedErrorMessage} '${kubernetesObject.metadata.namespace}'.` - : `${expectedErrorMessage} ''.`, - ); - }, - ); -}); - -describe("when pod namespace matches the namespace regex", () => { - it.each([["pepr-system"], ["pepr-uds-system"], ["uds-system"], ["some-thing-that-is-a-system"], ["your-system"]])( - "should not return an error message (namespace: '%s')", - namespace => { - const binding: Binding = { - ...defaultBinding, - kind: { kind: "Pod", group: "some-group" }, - filters: { - ...defaultFilters, - regexName: "", - regexNamespaces: [new RegExp("(.*)-system").source], - namespaces: [], - }, - }; - const kubernetesObject: KubernetesObject = { ...defaultKubernetesObject, metadata: { namespace: namespace } }; - const capabilityNamespaces: string[] = []; - const result = filterNoMatchReason(binding, kubernetesObject, capabilityNamespaces); - expect(result).toEqual(""); - }, - ); -}); - -// Names Fail -it("returns regex name filter error for Pods whos name does not match the regex", () => { - const binding: Binding = { - ...defaultBinding, - kind: { kind: "Pod", group: "some-group" }, - filters: { ...defaultFilters, regexName: "^system", namespaces: [] }, - }; - const obj = { metadata: { name: "pepr-demo" } }; - const objArray = [ - { ...obj }, - { ...obj, metadata: { name: "pepr-uds" } }, - { ...obj, metadata: { name: "pepr-core" } }, - { ...obj, metadata: { name: "uds-ns" } }, - { ...obj, metadata: { name: "uds" } }, - ]; - const capabilityNamespaces: string[] = []; - objArray.map(object => { - const result = filterNoMatchReason(binding, object as unknown as Partial, capabilityNamespaces); - expect(result).toEqual( - `Ignoring Watch Callback: Binding defines name regex '^system' but Object carries '${object?.metadata?.name}'.`, - ); - }); -}); - -// Names Pass -it("returns no regex name filter error for Pods whos name does match the regex", () => { - const binding: Binding = { - ...defaultBinding, - kind: { kind: "Pod", group: "some-group" }, - filters: { ...defaultFilters, regexName: "^system" }, - }; - const obj = { metadata: { name: "pepr-demo" } }; - const objArray = [ - { ...obj, metadata: { name: "systemd" } }, - { ...obj, metadata: { name: "systemic" } }, - { ...obj, metadata: { name: "system-of-kube-apiserver" } }, - { ...obj, metadata: { name: "system" } }, - { ...obj, metadata: { name: "system-uds" } }, - ]; - const capabilityNamespaces: string[] = []; - objArray.map(object => { - const result = filterNoMatchReason(binding, object as unknown as Partial, capabilityNamespaces); - expect(result).toEqual(``); - }); -}); - -describe("when capability namespaces are present", () => { - it("should return missingCarriableNamespace filter error for cluster-scoped objects", () => { - const binding: Binding = { - ...defaultBinding, - filters: { ...defaultFilters, regexName: "" }, - kind: { kind: "ClusterRole", group: "some-group" }, - }; - const obj: KubernetesObject = { - kind: "ClusterRole", - apiVersion: "rbac.authorization.k8s.io/v1", - metadata: { name: "clusterrole1" }, - }; - const capabilityNamespaces: string[] = ["monitoring"]; - const result = filterNoMatchReason(binding, obj, capabilityNamespaces); - expect(result).toEqual( - "Ignoring Watch Callback: Object does not carry a namespace but namespaces allowed by Capability are '[\"monitoring\"]'.", - ); - }); -}); - -it("returns mismatchedNamespace filter error for clusterScoped objects with namespace filters", () => { - const binding: Binding = { - ...defaultBinding, - kind: { kind: "ClusterRole", group: "some-group" }, - filters: { ...defaultFilters, namespaces: ["ns1"] }, - }; - const obj = { - kind: "ClusterRole", - apiVersion: "rbac.authorization.k8s.io/v1", - metadata: { name: "clusterrole1" }, - }; - const capabilityNamespaces: string[] = []; - const result = filterNoMatchReason(binding, obj as unknown as Partial, capabilityNamespaces); - expect(result).toEqual("Ignoring Watch Callback: Binding defines namespaces '[\"ns1\"]' but Object carries ''."); -}); - -it("returns namespace filter error for namespace objects with namespace filters", () => { - const binding: Binding = { - ...defaultBinding, - kind: { kind: "Namespace", group: "some-group" }, - filters: { ...defaultFilters, namespaces: ["ns1"] }, - }; - const obj = {}; - const capabilityNamespaces: string[] = []; - const result = filterNoMatchReason(binding, obj as unknown as Partial, capabilityNamespaces); - expect(result).toEqual("Ignoring Watch Callback: Cannot use namespace filter on a namespace object."); -}); - -it("return an Ignoring Watch Callback string if the binding name and object name are different", () => { - const binding: Binding = { - ...defaultBinding, - filters: { ...defaultFilters, name: "pepr" }, - }; - const obj = { - metadata: { - name: "not-pepr", - }, - }; - const capabilityNamespaces: string[] = []; - const result = filterNoMatchReason(binding, obj as unknown as Partial, capabilityNamespaces); - expect(result).toEqual(`Ignoring Watch Callback: Binding defines name 'pepr' but Object carries 'not-pepr'.`); -}); - -describe("when the binding name and KubernetesObject name are the same", () => { - it("should not return an Ignoring Watch Callback message", () => { - const binding: Binding = { - ...defaultBinding, - filters: { ...defaultFilters, regexName: "", name: "pepr" }, - }; - const obj: KubernetesObject = { - metadata: { name: "pepr" }, - }; - const capabilityNamespaces: string[] = []; - const result = filterNoMatchReason(binding, obj, capabilityNamespaces); - expect(result).toEqual(""); - }); -}); - -it("return deletionTimestamp error when there is no deletionTimestamp in the object", () => { - const binding: Binding = { - ...defaultBinding, - filters: { ...defaultFilters, deletionTimestamp: true }, - }; - const obj = { - metadata: {}, - }; - const capabilityNamespaces: string[] = []; - const result = filterNoMatchReason(binding, obj as unknown as Partial, capabilityNamespaces); - expect(result).toEqual("Ignoring Watch Callback: Binding defines deletionTimestamp but Object does not carry it."); -}); - -it("return no deletionTimestamp error when there is a deletionTimestamp in the object", () => { - const binding: Binding = { - ...defaultBinding, - filters: { ...defaultFilters, deletionTimestamp: true }, - }; - const obj = { - metadata: { - deletionTimestamp: "2021-01-01T00:00:00Z", - }, - }; - const capabilityNamespaces: string[] = []; - const result = filterNoMatchReason(binding, obj as unknown as Partial, capabilityNamespaces); - expect(result).not.toEqual("Ignoring Watch Callback: Binding defines deletionTimestamp Object does not carry it."); -}); - -it("returns label overlap error when there is no overlap between binding and object labels", () => { - const binding: Binding = { - ...defaultBinding, - filters: { ...defaultFilters, labels: { key: "value" } }, - }; - const obj = { - metadata: { labels: { anotherKey: "anotherValue" } }, - }; - const capabilityNamespaces: string[] = []; - const result = filterNoMatchReason(binding, obj as unknown as Partial, capabilityNamespaces); - expect(result).toEqual( - `Ignoring Watch Callback: Binding defines labels '{"key":"value"}' but Object carries '{"anotherKey":"anotherValue"}'.`, - ); -}); - -it("returns annotation overlap error when there is no overlap between binding and object annotations", () => { - const binding: Binding = { - ...defaultBinding, - filters: { ...defaultFilters, annotations: { key: "value" } }, - }; - const obj = { - metadata: { annotations: { anotherKey: "anotherValue" } }, - }; - const capabilityNamespaces: string[] = []; - const result = filterNoMatchReason(binding, obj as unknown as Partial, capabilityNamespaces); - expect(result).toEqual( - `Ignoring Watch Callback: Binding defines annotations '{"key":"value"}' but Object carries '{"anotherKey":"anotherValue"}'.`, - ); -}); - -it("returns capability namespace error when object is not in capability namespaces", () => { - const binding: Binding = { - model: kind.Pod, - event: Event.ANY, - kind: { - group: "", - version: "v1", - kind: "Pod", - }, - filters: { - name: "bleh", - namespaces: [], - regexNamespaces: [], - regexName: "", - labels: {}, - annotations: {}, - deletionTimestamp: false, - }, - watchCallback: callback, - }; - - const obj = { - metadata: { namespace: "ns2", name: "bleh" }, - }; - const capabilityNamespaces = ["ns1"]; - const result = filterNoMatchReason(binding, obj as unknown as Partial, capabilityNamespaces); - expect(result).toEqual( - `Ignoring Watch Callback: Object carries namespace 'ns2' but namespaces allowed by Capability are '["ns1"]'.`, - ); -}); - -it("returns binding namespace error when filter namespace is not part of capability namespaces", () => { - const binding: Binding = { - ...defaultBinding, - filters: { ...defaultFilters, namespaces: ["ns3"], regexNamespaces: [] }, - }; - const obj = {}; - const capabilityNamespaces = ["ns1", "ns2"]; - const result = filterNoMatchReason(binding, obj as unknown as Partial, capabilityNamespaces); - expect(result).toEqual( - `Ignoring Watch Callback: Binding defines namespaces ["ns3"] but namespaces allowed by Capability are '["ns1","ns2"]'.`, - ); -}); - -it("returns binding and object namespace error when they do not overlap", () => { - const binding: Binding = { - ...defaultBinding, - filters: { ...defaultFilters, namespaces: ["ns1"], regexNamespaces: [] }, - }; - const obj = { - metadata: { namespace: "ns2" }, - }; - const capabilityNamespaces = ["ns1", "ns2"]; - const result = filterNoMatchReason(binding, obj as unknown as Partial, capabilityNamespaces); - expect(result).toEqual(`Ignoring Watch Callback: Binding defines namespaces '["ns1"]' but Object carries 'ns2'.`); -}); - -describe("when a KubernetesObject is in an ingnored namespace", () => { - it("should return a watch violation message", () => { - const binding: Binding = { - ...defaultBinding, - filters: { ...defaultFilters, regexName: "", namespaces: ["ns3"] }, - }; - const kubernetesObject: KubernetesObject = { - ...defaultKubernetesObject, - metadata: { namespace: "ns3" }, - }; - const capabilityNamespaces = ["ns3"]; - const ignoredNamespaces = ["ns3"]; - const result = filterNoMatchReason(binding, kubernetesObject, capabilityNamespaces, ignoredNamespaces); - expect(result).toEqual( - `Ignoring Watch Callback: Object carries namespace 'ns3' but ignored namespaces include '["ns3"]'.`, - ); - }); -}); - -it("returns empty string when all checks pass", () => { - const binding: Binding = { - ...defaultBinding, - filters: { - ...defaultFilters, - regexName: "", - namespaces: ["ns1"], - labels: { key: "value" }, - annotations: { key: "value" }, - }, - }; - const obj = { - metadata: { namespace: "ns1", labels: { key: "value" }, annotations: { key: "value" } }, - }; - const capabilityNamespaces = ["ns1"]; - const result = filterNoMatchReason(binding, obj as unknown as Partial, capabilityNamespaces); - expect(result).toEqual(""); -}); - describe("validateHash", () => { let originalExit: (code?: number) => never; diff --git a/src/lib/processors/watch-processor.ts b/src/lib/processors/watch-processor.ts index fac0cb5df..59b3663d8 100644 --- a/src/lib/processors/watch-processor.ts +++ b/src/lib/processors/watch-processor.ts @@ -7,7 +7,7 @@ import { Event } from "../enums"; import { K8s, KubernetesObject, WatchCfg, WatchEvent } from "kubernetes-fluent-client"; import { Queue } from "../queue"; import { WatchPhase } from "kubernetes-fluent-client/dist/fluent/types"; -import { filterNoMatchReason } from "../filter/filterNoMatchReason"; +import { filterNoMatchReason } from "../filter/filter"; import { metricsCollector } from "../telemetry/metrics"; import { removeFinalizer } from "../finalizer"; From d48ce95e65d5b169c45d2141373e1a44cf325d51 Mon Sep 17 00:00:00 2001 From: Barrett LaFrance Date: Tue, 17 Dec 2024 06:54:14 -0600 Subject: [PATCH 3/4] wip: saving progress --- src/lib/filter/filter.ts | 69 ++++++++++++++++++++-------------------- 1 file changed, 34 insertions(+), 35 deletions(-) diff --git a/src/lib/filter/filter.ts b/src/lib/filter/filter.ts index 366591d34..fcf7b2d2a 100644 --- a/src/lib/filter/filter.ts +++ b/src/lib/filter/filter.ts @@ -43,6 +43,8 @@ import { } from "./adjudicators/adjudicators"; type AdjudicationResult = string | null; +type Adjudicator = () => AdjudicationResult; + /** * shouldSkipRequest determines if an admission request should be skipped based on the binding filters. * @@ -61,23 +63,23 @@ export function shouldSkipRequest( const obj = (req.operation === Operation.DELETE ? req.oldObject : req.object)!; const prefix = "Ignoring Admission Callback:"; - const adjudicators: Array<() => AdjudicationResult> = [ - (): AdjudicationResult => adjudicateMisboundDeleteWithDeletionTimestamp(binding), - (): AdjudicationResult => adjudicateMismatchedDeletionTimestamp(binding, obj), - (): AdjudicationResult => adjudicateMismatchedEvent(binding, req), - (): AdjudicationResult => adjudicateMismatchedName(binding, obj), - (): AdjudicationResult => adjudicateMismatchedGroup(binding, req), - (): AdjudicationResult => adjudicateMismatchedVersion(binding, req), - (): AdjudicationResult => adjudicateMismatchedKind(binding, req), - (): AdjudicationResult => adjudicateUnbindableNamespaces(capabilityNamespaces, binding), - (): AdjudicationResult => adjudicateUncarryableNamespace(capabilityNamespaces, obj), - (): AdjudicationResult => adjudicateMismatchedNamespace(binding, obj), - (): AdjudicationResult => adjudicateMismatchedLabels(binding, obj), - (): AdjudicationResult => adjudicateMismatchedAnnotations(binding, obj), - (): AdjudicationResult => adjudicateMismatchedNamespaceRegex(binding, obj), - (): AdjudicationResult => adjudicateMismatchedNameRegex(binding, obj), - (): AdjudicationResult => adjudicateCarriesIgnoredNamespace(ignoredNamespaces, obj), - (): AdjudicationResult => adjudicateMissingCarriableNamespace(capabilityNamespaces, obj), + const adjudicators: Adjudicator[] = [ + () => adjudicateMisboundDeleteWithDeletionTimestamp(binding), + () => adjudicateMismatchedDeletionTimestamp(binding, obj), + () => adjudicateMismatchedEvent(binding, req), + () => adjudicateMismatchedName(binding, obj), + () => adjudicateMismatchedGroup(binding, req), + () => adjudicateMismatchedVersion(binding, req), + () => adjudicateMismatchedKind(binding, req), + () => adjudicateUnbindableNamespaces(capabilityNamespaces, binding), + () => adjudicateUncarryableNamespace(capabilityNamespaces, obj), + () => adjudicateMismatchedNamespace(binding, obj), + () => adjudicateMismatchedLabels(binding, obj), + () => adjudicateMismatchedAnnotations(binding, obj), + () => adjudicateMismatchedNamespaceRegex(binding, obj), + () => adjudicateMismatchedNameRegex(binding, obj), + () => adjudicateCarriesIgnoredNamespace(ignoredNamespaces, obj), + () => adjudicateMissingCarriableNamespace(capabilityNamespaces, obj), ]; for (const adjudicator of adjudicators) { @@ -99,9 +101,6 @@ export function shouldSkipRequest( * @param capabilityNamespaces the namespaces allowed by capability * @param ignoredNamespaces the namespaces ignored by module config */ -/** - * Decide to run callback after the event comes back from API Server - **/ export function filterNoMatchReason( binding: Binding, obj: Partial, @@ -110,23 +109,23 @@ export function filterNoMatchReason( ): string { const prefix = "Ignoring Watch Callback:"; - const adjudicators: Array<() => AdjudicationResult> = [ - (): AdjudicationResult => adjudicateMismatchedDeletionTimestamp(binding, obj), - (): AdjudicationResult => adjudicateMismatchedName(binding, obj), - (): AdjudicationResult => adjudicateMisboundNamespace(binding), - (): AdjudicationResult => adjudicateMismatchedLabels(binding, obj), - (): AdjudicationResult => adjudicateMismatchedAnnotations(binding, obj), - (): AdjudicationResult => adjudicateUncarryableNamespace(capabilityNamespaces, obj), - (): AdjudicationResult => adjudicateUnbindableNamespaces(capabilityNamespaces, binding), - (): AdjudicationResult => adjudicateMismatchedNamespace(binding, obj), - (): AdjudicationResult => adjudicateMismatchedNamespaceRegex(binding, obj), - (): AdjudicationResult => adjudicateMismatchedNameRegex(binding, obj), - (): AdjudicationResult => adjudicateCarriesIgnoredNamespace(ignoredNamespaces, obj), - (): AdjudicationResult => adjudicateMissingCarriableNamespace(capabilityNamespaces, obj), + const adjudicators: Adjudicator[] = [ + () => adjudicateMismatchedDeletionTimestamp(binding, obj), + () => adjudicateMismatchedName(binding, obj), + () => adjudicateMisboundNamespace(binding), + () => adjudicateMismatchedLabels(binding, obj), + () => adjudicateMismatchedAnnotations(binding, obj), + () => adjudicateUncarryableNamespace(capabilityNamespaces, obj), + () => adjudicateUnbindableNamespaces(capabilityNamespaces, binding), + () => adjudicateMismatchedNamespace(binding, obj), + () => adjudicateMismatchedNamespaceRegex(binding, obj), + () => adjudicateMismatchedNameRegex(binding, obj), + () => adjudicateCarriesIgnoredNamespace(ignoredNamespaces, obj), + () => adjudicateMissingCarriableNamespace(capabilityNamespaces, obj), ]; - for (const adjudicate of adjudicators) { - const result = adjudicate(); + for (const adjudicator of adjudicators) { + const result = adjudicator(); if (result) { return `${prefix} ${result}`; } From 95bdf8ef573266c9f75a0097f2559e65a6e2746e Mon Sep 17 00:00:00 2001 From: Barrett LaFrance Date: Tue, 17 Dec 2024 08:09:39 -0600 Subject: [PATCH 4/4] wip: adding func return types, because --- src/lib/filter/filter.ts | 56 ++++++++++++++++++++-------------------- 1 file changed, 28 insertions(+), 28 deletions(-) diff --git a/src/lib/filter/filter.ts b/src/lib/filter/filter.ts index fcf7b2d2a..a0043286c 100644 --- a/src/lib/filter/filter.ts +++ b/src/lib/filter/filter.ts @@ -64,22 +64,22 @@ export function shouldSkipRequest( const prefix = "Ignoring Admission Callback:"; const adjudicators: Adjudicator[] = [ - () => adjudicateMisboundDeleteWithDeletionTimestamp(binding), - () => adjudicateMismatchedDeletionTimestamp(binding, obj), - () => adjudicateMismatchedEvent(binding, req), - () => adjudicateMismatchedName(binding, obj), - () => adjudicateMismatchedGroup(binding, req), - () => adjudicateMismatchedVersion(binding, req), - () => adjudicateMismatchedKind(binding, req), - () => adjudicateUnbindableNamespaces(capabilityNamespaces, binding), - () => adjudicateUncarryableNamespace(capabilityNamespaces, obj), - () => adjudicateMismatchedNamespace(binding, obj), - () => adjudicateMismatchedLabels(binding, obj), - () => adjudicateMismatchedAnnotations(binding, obj), - () => adjudicateMismatchedNamespaceRegex(binding, obj), - () => adjudicateMismatchedNameRegex(binding, obj), - () => adjudicateCarriesIgnoredNamespace(ignoredNamespaces, obj), - () => adjudicateMissingCarriableNamespace(capabilityNamespaces, obj), + (): AdjudicationResult => adjudicateMisboundDeleteWithDeletionTimestamp(binding), + (): AdjudicationResult => adjudicateMismatchedDeletionTimestamp(binding, obj), + (): AdjudicationResult => adjudicateMismatchedEvent(binding, req), + (): AdjudicationResult => adjudicateMismatchedName(binding, obj), + (): AdjudicationResult => adjudicateMismatchedGroup(binding, req), + (): AdjudicationResult => adjudicateMismatchedVersion(binding, req), + (): AdjudicationResult => adjudicateMismatchedKind(binding, req), + (): AdjudicationResult => adjudicateUnbindableNamespaces(capabilityNamespaces, binding), + (): AdjudicationResult => adjudicateUncarryableNamespace(capabilityNamespaces, obj), + (): AdjudicationResult => adjudicateMismatchedNamespace(binding, obj), + (): AdjudicationResult => adjudicateMismatchedLabels(binding, obj), + (): AdjudicationResult => adjudicateMismatchedAnnotations(binding, obj), + (): AdjudicationResult => adjudicateMismatchedNamespaceRegex(binding, obj), + (): AdjudicationResult => adjudicateMismatchedNameRegex(binding, obj), + (): AdjudicationResult => adjudicateCarriesIgnoredNamespace(ignoredNamespaces, obj), + (): AdjudicationResult => adjudicateMissingCarriableNamespace(capabilityNamespaces, obj), ]; for (const adjudicator of adjudicators) { @@ -110,18 +110,18 @@ export function filterNoMatchReason( const prefix = "Ignoring Watch Callback:"; const adjudicators: Adjudicator[] = [ - () => adjudicateMismatchedDeletionTimestamp(binding, obj), - () => adjudicateMismatchedName(binding, obj), - () => adjudicateMisboundNamespace(binding), - () => adjudicateMismatchedLabels(binding, obj), - () => adjudicateMismatchedAnnotations(binding, obj), - () => adjudicateUncarryableNamespace(capabilityNamespaces, obj), - () => adjudicateUnbindableNamespaces(capabilityNamespaces, binding), - () => adjudicateMismatchedNamespace(binding, obj), - () => adjudicateMismatchedNamespaceRegex(binding, obj), - () => adjudicateMismatchedNameRegex(binding, obj), - () => adjudicateCarriesIgnoredNamespace(ignoredNamespaces, obj), - () => adjudicateMissingCarriableNamespace(capabilityNamespaces, obj), + (): AdjudicationResult => adjudicateMismatchedDeletionTimestamp(binding, obj), + (): AdjudicationResult => adjudicateMismatchedName(binding, obj), + (): AdjudicationResult => adjudicateMisboundNamespace(binding), + (): AdjudicationResult => adjudicateMismatchedLabels(binding, obj), + (): AdjudicationResult => adjudicateMismatchedAnnotations(binding, obj), + (): AdjudicationResult => adjudicateUncarryableNamespace(capabilityNamespaces, obj), + (): AdjudicationResult => adjudicateUnbindableNamespaces(capabilityNamespaces, binding), + (): AdjudicationResult => adjudicateMismatchedNamespace(binding, obj), + (): AdjudicationResult => adjudicateMismatchedNamespaceRegex(binding, obj), + (): AdjudicationResult => adjudicateMismatchedNameRegex(binding, obj), + (): AdjudicationResult => adjudicateCarriesIgnoredNamespace(ignoredNamespaces, obj), + (): AdjudicationResult => adjudicateMissingCarriableNamespace(capabilityNamespaces, obj), ]; for (const adjudicator of adjudicators) {