pepr 0.34.1 → 0.35.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -13,7 +13,7 @@
13
13
  "/dist",
14
14
  "/src"
15
15
  ],
16
- "version": "0.34.1",
16
+ "version": "0.35.0",
17
17
  "main": "dist/lib.js",
18
18
  "types": "dist/lib.d.ts",
19
19
  "scripts": {
@@ -38,22 +38,22 @@
38
38
  "format:fix": "eslint src --fix && prettier src --write"
39
39
  },
40
40
  "dependencies": {
41
- "@types/ramda": "0.30.1",
41
+ "@types/ramda": "0.30.2",
42
42
  "express": "4.19.2",
43
43
  "fast-json-patch": "3.1.1",
44
44
  "json-pointer": "^0.6.2",
45
- "kubernetes-fluent-client": "3.0.1",
45
+ "kubernetes-fluent-client": "3.0.2",
46
46
  "pino": "9.3.2",
47
47
  "pino-pretty": "11.2.2",
48
48
  "prom-client": "15.1.3",
49
49
  "ramda": "0.30.1"
50
50
  },
51
51
  "devDependencies": {
52
- "@commitlint/cli": "19.4.0",
53
- "@commitlint/config-conventional": "19.2.2",
52
+ "@commitlint/cli": "19.4.1",
53
+ "@commitlint/config-conventional": "19.4.1",
54
54
  "@fast-check/jest": "^2.0.1",
55
55
  "@jest/globals": "29.7.0",
56
- "@types/eslint": "9.6.0",
56
+ "@types/eslint": "9.6.1",
57
57
  "@types/express": "4.17.21",
58
58
  "@types/json-pointer": "^1.0.34",
59
59
  "@types/node": "22.x.x",
@@ -63,8 +63,8 @@
63
63
  "fast-check": "^3.19.0",
64
64
  "jest": "29.7.0",
65
65
  "js-yaml": "^4.1.0",
66
- "nock": "13.5.4",
67
- "ts-jest": "29.2.4"
66
+ "nock": "^13.5.4",
67
+ "ts-jest": "29.2.5"
68
68
  },
