pepr 0.33.0 → 0.34.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.
Files changed (140) hide show
  1. package/README.md +2 -1
  2. package/dist/cli/banner.d.ts +2 -0
  3. package/dist/cli/banner.d.ts.map +1 -0
  4. package/dist/cli/build.d.ts +19 -0
  5. package/dist/cli/build.d.ts.map +1 -0
  6. package/dist/cli/deploy.d.ts +3 -0
  7. package/dist/cli/deploy.d.ts.map +1 -0
  8. package/dist/cli/dev.d.ts +3 -0
  9. package/dist/cli/dev.d.ts.map +1 -0
  10. package/dist/cli/format.d.ts +9 -0
  11. package/dist/cli/format.d.ts.map +1 -0
  12. package/dist/cli/init/index.d.ts +3 -0
  13. package/dist/cli/init/index.d.ts.map +1 -0
  14. package/dist/cli/init/templates.d.ts +196 -0
  15. package/dist/cli/init/templates.d.ts.map +1 -0
  16. package/dist/cli/init/utils.d.ts +21 -0
  17. package/dist/cli/init/utils.d.ts.map +1 -0
  18. package/dist/cli/init/utils.test.d.ts +2 -0
  19. package/dist/cli/init/utils.test.d.ts.map +1 -0
  20. package/dist/cli/init/walkthrough.d.ts +8 -0
  21. package/dist/cli/init/walkthrough.d.ts.map +1 -0
  22. package/dist/cli/init/walkthrough.test.d.ts +2 -0
  23. package/dist/cli/init/walkthrough.test.d.ts.map +1 -0
  24. package/dist/cli/kfc.d.ts +3 -0
  25. package/dist/cli/kfc.d.ts.map +1 -0
  26. package/dist/cli/monitor.d.ts +3 -0
  27. package/dist/cli/monitor.d.ts.map +1 -0
  28. package/dist/cli/root.d.ts +5 -0
  29. package/dist/cli/root.d.ts.map +1 -0
  30. package/dist/cli/update.d.ts +3 -0
  31. package/dist/cli/update.d.ts.map +1 -0
  32. package/dist/cli/uuid.d.ts +3 -0
  33. package/dist/cli/uuid.d.ts.map +1 -0
  34. package/dist/cli.js +68 -38
  35. package/dist/controller.js +1 -2
  36. package/dist/fixtures/loader.d.ts +5 -0
  37. package/dist/fixtures/loader.d.ts.map +1 -0
  38. package/dist/lib/assets/helm.d.ts +1 -0
  39. package/dist/lib/assets/helm.d.ts.map +1 -1
  40. package/dist/lib/assets/helm.test.d.ts +2 -0
  41. package/dist/lib/assets/helm.test.d.ts.map +1 -0
  42. package/dist/lib/assets/index.d.ts.map +1 -1
  43. package/dist/lib/assets/pods.d.ts +3 -0
  44. package/dist/lib/assets/pods.d.ts.map +1 -1
  45. package/dist/lib/assets/pods.test.d.ts +2 -0
  46. package/dist/lib/assets/pods.test.d.ts.map +1 -0
  47. package/dist/lib/assets/yaml.d.ts.map +1 -1
  48. package/dist/lib/errors.test.d.ts +2 -0
  49. package/dist/lib/errors.test.d.ts.map +1 -0
  50. package/dist/lib/filter.test.d.ts +2 -0
  51. package/dist/lib/filter.test.d.ts.map +1 -0
  52. package/dist/lib/helpers.d.ts +0 -5
  53. package/dist/lib/helpers.d.ts.map +1 -1
  54. package/dist/lib/helpers.test.d.ts +2 -0
  55. package/dist/lib/helpers.test.d.ts.map +1 -0
  56. package/dist/lib/included-files.test.d.ts +2 -0
  57. package/dist/lib/included-files.test.d.ts.map +1 -0
  58. package/dist/lib/logger.test.d.ts +2 -0
  59. package/dist/lib/logger.test.d.ts.map +1 -0
  60. package/dist/lib/metrics.d.ts +18 -0
  61. package/dist/lib/metrics.d.ts.map +1 -1
  62. package/dist/lib/metrics.test.d.ts +2 -0
  63. package/dist/lib/metrics.test.d.ts.map +1 -0
  64. package/dist/lib/module.test.d.ts +2 -0
  65. package/dist/lib/module.test.d.ts.map +1 -0
  66. package/dist/lib/mutate-request.test.d.ts +2 -0
  67. package/dist/lib/mutate-request.test.d.ts.map +1 -0
  68. package/dist/lib/queue.test.d.ts +2 -0
  69. package/dist/lib/queue.test.d.ts.map +1 -0
  70. package/dist/lib/schedule.test.d.ts +15 -0
  71. package/dist/lib/schedule.test.d.ts.map +1 -0
  72. package/dist/lib/storage.test.d.ts +2 -0
  73. package/dist/lib/storage.test.d.ts.map +1 -0
  74. package/dist/lib/tls.test.d.ts +2 -0
  75. package/dist/lib/tls.test.d.ts.map +1 -0
  76. package/dist/lib/utils.test.d.ts +2 -0
  77. package/dist/lib/utils.test.d.ts.map +1 -0
  78. package/dist/lib/validate-request.test.d.ts +2 -0
  79. package/dist/lib/validate-request.test.d.ts.map +1 -0
  80. package/dist/lib/watch-processor.d.ts.map +1 -1
  81. package/dist/lib/watch-processor.test.d.ts +2 -0
  82. package/dist/lib/watch-processor.test.d.ts.map +1 -0
  83. package/dist/lib.js +76 -20
  84. package/dist/lib.js.map +3 -3
  85. package/dist/sdk/sdk.test.d.ts +2 -0
  86. package/dist/sdk/sdk.test.d.ts.map +1 -0
  87. package/package.json +21 -15
  88. package/src/cli/banner.ts +63 -0
  89. package/src/cli/build.ts +370 -0
  90. package/src/cli/deploy.ts +105 -0
  91. package/src/cli/dev.ts +118 -0
  92. package/src/cli/format.ts +83 -0
  93. package/src/cli/init/index.ts +99 -0
  94. package/src/cli/init/templates.ts +124 -0
  95. package/src/cli/init/utils.test.ts +28 -0
  96. package/src/cli/init/utils.ts +55 -0
  97. package/src/cli/init/walkthrough.test.ts +21 -0
  98. package/src/cli/init/walkthrough.ts +96 -0
  99. package/src/cli/kfc.ts +45 -0
  100. package/src/cli/monitor.ts +101 -0
  101. package/src/cli/root.ts +12 -0
  102. package/src/cli/update.ts +95 -0
  103. package/src/cli/uuid.ts +44 -0
  104. package/src/fixtures/data/create-pod.json +271 -0
  105. package/src/fixtures/data/delete-pod.json +271 -0
  106. package/src/fixtures/loader.ts +18 -0
  107. package/src/lib/.prettierrc +14 -0
  108. package/src/lib/assets/helm.test.ts +64 -0
  109. package/src/lib/assets/helm.ts +35 -0
  110. package/src/lib/assets/index.ts +5 -1
  111. package/src/lib/assets/pods.test.ts +553 -0
  112. package/src/lib/assets/pods.ts +14 -6
  113. package/src/lib/assets/yaml.ts +15 -15
  114. package/src/lib/controller/index.ts +2 -2
  115. package/src/lib/errors.test.ts +85 -0
  116. package/src/lib/filter.test.ts +384 -0
  117. package/src/lib/helpers.test.ts +1192 -0
  118. package/src/lib/helpers.ts +0 -17
  119. package/src/lib/included-files.test.ts +22 -0
  120. package/src/lib/logger.test.ts +18 -0
  121. package/src/lib/metrics.test.ts +132 -0
  122. package/src/lib/metrics.ts +68 -6
  123. package/src/lib/module.test.ts +126 -0
  124. package/src/lib/mutate-request.test.ts +188 -0
  125. package/src/lib/queue.test.ts +58 -0
  126. package/src/lib/schedule.test.ts +217 -0
  127. package/src/lib/storage.test.ts +203 -0
  128. package/src/lib/tls.test.ts +18 -0
  129. package/src/lib/utils.test.ts +69 -0
  130. package/src/lib/validate-request.test.ts +124 -0
  131. package/src/lib/watch-processor.test.ts +322 -0
  132. package/src/lib/watch-processor.ts +20 -4
  133. package/src/sdk/sdk.test.ts +243 -0
  134. package/src/templates/.eslintrc.json +6 -0
  135. package/.prettierignore +0 -1
  136. package/CODE_OF_CONDUCT.md +0 -133
  137. package/SECURITY.md +0 -18
  138. package/SUPPORT.md +0 -16
  139. package/codecov.yaml +0 -19
  140. package/commitlint.config.js +0 -1
