pepr 0.35.0 → 0.36.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/dist/cli.js +1 -1
- package/dist/controller.js +1 -1
- package/dist/lib/capability.d.ts.map +1 -1
- package/dist/lib/filter.d.ts.map +1 -1
- package/dist/lib/helpers.d.ts.map +1 -1
- package/dist/lib/logger.d.ts +1 -1
- package/dist/lib/logger.d.ts.map +1 -1
- package/dist/lib/queue.d.ts +19 -3
- package/dist/lib/queue.d.ts.map +1 -1
- package/dist/lib/types.d.ts +3 -0
- package/dist/lib/types.d.ts.map +1 -1
- package/dist/lib/watch-processor.d.ts +7 -5
- package/dist/lib/watch-processor.d.ts.map +1 -1
- package/dist/lib.js +97 -42
- package/dist/lib.js.map +3 -3
- package/dist/sdk/sdk.d.ts +5 -3
- package/dist/sdk/sdk.d.ts.map +1 -1
- package/package.json +6 -6
- package/src/lib/capability.ts +9 -1
- package/src/lib/filter.test.ts +85 -1
- package/src/lib/filter.ts +9 -0
- package/src/lib/helpers.test.ts +34 -0
- package/src/lib/helpers.ts +5 -0
- package/src/lib/queue.test.ts +138 -44
- package/src/lib/queue.ts +48 -13
- package/src/lib/types.ts +3 -0
- package/src/lib/watch-processor.test.ts +89 -124
- package/src/lib/watch-processor.ts +34 -31
- package/src/sdk/sdk.test.ts +46 -13
- package/src/sdk/sdk.ts +15 -6
package/dist/sdk/sdk.d.ts
CHANGED
|
@@ -24,10 +24,12 @@ export declare function containers(request: PeprValidateRequest<a.Pod> | PeprMut
|
|
|
24
24
|
export declare function writeEvent(cr: GenericKind, event: Partial<kind.CoreEvent>, eventType: string, eventReason: string, reportingComponent: string, reportingInstance: string): Promise<void>;
|
|
25
25
|
/**
|
|
26
26
|
* Get the owner reference for a custom resource
|
|
27
|
-
* @param
|
|
28
|
-
* @
|
|
27
|
+
* @param customResource the custom resource to get the owner reference for
|
|
28
|
+
* @param blockOwnerDeletion if true, AND if the owner has the "foregroundDeletion" finalizer, then the owner cannot be deleted from the key-value store until this reference is removed.
|
|
29
|
+
* @param controller if true, this reference points to the managing controller.
|
|
30
|
+
* @returns the owner reference array for the custom resource
|
|
29
31
|
*/
|
|
30
|
-
export declare function getOwnerRefFrom(
|
|
32
|
+
export declare function getOwnerRefFrom(customResource: GenericKind, blockOwnerDeletion?: boolean, controller?: boolean): V1OwnerReference[];
|
|
31
33
|
/**
|
|
32
34
|
* Sanitize a resource name to make it a valid Kubernetes resource name.
|
|
33
35
|
*
|
package/dist/sdk/sdk.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"sdk.d.ts","sourceRoot":"","sources":["../../src/sdk/sdk.ts"],"names":[],"mappings":"AAGA,OAAO,EAAE,mBAAmB,EAAE,MAAM,yBAAyB,CAAC;AAC9D,OAAO,EAAE,iBAAiB,EAAE,MAAM,uBAAuB,CAAC;AAC1D,OAAO,EAAE,CAAC,EAAE,MAAM,QAAQ,CAAC;AAC3B,OAAO,EAAE,gBAAgB,EAAE,MAAM,yBAAyB,CAAC;AAC3D,OAAO,EAAE,WAAW,EAAE,MAAM,0BAA0B,CAAC;AACvD,OAAO,EAAO,IAAI,EAAE,MAAM,0BAA0B,CAAC;AAGrD;;;;;GAKG;AACH,wBAAgB,UAAU,CACxB,OAAO,EAAE,mBAAmB,CAAC,CAAC,CAAC,GAAG,CAAC,GAAG,iBAAiB,CAAC,CAAC,CAAC,GAAG,CAAC,EAC9D,aAAa,CAAC,EAAE,YAAY,GAAG,gBAAgB,GAAG,qBAAqB,mDAgBxE;AAED;;;;;;;;;GASG;AACH,wBAAsB,UAAU,CAC9B,EAAE,EAAE,WAAW,EACf,KAAK,EAAE,OAAO,CAAC,IAAI,CAAC,SAAS,CAAC,EAC9B,SAAS,EAAE,MAAM,EACjB,WAAW,EAAE,MAAM,EACnB,kBAAkB,EAAE,MAAM,EAC1B,iBAAiB,EAAE,MAAM,iBAwB1B;AAED
|
|
1
|
+
{"version":3,"file":"sdk.d.ts","sourceRoot":"","sources":["../../src/sdk/sdk.ts"],"names":[],"mappings":"AAGA,OAAO,EAAE,mBAAmB,EAAE,MAAM,yBAAyB,CAAC;AAC9D,OAAO,EAAE,iBAAiB,EAAE,MAAM,uBAAuB,CAAC;AAC1D,OAAO,EAAE,CAAC,EAAE,MAAM,QAAQ,CAAC;AAC3B,OAAO,EAAE,gBAAgB,EAAE,MAAM,yBAAyB,CAAC;AAC3D,OAAO,EAAE,WAAW,EAAE,MAAM,0BAA0B,CAAC;AACvD,OAAO,EAAO,IAAI,EAAE,MAAM,0BAA0B,CAAC;AAGrD;;;;;GAKG;AACH,wBAAgB,UAAU,CACxB,OAAO,EAAE,mBAAmB,CAAC,CAAC,CAAC,GAAG,CAAC,GAAG,iBAAiB,CAAC,CAAC,CAAC,GAAG,CAAC,EAC9D,aAAa,CAAC,EAAE,YAAY,GAAG,gBAAgB,GAAG,qBAAqB,mDAgBxE;AAED;;;;;;;;;GASG;AACH,wBAAsB,UAAU,CAC9B,EAAE,EAAE,WAAW,EACf,KAAK,EAAE,OAAO,CAAC,IAAI,CAAC,SAAS,CAAC,EAC9B,SAAS,EAAE,MAAM,EACjB,WAAW,EAAE,MAAM,EACnB,kBAAkB,EAAE,MAAM,EAC1B,iBAAiB,EAAE,MAAM,iBAwB1B;AAED;;;;;;GAMG;AACH,wBAAgB,eAAe,CAC7B,cAAc,EAAE,WAAW,EAC3B,kBAAkB,CAAC,EAAE,OAAO,EAC5B,UAAU,CAAC,EAAE,OAAO,GACnB,gBAAgB,EAAE,CAcpB;AAED;;;;;GAKG;AACH,wBAAgB,oBAAoB,CAAC,IAAI,EAAE,MAAM,UAYhD"}
|
package/package.json
CHANGED
|
@@ -13,7 +13,7 @@
|
|
|
13
13
|
"/dist",
|
|
14
14
|
"/src"
|
|
15
15
|
],
|
|
16
|
-
"version": "0.
|
|
16
|
+
"version": "0.36.0",
|
|
17
17
|
"main": "dist/lib.js",
|
|
18
18
|
"types": "dist/lib.d.ts",
|
|
19
19
|
"scripts": {
|
|
@@ -39,18 +39,18 @@
|
|
|
39
39
|
},
|
|
40
40
|
"dependencies": {
|
|
41
41
|
"@types/ramda": "0.30.2",
|
|
42
|
-
"express": "4.
|
|
42
|
+
"express": "4.21.0",
|
|
43
43
|
"fast-json-patch": "3.1.1",
|
|
44
44
|
"json-pointer": "^0.6.2",
|
|
45
|
-
"kubernetes-fluent-client": "3.0.
|
|
46
|
-
"pino": "9.
|
|
45
|
+
"kubernetes-fluent-client": "3.0.3",
|
|
46
|
+
"pino": "9.4.0",
|
|
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.
|
|
53
|
-
"@commitlint/config-conventional": "19.
|
|
52
|
+
"@commitlint/cli": "19.5.0",
|
|
53
|
+
"@commitlint/config-conventional": "19.5.0",
|
|
54
54
|
"@fast-check/jest": "^2.0.1",
|
|
55
55
|
"@jest/globals": "29.7.0",
|
|
56
56
|
"@types/eslint": "9.6.1",
|
package/src/lib/capability.ts
CHANGED
|
@@ -198,12 +198,13 @@ export class Capability implements CapabilityExport {
|
|
|
198
198
|
namespaces: [],
|
|
199
199
|
labels: {},
|
|
200
200
|
annotations: {},
|
|
201
|
+
deletionTimestamp: false,
|
|
201
202
|
},
|
|
202
203
|
};
|
|
203
204
|
|
|
204
205
|
const bindings = this.#bindings;
|
|
205
206
|
const prefix = `${this.#name}: ${model.name}`;
|
|
206
|
-
const commonChain = { WithLabel, WithAnnotation, Mutate, Validate, Watch, Reconcile };
|
|
207
|
+
const commonChain = { WithLabel, WithAnnotation, WithDeletionTimestamp, Mutate, Validate, Watch, Reconcile };
|
|
207
208
|
const isNotEmpty = (value: object) => Object.keys(value).length > 0;
|
|
208
209
|
const log = (message: string, cbString: string) => {
|
|
209
210
|
const filteredObj = pickBy(isNotEmpty, binding.filters);
|
|
@@ -277,6 +278,12 @@ export class Capability implements CapabilityExport {
|
|
|
277
278
|
return { ...commonChain, WithName };
|
|
278
279
|
}
|
|
279
280
|
|
|
281
|
+
function WithDeletionTimestamp(): BindingFilter<T> {
|
|
282
|
+
Log.debug("Add deletionTimestamp filter");
|
|
283
|
+
binding.filters.deletionTimestamp = true;
|
|
284
|
+
return commonChain;
|
|
285
|
+
}
|
|
286
|
+
|
|
280
287
|
function WithName(name: string): BindingFilter<T> {
|
|
281
288
|
Log.debug(`Add name filter ${name}`, prefix);
|
|
282
289
|
binding.filters.name = name;
|
|
@@ -301,6 +308,7 @@ export class Capability implements CapabilityExport {
|
|
|
301
308
|
...commonChain,
|
|
302
309
|
InNamespace,
|
|
303
310
|
WithName,
|
|
311
|
+
WithDeletionTimestamp,
|
|
304
312
|
};
|
|
305
313
|
}
|
|
306
314
|
|
package/src/lib/filter.test.ts
CHANGED
|
@@ -29,6 +29,7 @@ describe("Fuzzing shouldSkipRequest", () => {
|
|
|
29
29
|
namespaces: fc.array(fc.string()),
|
|
30
30
|
labels: fc.dictionary(fc.string(), fc.string()),
|
|
31
31
|
annotations: fc.dictionary(fc.string(), fc.string()),
|
|
32
|
+
deletionTimestamp: fc.boolean(),
|
|
32
33
|
}),
|
|
33
34
|
}),
|
|
34
35
|
fc.record({
|
|
@@ -41,6 +42,11 @@ describe("Fuzzing shouldSkipRequest", () => {
|
|
|
41
42
|
version: fc.string(),
|
|
42
43
|
kind: fc.string(),
|
|
43
44
|
}),
|
|
45
|
+
object: fc.record({
|
|
46
|
+
metadata: fc.record({
|
|
47
|
+
deletionTimestamp: fc.option(fc.date()),
|
|
48
|
+
}),
|
|
49
|
+
}),
|
|
44
50
|
}),
|
|
45
51
|
fc.array(fc.string()),
|
|
46
52
|
(binding, req, capabilityNamespaces) => {
|
|
@@ -69,6 +75,7 @@ describe("Property-Based Testing shouldSkipRequest", () => {
|
|
|
69
75
|
namespaces: fc.array(fc.string()),
|
|
70
76
|
labels: fc.dictionary(fc.string(), fc.string()),
|
|
71
77
|
annotations: fc.dictionary(fc.string(), fc.string()),
|
|
78
|
+
deletionTimestamp: fc.boolean(),
|
|
72
79
|
}),
|
|
73
80
|
}),
|
|
74
81
|
fc.record({
|
|
@@ -81,6 +88,11 @@ describe("Property-Based Testing shouldSkipRequest", () => {
|
|
|
81
88
|
version: fc.string(),
|
|
82
89
|
kind: fc.string(),
|
|
83
90
|
}),
|
|
91
|
+
object: fc.record({
|
|
92
|
+
metadata: fc.record({
|
|
93
|
+
deletionTimestamp: fc.option(fc.date()),
|
|
94
|
+
}),
|
|
95
|
+
}),
|
|
84
96
|
}),
|
|
85
97
|
fc.array(fc.string()),
|
|
86
98
|
(binding, req, capabilityNamespaces) => {
|
|
@@ -103,6 +115,7 @@ test("should reject when name does not match", () => {
|
|
|
103
115
|
namespaces: [],
|
|
104
116
|
labels: {},
|
|
105
117
|
annotations: {},
|
|
118
|
+
deletionTimestamp: false,
|
|
106
119
|
},
|
|
107
120
|
callback,
|
|
108
121
|
};
|
|
@@ -121,6 +134,7 @@ test("should reject when kind does not match", () => {
|
|
|
121
134
|
namespaces: [],
|
|
122
135
|
labels: {},
|
|
123
136
|
annotations: {},
|
|
137
|
+
deletionTimestamp: false,
|
|
124
138
|
},
|
|
125
139
|
callback,
|
|
126
140
|
};
|
|
@@ -139,6 +153,7 @@ test("should reject when group does not match", () => {
|
|
|
139
153
|
namespaces: [],
|
|
140
154
|
labels: {},
|
|
141
155
|
annotations: {},
|
|
156
|
+
deletionTimestamp: false,
|
|
142
157
|
},
|
|
143
158
|
callback,
|
|
144
159
|
};
|
|
@@ -161,6 +176,7 @@ test("should reject when version does not match", () => {
|
|
|
161
176
|
namespaces: [],
|
|
162
177
|
labels: {},
|
|
163
178
|
annotations: {},
|
|
179
|
+
deletionTimestamp: false,
|
|
164
180
|
},
|
|
165
181
|
callback,
|
|
166
182
|
};
|
|
@@ -179,6 +195,7 @@ test("should allow when group, version, and kind match", () => {
|
|
|
179
195
|
namespaces: [],
|
|
180
196
|
labels: {},
|
|
181
197
|
annotations: {},
|
|
198
|
+
deletionTimestamp: false,
|
|
182
199
|
},
|
|
183
200
|
callback,
|
|
184
201
|
};
|
|
@@ -201,6 +218,7 @@ test("should allow when kind match and others are empty", () => {
|
|
|
201
218
|
namespaces: [],
|
|
202
219
|
labels: {},
|
|
203
220
|
annotations: {},
|
|
221
|
+
deletionTimestamp: false,
|
|
204
222
|
},
|
|
205
223
|
callback,
|
|
206
224
|
};
|
|
@@ -219,6 +237,7 @@ test("should reject when teh capability namespace does not match", () => {
|
|
|
219
237
|
namespaces: [],
|
|
220
238
|
labels: {},
|
|
221
239
|
annotations: {},
|
|
240
|
+
deletionTimestamp: false,
|
|
222
241
|
},
|
|
223
242
|
callback,
|
|
224
243
|
};
|
|
@@ -237,6 +256,7 @@ test("should reject when namespace does not match", () => {
|
|
|
237
256
|
namespaces: ["bleh"],
|
|
238
257
|
labels: {},
|
|
239
258
|
annotations: {},
|
|
259
|
+
deletionTimestamp: false,
|
|
240
260
|
},
|
|
241
261
|
callback,
|
|
242
262
|
};
|
|
@@ -255,6 +275,7 @@ test("should allow when namespace is match", () => {
|
|
|
255
275
|
namespaces: ["default", "unicorn", "things"],
|
|
256
276
|
labels: {},
|
|
257
277
|
annotations: {},
|
|
278
|
+
deletionTimestamp: false,
|
|
258
279
|
},
|
|
259
280
|
callback,
|
|
260
281
|
};
|
|
@@ -275,6 +296,7 @@ test("should reject when label does not match", () => {
|
|
|
275
296
|
foo: "bar",
|
|
276
297
|
},
|
|
277
298
|
annotations: {},
|
|
299
|
+
deletionTimestamp: false,
|
|
278
300
|
},
|
|
279
301
|
callback,
|
|
280
302
|
};
|
|
@@ -290,7 +312,7 @@ test("should allow when label is match", () => {
|
|
|
290
312
|
kind: podKind,
|
|
291
313
|
filters: {
|
|
292
314
|
name: "",
|
|
293
|
-
|
|
315
|
+
deletionTimestamp: false,
|
|
294
316
|
namespaces: [],
|
|
295
317
|
labels: {
|
|
296
318
|
foo: "bar",
|
|
@@ -324,6 +346,7 @@ test("should reject when annotation does not match", () => {
|
|
|
324
346
|
annotations: {
|
|
325
347
|
foo: "bar",
|
|
326
348
|
},
|
|
349
|
+
deletionTimestamp: false,
|
|
327
350
|
},
|
|
328
351
|
callback,
|
|
329
352
|
};
|
|
@@ -345,6 +368,7 @@ test("should allow when annotation is match", () => {
|
|
|
345
368
|
foo: "bar",
|
|
346
369
|
test: "test1",
|
|
347
370
|
},
|
|
371
|
+
deletionTimestamp: false,
|
|
348
372
|
},
|
|
349
373
|
callback,
|
|
350
374
|
};
|
|
@@ -368,6 +392,7 @@ test("should use `oldObject` when the operation is `DELETE`", () => {
|
|
|
368
392
|
filters: {
|
|
369
393
|
name: "",
|
|
370
394
|
namespaces: [],
|
|
395
|
+
deletionTimestamp: false,
|
|
371
396
|
labels: {
|
|
372
397
|
"app.kubernetes.io/name": "cool-name-podinfo",
|
|
373
398
|
},
|
|
@@ -382,3 +407,62 @@ test("should use `oldObject` when the operation is `DELETE`", () => {
|
|
|
382
407
|
|
|
383
408
|
expect(shouldSkipRequest(binding, pod, [])).toBe(false);
|
|
384
409
|
});
|
|
410
|
+
|
|
411
|
+
test("should skip processing when deletionTimestamp is not present on pod", () => {
|
|
412
|
+
const binding = {
|
|
413
|
+
model: kind.Pod,
|
|
414
|
+
event: Event.Any,
|
|
415
|
+
kind: podKind,
|
|
416
|
+
filters: {
|
|
417
|
+
name: "",
|
|
418
|
+
namespaces: [],
|
|
419
|
+
labels: {},
|
|
420
|
+
annotations: {
|
|
421
|
+
foo: "bar",
|
|
422
|
+
test: "test1",
|
|
423
|
+
},
|
|
424
|
+
deletionTimestamp: true,
|
|
425
|
+
},
|
|
426
|
+
callback,
|
|
427
|
+
};
|
|
428
|
+
|
|
429
|
+
const pod = CreatePod();
|
|
430
|
+
pod.object.metadata = pod.object.metadata || {};
|
|
431
|
+
pod.object.metadata.annotations = {
|
|
432
|
+
foo: "bar",
|
|
433
|
+
test: "test1",
|
|
434
|
+
test2: "test2",
|
|
435
|
+
};
|
|
436
|
+
|
|
437
|
+
expect(shouldSkipRequest(binding, pod, [])).toBe(true);
|
|
438
|
+
});
|
|
439
|
+
|
|
440
|
+
test("should processing when deletionTimestamp is not present on pod", () => {
|
|
441
|
+
const binding = {
|
|
442
|
+
model: kind.Pod,
|
|
443
|
+
event: Event.Any,
|
|
444
|
+
kind: podKind,
|
|
445
|
+
filters: {
|
|
446
|
+
name: "",
|
|
447
|
+
namespaces: [],
|
|
448
|
+
labels: {},
|
|
449
|
+
annotations: {
|
|
450
|
+
foo: "bar",
|
|
451
|
+
test: "test1",
|
|
452
|
+
},
|
|
453
|
+
deletionTimestamp: true,
|
|
454
|
+
},
|
|
455
|
+
callback,
|
|
456
|
+
};
|
|
457
|
+
|
|
458
|
+
const pod = CreatePod();
|
|
459
|
+
pod.object.metadata = pod.object.metadata || {};
|
|
460
|
+
pod.object.metadata!.deletionTimestamp = new Date("2021-09-01T00:00:00Z");
|
|
461
|
+
pod.object.metadata.annotations = {
|
|
462
|
+
foo: "bar",
|
|
463
|
+
test: "test1",
|
|
464
|
+
test2: "test2",
|
|
465
|
+
};
|
|
466
|
+
|
|
467
|
+
expect(shouldSkipRequest(binding, pod, [])).toBe(false);
|
|
468
|
+
});
|
package/src/lib/filter.ts
CHANGED
|
@@ -22,6 +22,15 @@ export function shouldSkipRequest(binding: Binding, req: AdmissionRequest, capab
|
|
|
22
22
|
const { metadata } = srcObject || {};
|
|
23
23
|
const combinedNamespaces = [...namespaces, ...capabilityNamespaces];
|
|
24
24
|
|
|
25
|
+
// Delete bindings do not work through admission with DeletionTimestamp
|
|
26
|
+
if (binding.event.includes(Event.Delete) && binding.filters?.deletionTimestamp) {
|
|
27
|
+
return true;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
// Test for deletionTimestamp
|
|
31
|
+
if (binding.filters?.deletionTimestamp && !req.object.metadata?.deletionTimestamp) {
|
|
32
|
+
return true;
|
|
33
|
+
}
|
|
25
34
|
// Test for matching operation
|
|
26
35
|
if (!binding.event.includes(operation) && !binding.event.includes(Event.Any)) {
|
|
27
36
|
return true;
|
package/src/lib/helpers.test.ts
CHANGED
|
@@ -1056,6 +1056,40 @@ describe("filterMatcher", () => {
|
|
|
1056
1056
|
expect(result).toEqual("Ignoring Watch Callback: Cannot use a namespace filter in a namespace object.");
|
|
1057
1057
|
});
|
|
1058
1058
|
|
|
1059
|
+
test("return deletionTimestamp error when there is no deletionTimestamp in the object", () => {
|
|
1060
|
+
const binding = {
|
|
1061
|
+
filters: { deletionTimestamp: true },
|
|
1062
|
+
};
|
|
1063
|
+
const obj = {
|
|
1064
|
+
metadata: {},
|
|
1065
|
+
};
|
|
1066
|
+
const capabilityNamespaces: string[] = [];
|
|
1067
|
+
const result = filterNoMatchReason(
|
|
1068
|
+
binding as unknown as Partial<Binding>,
|
|
1069
|
+
obj as unknown as Partial<KubernetesObject>,
|
|
1070
|
+
capabilityNamespaces,
|
|
1071
|
+
);
|
|
1072
|
+
expect(result).toEqual("Ignoring Watch Callback: Object does not have a deletion timestamp.");
|
|
1073
|
+
});
|
|
1074
|
+
|
|
1075
|
+
test("return no deletionTimestamp error when there is a deletionTimestamp in the object", () => {
|
|
1076
|
+
const binding = {
|
|
1077
|
+
filters: { deletionTimestamp: true },
|
|
1078
|
+
};
|
|
1079
|
+
const obj = {
|
|
1080
|
+
metadata: {
|
|
1081
|
+
deletionTimestamp: "2021-01-01T00:00:00Z",
|
|
1082
|
+
},
|
|
1083
|
+
};
|
|
1084
|
+
const capabilityNamespaces: string[] = [];
|
|
1085
|
+
const result = filterNoMatchReason(
|
|
1086
|
+
binding as unknown as Partial<Binding>,
|
|
1087
|
+
obj as unknown as Partial<KubernetesObject>,
|
|
1088
|
+
capabilityNamespaces,
|
|
1089
|
+
);
|
|
1090
|
+
expect(result).not.toEqual("Ignoring Watch Callback: Object does not have a deletion timestamp.");
|
|
1091
|
+
});
|
|
1092
|
+
|
|
1059
1093
|
test("returns label overlap error when there is no overlap between binding and object labels", () => {
|
|
1060
1094
|
const binding = {
|
|
1061
1095
|
filters: { labels: { key: "value" } },
|
package/src/lib/helpers.ts
CHANGED
|
@@ -73,6 +73,11 @@ export function filterNoMatchReason(
|
|
|
73
73
|
obj: Partial<KubernetesObject>,
|
|
74
74
|
capabilityNamespaces: string[],
|
|
75
75
|
): string {
|
|
76
|
+
// binding deletionTimestamp filter and object deletionTimestamp dont match
|
|
77
|
+
if (binding.filters?.deletionTimestamp && !obj.metadata?.deletionTimestamp) {
|
|
78
|
+
return `Ignoring Watch Callback: Object does not have a deletion timestamp.`;
|
|
79
|
+
}
|
|
80
|
+
|
|
76
81
|
// binding kind is namespace with a InNamespace filter
|
|
77
82
|
if (binding.kind && binding.kind.kind === "Namespace" && binding.filters && binding.filters.namespaces.length !== 0) {
|
|
78
83
|
return `Ignoring Watch Callback: Cannot use a namespace filter in a namespace object.`;
|
package/src/lib/queue.test.ts
CHANGED
|
@@ -1,58 +1,152 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import { KubernetesObject } from "@kubernetes/client-node";
|
|
1
|
+
import { afterEach, describe, expect, jest, it } from "@jest/globals";
|
|
3
2
|
import { WatchPhase } from "kubernetes-fluent-client/dist/fluent/types";
|
|
4
3
|
import { Queue } from "./queue";
|
|
5
4
|
|
|
5
|
+
import Log from "./logger";
|
|
6
|
+
jest.mock("./logger");
|
|
7
|
+
|
|
6
8
|
describe("Queue", () => {
|
|
7
|
-
|
|
9
|
+
afterEach(() => {
|
|
10
|
+
jest.resetAllMocks();
|
|
11
|
+
});
|
|
12
|
+
|
|
13
|
+
it("is uniquely identifiable", () => {
|
|
14
|
+
const name = "kind/namespace";
|
|
15
|
+
const queue = new Queue(name);
|
|
16
|
+
|
|
17
|
+
const label = queue.label();
|
|
18
|
+
|
|
19
|
+
expect(label).toEqual(
|
|
20
|
+
expect.objectContaining({
|
|
21
|
+
// given name of queue
|
|
22
|
+
name,
|
|
23
|
+
|
|
24
|
+
// unique, generated value (to disambiguate similarly-named queues)
|
|
25
|
+
// <epoch timestamp (ms)>-<4 char hex>
|
|
26
|
+
uid: expect.stringMatching(/[0-9]{13}-[0-9a-f]{4}/),
|
|
27
|
+
}),
|
|
28
|
+
);
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
it("exposes runtime stats", async () => {
|
|
32
|
+
const name = "kind/namespace";
|
|
33
|
+
const queue = new Queue(name);
|
|
34
|
+
|
|
35
|
+
expect(queue.stats()).toEqual(
|
|
36
|
+
expect.objectContaining({
|
|
37
|
+
queue: queue.label(),
|
|
38
|
+
stats: { length: 0 },
|
|
39
|
+
}),
|
|
40
|
+
);
|
|
41
|
+
|
|
42
|
+
const kubeObj = { metadata: { name: "test-nm", namespace: "test-ns" } };
|
|
43
|
+
const watchCb = () =>
|
|
44
|
+
new Promise<void>(res => {
|
|
45
|
+
setTimeout(res, 100);
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
await Promise.all([
|
|
49
|
+
queue.enqueue(kubeObj, WatchPhase.Added, watchCb),
|
|
50
|
+
queue.enqueue(kubeObj, WatchPhase.Added, watchCb),
|
|
51
|
+
queue.enqueue(kubeObj, WatchPhase.Added, watchCb),
|
|
52
|
+
queue.enqueue(kubeObj, WatchPhase.Added, watchCb),
|
|
53
|
+
]);
|
|
54
|
+
|
|
55
|
+
const logDebug = Log.debug as jest.Mock;
|
|
56
|
+
const stats = logDebug.mock.calls
|
|
57
|
+
.flat()
|
|
58
|
+
.map(m => JSON.stringify(m))
|
|
59
|
+
.filter(m => m.includes('"stats":'));
|
|
8
60
|
|
|
9
|
-
|
|
10
|
-
|
|
61
|
+
[
|
|
62
|
+
'"length":1', // 1st entry runs near-immediately, so queue won't fill
|
|
63
|
+
'"length":1', // afterward, queue fills & unfills as callbacks process
|
|
64
|
+
'"length":2',
|
|
65
|
+
'"length":3',
|
|
66
|
+
'"length":3',
|
|
67
|
+
'"length":2',
|
|
68
|
+
'"length":1',
|
|
69
|
+
'"length":0',
|
|
70
|
+
].map((exp, idx) => {
|
|
71
|
+
expect(stats[idx]).toEqual(expect.stringContaining(exp));
|
|
72
|
+
});
|
|
11
73
|
});
|
|
12
74
|
|
|
13
|
-
|
|
14
|
-
const
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
const
|
|
75
|
+
it("resolves when an enqueued event dequeues without error", async () => {
|
|
76
|
+
const name = "kind/namespace";
|
|
77
|
+
const queue = new Queue(name);
|
|
78
|
+
|
|
79
|
+
const kubeObj = { metadata: { name: "test-nm", namespace: "test-ns" } };
|
|
80
|
+
const watchCb = () =>
|
|
81
|
+
new Promise<void>(res => {
|
|
82
|
+
setTimeout(res, 10);
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
const promise = queue.enqueue(kubeObj, WatchPhase.Added, watchCb);
|
|
18
86
|
expect(promise).toBeInstanceOf(Promise);
|
|
19
|
-
|
|
87
|
+
|
|
88
|
+
await expect(promise).resolves.not.toThrow();
|
|
20
89
|
});
|
|
21
90
|
|
|
22
|
-
|
|
23
|
-
const
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
const
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
91
|
+
it("rejects when an enqueued event dequeues with error", async () => {
|
|
92
|
+
const name = "kind/namespace";
|
|
93
|
+
const queue = new Queue(name);
|
|
94
|
+
|
|
95
|
+
const kubeObj = { metadata: { name: "test-nm", namespace: "test-ns" } };
|
|
96
|
+
const watchCb = () =>
|
|
97
|
+
new Promise<void>((_, reject) => {
|
|
98
|
+
setTimeout(() => {
|
|
99
|
+
reject("oof");
|
|
100
|
+
}, 10);
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
const promise = queue.enqueue(kubeObj, WatchPhase.Added, watchCb);
|
|
104
|
+
expect(promise).toBeInstanceOf(Promise);
|
|
105
|
+
|
|
106
|
+
await expect(promise).rejects.toBe("oof");
|
|
37
107
|
});
|
|
38
108
|
|
|
39
|
-
|
|
40
|
-
const
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
const
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
109
|
+
it("processes events in FIFO order", async () => {
|
|
110
|
+
const name = "kind/namespace";
|
|
111
|
+
const queue = new Queue(name);
|
|
112
|
+
|
|
113
|
+
const kubeObj = { metadata: { name: "test-nm", namespace: "test-ns" } };
|
|
114
|
+
const watchA = () =>
|
|
115
|
+
new Promise<void>(resolve => {
|
|
116
|
+
setTimeout(() => {
|
|
117
|
+
Log.info("watchA");
|
|
118
|
+
resolve();
|
|
119
|
+
}, 15);
|
|
120
|
+
});
|
|
121
|
+
const watchB = () =>
|
|
122
|
+
new Promise<void>(resolve => {
|
|
123
|
+
setTimeout(() => {
|
|
124
|
+
Log.info("watchB");
|
|
125
|
+
resolve();
|
|
126
|
+
}, 10);
|
|
127
|
+
});
|
|
128
|
+
const watchC = () =>
|
|
129
|
+
new Promise<void>(resolve => {
|
|
130
|
+
setTimeout(() => {
|
|
131
|
+
Log.info("watchC");
|
|
132
|
+
resolve();
|
|
133
|
+
}, 5);
|
|
134
|
+
});
|
|
135
|
+
|
|
136
|
+
await Promise.all([
|
|
137
|
+
queue.enqueue(kubeObj, WatchPhase.Added, watchA),
|
|
138
|
+
queue.enqueue(kubeObj, WatchPhase.Added, watchB),
|
|
139
|
+
queue.enqueue(kubeObj, WatchPhase.Added, watchC),
|
|
140
|
+
]);
|
|
141
|
+
|
|
142
|
+
const logInfo = Log.info as jest.Mock;
|
|
143
|
+
const calls = logInfo.mock.calls
|
|
144
|
+
.flat()
|
|
145
|
+
.map(m => JSON.stringify(m))
|
|
146
|
+
.filter(m => /"watch[ABC]"/.test(m));
|
|
147
|
+
|
|
148
|
+
['"watchA"', '"watchB"', '"watchC"'].map((exp, idx) => {
|
|
149
|
+
expect(calls[idx]).toEqual(expect.stringContaining(exp));
|
|
150
|
+
});
|
|
57
151
|
});
|
|
58
152
|
});
|
package/src/lib/queue.ts
CHANGED
|
@@ -2,11 +2,15 @@
|
|
|
2
2
|
// SPDX-FileCopyrightText: 2023-Present The Pepr Authors
|
|
3
3
|
import { KubernetesObject } from "@kubernetes/client-node";
|
|
4
4
|
import { WatchPhase } from "kubernetes-fluent-client/dist/fluent/types";
|
|
5
|
+
import { randomBytes } from "node:crypto";
|
|
5
6
|
import Log from "./logger";
|
|
6
7
|
|
|
8
|
+
type WatchCallback = (obj: KubernetesObject, phase: WatchPhase) => Promise<void>;
|
|
9
|
+
|
|
7
10
|
type QueueItem<K extends KubernetesObject> = {
|
|
8
11
|
item: K;
|
|
9
|
-
|
|
12
|
+
phase: WatchPhase;
|
|
13
|
+
callback: WatchCallback;
|
|
10
14
|
resolve: (value: void | PromiseLike<void>) => void;
|
|
11
15
|
reject: (reason?: string) => void;
|
|
12
16
|
};
|
|
@@ -15,16 +19,27 @@ type QueueItem<K extends KubernetesObject> = {
|
|
|
15
19
|
* Queue is a FIFO queue for reconciling
|
|
16
20
|
*/
|
|
17
21
|
export class Queue<K extends KubernetesObject> {
|
|
22
|
+
#name: string;
|
|
23
|
+
#uid: string;
|
|
18
24
|
#queue: QueueItem<K>[] = [];
|
|
19
25
|
#pendingPromise = false;
|
|
20
|
-
#reconcile?: (obj: KubernetesObject, type: WatchPhase) => Promise<void>;
|
|
21
26
|
|
|
22
|
-
constructor() {
|
|
23
|
-
this.#
|
|
27
|
+
constructor(name: string) {
|
|
28
|
+
this.#name = name;
|
|
29
|
+
this.#uid = `${Date.now()}-${randomBytes(2).toString("hex")}`;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
label() {
|
|
33
|
+
return { name: this.#name, uid: this.#uid };
|
|
24
34
|
}
|
|
25
35
|
|
|
26
|
-
|
|
27
|
-
|
|
36
|
+
stats() {
|
|
37
|
+
return {
|
|
38
|
+
queue: this.label(),
|
|
39
|
+
stats: {
|
|
40
|
+
length: this.#queue.length,
|
|
41
|
+
},
|
|
42
|
+
};
|
|
28
43
|
}
|
|
29
44
|
|
|
30
45
|
/**
|
|
@@ -32,12 +47,23 @@ export class Queue<K extends KubernetesObject> {
|
|
|
32
47
|
* reconciled.
|
|
33
48
|
*
|
|
34
49
|
* @param item The object to reconcile
|
|
50
|
+
* @param type The watch phase requested for reconcile
|
|
51
|
+
* @param reconcile The callback to enqueue for reconcile
|
|
35
52
|
* @returns A promise that resolves when the object is reconciled
|
|
36
53
|
*/
|
|
37
|
-
enqueue(item: K,
|
|
38
|
-
|
|
54
|
+
enqueue(item: K, phase: WatchPhase, reconcile: WatchCallback) {
|
|
55
|
+
const note = {
|
|
56
|
+
queue: this.label(),
|
|
57
|
+
item: {
|
|
58
|
+
name: item.metadata?.name,
|
|
59
|
+
namespace: item.metadata?.namespace,
|
|
60
|
+
resourceVersion: item.metadata?.resourceVersion,
|
|
61
|
+
},
|
|
62
|
+
};
|
|
63
|
+
Log.debug(note, "Enqueueing");
|
|
39
64
|
return new Promise<void>((resolve, reject) => {
|
|
40
|
-
this.#queue.push({ item,
|
|
65
|
+
this.#queue.push({ item, phase, callback: reconcile, resolve, reject });
|
|
66
|
+
Log.debug(this.stats(), "Queue stats - push");
|
|
41
67
|
return this.#dequeue();
|
|
42
68
|
});
|
|
43
69
|
}
|
|
@@ -68,16 +94,25 @@ export class Queue<K extends KubernetesObject> {
|
|
|
68
94
|
this.#pendingPromise = true;
|
|
69
95
|
|
|
70
96
|
// Reconcile the element
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
97
|
+
const note = {
|
|
98
|
+
queue: this.label(),
|
|
99
|
+
item: {
|
|
100
|
+
name: element.item.metadata?.name,
|
|
101
|
+
namespace: element.item.metadata?.namespace,
|
|
102
|
+
resourceVersion: element.item.metadata?.resourceVersion,
|
|
103
|
+
},
|
|
104
|
+
};
|
|
105
|
+
Log.debug(note, "Reconciling");
|
|
106
|
+
await element.callback(element.item, element.phase);
|
|
107
|
+
Log.debug(note, "Reconciled");
|
|
75
108
|
|
|
76
109
|
element.resolve();
|
|
77
110
|
} catch (e) {
|
|
78
111
|
Log.debug(`Error reconciling ${element.item.metadata!.name}`, { error: e });
|
|
79
112
|
element.reject(e);
|
|
80
113
|
} finally {
|
|
114
|
+
Log.debug(this.stats(), "Queue stats - shift");
|
|
115
|
+
|
|
81
116
|
// Reset the pending promise flag
|
|
82
117
|
Log.debug("Resetting pending promise and dequeuing");
|
|
83
118
|
this.#pendingPromise = false;
|