69
69
  "peerDependencies": {
70
70
  "@typescript-eslint/eslint-plugin": "7.18.0",
@@ -58,6 +58,7 @@ export function genPkgJSON(opts: InitOptions, pgkVerOverride?: string) {
58
58
  },
59
59
  dependencies: {
60
60
  pepr: pgkVerOverride || version,
61
+ nock: "13.5.4",
61
62
  },
62
63
  devDependencies: {
63
64
  typescript,
@@ -90,15 +90,9 @@ export function watcherDeployTemplate(buildTimestamp: string) {
90
90
  - /app/node_modules/pepr/dist/controller.js
91
91
  - {{ .Values.hash }}
92
92
  readinessProbe:
93
- httpGet:
94
- path: /healthz
95
- port: 3000
96
- scheme: HTTPS
93
+ {{- toYaml .Values.watcher.readinessProbe | nindent 12 }}
97
94
  livenessProbe:
98
- httpGet:
99
- path: /healthz
100
- port: 3000
101
- scheme: HTTPS
95
+ {{- toYaml .Values.watcher.livenessProbe | nindent 12 }}
102
96
  ports:
103
97
  - containerPort: 3000
104
98
  resources:
@@ -176,15 +170,9 @@ export function admissionDeployTemplate(buildTimestamp: string) {
176
170
  - /app/node_modules/pepr/dist/controller.js
177
171
  - {{ .Values.hash }}
178
172
  readinessProbe:
179
- httpGet:
180
- path: /healthz
181
- port: 3000
182
- scheme: HTTPS
173
+ {{- toYaml .Values.admission.readinessProbe | nindent 12 }}
183
174
  livenessProbe:
184
- httpGet:
185
- path: /healthz
186
- port: 3000
187
- scheme: HTTPS
175
+ {{- toYaml .Values.admission.livenessProbe | nindent 12 }}
188
176
  ports:
189
177
  - containerPort: 3000
190
178
  resources:
@@ -111,6 +111,7 @@ export function watcher(assets: Assets, hash: string, buildTimestamp: string, im
111
111
  port: 3000,
112
112
  scheme: "HTTPS",
113
113
  },
114
+ initialDelaySeconds: 10,
114
115
  },
115
116
  livenessProbe: {
116
117
  httpGet: {
@@ -118,6 +119,7 @@ export function watcher(assets: Assets, hash: string, buildTimestamp: string, im
118
119
  port: 3000,
119
120
  scheme: "HTTPS",
120
121
  },
122
+ initialDelaySeconds: 10,
121
123
  },
122
124
  ports: [
123
125
  {
@@ -248,6 +250,7 @@ export function deployment(
248
250
  port: 3000,
249
251
  scheme: "HTTPS",
250
252
  },
253
+ initialDelaySeconds: 10,
251
254
  },
252
255
  livenessProbe: {
253
256
  httpGet: {
@@ -255,6 +258,7 @@ export function deployment(
255
258
  port: 3000,
256
259
  scheme: "HTTPS",
257
260
  },
261
+ initialDelaySeconds: 10,
258
262
  },
259
263
  ports: [
260
264
  {
@@ -45,6 +45,22 @@ export async function overridesFile({ hash, name, image, config, apiToken }: Ass
45
45
  runAsNonRoot: true,
46
46
  fsGroup: 65532,
47
47
  },
48
+ readinessProbe: {
49
+ httpGet: {
50
+ path: "/healthz",
51
+ port: 3000,
52
+ scheme: "HTTPS",
53
+ },
54
+ initialDelaySeconds: 10,
55
+ },
56
+ livenessProbe: {
57
+ httpGet: {
58
+ path: "/healthz",
59
+ port: 3000,
60
+ scheme: "HTTPS",
61
+ },
62
+ initialDelaySeconds: 10,
63
+ },
48
64
  resources: {
49
65
  requests: {
50
66
  memory: "64Mi",
@@ -95,6 +111,22 @@ export async function overridesFile({ hash, name, image, config, apiToken }: Ass
95
111
  runAsNonRoot: true,
96
112
  fsGroup: 65532,
97
113
  },
114
+ readinessProbe: {
115
+ httpGet: {
116
+ path: "/healthz",
117
+ port: 3000,
118
+ scheme: "HTTPS",
119
+ },
120
+ initialDelaySeconds: 10,
121
+ },
122
+ livenessProbe: {
123
+ httpGet: {
124
+ path: "/healthz",
125
+ port: 3000,
126
+ scheme: "HTTPS",
127
+ },
128
+ initialDelaySeconds: 10,
129
+ },
98
130
  resources: {
99
131
  requests: {
100
132
  memory: "64Mi",
@@ -1,11 +1,11 @@
1
1
  // SPDX-License-Identifier: Apache-2.0
2
2
  // SPDX-FileCopyrightText: 2023-Present The Pepr Authors
3
- import { beforeEach, describe, expect, it, jest } from "@jest/globals";
3
+ import { afterAll, beforeEach, beforeAll, describe, expect, it, jest } from "@jest/globals";
4
4
  import { GenericClass, K8s, KubernetesObject, kind } from "kubernetes-fluent-client";
5
5
  import { K8sInit, WatchPhase } from "kubernetes-fluent-client/dist/fluent/types";
6
6
  import { WatchCfg, WatchEvent, Watcher } from "kubernetes-fluent-client/dist/fluent/watch";
7
7
  import { Capability } from "./capability";
8
- import { setupWatch, logEvent } from "./watch-processor";
8
+ import { setupWatch, logEvent, queueRecordKey } from "./watch-processor";
9
9
  import Log from "./logger";
10
10
  import { metricsCollector } from "./metrics";
11
11
 
@@ -88,7 +88,7 @@ describe("WatchProcessor", () => {
88
88
  resyncFailureMax: 5,
89
89
  resyncDelaySec: 5,
90
90
  lastSeenLimitSeconds: 300,
91
- relistIntervalSec: 1800,
91
+ relistIntervalSec: 600,
92
92
  };
93
93
 
94
94
  capabilities.push({
@@ -320,3 +320,134 @@ describe("logEvent function", () => {
320
320
  expect(Log.debug).toHaveBeenCalledWith(`Watch event ${WatchEvent.DATA_ERROR} received. ${message}.`);
321
321
  });
322
322
  });
323
+
324
+ describe("queueRecordKey", () => {
325
+ describe("PEPR_RECONCILE_STRATEGY=sharded", () => {
326
+ const original = process.env.PEPR_RECONCILE_STRATEGY;
327
+
328
+ beforeAll(() => {
329
+ process.env.PEPR_RECONCILE_STRATEGY = "sharded";
330
+ });
331
+ afterAll(() => {
332
+ process.env.PEPR_RECONCILE_STRATEGY = original;
333
+ });
334
+
335
+ it("should return correct key for an object with 'kind/name/namespace'", () => {
336
+ const obj: KubernetesObject = {
337
+ kind: "Pod",
338
+ metadata: {
339
+ name: "my-pod",
340
+ namespace: "my-namespace",
341
+ },
342
+ };
343
+
344
+ expect(queueRecordKey(obj)).toBe("Pod/my-pod/my-namespace");
345
+ });
346
+
347
+ it("should handle objects with missing namespace", () => {
348
+ const obj: KubernetesObject = {
349
+ kind: "Pod",
350
+ metadata: {
351
+ name: "my-pod",
352
+ },
353
+ };
354
+
355
+ expect(queueRecordKey(obj)).toBe("Pod/my-pod/cluster-scoped");
356
+ });
357
+
358
+ it("should handle objects with missing name", () => {
359
+ const obj: KubernetesObject = {
360
+ kind: "Pod",
361
+ metadata: {
362
+ namespace: "my-namespace",
363
+ },
364
+ };
365
+
366
+ expect(queueRecordKey(obj)).toBe("Pod/Unnamed/my-namespace");
367
+ });
368
+
369
+ it("should handle objects with missing metadata", () => {
370
+ const obj: KubernetesObject = {
371
+ kind: "Pod",
372
+ };
373
+
374
+ expect(queueRecordKey(obj)).toBe("Pod/Unnamed/cluster-scoped");
375
+ });
376
+
377
+ it("should handle objects with missing kind", () => {
378
+ const obj: KubernetesObject = {
379
+ metadata: {
380
+ name: "my-pod",
381
+ namespace: "my-namespace",
382
+ },
383
+ };
384
+
385
+ expect(queueRecordKey(obj)).toBe("UnknownKind/my-pod/my-namespace");
386
+ });
387
+
388
+ it("should handle completely empty objects", () => {
389
+ const obj: KubernetesObject = {};
390
+
391
+ expect(queueRecordKey(obj)).toBe("UnknownKind/Unnamed/cluster-scoped");
392
+ });
393
+ });
394
+
395
+ describe("PEPR_RECONCILE_STRATEGY=singular", () => {
396
+ const original = process.env.PEPR_RECONCILE_STRATEGY;
397
+
398
+ beforeAll(() => {
399
+ process.env.PEPR_RECONCILE_STRATEGY = "singular";
400
+ });
401
+ afterAll(() => {
402
+ process.env.PEPR_RECONCILE_STRATEGY = original;
403
+ });
404
+
405
+ it("should return correct key for an object with 'kind/namespace'", () => {
406
+ const obj: KubernetesObject = {
407
+ kind: "Pod",
408
+ metadata: {
409
+ name: "my-pod",
410
+ namespace: "my-namespace",
411
+ },
412
+ };
413
+
414
+ expect(queueRecordKey(obj)).toBe("Pod/my-namespace");
415
+ });
416
+
417
+ it("should handle objects with missing namespace", () => {
418
+ const obj: KubernetesObject = {
419
+ kind: "Pod",
420
+ metadata: {
421
+ name: "my-pod",
422
+ },
423
+ };
424
+
425
+ expect(queueRecordKey(obj)).toBe("Pod/cluster-scoped");
426
+ });
427
+
428
+ it("should handle objects with missing metadata", () => {
429
+ const obj: KubernetesObject = {
430
+ kind: "Pod",
431
+ };
432
+
433
+ expect(queueRecordKey(obj)).toBe("Pod/cluster-scoped");
434
+ });
435
+
436
+ it("should handle objects with missing kind", () => {
437
+ const obj: KubernetesObject = {
438
+ metadata: {
439
+ name: "my-pod",
440
+ namespace: "my-namespace",
441
+ },
442
+ };
443
+
444
+ expect(queueRecordKey(obj)).toBe("UnknownKind/my-namespace");
445
+ });
446
+
447
+ it("should handle completely empty objects", () => {
448
+ const obj: KubernetesObject = {};
449
+
450
+ expect(queueRecordKey(obj)).toBe("UnknownKind/cluster-scoped");
451
+ });
452
+ });
453
+ });
@@ -9,6 +9,29 @@ import { Queue } from "./queue";
9
9
  import { Binding, Event } from "./types";
10
10
  import { metricsCollector } from "./metrics";
11
11
 
12
+ // init a queueRecord record to store Queue instances for a given Kubernetes Object
13
+ const queueRecord: Record<string, Queue<KubernetesObject>> = {};
14
+
15
+ /**
16
+ * Get the key for a record in the queueRecord
17
+ *
18
+ * @param obj The object to get the key for
19
+ * @returns The key for the object
20
+ */
21
+ export function queueRecordKey(obj: KubernetesObject) {
22
+ const options = ["singular", "sharded"]; // TODO : ts-type this fella
23
+ const d3fault = "singular";
24
+
25
+ let strat = process.env.PEPR_RECONCILE_STRATEGY || d3fault;
26
+ strat = options.includes(strat) ? strat : d3fault;
27
+
28
+ const ns = obj.metadata?.namespace ?? "cluster-scoped";
29
+ const kind = obj.kind ?? "UnknownKind";
30
+ const name = obj.metadata?.name ?? "Unnamed";
31
+
32
+ return strat === "singular" ? `${kind}/${ns}` : `${kind}/${name}/${ns}`;
33
+ }
34
+
12
35
  // Watch configuration
13
36
  const watchCfg: WatchCfg = {
14
37
  resyncFailureMax: process.env.PEPR_RESYNC_FAILURE_MAX ? parseInt(process.env.PEPR_RESYNC_FAILURE_MAX, 10) : 5,
@@ -18,7 +41,7 @@ const watchCfg: WatchCfg = {
18
41
  : 300,
19
42
  relistIntervalSec: process.env.PEPR_RELIST_INTERVAL_SECONDS
20
43
  ? parseInt(process.env.PEPR_RELIST_INTERVAL_SECONDS, 10)
21
- : 1800,
44
+ : 600,
22
45
  };
23
46
 
24
47
  // Map the event to the watch phase
@@ -74,13 +97,20 @@ async function runBinding(binding: Binding, capabilityNamespaces: string[]) {
74
97
  }
75
98
  };
76
99
 
77
- const queue = new Queue();
78
- queue.setReconcile(watchCallback);
100
+ function getOrCreateQueue(key: string): Queue<KubernetesObject> {
101
+ if (!queueRecord[key]) {
102
+ queueRecord[key] = new Queue<KubernetesObject>();
103
+ queueRecord[key].setReconcile(watchCallback);
104
+ }
105
+ return queueRecord[key];
106
+ }
79
107
 
80
108
  // Setup the resource watch
81
109
  const watcher = K8s(binding.model, binding.filters).Watch(async (obj, type) => {
82
110
  Log.debug(obj, `Watch event ${type} received`);
83
111
 
112
+ const queue = getOrCreateQueue(queueRecordKey(obj));
113
+
84
114
  // If the binding is a queue, enqueue the object
85
115
  if (binding.isQueue) {
86
116
  await queue.enqueue(obj, type);
@@ -9,6 +9,7 @@ import {
9
9
  fetchStatus,
10
10
  kind,
11
11
  } from "pepr";
12
+ import nock from "nock";
12
13
 
13
14
  /**
14
15
  * The HelloPepr Capability is an example capability to demonstrate some general concepts of Pepr.
@@ -273,6 +274,14 @@ When(a.ConfigMap)
273
274
  .Mutate(cm => cm.SetLabel("got-jokes", "true"))
274
275
  .Watch(async cm => {
275
276
  const jokeURL = "https://icanhazdadjoke.com/";
277
+
278
+ // Set up Nock to mock the API calls globally with header matching
279
+ nock(jokeURL).get("/").reply(200, {
280
+ id: "R7UfaahVfFd",
281
+ joke: "Funny joke goes here.",
282
+ status: 200,
283
+ });
284
+
276
285
  // Try/catch is not needed as a response object will always be returned
277
286
  const response = await fetch<TheChuckNorrisJoke>(jokeURL, {
278
287
  headers: {