@@ -0,0 +1,322 @@
1
+ // SPDX-License-Identifier: Apache-2.0
2
+ // SPDX-FileCopyrightText: 2023-Present The Pepr Authors
3
+ import { beforeEach, describe, expect, it, jest } from "@jest/globals";
4
+ import { GenericClass, K8s, KubernetesObject, kind } from "kubernetes-fluent-client";
5
+ import { K8sInit, WatchPhase } from "kubernetes-fluent-client/dist/fluent/types";
6
+ import { WatchCfg, WatchEvent, Watcher } from "kubernetes-fluent-client/dist/fluent/watch";
7
+ import { Capability } from "./capability";
8
+ import { setupWatch, logEvent } from "./watch-processor";
9
+ import Log from "./logger";
10
+ import { metricsCollector } from "./metrics";
11
+
12
+ type onCallback = (eventName: string | symbol, listener: (msg: string) => void) => void;
13
+
14
+ // Mock the dependencies
15
+ jest.mock("kubernetes-fluent-client");
16
+
17
+ jest.mock("./logger", () => ({
18
+ debug: jest.fn(),
19
+ error: jest.fn(),
20
+ }));
21
+
22
+ jest.mock("./metrics", () => ({
23
+ metricsCollector: {
24
+ initCacheMissWindow: jest.fn(),
25
+ incCacheMiss: jest.fn(),
26
+ incRetryCount: jest.fn(),
27
+ },
28
+ }));
29
+
30
+ describe("WatchProcessor", () => {
31
+ const mockStart = jest.fn();
32
+ const mockK8s = jest.mocked(K8s);
33
+ const mockApply = jest.fn();
34
+ const mockGet = jest.fn();
35
+ const mockWatch = jest.fn();
36
+ const mockEvents = jest.fn() as jest.MockedFunction<onCallback>;
37
+
38
+ const capabilities = [
39
+ {
40
+ bindings: [
41
+ {
42
+ isWatch: true,
43
+ model: "someModel",
44
+ filters: {},
45
+ event: "Create",
46
+ watchCallback: () => {
47
+ console.log("words");
48
+ },
49
+ },
50
+ ],
51
+ },
52
+ ] as unknown as Capability[];
53
+
54
+ beforeEach(() => {
55
+ jest.resetAllMocks();
56
+ jest.useFakeTimers();
57
+
58
+ mockK8s.mockImplementation(<T extends GenericClass, K extends KubernetesObject>() => {
59
+ return {
60
+ Apply: mockApply,
61
+ InNamespace: jest.fn().mockReturnThis(),
62
+ Watch: mockWatch,
63
+ Get: mockGet,
64
+ } as unknown as K8sInit<T, K>;
65
+ });
66
+
67
+ mockWatch.mockImplementation(() => {
68
+ return {
69
+ start: mockStart,
70
+ events: {
71
+ on: mockEvents,
72
+ },
73
+ } as unknown as Watcher<typeof kind.Pod>;
74
+ });
75
+
76
+ mockGet.mockImplementation(() => ({
77
+ data: {
78
+ "42dae115ed-8aa1f3": "756",
79
+ "8aa1fde099-32a12": "750",
80
+ },
81
+ }));
82
+
83
+ mockApply.mockImplementation(() => Promise.resolve());
84
+ });
85
+
86
+ it("should setup watches for all bindings with isWatch=true", async () => {
87
+ const watchCfg: WatchCfg = {
88
+ resyncFailureMax: 5,
89
+ resyncDelaySec: 5,
90
+ lastSeenLimitSeconds: 300,
91
+ relistIntervalSec: 1800,
92
+ };
93
+
94
+ capabilities.push({
95
+ bindings: [
96
+ { isWatch: true, model: "someModel", filters: { name: "bleh" }, event: "Create", watchCallback: jest.fn() },
97
+ { isWatch: false, model: "someModel", filters: {}, event: "Create", watchCallback: jest.fn() },
98
+ ],
99
+ } as unknown as Capability);
100
+
101
+ setupWatch(capabilities);
102
+
103
+ expect(mockK8s).toHaveBeenCalledTimes(2);
104
+ expect(mockK8s).toHaveBeenNthCalledWith(1, "someModel", {});
105
+ expect(mockK8s).toHaveBeenNthCalledWith(2, "someModel", { name: "bleh" });
106
+
107
+ expect(mockWatch).toHaveBeenCalledTimes(2);
108
+ expect(mockWatch).toHaveBeenCalledWith(expect.any(Function), expect.objectContaining(watchCfg));
109
+ });
110
+
111
+ it("should not setup watches if capabilities array is empty", async () => {
112
+ await setupWatch([]);
113
+ expect(mockWatch).toHaveBeenCalledTimes(0);
114
+ });
115
+
116
+ it("should not setup watches if no bindings are present", async () => {
117
+ const capabilities = [{ bindings: [] }, { bindings: [] }] as unknown as Capability[];
118
+ await setupWatch(capabilities);
119
+ expect(mockWatch).toHaveBeenCalledTimes(0);
120
+ });
121
+
122
+ it("should exit if the watch fails to start", async () => {
123
+ const exitSpy = jest.spyOn(process, "exit").mockImplementation(() => {
124
+ return undefined as never;
125
+ });
126
+
127
+ mockStart.mockRejectedValue(new Error("err") as never);
128
+
129
+ await setupWatch(capabilities);
130
+
131
+ expect(exitSpy).toHaveBeenCalledWith(1);
132
+ });
133
+
134
+ it("should watch for the give_up event", async () => {
135
+ const exitSpy = jest.spyOn(process, "exit").mockImplementation(() => {
136
+ return undefined as never;
137
+ });
138
+
139
+ mockEvents.mockImplementation((eventName: string | symbol, listener: (msg: string) => void) => {
140
+ if (eventName === WatchEvent.GIVE_UP) {
141
+ expect(listener).toBeInstanceOf(Function);
142
+ listener("err");
143
+ expect(exitSpy).toHaveBeenCalledWith(1);
144
+ }
145
+ });
146
+
147
+ setupWatch(capabilities);
148
+ });
149
+
150
+ it("should setup watches with correct phases for different events", async () => {
151
+ const watchCallbackCreate = jest.fn();
152
+ const watchCallbackUpdate = jest.fn();
153
+ const watchCallbackDelete = jest.fn();
154
+
155
+ const capabilities = [
156
+ {
157
+ bindings: [
158
+ { isWatch: true, model: "someModel", filters: {}, event: "Create", watchCallback: watchCallbackCreate },
159
+ { isWatch: true, model: "someModel", filters: {}, event: "Update", watchCallback: watchCallbackUpdate },
160
+ { isWatch: true, model: "someModel", filters: {}, event: "Delete", watchCallback: watchCallbackDelete },
161
+ // Add more events here
162
+ ],
163
+ },
164
+ ] as unknown as Capability[];
165
+
166
+ setupWatch(capabilities);
167
+
168
+ type mockArg = [(payload: kind.Pod, phase: WatchPhase) => void, WatchCfg];
169
+
170
+ const firstCall = mockWatch.mock.calls[0] as unknown as mockArg;
171
+ const secondCall = mockWatch.mock.calls[1] as unknown as mockArg;
172
+ const thirdCall = mockWatch.mock.calls[2] as unknown as mockArg;
173
+
174
+ expect(firstCall[1].resyncFailureMax).toEqual(5);
175
+ expect(firstCall[1].resyncDelaySec).toEqual(5);
176
+ expect(firstCall[0]).toBeInstanceOf(Function);
177
+
178
+ firstCall[0]({} as kind.Pod, WatchPhase.Added);
179
+ expect(watchCallbackCreate).toHaveBeenCalledTimes(1);
180
+ expect(watchCallbackCreate).toHaveBeenCalledWith({}, WatchPhase.Added);
181
+
182
+ firstCall[0]({} as kind.Pod, WatchPhase.Modified);
183
+ firstCall[0]({} as kind.Pod, WatchPhase.Deleted);
184
+ expect(watchCallbackDelete).toHaveBeenCalledTimes(0);
185
+ expect(watchCallbackUpdate).toHaveBeenCalledTimes(0);
186
+
187
+ watchCallbackCreate.mockClear();
188
+ watchCallbackUpdate.mockClear();
189
+ watchCallbackDelete.mockClear();
190
+
191
+ secondCall[0]({} as kind.Pod, WatchPhase.Modified);
192
+ expect(watchCallbackUpdate).toHaveBeenCalledTimes(1);
193
+ expect(watchCallbackUpdate).toHaveBeenCalledWith({}, WatchPhase.Modified);
194
+
195
+ secondCall[0]({} as kind.Pod, WatchPhase.Added);
196
+ secondCall[0]({} as kind.Pod, WatchPhase.Deleted);
197
+ expect(watchCallbackCreate).toHaveBeenCalledTimes(0);
198
+ expect(watchCallbackDelete).toHaveBeenCalledTimes(0);
199
+
200
+ watchCallbackCreate.mockClear();
201
+ watchCallbackUpdate.mockClear();
202
+ watchCallbackDelete.mockClear();
203
+
204
+ thirdCall[0]({} as kind.Pod, WatchPhase.Deleted);
205
+ expect(watchCallbackDelete).toHaveBeenCalledTimes(1);
206
+ expect(watchCallbackDelete).toHaveBeenCalledWith({}, WatchPhase.Deleted);
207
+
208
+ thirdCall[0]({} as kind.Pod, WatchPhase.Added);
209
+ thirdCall[0]({} as kind.Pod, WatchPhase.Modified);
210
+ expect(watchCallbackCreate).toHaveBeenCalledTimes(0);
211
+ expect(watchCallbackUpdate).toHaveBeenCalledTimes(0);
212
+ });
213
+
214
+ it("should call the metricsCollector methods on respective events", async () => {
215
+ const mockIncCacheMiss = metricsCollector.incCacheMiss;
216
+ const mockInitCacheMissWindow = metricsCollector.initCacheMissWindow;
217
+ const mockIncRetryCount = metricsCollector.incRetryCount;
218
+
219
+ const watchCallback = jest.fn();
220
+ const capabilities = [
221
+ {
222
+ bindings: [{ isWatch: true, model: "someModel", filters: {}, event: "Create", watchCallback: watchCallback }],
223
+ },
224
+ ] as unknown as Capability[];
225
+
226
+ setupWatch(capabilities);
227
+
228
+ type mockArg = [(payload: kind.Pod, phase: WatchPhase) => void, WatchCfg];
229
+
230
+ const firstCall = mockWatch.mock.calls[0] as unknown as mockArg;
231
+
232
+ const cacheMissWindowName = "window-1";
233
+ const retryCount = "retry-1";
234
+
235
+ firstCall[0]({} as kind.Pod, WatchPhase.Added);
236
+ mockEvents.mock.calls.forEach(call => {
237
+ if (call[0] === WatchEvent.CACHE_MISS) {
238
+ call[1](cacheMissWindowName);
239
+ }
240
+ if (call[0] === WatchEvent.INIT_CACHE_MISS) {
241
+ call[1](cacheMissWindowName);
242
+ }
243
+ if (call[0] === WatchEvent.INC_RESYNC_FAILURE_COUNT) {
244
+ call[1](retryCount);
245
+ }
246
+ });
247
+
248
+ expect(mockIncCacheMiss).toHaveBeenCalledWith(cacheMissWindowName);
249
+ expect(mockInitCacheMissWindow).toHaveBeenCalledWith(cacheMissWindowName);
250
+ expect(mockIncRetryCount).toHaveBeenCalledWith(retryCount);
251
+ });
252
+
253
+ it("should call parseInt with process.env.PEPR_RELIST_INTERVAL_SECONDS", async () => {
254
+ const parseIntSpy = jest.spyOn(global, "parseInt");
255
+
256
+ process.env.PEPR_RELIST_INTERVAL_SECONDS = "1800";
257
+ process.env.PEPR_LAST_SEEN_LIMIT_SECONDS = "300";
258
+ process.env.PEPR_RESYNC_DELAY_SECONDS = "60";
259
+ process.env.PEPR_RESYNC_FAILURE_MAX = "5";
260
+
261
+ /* eslint-disable @typescript-eslint/no-unused-vars */
262
+ const watchCfg: WatchCfg = {
263
+ resyncFailureMax: parseInt(process.env.PEPR_RESYNC_FAILURE_MAX, 10),
264
+ resyncDelaySec: parseInt(process.env.PEPR_RESYNC_DELAY_SECONDS, 10),
265
+ lastSeenLimitSeconds: parseInt(process.env.PEPR_LAST_SEEN_LIMIT_SECONDS, 10),
266
+ relistIntervalSec: parseInt(process.env.PEPR_RELIST_INTERVAL_SECONDS, 10),
267
+ };
268
+
269
+ capabilities.push({
270
+ bindings: [
271
+ { isWatch: true, model: "someModel", filters: { name: "bleh" }, event: "Create", watchCallback: jest.fn() },
272
+ { isWatch: false, model: "someModel", filters: {}, event: "Create", watchCallback: jest.fn() },
273
+ ],
274
+ } as unknown as Capability);
275
+
276
+ setupWatch(capabilities);
277
+
278
+ expect(parseIntSpy).toHaveBeenCalledWith("1800", 10);
279
+ expect(parseIntSpy).toHaveBeenCalledWith("300", 10);
280
+ expect(parseIntSpy).toHaveBeenCalledWith("60", 10);
281
+ expect(parseIntSpy).toHaveBeenCalledWith("5", 10);
282
+ parseIntSpy.mockRestore();
283
+ });
284
+ });
285
+
286
+ describe("logEvent function", () => {
287
+ it("should handle data events", () => {
288
+ const mockObj = { id: "123", type: "Pod" } as KubernetesObject;
289
+ const message = "Test message";
290
+ logEvent(WatchEvent.DATA, message, mockObj);
291
+ expect(Log.debug).toHaveBeenCalledWith(mockObj, `Watch event ${WatchEvent.DATA} received. ${message}.`);
292
+ });
293
+
294
+ it("should handle CONNECT events", () => {
295
+ const url = "/api/v1/namespaces/default/pods?watch=true&resourceVersion=0";
296
+ logEvent(WatchEvent.CONNECT, url);
297
+ expect(Log.debug).toHaveBeenCalledWith(`Watch event ${WatchEvent.CONNECT} received. ${url}.`);
298
+ });
299
+
300
+ it("should handle LIST_ERROR events", () => {
301
+ const message = "LIST_ERROR";
302
+ logEvent(WatchEvent.LIST_ERROR, message);
303
+ expect(Log.debug).toHaveBeenCalledWith(`Watch event ${WatchEvent.LIST_ERROR} received. ${message}.`);
304
+ });
305
+ it("should handle LIST events", () => {
306
+ const podList = {
307
+ kind: "PodList",
308
+ apiVersion: "v1",
309
+ metadata: { resourceVersion: "10245" },
310
+ items: [],
311
+ };
312
+ const message = JSON.stringify(podList, undefined, 2);
313
+ logEvent(WatchEvent.LIST, message);
314
+ expect(Log.debug).toHaveBeenCalledWith(`Watch event ${WatchEvent.LIST} received. ${message}.`);
315
+ });
316
+
317
+ it("should handle DATA_ERROR events", () => {
318
+ const message = "Test message";
319
+ logEvent(WatchEvent.DATA_ERROR, message);
320
+ expect(Log.debug).toHaveBeenCalledWith(`Watch event ${WatchEvent.DATA_ERROR} received. ${message}.`);
321
+ });
322
+ });
@@ -7,14 +7,18 @@ import { filterNoMatchReason } from "./helpers";
7
7
  import Log from "./logger";
