From c9c152b5e36565e8ad2fbc3efad2589602ea6b6a Mon Sep 17 00:00:00 2001 From: Barrett <81570928+btlghrants@users.noreply.github.com> Date: Mon, 16 Sep 2024 09:40:50 -0500 Subject: [PATCH] feat: update pepr reconcile strategy (#1127) ## Description Adds some new options for the PEPR_RECONCILE_STRATEGY configuration option. End to End Test - Pepr Excellent Examples - [hello-pepr-reconcile-kind](https://github.com/defenseunicorns/pepr-excellent-examples/hello-pepr-reconcile-kind) - [hello-pepr-reconcile-kindns](https://github.com/defenseunicorns/pepr-excellent-examples/hello-pepr-reconcile-kindns) - [hello-pepr-reconcile-kindnsname](https://github.com/defenseunicorns/pepr-excellent-examples/hello-pepr-reconcile-kindnsname) - [hello-pepr-reconcile-global](https://github.com/defenseunicorns/pepr-excellent-examples/hello-pepr-reconcile-global) ## Related Issue Resolves #1126 ## Type of change - [ ] Bug fix (non-breaking change which fixes an issue) - [x] New feature (non-breaking change which adds functionality) - [ ] Other (security config, docs update, etc) ## Checklist before merging - [x] Unit, [Journey](https://github.com/defenseunicorns/pepr/tree/main/journey), [E2E Tests](https://github.com/defenseunicorns/pepr-excellent-examples), [docs](https://github.com/defenseunicorns/pepr/tree/main/docs), [adr](https://github.com/defenseunicorns/pepr/tree/main/adr) added or updated as needed - [ ] [Contributor Guide Steps](https://docs.pepr.dev/main/contribute/#submitting-a-pull-request) followed --------- Co-authored-by: Case Wylie --- docs/030_user-guide/120_customization.md | 12 +- src/lib/watch-processor.test.ts | 176 +++++++---------------- src/lib/watch-processor.ts | 12 +- 3 files changed, 65 insertions(+), 135 deletions(-) diff --git a/docs/030_user-guide/120_customization.md b/docs/030_user-guide/120_customization.md index 71d5eb3a8..27f0013ce 100644 --- a/docs/030_user-guide/120_customization.md +++ b/docs/030_user-guide/120_customization.md @@ -41,18 +41,18 @@ The Watch configuration is a part of the Pepr module that allows you to watch fo ## Configuring Reconcile -The [Reconcile Action](./030_actions/030_reconcile.md) allows you to maintain ordering of resource updates processed by a Pepr controller. The Reconcile configuration can be customized by specific enviroment variables of the Watcher Deployment and can be set in the `package.json` or in the helm `values.yaml` file. +The [Reconcile Action](./030_actions/030_reconcile.md) allows you to maintain ordering of resource updates processed by a Pepr controller. The Reconcile configuration can be customized via enviroment variable on the Watcher Deployment, which can be set in the `package.json` or in the helm `values.yaml` file. | Field | Description | Example Values | |-|-|-| -| `PEPR_RECONCILE_STRATEGY` | How Pepr should order resource updates being Reconcile()'d. | default: `"singular"` | +| `PEPR_RECONCILE_STRATEGY` | How Pepr should order resource updates being Reconcile()'d. | default: `"kind"` | | Available Options || |-|-| -| `singular` | Pepr will keep a single queue of events across _all_ Reconcile()'d resources of a kind. -| `sharded` | Pepr will keep multiple queues of events, one for each resource _instance_ of a kind. - - +| `kind` | separate queues of events for Reconcile()'d resources of a kind | +| `kindNs` | separate queues of events for Reconcile()'d resources of a kind, within a namespace | +| `kindNsName` | separate queues of events for Reconcile()'d resources of a kind, within a namespace, per name | +| `global` | a single queue of events for all Reconcile()'d resources | ## Customizing with Helm diff --git a/src/lib/watch-processor.test.ts b/src/lib/watch-processor.test.ts index 998a6ffba..607c76239 100644 --- a/src/lib/watch-processor.test.ts +++ b/src/lib/watch-processor.test.ts @@ -1,6 +1,6 @@ // SPDX-License-Identifier: Apache-2.0 // SPDX-FileCopyrightText: 2023-Present The Pepr Authors -import { afterAll, beforeEach, beforeAll, describe, expect, it, jest } from "@jest/globals"; +import { afterAll, beforeEach, describe, expect, it, jest } from "@jest/globals"; import { GenericClass, K8s, KubernetesObject, kind } from "kubernetes-fluent-client"; import { K8sInit, WatchPhase } from "kubernetes-fluent-client/dist/fluent/types"; import { WatchCfg, WatchEvent, Watcher } from "kubernetes-fluent-client/dist/fluent/watch"; @@ -330,133 +330,57 @@ describe("logEvent function", () => { }); describe("queueKey", () => { - describe("PEPR_RECONCILE_STRATEGY=sharded", () => { - const original = process.env.PEPR_RECONCILE_STRATEGY; - - beforeAll(() => { - process.env.PEPR_RECONCILE_STRATEGY = "sharded"; - }); - afterAll(() => { - process.env.PEPR_RECONCILE_STRATEGY = original; - }); - - it("should return correct key for an object with 'kind/name/namespace'", () => { - const obj: KubernetesObject = { - kind: "Pod", - metadata: { - name: "my-pod", - namespace: "my-namespace", - }, - }; - - expect(queueKey(obj)).toBe("Pod/my-pod/my-namespace"); - }); - - it("should handle objects with missing namespace", () => { - const obj: KubernetesObject = { - kind: "Pod", - metadata: { - name: "my-pod", - }, - }; - - expect(queueKey(obj)).toBe("Pod/my-pod/cluster-scoped"); - }); - - it("should handle objects with missing name", () => { - const obj: KubernetesObject = { - kind: "Pod", - metadata: { - namespace: "my-namespace", - }, - }; - - expect(queueKey(obj)).toBe("Pod/Unnamed/my-namespace"); - }); - - it("should handle objects with missing metadata", () => { - const obj: KubernetesObject = { - kind: "Pod", - }; - - expect(queueKey(obj)).toBe("Pod/Unnamed/cluster-scoped"); - }); - - it("should handle objects with missing kind", () => { - const obj: KubernetesObject = { - metadata: { - name: "my-pod", - namespace: "my-namespace", - }, - }; - - expect(queueKey(obj)).toBe("UnknownKind/my-pod/my-namespace"); - }); - - it("should handle completely empty objects", () => { - const obj: KubernetesObject = {}; - - expect(queueKey(obj)).toBe("UnknownKind/Unnamed/cluster-scoped"); - }); + const withKindNsName = { kind: "Pod", metadata: { namespace: "my-ns", name: "my-name" } } as KubernetesObject; + const withKindNs = { kind: "Pod", metadata: { namespace: "my-ns" } } as KubernetesObject; + const withKindName = { kind: "Pod", metadata: { name: "my-name" } } as KubernetesObject; + const withNsName = { metadata: { namespace: "my-ns", name: "my-name" } } as KubernetesObject; + const withKind = { kind: "Pod" } as KubernetesObject; + const withNs = { metadata: { namespace: "my-ns" } } as KubernetesObject; + const withName = { metadata: { name: "my-name" } } as KubernetesObject; + const withNone = {} as KubernetesObject; + + const original = process.env.PEPR_RECONCILE_STRATEGY; + + it.each([ + ["kind", withKindNsName, "Pod"], + ["kind", withKindNs, "Pod"], + ["kind", withKindName, "Pod"], + ["kind", withNsName, "UnknownKind"], + ["kind", withKind, "Pod"], + ["kind", withNs, "UnknownKind"], + ["kind", withName, "UnknownKind"], + ["kind", withNone, "UnknownKind"], + ["kindNs", withKindNsName, "Pod/my-ns"], + ["kindNs", withKindNs, "Pod/my-ns"], + ["kindNs", withKindName, "Pod/cluster-scoped"], + ["kindNs", withNsName, "UnknownKind/my-ns"], + ["kindNs", withKind, "Pod/cluster-scoped"], + ["kindNs", withNs, "UnknownKind/my-ns"], + ["kindNs", withName, "UnknownKind/cluster-scoped"], + ["kindNs", withNone, "UnknownKind/cluster-scoped"], + ["kindNsName", withKindNsName, "Pod/my-ns/my-name"], + ["kindNsName", withKindNs, "Pod/my-ns/Unnamed"], + ["kindNsName", withKindName, "Pod/cluster-scoped/my-name"], + ["kindNsName", withNsName, "UnknownKind/my-ns/my-name"], + ["kindNsName", withKind, "Pod/cluster-scoped/Unnamed"], + ["kindNsName", withNs, "UnknownKind/my-ns/Unnamed"], + ["kindNsName", withName, "UnknownKind/cluster-scoped/my-name"], + ["kindNsName", withNone, "UnknownKind/cluster-scoped/Unnamed"], + ["global", withKindNsName, "global"], + ["global", withKindNs, "global"], + ["global", withKindName, "global"], + ["global", withNsName, "global"], + ["global", withKind, "global"], + ["global", withNs, "global"], + ["global", withName, "global"], + ["global", withNone, "global"], + ])("PEPR_RECONCILE_STRATEGY='%s' over '%j' becomes '%s'", (strat, obj, key) => { + process.env.PEPR_RECONCILE_STRATEGY = strat; + expect(queueKey(obj)).toBe(key); }); - describe("PEPR_RECONCILE_STRATEGY=singular", () => { - const original = process.env.PEPR_RECONCILE_STRATEGY; - - beforeAll(() => { - process.env.PEPR_RECONCILE_STRATEGY = "singular"; - }); - afterAll(() => { - process.env.PEPR_RECONCILE_STRATEGY = original; - }); - - it("should return correct key for an object with 'kind/namespace'", () => { - const obj: KubernetesObject = { - kind: "Pod", - metadata: { - name: "my-pod", - namespace: "my-namespace", - }, - }; - - expect(queueKey(obj)).toBe("Pod/my-namespace"); - }); - - it("should handle objects with missing namespace", () => { - const obj: KubernetesObject = { - kind: "Pod", - metadata: { - name: "my-pod", - }, - }; - - expect(queueKey(obj)).toBe("Pod/cluster-scoped"); - }); - - it("should handle objects with missing metadata", () => { - const obj: KubernetesObject = { - kind: "Pod", - }; - - expect(queueKey(obj)).toBe("Pod/cluster-scoped"); - }); - - it("should handle objects with missing kind", () => { - const obj: KubernetesObject = { - metadata: { - name: "my-pod", - namespace: "my-namespace", - }, - }; - - expect(queueKey(obj)).toBe("UnknownKind/my-namespace"); - }); - - it("should handle completely empty objects", () => { - const obj: KubernetesObject = {}; - - expect(queueKey(obj)).toBe("UnknownKind/cluster-scoped"); - }); + afterAll(() => { + process.env.PEPR_RECONCILE_STRATEGY = original; }); }); diff --git a/src/lib/watch-processor.ts b/src/lib/watch-processor.ts index 1ed341383..dfec5ac29 100644 --- a/src/lib/watch-processor.ts +++ b/src/lib/watch-processor.ts @@ -19,8 +19,8 @@ const queues: Record> = {}; * @returns The key to a Queue in the list of queues */ export function queueKey(obj: KubernetesObject) { - const options = ["singular", "sharded"]; - const d3fault = "singular"; + const options = ["kind", "kindNs", "kindNsName", "global"]; + const d3fault = "kind"; let strat = process.env.PEPR_RECONCILE_STRATEGY || d3fault; strat = options.includes(strat) ? strat : d3fault; @@ -29,7 +29,13 @@ export function queueKey(obj: KubernetesObject) { const kind = obj.kind ?? "UnknownKind"; const name = obj.metadata?.name ?? "Unnamed"; - return strat === "singular" ? `${kind}/${ns}` : `${kind}/${name}/${ns}`; + const lookup: Record = { + kind: `${kind}`, + kindNs: `${kind}/${ns}`, + kindNsName: `${kind}/${ns}/${name}`, + global: "global", + }; + return lookup[strat]; } export function getOrCreateQueue(obj: KubernetesObject) {