From 1137b846e95fc11bbadb76efd46a9bd6b3f40e0c Mon Sep 17 00:00:00 2001 From: Sam Mayer Date: Thu, 7 Nov 2024 10:26:19 -0600 Subject: [PATCH 01/46] Create full mocks of AdmissionRequest --- src/lib/filter/adjudicators.ts | 16 +++++----------- 1 file changed, 5 insertions(+), 11 deletions(-) diff --git a/src/lib/filter/adjudicators.ts b/src/lib/filter/adjudicators.ts index 5ebe17d12..2ecf86bd5 100644 --- a/src/lib/filter/adjudicators.ts +++ b/src/lib/filter/adjudicators.ts @@ -32,22 +32,16 @@ import { KubernetesObject } from "kubernetes-fluent-client"; AdmissionRequest collectors */ export const declaredOperation = pipe( - (request: AdmissionRequest): Operation => request?.operation, - defaultTo(""), -); -export const declaredGroup = pipe( - (request: AdmissionRequest): string => request?.kind?.group, + (request: AdmissionRequest) => request?.operation, defaultTo(""), ); +export const declaredGroup = pipe((request: AdmissionRequest) => request?.kind?.group, defaultTo("")); export const declaredVersion = pipe( - (request: AdmissionRequest): string | undefined => request?.kind?.version, - defaultTo(""), -); -export const declaredKind = pipe( - (request: AdmissionRequest): string => request?.kind?.kind, + (request: AdmissionRequest) => request?.kind?.version, defaultTo(""), ); -export const declaredUid = pipe((request: AdmissionRequest): string => request?.uid, defaultTo("")); +export const declaredKind = pipe((request: AdmissionRequest) => request?.kind?.kind, defaultTo("")); +export const declaredUid = pipe((request: AdmissionRequest) => request?.uid, defaultTo("")); /* KubernetesObject collectors From 95467912d01aa5aa4462afcd745025a30f3199bb Mon Sep 17 00:00:00 2001 From: Sam Mayer Date: Thu, 7 Nov 2024 10:37:41 -0600 Subject: [PATCH 02/46] Add return type to Admission Request adjudicators --- src/lib/filter/adjudicators.ts | 16 +++++++++++----- 1 file changed, 11 insertions(+), 5 deletions(-) diff --git a/src/lib/filter/adjudicators.ts b/src/lib/filter/adjudicators.ts index 2ecf86bd5..5ebe17d12 100644 --- a/src/lib/filter/adjudicators.ts +++ b/src/lib/filter/adjudicators.ts @@ -32,16 +32,22 @@ import { KubernetesObject } from "kubernetes-fluent-client"; AdmissionRequest collectors */ export const declaredOperation = pipe( - (request: AdmissionRequest) => request?.operation, + (request: AdmissionRequest): Operation => request?.operation, + defaultTo(""), +); +export const declaredGroup = pipe( + (request: AdmissionRequest): string => request?.kind?.group, defaultTo(""), ); -export const declaredGroup = pipe((request: AdmissionRequest) => request?.kind?.group, defaultTo("")); export const declaredVersion = pipe( - (request: AdmissionRequest) => request?.kind?.version, + (request: AdmissionRequest): string | undefined => request?.kind?.version, + defaultTo(""), +); +export const declaredKind = pipe( + (request: AdmissionRequest): string => request?.kind?.kind, defaultTo(""), ); -export const declaredKind = pipe((request: AdmissionRequest) => request?.kind?.kind, defaultTo("")); -export const declaredUid = pipe((request: AdmissionRequest) => request?.uid, defaultTo("")); +export const declaredUid = pipe((request: AdmissionRequest): string => request?.uid, defaultTo("")); /* KubernetesObject collectors From 5d673d575e9f4c8e0f90b592f3a8178c737fa475 Mon Sep 17 00:00:00 2001 From: Sam Mayer Date: Thu, 7 Nov 2024 11:23:14 -0600 Subject: [PATCH 03/46] Address potentially undefined input to adjudicator --- src/lib/filter/adjudicators.ts | 27 +++++++++++++++++++++------ src/lib/filter/filter.ts | 16 ++++++++-------- 2 files changed, 29 insertions(+), 14 deletions(-) diff --git a/src/lib/filter/adjudicators.ts b/src/lib/filter/adjudicators.ts index 5ebe17d12..92d9d22c0 100644 --- a/src/lib/filter/adjudicators.ts +++ b/src/lib/filter/adjudicators.ts @@ -58,19 +58,34 @@ export const carriesDeletionTimestamp = pipe( ); export const missingDeletionTimestamp = complement(carriesDeletionTimestamp); -export const carriedKind = pipe(kubernetesObject => kubernetesObject?.metadata?.kind, defaultTo("not set")); -export const carriedVersion = pipe(kubernetesObject => kubernetesObject?.metadata?.version, defaultTo("not set")); -export const carriedName = pipe(kubernetesObject => kubernetesObject?.metadata?.name, defaultTo("")); +export const carriedKind = pipe((kubernetesObject: KubernetesObject) => kubernetesObject?.kind, defaultTo("not set")); +export const carriedVersion = pipe( + (kubernetesObject: KubernetesObject) => kubernetesObject?.metadata?.resourceVersion, + defaultTo("not set"), +); +export const carriedName = pipe( + (kubernetesObject: KubernetesObject) => kubernetesObject?.metadata?.name, + defaultTo(""), +); export const carriesName = pipe(carriedName, equals(""), not); export const missingName = complement(carriesName); -export const carriedNamespace = pipe(kubernetesObject => kubernetesObject?.metadata?.namespace, defaultTo("")); +export const carriedNamespace = pipe( + (kubernetesObject: KubernetesObject) => kubernetesObject?.metadata?.namespace, + defaultTo(""), +); export const carriesNamespace = pipe(carriedNamespace, equals(""), not); -export const carriedAnnotations = pipe(kubernetesObject => kubernetesObject?.metadata?.annotations, defaultTo({})); +export const carriedAnnotations = pipe( + (kubernetesObject: KubernetesObject) => kubernetesObject?.metadata?.annotations, + defaultTo({}), +); export const carriesAnnotations = pipe(carriedAnnotations, equals({}), not); -export const carriedLabels = pipe(kubernetesObject => kubernetesObject?.metadata?.labels, defaultTo({})); +export const carriedLabels = pipe( + (kubernetesObject: KubernetesObject) => kubernetesObject?.metadata?.labels, + defaultTo({}), +); export const carriesLabels = pipe(carriedLabels, equals({}), not); /* diff --git a/src/lib/filter/filter.ts b/src/lib/filter/filter.ts index cc59c4567..378751d41 100644 --- a/src/lib/filter/filter.ts +++ b/src/lib/filter/filter.ts @@ -71,7 +71,7 @@ export function shouldSkipRequest( ) : mismatchedName(binding, obj) ? - `${prefix} Binding defines name '${definedName(binding)}' but Object carries '${carriedName(obj)}'.` : + `${prefix} Binding defines name '${definedName(binding)}' but Object carries '${carriedName(obj ?? {})}'.` : mismatchedGroup(binding, req) ? ( @@ -99,44 +99,44 @@ export function shouldSkipRequest( uncarryableNamespace(capabilityNamespaces, obj) ? ( - `${prefix} Object carries namespace '${carriedNamespace(obj)}' ` + + `${prefix} Object carries namespace '${carriedNamespace(obj ?? {})}' ` + `but namespaces allowed by Capability are '${JSON.stringify(capabilityNamespaces)}'.` ) : mismatchedNamespace(binding, obj) ? ( `${prefix} Binding defines namespaces '${JSON.stringify(definedNamespaces(binding))}' ` + - `but Object carries '${carriedNamespace(obj)}'.` + `but Object carries '${carriedNamespace(obj ?? {})}'.` ) : mismatchedLabels(binding, obj) ? ( `${prefix} Binding defines labels '${JSON.stringify(definedLabels(binding))}' ` + - `but Object carries '${JSON.stringify(carriedLabels(obj))}'.` + `but Object carries '${JSON.stringify(carriedLabels(obj ?? {}))}'.` ) : mismatchedAnnotations(binding, obj) ? ( `${prefix} Binding defines annotations '${JSON.stringify(definedAnnotations(binding))}' ` + - `but Object carries '${JSON.stringify(carriedAnnotations(obj))}'.` + `but Object carries '${JSON.stringify(carriedAnnotations(obj ?? {}))}'.` ) : mismatchedNamespaceRegex(binding, obj) ? ( `${prefix} Binding defines namespace regexes ` + `'${JSON.stringify(definedNamespaceRegexes(binding))}' ` + - `but Object carries '${carriedNamespace(obj)}'.` + `but Object carries '${carriedNamespace(obj ?? {})}'.` ) : mismatchedNameRegex(binding, obj) ? ( `${prefix} Binding defines name regex '${definedNameRegex(binding)}' ` + - `but Object carries '${carriedName(obj)}'.` + `but Object carries '${carriedName(obj ?? {})}'.` ) : carriesIgnoredNamespace(ignoredNamespaces, obj) ? ( - `${prefix} Object carries namespace '${carriedNamespace(obj)}' ` + + `${prefix} Object carries namespace '${carriedNamespace(obj ?? {})}' ` + `but ignored namespaces include '${JSON.stringify(ignoredNamespaces)}'.` ) : From 12c8c4a88eb346076f0f83838f002205bfd1be6f Mon Sep 17 00:00:00 2001 From: Sam Mayer Date: Thu, 7 Nov 2024 11:28:08 -0600 Subject: [PATCH 04/46] Add funciton return types --- src/lib/filter/adjudicators.ts | 16 ++++++++++------ 1 file changed, 10 insertions(+), 6 deletions(-) diff --git a/src/lib/filter/adjudicators.ts b/src/lib/filter/adjudicators.ts index 92d9d22c0..af335ad77 100644 --- a/src/lib/filter/adjudicators.ts +++ b/src/lib/filter/adjudicators.ts @@ -58,32 +58,36 @@ export const carriesDeletionTimestamp = pipe( ); export const missingDeletionTimestamp = complement(carriesDeletionTimestamp); -export const carriedKind = pipe((kubernetesObject: KubernetesObject) => kubernetesObject?.kind, defaultTo("not set")); +export const carriedKind = pipe( + (kubernetesObject: KubernetesObject): string | undefined => kubernetesObject?.kind, + defaultTo("not set"), +); export const carriedVersion = pipe( - (kubernetesObject: KubernetesObject) => kubernetesObject?.metadata?.resourceVersion, + (kubernetesObject: KubernetesObject): string | undefined => kubernetesObject?.metadata?.resourceVersion, defaultTo("not set"), ); export const carriedName = pipe( - (kubernetesObject: KubernetesObject) => kubernetesObject?.metadata?.name, + (kubernetesObject: KubernetesObject): string | undefined => kubernetesObject?.metadata?.name, defaultTo(""), ); export const carriesName = pipe(carriedName, equals(""), not); export const missingName = complement(carriesName); export const carriedNamespace = pipe( - (kubernetesObject: KubernetesObject) => kubernetesObject?.metadata?.namespace, + (kubernetesObject: KubernetesObject): string | undefined => kubernetesObject?.metadata?.namespace, defaultTo(""), ); export const carriesNamespace = pipe(carriedNamespace, equals(""), not); export const carriedAnnotations = pipe( - (kubernetesObject: KubernetesObject) => kubernetesObject?.metadata?.annotations, + (kubernetesObject: KubernetesObject): { [key: string]: string } | undefined => + kubernetesObject?.metadata?.annotations, defaultTo({}), ); export const carriesAnnotations = pipe(carriedAnnotations, equals({}), not); export const carriedLabels = pipe( - (kubernetesObject: KubernetesObject) => kubernetesObject?.metadata?.labels, + (kubernetesObject: KubernetesObject): { [key: string]: string } | undefined => kubernetesObject?.metadata?.labels, defaultTo({}), ); export const carriesLabels = pipe(carriedLabels, equals({}), not); From cc3e8105157b0288127524a70e8f38eae557a30e Mon Sep 17 00:00:00 2001 From: Sam Mayer Date: Thu, 7 Nov 2024 12:17:03 -0600 Subject: [PATCH 05/46] Use consistent enum property names --- src/lib/assets/rbac.test.ts | 18 ++--- src/lib/assets/webhooks.ts | 4 +- src/lib/capability.test.ts | 4 +- src/lib/capability.ts | 14 ++-- src/lib/enums.ts | 10 +-- src/lib/filter/adjudicators.test.ts | 98 ++++++++++++------------ src/lib/filter/adjudicators.ts | 38 +++++---- src/lib/filter/filter.test.ts | 48 ++++++------ src/lib/filter/shouldSkipRequest.test.ts | 2 +- src/lib/helpers.test.ts | 2 +- src/lib/watch-processor.ts | 12 +-- 11 files changed, 128 insertions(+), 122 deletions(-) diff --git a/src/lib/assets/rbac.test.ts b/src/lib/assets/rbac.test.ts index 9d0b76603..9a630b84d 100644 --- a/src/lib/assets/rbac.test.ts +++ b/src/lib/assets/rbac.test.ts @@ -22,7 +22,7 @@ const mockCapabilities: CapabilityExport[] = [ { kind: { group: "pepr.dev", version: "v1", kind: "peprstore", plural: "peprstores" }, isWatch: false, - event: Event.Create, + event: Event.CREATE, model: {} as GenericClass, filters: { name: "", @@ -57,7 +57,7 @@ const mockCapabilities: CapabilityExport[] = [ }, isWatch: false, isFinalize: false, - event: Event.Create, + event: Event.CREATE, model: {} as GenericClass, filters: { name: "", @@ -87,7 +87,7 @@ const mockCapabilities: CapabilityExport[] = [ kind: { group: "", version: "v1", kind: "namespace", plural: "namespaces" }, isWatch: true, isFinalize: false, - event: Event.Create, + event: Event.CREATE, model: {} as GenericClass, filters: { name: "", @@ -117,7 +117,7 @@ const mockCapabilities: CapabilityExport[] = [ kind: { group: "", version: "v1", kind: "configmap", plural: "configmaps" }, isWatch: true, isFinalize: false, - event: Event.Create, + event: Event.CREATE, model: {} as GenericClass, filters: { name: "", @@ -211,7 +211,7 @@ describe("RBAC generation", () => { kind: { group: "pepr.dev", version: "v1", kind: "peprstore", plural: "peprstores" }, isWatch: false, isFinalize: true, - event: Event.Create, + event: Event.CREATE, model: {} as GenericClass, filters: { name: "", @@ -265,7 +265,7 @@ describe("RBAC generation", () => { { kind: { group: "pepr.dev", version: "v1", kind: "peprlog", plural: "peprlogs" }, isWatch: false, - event: Event.Create, + event: Event.CREATE, model: {} as GenericClass, filters: { name: "", @@ -294,7 +294,7 @@ describe("RBAC generation", () => { { kind: { group: "pepr.dev", version: "v1", kind: "peprlog", plural: "peprlogs" }, isWatch: false, - event: Event.Create, + event: Event.CREATE, model: {} as GenericClass, filters: { name: "", @@ -526,7 +526,7 @@ describe("clusterRole", () => { { kind: { group: "", version: "v1", kind: "node", plural: "nodes" }, isWatch: false, - event: Event.Create, + event: Event.CREATE, model: {} as GenericClass, filters: { name: "", @@ -582,7 +582,7 @@ describe("clusterRole", () => { { kind: { group: "apps", version: "v1", kind: "deployment", plural: "deployments" }, isWatch: false, - event: Event.Create, + event: Event.CREATE, model: {} as GenericClass, filters: { name: "", diff --git a/src/lib/assets/webhooks.ts b/src/lib/assets/webhooks.ts index b1c665284..3d8d45915 100644 --- a/src/lib/assets/webhooks.ts +++ b/src/lib/assets/webhooks.ts @@ -44,8 +44,8 @@ export async function generateWebhookRules(assets: Assets, isMutateWebhook: bool const operations: string[] = []; // CreateOrUpdate is a Pepr-specific event that is translated to Create and Update - if (event === Event.CreateOrUpdate) { - operations.push(Event.Create, Event.Update); + if (event === Event.CREATE_OR_UPDATE) { + operations.push(Event.CREATE, Event.UPDATE); } else { operations.push(event); } diff --git a/src/lib/capability.test.ts b/src/lib/capability.test.ts index 40e9d965a..0e49c9eff 100644 --- a/src/lib/capability.test.ts +++ b/src/lib/capability.test.ts @@ -606,7 +606,7 @@ describe("Capability", () => { capability.When(a.Pod).IsUpdated().InNamespace("default").Validate(mockValidateCallback); expect(capability.bindings).toHaveLength(1); // Ensure binding is created - expect(capability.bindings[0].event).toBe(Event.Update); + expect(capability.bindings[0].event).toBe(Event.UPDATE); }); it("should bind a delete event", async () => { @@ -624,7 +624,7 @@ describe("Capability", () => { expect(capability.bindings).toHaveLength(1); expect(capability.bindings).toHaveLength(1); // Ensure binding is created - expect(capability.bindings[0].event).toBe(Event.Delete); + expect(capability.bindings[0].event).toBe(Event.DELETE); }); it("should throw an error if neither matchedKind nor kind is provided", () => { diff --git a/src/lib/capability.ts b/src/lib/capability.ts index c1903dfe4..61aa26e9b 100644 --- a/src/lib/capability.ts +++ b/src/lib/capability.ts @@ -193,7 +193,7 @@ export class Capability implements CapabilityExport { model, // If the kind is not specified, use the matched kind from the model kind: kind || matchedKind, - event: Event.Any, + event: Event.ANY, filters: { name: "", namespaces: [], @@ -317,7 +317,7 @@ export class Capability implements CapabilityExport { ...binding, isMutate: true, isFinalize: true, - event: Event.Any, + event: Event.ANY, mutateCallback: addFinalizer, }; bindings.push(mutateBinding); @@ -329,7 +329,7 @@ export class Capability implements CapabilityExport { ...binding, isWatch: true, isFinalize: true, - event: Event.Update, + event: Event.UPDATE, finalizeCallback: async (update: InstanceType, logger = aliasLogger) => { Log.info(`Executing finalize action with alias: ${binding.alias || "no alias provided"}`); return await finalizeCallback(update, logger); @@ -401,10 +401,10 @@ export class Capability implements CapabilityExport { } return { - IsCreatedOrUpdated: () => bindEvent(Event.CreateOrUpdate), - IsCreated: () => bindEvent(Event.Create), - IsUpdated: () => bindEvent(Event.Update), - IsDeleted: () => bindEvent(Event.Delete), + IsCreatedOrUpdated: () => bindEvent(Event.CREATE_OR_UPDATE), + IsCreated: () => bindEvent(Event.CREATE), + IsUpdated: () => bindEvent(Event.UPDATE), + IsDeleted: () => bindEvent(Event.DELETE), }; }; } diff --git a/src/lib/enums.ts b/src/lib/enums.ts index 5e34c5c94..2dee36e2d 100644 --- a/src/lib/enums.ts +++ b/src/lib/enums.ts @@ -13,9 +13,9 @@ export enum Operation { * The type of Kubernetes mutating webhook event that the action is registered for. */ export enum Event { - Create = "CREATE", - Update = "UPDATE", - Delete = "DELETE", - CreateOrUpdate = "CREATEORUPDATE", - Any = "*", + CREATE = "CREATE", + UPDATE = "UPDATE", + DELETE = "DELETE", + CREATE_OR_UPDATE = "CREATEORUPDATE", + ANY = "*", } diff --git a/src/lib/filter/adjudicators.test.ts b/src/lib/filter/adjudicators.test.ts index a34553c14..77ad8e200 100644 --- a/src/lib/filter/adjudicators.test.ts +++ b/src/lib/filter/adjudicators.test.ts @@ -377,32 +377,32 @@ describe("operationMatchesEvent", () => { //[ Operation, Event, result ] it.each([ ["", "", true], - ["", Event.Create, false], + ["", Event.CREATE, false], [Operation.CREATE, "", false], - [Operation.CREATE, Event.Create, true], - [Operation.CREATE, Event.Update, false], - [Operation.CREATE, Event.Delete, false], - [Operation.CREATE, Event.CreateOrUpdate, true], - [Operation.CREATE, Event.Any, true], - - [Operation.UPDATE, Event.Create, false], - [Operation.UPDATE, Event.Update, true], - [Operation.UPDATE, Event.Delete, false], - [Operation.UPDATE, Event.CreateOrUpdate, true], - [Operation.UPDATE, Event.Any, true], - - [Operation.DELETE, Event.Create, false], - [Operation.DELETE, Event.Update, false], - [Operation.DELETE, Event.Delete, true], - [Operation.DELETE, Event.CreateOrUpdate, false], - [Operation.DELETE, Event.Any, true], - - [Operation.CONNECT, Event.Create, false], - [Operation.CONNECT, Event.Update, false], - [Operation.CONNECT, Event.Delete, false], - [Operation.CONNECT, Event.CreateOrUpdate, false], - [Operation.CONNECT, Event.Any, true], + [Operation.CREATE, Event.CREATE, true], + [Operation.CREATE, Event.UPDATE, false], + [Operation.CREATE, Event.DELETE, false], + [Operation.CREATE, Event.CREATE_OR_UPDATE, true], + [Operation.CREATE, Event.ANY, true], + + [Operation.UPDATE, Event.CREATE, false], + [Operation.UPDATE, Event.UPDATE, true], + [Operation.UPDATE, Event.DELETE, false], + [Operation.UPDATE, Event.CREATE_OR_UPDATE, true], + [Operation.UPDATE, Event.ANY, true], + + [Operation.DELETE, Event.CREATE, false], + [Operation.DELETE, Event.UPDATE, false], + [Operation.DELETE, Event.DELETE, true], + [Operation.DELETE, Event.CREATE_OR_UPDATE, false], + [Operation.DELETE, Event.ANY, true], + + [Operation.CONNECT, Event.CREATE, false], + [Operation.CONNECT, Event.UPDATE, false], + [Operation.CONNECT, Event.DELETE, false], + [Operation.CONNECT, Event.CREATE_OR_UPDATE, false], + [Operation.CONNECT, Event.ANY, true], ])("given operation %s and event %s, returns %s", (op, evt, expected) => { const result = sut.operationMatchesEvent(op, evt); @@ -415,31 +415,31 @@ describe("mismatchedEvent", () => { it.each([ [{}, {}, false], [{}, { operation: Operation.CREATE }, true], - [{ event: Event.Create }, {}, true], - - [{ event: Event.Create }, { operation: Operation.CREATE }, false], - [{ event: Event.Update }, { operation: Operation.CREATE }, true], - [{ event: Event.Delete }, { operation: Operation.CREATE }, true], - [{ event: Event.CreateOrUpdate }, { operation: Operation.CREATE }, false], - [{ event: Event.Any }, { operation: Operation.CREATE }, false], - - [{ event: Event.Create }, { operation: Operation.UPDATE }, true], - [{ event: Event.Update }, { operation: Operation.UPDATE }, false], - [{ event: Event.Delete }, { operation: Operation.UPDATE }, true], - [{ event: Event.CreateOrUpdate }, { operation: Operation.UPDATE }, false], - [{ event: Event.Any }, { operation: Operation.UPDATE }, false], - - [{ event: Event.Create }, { operation: Operation.DELETE }, true], - [{ event: Event.Update }, { operation: Operation.DELETE }, true], - [{ event: Event.Delete }, { operation: Operation.DELETE }, false], - [{ event: Event.CreateOrUpdate }, { operation: Operation.DELETE }, true], - [{ event: Event.Any }, { operation: Operation.DELETE }, false], - - [{ event: Event.Create }, { operation: Operation.CONNECT }, true], - [{ event: Event.Update }, { operation: Operation.CONNECT }, true], - [{ event: Event.Delete }, { operation: Operation.CONNECT }, true], - [{ event: Event.CreateOrUpdate }, { operation: Operation.CONNECT }, true], - [{ event: Event.Any }, { operation: Operation.CONNECT }, false], + [{ event: Event.CREATE }, {}, true], + + [{ event: Event.CREATE }, { operation: Operation.CREATE }, false], + [{ event: Event.UPDATE }, { operation: Operation.CREATE }, true], + [{ event: Event.DELETE }, { operation: Operation.CREATE }, true], + [{ event: Event.CREATE_OR_UPDATE }, { operation: Operation.CREATE }, false], + [{ event: Event.ANY }, { operation: Operation.CREATE }, false], + + [{ event: Event.CREATE }, { operation: Operation.UPDATE }, true], + [{ event: Event.UPDATE }, { operation: Operation.UPDATE }, false], + [{ event: Event.DELETE }, { operation: Operation.UPDATE }, true], + [{ event: Event.CREATE_OR_UPDATE }, { operation: Operation.UPDATE }, false], + [{ event: Event.ANY }, { operation: Operation.UPDATE }, false], + + [{ event: Event.CREATE }, { operation: Operation.DELETE }, true], + [{ event: Event.UPDATE }, { operation: Operation.DELETE }, true], + [{ event: Event.DELETE }, { operation: Operation.DELETE }, false], + [{ event: Event.CREATE_OR_UPDATE }, { operation: Operation.DELETE }, true], + [{ event: Event.ANY }, { operation: Operation.DELETE }, false], + + [{ event: Event.CREATE }, { operation: Operation.CONNECT }, true], + [{ event: Event.UPDATE }, { operation: Operation.CONNECT }, true], + [{ event: Event.DELETE }, { operation: Operation.CONNECT }, true], + [{ event: Event.CREATE_OR_UPDATE }, { operation: Operation.CONNECT }, true], + [{ event: Event.ANY }, { operation: Operation.CONNECT }, false], ])("given binding %j and admission request %j, returns %s", (bnd, req, expected) => { const binding = bnd as DeepPartial; const request = req as DeepPartial; diff --git a/src/lib/filter/adjudicators.ts b/src/lib/filter/adjudicators.ts index af335ad77..20c0b3a3a 100644 --- a/src/lib/filter/adjudicators.ts +++ b/src/lib/filter/adjudicators.ts @@ -2,7 +2,7 @@ // SPDX-FileCopyrightText: 2023-Present The Pepr Authors import { Event, Operation } from "../enums"; -import { AdmissionRequest } from "../../lib/types"; +import { AdmissionRequest, Binding } from "../../lib/types"; import { __, allPass, @@ -96,41 +96,47 @@ export const carriesLabels = pipe(carriedLabels, equals({}), not); Binding collectors */ -export const definesDeletionTimestamp = pipe(binding => binding?.filters?.deletionTimestamp, defaultTo(false)); +export const definesDeletionTimestamp = pipe( + (binding: Binding): boolean => binding?.filters?.deletionTimestamp, + defaultTo(false), +); export const ignoresDeletionTimestamp = complement(definesDeletionTimestamp); -export const definedName = pipe(binding => binding?.filters?.name, defaultTo("")); +export const definedName = pipe((binding: Binding): string => binding?.filters?.name, defaultTo("")); export const definesName = pipe(definedName, equals(""), not); export const ignoresName = complement(definesName); -export const definedNameRegex = pipe(binding => binding?.filters?.regexName, defaultTo("")); +export const definedNameRegex = pipe((binding: Binding): string => binding?.filters?.regexName, defaultTo("")); export const definesNameRegex = pipe(definedNameRegex, equals(""), not); -export const definedNamespaces = pipe(binding => binding?.filters?.namespaces, defaultTo([])); +export const definedNamespaces = pipe((binding: Binding): string[] => binding?.filters?.namespaces, defaultTo([])); export const definesNamespaces = pipe(definedNamespaces, equals([]), not); -export const definedNamespaceRegexes = pipe(binding => binding?.filters?.regexNamespaces, defaultTo([])); +export const definedNamespaceRegexes = pipe( + (binding: Binding): string[] => binding?.filters?.regexNamespaces, + defaultTo([]), +); export const definesNamespaceRegexes = pipe(definedNamespaceRegexes, equals([]), not); -export const definedAnnotations = pipe(binding => binding?.filters?.annotations, defaultTo({})); +export const definedAnnotations = pipe((binding: Binding) => binding?.filters?.annotations, defaultTo({})); export const definesAnnotations = pipe(definedAnnotations, equals({}), not); -export const definedLabels = pipe(binding => binding?.filters?.labels, defaultTo({})); +export const definedLabels = pipe((binding: Binding) => binding?.filters?.labels, defaultTo({})); export const definesLabels = pipe(definedLabels, equals({}), not); -export const definedEvent = pipe(binding => binding?.event, defaultTo("")); -export const definesDelete = pipe(definedEvent, equals(Operation.DELETE)); +export const definedEvent = pipe((binding: Binding): Event => binding?.event, defaultTo("")); +export const definesDelete = pipe(definedEvent, equals(Event.DELETE)); -export const definedGroup = pipe(binding => binding?.kind?.group, defaultTo("")); +export const definedGroup = pipe((binding: Binding): string => binding?.kind?.group, defaultTo("")); export const definesGroup = pipe(definedGroup, equals(""), not); -export const definedVersion = pipe(binding => binding?.kind?.version, defaultTo("")); +export const definedVersion = pipe((binding: Binding): string | undefined => binding?.kind?.version, defaultTo("")); export const definesVersion = pipe(definedVersion, equals(""), not); -export const definedKind = pipe(binding => binding?.kind?.kind, defaultTo("")); +export const definedKind = pipe((binding: Binding): string => binding?.kind?.kind, defaultTo("")); export const definesKind = pipe(definedKind, equals(""), not); -export const definedCategory = pipe(binding => { +export const definedCategory = pipe((binding: Binding) => { // prettier-ignore return ( binding.isFinalize ? "Finalize" : @@ -141,7 +147,7 @@ export const definedCategory = pipe(binding => { ); }); -export const definedCallback = pipe(binding => { +export const definedCallback = pipe((binding: Binding) => { // prettier-ignore return ( binding.isFinalize ? binding.finalizeCallback : @@ -264,7 +270,7 @@ export const unbindableNamespaces = allPass([ export const misboundDeleteWithDeletionTimestamp = allPass([definesDelete, definesDeletionTimestamp]); export const operationMatchesEvent = anyPass([ - pipe(nthArg(1), equals(Event.Any)), + pipe(nthArg(1), equals(Event.ANY)), pipe((operation, event) => operation === event), pipe((operation, event) => (operation ? event.includes(operation) : false)), ]); diff --git a/src/lib/filter/filter.test.ts b/src/lib/filter/filter.test.ts index cf4ecdfd4..a008945f3 100644 --- a/src/lib/filter/filter.test.ts +++ b/src/lib/filter/filter.test.ts @@ -109,7 +109,7 @@ describe("Property-Based Testing shouldSkipRequest", () => { test("create: should reject when regex name does not match", () => { const binding = { model: kind.Pod, - event: Event.Any, + event: Event.ANY, kind: podKind, filters: { name: "", @@ -131,7 +131,7 @@ test("create: should reject when regex name does not match", () => { test("create: should not reject when regex name does match", () => { const binding = { model: kind.Pod, - event: Event.Any, + event: Event.ANY, kind: podKind, filters: { name: "", @@ -151,7 +151,7 @@ test("create: should not reject when regex name does match", () => { test("delete: should reject when regex name does not match", () => { const binding = { model: kind.Pod, - event: Event.Any, + event: Event.ANY, kind: podKind, filters: { name: "", @@ -173,7 +173,7 @@ test("delete: should reject when regex name does not match", () => { test("delete: should not reject when regex name does match", () => { const binding = { model: kind.Pod, - event: Event.Any, + event: Event.ANY, kind: podKind, filters: { name: "", @@ -193,7 +193,7 @@ test("delete: should not reject when regex name does match", () => { test("create: should not reject when regex namespace does match", () => { const binding = { model: kind.Pod, - event: Event.Any, + event: Event.ANY, kind: podKind, filters: { name: "", @@ -213,7 +213,7 @@ test("create: should not reject when regex namespace does match", () => { test("create: should reject when regex namespace does not match", () => { const binding = { model: kind.Pod, - event: Event.Any, + event: Event.ANY, kind: podKind, filters: { name: "", @@ -235,7 +235,7 @@ test("create: should reject when regex namespace does not match", () => { test("delete: should reject when regex namespace does not match", () => { const binding = { model: kind.Pod, - event: Event.Any, + event: Event.ANY, kind: podKind, filters: { name: "", @@ -257,7 +257,7 @@ test("delete: should reject when regex namespace does not match", () => { test("delete: should not reject when regex namespace does match", () => { const binding = { model: kind.Pod, - event: Event.Any, + event: Event.ANY, kind: podKind, filters: { name: "", @@ -277,7 +277,7 @@ test("delete: should not reject when regex namespace does match", () => { test("delete: should reject when name does not match", () => { const binding = { model: kind.Pod, - event: Event.Any, + event: Event.ANY, kind: podKind, filters: { name: "bleh", @@ -299,7 +299,7 @@ test("delete: should reject when name does not match", () => { test("should reject when kind does not match", () => { const binding = { model: kind.Pod, - event: Event.Any, + event: Event.ANY, kind: { group: "", version: "v1", @@ -326,7 +326,7 @@ test("should reject when kind does not match", () => { test("should reject when group does not match", () => { const binding = { model: kind.Pod, - event: Event.Any, + event: Event.ANY, kind: { group: "Nope", version: "v1", @@ -353,7 +353,7 @@ test("should reject when group does not match", () => { test("should reject when version does not match", () => { const binding = { model: kind.Pod, - event: Event.Any, + event: Event.ANY, kind: { group: "", version: "Nope", @@ -380,7 +380,7 @@ test("should reject when version does not match", () => { test("should allow when group, version, and kind match", () => { const binding = { model: kind.Pod, - event: Event.Any, + event: Event.ANY, kind: podKind, filters: { name: "", @@ -401,7 +401,7 @@ test("should allow when group, version, and kind match", () => { test("should allow when kind match and others are empty", () => { const binding = { model: kind.Pod, - event: Event.Any, + event: Event.ANY, kind: { group: "", version: "", @@ -426,7 +426,7 @@ test("should allow when kind match and others are empty", () => { test("should reject when the capability namespace does not match", () => { const binding = { model: kind.Pod, - event: Event.Any, + event: Event.ANY, kind: podKind, filters: { name: "", @@ -449,7 +449,7 @@ test("should reject when the capability namespace does not match", () => { test("should reject when namespace does not match", () => { const binding = { model: kind.Pod, - event: Event.Any, + event: Event.ANY, kind: podKind, filters: { name: "", @@ -472,7 +472,7 @@ test("should reject when namespace does not match", () => { test("should allow when namespace is match", () => { const binding = { model: kind.Pod, - event: Event.Any, + event: Event.ANY, kind: podKind, filters: { name: "", @@ -493,7 +493,7 @@ test("should allow when namespace is match", () => { test("should reject when label does not match", () => { const binding = { model: kind.Pod, - event: Event.Any, + event: Event.ANY, kind: podKind, filters: { name: "", @@ -518,7 +518,7 @@ test("should reject when label does not match", () => { test("should allow when label is match", () => { const binding = { model: kind.Pod, - event: Event.Any, + event: Event.ANY, kind: podKind, filters: { name: "", @@ -549,7 +549,7 @@ test("should allow when label is match", () => { test("should reject when annotation does not match", () => { const binding = { model: kind.Pod, - event: Event.Any, + event: Event.ANY, kind: podKind, filters: { name: "", @@ -574,7 +574,7 @@ test("should reject when annotation does not match", () => { test("should allow when annotation is match", () => { const binding = { model: kind.Pod, - event: Event.Any, + event: Event.ANY, kind: podKind, filters: { name: "", @@ -605,7 +605,7 @@ test("should allow when annotation is match", () => { test("should use `oldObject` when the operation is `DELETE`", () => { const binding = { model: kind.Pod, - event: Event.Delete, + event: Event.DELETE, kind: podKind, filters: { name: "", @@ -629,7 +629,7 @@ test("should use `oldObject` when the operation is `DELETE`", () => { test("should allow when deletionTimestamp is present on pod", () => { const binding = { model: kind.Pod, - event: Event.Any, + event: Event.ANY, kind: podKind, filters: { name: "", @@ -661,7 +661,7 @@ test("should allow when deletionTimestamp is present on pod", () => { test("should reject when deletionTimestamp is not present on pod", () => { const binding = { model: kind.Pod, - event: Event.Any, + event: Event.ANY, kind: podKind, filters: { name: "", diff --git a/src/lib/filter/shouldSkipRequest.test.ts b/src/lib/filter/shouldSkipRequest.test.ts index 714a58025..e3e63482a 100644 --- a/src/lib/filter/shouldSkipRequest.test.ts +++ b/src/lib/filter/shouldSkipRequest.test.ts @@ -31,7 +31,7 @@ const defaultFilters = { }; const defaultBinding = { callback, - event: Event.Any, + event: Event.ANY, filters: defaultFilters, kind: podKind, model: kind.Pod, diff --git a/src/lib/helpers.test.ts b/src/lib/helpers.test.ts index d2a49fee4..a960adb2c 100644 --- a/src/lib/helpers.test.ts +++ b/src/lib/helpers.test.ts @@ -1331,7 +1331,7 @@ describe("filterNoMatchReason", () => { test("returns capability namespace error when object is not in capability namespaces", () => { const binding = { model: kind.Pod, - event: Event.Any, + event: Event.ANY, kind: { group: "", version: "v1", diff --git a/src/lib/watch-processor.ts b/src/lib/watch-processor.ts index 9712595fe..884b9b4fe 100644 --- a/src/lib/watch-processor.ts +++ b/src/lib/watch-processor.ts @@ -62,11 +62,11 @@ const watchCfg: WatchCfg = { // Map the event to the watch phase const eventToPhaseMap = { - [Event.Create]: [WatchPhase.Added], - [Event.Update]: [WatchPhase.Modified], - [Event.CreateOrUpdate]: [WatchPhase.Added, WatchPhase.Modified], - [Event.Delete]: [WatchPhase.Deleted], - [Event.Any]: [WatchPhase.Added, WatchPhase.Modified, WatchPhase.Deleted], + [Event.CREATE]: [WatchPhase.Added], + [Event.UPDATE]: [WatchPhase.Modified], + [Event.CREATE_OR_UPDATE]: [WatchPhase.Added, WatchPhase.Modified], + [Event.DELETE]: [WatchPhase.Deleted], + [Event.ANY]: [WatchPhase.Added, WatchPhase.Modified, WatchPhase.Deleted], }; /** @@ -90,7 +90,7 @@ export function setupWatch(capabilities: Capability[], ignoredNamespaces?: strin */ async function runBinding(binding: Binding, capabilityNamespaces: string[], ignoredNamespaces?: string[]) { // Get the phases to match, fallback to any - const phaseMatch: WatchPhase[] = eventToPhaseMap[binding.event] || eventToPhaseMap[Event.Any]; + const phaseMatch: WatchPhase[] = eventToPhaseMap[binding.event] || eventToPhaseMap[Event.ANY]; // The watch callback is run when an object is received or dequeued Log.debug({ watchCfg }, "Effective WatchConfig"); From d94543d3cee29409c21da27618f710904ad14b65 Mon Sep 17 00:00:00 2001 From: Sam Mayer Date: Thu, 7 Nov 2024 15:46:45 -0600 Subject: [PATCH 06/46] Reworking types to remove typescript warnings so that I can actually run tests --- src/lib/filter/adjudicators.test.ts | 35 ++------------------ src/lib/filter/adjudicators.ts | 50 +++++++++++++++++++++-------- src/lib/filter/filter.ts | 1 + src/lib/helpers.ts | 1 + src/lib/types.ts | 21 ++++++------ 5 files changed, 52 insertions(+), 56 deletions(-) diff --git a/src/lib/filter/adjudicators.test.ts b/src/lib/filter/adjudicators.test.ts index 77ad8e200..f64b73450 100644 --- a/src/lib/filter/adjudicators.test.ts +++ b/src/lib/filter/adjudicators.test.ts @@ -1,3 +1,4 @@ +/* eslint-disable max-statements */ // SPDX-License-Identifier: Apache-2.0 // SPDX-FileCopyrightText: 2023-Present The Pepr Authors @@ -15,41 +16,9 @@ describe("carriesDeletionTimestamp", () => { [{ metadata: { deletionTimestamp: null } }, false], [{ metadata: { deletionTimestamp: new Date() } }, true], ])("given %j, returns %s", (given, expected) => { + // eslint-disable-next-line @typescript-eslint/no-unused-vars const ko = given as DeepPartial; - const result = sut.carriesDeletionTimestamp(ko); - - expect(result).toBe(expected); - }); -}); - -describe("missingDeletionTimestamp", () => { - //[ KubernetesObject, result ] - it.each([ - [{}, true], - [{ metadata: {} }, true], - [{ metadata: { deletionTimestamp: null } }, true], - [{ metadata: { deletionTimestamp: new Date() } }, false], - ])("given %j, returns %s", (given, expected) => { - const ko = given as DeepPartial; - - const result = sut.missingDeletionTimestamp(ko); - - expect(result).toBe(expected); - }); -}); - -describe("mismatchedDeletionTimestamp", () => { - //[ Binding, KubernetesObject, result ] - it.each([ - [{}, {}, false], - [{}, { metadata: { deletionTimestamp: new Date() } }, false], - [{ filters: { deletionTimestamp: true } }, {}, true], - [{ filters: { deletionTimestamp: true } }, { metadata: { deletionTimestamp: new Date() } }, false], - ])("given binding %j and object %j, returns %s", (bnd, obj, expected) => { - const binding = bnd as DeepPartial; - const object = obj as DeepPartial; - const result = sut.mismatchedDeletionTimestamp(binding, object); expect(result).toBe(expected); diff --git a/src/lib/filter/adjudicators.ts b/src/lib/filter/adjudicators.ts index 20c0b3a3a..6768f138b 100644 --- a/src/lib/filter/adjudicators.ts +++ b/src/lib/filter/adjudicators.ts @@ -97,46 +97,64 @@ export const carriesLabels = pipe(carriedLabels, equals({}), not); */ export const definesDeletionTimestamp = pipe( - (binding: Binding): boolean => binding?.filters?.deletionTimestamp, + (binding: Binding): boolean | undefined => binding?.filters?.deletionTimestamp ?? undefined, defaultTo(false), ); export const ignoresDeletionTimestamp = complement(definesDeletionTimestamp); -export const definedName = pipe((binding: Binding): string => binding?.filters?.name, defaultTo("")); +export const definedName = pipe( + (binding: Partial): string | undefined => binding?.filters?.name, + defaultTo(""), +); export const definesName = pipe(definedName, equals(""), not); export const ignoresName = complement(definesName); -export const definedNameRegex = pipe((binding: Binding): string => binding?.filters?.regexName, defaultTo("")); +export const definedNameRegex = pipe( + (binding: Partial): string | undefined => binding?.filters?.regexName, + defaultTo(""), +); export const definesNameRegex = pipe(definedNameRegex, equals(""), not); -export const definedNamespaces = pipe((binding: Binding): string[] => binding?.filters?.namespaces, defaultTo([])); +export const definedNamespaces = pipe( + (binding: Partial): string[] | undefined => binding?.filters?.namespaces, + defaultTo([]), +); export const definesNamespaces = pipe(definedNamespaces, equals([]), not); export const definedNamespaceRegexes = pipe( - (binding: Binding): string[] => binding?.filters?.regexNamespaces, + (binding: Partial): string[] | undefined => binding?.filters?.regexNamespaces, defaultTo([]), ); export const definesNamespaceRegexes = pipe(definedNamespaceRegexes, equals([]), not); -export const definedAnnotations = pipe((binding: Binding) => binding?.filters?.annotations, defaultTo({})); +export const definedAnnotations = pipe((binding: Partial) => binding?.filters?.annotations, defaultTo({})); export const definesAnnotations = pipe(definedAnnotations, equals({}), not); -export const definedLabels = pipe((binding: Binding) => binding?.filters?.labels, defaultTo({})); +export const definedLabels = pipe((binding: Partial) => binding?.filters?.labels, defaultTo({})); export const definesLabels = pipe(definedLabels, equals({}), not); -export const definedEvent = pipe((binding: Binding): Event => binding?.event, defaultTo("")); +export const definedEvent = pipe((binding: Partial): Event => { + if (binding.event) { + return binding.event; + } else { + throw TypeError("binding.event was undefined"); + } +}, defaultTo("")); export const definesDelete = pipe(definedEvent, equals(Event.DELETE)); -export const definedGroup = pipe((binding: Binding): string => binding?.kind?.group, defaultTo("")); +export const definedGroup = pipe((binding: Partial): string => binding?.kind?.group, defaultTo("")); export const definesGroup = pipe(definedGroup, equals(""), not); -export const definedVersion = pipe((binding: Binding): string | undefined => binding?.kind?.version, defaultTo("")); +export const definedVersion = pipe( + (binding: Partial): string | undefined => binding?.kind?.version, + defaultTo(""), +); export const definesVersion = pipe(definedVersion, equals(""), not); -export const definedKind = pipe((binding: Binding): string => binding?.kind?.kind, defaultTo("")); +export const definedKind = pipe((binding: Partial): string => binding?.kind?.kind, defaultTo("")); export const definesKind = pipe(definedKind, equals(""), not); -export const definedCategory = pipe((binding: Binding) => { +export const definedCategory = pipe((binding: Partial) => { // prettier-ignore return ( binding.isFinalize ? "Finalize" : @@ -147,7 +165,7 @@ export const definedCategory = pipe((binding: Binding) => { ); }); -export const definedCallback = pipe((binding: Binding) => { +export const definedCallback = pipe((binding: Partial) => { // prettier-ignore return ( binding.isFinalize ? binding.finalizeCallback : @@ -185,7 +203,11 @@ export const misboundNamespace = allPass([bindsToNamespace, definesNamespaces]); export const mismatchedNamespace = allPass([ pipe(nthArg(0), definesNamespaces), - pipe((binding, kubernetesObject) => definedNamespaces(binding).includes(carriedNamespace(kubernetesObject)), not), + pipe( + (binding: Binding, kubernetesObject: KubernetesObject) => + definedNamespaces(binding).includes(carriedNamespace(kubernetesObject)), + not, + ), ]); export const mismatchedNamespaceRegex = allPass([ diff --git a/src/lib/filter/filter.ts b/src/lib/filter/filter.ts index 378751d41..3ed7dad48 100644 --- a/src/lib/filter/filter.ts +++ b/src/lib/filter/filter.ts @@ -1,3 +1,4 @@ +/* eslint-disable complexity */ // SPDX-License-Identifier: Apache-2.0 // SPDX-FileCopyrightText: 2023-Present The Pepr Authors diff --git a/src/lib/helpers.ts b/src/lib/helpers.ts index f401b87a1..e7bd2f24d 100644 --- a/src/lib/helpers.ts +++ b/src/lib/helpers.ts @@ -1,3 +1,4 @@ +/* eslint-disable complexity */ // SPDX-License-Identifier: Apache-2.0 // SPDX-FileCopyrightText: 2023-Present The Pepr Authors diff --git a/src/lib/types.ts b/src/lib/types.ts index 6a00c07be..7ce5a0a8a 100644 --- a/src/lib/types.ts +++ b/src/lib/types.ts @@ -70,6 +70,17 @@ export interface RegExpFilter { obj: RegExp; source: string; } + +export type Filters = { + annotations: Record; + deletionTimestamp: boolean; + labels: Record; + name: string; + namespaces: string[]; + regexName: string; + regexNamespaces: string[]; +}; + export type Binding = { event: Event; isMutate?: boolean; @@ -79,15 +90,7 @@ export type Binding = { isFinalize?: boolean; readonly model: GenericClass; readonly kind: GroupVersionKind; - readonly filters: { - name: string; - regexName: string; - namespaces: string[]; - regexNamespaces: string[]; - labels: Record; - annotations: Record; - deletionTimestamp: boolean; - }; + readonly filters: Filters; alias?: string; readonly mutateCallback?: MutateAction>; readonly validateCallback?: ValidateAction>; From a169536f7a8926c39deb04171f320f60f3ae1677 Mon Sep 17 00:00:00 2001 From: Sam Mayer Date: Mon, 18 Nov 2024 12:04:47 -0600 Subject: [PATCH 07/46] Work on resolving typing issues by using actual objects instead of partials --- src/lib/filter/adjudicators.test.ts | 2 +- src/lib/filter/adjudicators.ts | 20 +- .../adjudicators/bindingAdjudicators.test.ts | 520 ++++++++++++------ src/lib/filter/shouldSkipRequest.test.ts | 8 +- src/lib/helpers.ts | 38 +- 5 files changed, 386 insertions(+), 202 deletions(-) diff --git a/src/lib/filter/adjudicators.test.ts b/src/lib/filter/adjudicators.test.ts index f64b73450..6caa998ea 100644 --- a/src/lib/filter/adjudicators.test.ts +++ b/src/lib/filter/adjudicators.test.ts @@ -19,7 +19,7 @@ describe("carriesDeletionTimestamp", () => { // eslint-disable-next-line @typescript-eslint/no-unused-vars const ko = given as DeepPartial; - const result = sut.mismatchedDeletionTimestamp(binding, object); + const result = sut.mismatchedDeletionTimestamp(ko); expect(result).toBe(expected); }); diff --git a/src/lib/filter/adjudicators.ts b/src/lib/filter/adjudicators.ts index 6768f138b..3932400fd 100644 --- a/src/lib/filter/adjudicators.ts +++ b/src/lib/filter/adjudicators.ts @@ -115,16 +115,10 @@ export const definedNameRegex = pipe( ); export const definesNameRegex = pipe(definedNameRegex, equals(""), not); -export const definedNamespaces = pipe( - (binding: Partial): string[] | undefined => binding?.filters?.namespaces, - defaultTo([]), -); +export const definedNamespaces = pipe(binding => binding?.filters?.namespaces, defaultTo([])); export const definesNamespaces = pipe(definedNamespaces, equals([]), not); -export const definedNamespaceRegexes = pipe( - (binding: Partial): string[] | undefined => binding?.filters?.regexNamespaces, - defaultTo([]), -); +export const definedNamespaceRegexes = pipe(binding => binding?.filters?.regexNamespaces, defaultTo([])); export const definesNamespaceRegexes = pipe(definedNamespaceRegexes, equals([]), not); export const definedAnnotations = pipe((binding: Partial) => binding?.filters?.annotations, defaultTo({})); @@ -142,7 +136,7 @@ export const definedEvent = pipe((binding: Partial): Event => { }, defaultTo("")); export const definesDelete = pipe(definedEvent, equals(Event.DELETE)); -export const definedGroup = pipe((binding: Partial): string => binding?.kind?.group, defaultTo("")); +export const definedGroup = pipe((binding): string => binding?.kind?.group, defaultTo("")); export const definesGroup = pipe(definedGroup, equals(""), not); export const definedVersion = pipe( @@ -151,7 +145,7 @@ export const definedVersion = pipe( ); export const definesVersion = pipe(definedVersion, equals(""), not); -export const definedKind = pipe((binding: Partial): string => binding?.kind?.kind, defaultTo("")); +export const definedKind = pipe((binding): string => binding?.kind?.kind, defaultTo("")); export const definesKind = pipe(definedKind, equals(""), not); export const definedCategory = pipe((binding: Partial) => { @@ -203,11 +197,7 @@ export const misboundNamespace = allPass([bindsToNamespace, definesNamespaces]); export const mismatchedNamespace = allPass([ pipe(nthArg(0), definesNamespaces), - pipe( - (binding: Binding, kubernetesObject: KubernetesObject) => - definedNamespaces(binding).includes(carriedNamespace(kubernetesObject)), - not, - ), + pipe((binding, kubernetesObject) => definedNamespaces(binding).includes(carriedNamespace(kubernetesObject)), not), ]); export const mismatchedNamespaceRegex = allPass([ diff --git a/src/lib/filter/adjudicators/bindingAdjudicators.test.ts b/src/lib/filter/adjudicators/bindingAdjudicators.test.ts index ed9eecb96..1776cb88e 100644 --- a/src/lib/filter/adjudicators/bindingAdjudicators.test.ts +++ b/src/lib/filter/adjudicators/bindingAdjudicators.test.ts @@ -3,20 +3,47 @@ import { expect, describe, it } from "@jest/globals"; import * as sut from "../adjudicators"; -import { KubernetesObject } from "kubernetes-fluent-client"; -import { Binding, DeepPartial } from "../../types"; +import { kind, KubernetesObject } from "kubernetes-fluent-client"; +import { Binding, DeepPartial, ValidateActionResponse } from "../../types"; import { Event } from "../../enums"; +const defaultFilters = { + annotations: {}, + deletionTimestamp: false, + labels: {}, + name: "", + namespaces: [], + regexName: "^default$", + regexNamespaces: [], +}; +const defaultBinding: Binding = { + event: Event.ANY, + filters: defaultFilters, + kind: { kind: "some-kind", group: "some-group" }, + model: kind.Pod, + isFinalize: false, + isMutate: false, + isQueue: false, + isValidate: false, + isWatch: false, +}; + describe("definesDeletionTimestamp", () => { //[ Binding, result ] it.each([ - [{}, false], - [{ filters: {} }, false], - [{ filters: { deletionTimestamp: null } }, false], + // [{}, false], + // [{ filters: {} }, false], + // [{ filters: { deletionTimestamp: null } }, false], [{ filters: { deletionTimestamp: false } }, false], [{ filters: { deletionTimestamp: true } }, true], ])("given %j, returns %s", (given, expected) => { - const binding = given as DeepPartial; + const binding = { + ...defaultBinding, + filters: { + ...defaultFilters, + deletionTimestamp: given.filters.deletionTimestamp, + }, + }; const result = sut.definesDeletionTimestamp(binding); @@ -27,13 +54,19 @@ describe("definesDeletionTimestamp", () => { describe("ignoresDeletionTimestamp", () => { //[ Binding, result ] it.each([ - [{}, true], - [{ filters: {} }, true], - [{ filters: { deletionTimestamp: null } }, true], + // [{}, true], + // [{ filters: {} }, true], + // [{ filters: { deletionTimestamp: null } }, true], [{ filters: { deletionTimestamp: false } }, true], [{ filters: { deletionTimestamp: true } }, false], ])("given %j, returns %s", (given, expected) => { - const binding = given as DeepPartial; + const binding = { + ...defaultBinding, + filters: { + ...defaultFilters, + deletionTimestamp: given.filters.deletionTimestamp, + }, + }; const result = sut.ignoresDeletionTimestamp(binding); @@ -44,12 +77,18 @@ describe("ignoresDeletionTimestamp", () => { describe("definedName", () => { //[ Binding, result ] it.each([ - [{}, ""], - [{ filters: {} }, ""], - [{ filters: { name: null } }, ""], + // [{}, ""], + // [{ filters: {} }, ""], + // [{ filters: { name: null } }, ""], [{ filters: { name: "name" } }, "name"], ])("given %j, returns '%s'", (given, expected) => { - const binding = given as DeepPartial; + const binding = { + ...defaultBinding, + filters: { + ...defaultFilters, + name: given.filters.name, + }, + }; const result = sut.definedName(binding); @@ -60,12 +99,18 @@ describe("definedName", () => { describe("definesName", () => { //[ Binding, result ] it.each([ - [{}, false], - [{ filters: {} }, false], - [{ filters: { name: null } }, false], + // [{}, false], + // [{ filters: {} }, false], + // [{ filters: { name: null } }, false], [{ filters: { name: "name" } }, true], ])("given %j, returns %s", (given, expected) => { - const binding = given as DeepPartial; + const binding = { + ...defaultBinding, + filters: { + ...defaultFilters, + name: given.filters.name, + }, + }; const result = sut.definesName(binding); @@ -76,12 +121,18 @@ describe("definesName", () => { describe("ignoresName", () => { //[ Binding, result ] it.each([ - [{}, true], - [{ filters: {} }, true], - [{ filters: { name: null } }, true], + // [{}, true], + // [{ filters: {} }, true], + // [{ filters: { name: null } }, true], [{ filters: { name: "name" } }, false], ])("given %j, returns %s", (given, expected) => { - const binding = given as DeepPartial; + const binding = { + ...defaultBinding, + filters: { + ...defaultFilters, + name: given.filters.name, + }, + }; const result = sut.ignoresName(binding); @@ -92,12 +143,18 @@ describe("ignoresName", () => { describe("definedNameRegex", () => { //[ Binding, result ] it.each([ - [{}, ""], - [{ filters: {} }, ""], - [{ filters: { regexName: null } }, ""], - [{ filters: { regexName: "n.me" } }, "n.me"], + // [{}, ""], + // [{ filters: {} }, ""], + // [{ filters: { regexName: null } }, ""], + [{ filters: { regexName: "n.me" } }, "n.me"], // TODO: should this be a regex object? ])("given %j, returns '%s'", (given, expected) => { - const binding = given as DeepPartial; + const binding = { + ...defaultBinding, + filters: { + ...defaultFilters, + regexName: given.filters.regexName, + }, + }; const result = sut.definedNameRegex(binding); @@ -108,12 +165,18 @@ describe("definedNameRegex", () => { describe("definesNameRegex", () => { //[ Binding, result ] it.each([ - [{}, false], - [{ filters: {} }, false], - [{ filters: { regexName: null } }, false], + // [{}, false], + // [{ filters: {} }, false], + // [{ filters: { regexName: null } }, false], [{ filters: { regexName: "n.me" } }, true], ])("given %j, returns %s", (given, expected) => { - const binding = given as DeepPartial; + const binding = { + ...defaultBinding, + filters: { + ...defaultFilters, + regexName: given.filters.regexName, + }, + }; const result = sut.definesNameRegex(binding); @@ -121,17 +184,26 @@ describe("definesNameRegex", () => { }); }); +const defaultKubernetesObject: KubernetesObject = { + apiVersion: "some-version", + kind: "some-kind", + metadata: { name: "some-name" }, +}; + describe("carriedName", () => { //[ KubernetesObject, result ] it.each([ - [{}, ""], - [{ metadata: {} }, ""], - [{ metadata: { name: null } }, ""], + // [{}, ""], + // [{ metadata: {} }, ""], + // [{ metadata: { name: null } }, ""], [{ metadata: { name: "name" } }, "name"], ])("given %j, returns '%s'", (given, expected) => { - const binding = given as DeepPartial; + const kubernetesObject = { + ...defaultKubernetesObject, + metadata: { name: given.metadata.name }, + }; - const result = sut.carriedName(binding); + const result = sut.carriedName(kubernetesObject); expect(result).toBe(expected); }); @@ -140,14 +212,17 @@ describe("carriedName", () => { describe("carriesName", () => { //[ KubernetesObject, result ] it.each([ - [{}, false], - [{ metadata: {} }, false], - [{ metadata: { name: null } }, false], + // [{}, false], + // [{ metadata: {} }, false], + // [{ metadata: { name: null } }, false], [{ metadata: { name: "name" } }, true], ])("given %j, returns %s", (given, expected) => { - const binding = given as DeepPartial; + const kubernetesObject = { + ...defaultKubernetesObject, + metadata: { name: given.metadata.name }, + }; - const result = sut.carriesName(binding); + const result = sut.carriesName(kubernetesObject); expect(result).toBe(expected); }); @@ -156,14 +231,17 @@ describe("carriesName", () => { describe("missingName", () => { //[ Binding, result ] it.each([ - [{}, true], - [{ metadata: {} }, true], - [{ metadata: { name: null } }, true], + // [{}, true], + // [{ metadata: {} }, true], + // [{ metadata: { name: null } }, true], [{ metadata: { name: "name" } }, false], ])("given %j, returns %s", (given, expected) => { - const binding = given as DeepPartial; + const kubernetesObject = { + ...defaultKubernetesObject, + metadata: { name: given.metadata.name }, + }; - const result = sut.missingName(binding); + const result = sut.missingName(kubernetesObject); expect(result).toBe(expected); }); @@ -172,13 +250,19 @@ describe("missingName", () => { describe("bindsToNamespace", () => { //[ Binding, result ] it.each([ - [{}, false], - [{ kind: {} }, false], + // [{}, false], + // [{ kind: {} }, false], [{ kind: { kind: null } }, false], [{ kind: { kind: "" } }, false], [{ kind: { kind: "Namespace" } }, true], - ])("given binding %j returns %s", (bnd, expected) => { - const binding = bnd as DeepPartial; + ])("given binding %j returns %s", (given, expected) => { + const binding = { + ...defaultBinding, + filters: { + ...defaultFilters, + }, + kind: given.kind.kind, + }; const result = sut.bindsToNamespace(binding); @@ -189,14 +273,20 @@ describe("bindsToNamespace", () => { describe("definedNamespaces", () => { //[ Binding, result ] it.each([ - [{}, []], - [{ filters: {} }, []], + // [{}, []], + // [{ filters: {} }, []], [{ filters: { namespaces: null } }, []], [{ filters: { namespaces: [] } }, []], [{ filters: { namespaces: ["namespace"] } }, ["namespace"]], [{ filters: { namespaces: ["name", "space"] } }, ["name", "space"]], ])("given %j, returns %j", (given, expected) => { - const binding = given as DeepPartial; + const binding = { + ...defaultBinding, + filters: { + ...defaultFilters, + namespaces: given.filters.namespaces, + }, + }; const result = sut.definedNamespaces(binding); @@ -207,14 +297,20 @@ describe("definedNamespaces", () => { describe("definesNamespaces", () => { //[ Binding, result ] it.each([ - [{}, false], - [{ filters: {} }, false], + // [{}, false], + // [{ filters: {} }, false], [{ filters: { namespaces: null } }, false], [{ filters: { namespaces: [] } }, false], [{ filters: { namespaces: ["namespace"] } }, true], [{ filters: { namespaces: ["name", "space"] } }, true], ])("given %j, returns %s", (given, expected) => { - const binding = given as DeepPartial; + const binding = { + ...defaultBinding, + filters: { + ...defaultFilters, + namespaces: given.filters.namespaces, + }, + }; const result = sut.definesNamespaces(binding); @@ -225,14 +321,20 @@ describe("definesNamespaces", () => { describe("definedNamespaceRegexes", () => { //[ Binding, result ] it.each([ - [{}, []], - [{ filters: {} }, []], + // [{}, []], + // [{ filters: {} }, []], [{ filters: { regexNamespaces: null } }, []], [{ filters: { regexNamespaces: [] } }, []], [{ filters: { regexNamespaces: ["n.mesp.ce"] } }, ["n.mesp.ce"]], [{ filters: { regexNamespaces: ["n.me", "sp.ce"] } }, ["n.me", "sp.ce"]], ])("given %j, returns %j", (given, expected) => { - const binding = given as DeepPartial; + const binding = { + ...defaultBinding, + filters: { + ...defaultFilters, + namespaces: given.filters.regexNamespaces, + }, + }; const result = sut.definedNamespaceRegexes(binding); @@ -243,14 +345,20 @@ describe("definedNamespaceRegexes", () => { describe("definesNamespaceRegexes", () => { //[ Binding, result ] it.each([ - [{}, false], - [{ filters: {} }, false], + // [{}, false], + // [{ filters: {} }, false], [{ filters: { regexNamespaces: null } }, false], [{ filters: { regexNamespaces: [] } }, false], [{ filters: { regexNamespaces: ["n.mesp.ce"] } }, true], [{ filters: { regexNamespaces: ["n.me", "sp.ce"] } }, true], ])("given %j, returns %s", (given, expected) => { - const binding = given as DeepPartial; + const binding = { + ...defaultBinding, + filters: { + ...defaultFilters, + namespaces: given.filters.regexNamespaces, + }, + }; const result = sut.definesNamespaceRegexes(binding); @@ -267,7 +375,7 @@ describe("carriedNamespace", () => { [{ metadata: { namespace: "" } }, ""], [{ metadata: { namespace: "namespace" } }, "namespace"], ])("given %j, returns %j", (given, expected) => { - const binding = given as DeepPartial; + const binding = given as DeepPartial; const result = sut.carriedNamespace(binding); @@ -278,15 +386,18 @@ describe("carriedNamespace", () => { describe("carriesNamespace", () => { //[ KubernetesObject, result ] it.each([ - [{}, false], - [{ metadata: {} }, false], - [{ metadata: { namespace: null } }, false], + // [{}, false], + // [{ metadata: {} }, false], + // [{ metadata: { namespace: null } }, false], [{ metadata: { namespace: "" } }, false], [{ metadata: { namespace: "namespace" } }, true], ])("given %j, returns %s", (given, expected) => { - const binding = given as DeepPartial; + const kubernetesObject = { + ...defaultKubernetesObject, + metadata: { namespace: given.metadata.namespace }, + }; - const result = sut.carriesNamespace(binding); + const result = sut.carriesNamespace(kubernetesObject); expect(result).toBe(expected); }); @@ -300,7 +411,14 @@ describe("misboundNamespace", () => { [{ kind: { kind: "Namespace" }, filters: { namespaces: [] } }, false], [{ kind: { kind: "Namespace" }, filters: { namespaces: ["namespace"] } }, true], ])("given %j, returns %s", (given, expected) => { - const binding = given as DeepPartial; + const binding = { + ...defaultBinding, + filters: { + ...defaultFilters, + namespaces: given.filters.namespaces, + }, + kind: given.kind, + }; const result = sut.misboundNamespace(binding); @@ -311,14 +429,20 @@ describe("misboundNamespace", () => { describe("definedAnnotations", () => { //[ Binding, result ] it.each([ - [{}, {}], - [{ filters: {} }, {}], - [{ filters: { annotations: null } }, {}], + // [{}, {}], + // [{ filters: {} }, {}], + // [{ filters: { annotations: null } }, {}], [{ filters: { annotations: {} } }, {}], [{ filters: { annotations: { annotation: "" } } }, { annotation: "" }], [{ filters: { annotations: { anno: "tation" } } }, { anno: "tation" }], ])("given %j, returns %j", (given, expected) => { - const binding = given as DeepPartial; + const binding = { + ...defaultBinding, + filters: { + ...defaultFilters, + annotations: given.filters.annotations, + }, + }; const result = sut.definedAnnotations(binding); @@ -329,14 +453,20 @@ describe("definedAnnotations", () => { describe("definesAnnotations", () => { //[ Binding, result ] it.each([ - [{}, false], - [{ filters: {} }, false], - [{ filters: { annotations: null } }, false], + // [{}, false], + // [{ filters: {} }, false], + // [{ filters: { annotations: null } }, false], [{ filters: { annotations: {} } }, false], [{ filters: { annotations: { annotation: "" } } }, true], [{ filters: { annotations: { anno: "tation" } } }, true], ])("given %j, returns %s", (given, expected) => { - const binding = given as DeepPartial; + const binding = { + ...defaultBinding, + filters: { + ...defaultFilters, + annotations: given.filters.annotations, + }, + }; const result = sut.definesAnnotations(binding); @@ -347,16 +477,19 @@ describe("definesAnnotations", () => { describe("carriedAnnotations", () => { //[ KuberneteObject, result ] it.each([ - [{}, {}], - [{ metadata: {} }, {}], - [{ metadata: { annotations: null } }, {}], + // [{}, {}], + // [{ metadata: {} }, {}], + // [{ metadata: { annotations: null } }, {}], [{ metadata: { annotations: {} } }, {}], [{ metadata: { annotations: { annotation: "" } } }, { annotation: "" }], [{ metadata: { annotations: { anno: "tation" } } }, { anno: "tation" }], ])("given %j, returns %j", (given, expected) => { - const binding = given as DeepPartial; + const kubernetesObject = { + ...defaultKubernetesObject, + metadata: { annotations: given.metadata.annotations }, + }; - const result = sut.carriedAnnotations(binding); + const result = sut.carriedAnnotations(kubernetesObject); expect(result).toEqual(expected); }); @@ -365,16 +498,19 @@ describe("carriedAnnotations", () => { describe("carriesAnnotations", () => { //[ KubernetesObject, result ] it.each([ - [{}, false], - [{ metadata: {} }, false], - [{ metadata: { annotations: null } }, false], + // [{}, false], + // [{ metadata: {} }, false], + // [{ metadata: { annotations: null } }, false], [{ metadata: { annotations: {} } }, false], [{ metadata: { annotations: { annotation: "" } } }, true], [{ metadata: { annotations: { anno: "tation" } } }, true], ])("given %j, returns %s", (given, expected) => { - const binding = given as DeepPartial; + const kubernetesObject = { + ...defaultKubernetesObject, + metadata: { annotations: given.metadata.annotations }, + }; - const result = sut.carriesAnnotations(binding); + const result = sut.carriesAnnotations(kubernetesObject); expect(result).toBe(expected); }); @@ -383,14 +519,20 @@ describe("carriesAnnotations", () => { describe("definedLabels", () => { //[ Binding, result ] it.each([ - [{}, {}], - [{ filters: {} }, {}], - [{ filters: { labels: null } }, {}], + // [{}, {}], + // [{ filters: {} }, {}], + // [{ filters: { labels: null } }, {}], [{ filters: { labels: {} } }, {}], [{ filters: { labels: { label: "" } } }, { label: "" }], [{ filters: { labels: { lab: "el" } } }, { lab: "el" }], ])("given %j, returns %j", (given, expected) => { - const binding = given as DeepPartial; + const binding = { + ...defaultBinding, + filters: { + ...defaultFilters, + labels: given.filters.labels, + }, + }; const result = sut.definedLabels(binding); @@ -401,14 +543,20 @@ describe("definedLabels", () => { describe("definesLabels", () => { //[ Binding, result ] it.each([ - [{}, false], - [{ filters: {} }, false], - [{ filters: { labels: null } }, false], + // [{}, false], + // [{ filters: {} }, false], + // [{ filters: { labels: null } }, false], [{ filters: { labels: {} } }, false], [{ filters: { labels: { label: "" } } }, true], [{ filters: { labels: { lab: "el" } } }, true], ])("given %j, returns %s", (given, expected) => { - const binding = given as DeepPartial; + const binding = { + ...defaultBinding, + filters: { + ...defaultFilters, + labels: given.filters.labels, + }, + }; const result = sut.definesLabels(binding); @@ -419,16 +567,19 @@ describe("definesLabels", () => { describe("carriedLabels", () => { //[ KubernetesObject, result ] it.each([ - [{}, {}], - [{ metadata: {} }, {}], - [{ metadata: { labels: null } }, {}], + // [{}, {}], + // [{ metadata: {} }, {}], + // [{ metadata: { labels: null } }, {}], [{ metadata: { labels: {} } }, {}], [{ metadata: { labels: { label: "" } } }, { label: "" }], [{ metadata: { labels: { lab: "el" } } }, { lab: "el" }], ])("given %j, returns %j", (given, expected) => { - const binding = given as DeepPartial; + const kubernetesObject = { + ...defaultKubernetesObject, + metadata: { labels: given.metadata.labels }, + }; - const result = sut.carriedLabels(binding); + const result = sut.carriedLabels(kubernetesObject); expect(result).toEqual(expected); }); @@ -437,16 +588,19 @@ describe("carriedLabels", () => { describe("carriesLabels", () => { //[ KubernetesObject, result ] it.each([ - [{}, false], - [{ metadata: {} }, false], - [{ metadata: { labels: null } }, false], + // [{}, false], + // [{ metadata: {} }, false], + // [{ metadata: { labels: null } }, false], [{ metadata: { labels: {} } }, false], [{ metadata: { labels: { label: "" } } }, true], [{ metadata: { labels: { lab: "el" } } }, true], ])("given %j, returns %s", (given, expected) => { - const binding = given as DeepPartial; + const kubernetesObject = { + ...defaultKubernetesObject, + metadata: { labels: given.metadata.labels }, + }; - const result = sut.carriesLabels(binding); + const result = sut.carriesLabels(kubernetesObject); expect(result).toBe(expected); }); @@ -455,15 +609,18 @@ describe("carriesLabels", () => { describe("definedEvent", () => { //[ Binding, result ] it.each([ - [{}, ""], - [{ event: "" }, ""], - [{ event: "nonsense" }, "nonsense"], - [{ event: Event.Create }, Event.Create], - [{ event: Event.CreateOrUpdate }, Event.CreateOrUpdate], - [{ event: Event.Update }, Event.Update], - [{ event: Event.Delete }, Event.Delete], + // [{}, ""], + // [{ event: "" }, ""], + // [{ event: "nonsense" }, "nonsense"], + [{ event: Event.CREATE }, Event.CREATE], + [{ event: Event.CREATE_OR_UPDATE }, Event.CREATE_OR_UPDATE], + [{ event: Event.UPDATE }, Event.UPDATE], + [{ event: Event.DELETE }, Event.DELETE], ])("given %j, returns '%s'", (given, expected) => { - const binding = given as DeepPartial; + const binding = { + ...defaultBinding, + event: given.event, + }; const result = sut.definedEvent(binding); @@ -474,15 +631,18 @@ describe("definedEvent", () => { describe("definesDelete", () => { //[ Binding, result ] it.each([ - [{}, false], - [{ event: "" }, false], - [{ event: "nonsense" }, false], - [{ event: Event.Create }, false], - [{ event: Event.CreateOrUpdate }, false], - [{ event: Event.Update }, false], - [{ event: Event.Delete }, true], + // [{}, false], + // [{ event: "" }, false], + // [{ event: "nonsense" }, false], + [{ event: Event.CREATE }, false], + [{ event: Event.CREATE_OR_UPDATE }, false], + [{ event: Event.UPDATE }, false], + [{ event: Event.DELETE }, true], ])("given %j, returns %s", (given, expected) => { - const binding = given as DeepPartial; + const binding = { + ...defaultBinding, + event: given.event, + }; const result = sut.definesDelete(binding); @@ -493,18 +653,21 @@ describe("definesDelete", () => { describe("misboundDeleteWithDeletionTimestamp", () => { //[ Binding, result ] it.each([ - [{}, false], - [{ event: "" }, false], - [{ event: "nonsense" }, false], - [{ event: Event.Create }, false], - [{ event: Event.CreateOrUpdate }, false], - [{ event: Event.Update }, false], - [{ event: Event.Delete }, false], - [{ event: Event.Delete, filters: {} }, false], - [{ event: Event.Delete, filters: { deletionTimestamp: false } }, false], - [{ event: Event.Delete, filters: { deletionTimestamp: true } }, true], + // [{}, false], + // [{ event: "" }, false], + // [{ event: "nonsense" }, false], + [{ event: Event.CREATE }, false], + [{ event: Event.CREATE_OR_UPDATE }, false], + [{ event: Event.UPDATE }, false], + [{ event: Event.DELETE }, false], + [{ event: Event.DELETE, filters: {} }, false], + [{ event: Event.DELETE, filters: { deletionTimestamp: false } }, false], + [{ event: Event.DELETE, filters: { deletionTimestamp: true } }, true], ])("given %j, returns %s", (given, expected) => { - const binding = given as DeepPartial; + const binding = { + ...defaultBinding, + event: given.event, + }; const result = sut.misboundDeleteWithDeletionTimestamp(binding); @@ -515,14 +678,17 @@ describe("misboundDeleteWithDeletionTimestamp", () => { describe("definedGroup", () => { //[ Binding, result ] it.each([ - [{}, ""], - [{ kind: null }, ""], - [{ kind: {} }, ""], + // [{}, ""], + // [{ kind: null }, ""], + // [{ kind: {} }, ""], [{ kind: { group: null } }, ""], [{ kind: { group: "" } }, ""], [{ kind: { group: "group" } }, "group"], ])("given %j, returns '%s'", (given, expected) => { - const binding = given as DeepPartial; + const binding = { + ...defaultBinding, + kind: { group: given.kind.group }, + }; const result = sut.definedGroup(binding); @@ -533,14 +699,17 @@ describe("definedGroup", () => { describe("definesGroup", () => { //[ Binding, result ] it.each([ - [{}, false], - [{ kind: null }, false], - [{ kind: {} }, false], + // [{}, false], + // [{ kind: null }, false], + // [{ kind: {} }, false], [{ kind: { group: null } }, false], [{ kind: { group: "" } }, false], [{ kind: { group: "group" } }, true], ])("given %j, returns %s", (given, expected) => { - const binding = given as DeepPartial; + const binding = { + ...defaultBinding, + kind: { group: given.kind.group }, + }; const result = sut.definesGroup(binding); @@ -551,14 +720,17 @@ describe("definesGroup", () => { describe("definedVersion", () => { //[ Binding, result ] it.each([ - [{}, ""], - [{ kind: null }, ""], - [{ kind: {} }, ""], - [{ kind: { version: null } }, ""], + // [{}, ""], + // [{ kind: null }, ""], + // [{ kind: {} }, ""], + // [{ kind: { version: null } }, ""], [{ kind: { version: "" } }, ""], [{ kind: { version: "version" } }, "version"], ])("given %j, returns '%s'", (given, expected) => { - const binding = given as DeepPartial; + const binding = { + ...defaultBinding, + kind: { kind: "some-kind", group: "some-group", version: given.kind.version }, + }; const result = sut.definedVersion(binding); @@ -569,14 +741,17 @@ describe("definedVersion", () => { describe("definesVersion", () => { //[ Binding, result ] it.each([ - [{}, false], - [{ kind: null }, false], - [{ kind: {} }, false], - [{ kind: { version: null } }, false], + // [{}, false], + // [{ kind: null }, false], + // [{ kind: {} }, false], + // [{ kind: { version: null } }, false], [{ kind: { version: "" } }, false], [{ kind: { version: "version" } }, true], ])("given %j, returns %s", (given, expected) => { - const binding = given as DeepPartial; + const binding = { + ...defaultBinding, + kind: { kind: "some-kind", group: "some-group", version: given.kind.version }, + }; const result = sut.definesVersion(binding); @@ -587,14 +762,17 @@ describe("definesVersion", () => { describe("definedKind", () => { //[ Binding, result ] it.each([ - [{}, ""], - [{ kind: null }, ""], - [{ kind: {} }, ""], + // [{}, ""], + // [{ kind: null }, ""], + // [{ kind: {} }, ""], [{ kind: { kind: null } }, ""], [{ kind: { kind: "" } }, ""], [{ kind: { kind: "kind" } }, "kind"], ])("given %j, returns '%s'", (given, expected) => { - const binding = given as DeepPartial; + const binding = { + ...defaultBinding, + kind: { kind: given.kind.kind }, + }; const result = sut.definedKind(binding); @@ -605,14 +783,17 @@ describe("definedKind", () => { describe("definesKind", () => { //[ Binding, result ] it.each([ - [{}, false], - [{ kind: null }, false], - [{ kind: {} }, false], + // [{}, false], + // [{ kind: null }, false], + // [{ kind: {} }, false], [{ kind: { kind: null } }, false], [{ kind: { kind: "" } }, false], [{ kind: { kind: "kind" } }, true], ])("given %j, returns %s", (given, expected) => { - const binding = given as DeepPartial; + const binding = { + ...defaultBinding, + kind: { kind: given.kind.kind }, + }; const result = sut.definesKind(binding); @@ -629,7 +810,10 @@ describe("definedCategory", () => { [{ isWatch: true }, "Watch"], [{ isFinalize: true, isWatch: true }, "Finalize"], ])("given %j, returns %s", (given, expected) => { - const binding = given as DeepPartial; + const binding = { + ...defaultBinding, + ...given, + }; const result = sut.definedCategory(binding); @@ -638,7 +822,9 @@ describe("definedCategory", () => { }); describe("definedCallback", () => { - const validateCallback = () => {}; + const validateCallback = (): ValidateActionResponse => { + return { allowed: false }; + }; const mutateCallback = () => {}; const watchCallback = () => {}; const finalizeCallback = () => {}; @@ -651,7 +837,10 @@ describe("definedCallback", () => { [{ isWatch: true, watchCallback }, watchCallback], [{ isFinalize: true, finalizeCallback }, finalizeCallback], ])("given %j, returns %s", (given, expected) => { - const binding = given as DeepPartial; + const binding = { + ...defaultBinding, + ...given, + }; const result = sut.definedCallback(binding); @@ -660,7 +849,9 @@ describe("definedCallback", () => { }); describe("definedCallbackName", () => { - const validateCallback = () => {}; + const validateCallback = (): ValidateActionResponse => { + return { allowed: false }; + }; const mutateCallback = () => {}; const watchCallback = () => {}; const finalizeCallback = () => {}; @@ -673,7 +864,10 @@ describe("definedCallbackName", () => { [{ isWatch: true, watchCallback }, "watchCallback"], [{ isFinalize: true, finalizeCallback }, "finalizeCallback"], ])("given %j, returns %s", (given, expected) => { - const binding = given as DeepPartial; + const binding = { + ...defaultBinding, + ...given, + }; const result = sut.definedCallbackName(binding); diff --git a/src/lib/filter/shouldSkipRequest.test.ts b/src/lib/filter/shouldSkipRequest.test.ts index e3e63482a..0c432300c 100644 --- a/src/lib/filter/shouldSkipRequest.test.ts +++ b/src/lib/filter/shouldSkipRequest.test.ts @@ -39,7 +39,7 @@ const defaultBinding = { export const groupBinding = { callback, - event: Event.Create, + event: Event.CREATE, filters: defaultFilters, kind: deploymentKind, model: kind.Deployment, @@ -47,7 +47,7 @@ export const groupBinding = { export const clusterScopedBinding = { callback, - event: Event.Delete, + event: Event.DELETE, filters: defaultFilters, kind: clusterRoleKind, model: kind.ClusterRole, @@ -172,7 +172,7 @@ describe("when a capability defines namespaces and the admission request object it("should skip request when the capability namespace does not exist on the object", () => { const binding = { ...clusterScopedBinding, - event: Event.Create, + event: Event.CREATE, filters: { ...clusterScopedBinding.filters, regexName: "", @@ -190,7 +190,7 @@ describe("when a binding contains a cluster scoped object", () => { it("should skip request when the binding defines a namespace on a cluster scoped object", () => { const clusterScopedBindingWithNamespace = { ...clusterScopedBinding, - event: Event.Create, + event: Event.CREATE, filters: { ...clusterScopedBinding.filters, namespaces: ["namespace"], diff --git a/src/lib/helpers.ts b/src/lib/helpers.ts index e7bd2f24d..79c76fd26 100644 --- a/src/lib/helpers.ts +++ b/src/lib/helpers.ts @@ -75,7 +75,7 @@ export type RBACMap = { **/ export function filterNoMatchReason( binding: Partial, - obj: Partial, + object: Partial, capabilityNamespaces: string[], ignoredNamespaces?: string[], ): string { @@ -83,30 +83,30 @@ export function filterNoMatchReason( // prettier-ignore return ( - mismatchedDeletionTimestamp(binding, obj) ? + mismatchedDeletionTimestamp(binding, object) ? `${prefix} Binding defines deletionTimestamp but Object does not carry it.` : - mismatchedName(binding, obj) ? - `${prefix} Binding defines name '${definedName(binding)}' but Object carries '${carriedName(obj)}'.` : + mismatchedName(binding, object) ? + `${prefix} Binding defines name '${definedName(binding)}' but Object carries '${carriedName(object)}'.` : misboundNamespace(binding) ? `${prefix} Cannot use namespace filter on a namespace object.` : - mismatchedLabels(binding, obj) ? + mismatchedLabels(binding, object) ? ( `${prefix} Binding defines labels '${JSON.stringify(definedLabels(binding))}' ` + - `but Object carries '${JSON.stringify(carriedLabels(obj))}'.` + `but Object carries '${JSON.stringify(carriedLabels(object))}'.` ) : - mismatchedAnnotations(binding, obj) ? + mismatchedAnnotations(binding, object) ? ( `${prefix} Binding defines annotations '${JSON.stringify(definedAnnotations(binding))}' ` + - `but Object carries '${JSON.stringify(carriedAnnotations(obj))}'.` + `but Object carries '${JSON.stringify(carriedAnnotations(object))}'.` ) : - uncarryableNamespace(capabilityNamespaces, obj) ? + uncarryableNamespace(capabilityNamespaces, object) ? ( - `${prefix} Object carries namespace '${carriedNamespace(obj)}' ` + + `${prefix} Object carries namespace '${carriedNamespace(object)}' ` + `but namespaces allowed by Capability are '${JSON.stringify(capabilityNamespaces)}'.` ) : @@ -116,32 +116,32 @@ export function filterNoMatchReason( `but namespaces allowed by Capability are '${JSON.stringify(capabilityNamespaces)}'.` ) : - mismatchedNamespace(binding, obj) ? + mismatchedNamespace(binding, object) ? ( `${prefix} Binding defines namespaces '${JSON.stringify(definedNamespaces(binding))}' ` + - `but Object carries '${carriedNamespace(obj)}'.` + `but Object carries '${carriedNamespace(object)}'.` ) : - mismatchedNamespaceRegex(binding, obj) ? + mismatchedNamespaceRegex(binding, object) ? ( `${prefix} Binding defines namespace regexes ` + `'${JSON.stringify(definedNamespaceRegexes(binding))}' ` + - `but Object carries '${carriedNamespace(obj)}'.` + `but Object carries '${carriedNamespace(object)}'.` ) : - mismatchedNameRegex(binding, obj) ? + mismatchedNameRegex(binding, object) ? ( `${prefix} Binding defines name regex '${definedNameRegex(binding)}' ` + - `but Object carries '${carriedName(obj)}'.` + `but Object carries '${carriedName(object)}'.` ) : - carriesIgnoredNamespace(ignoredNamespaces, obj) ? + carriesIgnoredNamespace(ignoredNamespaces, object) ? ( - `${prefix} Object carries namespace '${carriedNamespace(obj)}' ` + + `${prefix} Object carries namespace '${carriedNamespace(object)}' ` + `but ignored namespaces include '${JSON.stringify(ignoredNamespaces)}'.` ) : - missingCarriableNamespace(capabilityNamespaces, obj) ? + missingCarriableNamespace(capabilityNamespaces, object) ? ( `${prefix} Object does not carry a namespace ` + `but namespaces allowed by Capability are '${JSON.stringify(capabilityNamespaces)}'.` From 8300b37fd7c55ebb3bf4dc9ae9d1701873e0a09b Mon Sep 17 00:00:00 2001 From: Sam Mayer Date: Mon, 18 Nov 2024 15:57:48 -0600 Subject: [PATCH 08/46] use explicit imports in test file --- src/lib/filter/adjudicators.test.ts | 55 +++++++++++++++++++---------- 1 file changed, 37 insertions(+), 18 deletions(-) diff --git a/src/lib/filter/adjudicators.test.ts b/src/lib/filter/adjudicators.test.ts index 6caa998ea..2290dd2d0 100644 --- a/src/lib/filter/adjudicators.test.ts +++ b/src/lib/filter/adjudicators.test.ts @@ -3,7 +3,26 @@ // SPDX-FileCopyrightText: 2023-Present The Pepr Authors import { expect, describe, it } from "@jest/globals"; -import * as sut from "./adjudicators"; +import { + bindsToKind, + carriesIgnoredNamespace, + metasMismatch, + mismatchedAnnotations, + mismatchedDeletionTimestamp, + mismatchedEvent, + mismatchedGroup, + mismatchedKind, + mismatchedLabels, + mismatchedName, + mismatchedNameRegex, + mismatchedNamespace, + mismatchedNamespaceRegex, + mismatchedVersion, + missingCarriableNamespace, + operationMatchesEvent, + unbindableNamespaces, + uncarryableNamespace, +} from "./adjudicators"; import { KubernetesObject } from "kubernetes-fluent-client"; import { AdmissionRequest, Binding, DeepPartial } from "../types"; import { Event, Operation } from "../enums"; @@ -36,7 +55,7 @@ describe("mismatchedName", () => { const binding = bnd as DeepPartial; const object = obj as DeepPartial; - const result = sut.mismatchedName(binding, object); + const result = mismatchedName(binding, object); expect(result).toBe(expected); }); @@ -58,7 +77,7 @@ describe("mismatchedNameRegex", () => { const binding = bnd as DeepPartial; const object = obj as DeepPartial; - const result = sut.mismatchedNameRegex(binding, object); + const result = mismatchedNameRegex(binding, object); expect(result).toBe(expected); }); @@ -80,7 +99,7 @@ describe("bindsToKind", () => { const binding = bnd as DeepPartial; const kind = knd as string; - const result = sut.bindsToKind(binding, kind); + const result = bindsToKind(binding, kind); expect(result).toBe(expected); }); @@ -98,7 +117,7 @@ describe("mismatchedNamespace", () => { const binding = bnd as DeepPartial; const object = obj as DeepPartial; - const result = sut.mismatchedNamespace(binding, object); + const result = mismatchedNamespace(binding, object); expect(result).toBe(expected); }); @@ -129,7 +148,7 @@ describe("mismatchedNamespaceRegex", () => { const binding = bnd as DeepPartial; const object = obj as DeepPartial; - const result = sut.mismatchedNamespaceRegex(binding, object); + const result = mismatchedNamespaceRegex(binding, object); expect(result).toBe(expected); }); @@ -155,7 +174,7 @@ describe("metasMismatch", () => { [{ an: "no", ta: "te" }, { an: "no", ta: "te" }, false], [{ an: "no", ta: "te" }, { an: "no", ta: "to" }, true], ])("given left %j and right %j, returns %s", (bnd, obj, expected) => { - const result = sut.metasMismatch(bnd, obj); + const result = metasMismatch(bnd, obj); expect(result).toBe(expected); }); @@ -189,7 +208,7 @@ describe("mismatchedAnnotations", () => { const binding = bnd as DeepPartial; const object = obj as DeepPartial; - const result = sut.mismatchedAnnotations(binding, object); + const result = mismatchedAnnotations(binding, object); expect(result).toBe(expected); }); @@ -217,7 +236,7 @@ describe("mismatchedLabels", () => { const binding = bnd as DeepPartial; const object = obj as DeepPartial; - const result = sut.mismatchedLabels(binding, object); + const result = mismatchedLabels(binding, object); expect(result).toBe(expected); }); @@ -248,7 +267,7 @@ describe("missingCarriableNamespace", () => { ])("given capabilityNamespaces %j and object %j, returns %s", (nss, obj, expected) => { const object = obj as DeepPartial; - const result = sut.missingCarriableNamespace(nss, object); + const result = missingCarriableNamespace(nss, object); expect(result).toBe(expected); }); @@ -277,7 +296,7 @@ describe("uncarryableNamespace", () => { ])("given capabilityNamespaces %j and object %j, returns %s", (nss, obj, expected) => { const object = obj as DeepPartial; - const result = sut.uncarryableNamespace(nss, object); + const result = uncarryableNamespace(nss, object); expect(result).toBe(expected); }); @@ -306,7 +325,7 @@ describe("carriesIgnoredNamespace", () => { ])("given capabilityNamespaces %j and object %j, returns %s", (nss, obj, expected) => { const object = obj as DeepPartial; - const result = sut.carriesIgnoredNamespace(nss, object); + const result = carriesIgnoredNamespace(nss, object); expect(result).toBe(expected); }); @@ -336,7 +355,7 @@ describe("unbindableNamespaces", () => { ])("given capabilityNamespaces %j and binding %j, returns %s", (nss, bnd, expected) => { const binding = bnd as DeepPartial; - const result = sut.unbindableNamespaces(nss, binding); + const result = unbindableNamespaces(nss, binding); expect(result).toBe(expected); }); @@ -373,7 +392,7 @@ describe("operationMatchesEvent", () => { [Operation.CONNECT, Event.CREATE_OR_UPDATE, false], [Operation.CONNECT, Event.ANY, true], ])("given operation %s and event %s, returns %s", (op, evt, expected) => { - const result = sut.operationMatchesEvent(op, evt); + const result = operationMatchesEvent(op, evt); expect(result).toEqual(expected); }); @@ -413,7 +432,7 @@ describe("mismatchedEvent", () => { const binding = bnd as DeepPartial; const request = req as DeepPartial; - const result = sut.mismatchedEvent(binding, request); + const result = mismatchedEvent(binding, request); expect(result).toEqual(expected); }); @@ -431,7 +450,7 @@ describe("mismatchedGroup", () => { const binding = bnd as DeepPartial; const request = req as DeepPartial; - const result = sut.mismatchedGroup(binding, request); + const result = mismatchedGroup(binding, request); expect(result).toEqual(expected); }); @@ -449,7 +468,7 @@ describe("mismatchedVersion", () => { const binding = bnd as DeepPartial; const request = req as DeepPartial; - const result = sut.mismatchedVersion(binding, request); + const result = mismatchedVersion(binding, request); expect(result).toEqual(expected); }); @@ -467,7 +486,7 @@ describe("mismatchedKind", () => { const binding = bnd as DeepPartial; const request = req as DeepPartial; - const result = sut.mismatchedKind(binding, request); + const result = mismatchedKind(binding, request); expect(result).toEqual(expected); }); From bf38e38b8204f1a0020617bea439d1becdc82371 Mon Sep 17 00:00:00 2001 From: Sam Mayer Date: Mon, 18 Nov 2024 16:10:14 -0600 Subject: [PATCH 09/46] Fix failing tests in bindingadjudicator --- .../adjudicators/bindingAdjudicators.test.ts | 65 ++++++++++++------- 1 file changed, 42 insertions(+), 23 deletions(-) diff --git a/src/lib/filter/adjudicators/bindingAdjudicators.test.ts b/src/lib/filter/adjudicators/bindingAdjudicators.test.ts index 1776cb88e..a47d3f507 100644 --- a/src/lib/filter/adjudicators/bindingAdjudicators.test.ts +++ b/src/lib/filter/adjudicators/bindingAdjudicators.test.ts @@ -261,7 +261,7 @@ describe("bindsToNamespace", () => { filters: { ...defaultFilters, }, - kind: given.kind.kind, + kind: { kind: given.kind.kind }, }; const result = sut.bindsToNamespace(binding); @@ -332,7 +332,7 @@ describe("definedNamespaceRegexes", () => { ...defaultBinding, filters: { ...defaultFilters, - namespaces: given.filters.regexNamespaces, + regexNamespaces: given.filters.regexNamespaces, }, }; @@ -356,7 +356,7 @@ describe("definesNamespaceRegexes", () => { ...defaultBinding, filters: { ...defaultFilters, - namespaces: given.filters.regexNamespaces, + regexNamespaces: given.filters.regexNamespaces, }, }; @@ -652,26 +652,45 @@ describe("definesDelete", () => { describe("misboundDeleteWithDeletionTimestamp", () => { //[ Binding, result ] - it.each([ - // [{}, false], - // [{ event: "" }, false], - // [{ event: "nonsense" }, false], - [{ event: Event.CREATE }, false], - [{ event: Event.CREATE_OR_UPDATE }, false], - [{ event: Event.UPDATE }, false], - [{ event: Event.DELETE }, false], - [{ event: Event.DELETE, filters: {} }, false], - [{ event: Event.DELETE, filters: { deletionTimestamp: false } }, false], - [{ event: Event.DELETE, filters: { deletionTimestamp: true } }, true], - ])("given %j, returns %s", (given, expected) => { - const binding = { - ...defaultBinding, - event: given.event, - }; - - const result = sut.misboundDeleteWithDeletionTimestamp(binding); - - expect(result).toEqual(expected); + describe("when filters are set", () => { + it.each([ + [{ event: Event.DELETE, filters: {} }, false], + [{ event: Event.DELETE, filters: { deletionTimestamp: false } }, false], + [{ event: Event.DELETE, filters: { deletionTimestamp: true } }, true], + ])("given %j, returns %s", (given, expected) => { + const binding = { + ...defaultBinding, + filters: + "deletionTimestamp" in given.filters + ? { ...defaultFilters, deletionTimestamp: given.filters.deletionTimestamp } + : defaultFilters, + event: given.event, + }; + + const result = sut.misboundDeleteWithDeletionTimestamp(binding); + + expect(result).toEqual(expected); + }); + }); + describe("when filters are not set", () => { + it.each([ + // [{}, false], + // [{ event: "" }, false], + // [{ event: "nonsense" }, false], + [{ event: Event.CREATE }, false], + [{ event: Event.CREATE_OR_UPDATE }, false], + [{ event: Event.UPDATE }, false], + [{ event: Event.DELETE }, false], + ])("given %j, returns %s", (given, expected) => { + const binding = { + ...defaultBinding, + event: given.event, + }; + + const result = sut.misboundDeleteWithDeletionTimestamp(binding); + + expect(result).toEqual(expected); + }); }); }); From 984660f55a443be75b0ff50a4acab95699f9bf01 Mon Sep 17 00:00:00 2001 From: Sam Mayer Date: Mon, 18 Nov 2024 16:42:10 -0600 Subject: [PATCH 10/46] tidy test exports, add typing, get passing tests --- src/lib/filter/adjudicators.test.ts | 61 +++++++++++++++--------- src/lib/filter/adjudicators.ts | 3 +- src/lib/filter/filter.test.ts | 4 +- src/lib/filter/shouldSkipRequest.test.ts | 2 +- 4 files changed, 43 insertions(+), 27 deletions(-) diff --git a/src/lib/filter/adjudicators.test.ts b/src/lib/filter/adjudicators.test.ts index 2290dd2d0..ddf89e2bc 100644 --- a/src/lib/filter/adjudicators.test.ts +++ b/src/lib/filter/adjudicators.test.ts @@ -8,7 +8,6 @@ import { carriesIgnoredNamespace, metasMismatch, mismatchedAnnotations, - mismatchedDeletionTimestamp, mismatchedEvent, mismatchedGroup, mismatchedKind, @@ -23,26 +22,36 @@ import { unbindableNamespaces, uncarryableNamespace, } from "./adjudicators"; -import { KubernetesObject } from "kubernetes-fluent-client"; +import { kind, KubernetesObject, modelToGroupVersionKind } from "kubernetes-fluent-client"; import { AdmissionRequest, Binding, DeepPartial } from "../types"; import { Event, Operation } from "../enums"; -describe("carriesDeletionTimestamp", () => { - //[ KubernetesObject, result ] - it.each([ - [{}, false], - [{ metadata: {} }, false], - [{ metadata: { deletionTimestamp: null } }, false], - [{ metadata: { deletionTimestamp: new Date() } }, true], - ])("given %j, returns %s", (given, expected) => { - // eslint-disable-next-line @typescript-eslint/no-unused-vars - const ko = given as DeepPartial; - - const result = sut.mismatchedDeletionTimestamp(ko); - - expect(result).toBe(expected); - }); -}); +const defaultFilters = { + annotations: {}, + deletionTimestamp: false, + labels: {}, + name: "", + namespaces: [], + regexName: "^default$", + regexNamespaces: [], +}; +const defaultBinding: Binding = { + event: Event.ANY, + filters: defaultFilters, + kind: modelToGroupVersionKind(kind.Pod.name), + model: kind.Pod, +}; + +const defaultAdmissionRequest = { + uid: "some-uid", + kind: { kind: "a-kind", group: "a-group" }, + group: "a-group", + resource: { group: "some-group", version: "some-version", resource: "some-resource" }, + operation: Operation.CONNECT, + name: "some-name", + userInfo: {}, + object: {}, +}; describe("mismatchedName", () => { //[ Binding, KubernetesObject, result ] @@ -401,9 +410,9 @@ describe("operationMatchesEvent", () => { describe("mismatchedEvent", () => { //[ Binding, AdmissionRequest, result ] it.each([ - [{}, {}, false], - [{}, { operation: Operation.CREATE }, true], - [{ event: Event.CREATE }, {}, true], + // [{}, {}, false], + // [{}, { operation: Operation.CREATE }, true], + // [{ event: Event.CREATE }, {}, true], [{ event: Event.CREATE }, { operation: Operation.CREATE }, false], [{ event: Event.UPDATE }, { operation: Operation.CREATE }, true], @@ -429,8 +438,14 @@ describe("mismatchedEvent", () => { [{ event: Event.CREATE_OR_UPDATE }, { operation: Operation.CONNECT }, true], [{ event: Event.ANY }, { operation: Operation.CONNECT }, false], ])("given binding %j and admission request %j, returns %s", (bnd, req, expected) => { - const binding = bnd as DeepPartial; - const request = req as DeepPartial; + const binding: Binding = { + ...defaultBinding, + event: bnd.event, + }; + const request: AdmissionRequest = { + ...defaultAdmissionRequest, + operation: req.operation, + }; const result = mismatchedEvent(binding, request); diff --git a/src/lib/filter/adjudicators.ts b/src/lib/filter/adjudicators.ts index 3932400fd..3764e0948 100644 --- a/src/lib/filter/adjudicators.ts +++ b/src/lib/filter/adjudicators.ts @@ -288,7 +288,8 @@ export const operationMatchesEvent = anyPass([ ]); export const mismatchedEvent = pipe( - (binding, request) => operationMatchesEvent(declaredOperation(request), definedEvent(binding)), + (binding: Binding, request: AdmissionRequest): boolean => + operationMatchesEvent(declaredOperation(request), definedEvent(binding)), not, ); diff --git a/src/lib/filter/filter.test.ts b/src/lib/filter/filter.test.ts index a008945f3..79bd0c52a 100644 --- a/src/lib/filter/filter.test.ts +++ b/src/lib/filter/filter.test.ts @@ -9,9 +9,9 @@ import { shouldSkipRequest } from "./filter"; import { AdmissionRequest, Binding } from "../types"; import { Event } from "../enums"; -export const callback = () => undefined; +const callback = () => undefined; -export const podKind = modelToGroupVersionKind(kind.Pod.name); +const podKind = modelToGroupVersionKind(kind.Pod.name); describe("Fuzzing shouldSkipRequest", () => { test("should handle random inputs without crashing", () => { diff --git a/src/lib/filter/shouldSkipRequest.test.ts b/src/lib/filter/shouldSkipRequest.test.ts index 0c432300c..6cad948ba 100644 --- a/src/lib/filter/shouldSkipRequest.test.ts +++ b/src/lib/filter/shouldSkipRequest.test.ts @@ -14,7 +14,7 @@ import { shouldSkipRequest } from "./filter"; import { AdmissionRequest, Binding } from "../types"; import { Event } from "../enums"; -export const callback = () => undefined; +const callback = () => undefined; export const podKind = modelToGroupVersionKind(kind.Pod.name); export const deploymentKind = modelToGroupVersionKind(kind.Deployment.name); From f5c3a8d5f590f291caefbd0730ada02bde6159bb Mon Sep 17 00:00:00 2001 From: Sam Mayer Date: Mon, 18 Nov 2024 17:26:49 -0600 Subject: [PATCH 11/46] Add typing to mismatchedEvent() --- src/lib/filter/adjudicators.test.ts | 84 ++++++++++++++--------------- src/lib/filter/adjudicators.ts | 4 +- 2 files changed, 41 insertions(+), 47 deletions(-) diff --git a/src/lib/filter/adjudicators.test.ts b/src/lib/filter/adjudicators.test.ts index ddf89e2bc..ecd3779f2 100644 --- a/src/lib/filter/adjudicators.test.ts +++ b/src/lib/filter/adjudicators.test.ts @@ -373,10 +373,6 @@ describe("unbindableNamespaces", () => { describe("operationMatchesEvent", () => { //[ Operation, Event, result ] it.each([ - ["", "", true], - ["", Event.CREATE, false], - [Operation.CREATE, "", false], - [Operation.CREATE, Event.CREATE, true], [Operation.CREATE, Event.UPDATE, false], [Operation.CREATE, Event.DELETE, false], @@ -409,47 +405,45 @@ describe("operationMatchesEvent", () => { describe("mismatchedEvent", () => { //[ Binding, AdmissionRequest, result ] - it.each([ - // [{}, {}, false], - // [{}, { operation: Operation.CREATE }, true], - // [{ event: Event.CREATE }, {}, true], - - [{ event: Event.CREATE }, { operation: Operation.CREATE }, false], - [{ event: Event.UPDATE }, { operation: Operation.CREATE }, true], - [{ event: Event.DELETE }, { operation: Operation.CREATE }, true], - [{ event: Event.CREATE_OR_UPDATE }, { operation: Operation.CREATE }, false], - [{ event: Event.ANY }, { operation: Operation.CREATE }, false], - - [{ event: Event.CREATE }, { operation: Operation.UPDATE }, true], - [{ event: Event.UPDATE }, { operation: Operation.UPDATE }, false], - [{ event: Event.DELETE }, { operation: Operation.UPDATE }, true], - [{ event: Event.CREATE_OR_UPDATE }, { operation: Operation.UPDATE }, false], - [{ event: Event.ANY }, { operation: Operation.UPDATE }, false], - - [{ event: Event.CREATE }, { operation: Operation.DELETE }, true], - [{ event: Event.UPDATE }, { operation: Operation.DELETE }, true], - [{ event: Event.DELETE }, { operation: Operation.DELETE }, false], - [{ event: Event.CREATE_OR_UPDATE }, { operation: Operation.DELETE }, true], - [{ event: Event.ANY }, { operation: Operation.DELETE }, false], - - [{ event: Event.CREATE }, { operation: Operation.CONNECT }, true], - [{ event: Event.UPDATE }, { operation: Operation.CONNECT }, true], - [{ event: Event.DELETE }, { operation: Operation.CONNECT }, true], - [{ event: Event.CREATE_OR_UPDATE }, { operation: Operation.CONNECT }, true], - [{ event: Event.ANY }, { operation: Operation.CONNECT }, false], - ])("given binding %j and admission request %j, returns %s", (bnd, req, expected) => { - const binding: Binding = { - ...defaultBinding, - event: bnd.event, - }; - const request: AdmissionRequest = { - ...defaultAdmissionRequest, - operation: req.operation, - }; - - const result = mismatchedEvent(binding, request); - - expect(result).toEqual(expected); + describe("when called with supported Event AND Operation types", () => { + it.each([ + [{ event: Event.CREATE }, { operation: Operation.CREATE }, false], + [{ event: Event.UPDATE }, { operation: Operation.CREATE }, true], + [{ event: Event.DELETE }, { operation: Operation.CREATE }, true], + [{ event: Event.CREATE_OR_UPDATE }, { operation: Operation.CREATE }, false], + [{ event: Event.ANY }, { operation: Operation.CREATE }, false], + + [{ event: Event.CREATE }, { operation: Operation.UPDATE }, true], + [{ event: Event.UPDATE }, { operation: Operation.UPDATE }, false], + [{ event: Event.DELETE }, { operation: Operation.UPDATE }, true], + [{ event: Event.CREATE_OR_UPDATE }, { operation: Operation.UPDATE }, false], + [{ event: Event.ANY }, { operation: Operation.UPDATE }, false], + + [{ event: Event.CREATE }, { operation: Operation.DELETE }, true], + [{ event: Event.UPDATE }, { operation: Operation.DELETE }, true], + [{ event: Event.DELETE }, { operation: Operation.DELETE }, false], + [{ event: Event.CREATE_OR_UPDATE }, { operation: Operation.DELETE }, true], + [{ event: Event.ANY }, { operation: Operation.DELETE }, false], + + [{ event: Event.CREATE }, { operation: Operation.CONNECT }, true], + [{ event: Event.UPDATE }, { operation: Operation.CONNECT }, true], + [{ event: Event.DELETE }, { operation: Operation.CONNECT }, true], + [{ event: Event.CREATE_OR_UPDATE }, { operation: Operation.CONNECT }, true], + [{ event: Event.ANY }, { operation: Operation.CONNECT }, false], + ])("given binding %j and admission request %j, returns %s", (bnd, req, expected) => { + const binding: Binding = { + ...defaultBinding, + event: bnd.event, + }; + const request: AdmissionRequest = { + ...defaultAdmissionRequest, + operation: req.operation, + }; + + const result = mismatchedEvent(binding, request); + + expect(result).toEqual(expected); + }); }); }); diff --git a/src/lib/filter/adjudicators.ts b/src/lib/filter/adjudicators.ts index 3764e0948..879420930 100644 --- a/src/lib/filter/adjudicators.ts +++ b/src/lib/filter/adjudicators.ts @@ -283,8 +283,8 @@ export const misboundDeleteWithDeletionTimestamp = allPass([definesDelete, defin export const operationMatchesEvent = anyPass([ pipe(nthArg(1), equals(Event.ANY)), - pipe((operation, event) => operation === event), - pipe((operation, event) => (operation ? event.includes(operation) : false)), + pipe((operation: Operation, event: Event): boolean => operation.valueOf() === event.valueOf()), + pipe((operation: Operation, event: Event): boolean => (operation ? event.includes(operation) : false)), ]); export const mismatchedEvent = pipe( From e7f9a5b2fa521e870bd1c0ea913abf48f4c40485 Mon Sep 17 00:00:00 2001 From: Sam Mayer Date: Tue, 19 Nov 2024 09:16:40 -0600 Subject: [PATCH 12/46] Update imports --- .../adjudicators/bindingAdjudicators.test.ts | 119 ++++++++++++------ 1 file changed, 79 insertions(+), 40 deletions(-) diff --git a/src/lib/filter/adjudicators/bindingAdjudicators.test.ts b/src/lib/filter/adjudicators/bindingAdjudicators.test.ts index a47d3f507..bb8945470 100644 --- a/src/lib/filter/adjudicators/bindingAdjudicators.test.ts +++ b/src/lib/filter/adjudicators/bindingAdjudicators.test.ts @@ -2,10 +2,49 @@ // SPDX-FileCopyrightText: 2023-Present The Pepr Authors import { expect, describe, it } from "@jest/globals"; -import * as sut from "../adjudicators"; import { kind, KubernetesObject } from "kubernetes-fluent-client"; import { Binding, DeepPartial, ValidateActionResponse } from "../../types"; import { Event } from "../../enums"; +import { + bindsToNamespace, + carriedAnnotations, + carriedLabels, + carriedName, + carriedNamespace, + carriesAnnotations, + carriesLabels, + carriesName, + carriesNamespace, + definedAnnotations, + definedCallback, + definedCallbackName, + definedCategory, + definedEvent, + definedGroup, + definedKind, + definedLabels, + definedName, + definedNameRegex, + definedNamespaceRegexes, + definedNamespaces, + definedVersion, + definesAnnotations, + definesDelete, + definesDeletionTimestamp, + definesGroup, + definesKind, + definesLabels, + definesName, + definesNameRegex, + definesNamespaceRegexes, + definesNamespaces, + definesVersion, + ignoresDeletionTimestamp, + ignoresName, + misboundDeleteWithDeletionTimestamp, + misboundNamespace, + missingName, +} from "../adjudicators"; const defaultFilters = { annotations: {}, @@ -45,7 +84,7 @@ describe("definesDeletionTimestamp", () => { }, }; - const result = sut.definesDeletionTimestamp(binding); + const result = definesDeletionTimestamp(binding); expect(result).toBe(expected); }); @@ -68,7 +107,7 @@ describe("ignoresDeletionTimestamp", () => { }, }; - const result = sut.ignoresDeletionTimestamp(binding); + const result = ignoresDeletionTimestamp(binding); expect(result).toBe(expected); }); @@ -90,7 +129,7 @@ describe("definedName", () => { }, }; - const result = sut.definedName(binding); + const result = definedName(binding); expect(result).toBe(expected); }); @@ -112,7 +151,7 @@ describe("definesName", () => { }, }; - const result = sut.definesName(binding); + const result = definesName(binding); expect(result).toBe(expected); }); @@ -134,7 +173,7 @@ describe("ignoresName", () => { }, }; - const result = sut.ignoresName(binding); + const result = ignoresName(binding); expect(result).toBe(expected); }); @@ -156,7 +195,7 @@ describe("definedNameRegex", () => { }, }; - const result = sut.definedNameRegex(binding); + const result = definedNameRegex(binding); expect(result).toBe(expected); }); @@ -178,7 +217,7 @@ describe("definesNameRegex", () => { }, }; - const result = sut.definesNameRegex(binding); + const result = definesNameRegex(binding); expect(result).toBe(expected); }); @@ -203,7 +242,7 @@ describe("carriedName", () => { metadata: { name: given.metadata.name }, }; - const result = sut.carriedName(kubernetesObject); + const result = carriedName(kubernetesObject); expect(result).toBe(expected); }); @@ -222,7 +261,7 @@ describe("carriesName", () => { metadata: { name: given.metadata.name }, }; - const result = sut.carriesName(kubernetesObject); + const result = carriesName(kubernetesObject); expect(result).toBe(expected); }); @@ -241,7 +280,7 @@ describe("missingName", () => { metadata: { name: given.metadata.name }, }; - const result = sut.missingName(kubernetesObject); + const result = missingName(kubernetesObject); expect(result).toBe(expected); }); @@ -264,7 +303,7 @@ describe("bindsToNamespace", () => { kind: { kind: given.kind.kind }, }; - const result = sut.bindsToNamespace(binding); + const result = bindsToNamespace(binding); expect(result).toBe(expected); }); @@ -288,7 +327,7 @@ describe("definedNamespaces", () => { }, }; - const result = sut.definedNamespaces(binding); + const result = definedNamespaces(binding); expect(result).toEqual(expected); }); @@ -312,7 +351,7 @@ describe("definesNamespaces", () => { }, }; - const result = sut.definesNamespaces(binding); + const result = definesNamespaces(binding); expect(result).toBe(expected); }); @@ -336,7 +375,7 @@ describe("definedNamespaceRegexes", () => { }, }; - const result = sut.definedNamespaceRegexes(binding); + const result = definedNamespaceRegexes(binding); expect(result).toEqual(expected); }); @@ -360,7 +399,7 @@ describe("definesNamespaceRegexes", () => { }, }; - const result = sut.definesNamespaceRegexes(binding); + const result = definesNamespaceRegexes(binding); expect(result).toBe(expected); }); @@ -377,7 +416,7 @@ describe("carriedNamespace", () => { ])("given %j, returns %j", (given, expected) => { const binding = given as DeepPartial; - const result = sut.carriedNamespace(binding); + const result = carriedNamespace(binding); expect(result).toEqual(expected); }); @@ -397,7 +436,7 @@ describe("carriesNamespace", () => { metadata: { namespace: given.metadata.namespace }, }; - const result = sut.carriesNamespace(kubernetesObject); + const result = carriesNamespace(kubernetesObject); expect(result).toBe(expected); }); @@ -420,7 +459,7 @@ describe("misboundNamespace", () => { kind: given.kind, }; - const result = sut.misboundNamespace(binding); + const result = misboundNamespace(binding); expect(result).toBe(expected); }); @@ -444,7 +483,7 @@ describe("definedAnnotations", () => { }, }; - const result = sut.definedAnnotations(binding); + const result = definedAnnotations(binding); expect(result).toEqual(expected); }); @@ -468,7 +507,7 @@ describe("definesAnnotations", () => { }, }; - const result = sut.definesAnnotations(binding); + const result = definesAnnotations(binding); expect(result).toBe(expected); }); @@ -489,7 +528,7 @@ describe("carriedAnnotations", () => { metadata: { annotations: given.metadata.annotations }, }; - const result = sut.carriedAnnotations(kubernetesObject); + const result = carriedAnnotations(kubernetesObject); expect(result).toEqual(expected); }); @@ -510,7 +549,7 @@ describe("carriesAnnotations", () => { metadata: { annotations: given.metadata.annotations }, }; - const result = sut.carriesAnnotations(kubernetesObject); + const result = carriesAnnotations(kubernetesObject); expect(result).toBe(expected); }); @@ -534,7 +573,7 @@ describe("definedLabels", () => { }, }; - const result = sut.definedLabels(binding); + const result = definedLabels(binding); expect(result).toEqual(expected); }); @@ -558,7 +597,7 @@ describe("definesLabels", () => { }, }; - const result = sut.definesLabels(binding); + const result = definesLabels(binding); expect(result).toBe(expected); }); @@ -579,7 +618,7 @@ describe("carriedLabels", () => { metadata: { labels: given.metadata.labels }, }; - const result = sut.carriedLabels(kubernetesObject); + const result = carriedLabels(kubernetesObject); expect(result).toEqual(expected); }); @@ -600,7 +639,7 @@ describe("carriesLabels", () => { metadata: { labels: given.metadata.labels }, }; - const result = sut.carriesLabels(kubernetesObject); + const result = carriesLabels(kubernetesObject); expect(result).toBe(expected); }); @@ -622,7 +661,7 @@ describe("definedEvent", () => { event: given.event, }; - const result = sut.definedEvent(binding); + const result = definedEvent(binding); expect(result).toEqual(expected); }); @@ -644,7 +683,7 @@ describe("definesDelete", () => { event: given.event, }; - const result = sut.definesDelete(binding); + const result = definesDelete(binding); expect(result).toEqual(expected); }); @@ -667,7 +706,7 @@ describe("misboundDeleteWithDeletionTimestamp", () => { event: given.event, }; - const result = sut.misboundDeleteWithDeletionTimestamp(binding); + const result = misboundDeleteWithDeletionTimestamp(binding); expect(result).toEqual(expected); }); @@ -687,7 +726,7 @@ describe("misboundDeleteWithDeletionTimestamp", () => { event: given.event, }; - const result = sut.misboundDeleteWithDeletionTimestamp(binding); + const result = misboundDeleteWithDeletionTimestamp(binding); expect(result).toEqual(expected); }); @@ -709,7 +748,7 @@ describe("definedGroup", () => { kind: { group: given.kind.group }, }; - const result = sut.definedGroup(binding); + const result = definedGroup(binding); expect(result).toEqual(expected); }); @@ -730,7 +769,7 @@ describe("definesGroup", () => { kind: { group: given.kind.group }, }; - const result = sut.definesGroup(binding); + const result = definesGroup(binding); expect(result).toEqual(expected); }); @@ -751,7 +790,7 @@ describe("definedVersion", () => { kind: { kind: "some-kind", group: "some-group", version: given.kind.version }, }; - const result = sut.definedVersion(binding); + const result = definedVersion(binding); expect(result).toEqual(expected); }); @@ -772,7 +811,7 @@ describe("definesVersion", () => { kind: { kind: "some-kind", group: "some-group", version: given.kind.version }, }; - const result = sut.definesVersion(binding); + const result = definesVersion(binding); expect(result).toEqual(expected); }); @@ -793,7 +832,7 @@ describe("definedKind", () => { kind: { kind: given.kind.kind }, }; - const result = sut.definedKind(binding); + const result = definedKind(binding); expect(result).toEqual(expected); }); @@ -814,7 +853,7 @@ describe("definesKind", () => { kind: { kind: given.kind.kind }, }; - const result = sut.definesKind(binding); + const result = definesKind(binding); expect(result).toEqual(expected); }); @@ -834,7 +873,7 @@ describe("definedCategory", () => { ...given, }; - const result = sut.definedCategory(binding); + const result = definedCategory(binding); expect(result).toEqual(expected); }); @@ -861,7 +900,7 @@ describe("definedCallback", () => { ...given, }; - const result = sut.definedCallback(binding); + const result = definedCallback(binding); expect(result).toEqual(expected); }); @@ -888,7 +927,7 @@ describe("definedCallbackName", () => { ...given, }; - const result = sut.definedCallbackName(binding); + const result = definedCallbackName(binding); expect(result).toEqual(expected); }); From 19a7dcd2af01f811af4c43fcd103f01b6861ad7c Mon Sep 17 00:00:00 2001 From: Sam Mayer Date: Tue, 19 Nov 2024 09:19:42 -0600 Subject: [PATCH 13/46] Remove obsolete test cases now that we have typing support --- src/lib/filter/adjudicators.ts | 2 +- src/lib/filter/adjudicators/bindingAdjudicators.test.ts | 3 --- 2 files changed, 1 insertion(+), 4 deletions(-) diff --git a/src/lib/filter/adjudicators.ts b/src/lib/filter/adjudicators.ts index 879420930..c961ed030 100644 --- a/src/lib/filter/adjudicators.ts +++ b/src/lib/filter/adjudicators.ts @@ -97,7 +97,7 @@ export const carriesLabels = pipe(carriedLabels, equals({}), not); */ export const definesDeletionTimestamp = pipe( - (binding: Binding): boolean | undefined => binding?.filters?.deletionTimestamp ?? undefined, + (binding: Binding): boolean => binding?.filters?.deletionTimestamp ?? false, defaultTo(false), ); export const ignoresDeletionTimestamp = complement(definesDeletionTimestamp); diff --git a/src/lib/filter/adjudicators/bindingAdjudicators.test.ts b/src/lib/filter/adjudicators/bindingAdjudicators.test.ts index bb8945470..8cf0eb1d0 100644 --- a/src/lib/filter/adjudicators/bindingAdjudicators.test.ts +++ b/src/lib/filter/adjudicators/bindingAdjudicators.test.ts @@ -70,9 +70,6 @@ const defaultBinding: Binding = { describe("definesDeletionTimestamp", () => { //[ Binding, result ] it.each([ - // [{}, false], - // [{ filters: {} }, false], - // [{ filters: { deletionTimestamp: null } }, false], [{ filters: { deletionTimestamp: false } }, false], [{ filters: { deletionTimestamp: true } }, true], ])("given %j, returns %s", (given, expected) => { From 1eb7cde8720682f6a660ea0853480209739e123c Mon Sep 17 00:00:00 2001 From: Sam Mayer Date: Tue, 19 Nov 2024 09:26:34 -0600 Subject: [PATCH 14/46] Remove undefined return type on definedName() --- src/lib/filter/adjudicators.ts | 7 +++---- .../filter/adjudicators/bindingAdjudicators.test.ts | 10 +--------- 2 files changed, 4 insertions(+), 13 deletions(-) diff --git a/src/lib/filter/adjudicators.ts b/src/lib/filter/adjudicators.ts index c961ed030..efa50ed0f 100644 --- a/src/lib/filter/adjudicators.ts +++ b/src/lib/filter/adjudicators.ts @@ -102,10 +102,9 @@ export const definesDeletionTimestamp = pipe( ); export const ignoresDeletionTimestamp = complement(definesDeletionTimestamp); -export const definedName = pipe( - (binding: Partial): string | undefined => binding?.filters?.name, - defaultTo(""), -); +export const definedName = pipe((binding: Binding): string => { + return "name" in binding.filters ? binding.filters.name : ""; +}, defaultTo("")); export const definesName = pipe(definedName, equals(""), not); export const ignoresName = complement(definesName); diff --git a/src/lib/filter/adjudicators/bindingAdjudicators.test.ts b/src/lib/filter/adjudicators/bindingAdjudicators.test.ts index 8cf0eb1d0..163e33542 100644 --- a/src/lib/filter/adjudicators/bindingAdjudicators.test.ts +++ b/src/lib/filter/adjudicators/bindingAdjudicators.test.ts @@ -90,9 +90,6 @@ describe("definesDeletionTimestamp", () => { describe("ignoresDeletionTimestamp", () => { //[ Binding, result ] it.each([ - // [{}, true], - // [{ filters: {} }, true], - // [{ filters: { deletionTimestamp: null } }, true], [{ filters: { deletionTimestamp: false } }, true], [{ filters: { deletionTimestamp: true } }, false], ])("given %j, returns %s", (given, expected) => { @@ -112,12 +109,7 @@ describe("ignoresDeletionTimestamp", () => { describe("definedName", () => { //[ Binding, result ] - it.each([ - // [{}, ""], - // [{ filters: {} }, ""], - // [{ filters: { name: null } }, ""], - [{ filters: { name: "name" } }, "name"], - ])("given %j, returns '%s'", (given, expected) => { + it.each([[{ filters: { name: "name" } }, "name"]])("given %j, returns '%s'", (given, expected) => { const binding = { ...defaultBinding, filters: { From 9aa94e9b305c829a58120137a10f94bed54ccef6 Mon Sep 17 00:00:00 2001 From: Sam Mayer Date: Tue, 19 Nov 2024 16:36:48 -0600 Subject: [PATCH 15/46] Remove obsolete test cases now that we use typing, manage typing for regexes --- src/lib/capability.ts | 2 +- src/lib/filter/adjudicators.test.ts | 19 +- src/lib/filter/adjudicators.ts | 27 +- .../adjudicators/bindingAdjudicators.test.ts | 19 +- ...indingKubernetesObjectAdjudicators.test.ts | 40 +- src/lib/filter/filter.test.ts | 8 +- src/lib/filter/shouldSkipRequest.test.ts | 8 +- src/lib/helpers.test.ts | 763 +++++++++--------- src/lib/helpers.ts | 8 +- src/lib/types.ts | 4 +- 10 files changed, 466 insertions(+), 432 deletions(-) diff --git a/src/lib/capability.ts b/src/lib/capability.ts index 61aa26e9b..2046cd92a 100644 --- a/src/lib/capability.ts +++ b/src/lib/capability.ts @@ -347,7 +347,7 @@ export class Capability implements CapabilityExport { function InNamespaceRegex(...namespaces: RegExp[]): BindingWithName { Log.debug(`Add regex namespaces filter ${namespaces}`, prefix); - binding.filters.regexNamespaces.push(...namespaces.map(regex => regex.source)); + binding.filters.regexNamespaces.push(...namespaces); return { ...commonChain, WithName, WithNameRegex }; } diff --git a/src/lib/filter/adjudicators.test.ts b/src/lib/filter/adjudicators.test.ts index ecd3779f2..fd11636b9 100644 --- a/src/lib/filter/adjudicators.test.ts +++ b/src/lib/filter/adjudicators.test.ts @@ -53,6 +53,12 @@ const defaultAdmissionRequest = { object: {}, }; +const defaultKubernetesObject: KubernetesObject = { + apiVersion: "some-version", + kind: "some-kind", + metadata: { name: "some-name" }, +}; + describe("mismatchedName", () => { //[ Binding, KubernetesObject, result ] it.each([ @@ -61,10 +67,17 @@ describe("mismatchedName", () => { [{ filters: { name: "name" } }, {}, true], [{ filters: { name: "name" } }, { metadata: { name: "name" } }, false], ])("given binding %j and object %j, returns %s", (bnd, obj, expected) => { - const binding = bnd as DeepPartial; - const object = obj as DeepPartial; + const binding: Binding = { + ...defaultBinding, + filters: "filters" in bnd ? { ...defaultFilters, name: bnd.filters.name } : { ...defaultFilters }, + }; + + const kubernetesObject: KubernetesObject = { + ...defaultKubernetesObject, + metadata: "metadata" in obj ? obj.metadata : defaultKubernetesObject.metadata, + }; - const result = mismatchedName(binding, object); + const result = mismatchedName(binding, kubernetesObject); expect(result).toBe(expected); }); diff --git a/src/lib/filter/adjudicators.ts b/src/lib/filter/adjudicators.ts index efa50ed0f..55eb1e5a0 100644 --- a/src/lib/filter/adjudicators.ts +++ b/src/lib/filter/adjudicators.ts @@ -109,7 +109,8 @@ export const definesName = pipe(definedName, equals(""), not); export const ignoresName = complement(definesName); export const definedNameRegex = pipe( - (binding: Partial): string | undefined => binding?.filters?.regexName, + (binding: Partial): string | undefined => + typeof binding.filters?.regexName === "string" ? binding.filters.regexName : binding.filters?.regexName.source, defaultTo(""), ); export const definesNameRegex = pipe(definedNameRegex, equals(""), not); @@ -117,8 +118,12 @@ export const definesNameRegex = pipe(definedNameRegex, equals(""), not); export const definedNamespaces = pipe(binding => binding?.filters?.namespaces, defaultTo([])); export const definesNamespaces = pipe(definedNamespaces, equals([]), not); -export const definedNamespaceRegexes = pipe(binding => binding?.filters?.regexNamespaces, defaultTo([])); -export const definesNamespaceRegexes = pipe(definedNamespaceRegexes, equals([]), not); +export const definedNamespaceRegexes = pipe( + (binding: Binding): string[] => + binding.filters.regexNamespaces.map(regex => regex.toString().replace(/^\/|\/$/g, "")), + defaultTo([]), +); +export const definesNamespaceRegexes = pipe(definedNamespaceRegexes, equals([] as string[]), not); export const definedAnnotations = pipe((binding: Partial) => binding?.filters?.annotations, defaultTo({})); export const definesAnnotations = pipe(definedAnnotations, equals({}), not); @@ -200,13 +205,17 @@ export const mismatchedNamespace = allPass([ ]); export const mismatchedNamespaceRegex = allPass([ + // Check if `definesNamespaceRegexes` returns true pipe(nthArg(0), definesNamespaceRegexes), - pipe((binding, kubernetesObject) => - pipe( - any((regEx: string) => new RegExp(regEx).test(carriedNamespace(kubernetesObject))), - not, - )(definedNamespaceRegexes(binding)), - ), + + // Check if no regex matches + (binding: Binding, kubernetesObject: KubernetesObject) => { + // Convert definedNamespaceRegexes(binding) from string[] to RegExp[] + const regexArray = definedNamespaceRegexes(binding).map(regexStr => new RegExp(regexStr)); + + // Check if no regex matches the namespace of the Kubernetes object + return not(any((regEx: RegExp) => regEx.test(carriedNamespace(kubernetesObject)), regexArray)); + }, ]); export const metasMismatch = pipe( diff --git a/src/lib/filter/adjudicators/bindingAdjudicators.test.ts b/src/lib/filter/adjudicators/bindingAdjudicators.test.ts index 163e33542..af7cf860c 100644 --- a/src/lib/filter/adjudicators/bindingAdjudicators.test.ts +++ b/src/lib/filter/adjudicators/bindingAdjudicators.test.ts @@ -67,6 +67,12 @@ const defaultBinding: Binding = { isWatch: false, }; +const defaultKubernetesObject: KubernetesObject = { + apiVersion: "some-version", + kind: "some-kind", + metadata: { name: "some-name" }, +}; + describe("definesDeletionTimestamp", () => { //[ Binding, result ] it.each([ @@ -126,12 +132,7 @@ describe("definedName", () => { describe("definesName", () => { //[ Binding, result ] - it.each([ - // [{}, false], - // [{ filters: {} }, false], - // [{ filters: { name: null } }, false], - [{ filters: { name: "name" } }, true], - ])("given %j, returns %s", (given, expected) => { + it.each([[{ filters: { name: "name" } }, true]])("given %j, returns %s", (given, expected) => { const binding = { ...defaultBinding, filters: { @@ -212,12 +213,6 @@ describe("definesNameRegex", () => { }); }); -const defaultKubernetesObject: KubernetesObject = { - apiVersion: "some-version", - kind: "some-kind", - metadata: { name: "some-name" }, -}; - describe("carriedName", () => { //[ KubernetesObject, result ] it.each([ diff --git a/src/lib/filter/adjudicators/bindingKubernetesObjectAdjudicators.test.ts b/src/lib/filter/adjudicators/bindingKubernetesObjectAdjudicators.test.ts index eeeedaa50..fb02382c9 100644 --- a/src/lib/filter/adjudicators/bindingKubernetesObjectAdjudicators.test.ts +++ b/src/lib/filter/adjudicators/bindingKubernetesObjectAdjudicators.test.ts @@ -4,8 +4,36 @@ import { expect, describe, it } from "@jest/globals"; import * as sut from "../adjudicators"; -import { KubernetesObject } from "kubernetes-fluent-client"; +import { kind, KubernetesObject } from "kubernetes-fluent-client"; import { Binding, DeepPartial } from "../../types"; +import { Event } from "../../enums"; + +const defaultFilters = { + annotations: {}, + deletionTimestamp: false, + labels: {}, + name: "", + namespaces: [], + regexName: "^default$", + regexNamespaces: [], +}; +const defaultBinding: Binding = { + event: Event.ANY, + filters: defaultFilters, + kind: { kind: "some-kind", group: "some-group" }, + model: kind.Pod, + isFinalize: false, + isMutate: false, + isQueue: false, + isValidate: false, + isWatch: false, +}; + +const defaultKubernetesObject: KubernetesObject = { + apiVersion: "some-version", + kind: "some-kind", + metadata: { name: "some-name" }, +}; describe("mismatchedName", () => { //[ Binding, KubernetesObject, result ] @@ -15,8 +43,14 @@ describe("mismatchedName", () => { [{ filters: { name: "name" } }, {}, true], [{ filters: { name: "name" } }, { metadata: { name: "name" } }, false], ])("given binding %j and object %j, returns %s", (bnd, obj, expected) => { - const binding = bnd as DeepPartial; - const object = obj as DeepPartial; + const binding = { + ...defaultBinding, + filters: "filters" in bnd ? { ...defaultFilters, name: bnd.filters.name } : { ...defaultFilters }, + }; + const object = { + ...defaultKubernetesObject, + metadata: "metadata" in obj ? obj.metadata : defaultKubernetesObject.metadata, + }; const result = sut.mismatchedName(binding, object); diff --git a/src/lib/filter/filter.test.ts b/src/lib/filter/filter.test.ts index 79bd0c52a..54bb9286b 100644 --- a/src/lib/filter/filter.test.ts +++ b/src/lib/filter/filter.test.ts @@ -198,7 +198,7 @@ test("create: should not reject when regex namespace does match", () => { filters: { name: "", namespaces: [], - regexNamespaces: ["^helm"], + regexNamespaces: [RegExp("^helm")], regexName: "", labels: {}, annotations: {}, @@ -218,7 +218,7 @@ test("create: should reject when regex namespace does not match", () => { filters: { name: "", namespaces: [], - regexNamespaces: ["^argo"], + regexNamespaces: [RegExp("^argo")], regexName: "", labels: {}, annotations: {}, @@ -240,7 +240,7 @@ test("delete: should reject when regex namespace does not match", () => { filters: { name: "", namespaces: [], - regexNamespaces: ["^argo"], + regexNamespaces: [RegExp("^argo")], regexName: "", labels: {}, annotations: {}, @@ -262,7 +262,7 @@ test("delete: should not reject when regex namespace does match", () => { filters: { name: "", namespaces: [], - regexNamespaces: ["^helm"], + regexNamespaces: [RegExp("^helm")], regexName: "", labels: {}, annotations: {}, diff --git a/src/lib/filter/shouldSkipRequest.test.ts b/src/lib/filter/shouldSkipRequest.test.ts index 6cad948ba..e66c5c830 100644 --- a/src/lib/filter/shouldSkipRequest.test.ts +++ b/src/lib/filter/shouldSkipRequest.test.ts @@ -220,7 +220,7 @@ describe("when a pod is created", () => { it("should not reject when regex namespace does match", () => { const filters = { ...defaultFilters, - regexNamespaces: ["^helm"], + regexNamespaces: [RegExp("^helm")], regexName: "", }; @@ -230,7 +230,7 @@ describe("when a pod is created", () => { }); it("should reject when regex namespace does not match", () => { - const filters = { ...defaultFilters, regexNamespaces: ["^argo"] }; + const filters = { ...defaultFilters, regexNamespaces: [RegExp("^argo")] }; const binding = { ...defaultBinding, filters }; const pod = AdmissionRequestCreatePod(); expect(shouldSkipRequest(binding, pod, [])).toMatch( @@ -271,7 +271,7 @@ describe("when a pod is deleted", () => { }); it("should reject when regex namespace does not match", () => { - const filters = { ...defaultFilters, regexNamespaces: ["^argo"] }; + const filters = { ...defaultFilters, regexNamespaces: [RegExp("^argo")] }; const binding = { ...defaultBinding, filters, @@ -285,7 +285,7 @@ describe("when a pod is deleted", () => { it("should not reject when regex namespace does match", () => { const filters = { ...defaultFilters, - regexNamespaces: ["^helm"], + regexNamespaces: [RegExp("^helm")], regexName: "", labels: {}, annotations: {}, diff --git a/src/lib/helpers.test.ts b/src/lib/helpers.test.ts index a960adb2c..40afb52f7 100644 --- a/src/lib/helpers.test.ts +++ b/src/lib/helpers.test.ts @@ -27,7 +27,7 @@ import { } from "./helpers"; import { sanitizeResourceName } from "../sdk/sdk"; import * as fc from "fast-check"; -import { expect, describe, test, jest, beforeEach, afterEach } from "@jest/globals"; +import { expect, describe, test, jest, beforeEach, afterEach, it } from "@jest/globals"; import { promises as fs } from "fs"; import { SpiedFunction } from "jest-mock"; import { K8s, GenericClass, KubernetesObject, kind } from "kubernetes-fluent-client"; @@ -51,6 +51,27 @@ jest.mock("fs", () => { }; }); +const defaultFilters = { + annotations: {}, + deletionTimestamp: false, + labels: {}, + name: "", + namespaces: [], + regexName: "", + regexNamespaces: [], +}; +const defaultBinding: Binding = { + event: Event.ANY, + filters: defaultFilters, + kind: { kind: "some-kind", group: "some-group" }, + model: kind.Pod, + isFinalize: false, + isMutate: false, + isQueue: false, + isValidate: false, + isWatch: false, +}; + const mockCapabilities: CapabilityExport[] = JSON.parse(`[ { "name": "hello-pepr", @@ -517,7 +538,7 @@ describe("generateWatchNamespaceError", () => { }); }); -const nsViolation: CapabilityExport[] = JSON.parse(`[ +const namespaceViolation: CapabilityExport[] = JSON.parse(`[ { "name": "test-capability-namespaces", "description": "Should be confined to namespaces listed in capabilities and not be able to use ignored namespaces", @@ -571,34 +592,35 @@ const allNSCapabilities: CapabilityExport[] = JSON.parse(`[ } ]`); -const nonNsViolation: CapabilityExport[] = JSON.parse(`[ +const nonNsViolation: CapabilityExport[] = [ { - "name": "test-capability-namespaces", - "description": "Should be confined to namespaces listed in capabilities and not be able to use ignored namespaces", - "namespaces": [ - "miami", - "dallas", - "milwaukee" - ], - "bindings": [ - { - "kind": { - "kind": "Namespace", - "version": "v1", - "group": "" - }, - "event": "CREATE", - "filters": { - "name": "", - "namespaces": ["miami"], - "labels": {}, - "annotations": {} - }, - "isMutate": true - } - ] - } -]`); + name: "test-capability-namespaces", + description: "Should be confined to namespaces listed in capabilities and not be able to use ignored namespaces", + namespaces: ["miami", "dallas", "milwaukee"], + bindings: [ + { + kind: { + kind: "Namespace", + version: "v1", + group: "", + }, + model: kind.Pod, + event: Event.CREATE, + filters: { + name: "", + namespaces: ["miami"], + labels: {}, + annotations: {}, + deletionTimestamp: false, + regexName: "", + regexNamespaces: [], + }, + isMutate: true, + }, + ], + hasSchedule: false, + }, +]; describe("namespaceComplianceValidator", () => { let errorSpy: SpiedFunction<{ (...data: unknown[]): void; (message?: unknown, ...optionalParams: unknown[]): void }>; @@ -610,71 +632,71 @@ describe("namespaceComplianceValidator", () => { errorSpy.mockRestore(); }); test("should throw error for invalid regex namespaces", () => { - const nsViolationCapability: CapabilityExport = { + const namespaceViolationCapability: CapabilityExport = { ...nonNsViolation[0], bindings: nonNsViolation[0].bindings.map(binding => ({ ...binding, filters: { ...binding.filters, namespaces: [], - regexNamespaces: [new RegExp(/^system/).source], + regexNamespaces: [new RegExp(/^system/)], }, })), }; expect(() => { - namespaceComplianceValidator(nsViolationCapability); + namespaceComplianceValidator(namespaceViolationCapability); }).toThrowError( - `Ignoring Watch Callback: Object namespace does not match any capability namespace with regex ${nsViolationCapability.bindings[0].filters.regexNamespaces[0]}.`, + `Ignoring Watch Callback: Object namespace does not match any capability namespace with regex ${namespaceViolationCapability.bindings[0].filters.regexNamespaces[0]}.`, ); }); test("should not throw an error for valid regex namespaces", () => { - const nonnsViolationCapability: CapabilityExport = { + const nonNamespaceViolationCapability: CapabilityExport = { ...nonNsViolation[0], bindings: nonNsViolation[0].bindings.map(binding => ({ ...binding, filters: { ...binding.filters, namespaces: [], - regexNamespaces: [new RegExp(/^mia/).source], + regexNamespaces: [new RegExp(/^mia/)], }, })), }; expect(() => { - namespaceComplianceValidator(nonnsViolationCapability); + namespaceComplianceValidator(nonNamespaceViolationCapability); }).not.toThrow(); }); test("should throw error for invalid regex ignored namespaces", () => { - const nsViolationCapability: CapabilityExport = { + const namespaceViolationCapability: CapabilityExport = { ...nonNsViolation[0], bindings: nonNsViolation[0].bindings.map(binding => ({ ...binding, filters: { ...binding.filters, namespaces: [], - regexNamespaces: [new RegExp(/^mia/).source], + regexNamespaces: [new RegExp(/^mia/)], }, })), }; expect(() => { - namespaceComplianceValidator(nsViolationCapability, ["miami"]); + namespaceComplianceValidator(namespaceViolationCapability, ["miami"]); }).toThrowError( - `Ignoring Watch Callback: Regex namespace: ${nsViolationCapability.bindings[0].filters.regexNamespaces[0]}, is an ignored namespace: miami.`, + `Ignoring Watch Callback: Regex namespace: ${namespaceViolationCapability.bindings[0].filters.regexNamespaces[0]}, is an ignored namespace: miami.`, ); }); test("should not throw an error for valid regex ignored namespaces", () => { - const nonnsViolationCapability: CapabilityExport = { + const nonNamespaceViolationCapability: CapabilityExport = { ...nonNsViolation[0], bindings: nonNsViolation[0].bindings.map(binding => ({ ...binding, filters: { ...binding.filters, namespaces: [], - regexNamespaces: [new RegExp(/^mia/).source], + regexNamespaces: [new RegExp(/^mia/)], }, })), }; expect(() => { - namespaceComplianceValidator(nonnsViolationCapability, ["Seattle"]); + namespaceComplianceValidator(nonNamespaceViolationCapability, ["Seattle"]); }).not.toThrow(); }); test("should not throw an error for valid namespaces", () => { @@ -685,7 +707,7 @@ describe("namespaceComplianceValidator", () => { test("should throw an error for binding namespace using a non capability namespace", () => { try { - namespaceComplianceValidator(nsViolation[0]); + namespaceComplianceValidator(namespaceViolation[0]); } catch (e) { expect(e.message).toBe( "Error in test-capability-namespaces capability. A binding violates namespace rules. Please check ignoredNamespaces and capability namespaces: Binding uses namespace not governed by capability: bindingNamespaces: [new york] capabilityNamespaces: [miami, dallas, milwaukee].", @@ -1069,367 +1091,328 @@ describe("replaceString", () => { }); describe("filterNoMatchReason", () => { - test("returns regex namespace filter error for Pods whos namespace does not match the regex", () => { - const binding = { - kind: { kind: "Pod" }, - filters: { regexNamespaces: ["(.*)-system"], namespaces: [] }, - }; - const obj = { metadata: { namespace: "pepr-demo" } }; - const objArray = [ - { ...obj }, - { ...obj, metadata: { namespace: "pepr-uds" } }, - { ...obj, metadata: { namespace: "pepr-core" } }, - { ...obj, metadata: { namespace: "uds-ns" } }, - { ...obj, metadata: { namespace: "uds" } }, - ]; - const capabilityNamespaces: string[] = []; - objArray.map(object => { - const result = filterNoMatchReason( - binding as unknown as Partial, - object as unknown as Partial, - capabilityNamespaces, - ); - expect(result).toEqual( - `Ignoring Watch Callback: Binding defines namespace regexes '["(.*)-system"]' but Object carries '${object?.metadata?.namespace}'.`, - ); - }); - }); - - test("returns no regex namespace filter error for Pods whos namespace does match the regex", () => { - const binding = { - kind: { kind: "Pod" }, - filters: { regexNamespaces: [/(.*)-system/], namespaces: [] }, - }; - const obj = { metadata: { namespace: "pepr-demo" } }; - const objArray = [ - { ...obj, metadata: { namespace: "pepr-system" } }, - { ...obj, metadata: { namespace: "pepr-uds-system" } }, - { ...obj, metadata: { namespace: "uds-system" } }, - { ...obj, metadata: { namespace: "some-thing-that-is-a-system" } }, - { ...obj, metadata: { namespace: "your-system" } }, - ]; - const capabilityNamespaces: string[] = []; - objArray.map(object => { - const result = filterNoMatchReason( - binding as unknown as Partial, - object as unknown as Partial, - capabilityNamespaces, - ); - expect(result).toEqual(``); - }); - }); - - // Names Fail - test("returns regex name filter error for Pods whos name does not match the regex", () => { - const binding = { - kind: { kind: "Pod" }, - filters: { 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 as unknown as Partial, - object as unknown as Partial, - capabilityNamespaces, - ); + const defaultKubernetesObject: KubernetesObject = { + apiVersion: "some-version", + kind: "some-kind", + metadata: { name: "some-name" }, + }; + 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 object: KubernetesObject = obj.metadata + ? { ...defaultKubernetesObject, metadata: { ...defaultKubernetesObject, namespace: obj.metadata.namespace } } + : defaultKubernetesObject; + const binding: Binding = { + ...defaultBinding, + kind: { kind: "Pod", group: "some-group" }, + filters: { ...defaultFilters, regexNamespaces: [RegExp("(.*)-system")] }, + }; + + const capabilityNamespaces: string[] = []; + const result = filterNoMatchReason(binding, object, capabilityNamespaces); expect(result).toEqual( - `Ignoring Watch Callback: Binding defines name regex '^system' but Object carries '${object?.metadata?.name}'.`, + `Ignoring Watch Callback: Binding defines namespace regexes '["(.*)-system"]' but Object carries '${object.metadata?.namespace}'.`, ); - }); - }); + }, + ); +}); - // Names Pass - test("returns no regex name filter error for Pods whos name does match the regex", () => { - const binding = { - kind: { kind: "Pod" }, - filters: { regexName: /^system/, namespaces: [] }, - }; - 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 as unknown as Partial, - object as unknown as Partial, - capabilityNamespaces, - ); - expect(result).toEqual(``); - }); +test("returns no regex namespace filter error for Pods whos namespace does match the regex", () => { + const binding: Binding = { + ...defaultBinding, + kind: { kind: "Pod", group: "some-group" }, + filters: { ...defaultFilters, regexNamespaces: [/(.*)-system/], namespaces: [] }, + }; + const obj = { metadata: { namespace: "pepr-demo" } }; + const objArray = [ + { ...obj, metadata: { namespace: "pepr-system" } }, + { ...obj, metadata: { namespace: "pepr-uds-system" } }, + { ...obj, metadata: { namespace: "uds-system" } }, + { ...obj, metadata: { namespace: "some-thing-that-is-a-system" } }, + { ...obj, metadata: { namespace: "your-system" } }, + ]; + const capabilityNamespaces: string[] = []; + objArray.map(object => { + const result = filterNoMatchReason(binding, object as unknown as Partial, capabilityNamespaces); + expect(result).toEqual(``); }); +}); - test("returns missingCarriableNamespace filter error for cluster-scoped objects when capability namespaces are present", () => { - const binding = { - kind: { kind: "ClusterRole" }, - }; - const obj = { - kind: "ClusterRole", - apiVersion: "rbac.authorization.k8s.io/v1", - metadata: { name: "clusterrole1" }, - }; - const capabilityNamespaces: string[] = ["monitoring"]; - const result = filterNoMatchReason( - binding as unknown as Partial, - obj as unknown as Partial, - capabilityNamespaces, - ); +// Names Fail +test("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: Object does not carry a namespace but namespaces allowed by Capability are '[\"monitoring\"]'.", + `Ignoring Watch Callback: Binding defines name regex '^system' but Object carries '${object?.metadata?.name}'.`, ); }); +}); - test("returns mismatchedNamespace filter error for clusterScoped objects with namespace filters", () => { - const binding = { - kind: { kind: "ClusterRole" }, - filters: { namespaces: ["ns1"] }, - }; - const obj = { - kind: "ClusterRole", - apiVersion: "rbac.authorization.k8s.io/v1", - metadata: { name: "clusterrole1" }, - }; - const capabilityNamespaces: string[] = []; - const result = filterNoMatchReason( - binding as unknown as Partial, - obj as unknown as Partial, - capabilityNamespaces, - ); - expect(result).toEqual("Ignoring Watch Callback: Binding defines namespaces '[\"ns1\"]' but Object carries ''."); +// Names Pass +test("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(``); }); +}); - test("returns namespace filter error for namespace objects with namespace filters", () => { - const binding = { - kind: { kind: "Namespace" }, - filters: { namespaces: ["ns1"] }, - }; - const obj = {}; - const capabilityNamespaces: string[] = []; - const result = filterNoMatchReason( - binding as unknown as Partial, - obj as unknown as Partial, - capabilityNamespaces, - ); - expect(result).toEqual("Ignoring Watch Callback: Cannot use namespace filter on a namespace object."); - }); +test("returns missingCarriableNamespace filter error for cluster-scoped objects when capability namespaces are present", () => { + const binding: Binding = { + ...defaultBinding, + kind: { kind: "ClusterRole", group: "some-group" }, + }; + const obj = { + kind: "ClusterRole", + apiVersion: "rbac.authorization.k8s.io/v1", + metadata: { name: "clusterrole1" }, + }; + const capabilityNamespaces: string[] = ["monitoring"]; + const result = filterNoMatchReason(binding, obj as unknown as Partial, capabilityNamespaces); + expect(result).toEqual( + "Ignoring Watch Callback: Object does not carry a namespace but namespaces allowed by Capability are '[\"monitoring\"]'.", + ); +}); - test("return an Ignoring Watch Callback string if the binding name and object name are different", () => { - const binding = { - filters: { name: "pepr" }, - }; - const obj = { - metadata: { - name: "not-pepr", - }, - }; - const capabilityNamespaces: string[] = []; - const result = filterNoMatchReason( - binding as unknown as Partial, - obj as unknown as Partial, - capabilityNamespaces, - ); - expect(result).toEqual(`Ignoring Watch Callback: Binding defines name 'pepr' but Object carries 'not-pepr'.`); - }); - test("returns no Ignoring Watch Callback string if the binding name and object name are the same", () => { - const binding = { - filters: { name: "pepr" }, - }; - const obj = { - metadata: { name: "pepr" }, - }; - const capabilityNamespaces: string[] = []; - const result = filterNoMatchReason( - binding as unknown as Partial, - obj as unknown as Partial, - capabilityNamespaces, - ); - expect(result).toEqual(""); - }); +test("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 ''."); +}); - test("return deletionTimestamp error when there is no deletionTimestamp in the object", () => { - const binding = { - filters: { deletionTimestamp: true }, - }; - const obj = { - metadata: {}, - }; - const capabilityNamespaces: string[] = []; - const result = filterNoMatchReason( - binding as unknown as Partial, - obj as unknown as Partial, - capabilityNamespaces, - ); - expect(result).toEqual("Ignoring Watch Callback: Binding defines deletionTimestamp but Object does not carry it."); - }); +test("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."); +}); - test("return no deletionTimestamp error when there is a deletionTimestamp in the object", () => { - const binding = { - filters: { deletionTimestamp: true }, - }; - const obj = { - metadata: { - deletionTimestamp: "2021-01-01T00:00:00Z", - }, - }; - const capabilityNamespaces: string[] = []; - const result = filterNoMatchReason( - binding as unknown as Partial, - obj as unknown as Partial, - capabilityNamespaces, - ); - expect(result).not.toEqual("Ignoring Watch Callback: Binding defines deletionTimestamp Object does not carry it."); - }); +test("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'.`); +}); +test("returns no Ignoring Watch Callback string if the binding name and object name are the same", () => { + const binding: Binding = { + ...defaultBinding, + filters: { ...defaultFilters, name: "pepr" }, + }; + const obj = { + metadata: { name: "pepr" }, + }; + const capabilityNamespaces: string[] = []; + const result = filterNoMatchReason(binding, obj as unknown as Partial, capabilityNamespaces); + expect(result).toEqual(""); +}); - test("returns label overlap error when there is no overlap between binding and object labels", () => { - const binding = { - filters: { labels: { key: "value" } }, - }; - const obj = { - metadata: { labels: { anotherKey: "anotherValue" } }, - }; - const capabilityNamespaces: string[] = []; - const result = filterNoMatchReason( - binding as unknown as Partial, - obj as unknown as Partial, - capabilityNamespaces, - ); - expect(result).toEqual( - `Ignoring Watch Callback: Binding defines labels '{"key":"value"}' but Object carries '{"anotherKey":"anotherValue"}'.`, - ); - }); +test("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."); +}); - test("returns annotation overlap error when there is no overlap between binding and object annotations", () => { - const binding = { - filters: { annotations: { key: "value" } }, - }; - const obj = { - metadata: { annotations: { anotherKey: "anotherValue" } }, - }; - const capabilityNamespaces: string[] = []; - const result = filterNoMatchReason( - binding as unknown as Partial, - obj as unknown as Partial, - capabilityNamespaces, - ); - expect(result).toEqual( - `Ignoring Watch Callback: Binding defines annotations '{"key":"value"}' but Object carries '{"anotherKey":"anotherValue"}'.`, - ); - }); +test("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."); +}); - test("returns capability namespace error when object is not in capability namespaces", () => { - const binding = { - model: kind.Pod, - event: Event.ANY, - kind: { - group: "", - version: "v1", - kind: "Pod", - }, - filters: { - name: "bleh", - namespaces: [], - regexNamespaces: [], - regexName: "", - labels: {}, - annotations: {}, - deletionTimestamp: false, - }, - callback, - }; +test("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"}'.`, + ); +}); - const obj = { - metadata: { namespace: "ns2", name: "bleh" }, - }; - const capabilityNamespaces = ["ns1"]; - const result = filterNoMatchReason( - binding as Binding, - obj as unknown as Partial, - capabilityNamespaces, - ); - expect(result).toEqual( - `Ignoring Watch Callback: Object carries namespace 'ns2' but namespaces allowed by Capability are '["ns1"]'.`, - ); - }); +test("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"}'.`, + ); +}); - test("returns binding namespace error when filter namespace is not part of capability namespaces", () => { - const binding = { - filters: { namespaces: ["ns3"], regexNamespaces: [] }, - }; - const obj = {}; - const capabilityNamespaces = ["ns1", "ns2"]; - const result = filterNoMatchReason( - binding as unknown as Partial, - obj as unknown as Partial, - capabilityNamespaces, - ); - expect(result).toEqual( - `Ignoring Watch Callback: Binding defines namespaces ["ns3"] but namespaces allowed by Capability are '["ns1","ns2"]'.`, - ); - }); +test("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, + }; - test("returns binding and object namespace error when they do not overlap", () => { - const binding = { - filters: { namespaces: ["ns1"], regexNamespaces: [] }, - }; - const obj = { - metadata: { namespace: "ns2" }, - }; - const capabilityNamespaces = ["ns1", "ns2"]; - const result = filterNoMatchReason( - binding as unknown as Partial, - obj as unknown as Partial, - capabilityNamespaces, - ); - expect(result).toEqual(`Ignoring Watch Callback: Binding defines namespaces '["ns1"]' but Object carries 'ns2'.`); - }); + const obj = { + metadata: { namespace: "ns2", name: "bleh" }, + }; + const capabilityNamespaces = ["ns1"]; + const result = filterNoMatchReason( + binding as Binding, + obj as unknown as Partial, + capabilityNamespaces, + ); + expect(result).toEqual( + `Ignoring Watch Callback: Object carries namespace 'ns2' but namespaces allowed by Capability are '["ns1"]'.`, + ); +}); - test("return watch violation message when object is in an ignored namespace", () => { - const binding = { - filters: { namespaces: ["ns3"] }, - }; - const obj = { - metadata: { namespace: "ns3" }, - }; - const capabilityNamespaces = ["ns3"]; - const ignoredNamespaces = ["ns3"]; - const result = filterNoMatchReason( - binding as unknown as Partial, - obj as unknown as Partial, - capabilityNamespaces, - ignoredNamespaces, - ); - expect(result).toEqual( - `Ignoring Watch Callback: Object carries namespace 'ns3' but ignored namespaces include '["ns3"]'.`, - ); - }); +test("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"]'.`, + ); +}); - test("returns empty string when all checks pass", () => { - const binding = { - filters: { 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 as unknown as Partial, - obj as unknown as Partial, - capabilityNamespaces, - ); - expect(result).toEqual(""); - }); +test("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'.`); +}); + +test("return watch violation message when object is in an ignored namespace", () => { + const binding: Binding = { + ...defaultBinding, + filters: { ...defaultFilters, namespaces: ["ns3"] }, + }; + const obj = { + metadata: { namespace: "ns3" }, + }; + const capabilityNamespaces = ["ns3"]; + const ignoredNamespaces = ["ns3"]; + const result = filterNoMatchReason( + binding, + obj as unknown as Partial, + capabilityNamespaces, + ignoredNamespaces, + ); + expect(result).toEqual( + `Ignoring Watch Callback: Object carries namespace 'ns3' but ignored namespaces include '["ns3"]'.`, + ); +}); + +test("returns empty string when all checks pass", () => { + const binding: Binding = { + ...defaultBinding, + filters: { ...defaultFilters, 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", () => { @@ -1468,21 +1451,21 @@ describe("matchesRegex", () => { test("should return true for a valid pattern that matches the string", () => { const pattern = /abc/; const testString = "abc123"; - const result = matchesRegex(new RegExp(pattern).source, testString); + const result = matchesRegex(new RegExp(pattern), testString); expect(result).toBe(true); }); test("should return false for a valid pattern that does not match the string", () => { const pattern = /xyz/; const testString = "abc123"; - const result = matchesRegex(new RegExp(pattern).source, testString); + const result = matchesRegex(new RegExp(pattern), testString); expect(result).toBe(false); }); test("should return false for an invalid regex pattern", () => { const invalidPattern = new RegExp(/^p/); // Invalid regex with unclosed bracket const testString = "test"; - const result = matchesRegex(invalidPattern.source, testString); + const result = matchesRegex(invalidPattern, testString); expect(result).toBe(false); }); @@ -1499,28 +1482,28 @@ describe("matchesRegex", () => { test("should return true for an empty string matching an empty regex", () => { const pattern = new RegExp(""); const testString = ""; - const result = matchesRegex(new RegExp(pattern).source, testString); + const result = matchesRegex(new RegExp(pattern), testString); expect(result).toBe(true); }); test("should return false for an empty string and a non-empty regex", () => { const pattern = new RegExp("abc"); const testString = ""; - const result = matchesRegex(new RegExp(pattern).source, testString); + const result = matchesRegex(new RegExp(pattern), testString); expect(result).toBe(false); }); test("should return true for a complex valid regex that matches", () => { const pattern = /^[a-zA-Z0-9]+@[a-zA-Z0-9]+\.[A-Za-z]+$/; const testString = "test@example.com"; - const result = matchesRegex(new RegExp(pattern).source, testString); + const result = matchesRegex(new RegExp(pattern), testString); expect(result).toBe(true); }); test("should return false for a complex valid regex that does not match", () => { const pattern = /^[a-zA-Z0-9]+@[a-zA-Z0-9]+\.[A-Za-z]+$/; const testString = "invalid-email.com"; - const result = matchesRegex(new RegExp(pattern).source, testString); + const result = matchesRegex(new RegExp(pattern), testString); expect(result).toBe(false); }); }); diff --git a/src/lib/helpers.ts b/src/lib/helpers.ts index 79c76fd26..1cd9fb350 100644 --- a/src/lib/helpers.ts +++ b/src/lib/helpers.ts @@ -32,7 +32,7 @@ import { uncarryableNamespace, } from "./filter/adjudicators"; -export function matchesRegex(pattern: string, testString: string): boolean { +export function matchesRegex(pattern: RegExp, testString: string): boolean { // edge-case if (!pattern) { return false; @@ -74,7 +74,7 @@ export type RBACMap = { * Decide to run callback after the event comes back from API Server **/ export function filterNoMatchReason( - binding: Partial, + binding: Binding, object: Partial, capabilityNamespaces: string[], ignoredNamespaces?: string[], @@ -256,7 +256,7 @@ export function generateWatchNamespaceError( return err.replace(/\.([^ ])/g, ". $1"); } -// namespaceComplianceValidator ensures that capability bindinds respect ignored and capability namespaces +// namespaceComplianceValidator ensures that capability binds respect ignored and capability namespaces export function namespaceComplianceValidator(capability: CapabilityExport, ignoredNamespaces?: string[]) { const { namespaces: capabilityNamespaces, bindings, name } = capability; const bindingNamespaces = bindings.flatMap((binding: Binding) => binding.filters.namespaces); @@ -284,7 +284,7 @@ export function namespaceComplianceValidator(capability: CapabilityExport, ignor for (const regexNamespace of bindingRegexNamespaces) { let matches = false; matches = - regexNamespace !== "" && + regexNamespace.source !== "" && capabilityNamespaces.some(capabilityNamespace => matchesRegex(regexNamespace, capabilityNamespace)); if (!matches) { throw new Error( diff --git a/src/lib/types.ts b/src/lib/types.ts index 7ce5a0a8a..381c49f25 100644 --- a/src/lib/types.ts +++ b/src/lib/types.ts @@ -77,8 +77,8 @@ export type Filters = { labels: Record; name: string; namespaces: string[]; - regexName: string; - regexNamespaces: string[]; + regexName: string | RegExp; + regexNamespaces: RegExp[]; // TODO: Test the Regexp path?? }; export type Binding = { From 6fb930a0f52fccf631697e8aa6a437e3d5bdf34c Mon Sep 17 00:00:00 2001 From: Sam Mayer Date: Tue, 19 Nov 2024 16:49:10 -0600 Subject: [PATCH 16/46] Update imports --- ...indingKubernetesObjectAdjudicators.test.ts | 38 ++++++++++++------- 1 file changed, 25 insertions(+), 13 deletions(-) diff --git a/src/lib/filter/adjudicators/bindingKubernetesObjectAdjudicators.test.ts b/src/lib/filter/adjudicators/bindingKubernetesObjectAdjudicators.test.ts index fb02382c9..f3f9a8f08 100644 --- a/src/lib/filter/adjudicators/bindingKubernetesObjectAdjudicators.test.ts +++ b/src/lib/filter/adjudicators/bindingKubernetesObjectAdjudicators.test.ts @@ -3,10 +3,19 @@ // SPDX-FileCopyrightText: 2023-Present The Pepr Authors import { expect, describe, it } from "@jest/globals"; -import * as sut from "../adjudicators"; import { kind, KubernetesObject } from "kubernetes-fluent-client"; import { Binding, DeepPartial } from "../../types"; import { Event } from "../../enums"; +import { + mismatchedName, + mismatchedDeletionTimestamp, + mismatchedNameRegex, + mismatchedNamespace, + mismatchedNamespaceRegex, + mismatchedAnnotations, + mismatchedLabels, + metasMismatch, +} from "../adjudicators"; const defaultFilters = { annotations: {}, @@ -52,7 +61,7 @@ describe("mismatchedName", () => { metadata: "metadata" in obj ? obj.metadata : defaultKubernetesObject.metadata, }; - const result = sut.mismatchedName(binding, object); + const result = mismatchedName(binding, object); expect(result).toBe(expected); }); @@ -69,7 +78,7 @@ describe("mismatchedDeletionTimestamp", () => { const binding = bnd as DeepPartial; const object = obj as DeepPartial; - const result = sut.mismatchedDeletionTimestamp(binding, object); + const result = mismatchedDeletionTimestamp(binding, object); expect(result).toBe(expected); }); @@ -91,7 +100,7 @@ describe("mismatchedNameRegex", () => { const binding = bnd as DeepPartial; const object = obj as DeepPartial; - const result = sut.mismatchedNameRegex(binding, object); + const result = mismatchedNameRegex(binding, object); expect(result).toBe(expected); }); @@ -109,7 +118,7 @@ describe("mismatchedNamespace", () => { const binding = bnd as DeepPartial; const object = obj as DeepPartial; - const result = sut.mismatchedNamespace(binding, object); + const result = mismatchedNamespace(binding, object); expect(result).toBe(expected); }); @@ -118,8 +127,8 @@ describe("mismatchedNamespace", () => { describe("mismatchedNamespaceRegex", () => { //[ Binding, KubernetesObject, result ] it.each([ - [{}, {}, false], - [{}, { metadata: { namespace: "namespace" } }, false], + // [{}, {}, false], + // [{}, { metadata: { namespace: "namespace" } }, false], [{ filters: { regexNamespaces: ["^n.mespace$"] } }, {}, true], [{ filters: { regexNamespaces: ["^n[aeiou]mespace$"] } }, { metadata: { namespace: "namespace" } }, false], @@ -137,10 +146,13 @@ describe("mismatchedNamespaceRegex", () => { true, ], ])("given binding %j and object %j, returns %s", (bnd, obj, expected) => { - const binding = bnd as DeepPartial; - const object = obj as DeepPartial; + const binding = { ...defaultBinding, filters: { ...defaultFilters, regexNamespace: bnd.filters.regexNamespaces } }; + const object = { + ...defaultKubernetesObject, + metadata: "metadata" in obj ? obj.metadata : defaultKubernetesObject.metadata, + }; - const result = sut.mismatchedNamespaceRegex(binding, object); + const result = mismatchedNamespaceRegex(binding, object); expect(result).toBe(expected); }); @@ -174,7 +186,7 @@ describe("mismatchedAnnotations", () => { const binding = bnd as DeepPartial; const object = obj as DeepPartial; - const result = sut.mismatchedAnnotations(binding, object); + const result = mismatchedAnnotations(binding, object); expect(result).toBe(expected); }); @@ -202,7 +214,7 @@ describe("mismatchedLabels", () => { const binding = bnd as DeepPartial; const object = obj as DeepPartial; - const result = sut.mismatchedLabels(binding, object); + const result = mismatchedLabels(binding, object); expect(result).toBe(expected); }); @@ -228,7 +240,7 @@ describe("metasMismatch", () => { [{ an: "no", ta: "te" }, { an: "no", ta: "te" }, false], [{ an: "no", ta: "te" }, { an: "no", ta: "to" }, true], ])("given left %j and right %j, returns %s", (bnd, obj, expected) => { - const result = sut.metasMismatch(bnd, obj); + const result = metasMismatch(bnd, obj); expect(result).toBe(expected); }); From 4c78fe0f27c964fa64d649f875d68077a6652e3d Mon Sep 17 00:00:00 2001 From: Sam Mayer Date: Wed, 20 Nov 2024 09:48:01 -0600 Subject: [PATCH 17/46] Use regexes in test data to match typing --- src/lib/filter/adjudicators.test.ts | 38 +++++++++++-------- .../adjudicators/bindingAdjudicators.test.ts | 12 +++--- ...indingKubernetesObjectAdjudicators.test.ts | 27 +++++++------ 3 files changed, 43 insertions(+), 34 deletions(-) diff --git a/src/lib/filter/adjudicators.test.ts b/src/lib/filter/adjudicators.test.ts index fd11636b9..348c11e97 100644 --- a/src/lib/filter/adjudicators.test.ts +++ b/src/lib/filter/adjudicators.test.ts @@ -148,27 +148,33 @@ describe("mismatchedNamespace", () => { describe("mismatchedNamespaceRegex", () => { //[ Binding, KubernetesObject, result ] it.each([ - [{}, {}, false], - [{}, { metadata: { namespace: "namespace" } }, false], - [{ filters: { regexNamespaces: ["^n.mespace$"] } }, {}, true], - - [{ filters: { regexNamespaces: ["^n[aeiou]mespace$"] } }, { metadata: { namespace: "namespace" } }, false], - [{ filters: { regexNamespaces: ["^n[aeiou]mespace$"] } }, { metadata: { namespace: "nemespace" } }, false], - [{ filters: { regexNamespaces: ["^n[aeiou]mespace$"] } }, { metadata: { namespace: "nimespace" } }, false], - [{ filters: { regexNamespaces: ["^n[aeiou]mespace$"] } }, { metadata: { namespace: "nomespace" } }, false], - [{ filters: { regexNamespaces: ["^n[aeiou]mespace$"] } }, { metadata: { namespace: "numespace" } }, false], - [{ filters: { regexNamespaces: ["^n[aeiou]mespace$"] } }, { metadata: { namespace: "n3mespace" } }, true], - - [{ filters: { regexNamespaces: ["^n[aeiou]me$", "^sp[aeiou]ce$"] } }, { metadata: { namespace: "name" } }, false], - [{ filters: { regexNamespaces: ["^n[aeiou]me$", "^sp[aeiou]ce$"] } }, { metadata: { namespace: "space" } }, false], + // [{}, {}, false], + // [{}, { metadata: { namespace: "namespace" } }, false], + [{ filters: { regexNamespaces: [/^n.mespace$/] } }, {}, true], + + [{ filters: { regexNamespaces: [/^n[aeiou]mespace$/] } }, { metadata: { namespace: "namespace" } }, false], + [{ filters: { regexNamespaces: [/^n[aeiou]mespace$/] } }, { metadata: { namespace: "nemespace" } }, false], + [{ filters: { regexNamespaces: [/^n[aeiou]mespace$/] } }, { metadata: { namespace: "nimespace" } }, false], + [{ filters: { regexNamespaces: [/^n[aeiou]mespace$/] } }, { metadata: { namespace: "nomespace" } }, false], + [{ filters: { regexNamespaces: [/^n[aeiou]mespace$/] } }, { metadata: { namespace: "numespace" } }, false], + [{ filters: { regexNamespaces: [/^n[aeiou]mespace$/] } }, { metadata: { namespace: "n3mespace" } }, true], + + [{ filters: { regexNamespaces: [/^n[eeiou]me$/, /^sp[aeiou]ce$/] } }, { metadata: { namespace: "name" } }, false], + [{ filters: { regexNamespaces: [/^n[aeiou]me$/, /^sp[aeiou]ce$/] } }, { metadata: { namespace: "space" } }, false], [ - { filters: { regexNamespaces: ["^n[aeiou]me$", "^sp[aeiou]ce$"] } }, + { filters: { regexNamespaces: [/^n[aeiou]me$/, /^sp[aeiou]ce$/] } }, { metadata: { namespace: "namespace" } }, true, ], ])("given binding %j and object %j, returns %s", (bnd, obj, expected) => { - const binding = bnd as DeepPartial; - const object = obj as DeepPartial; + const binding: Binding = { + ...defaultBinding, + filters: { ...defaultFilters, regexNamespaces: bnd.filters.regexNamespaces }, + }; + const object: KubernetesObject = { + ...defaultKubernetesObject, + metadata: "metadata" in obj ? obj.metadata : defaultKubernetesObject.metadata, + }; const result = mismatchedNamespaceRegex(binding, object); diff --git a/src/lib/filter/adjudicators/bindingAdjudicators.test.ts b/src/lib/filter/adjudicators/bindingAdjudicators.test.ts index af7cf860c..304ab6532 100644 --- a/src/lib/filter/adjudicators/bindingAdjudicators.test.ts +++ b/src/lib/filter/adjudicators/bindingAdjudicators.test.ts @@ -346,10 +346,10 @@ describe("definedNamespaceRegexes", () => { it.each([ // [{}, []], // [{ filters: {} }, []], - [{ filters: { regexNamespaces: null } }, []], + // [{ filters: { regexNamespaces: null } }, []], [{ filters: { regexNamespaces: [] } }, []], - [{ filters: { regexNamespaces: ["n.mesp.ce"] } }, ["n.mesp.ce"]], - [{ filters: { regexNamespaces: ["n.me", "sp.ce"] } }, ["n.me", "sp.ce"]], + [{ filters: { regexNamespaces: [/n.mesp.ce/] } }, ["n.mesp.ce"]], + [{ filters: { regexNamespaces: [/n.me/, /sp.ce/] } }, ["n.me", "sp.ce"]], ])("given %j, returns %j", (given, expected) => { const binding = { ...defaultBinding, @@ -370,10 +370,10 @@ describe("definesNamespaceRegexes", () => { it.each([ // [{}, false], // [{ filters: {} }, false], - [{ filters: { regexNamespaces: null } }, false], + // [{ filters: { regexNamespaces: null } }, false], [{ filters: { regexNamespaces: [] } }, false], - [{ filters: { regexNamespaces: ["n.mesp.ce"] } }, true], - [{ filters: { regexNamespaces: ["n.me", "sp.ce"] } }, true], + [{ filters: { regexNamespaces: [/n.mesp.ce/] } }, true], + [{ filters: { regexNamespaces: [/n.me/, /sp.ce/] } }, true], ])("given %j, returns %s", (given, expected) => { const binding = { ...defaultBinding, diff --git a/src/lib/filter/adjudicators/bindingKubernetesObjectAdjudicators.test.ts b/src/lib/filter/adjudicators/bindingKubernetesObjectAdjudicators.test.ts index f3f9a8f08..3a28f4c98 100644 --- a/src/lib/filter/adjudicators/bindingKubernetesObjectAdjudicators.test.ts +++ b/src/lib/filter/adjudicators/bindingKubernetesObjectAdjudicators.test.ts @@ -129,25 +129,28 @@ describe("mismatchedNamespaceRegex", () => { it.each([ // [{}, {}, false], // [{}, { metadata: { namespace: "namespace" } }, false], - [{ filters: { regexNamespaces: ["^n.mespace$"] } }, {}, true], + [{ filters: { regexNamespaces: [/^n.mespace$/] } }, {}, true], - [{ filters: { regexNamespaces: ["^n[aeiou]mespace$"] } }, { metadata: { namespace: "namespace" } }, false], - [{ filters: { regexNamespaces: ["^n[aeiou]mespace$"] } }, { metadata: { namespace: "nemespace" } }, false], - [{ filters: { regexNamespaces: ["^n[aeiou]mespace$"] } }, { metadata: { namespace: "nimespace" } }, false], - [{ filters: { regexNamespaces: ["^n[aeiou]mespace$"] } }, { metadata: { namespace: "nomespace" } }, false], - [{ filters: { regexNamespaces: ["^n[aeiou]mespace$"] } }, { metadata: { namespace: "numespace" } }, false], - [{ filters: { regexNamespaces: ["^n[aeiou]mespace$"] } }, { metadata: { namespace: "n3mespace" } }, true], + [{ filters: { regexNamespaces: [/^n[aeiou]mespace$/] } }, { metadata: { namespace: "namespace" } }, false], + [{ filters: { regexNamespaces: [/^n[aeiou]mespace$/] } }, { metadata: { namespace: "nemespace" } }, false], + [{ filters: { regexNamespaces: [/^n[aeiou]mespace$/] } }, { metadata: { namespace: "nimespace" } }, false], + [{ filters: { regexNamespaces: [/^n[aeiou]mespace$/] } }, { metadata: { namespace: "nomespace" } }, false], + [{ filters: { regexNamespaces: [/^n[aeiou]mespace$/] } }, { metadata: { namespace: "numespace" } }, false], + [{ filters: { regexNamespaces: [/^n[aeiou]mespace$/] } }, { metadata: { namespace: "n3mespace" } }, true], - [{ filters: { regexNamespaces: ["^n[aeiou]me$", "^sp[aeiou]ce$"] } }, { metadata: { namespace: "name" } }, false], - [{ filters: { regexNamespaces: ["^n[aeiou]me$", "^sp[aeiou]ce$"] } }, { metadata: { namespace: "space" } }, false], + [{ filters: { regexNamespaces: [/^n[aeiou]me$/, /^sp[aeiou]ce$/] } }, { metadata: { namespace: "name" } }, false], + [{ filters: { regexNamespaces: [/^n[aeiou]me$/, /^sp[aeiou]ce$/] } }, { metadata: { namespace: "space" } }, false], [ - { filters: { regexNamespaces: ["^n[aeiou]me$", "^sp[aeiou]ce$"] } }, + { filters: { regexNamespaces: [/^n[aeiou]me$/, /^sp[aeiou]ce$/] } }, { metadata: { namespace: "namespace" } }, true, ], ])("given binding %j and object %j, returns %s", (bnd, obj, expected) => { - const binding = { ...defaultBinding, filters: { ...defaultFilters, regexNamespace: bnd.filters.regexNamespaces } }; - const object = { + const binding: Binding = { + ...defaultBinding, + filters: { ...defaultFilters, regexNamespaces: bnd.filters.regexNamespaces }, + }; + const object: KubernetesObject = { ...defaultKubernetesObject, metadata: "metadata" in obj ? obj.metadata : defaultKubernetesObject.metadata, }; From dd2f84d09a24f5d3551ef11d82f03cfbc9e78223 Mon Sep 17 00:00:00 2001 From: Sam Mayer Date: Wed, 20 Nov 2024 09:48:32 -0600 Subject: [PATCH 18/46] Update test case to handle typing with a null test case --- src/lib/helpers.test.ts | 16 +++++++++++----- 1 file changed, 11 insertions(+), 5 deletions(-) diff --git a/src/lib/helpers.test.ts b/src/lib/helpers.test.ts index 40afb52f7..4d32549a7 100644 --- a/src/lib/helpers.test.ts +++ b/src/lib/helpers.test.ts @@ -1105,9 +1105,12 @@ describe("filterNoMatchReason", () => { ])( "given %j, it returns regex namespace filter error for Pods whose namespace does not match the regex", (obj: KubernetesObject) => { - const object: KubernetesObject = obj.metadata - ? { ...defaultKubernetesObject, metadata: { ...defaultKubernetesObject, namespace: obj.metadata.namespace } } - : defaultKubernetesObject; + 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" }, @@ -1115,9 +1118,12 @@ describe("filterNoMatchReason", () => { }; const capabilityNamespaces: string[] = []; - const result = filterNoMatchReason(binding, object, capabilityNamespaces); + const expectedErrorMessage = `Ignoring Watch Callback: Binding defines namespace regexes '["(.*)-system"]' but Object carries`; + const result = filterNoMatchReason(binding, kubernetesObject, capabilityNamespaces); expect(result).toEqual( - `Ignoring Watch Callback: Binding defines namespace regexes '["(.*)-system"]' but Object carries '${object.metadata?.namespace}'.`, + typeof kubernetesObject.metadata === "object" && obj !== null && Object.keys(obj).length > 0 + ? `${expectedErrorMessage} '${kubernetesObject.metadata.namespace}'.` + : `${expectedErrorMessage} ''.`, ); }, ); From eaf481f6c525cc09e397834aa6940696ba6b193f Mon Sep 17 00:00:00 2001 From: Sam Mayer Date: Wed, 20 Nov 2024 11:12:51 -0600 Subject: [PATCH 19/46] Resolve test failure for mismatchedNamespaceRegex() --- src/lib/filter/adjudicators.test.ts | 30 ++++++++++++++--------------- src/lib/filter/adjudicators.ts | 3 ++- 2 files changed, 16 insertions(+), 17 deletions(-) diff --git a/src/lib/filter/adjudicators.test.ts b/src/lib/filter/adjudicators.test.ts index 348c11e97..818b217d4 100644 --- a/src/lib/filter/adjudicators.test.ts +++ b/src/lib/filter/adjudicators.test.ts @@ -147,36 +147,34 @@ describe("mismatchedNamespace", () => { describe("mismatchedNamespaceRegex", () => { //[ Binding, KubernetesObject, result ] + const testRegex1 = /^n[aeiou]mespace$/; + const testRegex2 = /^n[aeiou]me$/; + const testRegex3 = /^sp[aeiou]ce$/; it.each([ // [{}, {}, false], // [{}, { metadata: { namespace: "namespace" } }, false], [{ filters: { regexNamespaces: [/^n.mespace$/] } }, {}, true], - [{ filters: { regexNamespaces: [/^n[aeiou]mespace$/] } }, { metadata: { namespace: "namespace" } }, false], - [{ filters: { regexNamespaces: [/^n[aeiou]mespace$/] } }, { metadata: { namespace: "nemespace" } }, false], - [{ filters: { regexNamespaces: [/^n[aeiou]mespace$/] } }, { metadata: { namespace: "nimespace" } }, false], - [{ filters: { regexNamespaces: [/^n[aeiou]mespace$/] } }, { metadata: { namespace: "nomespace" } }, false], - [{ filters: { regexNamespaces: [/^n[aeiou]mespace$/] } }, { metadata: { namespace: "numespace" } }, false], - [{ filters: { regexNamespaces: [/^n[aeiou]mespace$/] } }, { metadata: { namespace: "n3mespace" } }, true], + [{ filters: { regexNamespaces: [testRegex1] } }, { metadata: { namespace: "namespace" } }, false], + [{ filters: { regexNamespaces: [testRegex1] } }, { metadata: { namespace: "nemespace" } }, false], + [{ filters: { regexNamespaces: [testRegex1] } }, { metadata: { namespace: "nimespace" } }, false], + [{ filters: { regexNamespaces: [testRegex1] } }, { metadata: { namespace: "nomespace" } }, false], + [{ filters: { regexNamespaces: [testRegex1] } }, { metadata: { namespace: "numespace" } }, false], + [{ filters: { regexNamespaces: [testRegex1] } }, { metadata: { namespace: "n3mespace" } }, true], - [{ filters: { regexNamespaces: [/^n[eeiou]me$/, /^sp[aeiou]ce$/] } }, { metadata: { namespace: "name" } }, false], - [{ filters: { regexNamespaces: [/^n[aeiou]me$/, /^sp[aeiou]ce$/] } }, { metadata: { namespace: "space" } }, false], - [ - { filters: { regexNamespaces: [/^n[aeiou]me$/, /^sp[aeiou]ce$/] } }, - { metadata: { namespace: "namespace" } }, - true, - ], + [{ filters: { regexNamespaces: [testRegex2, testRegex3] } }, { metadata: { namespace: "name" } }, false], + [{ filters: { regexNamespaces: [testRegex2, testRegex3] } }, { metadata: { namespace: "space" } }, false], + [{ filters: { regexNamespaces: [testRegex2, testRegex3] } }, { metadata: { namespace: "namespace" } }, true], ])("given binding %j and object %j, returns %s", (bnd, obj, expected) => { const binding: Binding = { ...defaultBinding, filters: { ...defaultFilters, regexNamespaces: bnd.filters.regexNamespaces }, }; - const object: KubernetesObject = { + const kubernetesObject: KubernetesObject = { ...defaultKubernetesObject, metadata: "metadata" in obj ? obj.metadata : defaultKubernetesObject.metadata, }; - - const result = mismatchedNamespaceRegex(binding, object); + const result = mismatchedNamespaceRegex(binding, kubernetesObject); expect(result).toBe(expected); }); diff --git a/src/lib/filter/adjudicators.ts b/src/lib/filter/adjudicators.ts index 55eb1e5a0..a5000a5cc 100644 --- a/src/lib/filter/adjudicators.ts +++ b/src/lib/filter/adjudicators.ts @@ -214,7 +214,8 @@ export const mismatchedNamespaceRegex = allPass([ const regexArray = definedNamespaceRegexes(binding).map(regexStr => new RegExp(regexStr)); // Check if no regex matches the namespace of the Kubernetes object - return not(any((regEx: RegExp) => regEx.test(carriedNamespace(kubernetesObject)), regexArray)); + const result = not(any((regEx: RegExp) => regEx.test(carriedNamespace(kubernetesObject)), regexArray)); + return result; }, ]); From 25081c25743b47a392c701a1e147b3d5187c1475 Mon Sep 17 00:00:00 2001 From: Sam Mayer Date: Wed, 20 Nov 2024 17:14:01 -0600 Subject: [PATCH 20/46] Move default test objects to central location --- src/lib/filter/adjudicators.test.ts | 41 ++++--------------- .../filter/adjudicators/defaultTestObjects.ts | 36 ++++++++++++++++ 2 files changed, 43 insertions(+), 34 deletions(-) create mode 100644 src/lib/filter/adjudicators/defaultTestObjects.ts diff --git a/src/lib/filter/adjudicators.test.ts b/src/lib/filter/adjudicators.test.ts index 818b217d4..020bca50c 100644 --- a/src/lib/filter/adjudicators.test.ts +++ b/src/lib/filter/adjudicators.test.ts @@ -22,42 +22,15 @@ import { unbindableNamespaces, uncarryableNamespace, } from "./adjudicators"; -import { kind, KubernetesObject, modelToGroupVersionKind } from "kubernetes-fluent-client"; +import { KubernetesObject } from "kubernetes-fluent-client"; import { AdmissionRequest, Binding, DeepPartial } from "../types"; import { Event, Operation } from "../enums"; - -const defaultFilters = { - annotations: {}, - deletionTimestamp: false, - labels: {}, - name: "", - namespaces: [], - regexName: "^default$", - regexNamespaces: [], -}; -const defaultBinding: Binding = { - event: Event.ANY, - filters: defaultFilters, - kind: modelToGroupVersionKind(kind.Pod.name), - model: kind.Pod, -}; - -const defaultAdmissionRequest = { - uid: "some-uid", - kind: { kind: "a-kind", group: "a-group" }, - group: "a-group", - resource: { group: "some-group", version: "some-version", resource: "some-resource" }, - operation: Operation.CONNECT, - name: "some-name", - userInfo: {}, - object: {}, -}; - -const defaultKubernetesObject: KubernetesObject = { - apiVersion: "some-version", - kind: "some-kind", - metadata: { name: "some-name" }, -}; +import { + defaultAdmissionRequest, + defaultBinding, + defaultFilters, + defaultKubernetesObject, +} from "./adjudicators/defaultTestObjects"; describe("mismatchedName", () => { //[ Binding, KubernetesObject, result ] diff --git a/src/lib/filter/adjudicators/defaultTestObjects.ts b/src/lib/filter/adjudicators/defaultTestObjects.ts new file mode 100644 index 000000000..884173f5e --- /dev/null +++ b/src/lib/filter/adjudicators/defaultTestObjects.ts @@ -0,0 +1,36 @@ +import { modelToGroupVersionKind, kind, KubernetesObject } from "kubernetes-fluent-client"; +import { Event, Operation } from "../../enums"; +import { Binding } from "../../types"; + +export const defaultFilters = { + annotations: {}, + deletionTimestamp: false, + labels: {}, + name: "", + namespaces: [], + regexName: "^default$", + regexNamespaces: [], +}; +export const defaultBinding: Binding = { + event: Event.ANY, + filters: defaultFilters, + kind: modelToGroupVersionKind(kind.Pod.name), + model: kind.Pod, +}; + +export const defaultAdmissionRequest = { + uid: "some-uid", + kind: { kind: "a-kind", group: "a-group" }, + group: "a-group", + resource: { group: "some-group", version: "some-version", resource: "some-resource" }, + operation: Operation.CONNECT, + name: "some-name", + userInfo: {}, + object: {}, +}; + +export const defaultKubernetesObject: KubernetesObject = { + apiVersion: "some-version", + kind: "some-kind", + metadata: { name: "some-name" }, +}; From 5cc56671d7dd586f6d95f1ac7c7cd8da69147c12 Mon Sep 17 00:00:00 2001 From: Sam Mayer Date: Wed, 20 Nov 2024 17:16:18 -0600 Subject: [PATCH 21/46] Use defaultTestObjects in bindingAdjudicators.test.ts --- .../adjudicators/bindingAdjudicators.test.ts | 32 ++----------------- .../filter/adjudicators/defaultTestObjects.ts | 5 +++ 2 files changed, 8 insertions(+), 29 deletions(-) diff --git a/src/lib/filter/adjudicators/bindingAdjudicators.test.ts b/src/lib/filter/adjudicators/bindingAdjudicators.test.ts index a986a680f..d4aa5fc13 100644 --- a/src/lib/filter/adjudicators/bindingAdjudicators.test.ts +++ b/src/lib/filter/adjudicators/bindingAdjudicators.test.ts @@ -2,8 +2,8 @@ // SPDX-FileCopyrightText: 2023-Present The Pepr Authors import { expect, describe, it } from "@jest/globals"; -import { kind, KubernetesObject } from "kubernetes-fluent-client"; -import { Binding, DeepPartial, ValidateActionResponse } from "../../types"; +import { KubernetesObject } from "kubernetes-fluent-client"; +import { DeepPartial, ValidateActionResponse } from "../../types"; import { Event } from "../../enums"; import { bindsToNamespace, @@ -45,33 +45,7 @@ import { misboundNamespace, missingName, } from "../adjudicators"; - -const defaultFilters = { - annotations: {}, - deletionTimestamp: false, - labels: {}, - name: "", - namespaces: [], - regexName: "^default$", - regexNamespaces: [], -}; -const defaultBinding: Binding = { - event: Event.ANY, - filters: defaultFilters, - kind: { kind: "some-kind", group: "some-group" }, - model: kind.Pod, - isFinalize: false, - isMutate: false, - isQueue: false, - isValidate: false, - isWatch: false, -}; - -const defaultKubernetesObject: KubernetesObject = { - apiVersion: "some-version", - kind: "some-kind", - metadata: { name: "some-name" }, -}; +import { defaultBinding, defaultFilters, defaultKubernetesObject } from "./defaultTestObjects"; describe("definesDeletionTimestamp", () => { //[ Binding, result ] diff --git a/src/lib/filter/adjudicators/defaultTestObjects.ts b/src/lib/filter/adjudicators/defaultTestObjects.ts index 884173f5e..5d195e24e 100644 --- a/src/lib/filter/adjudicators/defaultTestObjects.ts +++ b/src/lib/filter/adjudicators/defaultTestObjects.ts @@ -16,6 +16,11 @@ export const defaultBinding: Binding = { filters: defaultFilters, kind: modelToGroupVersionKind(kind.Pod.name), model: kind.Pod, + isFinalize: false, //Lots of optionals that maybe don't belong here. Would be nice to choose to include + isMutate: false, + isQueue: false, + isValidate: false, + isWatch: false, }; export const defaultAdmissionRequest = { From aa24a3032351de834f844538c7aa832bffc11250 Mon Sep 17 00:00:00 2001 From: Sam Mayer Date: Wed, 20 Nov 2024 17:21:19 -0600 Subject: [PATCH 22/46] Centralize more default test values --- ...indingKubernetesObjectAdjudicators.test.ts | 31 ++----------------- .../filter/adjudicators/defaultTestObjects.ts | 22 +++++++++++++ src/lib/filter/shouldSkipRequest.test.ts | 30 ++---------------- src/lib/helpers.test.ts | 27 +--------------- 4 files changed, 27 insertions(+), 83 deletions(-) diff --git a/src/lib/filter/adjudicators/bindingKubernetesObjectAdjudicators.test.ts b/src/lib/filter/adjudicators/bindingKubernetesObjectAdjudicators.test.ts index 3a28f4c98..821ce2f92 100644 --- a/src/lib/filter/adjudicators/bindingKubernetesObjectAdjudicators.test.ts +++ b/src/lib/filter/adjudicators/bindingKubernetesObjectAdjudicators.test.ts @@ -3,9 +3,8 @@ // SPDX-FileCopyrightText: 2023-Present The Pepr Authors import { expect, describe, it } from "@jest/globals"; -import { kind, KubernetesObject } from "kubernetes-fluent-client"; +import { KubernetesObject } from "kubernetes-fluent-client"; import { Binding, DeepPartial } from "../../types"; -import { Event } from "../../enums"; import { mismatchedName, mismatchedDeletionTimestamp, @@ -16,33 +15,7 @@ import { mismatchedLabels, metasMismatch, } from "../adjudicators"; - -const defaultFilters = { - annotations: {}, - deletionTimestamp: false, - labels: {}, - name: "", - namespaces: [], - regexName: "^default$", - regexNamespaces: [], -}; -const defaultBinding: Binding = { - event: Event.ANY, - filters: defaultFilters, - kind: { kind: "some-kind", group: "some-group" }, - model: kind.Pod, - isFinalize: false, - isMutate: false, - isQueue: false, - isValidate: false, - isWatch: false, -}; - -const defaultKubernetesObject: KubernetesObject = { - apiVersion: "some-version", - kind: "some-kind", - metadata: { name: "some-name" }, -}; +import { defaultBinding, defaultFilters, defaultKubernetesObject } from "./defaultTestObjects"; describe("mismatchedName", () => { //[ Binding, KubernetesObject, result ] diff --git a/src/lib/filter/adjudicators/defaultTestObjects.ts b/src/lib/filter/adjudicators/defaultTestObjects.ts index 5d195e24e..dab8c8171 100644 --- a/src/lib/filter/adjudicators/defaultTestObjects.ts +++ b/src/lib/filter/adjudicators/defaultTestObjects.ts @@ -14,6 +14,7 @@ export const defaultFilters = { export const defaultBinding: Binding = { event: Event.ANY, filters: defaultFilters, + // kind: { kind: "some-kind", group: "some-group" }, // Should it be this instead?? Used elsewhere. kind: modelToGroupVersionKind(kind.Pod.name), model: kind.Pod, isFinalize: false, //Lots of optionals that maybe don't belong here. Would be nice to choose to include @@ -39,3 +40,24 @@ export const defaultKubernetesObject: KubernetesObject = { kind: "some-kind", metadata: { name: "some-name" }, }; + +const callback = () => undefined; +// const podKind = modelToGroupVersionKind(kind.Pod.name); +const deploymentKind = modelToGroupVersionKind(kind.Deployment.name); +const clusterRoleKind = modelToGroupVersionKind(kind.ClusterRole.name); + +export const groupBinding = { + callback, + event: Event.CREATE, + filters: defaultFilters, + kind: deploymentKind, + model: kind.Deployment, +}; + +export const clusterScopedBinding = { + callback, + event: Event.DELETE, + filters: defaultFilters, + kind: clusterRoleKind, + model: kind.ClusterRole, +}; diff --git a/src/lib/filter/shouldSkipRequest.test.ts b/src/lib/filter/shouldSkipRequest.test.ts index e66c5c830..a42d0a264 100644 --- a/src/lib/filter/shouldSkipRequest.test.ts +++ b/src/lib/filter/shouldSkipRequest.test.ts @@ -13,22 +13,12 @@ import { import { shouldSkipRequest } from "./filter"; import { AdmissionRequest, Binding } from "../types"; import { Event } from "../enums"; +import { clusterScopedBinding, defaultFilters, groupBinding } from "./adjudicators/defaultTestObjects"; const callback = () => undefined; export const podKind = modelToGroupVersionKind(kind.Pod.name); -export const deploymentKind = modelToGroupVersionKind(kind.Deployment.name); -export const clusterRoleKind = modelToGroupVersionKind(kind.ClusterRole.name); - -const defaultFilters = { - annotations: {}, - deletionTimestamp: false, - labels: {}, - name: "", - namespaces: [], - regexName: "^default$", - regexNamespaces: [], -}; + const defaultBinding = { callback, event: Event.ANY, @@ -37,22 +27,6 @@ const defaultBinding = { model: kind.Pod, }; -export const groupBinding = { - callback, - event: Event.CREATE, - filters: defaultFilters, - kind: deploymentKind, - model: kind.Deployment, -}; - -export const clusterScopedBinding = { - callback, - event: Event.DELETE, - filters: defaultFilters, - kind: clusterRoleKind, - model: kind.ClusterRole, -}; - describe("when fuzzing shouldSkipRequest", () => { it("should handle random inputs without crashing", () => { fc.assert( diff --git a/src/lib/helpers.test.ts b/src/lib/helpers.test.ts index 4d32549a7..1ec901c00 100644 --- a/src/lib/helpers.test.ts +++ b/src/lib/helpers.test.ts @@ -32,6 +32,7 @@ import { promises as fs } from "fs"; import { SpiedFunction } from "jest-mock"; import { K8s, GenericClass, KubernetesObject, kind } from "kubernetes-fluent-client"; import { K8sInit } from "kubernetes-fluent-client/dist/fluent/types"; +import { defaultBinding, defaultFilters, defaultKubernetesObject } from "./filter/adjudicators/defaultTestObjects"; export const callback = () => undefined; @@ -51,27 +52,6 @@ jest.mock("fs", () => { }; }); -const defaultFilters = { - annotations: {}, - deletionTimestamp: false, - labels: {}, - name: "", - namespaces: [], - regexName: "", - regexNamespaces: [], -}; -const defaultBinding: Binding = { - event: Event.ANY, - filters: defaultFilters, - kind: { kind: "some-kind", group: "some-group" }, - model: kind.Pod, - isFinalize: false, - isMutate: false, - isQueue: false, - isValidate: false, - isWatch: false, -}; - const mockCapabilities: CapabilityExport[] = JSON.parse(`[ { "name": "hello-pepr", @@ -1091,11 +1071,6 @@ describe("replaceString", () => { }); describe("filterNoMatchReason", () => { - const defaultKubernetesObject: KubernetesObject = { - apiVersion: "some-version", - kind: "some-kind", - metadata: { name: "some-name" }, - }; it.each([ [{}], [{ metadata: { namespace: "pepr-uds" } }], From 2bdad5ca75937d66f2b22cec138182e9181126b0 Mon Sep 17 00:00:00 2001 From: Sam Mayer Date: Thu, 21 Nov 2024 11:26:25 -0600 Subject: [PATCH 23/46] Undo type coversion to RexExp, use string --- src/lib/capability.ts | 6 +- src/lib/filter/adjudicators.test.ts | 8 +- src/lib/filter/adjudicators.ts | 3 +- .../adjudicators/bindingAdjudicators.test.ts | 8 +- ...indingKubernetesObjectAdjudicators.test.ts | 33 +++-- .../filter/adjudicators/defaultTestObjects.ts | 18 +-- src/lib/filter/filter.test.ts | 8 +- src/lib/filter/shouldSkipRequest.test.ts | 139 ++++++++---------- src/lib/helpers.test.ts | 16 +- src/lib/helpers.ts | 12 +- src/lib/types.ts | 8 +- 11 files changed, 124 insertions(+), 135 deletions(-) diff --git a/src/lib/capability.ts b/src/lib/capability.ts index 2046cd92a..0ca878bbd 100644 --- a/src/lib/capability.ts +++ b/src/lib/capability.ts @@ -345,7 +345,7 @@ export class Capability implements CapabilityExport { return { ...commonChain, WithName, WithNameRegex }; } - function InNamespaceRegex(...namespaces: RegExp[]): BindingWithName { + function InNamespaceRegex(...namespaces: string[]): BindingWithName { Log.debug(`Add regex namespaces filter ${namespaces}`, prefix); binding.filters.regexNamespaces.push(...namespaces); return { ...commonChain, WithName, WithNameRegex }; @@ -357,9 +357,9 @@ export class Capability implements CapabilityExport { return commonChain; } - function WithNameRegex(regexName: RegExp): BindingFilter { + function WithNameRegex(regexName: string): BindingFilter { Log.debug(`Add regex name filter ${regexName}`, prefix); - binding.filters.regexName = regexName.source; + binding.filters.regexName = regexName; return commonChain; } diff --git a/src/lib/filter/adjudicators.test.ts b/src/lib/filter/adjudicators.test.ts index 020bca50c..8b77882a2 100644 --- a/src/lib/filter/adjudicators.test.ts +++ b/src/lib/filter/adjudicators.test.ts @@ -120,13 +120,13 @@ describe("mismatchedNamespace", () => { describe("mismatchedNamespaceRegex", () => { //[ Binding, KubernetesObject, result ] - const testRegex1 = /^n[aeiou]mespace$/; - const testRegex2 = /^n[aeiou]me$/; - const testRegex3 = /^sp[aeiou]ce$/; + const testRegex1 = "^n[aeiou]mespace$"; + const testRegex2 = "^n[aeiou]me$"; + const testRegex3 = "^sp[aeiou]ce$"; it.each([ // [{}, {}, false], // [{}, { metadata: { namespace: "namespace" } }, false], - [{ filters: { regexNamespaces: [/^n.mespace$/] } }, {}, true], + [{ filters: { regexNamespaces: ["^n.mespace$"] } }, {}, true], [{ filters: { regexNamespaces: [testRegex1] } }, { metadata: { namespace: "namespace" } }, false], [{ filters: { regexNamespaces: [testRegex1] } }, { metadata: { namespace: "nemespace" } }, false], diff --git a/src/lib/filter/adjudicators.ts b/src/lib/filter/adjudicators.ts index a5000a5cc..f25093dd3 100644 --- a/src/lib/filter/adjudicators.ts +++ b/src/lib/filter/adjudicators.ts @@ -109,8 +109,7 @@ export const definesName = pipe(definedName, equals(""), not); export const ignoresName = complement(definesName); export const definedNameRegex = pipe( - (binding: Partial): string | undefined => - typeof binding.filters?.regexName === "string" ? binding.filters.regexName : binding.filters?.regexName.source, + (binding: Partial): string | undefined => binding.filters?.regexName, defaultTo(""), ); export const definesNameRegex = pipe(definedNameRegex, equals(""), not); diff --git a/src/lib/filter/adjudicators/bindingAdjudicators.test.ts b/src/lib/filter/adjudicators/bindingAdjudicators.test.ts index d4aa5fc13..dd76b4de6 100644 --- a/src/lib/filter/adjudicators/bindingAdjudicators.test.ts +++ b/src/lib/filter/adjudicators/bindingAdjudicators.test.ts @@ -322,8 +322,8 @@ describe("definedNamespaceRegexes", () => { // [{ filters: {} }, []], // [{ filters: { regexNamespaces: null } }, []], [{ filters: { regexNamespaces: [] } }, []], - [{ filters: { regexNamespaces: [/n.mesp.ce/] } }, ["n.mesp.ce"]], - [{ filters: { regexNamespaces: [/n.me/, /sp.ce/] } }, ["n.me", "sp.ce"]], + [{ filters: { regexNamespaces: ["n.mesp.ce"] } }, ["n.mesp.ce"]], + [{ filters: { regexNamespaces: ["n.me", "sp.ce"] } }, ["n.me", "sp.ce"]], ])("given %j, returns %j", (given, expected) => { const binding = { ...defaultBinding, @@ -346,8 +346,8 @@ describe("definesNamespaceRegexes", () => { // [{ filters: {} }, false], // [{ filters: { regexNamespaces: null } }, false], [{ filters: { regexNamespaces: [] } }, false], - [{ filters: { regexNamespaces: [/n.mesp.ce/] } }, true], - [{ filters: { regexNamespaces: [/n.me/, /sp.ce/] } }, true], + [{ filters: { regexNamespaces: ["n.mesp.ce"] } }, true], + [{ filters: { regexNamespaces: ["n.me", "sp.ce"] } }, true], ])("given %j, returns %s", (given, expected) => { const binding = { ...defaultBinding, diff --git a/src/lib/filter/adjudicators/bindingKubernetesObjectAdjudicators.test.ts b/src/lib/filter/adjudicators/bindingKubernetesObjectAdjudicators.test.ts index 821ce2f92..9738f124f 100644 --- a/src/lib/filter/adjudicators/bindingKubernetesObjectAdjudicators.test.ts +++ b/src/lib/filter/adjudicators/bindingKubernetesObjectAdjudicators.test.ts @@ -99,25 +99,26 @@ describe("mismatchedNamespace", () => { describe("mismatchedNamespaceRegex", () => { //[ Binding, KubernetesObject, result ] + const testRegex1 = "^n.mespace$"; + const testRegex2 = "^n[aeiou]mespace$"; + const testRegex3 = "^n[aeiou]me$"; + const testRegex4 = "^sp[aeiou]ce$"; + it.each([ // [{}, {}, false], // [{}, { metadata: { namespace: "namespace" } }, false], - [{ filters: { regexNamespaces: [/^n.mespace$/] } }, {}, true], - - [{ filters: { regexNamespaces: [/^n[aeiou]mespace$/] } }, { metadata: { namespace: "namespace" } }, false], - [{ filters: { regexNamespaces: [/^n[aeiou]mespace$/] } }, { metadata: { namespace: "nemespace" } }, false], - [{ filters: { regexNamespaces: [/^n[aeiou]mespace$/] } }, { metadata: { namespace: "nimespace" } }, false], - [{ filters: { regexNamespaces: [/^n[aeiou]mespace$/] } }, { metadata: { namespace: "nomespace" } }, false], - [{ filters: { regexNamespaces: [/^n[aeiou]mespace$/] } }, { metadata: { namespace: "numespace" } }, false], - [{ filters: { regexNamespaces: [/^n[aeiou]mespace$/] } }, { metadata: { namespace: "n3mespace" } }, true], - - [{ filters: { regexNamespaces: [/^n[aeiou]me$/, /^sp[aeiou]ce$/] } }, { metadata: { namespace: "name" } }, false], - [{ filters: { regexNamespaces: [/^n[aeiou]me$/, /^sp[aeiou]ce$/] } }, { metadata: { namespace: "space" } }, false], - [ - { filters: { regexNamespaces: [/^n[aeiou]me$/, /^sp[aeiou]ce$/] } }, - { metadata: { namespace: "namespace" } }, - true, - ], + [{ filters: { regexNamespaces: [testRegex1] } }, {}, true], + + [{ filters: { regexNamespaces: [testRegex2] } }, { metadata: { namespace: "namespace" } }, false], + [{ filters: { regexNamespaces: [testRegex2] } }, { metadata: { namespace: "nemespace" } }, false], + [{ filters: { regexNamespaces: [testRegex2] } }, { metadata: { namespace: "nimespace" } }, false], + [{ filters: { regexNamespaces: [testRegex2] } }, { metadata: { namespace: "nomespace" } }, false], + [{ filters: { regexNamespaces: [testRegex2] } }, { metadata: { namespace: "numespace" } }, false], + [{ filters: { regexNamespaces: [testRegex2] } }, { metadata: { namespace: "n3mespace" } }, true], + + [{ filters: { regexNamespaces: [testRegex3, testRegex4] } }, { metadata: { namespace: "name" } }, false], + [{ filters: { regexNamespaces: [testRegex3, testRegex4] } }, { metadata: { namespace: "space" } }, false], + [{ filters: { regexNamespaces: [testRegex3, testRegex4] } }, { metadata: { namespace: "namespace" } }, true], ])("given binding %j and object %j, returns %s", (bnd, obj, expected) => { const binding: Binding = { ...defaultBinding, diff --git a/src/lib/filter/adjudicators/defaultTestObjects.ts b/src/lib/filter/adjudicators/defaultTestObjects.ts index dab8c8171..8af292d68 100644 --- a/src/lib/filter/adjudicators/defaultTestObjects.ts +++ b/src/lib/filter/adjudicators/defaultTestObjects.ts @@ -9,13 +9,12 @@ export const defaultFilters = { name: "", namespaces: [], regexName: "^default$", - regexNamespaces: [], + regexNamespaces: [] as string[], }; export const defaultBinding: Binding = { event: Event.ANY, filters: defaultFilters, - // kind: { kind: "some-kind", group: "some-group" }, // Should it be this instead?? Used elsewhere. - kind: modelToGroupVersionKind(kind.Pod.name), + kind: { kind: "some-kind", group: "some-group" }, // Should it be this instead?? Used elsewhere. model: kind.Pod, isFinalize: false, //Lots of optionals that maybe don't belong here. Would be nice to choose to include isMutate: false, @@ -41,21 +40,18 @@ export const defaultKubernetesObject: KubernetesObject = { metadata: { name: "some-name" }, }; -const callback = () => undefined; -// const podKind = modelToGroupVersionKind(kind.Pod.name); -const deploymentKind = modelToGroupVersionKind(kind.Deployment.name); -const clusterRoleKind = modelToGroupVersionKind(kind.ClusterRole.name); +export const podKind = modelToGroupVersionKind("V1Pod"); +export const deploymentKind = modelToGroupVersionKind("V1Deployment"); +export const clusterRoleKind = modelToGroupVersionKind("V1ClusterRole"); -export const groupBinding = { - callback, +export const groupBinding: Binding = { event: Event.CREATE, filters: defaultFilters, kind: deploymentKind, model: kind.Deployment, }; -export const clusterScopedBinding = { - callback, +export const clusterScopedBinding: Binding = { event: Event.DELETE, filters: defaultFilters, kind: clusterRoleKind, diff --git a/src/lib/filter/filter.test.ts b/src/lib/filter/filter.test.ts index 54bb9286b..79bd0c52a 100644 --- a/src/lib/filter/filter.test.ts +++ b/src/lib/filter/filter.test.ts @@ -198,7 +198,7 @@ test("create: should not reject when regex namespace does match", () => { filters: { name: "", namespaces: [], - regexNamespaces: [RegExp("^helm")], + regexNamespaces: ["^helm"], regexName: "", labels: {}, annotations: {}, @@ -218,7 +218,7 @@ test("create: should reject when regex namespace does not match", () => { filters: { name: "", namespaces: [], - regexNamespaces: [RegExp("^argo")], + regexNamespaces: ["^argo"], regexName: "", labels: {}, annotations: {}, @@ -240,7 +240,7 @@ test("delete: should reject when regex namespace does not match", () => { filters: { name: "", namespaces: [], - regexNamespaces: [RegExp("^argo")], + regexNamespaces: ["^argo"], regexName: "", labels: {}, annotations: {}, @@ -262,7 +262,7 @@ test("delete: should not reject when regex namespace does match", () => { filters: { name: "", namespaces: [], - regexNamespaces: [RegExp("^helm")], + regexNamespaces: ["^helm"], regexName: "", labels: {}, annotations: {}, diff --git a/src/lib/filter/shouldSkipRequest.test.ts b/src/lib/filter/shouldSkipRequest.test.ts index a42d0a264..a25f75853 100644 --- a/src/lib/filter/shouldSkipRequest.test.ts +++ b/src/lib/filter/shouldSkipRequest.test.ts @@ -2,7 +2,7 @@ // SPDX-FileCopyrightText: 2023-Present The Pepr Authors import { expect, describe, it } from "@jest/globals"; -import { kind, modelToGroupVersionKind } from "kubernetes-fluent-client"; +import { kind } from "kubernetes-fluent-client"; import * as fc from "fast-check"; import { AdmissionRequestCreateClusterRole, @@ -11,16 +11,11 @@ import { AdmissionRequestDeletePod, } from "../../fixtures/loader"; import { shouldSkipRequest } from "./filter"; -import { AdmissionRequest, Binding } from "../types"; +import { AdmissionRequest, Binding, Filters } from "../types"; import { Event } from "../enums"; -import { clusterScopedBinding, defaultFilters, groupBinding } from "./adjudicators/defaultTestObjects"; +import { clusterScopedBinding, defaultFilters, groupBinding, podKind } from "./adjudicators/defaultTestObjects"; -const callback = () => undefined; - -export const podKind = modelToGroupVersionKind(kind.Pod.name); - -const defaultBinding = { - callback, +const defaultBinding: Binding = { event: Event.ANY, filters: defaultFilters, kind: podKind, @@ -129,7 +124,7 @@ describe("when a binding contains a group scoped object", () => { ); }); it("should not skip request when the group is the same", () => { - const groupBindingNoRegex = { + const groupBindingNoRegex: Binding = { ...groupBinding, filters: { ...groupBinding.filters, @@ -144,7 +139,7 @@ describe("when a capability defines namespaces and the admission request object const capabilityNamespaces = ["monitoring"]; const admissionRequestCreateClusterRole = AdmissionRequestCreateClusterRole(); it("should skip request when the capability namespace does not exist on the object", () => { - const binding = { + const binding: Binding = { ...clusterScopedBinding, event: Event.CREATE, filters: { @@ -162,7 +157,7 @@ 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 = { + const clusterScopedBindingWithNamespace: Binding = { ...clusterScopedBinding, event: Event.CREATE, filters: { @@ -185,41 +180,41 @@ describe("when a pod is created", () => { }); it("should not reject when regex name does match", () => { - const filters = { ...defaultFilters, regexName: "^cool" }; - const binding = { ...defaultBinding, filters }; + 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 = { + const filters: Filters = { ...defaultFilters, - regexNamespaces: [RegExp("^helm")], + regexNamespaces: ["^helm"], regexName: "", }; - const binding = { ...defaultBinding, filters }; + const binding: Binding = { ...defaultBinding, filters }; const pod = AdmissionRequestCreatePod(); expect(shouldSkipRequest(binding, pod, [])).toBe(""); }); it("should reject when regex namespace does not match", () => { - const filters = { ...defaultFilters, regexNamespaces: [RegExp("^argo")] }; - const binding = { ...defaultBinding, filters }; + const filters: Filters = { ...defaultFilters, regexNamespaces: ["^argo"] }; + 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 = { ...defaultFilters, regexName: "" }; - const binding = { ...defaultBinding, filters }; + 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 = { ...defaultFilters, regexName: "" }; - const binding = { ...defaultBinding, filters }; + 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 '.+'./, @@ -229,8 +224,8 @@ describe("when a pod is created", () => { describe("when a pod is deleted", () => { it("should reject when regex name does not match", () => { - const filters = { ...defaultFilters, regexName: "^default$" }; - const binding = { ...defaultBinding, filters }; + 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 '.+'./, @@ -238,15 +233,15 @@ describe("when a pod is deleted", () => { }); it("should not reject when regex name does match", () => { - const filters = { ...defaultFilters, regexName: "^cool" }; - const binding = { ...defaultBinding, filters }; + 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 = { ...defaultFilters, regexNamespaces: [RegExp("^argo")] }; - const binding = { + const filters: Filters = { ...defaultFilters, regexNamespaces: ["eargo"] }; + const binding: Binding = { ...defaultBinding, filters, }; @@ -257,15 +252,15 @@ describe("when a pod is deleted", () => { }); it("should not reject when regex namespace does match", () => { - const filters = { + const filters: Filters = { ...defaultFilters, - regexNamespaces: [RegExp("^helm")], + regexNamespaces: ["^helm"], regexName: "", labels: {}, annotations: {}, deletionTimestamp: false, }; - const binding = { + const binding: Binding = { ...defaultBinding, filters, }; @@ -274,8 +269,8 @@ describe("when a pod is deleted", () => { }); it("should reject when name does not match", () => { - const filters = { ...defaultFilters, name: "bleh", regexName: "^not-cool" }; - const binding = { + const filters: Filters = { ...defaultFilters, name: "bleh", regexName: "^not-cool" }; + const binding: Binding = { ...defaultBinding, filters, }; @@ -286,8 +281,8 @@ describe("when a pod is deleted", () => { }); it("should reject when namespace is ignored", () => { - const filters = { ...defaultFilters, regexName: "", namespaces: [] }; - const binding = { + const filters: Filters = { ...defaultFilters, regexName: "", namespaces: [] }; + const binding: Binding = { ...defaultBinding, filters, }; @@ -298,11 +293,10 @@ describe("when a pod is deleted", () => { }); it("should not reject when namespace is not ignored", () => { - const filters = { ...defaultFilters, regexName: "" }; - const binding = { + const filters: Filters = { ...defaultFilters, regexName: "" }; + const binding: Binding = { ...defaultBinding, filters, - callback, }; const pod = AdmissionRequestDeletePod(); expect(shouldSkipRequest(binding, pod, [])).toMatch(""); @@ -310,8 +304,8 @@ describe("when a pod is deleted", () => { }); it("should reject when kind does not match", () => { - const filters = { ...defaultFilters, regexName: "" }; - const binding = { + const filters: Filters = { ...defaultFilters, regexName: "" }; + const binding: Binding = { ...defaultBinding, kind: { group: "", @@ -319,7 +313,6 @@ it("should reject when kind does not match", () => { kind: "Nope", }, filters, - callback, }; const pod = AdmissionRequestCreatePod(); @@ -329,8 +322,8 @@ it("should reject when kind does not match", () => { }); it("should reject when group does not match", () => { - const filters = { ...defaultFilters, regexName: "" }; - const binding = { + const filters: Filters = { ...defaultFilters, regexName: "" }; + const binding: Binding = { ...defaultBinding, kind: { group: "Nope", @@ -338,7 +331,6 @@ it("should reject when group does not match", () => { kind: "Pod", }, filters, - callback, }; const pod = AdmissionRequestCreatePod(); @@ -348,8 +340,8 @@ it("should reject when group does not match", () => { }); it("should reject when version does not match", () => { - const filters = { ...defaultFilters, regexName: "" }; - const binding = { + const filters: Filters = { ...defaultFilters, regexName: "" }; + const binding: Binding = { ...defaultBinding, kind: { group: "", @@ -357,7 +349,6 @@ it("should reject when version does not match", () => { kind: "Pod", }, filters, - callback, }; const pod = AdmissionRequestCreatePod(); @@ -367,25 +358,25 @@ it("should reject when version does not match", () => { }); it("should allow when group, version, and kind match", () => { - const filters = { ...defaultFilters, regexName: "" }; - const binding = { ...defaultBinding, filters }; + 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 = { ...defaultFilters, regexName: "" }; + const filters: Filters = { ...defaultFilters, regexName: "" }; - const binding = { ...defaultBinding, filters }; + 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 = { ...defaultFilters }; - const binding = { + const filters: Filters = { ...defaultFilters }; + const binding: Binding = { ...defaultBinding, filters, }; @@ -397,8 +388,8 @@ it("should reject when the capability namespace does not match", () => { }); it("should reject when namespace does not match", () => { - const filters = { ...defaultFilters, namespaces: ["bleh"] }; - const binding = { ...defaultBinding, filters }; + const filters: Filters = { ...defaultFilters, namespaces: ["bleh"] }; + const binding: Binding = { ...defaultBinding, filters }; const pod = AdmissionRequestCreatePod(); expect(shouldSkipRequest(binding, pod, [])).toMatch( @@ -407,7 +398,7 @@ it("should reject when namespace does not match", () => { }); it("should allow when namespace is match", () => { - const filters = { + const filters: Filters = { ...defaultFilters, namespaces: ["helm-releasename", "unicorn", "things"], labels: {}, @@ -416,7 +407,7 @@ it("should allow when namespace is match", () => { regexNamespaces: [], regexName: "", }; - const binding = { + const binding: Binding = { ...defaultBinding, filters, }; @@ -426,13 +417,13 @@ it("should allow when namespace is match", () => { }); it("should reject when label does not match", () => { - const filters = { + const filters: Filters = { ...defaultFilters, labels: { foo: "bar", }, }; - const binding = { + const binding: Binding = { ...defaultBinding, filters, }; @@ -444,7 +435,7 @@ it("should reject when label does not match", () => { }); it("should allow when label is match", () => { - const filters = { + const filters: Filters = { ...defaultFilters, regexName: "", labels: { @@ -453,7 +444,7 @@ it("should allow when label is match", () => { }, annotations: {}, }; - const binding = { + const binding: Binding = { ...defaultBinding, filters, }; @@ -470,13 +461,13 @@ it("should allow when label is match", () => { }); it("should reject when annotation does not match", () => { - const filters = { + const filters: Filters = { ...defaultFilters, annotations: { foo: "bar", }, }; - const binding = { + const binding: Binding = { ...defaultBinding, filters, }; @@ -488,7 +479,7 @@ it("should reject when annotation does not match", () => { }); it("should allow when annotation is match", () => { - const filters = { + const filters: Filters = { name: "", namespaces: [], labels: {}, @@ -500,7 +491,7 @@ it("should allow when annotation is match", () => { regexNamespaces: [], regexName: "", }; - const binding = { + const binding: Binding = { ...defaultBinding, filters, }; @@ -517,7 +508,7 @@ it("should allow when annotation is match", () => { }); it("should use `oldObject` when the operation is `DELETE`", () => { - const filters = { + const filters: Filters = { ...defaultFilters, regexNamespaces: [], regexName: "", @@ -527,7 +518,7 @@ it("should use `oldObject` when the operation is `DELETE`", () => { }, annotations: {}, }; - const binding = { + const binding: Binding = { ...defaultBinding, filters, }; @@ -538,7 +529,7 @@ it("should use `oldObject` when the operation is `DELETE`", () => { }); it("should allow when deletionTimestamp is present on pod", () => { - const filters = { + const filters: Filters = { name: "", namespaces: [], labels: {}, @@ -550,7 +541,7 @@ it("should allow when deletionTimestamp is present on pod", () => { }, deletionTimestamp: true, }; - const binding = { + const binding: Binding = { ...defaultBinding, filters, }; @@ -568,7 +559,7 @@ it("should allow when deletionTimestamp is present on pod", () => { }); it("should reject when deletionTimestamp is not present on pod", () => { - const filters = { + const filters: Filters = { ...defaultFilters, regexName: "", annotations: { @@ -577,7 +568,7 @@ it("should reject when deletionTimestamp is not present on pod", () => { }, deletionTimestamp: true, }; - const binding = { ...defaultBinding, filters }; + const binding: Binding = { ...defaultBinding, filters }; const pod = AdmissionRequestCreatePod(); pod.object.metadata = pod.object.metadata || {}; @@ -593,13 +584,13 @@ it("should reject when deletionTimestamp is not present on pod", () => { }); describe("when multiple filters are triggered", () => { - const filters = { + const filters: Filters = { ...defaultFilters, regexName: "asdf", name: "not-a-match", namespaces: ["not-allowed", "also-not-matching"], }; - const binding = { ...defaultBinding, filters }; + const binding: Binding = { ...defaultBinding, filters }; it("should display the failure message for the first matching filter", () => { const pod = AdmissionRequestCreatePod(); expect(shouldSkipRequest(binding, pod, [])).toMatch( diff --git a/src/lib/helpers.test.ts b/src/lib/helpers.test.ts index 1ec901c00..417af6593 100644 --- a/src/lib/helpers.test.ts +++ b/src/lib/helpers.test.ts @@ -619,7 +619,7 @@ describe("namespaceComplianceValidator", () => { filters: { ...binding.filters, namespaces: [], - regexNamespaces: [new RegExp(/^system/)], + regexNamespaces: ["^system"], }, })), }; @@ -637,7 +637,7 @@ describe("namespaceComplianceValidator", () => { filters: { ...binding.filters, namespaces: [], - regexNamespaces: [new RegExp(/^mia/)], + regexNamespaces: ["^mia"], }, })), }; @@ -653,7 +653,7 @@ describe("namespaceComplianceValidator", () => { filters: { ...binding.filters, namespaces: [], - regexNamespaces: [new RegExp(/^mia/)], + regexNamespaces: ["^mia"], }, })), }; @@ -671,7 +671,7 @@ describe("namespaceComplianceValidator", () => { filters: { ...binding.filters, namespaces: [], - regexNamespaces: [new RegExp(/^mia/)], + regexNamespaces: ["^mia"], }, })), }; @@ -1089,7 +1089,7 @@ describe("filterNoMatchReason", () => { const binding: Binding = { ...defaultBinding, kind: { kind: "Pod", group: "some-group" }, - filters: { ...defaultFilters, regexNamespaces: [RegExp("(.*)-system")] }, + filters: { ...defaultFilters, regexNamespaces: ["(.*)-system"] }, }; const capabilityNamespaces: string[] = []; @@ -1108,7 +1108,7 @@ test("returns no regex namespace filter error for Pods whos namespace does match const binding: Binding = { ...defaultBinding, kind: { kind: "Pod", group: "some-group" }, - filters: { ...defaultFilters, regexNamespaces: [/(.*)-system/], namespaces: [] }, + filters: { ...defaultFilters, regexNamespaces: ["(.*)-system"], namespaces: [] }, }; const obj = { metadata: { namespace: "pepr-demo" } }; const objArray = [ @@ -1130,7 +1130,7 @@ test("returns regex name filter error for Pods whos name does not match the rege const binding: Binding = { ...defaultBinding, kind: { kind: "Pod", group: "some-group" }, - filters: { ...defaultFilters, regexName: /^system/, namespaces: [] }, + filters: { ...defaultFilters, regexName: "^system", namespaces: [] }, }; const obj = { metadata: { name: "pepr-demo" } }; const objArray = [ @@ -1154,7 +1154,7 @@ test("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/ }, + filters: { ...defaultFilters, regexName: "^system" }, }; const obj = { metadata: { name: "pepr-demo" } }; const objArray = [ diff --git a/src/lib/helpers.ts b/src/lib/helpers.ts index 1cd9fb350..9898b63af 100644 --- a/src/lib/helpers.ts +++ b/src/lib/helpers.ts @@ -259,8 +259,10 @@ export function generateWatchNamespaceError( // namespaceComplianceValidator ensures that capability binds respect ignored and capability namespaces export function namespaceComplianceValidator(capability: CapabilityExport, ignoredNamespaces?: string[]) { const { namespaces: capabilityNamespaces, bindings, name } = capability; - const bindingNamespaces = bindings.flatMap((binding: Binding) => binding.filters.namespaces); - const bindingRegexNamespaces = bindings.flatMap((binding: Binding) => binding.filters.regexNamespaces || []); + const bindingNamespaces: string[] = bindings.flatMap((binding: Binding) => binding.filters.namespaces); + const bindingRegexNamespaces: string[] = bindings.flatMap( + (binding: Binding) => binding.filters.regexNamespaces || [], + ); const namespaceError = generateWatchNamespaceError( ignoredNamespaces ? ignoredNamespaces : [], @@ -284,8 +286,8 @@ export function namespaceComplianceValidator(capability: CapabilityExport, ignor for (const regexNamespace of bindingRegexNamespaces) { let matches = false; matches = - regexNamespace.source !== "" && - capabilityNamespaces.some(capabilityNamespace => matchesRegex(regexNamespace, capabilityNamespace)); + regexNamespace !== "" && + capabilityNamespaces.some(capabilityNamespace => matchesRegex(new RegExp(regexNamespace), capabilityNamespace)); if (!matches) { throw new Error( `Ignoring Watch Callback: Object namespace does not match any capability namespace with regex ${regexNamespace}.`, @@ -301,7 +303,7 @@ export function namespaceComplianceValidator(capability: CapabilityExport, ignor ignoredNamespaces.length > 0 ) { for (const regexNamespace of bindingRegexNamespaces) { - const matchedNS = ignoredNamespaces.find(ignoredNS => matchesRegex(regexNamespace, ignoredNS)); + const matchedNS = ignoredNamespaces.find(ignoredNS => matchesRegex(new RegExp(regexNamespace), ignoredNS)); if (matchedNS) { throw new Error( `Ignoring Watch Callback: Regex namespace: ${regexNamespace}, is an ignored namespace: ${matchedNS}.`, diff --git a/src/lib/types.ts b/src/lib/types.ts index 381c49f25..1d2e11238 100644 --- a/src/lib/types.ts +++ b/src/lib/types.ts @@ -77,8 +77,8 @@ export type Filters = { labels: Record; name: string; namespaces: string[]; - regexName: string | RegExp; - regexNamespaces: RegExp[]; // TODO: Test the Regexp path?? + regexName: string; + regexNamespaces: string[]; }; export type Binding = { @@ -143,14 +143,14 @@ export type BindingWithName = BindingFilter & { /** Only apply the action if the resource name matches the specified name. */ WithName: (name: string) => BindingFilter; /** Only apply the action if the resource name matches the specified regex name. */ - WithNameRegex: (name: RegExp) => BindingFilter; + WithNameRegex: (name: string) => BindingFilter; }; export type BindingAll = BindingWithName & { /** Only apply the action if the resource is in one of the specified namespaces.*/ InNamespace: (...namespaces: string[]) => BindingWithName; /** Only apply the action if the resource is in one of the specified regex namespaces.*/ - InNamespaceRegex: (...namespaces: RegExp[]) => BindingWithName; + InNamespaceRegex: (...namespaces: string[]) => BindingWithName; }; export type CommonActionChain = MutateActionChain & { From e08e896629f8654e2b8270e59bc8af8b76aaab01 Mon Sep 17 00:00:00 2001 From: Sam Mayer Date: Thu, 21 Nov 2024 11:29:11 -0600 Subject: [PATCH 24/46] Remove unecessary message output sanitization --- src/lib/filter/adjudicators.ts | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/lib/filter/adjudicators.ts b/src/lib/filter/adjudicators.ts index f25093dd3..f115d25c0 100644 --- a/src/lib/filter/adjudicators.ts +++ b/src/lib/filter/adjudicators.ts @@ -118,8 +118,7 @@ export const definedNamespaces = pipe(binding => binding?.filters?.namespaces, d export const definesNamespaces = pipe(definedNamespaces, equals([]), not); export const definedNamespaceRegexes = pipe( - (binding: Binding): string[] => - binding.filters.regexNamespaces.map(regex => regex.toString().replace(/^\/|\/$/g, "")), + (binding: Binding): string[] => binding.filters.regexNamespaces, defaultTo([]), ); export const definesNamespaceRegexes = pipe(definedNamespaceRegexes, equals([] as string[]), not); From 786c1a21770a60ac92e0f1f9e7079ddb0251afd2 Mon Sep 17 00:00:00 2001 From: Sam Mayer Date: Thu, 21 Nov 2024 11:39:47 -0600 Subject: [PATCH 25/46] Rearrange import and managed regex usage in tests --- .../filter/adjudicators/defaultTestObjects.ts | 20 +------- src/lib/filter/shouldSkipRequest.test.ts | 3 +- src/lib/helpers.test.ts | 48 +++++++++++++------ src/lib/helpers.ts | 6 +-- 4 files changed, 39 insertions(+), 38 deletions(-) diff --git a/src/lib/filter/adjudicators/defaultTestObjects.ts b/src/lib/filter/adjudicators/defaultTestObjects.ts index 8af292d68..f734e234c 100644 --- a/src/lib/filter/adjudicators/defaultTestObjects.ts +++ b/src/lib/filter/adjudicators/defaultTestObjects.ts @@ -1,4 +1,4 @@ -import { modelToGroupVersionKind, kind, KubernetesObject } from "kubernetes-fluent-client"; +import { kind, KubernetesObject } from "kubernetes-fluent-client"; import { Event, Operation } from "../../enums"; import { Binding } from "../../types"; @@ -39,21 +39,3 @@ export const defaultKubernetesObject: KubernetesObject = { kind: "some-kind", metadata: { name: "some-name" }, }; - -export const podKind = modelToGroupVersionKind("V1Pod"); -export const deploymentKind = modelToGroupVersionKind("V1Deployment"); -export const clusterRoleKind = modelToGroupVersionKind("V1ClusterRole"); - -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, -}; diff --git a/src/lib/filter/shouldSkipRequest.test.ts b/src/lib/filter/shouldSkipRequest.test.ts index a25f75853..89565e5c7 100644 --- a/src/lib/filter/shouldSkipRequest.test.ts +++ b/src/lib/filter/shouldSkipRequest.test.ts @@ -13,7 +13,8 @@ import { import { shouldSkipRequest } from "./filter"; import { AdmissionRequest, Binding, Filters } from "../types"; import { Event } from "../enums"; -import { clusterScopedBinding, defaultFilters, groupBinding, podKind } from "./adjudicators/defaultTestObjects"; +import { defaultFilters } from "./adjudicators/defaultTestObjects"; +import { clusterScopedBinding, groupBinding, podKind } from "../helpers.test"; const defaultBinding: Binding = { event: Event.ANY, diff --git a/src/lib/helpers.test.ts b/src/lib/helpers.test.ts index 417af6593..5a0ee3a27 100644 --- a/src/lib/helpers.test.ts +++ b/src/lib/helpers.test.ts @@ -30,12 +30,30 @@ import * as fc from "fast-check"; import { expect, describe, test, jest, beforeEach, afterEach, it } from "@jest/globals"; import { promises as fs } from "fs"; import { SpiedFunction } from "jest-mock"; -import { K8s, GenericClass, KubernetesObject, kind } from "kubernetes-fluent-client"; +import { K8s, GenericClass, KubernetesObject, kind, modelToGroupVersionKind } from "kubernetes-fluent-client"; import { K8sInit } from "kubernetes-fluent-client/dist/fluent/types"; -import { defaultBinding, defaultFilters, defaultKubernetesObject } from "./filter/adjudicators/defaultTestObjects"; +import { defaultFilters, defaultKubernetesObject, defaultBinding } from "./filter/adjudicators/defaultTestObjects"; +// import { defaultBinding, defaultFilters, defaultKubernetesObject } from "./filter/adjudicators/defaultTestObjects"; 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, +}; jest.mock("kubernetes-fluent-client", () => { return { K8s: jest.fn(), @@ -1430,21 +1448,21 @@ describe("validateHash", () => { describe("matchesRegex", () => { test("should return true for a valid pattern that matches the string", () => { - const pattern = /abc/; + const pattern = "abc"; const testString = "abc123"; - const result = matchesRegex(new RegExp(pattern), testString); + const result = matchesRegex(pattern, testString); expect(result).toBe(true); }); test("should return false for a valid pattern that does not match the string", () => { - const pattern = /xyz/; + const pattern = "xyz"; const testString = "abc123"; - const result = matchesRegex(new RegExp(pattern), testString); + const result = matchesRegex(pattern, testString); expect(result).toBe(false); }); test("should return false for an invalid regex pattern", () => { - const invalidPattern = new RegExp(/^p/); // Invalid regex with unclosed bracket + const invalidPattern = "^p"; // Invalid regex with unclosed bracket const testString = "test"; const result = matchesRegex(invalidPattern, testString); expect(result).toBe(false); @@ -1461,30 +1479,30 @@ describe("matchesRegex", () => { }); test("should return true for an empty string matching an empty regex", () => { - const pattern = new RegExp(""); + const pattern = ""; const testString = ""; - const result = matchesRegex(new RegExp(pattern), testString); + const result = matchesRegex(pattern, testString); expect(result).toBe(true); }); test("should return false for an empty string and a non-empty regex", () => { - const pattern = new RegExp("abc"); + const pattern = "abc"; const testString = ""; - const result = matchesRegex(new RegExp(pattern), testString); + const result = matchesRegex(pattern, testString); expect(result).toBe(false); }); test("should return true for a complex valid regex that matches", () => { - const pattern = /^[a-zA-Z0-9]+@[a-zA-Z0-9]+\.[A-Za-z]+$/; + const pattern = "^[a-zA-Z0-9]+@[a-zA-Z0-9]+.[A-Za-z]+$"; const testString = "test@example.com"; - const result = matchesRegex(new RegExp(pattern), testString); + const result = matchesRegex(pattern, testString); expect(result).toBe(true); }); test("should return false for a complex valid regex that does not match", () => { - const pattern = /^[a-zA-Z0-9]+@[a-zA-Z0-9]+\.[A-Za-z]+$/; + const pattern = "^[a-zA-Z0-9]+@[a-zA-Z0-9]+.[A-Za-z]+$"; const testString = "invalid-email.com"; - const result = matchesRegex(new RegExp(pattern), testString); + const result = matchesRegex(pattern, testString); expect(result).toBe(false); }); }); diff --git a/src/lib/helpers.ts b/src/lib/helpers.ts index 9898b63af..d00aecc19 100644 --- a/src/lib/helpers.ts +++ b/src/lib/helpers.ts @@ -32,7 +32,7 @@ import { uncarryableNamespace, } from "./filter/adjudicators"; -export function matchesRegex(pattern: RegExp, testString: string): boolean { +export function matchesRegex(pattern: string, testString: string): boolean { // edge-case if (!pattern) { return false; @@ -287,7 +287,7 @@ export function namespaceComplianceValidator(capability: CapabilityExport, ignor let matches = false; matches = regexNamespace !== "" && - capabilityNamespaces.some(capabilityNamespace => matchesRegex(new RegExp(regexNamespace), capabilityNamespace)); + capabilityNamespaces.some(capabilityNamespace => matchesRegex(regexNamespace, capabilityNamespace)); if (!matches) { throw new Error( `Ignoring Watch Callback: Object namespace does not match any capability namespace with regex ${regexNamespace}.`, @@ -303,7 +303,7 @@ export function namespaceComplianceValidator(capability: CapabilityExport, ignor ignoredNamespaces.length > 0 ) { for (const regexNamespace of bindingRegexNamespaces) { - const matchedNS = ignoredNamespaces.find(ignoredNS => matchesRegex(new RegExp(regexNamespace), ignoredNS)); + const matchedNS = ignoredNamespaces.find(ignoredNS => matchesRegex(regexNamespace, ignoredNS)); if (matchedNS) { throw new Error( `Ignoring Watch Callback: Regex namespace: ${regexNamespace}, is an ignored namespace: ${matchedNS}.`, From 9cc92672f2d0704d811e3e361d821b6ef4cb7246 Mon Sep 17 00:00:00 2001 From: Sam Mayer Date: Thu, 21 Nov 2024 13:47:36 -0600 Subject: [PATCH 26/46] Move filesystem helper to specific file --- src/lib/filesystemHelpers.ts | 16 ++++++++++ src/lib/filesytemHelpers.test.ts | 51 ++++++++++++++++++++++++++++++++ src/lib/helpers.test.ts | 50 ------------------------------- src/lib/helpers.ts | 13 -------- 4 files changed, 67 insertions(+), 63 deletions(-) create mode 100644 src/lib/filesystemHelpers.ts create mode 100644 src/lib/filesytemHelpers.test.ts diff --git a/src/lib/filesystemHelpers.ts b/src/lib/filesystemHelpers.ts new file mode 100644 index 000000000..3a581d61f --- /dev/null +++ b/src/lib/filesystemHelpers.ts @@ -0,0 +1,16 @@ +// SPDX-License-Identifier: Apache-2.0 +// SPDX-FileCopyrightText: 2023-Present The Pepr Authors + +import { promises } from "fs"; + +export async function createDirectoryIfNotExists(path: string) { + try { + await promises.access(path); + } catch (error) { + if (error.code === "ENOENT") { + await promises.mkdir(path, { recursive: true }); + } else { + throw error; + } + } +} diff --git a/src/lib/filesytemHelpers.test.ts b/src/lib/filesytemHelpers.test.ts new file mode 100644 index 000000000..ef51107af --- /dev/null +++ b/src/lib/filesytemHelpers.test.ts @@ -0,0 +1,51 @@ +import { describe, jest, expect, it } from "@jest/globals"; +import { createDirectoryIfNotExists } from "./filesystemHelpers"; +import { promises as fs } from "fs"; + +jest.mock("fs", () => { + return { + promises: { + access: jest.fn(), + mkdir: jest.fn(), + }, + }; +}); + +describe("createDirectoryIfNotExists function", () => { + it("should create a directory if it doesn't exist", async () => { + (fs.access as jest.Mock).mockRejectedValue({ code: "ENOENT" } as never); + (fs.mkdir as jest.Mock).mockResolvedValue(undefined as never); + + const directoryPath = "/pepr/pepr-test-module/asdf"; + + await createDirectoryIfNotExists(directoryPath); + + expect(fs.access).toHaveBeenCalledWith(directoryPath); + expect(fs.mkdir).toHaveBeenCalledWith(directoryPath, { recursive: true }); + }); + + it("should not create a directory if it already exists", async () => { + jest.resetAllMocks(); + (fs.access as jest.Mock).mockResolvedValue(undefined as never); + + const directoryPath = "/pepr/pepr-test-module/asdf"; + + await createDirectoryIfNotExists(directoryPath); + + expect(fs.access).toHaveBeenCalledWith(directoryPath); + expect(fs.mkdir).not.toHaveBeenCalled(); + }); + + it("should throw an error for other fs.access errors", async () => { + jest.resetAllMocks(); + (fs.access as jest.Mock).mockRejectedValue({ code: "ERROR" } as never); + + const directoryPath = "/pepr/pepr-test-module/asdf"; + + try { + await createDirectoryIfNotExists(directoryPath); + } catch (error) { + expect(error.code).toEqual("ERROR"); + } + }); +}); diff --git a/src/lib/helpers.test.ts b/src/lib/helpers.test.ts index 5a0ee3a27..d5dcfe4c3 100644 --- a/src/lib/helpers.test.ts +++ b/src/lib/helpers.test.ts @@ -6,7 +6,6 @@ import { Event } from "./enums"; import { addVerbIfNotExists, bindingAndCapabilityNSConflict, - createDirectoryIfNotExists, createRBACMap, checkDeploymentStatus, filterNoMatchReason, @@ -28,7 +27,6 @@ import { import { sanitizeResourceName } from "../sdk/sdk"; import * as fc from "fast-check"; import { expect, describe, test, jest, beforeEach, afterEach, it } from "@jest/globals"; -import { promises as fs } from "fs"; import { SpiedFunction } from "jest-mock"; import { K8s, GenericClass, KubernetesObject, kind, modelToGroupVersionKind } from "kubernetes-fluent-client"; import { K8sInit } from "kubernetes-fluent-client/dist/fluent/types"; @@ -61,15 +59,6 @@ jest.mock("kubernetes-fluent-client", () => { }; }); -jest.mock("fs", () => { - return { - promises: { - access: jest.fn(), - mkdir: jest.fn(), - }, - }; -}); - const mockCapabilities: CapabilityExport[] = JSON.parse(`[ { "name": "hello-pepr", @@ -393,45 +382,6 @@ describe("addVerbIfNotExists", () => { }); }); -describe("createDirectoryIfNotExists function", () => { - test("should create a directory if it doesn't exist", async () => { - (fs.access as jest.Mock).mockRejectedValue({ code: "ENOENT" } as never); - (fs.mkdir as jest.Mock).mockResolvedValue(undefined as never); - - const directoryPath = "/pepr/pepr-test-module/asdf"; - - await createDirectoryIfNotExists(directoryPath); - - expect(fs.access).toHaveBeenCalledWith(directoryPath); - expect(fs.mkdir).toHaveBeenCalledWith(directoryPath, { recursive: true }); - }); - - test("should not create a directory if it already exists", async () => { - jest.resetAllMocks(); - (fs.access as jest.Mock).mockResolvedValue(undefined as never); - - const directoryPath = "/pepr/pepr-test-module/asdf"; - - await createDirectoryIfNotExists(directoryPath); - - expect(fs.access).toHaveBeenCalledWith(directoryPath); - expect(fs.mkdir).not.toHaveBeenCalled(); - }); - - test("should throw an error for other fs.access errors", async () => { - jest.resetAllMocks(); - (fs.access as jest.Mock).mockRejectedValue({ code: "ERROR" } as never); - - const directoryPath = "/pepr/pepr-test-module/asdf"; - - try { - await createDirectoryIfNotExists(directoryPath); - } catch (error) { - expect(error.code).toEqual("ERROR"); - } - }); -}); - describe("hasAnyOverlap", () => { test("returns true for overlapping arrays", () => { expect(hasAnyOverlap([1, 2, 3], [3, 4, 5])).toBe(true); diff --git a/src/lib/helpers.ts b/src/lib/helpers.ts index d00aecc19..007f903e7 100644 --- a/src/lib/helpers.ts +++ b/src/lib/helpers.ts @@ -2,7 +2,6 @@ // SPDX-License-Identifier: Apache-2.0 // SPDX-FileCopyrightText: 2023-Present The Pepr Authors -import { promises as fs } from "fs"; import { K8s, KubernetesObject, kind } from "kubernetes-fluent-client"; import Log from "./logger"; import { Binding, CapabilityExport } from "./types"; @@ -192,18 +191,6 @@ export function createRBACMap(capabilities: CapabilityExport[]): RBACMap { }, {}); } -export async function createDirectoryIfNotExists(path: string) { - try { - await fs.access(path); - } catch (error) { - if (error.code === "ENOENT") { - await fs.mkdir(path, { recursive: true }); - } else { - throw error; - } - } -} - export function hasEveryOverlap(array1: T[], array2: T[]): boolean { if (!Array.isArray(array1) || !Array.isArray(array2)) { return false; From a0dab61f0aed5fdaa53aaf74b5b94bad42a42233 Mon Sep 17 00:00:00 2001 From: Sam Mayer Date: Thu, 21 Nov 2024 13:51:55 -0600 Subject: [PATCH 27/46] Remove a test case obviated by the use of typing --- src/lib/helpers.test.ts | 10 ---------- 1 file changed, 10 deletions(-) diff --git a/src/lib/helpers.test.ts b/src/lib/helpers.test.ts index d5dcfe4c3..c575a0208 100644 --- a/src/lib/helpers.test.ts +++ b/src/lib/helpers.test.ts @@ -1418,16 +1418,6 @@ describe("matchesRegex", () => { expect(result).toBe(false); }); - test("should return false when pattern is null or undefined", () => { - const testString = "abc123"; - // Check for undefined - // eslint-disable-next-line @typescript-eslint/no-explicit-any - expect(matchesRegex(undefined as any, testString)).toBe(false); - // Check for null - // eslint-disable-next-line @typescript-eslint/no-explicit-any - expect(matchesRegex(null as any, testString)).toBe(false); - }); - test("should return true for an empty string matching an empty regex", () => { const pattern = ""; const testString = ""; From 8bbcfd1693904ad9f5160b853c5ba5d99cd6a7fb Mon Sep 17 00:00:00 2001 From: Sam Mayer Date: Thu, 21 Nov 2024 13:52:09 -0600 Subject: [PATCH 28/46] Remove kfc mock --- src/lib/helpers.test.ts | 6 ------ 1 file changed, 6 deletions(-) diff --git a/src/lib/helpers.test.ts b/src/lib/helpers.test.ts index c575a0208..075636c7b 100644 --- a/src/lib/helpers.test.ts +++ b/src/lib/helpers.test.ts @@ -52,12 +52,6 @@ export const clusterScopedBinding: Binding = { kind: clusterRoleKind, model: kind.ClusterRole, }; -jest.mock("kubernetes-fluent-client", () => { - return { - K8s: jest.fn(), - kind: jest.fn(), - }; -}); const mockCapabilities: CapabilityExport[] = JSON.parse(`[ { From 6ca317b009b846f90204826663aca0683a695701 Mon Sep 17 00:00:00 2001 From: Sam Mayer Date: Thu, 21 Nov 2024 13:55:17 -0600 Subject: [PATCH 29/46] Use it() instead of test() to match domain-driven test naming style --- src/lib/helpers.test.ts | 164 ++++++++++++++++++++-------------------- 1 file changed, 82 insertions(+), 82 deletions(-) diff --git a/src/lib/helpers.test.ts b/src/lib/helpers.test.ts index 075636c7b..f5b82c8c3 100644 --- a/src/lib/helpers.test.ts +++ b/src/lib/helpers.test.ts @@ -26,7 +26,7 @@ import { } from "./helpers"; import { sanitizeResourceName } from "../sdk/sdk"; import * as fc from "fast-check"; -import { expect, describe, test, jest, beforeEach, afterEach, it } from "@jest/globals"; +import { expect, describe, jest, beforeEach, afterEach, it } from "@jest/globals"; import { SpiedFunction } from "jest-mock"; import { K8s, GenericClass, KubernetesObject, kind, modelToGroupVersionKind } from "kubernetes-fluent-client"; import { K8sInit } from "kubernetes-fluent-client/dist/fluent/types"; @@ -300,7 +300,7 @@ const mockCapabilities: CapabilityExport[] = JSON.parse(`[ ]`); describe("validateCapabilityNames Property-Based Tests", () => { - test("should only accept names that are valid after sanitation", () => { + it("should only accept names that are valid after sanitation", () => { fc.assert( fc.property( fc.array( @@ -325,24 +325,24 @@ describe("validateCapabilityNames Property-Based Tests", () => { }); describe("validateCapabilityNames", () => { - test("should return true if all capability names are valid", () => { + it("should return true if all capability names are valid", () => { const capabilities = mockCapabilities; expect(() => validateCapabilityNames(capabilities)).not.toThrow(); }); - test("should throw an error if a capability name is invalid", () => { + it("should throw an error if a capability name is invalid", () => { const capabilities = mockCapabilities; capabilities[0].name = "invalid name"; expect(() => validateCapabilityNames(capabilities)).toThrowError(ValidationError); }); - test("should ignore when capabilities are not loaded", () => { + it("should ignore when capabilities are not loaded", () => { expect(validateCapabilityNames(undefined)).toBe(undefined); }); }); describe("createRBACMap", () => { - test("should return the correct RBACMap for given capabilities", () => { + it("should return the correct RBACMap for given capabilities", () => { const result = createRBACMap(mockCapabilities); const expected = { @@ -363,13 +363,13 @@ describe("createRBACMap", () => { }); describe("addVerbIfNotExists", () => { - test("should add a verb if it does not exist in the array", () => { + it("should add a verb if it does not exist in the array", () => { const verbs = ["get", "list"]; addVerbIfNotExists(verbs, "watch"); expect(verbs).toEqual(["get", "list", "watch"]); }); - test("should not add a verb if it already exists in the array", () => { + it("should not add a verb if it already exists in the array", () => { const verbs = ["get", "list", "watch"]; addVerbIfNotExists(verbs, "get"); expect(verbs).toEqual(["get", "list", "watch"]); // The array remains unchanged @@ -377,104 +377,104 @@ describe("addVerbIfNotExists", () => { }); describe("hasAnyOverlap", () => { - test("returns true for overlapping arrays", () => { + it("returns true for overlapping arrays", () => { expect(hasAnyOverlap([1, 2, 3], [3, 4, 5])).toBe(true); }); - test("returns false for non-overlapping arrays", () => { + it("returns false for non-overlapping arrays", () => { expect(hasAnyOverlap([1, 2, 3], [4, 5, 6])).toBe(false); }); - test("returns false for empty arrays", () => { + it("returns false for empty arrays", () => { expect(hasAnyOverlap([], [1, 2, 3])).toBe(false); expect(hasAnyOverlap([1, 2, 3], [])).toBe(false); }); - test("returns false for two empty arrays", () => { + it("returns false for two empty arrays", () => { expect(hasAnyOverlap([], [])).toBe(false); }); }); describe("hasEveryOverlap", () => { - test("returns true if all elements in array1 are in array2", () => { + it("returns true if all elements in array1 are in array2", () => { expect(hasEveryOverlap([1, 2], [1, 2, 3])).toBe(true); }); - test("returns false if any element in array1 is not in array2", () => { + it("returns false if any element in array1 is not in array2", () => { expect(hasEveryOverlap([1, 2, 4], [1, 2, 3])).toBe(false); }); - test("returns true if array1 is empty", () => { + it("returns true if array1 is empty", () => { expect(hasEveryOverlap([], [1, 2, 3])).toBe(true); }); - test("returns false if array2 is empty", () => { + it("returns false if array2 is empty", () => { expect(hasEveryOverlap([1, 2], [])).toBe(false); }); - test("returns true if both arrays are empty", () => { + it("returns true if both arrays are empty", () => { expect(hasEveryOverlap([], [])).toBe(true); }); }); describe("ignoredNamespaceConflict", () => { - test("returns true if there is an overlap", () => { + it("returns true if there is an overlap", () => { expect(ignoredNamespaceConflict(["ns1", "ns2"], ["ns2", "ns3"])).toBe(true); }); - test("returns false if there is no overlap", () => { + it("returns false if there is no overlap", () => { expect(ignoredNamespaceConflict(["ns1", "ns2"], ["ns3", "ns4"])).toBe(false); }); - test("returns false if either array is empty", () => { + it("returns false if either array is empty", () => { expect(ignoredNamespaceConflict([], ["ns1", "ns2"])).toBe(false); expect(ignoredNamespaceConflict(["ns1", "ns2"], [])).toBe(false); }); - test("returns false if both arrays are empty", () => { + it("returns false if both arrays are empty", () => { expect(ignoredNamespaceConflict([], [])).toBe(false); }); }); describe("bindingAndCapabilityNSConflict", () => { - test("returns false if capabilityNamespaces is empty", () => { + it("returns false if capabilityNamespaces is empty", () => { expect(bindingAndCapabilityNSConflict(["ns1", "ns2"], [])).toBe(false); }); - test("returns true if capability namespaces are not empty and there is no overlap with binding namespaces", () => { + it("returns true if capability namespaces are not empty and there is no overlap with binding namespaces", () => { expect(bindingAndCapabilityNSConflict(["ns1", "ns2"], ["ns3", "ns4"])).toBe(true); }); - test("returns true if capability namespaces are not empty and there is an overlap", () => { + it("returns true if capability namespaces are not empty and there is an overlap", () => { expect(bindingAndCapabilityNSConflict(["ns1", "ns2"], ["ns2", "ns3"])).toBe(true); }); - test("returns false if both arrays are empty", () => { + it("returns false if both arrays are empty", () => { expect(bindingAndCapabilityNSConflict([], [])).toBe(false); }); }); describe("generateWatchNamespaceError", () => { - test("returns error for ignored namespace conflict", () => { + it("returns error for ignored namespace conflict", () => { const error = generateWatchNamespaceError(["ns1"], ["ns1"], []); expect(error).toBe("Binding uses a Pepr ignored namespace: ignoredNamespaces: [ns1] bindingNamespaces: [ns1]."); }); - test("returns error for binding and capability namespace conflict", () => { + it("returns error for binding and capability namespace conflict", () => { const error = generateWatchNamespaceError([""], ["ns2"], ["ns3"]); expect(error).toBe( "Binding uses namespace not governed by capability: bindingNamespaces: [ns2] capabilityNamespaces: [ns3].", ); }); - test("returns combined error for both conflicts", () => { + it("returns combined error for both conflicts", () => { const error = generateWatchNamespaceError(["ns1"], ["ns1"], ["ns3", "ns4"]); expect(error).toBe( "Binding uses a Pepr ignored namespace: ignoredNamespaces: [ns1] bindingNamespaces: [ns1]. Binding uses namespace not governed by capability: bindingNamespaces: [ns1] capabilityNamespaces: [ns3, ns4].", ); }); - test("returns empty string when there are no conflicts", () => { + it("returns empty string when there are no conflicts", () => { const error = generateWatchNamespaceError([], ["ns2"], []); expect(error).toBe(""); }); @@ -573,7 +573,7 @@ describe("namespaceComplianceValidator", () => { afterEach(() => { errorSpy.mockRestore(); }); - test("should throw error for invalid regex namespaces", () => { + it("should throw error for invalid regex namespaces", () => { const namespaceViolationCapability: CapabilityExport = { ...nonNsViolation[0], bindings: nonNsViolation[0].bindings.map(binding => ({ @@ -591,7 +591,7 @@ describe("namespaceComplianceValidator", () => { `Ignoring Watch Callback: Object namespace does not match any capability namespace with regex ${namespaceViolationCapability.bindings[0].filters.regexNamespaces[0]}.`, ); }); - test("should not throw an error for valid regex namespaces", () => { + it("should not throw an error for valid regex namespaces", () => { const nonNamespaceViolationCapability: CapabilityExport = { ...nonNsViolation[0], bindings: nonNsViolation[0].bindings.map(binding => ({ @@ -607,7 +607,7 @@ describe("namespaceComplianceValidator", () => { namespaceComplianceValidator(nonNamespaceViolationCapability); }).not.toThrow(); }); - test("should throw error for invalid regex ignored namespaces", () => { + it("should throw error for invalid regex ignored namespaces", () => { const namespaceViolationCapability: CapabilityExport = { ...nonNsViolation[0], bindings: nonNsViolation[0].bindings.map(binding => ({ @@ -625,7 +625,7 @@ describe("namespaceComplianceValidator", () => { `Ignoring Watch Callback: Regex namespace: ${namespaceViolationCapability.bindings[0].filters.regexNamespaces[0]}, is an ignored namespace: miami.`, ); }); - test("should not throw an error for valid regex ignored namespaces", () => { + it("should not throw an error for valid regex ignored namespaces", () => { const nonNamespaceViolationCapability: CapabilityExport = { ...nonNsViolation[0], bindings: nonNsViolation[0].bindings.map(binding => ({ @@ -641,13 +641,13 @@ describe("namespaceComplianceValidator", () => { namespaceComplianceValidator(nonNamespaceViolationCapability, ["Seattle"]); }).not.toThrow(); }); - test("should not throw an error for valid namespaces", () => { + it("should not throw an error for valid namespaces", () => { expect(() => { namespaceComplianceValidator(nonNsViolation[0]); }).not.toThrow(); }); - test("should throw an error for binding namespace using a non capability namespace", () => { + it("should throw an error for binding namespace using a non capability namespace", () => { try { namespaceComplianceValidator(namespaceViolation[0]); } catch (e) { @@ -657,7 +657,7 @@ describe("namespaceComplianceValidator", () => { } }); - test("should throw an error for binding namespace using an ignored namespace: Part 1", () => { + it("should throw an error for binding namespace using an ignored namespace: Part 1", () => { /* * this test case lists miami as a capability namespace, but also as an ignored namespace * in this case, there should be an error since ignoredNamespaces have precedence @@ -671,7 +671,7 @@ describe("namespaceComplianceValidator", () => { } }); - test("should throw an error for binding namespace using an ignored namespace: Part 2", () => { + it("should throw an error for binding namespace using an ignored namespace: Part 2", () => { /* * This capability uses all namespaces but new york should be ignored * the binding uses new york so it should fail @@ -697,7 +697,7 @@ describe("checkDeploymentStatus", () => { jest.resetAllMocks(); jest.useRealTimers(); }); - test("should return true if all deployments are ready", async () => { + it("should return true if all deployments are ready", async () => { const deployments = { items: [ { @@ -739,7 +739,7 @@ describe("checkDeploymentStatus", () => { expect(result).toBe(expected); }); - test("should return false if any deployments are not ready", async () => { + it("should return false if any deployments are not ready", async () => { const deployments = { items: [ { @@ -794,7 +794,7 @@ describe("namespaceDeploymentsReady", () => { jest.useRealTimers(); }); - test("should return true if all deployments are ready", async () => { + it("should return true if all deployments are ready", async () => { const deployments = { items: [ { @@ -836,7 +836,7 @@ describe("namespaceDeploymentsReady", () => { expect(result).toBe(expected); }); - test("should call checkDeploymentStatus if any deployments are not ready", async () => { + it("should call checkDeploymentStatus if any deployments are not ready", async () => { const deployments = { items: [ { @@ -920,42 +920,42 @@ describe("namespaceDeploymentsReady", () => { describe("parseTimeout", () => { const PREV = "a"; - test("should return a number when a valid string number between 1 and 30 is provided", () => { + it("should return a number when a valid string number between 1 and 30 is provided", () => { expect(parseTimeout("5", PREV)).toBe(5); expect(parseTimeout("1", PREV)).toBe(1); expect(parseTimeout("30", PREV)).toBe(30); }); - test("should throw an InvalidArgumentError for non-numeric strings", () => { + it("should throw an InvalidArgumentError for non-numeric strings", () => { expect(() => parseTimeout("abc", PREV)).toThrow(Error); expect(() => parseTimeout("", PREV)).toThrow(Error); }); - test("should throw an InvalidArgumentError for numbers outside the 1-30 range", () => { + it("should throw an InvalidArgumentError for numbers outside the 1-30 range", () => { expect(() => parseTimeout("0", PREV)).toThrow(Error); expect(() => parseTimeout("31", PREV)).toThrow(Error); }); - test("should throw an InvalidArgumentError for numeric strings that represent floating point numbers", () => { + it("should throw an InvalidArgumentError for numeric strings that represent floating point numbers", () => { expect(() => parseTimeout("5.5", PREV)).toThrow(Error); expect(() => parseTimeout("20.1", PREV)).toThrow(Error); }); }); describe("secretOverLimit", () => { - test("should return true for a string larger than 1MiB", () => { + it("should return true for a string larger than 1MiB", () => { const largeString = "a".repeat(1048577); expect(secretOverLimit(largeString)).toBe(true); }); - test("should return false for a string smaller than 1MiB", () => { + it("should return false for a string smaller than 1MiB", () => { const smallString = "a".repeat(1048575); expect(secretOverLimit(smallString)).toBe(false); }); }); describe("dedent", () => { - test("removes leading spaces based on the smallest indentation", () => { + it("removes leading spaces based on the smallest indentation", () => { const input = ` kind: Namespace metadata: @@ -968,13 +968,13 @@ describe("dedent", () => { expect(inputArray[2]).toBe(" name: pepr-system"); }); - test("does not remove internal spacing of lines", () => { + it("does not remove internal spacing of lines", () => { const input = `kind: ->>> Namespace`; expect(dedent(input)).toBe("kind: ->>> Namespace"); }); - test("handles strings without leading whitespace consistently", () => { + it("handles strings without leading whitespace consistently", () => { const input = `kind: Namespace metadata:`; @@ -983,7 +983,7 @@ metadata:`; expect(inputArray[1]).toBe("metadata:"); }); - test("handles empty strings without crashing", () => { + it("handles empty strings without crashing", () => { const input = ``; const expected = ``; expect(dedent(input)).toBe(expected); @@ -991,7 +991,7 @@ metadata:`; }); describe("replaceString", () => { - test("replaces single instance of a string", () => { + it("replaces single instance of a string", () => { const original = "Hello, world!"; const stringA = "world"; const stringB = "Jest"; @@ -999,7 +999,7 @@ describe("replaceString", () => { expect(replaceString(original, stringA, stringB)).toBe(expected); }); - test("replaces multiple instances of a string", () => { + it("replaces multiple instances of a string", () => { const original = "Repeat, repeat, repeat"; const stringA = "repeat"; const stringB = "done"; @@ -1007,7 +1007,7 @@ describe("replaceString", () => { expect(replaceString(original, stringA, stringB)).toBe(expected); }); - test("does nothing if string to replace is not found", () => { + it("does nothing if string to replace is not found", () => { const original = "Nothing changes here"; const stringA = "absent"; const stringB = "present"; @@ -1015,7 +1015,7 @@ describe("replaceString", () => { expect(replaceString(original, stringA, stringB)).toBe(expected); }); - test("escapes special regex characters in string to be replaced", () => { + it("escapes special regex characters in string to be replaced", () => { const original = "Find the period."; const stringA = "."; const stringB = "!"; @@ -1023,7 +1023,7 @@ describe("replaceString", () => { expect(replaceString(original, stringA, stringB)).toBe(expected); }); - test("replaces string with empty string if stringB is empty", () => { + it("replaces string with empty string if stringB is empty", () => { const original = "Remove this part."; const stringA = " this part"; const stringB = ""; @@ -1066,7 +1066,7 @@ describe("filterNoMatchReason", () => { ); }); -test("returns no regex namespace filter error for Pods whos namespace does match the regex", () => { +it("returns no regex namespace filter error for Pods whos namespace does match the regex", () => { const binding: Binding = { ...defaultBinding, kind: { kind: "Pod", group: "some-group" }, @@ -1088,7 +1088,7 @@ test("returns no regex namespace filter error for Pods whos namespace does match }); // Names Fail -test("returns regex name filter error for Pods whos name does not match the regex", () => { +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" }, @@ -1112,7 +1112,7 @@ test("returns regex name filter error for Pods whos name does not match the rege }); // Names Pass -test("returns no regex name filter error for Pods whos name does match the regex", () => { +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" }, @@ -1133,7 +1133,7 @@ test("returns no regex name filter error for Pods whos name does match the regex }); }); -test("returns missingCarriableNamespace filter error for cluster-scoped objects when capability namespaces are present", () => { +it("returns missingCarriableNamespace filter error for cluster-scoped objects when capability namespaces are present", () => { const binding: Binding = { ...defaultBinding, kind: { kind: "ClusterRole", group: "some-group" }, @@ -1150,7 +1150,7 @@ test("returns missingCarriableNamespace filter error for cluster-scoped objects ); }); -test("returns mismatchedNamespace filter error for clusterScoped objects with namespace filters", () => { +it("returns mismatchedNamespace filter error for clusterScoped objects with namespace filters", () => { const binding: Binding = { ...defaultBinding, kind: { kind: "ClusterRole", group: "some-group" }, @@ -1166,7 +1166,7 @@ test("returns mismatchedNamespace filter error for clusterScoped objects with na expect(result).toEqual("Ignoring Watch Callback: Binding defines namespaces '[\"ns1\"]' but Object carries ''."); }); -test("returns namespace filter error for namespace objects with namespace filters", () => { +it("returns namespace filter error for namespace objects with namespace filters", () => { const binding: Binding = { ...defaultBinding, kind: { kind: "Namespace", group: "some-group" }, @@ -1178,7 +1178,7 @@ test("returns namespace filter error for namespace objects with namespace filter expect(result).toEqual("Ignoring Watch Callback: Cannot use namespace filter on a namespace object."); }); -test("return an Ignoring Watch Callback string if the binding name and object name are different", () => { +it("return an Ignoring Watch Callback string if the binding name and object name are different", () => { const binding: Binding = { ...defaultBinding, filters: { ...defaultFilters, name: "pepr" }, @@ -1192,7 +1192,7 @@ test("return an Ignoring Watch Callback string if the binding name and object na 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'.`); }); -test("returns no Ignoring Watch Callback string if the binding name and object name are the same", () => { +it("returns no Ignoring Watch Callback string if the binding name and object name are the same", () => { const binding: Binding = { ...defaultBinding, filters: { ...defaultFilters, name: "pepr" }, @@ -1205,7 +1205,7 @@ test("returns no Ignoring Watch Callback string if the binding name and object n expect(result).toEqual(""); }); -test("return deletionTimestamp error when there is no deletionTimestamp in the object", () => { +it("return deletionTimestamp error when there is no deletionTimestamp in the object", () => { const binding: Binding = { ...defaultBinding, filters: { ...defaultFilters, deletionTimestamp: true }, @@ -1218,7 +1218,7 @@ test("return deletionTimestamp error when there is no deletionTimestamp in the o expect(result).toEqual("Ignoring Watch Callback: Binding defines deletionTimestamp but Object does not carry it."); }); -test("return no deletionTimestamp error when there is a deletionTimestamp in the object", () => { +it("return no deletionTimestamp error when there is a deletionTimestamp in the object", () => { const binding: Binding = { ...defaultBinding, filters: { ...defaultFilters, deletionTimestamp: true }, @@ -1233,7 +1233,7 @@ test("return no deletionTimestamp error when there is a deletionTimestamp in the expect(result).not.toEqual("Ignoring Watch Callback: Binding defines deletionTimestamp Object does not carry it."); }); -test("returns label overlap error when there is no overlap between binding and object labels", () => { +it("returns label overlap error when there is no overlap between binding and object labels", () => { const binding: Binding = { ...defaultBinding, filters: { ...defaultFilters, labels: { key: "value" } }, @@ -1248,7 +1248,7 @@ test("returns label overlap error when there is no overlap between binding and o ); }); -test("returns annotation overlap error when there is no overlap between binding and object annotations", () => { +it("returns annotation overlap error when there is no overlap between binding and object annotations", () => { const binding: Binding = { ...defaultBinding, filters: { ...defaultFilters, annotations: { key: "value" } }, @@ -1263,7 +1263,7 @@ test("returns annotation overlap error when there is no overlap between binding ); }); -test("returns capability namespace error when object is not in capability namespaces", () => { +it("returns capability namespace error when object is not in capability namespaces", () => { const binding: Binding = { model: kind.Pod, event: Event.ANY, @@ -1298,7 +1298,7 @@ test("returns capability namespace error when object is not in capability namesp ); }); -test("returns binding namespace error when filter namespace is not part of capability namespaces", () => { +it("returns binding namespace error when filter namespace is not part of capability namespaces", () => { const binding: Binding = { ...defaultBinding, filters: { ...defaultFilters, namespaces: ["ns3"], regexNamespaces: [] }, @@ -1311,7 +1311,7 @@ test("returns binding namespace error when filter namespace is not part of capab ); }); -test("returns binding and object namespace error when they do not overlap", () => { +it("returns binding and object namespace error when they do not overlap", () => { const binding: Binding = { ...defaultBinding, filters: { ...defaultFilters, namespaces: ["ns1"], regexNamespaces: [] }, @@ -1324,7 +1324,7 @@ test("returns binding and object namespace error when they do not overlap", () = expect(result).toEqual(`Ignoring Watch Callback: Binding defines namespaces '["ns1"]' but Object carries 'ns2'.`); }); -test("return watch violation message when object is in an ignored namespace", () => { +it("return watch violation message when object is in an ignored namespace", () => { const binding: Binding = { ...defaultBinding, filters: { ...defaultFilters, namespaces: ["ns3"] }, @@ -1345,7 +1345,7 @@ test("return watch violation message when object is in an ignored namespace", () ); }); -test("returns empty string when all checks pass", () => { +it("returns empty string when all checks pass", () => { const binding: Binding = { ...defaultBinding, filters: { ...defaultFilters, namespaces: ["ns1"], labels: { key: "value" }, annotations: { key: "value" } }, @@ -1369,7 +1369,7 @@ describe("validateHash", () => { afterEach(() => { process.exit = originalExit; }); - test("should throw ValidationError for invalid hash values", () => { + it("should throw ValidationError for invalid hash values", () => { // Examples of invalid hashes const invalidHashes = [ "", // Empty string @@ -1383,7 +1383,7 @@ describe("validateHash", () => { }); }); - test("should not throw ValidationError for valid SHA-256 hash", () => { + it("should not throw ValidationError for valid SHA-256 hash", () => { // Example of a valid SHA-256 hash const validHash = "abc123def456abc123def456abc123def456abc123def456abc123def456abc1"; expect(() => validateHash(validHash)).not.toThrow(); @@ -1391,49 +1391,49 @@ describe("validateHash", () => { }); describe("matchesRegex", () => { - test("should return true for a valid pattern that matches the string", () => { + it("should return true for a valid pattern that matches the string", () => { const pattern = "abc"; const testString = "abc123"; const result = matchesRegex(pattern, testString); expect(result).toBe(true); }); - test("should return false for a valid pattern that does not match the string", () => { + it("should return false for a valid pattern that does not match the string", () => { const pattern = "xyz"; const testString = "abc123"; const result = matchesRegex(pattern, testString); expect(result).toBe(false); }); - test("should return false for an invalid regex pattern", () => { + it("should return false for an invalid regex pattern", () => { const invalidPattern = "^p"; // Invalid regex with unclosed bracket const testString = "test"; const result = matchesRegex(invalidPattern, testString); expect(result).toBe(false); }); - test("should return true for an empty string matching an empty regex", () => { + it("should return true for an empty string matching an empty regex", () => { const pattern = ""; const testString = ""; const result = matchesRegex(pattern, testString); expect(result).toBe(true); }); - test("should return false for an empty string and a non-empty regex", () => { + it("should return false for an empty string and a non-empty regex", () => { const pattern = "abc"; const testString = ""; const result = matchesRegex(pattern, testString); expect(result).toBe(false); }); - test("should return true for a complex valid regex that matches", () => { + it("should return true for a complex valid regex that matches", () => { const pattern = "^[a-zA-Z0-9]+@[a-zA-Z0-9]+.[A-Za-z]+$"; const testString = "test@example.com"; const result = matchesRegex(pattern, testString); expect(result).toBe(true); }); - test("should return false for a complex valid regex that does not match", () => { + it("should return false for a complex valid regex that does not match", () => { const pattern = "^[a-zA-Z0-9]+@[a-zA-Z0-9]+.[A-Za-z]+$"; const testString = "invalid-email.com"; const result = matchesRegex(pattern, testString); From 909e890f1c55955f6b1fe879964157e059a4c900 Mon Sep 17 00:00:00 2001 From: Sam Mayer Date: Thu, 21 Nov 2024 14:29:26 -0600 Subject: [PATCH 30/46] Rewrite test using full objects and it.each() --- src/lib/helpers.test.ts | 34 +++++++++++++++------------------- src/lib/helpers.ts | 4 ++-- 2 files changed, 17 insertions(+), 21 deletions(-) diff --git a/src/lib/helpers.test.ts b/src/lib/helpers.test.ts index f5b82c8c3..6393400d1 100644 --- a/src/lib/helpers.test.ts +++ b/src/lib/helpers.test.ts @@ -1066,25 +1066,21 @@ describe("filterNoMatchReason", () => { ); }); -it("returns no regex namespace filter error for Pods whos namespace does match the regex", () => { - const binding: Binding = { - ...defaultBinding, - kind: { kind: "Pod", group: "some-group" }, - filters: { ...defaultFilters, regexNamespaces: ["(.*)-system"], namespaces: [] }, - }; - const obj = { metadata: { namespace: "pepr-demo" } }; - const objArray = [ - { ...obj, metadata: { namespace: "pepr-system" } }, - { ...obj, metadata: { namespace: "pepr-uds-system" } }, - { ...obj, metadata: { namespace: "uds-system" } }, - { ...obj, metadata: { namespace: "some-thing-that-is-a-system" } }, - { ...obj, metadata: { namespace: "your-system" } }, - ]; - const capabilityNamespaces: string[] = []; - objArray.map(object => { - const result = filterNoMatchReason(binding, object as unknown as Partial, capabilityNamespaces); - expect(result).toEqual(``); - }); +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: ["(.*)-system"], namespaces: [] }, + }; + const kubernetesObject: KubernetesObject = { ...defaultKubernetesObject, metadata: { namespace: namespace } }; + const capabilityNamespaces: string[] = []; + const result = filterNoMatchReason(binding, kubernetesObject, capabilityNamespaces); + expect(result).toEqual(""); + }, + ); }); // Names Fail diff --git a/src/lib/helpers.ts b/src/lib/helpers.ts index 007f903e7..fb66a90fd 100644 --- a/src/lib/helpers.ts +++ b/src/lib/helpers.ts @@ -364,7 +364,7 @@ export const parseTimeout = (value: string, previous: unknown): number => { }; // Remove leading whitespace while keeping format of file -export function dedent(file: string) { +export function dedent(file: string): string { // Check if the first line is empty and remove it const lines = file.split("\n"); if (lines[0].trim() === "") { @@ -381,7 +381,7 @@ export function dedent(file: string) { return file; } -export function replaceString(str: string, stringA: string, stringB: string) { +export function replaceString(str: string, stringA: string, stringB: string): string { // eslint-disable-next-line no-useless-escape const escapedStringA = stringA.replace(/[-\/\\^$*+?.()|[\]{}]/g, "\\$&"); const regExp = new RegExp(escapedStringA, "g"); From 937ce1940759b83985e156350998b1bc753ed35d Mon Sep 17 00:00:00 2001 From: Sam Mayer Date: Thu, 21 Nov 2024 14:32:43 -0600 Subject: [PATCH 31/46] Remove unnecessary regex edge case now that we use typing --- src/lib/helpers.ts | 8 +------- 1 file changed, 1 insertion(+), 7 deletions(-) diff --git a/src/lib/helpers.ts b/src/lib/helpers.ts index fb66a90fd..7c3cc4f0c 100644 --- a/src/lib/helpers.ts +++ b/src/lib/helpers.ts @@ -32,13 +32,7 @@ import { } from "./filter/adjudicators"; export function matchesRegex(pattern: string, testString: string): boolean { - // edge-case - if (!pattern) { - return false; - } - - const regex = new RegExp(pattern); - return regex.test(testString); + return new RegExp(pattern).test(testString); } export class ValidationError extends Error {} From a98e0dfb90b695c443bd12cc3e485becebfb2fdb Mon Sep 17 00:00:00 2001 From: Sam Mayer Date: Thu, 21 Nov 2024 14:34:03 -0600 Subject: [PATCH 32/46] Update regex to match test condition --- src/lib/helpers.test.ts | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/src/lib/helpers.test.ts b/src/lib/helpers.test.ts index 6393400d1..b7aa4f0da 100644 --- a/src/lib/helpers.test.ts +++ b/src/lib/helpers.test.ts @@ -1344,7 +1344,13 @@ it("return watch violation message when object is in an ignored namespace", () = it("returns empty string when all checks pass", () => { const binding: Binding = { ...defaultBinding, - filters: { ...defaultFilters, namespaces: ["ns1"], labels: { key: "value" }, annotations: { key: "value" } }, + filters: { + ...defaultFilters, + regexName: "", + namespaces: ["ns1"], + labels: { key: "value" }, + annotations: { key: "value" }, + }, }; const obj = { metadata: { namespace: "ns1", labels: { key: "value" }, annotations: { key: "value" } }, From 5a0753d1c265b287ae21f9c0a5347a5431dfea99 Mon Sep 17 00:00:00 2001 From: Sam Mayer Date: Thu, 21 Nov 2024 14:37:02 -0600 Subject: [PATCH 33/46] Restructure test format --- src/lib/helpers.test.ts | 36 +++++++++++++++++------------------- 1 file changed, 17 insertions(+), 19 deletions(-) diff --git a/src/lib/helpers.test.ts b/src/lib/helpers.test.ts index b7aa4f0da..447a4d467 100644 --- a/src/lib/helpers.test.ts +++ b/src/lib/helpers.test.ts @@ -1320,25 +1320,23 @@ it("returns binding and object namespace error when they do not overlap", () => expect(result).toEqual(`Ignoring Watch Callback: Binding defines namespaces '["ns1"]' but Object carries 'ns2'.`); }); -it("return watch violation message when object is in an ignored namespace", () => { - const binding: Binding = { - ...defaultBinding, - filters: { ...defaultFilters, namespaces: ["ns3"] }, - }; - const obj = { - metadata: { namespace: "ns3" }, - }; - const capabilityNamespaces = ["ns3"]; - const ignoredNamespaces = ["ns3"]; - const result = filterNoMatchReason( - binding, - obj as unknown as Partial, - capabilityNamespaces, - ignoredNamespaces, - ); - expect(result).toEqual( - `Ignoring Watch Callback: Object carries namespace 'ns3' but ignored namespaces include '["ns3"]'.`, - ); +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", () => { From d98348155276b4622276d54ad6ac1d1d7a7e47aa Mon Sep 17 00:00:00 2001 From: Sam Mayer Date: Thu, 21 Nov 2024 14:41:43 -0600 Subject: [PATCH 34/46] Restructure name equality test --- src/lib/helpers.test.ts | 25 ++++++++++++++----------- 1 file changed, 14 insertions(+), 11 deletions(-) diff --git a/src/lib/helpers.test.ts b/src/lib/helpers.test.ts index 447a4d467..7b1b50128 100644 --- a/src/lib/helpers.test.ts +++ b/src/lib/helpers.test.ts @@ -1188,17 +1188,20 @@ it("return an Ignoring Watch Callback string if the binding name and object name 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'.`); }); -it("returns no Ignoring Watch Callback string if the binding name and object name are the same", () => { - const binding: Binding = { - ...defaultBinding, - filters: { ...defaultFilters, name: "pepr" }, - }; - const obj = { - metadata: { name: "pepr" }, - }; - const capabilityNamespaces: string[] = []; - const result = filterNoMatchReason(binding, obj as unknown as Partial, capabilityNamespaces); - expect(result).toEqual(""); + +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", () => { From d887b53bcf3844376d46fb170d620cdd5af25ae2 Mon Sep 17 00:00:00 2001 From: Sam Mayer Date: Thu, 21 Nov 2024 14:51:09 -0600 Subject: [PATCH 35/46] Remove mockK8s.mockImplementation() --- src/lib/helpers.test.ts | 36 +----------------------------------- 1 file changed, 1 insertion(+), 35 deletions(-) diff --git a/src/lib/helpers.test.ts b/src/lib/helpers.test.ts index 7b1b50128..d2cc0e076 100644 --- a/src/lib/helpers.test.ts +++ b/src/lib/helpers.test.ts @@ -1,3 +1,4 @@ +/* eslint-disable @typescript-eslint/no-unused-vars */ // SPDX-License-Identifier: Apache-2.0 // SPDX-FileCopyrightText: 2023-Present The Pepr Authors @@ -727,13 +728,6 @@ describe("checkDeploymentStatus", () => { ], }; - mockK8s.mockImplementation(() => { - return { - InNamespace: jest.fn().mockReturnThis(), - Get: () => deployments, - } as unknown as K8sInit; - }); - const expected = true; const result = await checkDeploymentStatus("pepr-system"); expect(result).toBe(expected); @@ -769,13 +763,6 @@ describe("checkDeploymentStatus", () => { ], }; - mockK8s.mockImplementation(() => { - return { - InNamespace: jest.fn().mockReturnThis(), - Get: () => deployments, - } as unknown as K8sInit; - }); - const expected = false; const result = await checkDeploymentStatus("pepr-system"); expect(result).toBe(expected); @@ -824,13 +811,6 @@ describe("namespaceDeploymentsReady", () => { ], }; - mockK8s.mockImplementation(() => { - return { - InNamespace: jest.fn().mockReturnThis(), - Get: () => deployments, - } as unknown as K8sInit; - }); - const expected = true; const result = await namespaceDeploymentsReady(); expect(result).toBe(expected); @@ -895,20 +875,6 @@ describe("namespaceDeploymentsReady", () => { ], }; - mockK8s - .mockImplementation(() => { - return { - InNamespace: jest.fn().mockReturnThis(), - Get: () => deployments, - } as unknown as K8sInit; - }) - .mockImplementation(() => { - return { - InNamespace: jest.fn().mockReturnThis(), - Get: () => deployments2, - } as unknown as K8sInit; - }); - const expected = true; const result = await namespaceDeploymentsReady(); From 2e44cc940796d639d0b0584638b74ac0f23740fe Mon Sep 17 00:00:00 2001 From: Sam Mayer Date: Thu, 21 Nov 2024 14:51:24 -0600 Subject: [PATCH 36/46] Restructure test --- src/lib/helpers.test.ts | 33 ++++++++++++++++++--------------- 1 file changed, 18 insertions(+), 15 deletions(-) diff --git a/src/lib/helpers.test.ts b/src/lib/helpers.test.ts index d2cc0e076..7182b9018 100644 --- a/src/lib/helpers.test.ts +++ b/src/lib/helpers.test.ts @@ -1095,21 +1095,24 @@ it("returns no regex name filter error for Pods whos name does match the regex", }); }); -it("returns missingCarriableNamespace filter error for cluster-scoped objects when capability namespaces are present", () => { - const binding: Binding = { - ...defaultBinding, - kind: { kind: "ClusterRole", group: "some-group" }, - }; - const obj = { - kind: "ClusterRole", - apiVersion: "rbac.authorization.k8s.io/v1", - metadata: { name: "clusterrole1" }, - }; - const capabilityNamespaces: string[] = ["monitoring"]; - const result = filterNoMatchReason(binding, obj as unknown as Partial, capabilityNamespaces); - expect(result).toEqual( - "Ignoring Watch Callback: Object does not carry a namespace but namespaces allowed by Capability are '[\"monitoring\"]'.", - ); +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", () => { From 5ac5159d00459268822d21add41cda0b7c1f076f Mon Sep 17 00:00:00 2001 From: Sam Mayer Date: Thu, 21 Nov 2024 14:55:07 -0600 Subject: [PATCH 37/46] Put related tests into a describe() block --- src/lib/helpers.test.ts | 111 ++++++++++++++++++++-------------------- 1 file changed, 56 insertions(+), 55 deletions(-) diff --git a/src/lib/helpers.test.ts b/src/lib/helpers.test.ts index 7182b9018..94b27f146 100644 --- a/src/lib/helpers.test.ts +++ b/src/lib/helpers.test.ts @@ -733,7 +733,44 @@ describe("checkDeploymentStatus", () => { expect(result).toBe(expected); }); - it("should return false if any deployments are not ready", async () => { + describe("when any deployments are not ready", () => { + it("should return false", async () => { + const deployments = { + items: [ + { + metadata: { + name: "watcher", + namespace: "pepr-system", + }, + spec: { + replicas: 1, + }, + status: { + readyReplicas: 1, + }, + }, + { + metadata: { + name: "admission", + namespace: "pepr-system", + }, + spec: { + replicas: 2, + }, + status: { + readyReplicas: 1, + }, + }, + ], + }; + + const expected = false; + const result = await checkDeploymentStatus("pepr-system"); + expect(result).toBe(expected); + }); + }); + + it("should call checkDeploymentStatus", async () => { const deployments = { items: [ { @@ -763,26 +800,7 @@ describe("checkDeploymentStatus", () => { ], }; - const expected = false; - const result = await checkDeploymentStatus("pepr-system"); - expect(result).toBe(expected); - }); -}); - -describe("namespaceDeploymentsReady", () => { - const mockK8s = jest.mocked(K8s); - - beforeEach(() => { - jest.useFakeTimers(); - }); - afterEach(() => { - jest.clearAllMocks(); - jest.resetAllMocks(); - jest.useRealTimers(); - }); - - it("should return true if all deployments are ready", async () => { - const deployments = { + const deployments2 = { items: [ { metadata: { @@ -813,40 +831,27 @@ describe("namespaceDeploymentsReady", () => { const expected = true; const result = await namespaceDeploymentsReady(); + expect(result).toBe(expected); + + expect(mockK8s).toHaveBeenCalledTimes(1); }); +}); - it("should call checkDeploymentStatus if any deployments are not ready", async () => { - const deployments = { - items: [ - { - metadata: { - name: "watcher", - namespace: "pepr-system", - }, - spec: { - replicas: 1, - }, - status: { - readyReplicas: 1, - }, - }, - { - metadata: { - name: "admission", - namespace: "pepr-system", - }, - spec: { - replicas: 2, - }, - status: { - readyReplicas: 1, - }, - }, - ], - }; +describe("namespaceDeploymentsReady", () => { + const mockK8s = jest.mocked(K8s); - const deployments2 = { + beforeEach(() => { + jest.useFakeTimers(); + }); + afterEach(() => { + jest.clearAllMocks(); + jest.resetAllMocks(); + jest.useRealTimers(); + }); + + it("should return true if all deployments are ready", async () => { + const deployments = { items: [ { metadata: { @@ -877,13 +882,9 @@ describe("namespaceDeploymentsReady", () => { const expected = true; const result = await namespaceDeploymentsReady(); - expect(result).toBe(expected); - - expect(mockK8s).toHaveBeenCalledTimes(1); }); }); - describe("parseTimeout", () => { const PREV = "a"; it("should return a number when a valid string number between 1 and 30 is provided", () => { From 7c2ae106b9d02072a25b5d95fb860c5fe14e1247 Mon Sep 17 00:00:00 2001 From: Sam Mayer Date: Thu, 21 Nov 2024 15:02:57 -0600 Subject: [PATCH 38/46] Extract large function to new file --- src/lib/checkDeploymentStatus.test.ts | 156 ++++++++++++++++++++++++++ src/lib/checkDeploymentStatus.ts | 29 +++++ src/lib/helpers.test.ts | 155 +------------------------ src/lib/helpers.ts | 29 +---- 4 files changed, 188 insertions(+), 181 deletions(-) create mode 100644 src/lib/checkDeploymentStatus.test.ts create mode 100644 src/lib/checkDeploymentStatus.ts diff --git a/src/lib/checkDeploymentStatus.test.ts b/src/lib/checkDeploymentStatus.test.ts new file mode 100644 index 000000000..3e4811ac2 --- /dev/null +++ b/src/lib/checkDeploymentStatus.test.ts @@ -0,0 +1,156 @@ +/* eslint-disable @typescript-eslint/no-unused-vars */ +import { describe, jest, beforeEach, afterEach, it, expect } from "@jest/globals"; +import { K8s } from "kubernetes-fluent-client"; +import { checkDeploymentStatus } from "./checkDeploymentStatus"; +import { namespaceDeploymentsReady } from "./helpers"; + +describe("checkDeploymentStatus", () => { + const mockK8s = jest.mocked(K8s); + + beforeEach(() => { + jest.useFakeTimers(); + }); + afterEach(() => { + jest.clearAllMocks(); + jest.resetAllMocks(); + jest.useRealTimers(); + }); + it("should return true if all deployments are ready", async () => { + const deployments = { + items: [ + { + metadata: { + name: "watcher", + namespace: "pepr-system", + }, + spec: { + replicas: 1, + }, + status: { + readyReplicas: 1, + }, + }, + { + metadata: { + name: "admission", + namespace: "pepr-system", + }, + spec: { + replicas: 2, + }, + status: { + readyReplicas: 2, + }, + }, + ], + }; + + const expected = true; + const result = await checkDeploymentStatus("pepr-system"); + expect(result).toBe(expected); + }); + + describe("when any deployments are not ready", () => { + it("should return false", async () => { + const deployments = { + items: [ + { + metadata: { + name: "watcher", + namespace: "pepr-system", + }, + spec: { + replicas: 1, + }, + status: { + readyReplicas: 1, + }, + }, + { + metadata: { + name: "admission", + namespace: "pepr-system", + }, + spec: { + replicas: 2, + }, + status: { + readyReplicas: 1, + }, + }, + ], + }; + + const expected = false; + const result = await checkDeploymentStatus("pepr-system"); + expect(result).toBe(expected); + }); + }); + + it("should call checkDeploymentStatus", async () => { + const deployments = { + items: [ + { + metadata: { + name: "watcher", + namespace: "pepr-system", + }, + spec: { + replicas: 1, + }, + status: { + readyReplicas: 1, + }, + }, + { + metadata: { + name: "admission", + namespace: "pepr-system", + }, + spec: { + replicas: 2, + }, + status: { + readyReplicas: 1, + }, + }, + ], + }; + + const deployments2 = { + items: [ + { + metadata: { + name: "watcher", + namespace: "pepr-system", + }, + spec: { + replicas: 1, + }, + status: { + readyReplicas: 1, + }, + }, + { + metadata: { + name: "admission", + namespace: "pepr-system", + }, + spec: { + replicas: 2, + }, + status: { + readyReplicas: 2, + }, + }, + ], + }; + + const expected = true; + const result = await namespaceDeploymentsReady(); + + expect(result).toBe(expected); + + expect(mockK8s).toHaveBeenCalledTimes(1); + }); +}); diff --git a/src/lib/checkDeploymentStatus.ts b/src/lib/checkDeploymentStatus.ts new file mode 100644 index 000000000..50de9da54 --- /dev/null +++ b/src/lib/checkDeploymentStatus.ts @@ -0,0 +1,29 @@ +// check to see if all replicas are ready for all deployments in the pepr-system namespace + +import { K8s, kind } from "kubernetes-fluent-client"; +import Log from "./logger"; + +// returns true if all deployments are ready, false otherwise +export async function checkDeploymentStatus(namespace: string) { + const deployments = await K8s(kind.Deployment).InNamespace(namespace).Get(); + let status = false; + let readyCount = 0; + + for (const deployment of deployments.items) { + const readyReplicas = deployment.status?.readyReplicas ? deployment.status?.readyReplicas : 0; + if (deployment.status?.readyReplicas !== deployment.spec?.replicas) { + Log.info( + `Waiting for deployment ${deployment.metadata?.name} rollout to finish: ${readyReplicas} of ${deployment.spec?.replicas} replicas are available`, + ); + } else { + Log.info( + `Deployment ${deployment.metadata?.name} rolled out: ${readyReplicas} of ${deployment.spec?.replicas} replicas are available`, + ); + readyCount++; + } + } + if (readyCount === deployments.items.length) { + status = true; + } + return status; +} diff --git a/src/lib/helpers.test.ts b/src/lib/helpers.test.ts index 94b27f146..dbaa0f444 100644 --- a/src/lib/helpers.test.ts +++ b/src/lib/helpers.test.ts @@ -8,7 +8,6 @@ import { addVerbIfNotExists, bindingAndCapabilityNSConflict, createRBACMap, - checkDeploymentStatus, filterNoMatchReason, dedent, generateWatchNamespaceError, @@ -29,8 +28,7 @@ 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 { K8s, GenericClass, KubernetesObject, kind, modelToGroupVersionKind } from "kubernetes-fluent-client"; -import { K8sInit } from "kubernetes-fluent-client/dist/fluent/types"; +import { K8s, KubernetesObject, kind, modelToGroupVersionKind } from "kubernetes-fluent-client"; import { defaultFilters, defaultKubernetesObject, defaultBinding } from "./filter/adjudicators/defaultTestObjects"; // import { defaultBinding, defaultFilters, defaultKubernetesObject } from "./filter/adjudicators/defaultTestObjects"; @@ -687,157 +685,6 @@ describe("namespaceComplianceValidator", () => { }); }); -describe("checkDeploymentStatus", () => { - const mockK8s = jest.mocked(K8s); - - beforeEach(() => { - jest.useFakeTimers(); - }); - afterEach(() => { - jest.clearAllMocks(); - jest.resetAllMocks(); - jest.useRealTimers(); - }); - it("should return true if all deployments are ready", async () => { - const deployments = { - items: [ - { - metadata: { - name: "watcher", - namespace: "pepr-system", - }, - spec: { - replicas: 1, - }, - status: { - readyReplicas: 1, - }, - }, - { - metadata: { - name: "admission", - namespace: "pepr-system", - }, - spec: { - replicas: 2, - }, - status: { - readyReplicas: 2, - }, - }, - ], - }; - - const expected = true; - const result = await checkDeploymentStatus("pepr-system"); - expect(result).toBe(expected); - }); - - describe("when any deployments are not ready", () => { - it("should return false", async () => { - const deployments = { - items: [ - { - metadata: { - name: "watcher", - namespace: "pepr-system", - }, - spec: { - replicas: 1, - }, - status: { - readyReplicas: 1, - }, - }, - { - metadata: { - name: "admission", - namespace: "pepr-system", - }, - spec: { - replicas: 2, - }, - status: { - readyReplicas: 1, - }, - }, - ], - }; - - const expected = false; - const result = await checkDeploymentStatus("pepr-system"); - expect(result).toBe(expected); - }); - }); - - it("should call checkDeploymentStatus", async () => { - const deployments = { - items: [ - { - metadata: { - name: "watcher", - namespace: "pepr-system", - }, - spec: { - replicas: 1, - }, - status: { - readyReplicas: 1, - }, - }, - { - metadata: { - name: "admission", - namespace: "pepr-system", - }, - spec: { - replicas: 2, - }, - status: { - readyReplicas: 1, - }, - }, - ], - }; - - const deployments2 = { - items: [ - { - metadata: { - name: "watcher", - namespace: "pepr-system", - }, - spec: { - replicas: 1, - }, - status: { - readyReplicas: 1, - }, - }, - { - metadata: { - name: "admission", - namespace: "pepr-system", - }, - spec: { - replicas: 2, - }, - status: { - readyReplicas: 2, - }, - }, - ], - }; - - const expected = true; - const result = await namespaceDeploymentsReady(); - - expect(result).toBe(expected); - - expect(mockK8s).toHaveBeenCalledTimes(1); - }); -}); - describe("namespaceDeploymentsReady", () => { const mockK8s = jest.mocked(K8s); diff --git a/src/lib/helpers.ts b/src/lib/helpers.ts index 7c3cc4f0c..89c32613d 100644 --- a/src/lib/helpers.ts +++ b/src/lib/helpers.ts @@ -2,7 +2,7 @@ // SPDX-License-Identifier: Apache-2.0 // SPDX-FileCopyrightText: 2023-Present The Pepr Authors -import { K8s, KubernetesObject, kind } from "kubernetes-fluent-client"; +import { KubernetesObject } from "kubernetes-fluent-client"; import Log from "./logger"; import { Binding, CapabilityExport } from "./types"; import { sanitizeResourceName } from "../sdk/sdk"; @@ -30,6 +30,7 @@ import { unbindableNamespaces, uncarryableNamespace, } from "./filter/adjudicators"; +import { checkDeploymentStatus } from "./checkDeploymentStatus"; export function matchesRegex(pattern: string, testString: string): boolean { return new RegExp(pattern).test(testString); @@ -294,32 +295,6 @@ export function namespaceComplianceValidator(capability: CapabilityExport, ignor } } -// check to see if all replicas are ready for all deployments in the pepr-system namespace -// returns true if all deployments are ready, false otherwise -export async function checkDeploymentStatus(namespace: string) { - const deployments = await K8s(kind.Deployment).InNamespace(namespace).Get(); - let status = false; - let readyCount = 0; - - for (const deployment of deployments.items) { - const readyReplicas = deployment.status?.readyReplicas ? deployment.status?.readyReplicas : 0; - if (deployment.status?.readyReplicas !== deployment.spec?.replicas) { - Log.info( - `Waiting for deployment ${deployment.metadata?.name} rollout to finish: ${readyReplicas} of ${deployment.spec?.replicas} replicas are available`, - ); - } else { - Log.info( - `Deployment ${deployment.metadata?.name} rolled out: ${readyReplicas} of ${deployment.spec?.replicas} replicas are available`, - ); - readyCount++; - } - } - if (readyCount === deployments.items.length) { - status = true; - } - return status; -} - // wait for all deployments in the pepr-system namespace to be ready export async function namespaceDeploymentsReady(namespace: string = "pepr-system") { Log.info(`Checking ${namespace} deployments status...`); From 7434ce88001ba857aebe1dc93661e04b165acf39 Mon Sep 17 00:00:00 2001 From: Sam Mayer Date: Thu, 21 Nov 2024 15:07:54 -0600 Subject: [PATCH 39/46] Remove mocks and set return type --- src/lib/checkDeploymentStatus.test.ts | 15 +-------------- src/lib/checkDeploymentStatus.ts | 2 +- 2 files changed, 2 insertions(+), 15 deletions(-) diff --git a/src/lib/checkDeploymentStatus.test.ts b/src/lib/checkDeploymentStatus.test.ts index 3e4811ac2..ea0ab4a29 100644 --- a/src/lib/checkDeploymentStatus.test.ts +++ b/src/lib/checkDeploymentStatus.test.ts @@ -1,20 +1,9 @@ /* eslint-disable @typescript-eslint/no-unused-vars */ -import { describe, jest, beforeEach, afterEach, it, expect } from "@jest/globals"; -import { K8s } from "kubernetes-fluent-client"; +import { describe, it, expect } from "@jest/globals"; import { checkDeploymentStatus } from "./checkDeploymentStatus"; import { namespaceDeploymentsReady } from "./helpers"; describe("checkDeploymentStatus", () => { - const mockK8s = jest.mocked(K8s); - - beforeEach(() => { - jest.useFakeTimers(); - }); - afterEach(() => { - jest.clearAllMocks(); - jest.resetAllMocks(); - jest.useRealTimers(); - }); it("should return true if all deployments are ready", async () => { const deployments = { items: [ @@ -150,7 +139,5 @@ describe("checkDeploymentStatus", () => { const result = await namespaceDeploymentsReady(); expect(result).toBe(expected); - - expect(mockK8s).toHaveBeenCalledTimes(1); }); }); diff --git a/src/lib/checkDeploymentStatus.ts b/src/lib/checkDeploymentStatus.ts index 50de9da54..f101fb9ee 100644 --- a/src/lib/checkDeploymentStatus.ts +++ b/src/lib/checkDeploymentStatus.ts @@ -4,7 +4,7 @@ import { K8s, kind } from "kubernetes-fluent-client"; import Log from "./logger"; // returns true if all deployments are ready, false otherwise -export async function checkDeploymentStatus(namespace: string) { +export async function checkDeploymentStatus(namespace: string): Promise { const deployments = await K8s(kind.Deployment).InNamespace(namespace).Get(); let status = false; let readyCount = 0; From 172ebf43461e48ff813bf970254f0732fdff24cb Mon Sep 17 00:00:00 2001 From: Sam Mayer Date: Thu, 21 Nov 2024 15:12:05 -0600 Subject: [PATCH 40/46] Update import from cli --- src/cli/build.ts | 3 ++- src/lib/checkDeploymentStatus.ts | 1 - 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/cli/build.ts b/src/cli/build.ts index 27160977e..f48f477ac 100644 --- a/src/cli/build.ts +++ b/src/cli/build.ts @@ -11,9 +11,10 @@ import { dependencies, version } from "./init/templates"; import { RootCmd } from "./root"; import { peprFormat } from "./format"; import { Option } from "commander"; -import { createDirectoryIfNotExists, validateCapabilityNames, parseTimeout } from "../lib/helpers"; +import { validateCapabilityNames, parseTimeout } from "../lib/helpers"; import { sanitizeResourceName } from "../sdk/sdk"; import { determineRbacMode } from "./build.helpers"; +import { createDirectoryIfNotExists } from "../lib/filesystemHelpers"; const peprTS = "pepr.ts"; let outputDir: string = "dist"; export type Reloader = (opts: BuildResult) => void | Promise; diff --git a/src/lib/checkDeploymentStatus.ts b/src/lib/checkDeploymentStatus.ts index f101fb9ee..980ac9a22 100644 --- a/src/lib/checkDeploymentStatus.ts +++ b/src/lib/checkDeploymentStatus.ts @@ -3,7 +3,6 @@ import { K8s, kind } from "kubernetes-fluent-client"; import Log from "./logger"; -// returns true if all deployments are ready, false otherwise export async function checkDeploymentStatus(namespace: string): Promise { const deployments = await K8s(kind.Deployment).InNamespace(namespace).Get(); let status = false; From ac8aa241cf577edf29261a2d96762b530904b334 Mon Sep 17 00:00:00 2001 From: Sam Mayer Date: Thu, 21 Nov 2024 15:14:21 -0600 Subject: [PATCH 41/46] Update imports --- src/lib/assets/index.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/lib/assets/index.ts b/src/lib/assets/index.ts index 3cb42fcef..4e13f6041 100644 --- a/src/lib/assets/index.ts +++ b/src/lib/assets/index.ts @@ -11,7 +11,7 @@ import { deploy } from "./deploy"; import { loadCapabilities } from "./loader"; import { allYaml, zarfYaml, overridesFile, zarfYamlChart } from "./yaml"; import { namespaceComplianceValidator, replaceString } from "../helpers"; -import { createDirectoryIfNotExists, dedent } from "../helpers"; +import { dedent } from "../helpers"; import { resolve } from "path"; import { chartYaml, @@ -27,6 +27,7 @@ import { apiTokenSecret, service, tlsSecret, watcherService } from "./networking import { watcher, moduleSecret } from "./pods"; import { clusterRoleBinding, serviceAccount, storeRole, storeRoleBinding } from "./rbac"; +import { createDirectoryIfNotExists } from "../filesystemHelpers"; export class Assets { readonly name: string; readonly tls: TLSOut; From 06ea6cc4e977df559c224d94d935649cf50831a9 Mon Sep 17 00:00:00 2001 From: Sam Mayer Date: Thu, 21 Nov 2024 15:53:05 -0600 Subject: [PATCH 42/46] Update imports after file move --- src/lib/deploymentChecks.test.ts | 3 +-- src/lib/helpers.test.ts | 2 +- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/src/lib/deploymentChecks.test.ts b/src/lib/deploymentChecks.test.ts index 65d095ade..db97e0270 100644 --- a/src/lib/deploymentChecks.test.ts +++ b/src/lib/deploymentChecks.test.ts @@ -1,7 +1,6 @@ /* eslint-disable @typescript-eslint/no-unused-vars */ import { describe, jest, beforeEach, afterEach, it, expect } from "@jest/globals"; -import { checkDeploymentStatus } from "./deploymentChecks"; -import { namespaceDeploymentsReady } from "./helpers"; +import { checkDeploymentStatus, namespaceDeploymentsReady } from "./deploymentChecks"; import { GenericClass, K8s, KubernetesObject } from "kubernetes-fluent-client"; import { K8sInit } from "kubernetes-fluent-client/dist/fluent/types"; diff --git a/src/lib/helpers.test.ts b/src/lib/helpers.test.ts index dbaa0f444..f95a2711d 100644 --- a/src/lib/helpers.test.ts +++ b/src/lib/helpers.test.ts @@ -15,7 +15,6 @@ import { hasEveryOverlap, ignoredNamespaceConflict, matchesRegex, - namespaceDeploymentsReady, namespaceComplianceValidator, parseTimeout, replaceString, @@ -30,6 +29,7 @@ import { expect, describe, jest, beforeEach, afterEach, it } from "@jest/globals import { SpiedFunction } from "jest-mock"; import { K8s, KubernetesObject, kind, modelToGroupVersionKind } from "kubernetes-fluent-client"; import { defaultFilters, defaultKubernetesObject, defaultBinding } from "./filter/adjudicators/defaultTestObjects"; +import { namespaceDeploymentsReady } from "./deploymentChecks"; // import { defaultBinding, defaultFilters, defaultKubernetesObject } from "./filter/adjudicators/defaultTestObjects"; export const callback = () => undefined; From 0eb3f1ecfc244ccfe96e0571050b67d6ab90a827 Mon Sep 17 00:00:00 2001 From: Sam Mayer Date: Thu, 21 Nov 2024 16:01:22 -0600 Subject: [PATCH 43/46] Move some helper functions to deploymentChecks --- src/cli/deploy.ts | 3 +- src/lib/deploymentChecks.test.ts | 243 +++++++++++++++++++++++++++++++ src/lib/deploymentChecks.ts | 43 ++++++ src/lib/helpers.test.ts | 237 +----------------------------- src/lib/helpers.ts | 42 +----- 5 files changed, 290 insertions(+), 278 deletions(-) create mode 100644 src/lib/deploymentChecks.test.ts create mode 100644 src/lib/deploymentChecks.ts diff --git a/src/cli/deploy.ts b/src/cli/deploy.ts index a49996c17..526f7178e 100644 --- a/src/cli/deploy.ts +++ b/src/cli/deploy.ts @@ -6,10 +6,11 @@ import prompt from "prompts"; import { Assets } from "../lib/assets"; import { buildModule } from "./build"; import { RootCmd } from "./root"; -import { validateCapabilityNames, namespaceDeploymentsReady } from "../lib/helpers"; +import { validateCapabilityNames } from "../lib/helpers"; import { ImagePullSecret } from "../lib/types"; import { sanitizeName } from "./init/utils"; import { deployImagePullSecret } from "../lib/assets/deploy"; +import { namespaceDeploymentsReady } from "../lib/deploymentChecks"; export default function (program: RootCmd) { program diff --git a/src/lib/deploymentChecks.test.ts b/src/lib/deploymentChecks.test.ts new file mode 100644 index 000000000..74e191aa2 --- /dev/null +++ b/src/lib/deploymentChecks.test.ts @@ -0,0 +1,243 @@ +import { describe, jest, test, beforeEach, afterEach, expect } from "@jest/globals"; +import { K8s, GenericClass, KubernetesObject } from "kubernetes-fluent-client"; +import { K8sInit } from "kubernetes-fluent-client/dist/fluent/types"; +import { checkDeploymentStatus, namespaceDeploymentsReady } from "./deploymentChecks"; + +jest.mock("kubernetes-fluent-client", () => { + return { + K8s: jest.fn(), + kind: jest.fn(), + }; +}); + +describe("namespaceDeploymentsReady", () => { + const mockK8s = jest.mocked(K8s); + + beforeEach(() => { + jest.useFakeTimers(); + }); + afterEach(() => { + jest.clearAllMocks(); + jest.resetAllMocks(); + jest.useRealTimers(); + }); + + test("should return true if all deployments are ready", async () => { + const deployments = { + items: [ + { + metadata: { + name: "watcher", + namespace: "pepr-system", + }, + spec: { + replicas: 1, + }, + status: { + readyReplicas: 1, + }, + }, + { + metadata: { + name: "admission", + namespace: "pepr-system", + }, + spec: { + replicas: 2, + }, + status: { + readyReplicas: 2, + }, + }, + ], + }; + + mockK8s.mockImplementation(() => { + return { + InNamespace: jest.fn().mockReturnThis(), + Get: () => deployments, + } as unknown as K8sInit; + }); + + const expected = true; + const result = await namespaceDeploymentsReady(); + expect(result).toBe(expected); + }); + + test("should call checkDeploymentStatus if any deployments are not ready", async () => { + const deployments = { + items: [ + { + metadata: { + name: "watcher", + namespace: "pepr-system", + }, + spec: { + replicas: 1, + }, + status: { + readyReplicas: 1, + }, + }, + { + metadata: { + name: "admission", + namespace: "pepr-system", + }, + spec: { + replicas: 2, + }, + status: { + readyReplicas: 1, + }, + }, + ], + }; + + const deployments2 = { + items: [ + { + metadata: { + name: "watcher", + namespace: "pepr-system", + }, + spec: { + replicas: 1, + }, + status: { + readyReplicas: 1, + }, + }, + { + metadata: { + name: "admission", + namespace: "pepr-system", + }, + spec: { + replicas: 2, + }, + status: { + readyReplicas: 2, + }, + }, + ], + }; + + mockK8s + .mockImplementation(() => { + return { + InNamespace: jest.fn().mockReturnThis(), + Get: () => deployments, + } as unknown as K8sInit; + }) + .mockImplementation(() => { + return { + InNamespace: jest.fn().mockReturnThis(), + Get: () => deployments2, + } as unknown as K8sInit; + }); + + const expected = true; + const result = await namespaceDeploymentsReady(); + + expect(result).toBe(expected); + + expect(mockK8s).toHaveBeenCalledTimes(1); + }); +}); + +describe("checkDeploymentStatus", () => { + const mockK8s = jest.mocked(K8s); + + beforeEach(() => { + jest.useFakeTimers(); + }); + afterEach(() => { + jest.clearAllMocks(); + jest.resetAllMocks(); + jest.useRealTimers(); + }); + test("should return true if all deployments are ready", async () => { + const deployments = { + items: [ + { + metadata: { + name: "watcher", + namespace: "pepr-system", + }, + spec: { + replicas: 1, + }, + status: { + readyReplicas: 1, + }, + }, + { + metadata: { + name: "admission", + namespace: "pepr-system", + }, + spec: { + replicas: 2, + }, + status: { + readyReplicas: 2, + }, + }, + ], + }; + + mockK8s.mockImplementation(() => { + return { + InNamespace: jest.fn().mockReturnThis(), + Get: () => deployments, + } as unknown as K8sInit; + }); + + const expected = true; + const result = await checkDeploymentStatus("pepr-system"); + expect(result).toBe(expected); + }); + + test("should return false if any deployments are not ready", async () => { + const deployments = { + items: [ + { + metadata: { + name: "watcher", + namespace: "pepr-system", + }, + spec: { + replicas: 1, + }, + status: { + readyReplicas: 1, + }, + }, + { + metadata: { + name: "admission", + namespace: "pepr-system", + }, + spec: { + replicas: 2, + }, + status: { + readyReplicas: 1, + }, + }, + ], + }; + + mockK8s.mockImplementation(() => { + return { + InNamespace: jest.fn().mockReturnThis(), + Get: () => deployments, + } as unknown as K8sInit; + }); + + const expected = false; + const result = await checkDeploymentStatus("pepr-system"); + expect(result).toBe(expected); + }); +}); diff --git a/src/lib/deploymentChecks.ts b/src/lib/deploymentChecks.ts new file mode 100644 index 000000000..282eb5f0d --- /dev/null +++ b/src/lib/deploymentChecks.ts @@ -0,0 +1,43 @@ +// check to see if all replicas are ready for all deployments in the pepr-system namespace + +import { K8s, kind } from "kubernetes-fluent-client"; +import Log from "./logger"; + +// returns true if all deployments are ready, false otherwise +export async function checkDeploymentStatus(namespace: string) { + const deployments = await K8s(kind.Deployment).InNamespace(namespace).Get(); + let status = false; + let readyCount = 0; + + for (const deployment of deployments.items) { + const readyReplicas = deployment.status?.readyReplicas ? deployment.status?.readyReplicas : 0; + if (deployment.status?.readyReplicas !== deployment.spec?.replicas) { + Log.info( + `Waiting for deployment ${deployment.metadata?.name} rollout to finish: ${readyReplicas} of ${deployment.spec?.replicas} replicas are available`, + ); + } else { + Log.info( + `Deployment ${deployment.metadata?.name} rolled out: ${readyReplicas} of ${deployment.spec?.replicas} replicas are available`, + ); + readyCount++; + } + } + if (readyCount === deployments.items.length) { + status = true; + } + return status; +} + +// wait for all deployments in the pepr-system namespace to be ready +export async function namespaceDeploymentsReady(namespace: string = "pepr-system") { + Log.info(`Checking ${namespace} deployments status...`); + let ready = false; + while (!ready) { + ready = await checkDeploymentStatus(namespace); + if (ready) { + return ready; + } + await new Promise(resolve => setTimeout(resolve, 1000)); + } + Log.info(`All ${namespace} deployments are ready`); +} diff --git a/src/lib/helpers.test.ts b/src/lib/helpers.test.ts index a960adb2c..e093c24c4 100644 --- a/src/lib/helpers.test.ts +++ b/src/lib/helpers.test.ts @@ -8,7 +8,6 @@ import { bindingAndCapabilityNSConflict, createDirectoryIfNotExists, createRBACMap, - checkDeploymentStatus, filterNoMatchReason, dedent, generateWatchNamespaceError, @@ -16,7 +15,6 @@ import { hasEveryOverlap, ignoredNamespaceConflict, matchesRegex, - namespaceDeploymentsReady, namespaceComplianceValidator, parseTimeout, replaceString, @@ -30,8 +28,7 @@ import * as fc from "fast-check"; import { expect, describe, test, jest, beforeEach, afterEach } from "@jest/globals"; import { promises as fs } from "fs"; import { SpiedFunction } from "jest-mock"; -import { K8s, GenericClass, KubernetesObject, kind } from "kubernetes-fluent-client"; -import { K8sInit } from "kubernetes-fluent-client/dist/fluent/types"; +import { KubernetesObject, kind } from "kubernetes-fluent-client"; export const callback = () => undefined; @@ -722,238 +719,6 @@ describe("namespaceComplianceValidator", () => { }); }); -describe("checkDeploymentStatus", () => { - const mockK8s = jest.mocked(K8s); - - beforeEach(() => { - jest.useFakeTimers(); - }); - afterEach(() => { - jest.clearAllMocks(); - jest.resetAllMocks(); - jest.useRealTimers(); - }); - test("should return true if all deployments are ready", async () => { - const deployments = { - items: [ - { - metadata: { - name: "watcher", - namespace: "pepr-system", - }, - spec: { - replicas: 1, - }, - status: { - readyReplicas: 1, - }, - }, - { - metadata: { - name: "admission", - namespace: "pepr-system", - }, - spec: { - replicas: 2, - }, - status: { - readyReplicas: 2, - }, - }, - ], - }; - - mockK8s.mockImplementation(() => { - return { - InNamespace: jest.fn().mockReturnThis(), - Get: () => deployments, - } as unknown as K8sInit; - }); - - const expected = true; - const result = await checkDeploymentStatus("pepr-system"); - expect(result).toBe(expected); - }); - - test("should return false if any deployments are not ready", async () => { - const deployments = { - items: [ - { - metadata: { - name: "watcher", - namespace: "pepr-system", - }, - spec: { - replicas: 1, - }, - status: { - readyReplicas: 1, - }, - }, - { - metadata: { - name: "admission", - namespace: "pepr-system", - }, - spec: { - replicas: 2, - }, - status: { - readyReplicas: 1, - }, - }, - ], - }; - - mockK8s.mockImplementation(() => { - return { - InNamespace: jest.fn().mockReturnThis(), - Get: () => deployments, - } as unknown as K8sInit; - }); - - const expected = false; - const result = await checkDeploymentStatus("pepr-system"); - expect(result).toBe(expected); - }); -}); - -describe("namespaceDeploymentsReady", () => { - const mockK8s = jest.mocked(K8s); - - beforeEach(() => { - jest.useFakeTimers(); - }); - afterEach(() => { - jest.clearAllMocks(); - jest.resetAllMocks(); - jest.useRealTimers(); - }); - - test("should return true if all deployments are ready", async () => { - const deployments = { - items: [ - { - metadata: { - name: "watcher", - namespace: "pepr-system", - }, - spec: { - replicas: 1, - }, - status: { - readyReplicas: 1, - }, - }, - { - metadata: { - name: "admission", - namespace: "pepr-system", - }, - spec: { - replicas: 2, - }, - status: { - readyReplicas: 2, - }, - }, - ], - }; - - mockK8s.mockImplementation(() => { - return { - InNamespace: jest.fn().mockReturnThis(), - Get: () => deployments, - } as unknown as K8sInit; - }); - - const expected = true; - const result = await namespaceDeploymentsReady(); - expect(result).toBe(expected); - }); - - test("should call checkDeploymentStatus if any deployments are not ready", async () => { - const deployments = { - items: [ - { - metadata: { - name: "watcher", - namespace: "pepr-system", - }, - spec: { - replicas: 1, - }, - status: { - readyReplicas: 1, - }, - }, - { - metadata: { - name: "admission", - namespace: "pepr-system", - }, - spec: { - replicas: 2, - }, - status: { - readyReplicas: 1, - }, - }, - ], - }; - - const deployments2 = { - items: [ - { - metadata: { - name: "watcher", - namespace: "pepr-system", - }, - spec: { - replicas: 1, - }, - status: { - readyReplicas: 1, - }, - }, - { - metadata: { - name: "admission", - namespace: "pepr-system", - }, - spec: { - replicas: 2, - }, - status: { - readyReplicas: 2, - }, - }, - ], - }; - - mockK8s - .mockImplementation(() => { - return { - InNamespace: jest.fn().mockReturnThis(), - Get: () => deployments, - } as unknown as K8sInit; - }) - .mockImplementation(() => { - return { - InNamespace: jest.fn().mockReturnThis(), - Get: () => deployments2, - } as unknown as K8sInit; - }); - - const expected = true; - const result = await namespaceDeploymentsReady(); - - expect(result).toBe(expected); - - expect(mockK8s).toHaveBeenCalledTimes(1); - }); -}); - describe("parseTimeout", () => { const PREV = "a"; test("should return a number when a valid string number between 1 and 30 is provided", () => { diff --git a/src/lib/helpers.ts b/src/lib/helpers.ts index f401b87a1..1709909f9 100644 --- a/src/lib/helpers.ts +++ b/src/lib/helpers.ts @@ -2,7 +2,7 @@ // SPDX-FileCopyrightText: 2023-Present The Pepr Authors import { promises as fs } from "fs"; -import { K8s, KubernetesObject, kind } from "kubernetes-fluent-client"; +import { KubernetesObject } from "kubernetes-fluent-client"; import Log from "./logger"; import { Binding, CapabilityExport } from "./types"; import { sanitizeResourceName } from "../sdk/sdk"; @@ -310,46 +310,6 @@ export function namespaceComplianceValidator(capability: CapabilityExport, ignor } } -// check to see if all replicas are ready for all deployments in the pepr-system namespace -// returns true if all deployments are ready, false otherwise -export async function checkDeploymentStatus(namespace: string) { - const deployments = await K8s(kind.Deployment).InNamespace(namespace).Get(); - let status = false; - let readyCount = 0; - - for (const deployment of deployments.items) { - const readyReplicas = deployment.status?.readyReplicas ? deployment.status?.readyReplicas : 0; - if (deployment.status?.readyReplicas !== deployment.spec?.replicas) { - Log.info( - `Waiting for deployment ${deployment.metadata?.name} rollout to finish: ${readyReplicas} of ${deployment.spec?.replicas} replicas are available`, - ); - } else { - Log.info( - `Deployment ${deployment.metadata?.name} rolled out: ${readyReplicas} of ${deployment.spec?.replicas} replicas are available`, - ); - readyCount++; - } - } - if (readyCount === deployments.items.length) { - status = true; - } - return status; -} - -// wait for all deployments in the pepr-system namespace to be ready -export async function namespaceDeploymentsReady(namespace: string = "pepr-system") { - Log.info(`Checking ${namespace} deployments status...`); - let ready = false; - while (!ready) { - ready = await checkDeploymentStatus(namespace); - if (ready) { - return ready; - } - await new Promise(resolve => setTimeout(resolve, 1000)); - } - Log.info(`All ${namespace} deployments are ready`); -} - // check if secret is over the size limit export function secretOverLimit(str: string): boolean { const encoder = new TextEncoder(); From cb1d7bb57e919ad659d8504dbd7662a74937eea3 Mon Sep 17 00:00:00 2001 From: Sam Mayer Date: Thu, 21 Nov 2024 16:10:22 -0600 Subject: [PATCH 44/46] Remove file restructure for next commit --- src/lib/deploymentChecks.test.ts | 175 ------------------------------- src/lib/deploymentChecks.ts | 42 -------- 2 files changed, 217 deletions(-) delete mode 100644 src/lib/deploymentChecks.test.ts delete mode 100644 src/lib/deploymentChecks.ts diff --git a/src/lib/deploymentChecks.test.ts b/src/lib/deploymentChecks.test.ts deleted file mode 100644 index db97e0270..000000000 --- a/src/lib/deploymentChecks.test.ts +++ /dev/null @@ -1,175 +0,0 @@ -/* eslint-disable @typescript-eslint/no-unused-vars */ -import { describe, jest, beforeEach, afterEach, it, expect } from "@jest/globals"; -import { checkDeploymentStatus, namespaceDeploymentsReady } from "./deploymentChecks"; -import { GenericClass, K8s, KubernetesObject } from "kubernetes-fluent-client"; -import { K8sInit } from "kubernetes-fluent-client/dist/fluent/types"; - -const mockK8s = jest.mocked(K8s); - -describe("checkDeploymentStatus", () => { - it("should return true if all deployments are ready", async () => { - const deployments = { - items: [ - { - metadata: { - name: "watcher", - namespace: "pepr-system", - }, - spec: { - replicas: 1, - }, - status: { - readyReplicas: 1, - }, - }, - { - metadata: { - name: "admission", - namespace: "pepr-system", - }, - spec: { - replicas: 2, - }, - status: { - readyReplicas: 2, - }, - }, - ], - }; - - const expected = true; - const result = await checkDeploymentStatus("pepr-system"); - expect(result).toBe(expected); - }); - - describe("when any deployments are not ready", () => { - it("should return false", async () => { - const deployments = { - items: [ - { - metadata: { - name: "watcher", - namespace: "pepr-system", - }, - spec: { - replicas: 1, - }, - status: { - readyReplicas: 1, - }, - }, - { - metadata: { - name: "admission", - namespace: "pepr-system", - }, - spec: { - replicas: 2, - }, - status: { - readyReplicas: 1, - }, - }, - ], - }; - - const expected = false; - const result = await checkDeploymentStatus("pepr-system"); - expect(result).toBe(expected); - }); - }); - - describe("namespaceDeployments ready", () => { - const mockK8s = jest.mocked(K8s); - - beforeEach(() => { - jest.useFakeTimers(); - }); - afterEach(() => { - jest.clearAllMocks(); - jest.resetAllMocks(); - jest.useRealTimers(); - }); - - it("should call checkDeploymentStatus", async () => { - const deployments = { - items: [ - { - metadata: { - name: "watcher", - namespace: "pepr-system", - }, - spec: { - replicas: 1, - }, - status: { - readyReplicas: 1, - }, - }, - { - metadata: { - name: "admission", - namespace: "pepr-system", - }, - spec: { - replicas: 2, - }, - status: { - readyReplicas: 1, - }, - }, - ], - }; - - const deployments2 = { - items: [ - { - metadata: { - name: "watcher", - namespace: "pepr-system", - }, - spec: { - replicas: 1, - }, - status: { - readyReplicas: 1, - }, - }, - { - metadata: { - name: "admission", - namespace: "pepr-system", - }, - spec: { - replicas: 2, - }, - status: { - readyReplicas: 2, - }, - }, - ], - }; - - mockK8s - .mockImplementation(() => { - return { - InNamespace: jest.fn().mockReturnThis(), - Get: () => deployments, - } as unknown as K8sInit; - }) - .mockImplementation(() => { - return { - InNamespace: jest.fn().mockReturnThis(), - Get: () => deployments2, - } as unknown as K8sInit; - }); - - const expected = true; - const result = await namespaceDeploymentsReady(); - - expect(result).toBe(expected); - - expect(mockK8s).toHaveBeenCalledTimes(1); - }); - }); -}); diff --git a/src/lib/deploymentChecks.ts b/src/lib/deploymentChecks.ts deleted file mode 100644 index 95e3708f3..000000000 --- a/src/lib/deploymentChecks.ts +++ /dev/null @@ -1,42 +0,0 @@ -// check to see if all replicas are ready for all deployments in the pepr-system namespace - -import { K8s, kind } from "kubernetes-fluent-client"; -import Log from "./logger"; - -export async function checkDeploymentStatus(namespace: string): Promise { - const deployments = await K8s(kind.Deployment).InNamespace(namespace).Get(); - let status = false; - let readyCount = 0; - - for (const deployment of deployments.items) { - const readyReplicas = deployment.status?.readyReplicas ? deployment.status?.readyReplicas : 0; - if (deployment.status?.readyReplicas !== deployment.spec?.replicas) { - Log.info( - `Waiting for deployment ${deployment.metadata?.name} rollout to finish: ${readyReplicas} of ${deployment.spec?.replicas} replicas are available`, - ); - } else { - Log.info( - `Deployment ${deployment.metadata?.name} rolled out: ${readyReplicas} of ${deployment.spec?.replicas} replicas are available`, - ); - readyCount++; - } - } - if (readyCount === deployments.items.length) { - status = true; - } - return status; -} - -// wait for all deployments in the pepr-system namespace to be ready -export async function namespaceDeploymentsReady(namespace: string = "pepr-system") { - Log.info(`Checking ${namespace} deployments status...`); - let ready = false; - while (!ready) { - ready = await checkDeploymentStatus(namespace); - if (ready) { - return ready; - } - await new Promise(resolve => setTimeout(resolve, 1000)); - } - Log.info(`All ${namespace} deployments are ready`); -} From 257c4b8f90827e8ba2d2c49932df545f38378247 Mon Sep 17 00:00:00 2001 From: Sam Mayer Date: Fri, 22 Nov 2024 13:48:39 -0600 Subject: [PATCH 45/46] Undo regexp work to revisit on a separate PR --- src/lib/capability.ts | 8 ++++---- src/lib/filter/adjudicators.ts | 23 ++++++++--------------- src/lib/helpers.ts | 2 +- src/lib/types.ts | 4 ++-- 4 files changed, 15 insertions(+), 22 deletions(-) diff --git a/src/lib/capability.ts b/src/lib/capability.ts index 0ca878bbd..61aa26e9b 100644 --- a/src/lib/capability.ts +++ b/src/lib/capability.ts @@ -345,9 +345,9 @@ export class Capability implements CapabilityExport { return { ...commonChain, WithName, WithNameRegex }; } - function InNamespaceRegex(...namespaces: string[]): BindingWithName { + function InNamespaceRegex(...namespaces: RegExp[]): BindingWithName { Log.debug(`Add regex namespaces filter ${namespaces}`, prefix); - binding.filters.regexNamespaces.push(...namespaces); + binding.filters.regexNamespaces.push(...namespaces.map(regex => regex.source)); return { ...commonChain, WithName, WithNameRegex }; } @@ -357,9 +357,9 @@ export class Capability implements CapabilityExport { return commonChain; } - function WithNameRegex(regexName: string): BindingFilter { + function WithNameRegex(regexName: RegExp): BindingFilter { Log.debug(`Add regex name filter ${regexName}`, prefix); - binding.filters.regexName = regexName; + binding.filters.regexName = regexName.source; return commonChain; } diff --git a/src/lib/filter/adjudicators.ts b/src/lib/filter/adjudicators.ts index f115d25c0..2623bb32f 100644 --- a/src/lib/filter/adjudicators.ts +++ b/src/lib/filter/adjudicators.ts @@ -117,11 +117,8 @@ export const definesNameRegex = pipe(definedNameRegex, equals(""), not); export const definedNamespaces = pipe(binding => binding?.filters?.namespaces, defaultTo([])); export const definesNamespaces = pipe(definedNamespaces, equals([]), not); -export const definedNamespaceRegexes = pipe( - (binding: Binding): string[] => binding.filters.regexNamespaces, - defaultTo([]), -); -export const definesNamespaceRegexes = pipe(definedNamespaceRegexes, equals([] as string[]), not); +export const definedNamespaceRegexes = pipe(binding => binding?.filters?.regexNamespaces, defaultTo([])); +export const definesNamespaceRegexes = pipe(definedNamespaceRegexes, equals([]), not); export const definedAnnotations = pipe((binding: Partial) => binding?.filters?.annotations, defaultTo({})); export const definesAnnotations = pipe(definedAnnotations, equals({}), not); @@ -205,16 +202,12 @@ export const mismatchedNamespace = allPass([ export const mismatchedNamespaceRegex = allPass([ // Check if `definesNamespaceRegexes` returns true pipe(nthArg(0), definesNamespaceRegexes), - - // Check if no regex matches - (binding: Binding, kubernetesObject: KubernetesObject) => { - // Convert definedNamespaceRegexes(binding) from string[] to RegExp[] - const regexArray = definedNamespaceRegexes(binding).map(regexStr => new RegExp(regexStr)); - - // Check if no regex matches the namespace of the Kubernetes object - const result = not(any((regEx: RegExp) => regEx.test(carriedNamespace(kubernetesObject)), regexArray)); - return result; - }, + pipe((binding, kubernetesObject) => + pipe( + any((regEx: string) => new RegExp(regEx).test(carriedNamespace(kubernetesObject))), + not, + )(definedNamespaceRegexes(binding)), + ), ]); export const metasMismatch = pipe( diff --git a/src/lib/helpers.ts b/src/lib/helpers.ts index 5af8dff46..c5fdf05c9 100644 --- a/src/lib/helpers.ts +++ b/src/lib/helpers.ts @@ -237,7 +237,7 @@ export function generateWatchNamespaceError( return err.replace(/\.([^ ])/g, ". $1"); } -// namespaceComplianceValidator ensures that capability binds respect ignored and capability namespaces +// namespaceComplianceValidator ensures that capability bindings respect ignored and capability namespaces export function namespaceComplianceValidator(capability: CapabilityExport, ignoredNamespaces?: string[]) { const { namespaces: capabilityNamespaces, bindings, name } = capability; const bindingNamespaces: string[] = bindings.flatMap((binding: Binding) => binding.filters.namespaces); diff --git a/src/lib/types.ts b/src/lib/types.ts index 1d2e11238..7ce5a0a8a 100644 --- a/src/lib/types.ts +++ b/src/lib/types.ts @@ -143,14 +143,14 @@ export type BindingWithName = BindingFilter & { /** Only apply the action if the resource name matches the specified name. */ WithName: (name: string) => BindingFilter; /** Only apply the action if the resource name matches the specified regex name. */ - WithNameRegex: (name: string) => BindingFilter; + WithNameRegex: (name: RegExp) => BindingFilter; }; export type BindingAll = BindingWithName & { /** Only apply the action if the resource is in one of the specified namespaces.*/ InNamespace: (...namespaces: string[]) => BindingWithName; /** Only apply the action if the resource is in one of the specified regex namespaces.*/ - InNamespaceRegex: (...namespaces: string[]) => BindingWithName; + InNamespaceRegex: (...namespaces: RegExp[]) => BindingWithName; }; export type CommonActionChain = MutateActionChain & { From 7f1bbec87c7544f128484c3a3495711590939e49 Mon Sep 17 00:00:00 2001 From: Sam Mayer Date: Fri, 22 Nov 2024 14:19:03 -0600 Subject: [PATCH 46/46] Revert "Undo regexp work to revisit on a separate PR" This reverts commit 257c4b8f90827e8ba2d2c49932df545f38378247. --- src/lib/capability.ts | 8 ++++---- src/lib/filter/adjudicators.ts | 23 +++++++++++++++-------- src/lib/helpers.ts | 2 +- src/lib/types.ts | 4 ++-- 4 files changed, 22 insertions(+), 15 deletions(-) diff --git a/src/lib/capability.ts b/src/lib/capability.ts index 61aa26e9b..0ca878bbd 100644 --- a/src/lib/capability.ts +++ b/src/lib/capability.ts @@ -345,9 +345,9 @@ export class Capability implements CapabilityExport { return { ...commonChain, WithName, WithNameRegex }; } - function InNamespaceRegex(...namespaces: RegExp[]): BindingWithName { + function InNamespaceRegex(...namespaces: string[]): BindingWithName { Log.debug(`Add regex namespaces filter ${namespaces}`, prefix); - binding.filters.regexNamespaces.push(...namespaces.map(regex => regex.source)); + binding.filters.regexNamespaces.push(...namespaces); return { ...commonChain, WithName, WithNameRegex }; } @@ -357,9 +357,9 @@ export class Capability implements CapabilityExport { return commonChain; } - function WithNameRegex(regexName: RegExp): BindingFilter { + function WithNameRegex(regexName: string): BindingFilter { Log.debug(`Add regex name filter ${regexName}`, prefix); - binding.filters.regexName = regexName.source; + binding.filters.regexName = regexName; return commonChain; } diff --git a/src/lib/filter/adjudicators.ts b/src/lib/filter/adjudicators.ts index 2623bb32f..f115d25c0 100644 --- a/src/lib/filter/adjudicators.ts +++ b/src/lib/filter/adjudicators.ts @@ -117,8 +117,11 @@ export const definesNameRegex = pipe(definedNameRegex, equals(""), not); export const definedNamespaces = pipe(binding => binding?.filters?.namespaces, defaultTo([])); export const definesNamespaces = pipe(definedNamespaces, equals([]), not); -export const definedNamespaceRegexes = pipe(binding => binding?.filters?.regexNamespaces, defaultTo([])); -export const definesNamespaceRegexes = pipe(definedNamespaceRegexes, equals([]), not); +export const definedNamespaceRegexes = pipe( + (binding: Binding): string[] => binding.filters.regexNamespaces, + defaultTo([]), +); +export const definesNamespaceRegexes = pipe(definedNamespaceRegexes, equals([] as string[]), not); export const definedAnnotations = pipe((binding: Partial) => binding?.filters?.annotations, defaultTo({})); export const definesAnnotations = pipe(definedAnnotations, equals({}), not); @@ -202,12 +205,16 @@ export const mismatchedNamespace = allPass([ export const mismatchedNamespaceRegex = allPass([ // Check if `definesNamespaceRegexes` returns true pipe(nthArg(0), definesNamespaceRegexes), - pipe((binding, kubernetesObject) => - pipe( - any((regEx: string) => new RegExp(regEx).test(carriedNamespace(kubernetesObject))), - not, - )(definedNamespaceRegexes(binding)), - ), + + // Check if no regex matches + (binding: Binding, kubernetesObject: KubernetesObject) => { + // Convert definedNamespaceRegexes(binding) from string[] to RegExp[] + const regexArray = definedNamespaceRegexes(binding).map(regexStr => new RegExp(regexStr)); + + // Check if no regex matches the namespace of the Kubernetes object + const result = not(any((regEx: RegExp) => regEx.test(carriedNamespace(kubernetesObject)), regexArray)); + return result; + }, ]); export const metasMismatch = pipe( diff --git a/src/lib/helpers.ts b/src/lib/helpers.ts index c5fdf05c9..5af8dff46 100644 --- a/src/lib/helpers.ts +++ b/src/lib/helpers.ts @@ -237,7 +237,7 @@ export function generateWatchNamespaceError( return err.replace(/\.([^ ])/g, ". $1"); } -// namespaceComplianceValidator ensures that capability bindings respect ignored and capability namespaces +// namespaceComplianceValidator ensures that capability binds respect ignored and capability namespaces export function namespaceComplianceValidator(capability: CapabilityExport, ignoredNamespaces?: string[]) { const { namespaces: capabilityNamespaces, bindings, name } = capability; const bindingNamespaces: string[] = bindings.flatMap((binding: Binding) => binding.filters.namespaces); diff --git a/src/lib/types.ts b/src/lib/types.ts index 7ce5a0a8a..1d2e11238 100644 --- a/src/lib/types.ts +++ b/src/lib/types.ts @@ -143,14 +143,14 @@ export type BindingWithName = BindingFilter & { /** Only apply the action if the resource name matches the specified name. */ WithName: (name: string) => BindingFilter; /** Only apply the action if the resource name matches the specified regex name. */ - WithNameRegex: (name: RegExp) => BindingFilter; + WithNameRegex: (name: string) => BindingFilter; }; export type BindingAll = BindingWithName & { /** Only apply the action if the resource is in one of the specified namespaces.*/ InNamespace: (...namespaces: string[]) => BindingWithName; /** Only apply the action if the resource is in one of the specified regex namespaces.*/ - InNamespaceRegex: (...namespaces: RegExp[]) => BindingWithName; + InNamespaceRegex: (...namespaces: string[]) => BindingWithName; }; export type CommonActionChain = MutateActionChain & {