8
8
  import { Queue } from "./queue";
9
9
  import { Binding, Event } from "./types";
10
+ import { metricsCollector } from "./metrics";
10
11
 
11
12
  // Watch configuration
12
13
  const watchCfg: WatchCfg = {
13
- retryMax: process.env.PEPR_RETRYMAX ? parseInt(process.env.PEPR_RETRYMAX, 10) : 5,
14
- retryDelaySec: process.env.PEPR_RETRYDELAYSECONDS ? parseInt(process.env.PEPR_RETRYDELAYSECONDS, 10) : 5,
15
- resyncIntervalSec: process.env.PEPR_RESYNCINTERVALSECONDS
16
- ? parseInt(process.env.PEPR_RESYNCINTERVALSECONDS, 10)
14
+ resyncFailureMax: process.env.PEPR_RESYNC_FAILURE_MAX ? parseInt(process.env.PEPR_RESYNC_FAILURE_MAX, 10) : 5,
15
+ resyncDelaySec: process.env.PEPR_RESYNC_DELAY_SECONDS ? parseInt(process.env.PEPR_RESYNC_DELAY_SECONDS, 10) : 5,
16
+ lastSeenLimitSeconds: process.env.PEPR_LAST_SEEN_LIMIT_SECONDS
17
+ ? parseInt(process.env.PEPR_LAST_SEEN_LIMIT_SECONDS, 10)
17
18
  : 300,
19
+ relistIntervalSec: process.env.PEPR_RELIST_INTERVAL_SECONDS
20
+ ? parseInt(process.env.PEPR_RELIST_INTERVAL_SECONDS, 10)
21
+ : 1800,
18
22
  };
19
23
 
20
24
  // Map the event to the watch phase
@@ -105,6 +109,18 @@ async function runBinding(binding: Binding, capabilityNamespaces: string[]) {
105
109
  watcher.events.on(WatchEvent.NETWORK_ERROR, err => logEvent(WatchEvent.NETWORK_ERROR, err.message));
106
110
  watcher.events.on(WatchEvent.LIST_ERROR, err => logEvent(WatchEvent.LIST_ERROR, err.message));
107
111
  watcher.events.on(WatchEvent.LIST, list => logEvent(WatchEvent.LIST, JSON.stringify(list, undefined, 2)));
112
+ watcher.events.on(WatchEvent.CACHE_MISS, windowName => {
113
+ metricsCollector.incCacheMiss(windowName);
114
+ });
115
+
116
+ watcher.events.on(WatchEvent.INIT_CACHE_MISS, windowName => {
117
+ metricsCollector.initCacheMissWindow(windowName);
118
+ });
119
+
120
+ watcher.events.on(WatchEvent.INC_RESYNC_FAILURE_COUNT, retryCount => {
121
+ metricsCollector.incRetryCount(retryCount);
122
+ });
123
+
108
124
  // Start the watch
109
125
  try {
110
126
  await watcher.start();
@@ -0,0 +1,243 @@
1
+ // SPDX-License-Identifier: Apache-2.0
2
+ // SPDX-FileCopyrightText: 2023-Present The Pepr Authors
3
+
4
+ import { expect, test } from "@jest/globals";
5
+ import { PeprValidateRequest } from "../lib/validate-request";
6
+ import { PeprMutateRequest } from "../lib/mutate-request";
7
+ import { a } from "../lib";
8
+ import { containers, writeEvent, getOwnerRefFrom, sanitizeResourceName } from "./sdk";
9
+ import * as fc from "fast-check";
10
+ import { beforeEach, describe, it, jest } from "@jest/globals";
11
+ import { GenericKind } from "kubernetes-fluent-client";
12
+ import { K8s, kind } from "kubernetes-fluent-client";
13
+ import { Mock } from "jest-mock";
14
+
15
+ jest.mock("kubernetes-fluent-client", () => ({
16
+ K8s: jest.fn(),
17
+ Log: {
18
+ debug: jest.fn(),
19
+ warn: jest.fn(),
20
+ error: jest.fn(),
21
+ },
22
+ kind: {
23
+ CoreEvent: "CoreEvent",
24
+ },
25
+ }));
26
+
27
+ describe("containers", () => {
28
+ test("should return a list of containers in the pod when in a validate block", async () => {
29
+ const standardContainers = [
30
+ {
31
+ name: "container-1",
32
+ },
33
+ ];
34
+ const initContainers = [
35
+ {
36
+ name: "init-container-1",
37
+ },
38
+ ];
39
+ const ephemeralContainers = [
40
+ {
41
+ name: "ephemeral-container-1",
42
+ },
43
+ ];
44
+ const allContainers = [...standardContainers, ...initContainers, ...ephemeralContainers];
45
+ const peprValidationRequest = {
46
+ Raw: {
47
+ spec: {
48
+ containers: standardContainers,
49
+ initContainers,
50
+ ephemeralContainers,
51
+ },
52
+ },
53
+ } as PeprValidateRequest<a.Pod>;
54
+
55
+ let result = containers(peprValidationRequest);
56
+ expect(result).toEqual(expect.arrayContaining(allContainers));
57
+ expect(result).toHaveLength(allContainers.length);
58
+
59
+ result = containers(peprValidationRequest, "containers");
60
+ expect(result).toEqual(expect.arrayContaining(standardContainers));
61
+ expect(result).toHaveLength(standardContainers.length);
62
+
63
+ result = containers(peprValidationRequest, "initContainers");
64
+ expect(result).toEqual(expect.arrayContaining(initContainers));
65
+ expect(result).toHaveLength(initContainers.length);
66
+
67
+ result = containers(peprValidationRequest, "ephemeralContainers");
68
+ expect(result).toEqual(expect.arrayContaining(ephemeralContainers));
69
+ expect(result).toHaveLength(ephemeralContainers.length);
70
+ });
71
+
72
+ test("should return a list of containers in the pod when in a mutate block", async () => {
73
+ const standardContainers = [
74
+ {
75
+ name: "container-1",
76
+ },
77
+ ];
78
+ const initContainers = [
79
+ {
80
+ name: "init-container-1",
81
+ },
82
+ ];
83
+ const ephemeralContainers = [
84
+ {
85
+ name: "ephemeral-container-1",
86
+ },
87
+ ];
88
+ const allContainers = [...standardContainers, ...initContainers, ...ephemeralContainers];
89
+ const peprMutateRequest = {
90
+ Raw: {
91
+ spec: {
92
+ containers: standardContainers,
93
+ initContainers,
94
+ ephemeralContainers,
95
+ },
96
+ },
97
+ } as PeprMutateRequest<a.Pod>;
98
+
99
+ let result = containers(peprMutateRequest);
100
+ expect(result).toEqual(expect.arrayContaining(allContainers));
101
+ expect(result).toHaveLength(allContainers.length);
102
+
103
+ result = containers(peprMutateRequest, "containers");
104
+ expect(result).toEqual(expect.arrayContaining(standardContainers));
105
+ expect(result).toHaveLength(standardContainers.length);
106
+
107
+ result = containers(peprMutateRequest, "initContainers");
108
+ expect(result).toEqual(expect.arrayContaining(initContainers));
109
+ expect(result).toHaveLength(initContainers.length);
110
+
111
+ result = containers(peprMutateRequest, "ephemeralContainers");
112
+ expect(result).toEqual(expect.arrayContaining(ephemeralContainers));
113
+ expect(result).toHaveLength(ephemeralContainers.length);
114
+ });
115
+ });
116
+
117
+ describe("writeEvent", () => {
118
+ let Create: Mock;
119
+ beforeEach(() => {
120
+ jest.clearAllMocks();
121
+
122
+ Create = jest.fn();
123
+
124
+ (K8s as jest.Mock).mockImplementation(() => ({
125
+ Create,
126
+ PatchStatus: jest.fn(),
127
+ }));
128
+ });
129
+
130
+ it("should write a K8s event for the CRD", async () => {
131
+ const cr = {
132
+ apiVersion: "v1",
133
+ kind: "Package",
134
+ metadata: { name: "test", namespace: "default", uid: "1" },
135
+ };
136
+ const event = { message: "Test event" };
137
+ await writeEvent(
138
+ cr as GenericKind,
139
+ event,
140
+ "Warning",
141
+ "ReconciliationFailed",
142
+ "uds.dev/operator",
143
+ process.env.HOSTNAME as string,
144
+ );
145
+ expect(K8s).toHaveBeenCalledWith(kind.CoreEvent);
146
+ expect(Create).toHaveBeenCalledWith({
147
+ ...event,
148
+ type: "Warning",
149
+ reason: "ReconciliationFailed",
150
+ metadata: { namespace: "default", generateName: "test" },
151
+ involvedObject: {
152
+ apiVersion: "v1",
153
+ kind: "Package",
154
+ name: "test",
155
+ namespace: "default",
156
+ uid: "1",
157
+ },
158
+ firstTimestamp: expect.any(Date),
159
+ reportingComponent: "uds.dev/operator",
160
+ reportingInstance: process.env.HOSTNAME,
161
+ });
162
+ });
163
+ });
164
+
165
+ describe("getOwnerRefFrom", () => {
166
+ it("should return the owner reference for the CRD", () => {
167
+ const cr = {
168
+ apiVersion: "v1",
169
+ kind: "Package",
170
+ metadata: { name: "test", namespace: "default", uid: "1" },
171
+ };
172
+ const ownerRef = getOwnerRefFrom(cr as GenericKind);
173
+ expect(ownerRef).toEqual([
174
+ {
175
+ apiVersion: "v1",
176
+ kind: "Package",
177
+ name: "test",
178
+ uid: "1",
179
+ },
180
+ ]);
181
+ });
182
+ });
183
+ describe("sanitizeResourceName Fuzzing Tests", () => {
184
+ test("should handle any random string input", () => {
185
+ fc.assert(
186
+ fc.property(fc.string(), name => {
187
+ expect(() => sanitizeResourceName(name)).not.toThrow();
188
+ const sanitized = sanitizeResourceName(name);
189
+ expect(typeof sanitized).toBe("string");
190
+ }),
191
+ );
192
+ });
193
+ });
194
+
195
+ describe("sanitizeResourceName Property-Based Tests", () => {
196
+ test("should always return lowercase, alphanumeric names without leading/trailing hyphens", () => {
197
+ fc.assert(
198
+ fc.property(fc.string(), name => {
199
+ const sanitized = sanitizeResourceName(name);
200
+ if (sanitized.length > 0) {
201
+ expect(sanitized).toMatch(/^[a-z0-9]+(?:-[a-z0-9]+)*$/);
202
+ }
203
+ expect(sanitized).toBe(sanitized.toLowerCase());
204
+ expect(sanitized.length).toBeLessThanOrEqual(250);
205
+ }),
206
+ );
207
+ });
208
+ });
209
+
210
+ describe("sanitizeResourceName", () => {
211
+ it("should return same resource name if no sanitization needed", () => {
212
+ const resourceName = "test-resource";
213
+ const sanitizedResourceName = sanitizeResourceName(resourceName);
214
+ expect(sanitizedResourceName).toEqual("test-resource");
215
+ });
216
+
217
+ it("should replace capital letters with lowercase letters", () => {
218
+ const resourceName = "Test-ResourCe";
219
+ const sanitizedResourceName = sanitizeResourceName(resourceName);
220
+ expect(sanitizedResourceName).toEqual("test-resource");
221
+ });
222
+
223
+ it("should replace sequences of non-alphanumeric characters with a single -", () => {
224
+ const resourceName = "test-*^%- -!=!resource";
225
+ const sanitizedResourceName = sanitizeResourceName(resourceName);
226
+ expect(sanitizedResourceName).toEqual("test-resource");
227
+ });
228
+
229
+ it("should truncate name to 250 characters", () => {
230
+ const resourceName =
231
+ "test-resourceresourceresourceresourceresourceresourceresourceresourceresourceresourceresourceresourceresourceresourceresourceresourceresourceresourceresourceresourceresourceresourceresourceresourceresourceresourceresourceresourceresourceresourceresource";
232
+ const sanitizedResourceName = sanitizeResourceName(resourceName);
233
+ expect(sanitizedResourceName).toEqual(
234
+ "test-resourceresourceresourceresourceresourceresourceresourceresourceresourceresourceresourceresourceresourceresourceresourceresourceresourceresourceresourceresourceresourceresourceresourceresourceresourceresourceresourceresourceresourceresourceresou",
235
+ );
236
+ });
237
+
238
+ it("should remove leading and trailing non-letter characters", () => {
239
+ const resourceName = " 1=-test-resource *2 ";
240
+ const sanitizedResourceName = sanitizeResourceName(resourceName);
241
+ expect(sanitizedResourceName).toEqual("test-resource");
242
+ });
243
+ });
@@ -0,0 +1,6 @@
1
+ {
2
+ "extends": [".eslintrc.template.json"],
3
+ "parserOptions": {
4
+ "project": ["src/templates/tsconfig.json"]
5
+ }
6
+ }
package/.prettierignore DELETED
@@ -1 +0,0 @@
1
- src/cli/init/templates/data.json