pepr 0.35.0 → 0.37.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 (107) hide show
  1. package/dist/cli/init/index.d.ts.map +1 -1
  2. package/dist/cli/init/templates.d.ts +3 -1
  3. package/dist/cli/init/templates.d.ts.map +1 -1
  4. package/dist/cli/init/utils.d.ts.map +1 -1
  5. package/dist/cli/init/walkthrough.d.ts +10 -3
  6. package/dist/cli/init/walkthrough.d.ts.map +1 -1
  7. package/dist/cli.js +253 -31
  8. package/dist/controller.js +138 -1
  9. package/dist/lib/adjudicators.d.ts +63 -0
  10. package/dist/lib/adjudicators.d.ts.map +1 -0
  11. package/dist/lib/adjudicators.test.d.ts +2 -0
  12. package/dist/lib/adjudicators.test.d.ts.map +1 -0
  13. package/dist/lib/assets/loader.d.ts.map +1 -1
  14. package/dist/lib/assets/pods.d.ts +1 -0
  15. package/dist/lib/assets/pods.d.ts.map +1 -1
  16. package/dist/lib/capability.d.ts +1 -0
  17. package/dist/lib/capability.d.ts.map +1 -1
  18. package/dist/lib/capability.test.d.ts +2 -0
  19. package/dist/lib/capability.test.d.ts.map +1 -0
  20. package/dist/lib/controller/index.d.ts.map +1 -1
  21. package/dist/lib/controller/store.d.ts +4 -0
  22. package/dist/lib/controller/store.d.ts.map +1 -1
  23. package/dist/lib/controller/store.test.d.ts +2 -0
  24. package/dist/lib/controller/store.test.d.ts.map +1 -0
  25. package/dist/lib/filter.d.ts +2 -3
  26. package/dist/lib/filter.d.ts.map +1 -1
  27. package/dist/lib/filter.test.d.ts +2 -1
  28. package/dist/lib/filter.test.d.ts.map +1 -1
  29. package/dist/lib/finalizer.d.ts +6 -0
  30. package/dist/lib/finalizer.d.ts.map +1 -0
  31. package/dist/lib/finalizer.test.d.ts +2 -0
  32. package/dist/lib/finalizer.test.d.ts.map +1 -0
  33. package/dist/lib/helpers.d.ts +2 -2
  34. package/dist/lib/helpers.d.ts.map +1 -1
  35. package/dist/lib/helpers.test.d.ts +1 -1
  36. package/dist/lib/helpers.test.d.ts.map +1 -1
  37. package/dist/lib/k8s.d.ts.map +1 -1
  38. package/dist/lib/logger.d.ts +1 -1
  39. package/dist/lib/logger.d.ts.map +1 -1
  40. package/dist/lib/module.d.ts +2 -1
  41. package/dist/lib/module.d.ts.map +1 -1
  42. package/dist/lib/mutate-processor.d.ts +2 -1
  43. package/dist/lib/mutate-processor.d.ts.map +1 -1
  44. package/dist/lib/mutate-request.d.ts +1 -2
  45. package/dist/lib/mutate-request.d.ts.map +1 -1
  46. package/dist/lib/queue.d.ts +19 -3
  47. package/dist/lib/queue.d.ts.map +1 -1
  48. package/dist/lib/schedule.d.ts +1 -2
  49. package/dist/lib/schedule.d.ts.map +1 -1
  50. package/dist/lib/storage.d.ts.map +1 -1
  51. package/dist/lib/types.d.ts +118 -6
  52. package/dist/lib/types.d.ts.map +1 -1
  53. package/dist/lib/validate-processor.d.ts +4 -2
  54. package/dist/lib/validate-processor.d.ts.map +1 -1
  55. package/dist/lib/validate-request.d.ts +1 -1
  56. package/dist/lib/validate-request.d.ts.map +1 -1
  57. package/dist/lib/watch-processor.d.ts +8 -6
  58. package/dist/lib/watch-processor.d.ts.map +1 -1
  59. package/dist/lib.js +467 -233
  60. package/dist/lib.js.map +4 -4
  61. package/dist/sdk/sdk.d.ts +5 -3
  62. package/dist/sdk/sdk.d.ts.map +1 -1
  63. package/package.json +13 -11
  64. package/src/cli/build.ts +3 -3
  65. package/src/cli/init/index.ts +20 -11
  66. package/src/cli/init/templates.ts +1 -1
  67. package/src/cli/init/utils.test.ts +11 -20
  68. package/src/cli/init/utils.ts +5 -0
  69. package/src/cli/init/walkthrough.test.ts +92 -11
  70. package/src/cli/init/walkthrough.ts +71 -16
  71. package/src/cli/monitor.ts +1 -1
  72. package/src/cli.ts +4 -2
  73. package/src/fixtures/data/create-pod.json +1 -1
  74. package/src/fixtures/data/delete-pod.json +1 -1
  75. package/src/lib/adjudicators.test.ts +1232 -0
  76. package/src/lib/adjudicators.ts +235 -0
  77. package/src/lib/assets/index.ts +1 -1
  78. package/src/lib/assets/loader.ts +1 -0
  79. package/src/lib/assets/webhooks.ts +1 -1
  80. package/src/lib/capability.test.ts +655 -0
  81. package/src/lib/capability.ts +112 -11
  82. package/src/lib/controller/index.ts +7 -4
  83. package/src/lib/controller/store.test.ts +131 -0
  84. package/src/lib/controller/store.ts +43 -5
  85. package/src/lib/filter.test.ts +279 -9
  86. package/src/lib/filter.ts +46 -98
  87. package/src/lib/finalizer.test.ts +236 -0
  88. package/src/lib/finalizer.ts +63 -0
  89. package/src/lib/helpers.test.ts +359 -65
  90. package/src/lib/helpers.ts +141 -95
  91. package/src/lib/k8s.ts +4 -0
  92. package/src/lib/module.ts +3 -3
  93. package/src/lib/mutate-processor.ts +5 -4
  94. package/src/lib/mutate-request.test.ts +1 -2
  95. package/src/lib/mutate-request.ts +1 -3
  96. package/src/lib/queue.test.ts +138 -44
  97. package/src/lib/queue.ts +48 -13
  98. package/src/lib/schedule.ts +1 -1
  99. package/src/lib/storage.ts +5 -6
  100. package/src/lib/types.ts +154 -5
  101. package/src/lib/validate-processor.ts +5 -2
  102. package/src/lib/validate-request.test.ts +1 -4
  103. package/src/lib/validate-request.ts +1 -1
  104. package/src/lib/watch-processor.test.ts +89 -124
  105. package/src/lib/watch-processor.ts +52 -35
  106. package/src/sdk/sdk.test.ts +46 -13
  107. package/src/sdk/sdk.ts +15 -6
@@ -6,6 +6,39 @@ import { K8s, KubernetesObject, kind } from "kubernetes-fluent-client";
6
6
  import Log from "./logger";
7
7
  import { Binding, CapabilityExport } from "./types";
8
8
  import { sanitizeResourceName } from "../sdk/sdk";
9
+ import {
10
+ carriedAnnotations,
11
+ carriedLabels,
12
+ carriedName,
13
+ carriedNamespace,
14
+ carriesIgnoredNamespace,
15
+ definedAnnotations,
16
+ definedLabels,
17
+ definedName,
18
+ definedNameRegex,
19
+ definedNamespaces,
20
+ definedNamespaceRegexes,
21
+ misboundNamespace,
22
+ mismatchedAnnotations,
23
+ mismatchedDeletionTimestamp,
24
+ mismatchedLabels,
25
+ mismatchedName,
26
+ mismatchedNameRegex,
27
+ mismatchedNamespace,
28
+ mismatchedNamespaceRegex,
29
+ unbindableNamespaces,
30
+ uncarryableNamespace,
31
+ } from "./adjudicators";
32
+
33
+ export function matchesRegex(pattern: string, testString: string): boolean {
34
+ // edge-case
35
+ if (!pattern) {
36
+ return false;
37
+ }
38
+
39
+ const regex = new RegExp(pattern);
40
+ return regex.test(testString);
41
+ }
9
42
 
10
43
  export class ValidationError extends Error {}
11
44
 
@@ -35,36 +68,6 @@ type RBACMap = {
35
68
  };
36
69
  };
37
70
 
38
- // check for overlap with labels and annotations between bindings and kubernetes objects
39
- export function checkOverlap(bindingFilters: Record<string, string>, objectFilters: Record<string, string>): boolean {
40
- // True if labels/annotations are empty
41
- if (Object.keys(bindingFilters).length === 0) {
42
- return true;
43
- }
44
-
45
- let matchCount = 0;
46
-
47
- for (const key in bindingFilters) {
48
- // object must have label/annotation
49
- if (Object.prototype.hasOwnProperty.call(objectFilters, key)) {
50
- const val1 = bindingFilters[key];
51
- const val2 = objectFilters[key];
52
-
53
- // If bindingFilter has empty value for this key, only need to ensure objectFilter has this key
54
- if (val1 === "" && key in objectFilters) {
55
- matchCount++;
56
- }
57
- // If bindingFilter has a value, it must match the value in objectFilter
58
- else if (val1 !== "" && val1 === val2) {
59
- matchCount++;
60
- }
61
- }
62
- }
63
-
64
- // For single-key objects in bindingFilter or matching all keys in multiple-keys scenario
65
- return matchCount === Object.keys(bindingFilters).length;
66
- }
67
-
68
71
  /**
69
72
  * Decide to run callback after the event comes back from API Server
70
73
  **/
@@ -72,71 +75,72 @@ export function filterNoMatchReason(
72
75
  binding: Partial<Binding>,
73
76
  obj: Partial<KubernetesObject>,
74
77
  capabilityNamespaces: string[],
78
+ ignoredNamespaces?: string[],
75
79
  ): string {
76
- // binding kind is namespace with a InNamespace filter
77
- if (binding.kind && binding.kind.kind === "Namespace" && binding.filters && binding.filters.namespaces.length !== 0) {
78
- return `Ignoring Watch Callback: Cannot use a namespace filter in a namespace object.`;
79
- }
80
-
81
- if (typeof obj === "object" && obj !== null && "metadata" in obj && obj.metadata !== undefined && binding.filters) {
82
- // binding labels and object labels dont match
83
- if (obj.metadata.labels && !checkOverlap(binding.filters.labels, obj.metadata.labels)) {
84
- return `Ignoring Watch Callback: No overlap between binding and object labels. Binding labels ${JSON.stringify(
85
- binding.filters.labels,
86
- )}, Object Labels ${JSON.stringify(obj.metadata.labels)}.`;
87
- }
88
-
89
- // binding annotations and object annotations dont match
90
- if (obj.metadata.annotations && !checkOverlap(binding.filters.annotations, obj.metadata.annotations)) {
91
- return `Ignoring Watch Callback: No overlap between binding and object annotations. Binding annotations ${JSON.stringify(
92
- binding.filters.annotations,
93
- )}, Object annotations ${JSON.stringify(obj.metadata.annotations)}.`;
94
- }
95
- }
96
-
97
- // Check object is in the capability namespace
98
- if (
99
- Array.isArray(capabilityNamespaces) &&
100
- capabilityNamespaces.length > 0 &&
101
- obj.metadata &&
102
- obj.metadata.namespace &&
103
- !capabilityNamespaces.includes(obj.metadata.namespace)
104
- ) {
105
- return `Ignoring Watch Callback: Object is not in the capability namespace. Capability namespaces: ${capabilityNamespaces.join(
106
- ", ",
107
- )}, Object namespace: ${obj.metadata.namespace}.`;
108
- }
109
-
110
- // chceck every filter namespace is a capability namespace
111
- if (
112
- Array.isArray(capabilityNamespaces) &&
113
- capabilityNamespaces.length > 0 &&
114
- binding.filters &&
115
- Array.isArray(binding.filters.namespaces) &&
116
- binding.filters.namespaces.length > 0 &&
117
- !binding.filters.namespaces.every(ns => capabilityNamespaces.includes(ns))
118
- ) {
119
- return `Ignoring Watch Callback: Binding namespace is not part of capability namespaces. Capability namespaces: ${capabilityNamespaces.join(
120
- ", ",
121
- )}, Binding namespaces: ${binding.filters.namespaces.join(", ")}.`;
122
- }
123
-
124
- // filter namespace is not the same of object namespace
125
- if (
126
- binding.filters &&
127
- Array.isArray(binding.filters.namespaces) &&
128
- binding.filters.namespaces.length > 0 &&
129
- obj.metadata &&
130
- obj.metadata.namespace &&
131
- !binding.filters.namespaces.includes(obj.metadata.namespace)
132
- ) {
133
- return `Ignoring Watch Callback: Binding namespace and object namespace are not the same. Binding namespaces: ${binding.filters.namespaces.join(
134
- ", ",
135
- )}, Object namespace: ${obj.metadata.namespace}.`;
136
- }
137
-
138
- // no problems
139
- return "";
80
+ const prefix = "Ignoring Watch Callback:";
81
+
82
+ // prettier-ignore
83
+ return (
84
+ mismatchedDeletionTimestamp(binding, obj) ?
85
+ `${prefix} Binding defines deletionTimestamp but Object does not carry it.` :
86
+
87
+ mismatchedName(binding, obj) ?
88
+ `${prefix} Binding defines name '${definedName(binding)}' but Object carries '${carriedName(obj)}'.` :
89
+
90
+ misboundNamespace(binding) ?
91
+ `${prefix} Cannot use namespace filter on a namespace object.` :
92
+
93
+ mismatchedLabels(binding, obj) ?
94
+ (
95
+ `${prefix} Binding defines labels '${JSON.stringify(definedLabels(binding))}' ` +
96
+ `but Object carries '${JSON.stringify(carriedLabels(obj))}'.`
97
+ ) :
98
+
99
+ mismatchedAnnotations(binding, obj) ?
100
+ (
101
+ `${prefix} Binding defines annotations '${JSON.stringify(definedAnnotations(binding))}' ` +
102
+ `but Object carries '${JSON.stringify(carriedAnnotations(obj))}'.`
103
+ ) :
104
+
105
+ uncarryableNamespace(capabilityNamespaces, obj) ?
106
+ (
107
+ `${prefix} Object carries namespace '${carriedNamespace(obj)}' ` +
108
+ `but namespaces allowed by Capability are '${JSON.stringify(capabilityNamespaces)}'.`
109
+ ) :
110
+
111
+ unbindableNamespaces(capabilityNamespaces, binding) ?
112
+ (
113
+ `${prefix} Binding defines namespaces ${JSON.stringify(definedNamespaces(binding))} ` +
114
+ `but namespaces allowed by Capability are '${JSON.stringify(capabilityNamespaces)}'.`
115
+ ) :
116
+
117
+ mismatchedNamespace(binding, obj) ?
118
+ (
119
+ `${prefix} Binding defines namespaces '${JSON.stringify(definedNamespaces(binding))}' ` +
120
+ `but Object carries '${carriedNamespace(obj)}'.`
121
+ ) :
122
+
123
+ mismatchedNamespaceRegex(binding, obj) ?
124
+ (
125
+ `${prefix} Binding defines namespace regexes ` +
126
+ `'${JSON.stringify(definedNamespaceRegexes(binding))}' ` +
127
+ `but Object carries '${carriedNamespace(obj)}'.`
128
+ ) :
129
+
130
+ mismatchedNameRegex(binding, obj) ?
131
+ (
132
+ `${prefix} Binding defines name regex '${definedNameRegex(binding)}' ` +
133
+ `but Object carries '${carriedName(obj)}'.`
134
+ ) :
135
+
136
+ carriesIgnoredNamespace(ignoredNamespaces, obj) ?
137
+ (
138
+ `${prefix} Object carries namespace '${carriedNamespace(obj)}' ` +
139
+ `but ignored namespaces include '${JSON.stringify(ignoredNamespaces)}'.`
140
+ ) :
141
+
142
+ ""
143
+ );
140
144
  }
141
145
 
142
146
  export function addVerbIfNotExists(verbs: string[], verb: string) {
@@ -239,7 +243,8 @@ export function generateWatchNamespaceError(
239
243
  // namespaceComplianceValidator ensures that capability bindinds respect ignored and capability namespaces
240
244
  export function namespaceComplianceValidator(capability: CapabilityExport, ignoredNamespaces?: string[]) {
241
245
  const { namespaces: capabilityNamespaces, bindings, name } = capability;
242
- const bindingNamespaces = bindings.flatMap(binding => binding.filters.namespaces);
246
+ const bindingNamespaces = bindings.flatMap((binding: Binding) => binding.filters.namespaces);
247
+ const bindingRegexNamespaces = bindings.flatMap((binding: Binding) => binding.filters.regexNamespaces || []);
243
248
 
244
249
  const namespaceError = generateWatchNamespaceError(
245
250
  ignoredNamespaces ? ignoredNamespaces : [],
@@ -251,6 +256,47 @@ export function namespaceComplianceValidator(capability: CapabilityExport, ignor
251
256
  `Error in ${name} capability. A binding violates namespace rules. Please check ignoredNamespaces and capability namespaces: ${namespaceError}`,
252
257
  );
253
258
  }
259
+
260
+ // Ensure that each regexNamespace matches a capabilityNamespace
261
+
262
+ if (
263
+ bindingRegexNamespaces &&
264
+ bindingRegexNamespaces.length > 0 &&
265
+ capabilityNamespaces &&
266
+ capabilityNamespaces.length > 0
267
+ ) {
268
+ for (const regexNamespace of bindingRegexNamespaces) {
269
+ let matches = false;
270
+ for (const capabilityNamespace of capabilityNamespaces) {
271
+ if (regexNamespace !== "" && matchesRegex(regexNamespace, capabilityNamespace)) {
272
+ matches = true;
273
+ break;
274
+ }
275
+ }
276
+ if (!matches) {
277
+ throw new Error(
278
+ `Ignoring Watch Callback: Object namespace does not match any capability namespace with regex ${regexNamespace}.`,
279
+ );
280
+ }
281
+ }
282
+ }
283
+ // ensure regexNamespaces do not match ignored ns
284
+ if (
285
+ bindingRegexNamespaces &&
286
+ bindingRegexNamespaces.length > 0 &&
287
+ ignoredNamespaces &&
288
+ ignoredNamespaces.length > 0
289
+ ) {
290
+ for (const regexNamespace of bindingRegexNamespaces) {
291
+ for (const ignoredNS of ignoredNamespaces) {
292
+ if (matchesRegex(regexNamespace, ignoredNS)) {
293
+ throw new Error(
294
+ `Ignoring Watch Callback: Regex namespace: ${regexNamespace}, is an ignored namespace: ${ignoredNS}.`,
295
+ );
296
+ }
297
+ }
298
+ }
299
+ }
254
300
  }
255
301
 
256
302
  // check to see if all replicas are ready for all deployments in the pepr-system namespace
package/src/lib/k8s.ts CHANGED
@@ -3,6 +3,7 @@
3
3
 
4
4
  import { GenericKind, GroupVersionKind, KubernetesObject, RegisterKind } from "kubernetes-fluent-client";
5
5
 
6
+ // DEPRECATED: Use Operation in types.ts instead
6
7
  export enum Operation {
7
8
  CREATE = "CREATE",
8
9
  UPDATE = "UPDATE",
@@ -27,6 +28,7 @@ export const peprStoreGVK = {
27
28
 
28
29
  RegisterKind(PeprStore, peprStoreGVK);
29
30
 
31
+ // DEPRECATED: Use Operation in types.ts instead
30
32
  /**
31
33
  * GroupVersionResource unambiguously identifies a resource. It doesn't anonymously include GroupVersion
32
34
  * to avoid automatic coercion. It doesn't use a GroupVersion to avoid custom marshalling
@@ -37,6 +39,8 @@ export interface GroupVersionResource {
37
39
  readonly resource: string;
38
40
  }
39
41
 
42
+ // DEPRECATED: Use Operation in types.ts instead
43
+
40
44
  /**
41
45
  * A Kubernetes admission request to be processed by a capability.
42
46
  */
package/src/lib/module.ts CHANGED
@@ -4,8 +4,8 @@ import { clone } from "ramda";
4
4
  import { Capability } from "./capability";
5
5
  import { Controller } from "./controller";
6
6
  import { ValidateError } from "./errors";
7
- import { AdmissionRequest, MutateResponse, ValidateResponse, WebhookIgnore } from "./k8s";
8
- import { CapabilityExport } from "./types";
7
+ import { MutateResponse, ValidateResponse, WebhookIgnore } from "./k8s";
8
+ import { CapabilityExport, AdmissionRequest } from "./types";
9
9
  import { setupWatch } from "./watch-processor";
10
10
  import { Log } from "../lib";
11
11
 
@@ -108,7 +108,7 @@ export class PeprModule {
108
108
  // Wait for the controller to be ready before setting up watches
109
109
  if (isWatchMode() || isDevMode()) {
110
110
  try {
111
- setupWatch(capabilities);
111
+ setupWatch(capabilities, pepr?.alwaysIgnore?.namespaces);
112
112
  } catch (e) {
113
113
  Log.error(e, "Error setting up watch");
114
114
  process.exit(1);
@@ -7,7 +7,8 @@ import { kind } from "kubernetes-fluent-client";
7
7
  import { Capability } from "./capability";
8
8
  import { Errors } from "./errors";
9
9
  import { shouldSkipRequest } from "./filter";
10
- import { MutateResponse, AdmissionRequest } from "./k8s";
10
+ import { MutateResponse } from "./k8s";
11
+ import { AdmissionRequest } from "./types";
11
12
  import Log from "./logger";
12
13
  import { ModuleConfig } from "./module";
13
14
  import { PeprMutateRequest } from "./mutate-request";
@@ -42,7 +43,6 @@ export async function mutateProcessor(
42
43
 
43
44
  for (const { name, bindings, namespaces } of capabilities) {
44
45
  const actionMetadata = { ...reqMetadata, name };
45
-
46
46
  for (const action of bindings) {
47
47
  // Skip this action if it's not a mutate action
48
48
  if (!action.mutateCallback) {
@@ -50,13 +50,12 @@ export async function mutateProcessor(
50
50
  }
51
51
 
52
52
  // Continue to the next action without doing anything if this one should be skipped
53
- if (shouldSkipRequest(action, req, namespaces)) {
53
+ if (shouldSkipRequest(action, req, namespaces, config?.alwaysIgnore?.namespaces)) {
54
54
  continue;
55
55
  }
56
56
 
57
57
  const label = action.mutateCallback.name;
58
58
  Log.info(actionMetadata, `Processing mutation action (${label})`);
59
-
60
59
  matchedAction = true;
61
60
 
62
61
  // Add annotations to the request to indicate that the capability started processing
@@ -79,6 +78,7 @@ export async function mutateProcessor(
79
78
  // Run the action
80
79
  await action.mutateCallback(wrapped);
81
80
 
81
+ // Log on success
82
82
  Log.info(actionMetadata, `Mutation action succeeded (${label})`);
83
83
 
84
84
  // Add annotations to the request to indicate that the capability succeeded
@@ -99,6 +99,7 @@ export async function mutateProcessor(
99
99
  errorMessage = "An error occurred with the mutate action.";
100
100
  }
101
101
 
102
+ // Log on failure
102
103
  Log.error(actionMetadata, `Action failed: ${errorMessage}`);
103
104
  response.warnings.push(`Action failed: ${errorMessage}`);
104
105
 
@@ -3,8 +3,7 @@
3
3
 
4
4
  import { beforeEach, describe, expect, it } from "@jest/globals";
5
5
  import { KubernetesObject } from "kubernetes-fluent-client";
6
-
7
- import { Operation, AdmissionRequest } from "./k8s";
6
+ import { Operation, AdmissionRequest } from "./types";
8
7
  import { PeprMutateRequest } from "./mutate-request";
9
8
 
10
9
  describe("PeprMutateRequest", () => {
@@ -3,9 +3,7 @@
3
3
 
4
4
  import { KubernetesObject } from "kubernetes-fluent-client";
5
5
  import { clone, mergeDeepRight } from "ramda";
6
-
7
- import { AdmissionRequest, Operation } from "./k8s";
8
- import { DeepPartial } from "./types";
6
+ import { Operation, AdmissionRequest, DeepPartial } from "./types";
9
7
 
10
8
  /**
11
9
  * The RequestWrapper class provides methods to modify Kubernetes objects in the context
@@ -1,58 +1,152 @@
1
- import { beforeEach, describe, expect, jest, test } from "@jest/globals";
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
- let queue: Queue<KubernetesObject>;
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
- beforeEach(() => {
10
- queue = new Queue();
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
- test("enqueue should add a pod to the queue and return a promise", async () => {
14
- const pod = {
15
- metadata: { name: "test-pod", namespace: "test-pod" },
16
- };
17
- const promise = queue.enqueue(pod, WatchPhase.Added);
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
- await promise;
87
+
88
+ await expect(promise).resolves.not.toThrow();
20
89
  });
21
90
 
22
- test("dequeue should process pods in FIFO order", async () => {
23
- const mockPod = {
24
- metadata: { name: "test-pod", namespace: "test-namespace" },
25
- };
26
- const mockPod2 = {
27
- metadata: { name: "test-pod-2", namespace: "test-namespace-2" },
28
- };
29
-
30
- // Enqueue two packages
31
- const promise1 = queue.enqueue(mockPod, WatchPhase.Added);
32
- const promise2 = queue.enqueue(mockPod2, WatchPhase.Modified);
33
-
34
- // Wait for both promises to resolve
35
- await promise1;
36
- await promise2;
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
- test("dequeue should handle errors in pod processing", async () => {
40
- const mockPod = {
41
- metadata: { name: "test-pod", namespace: "test-namespace" },
42
- };
43
- const error = new Error("reconciliation failed");
44
- jest.spyOn(queue, "setReconcile").mockRejectedValueOnce(error as never);
45
-
46
- try {
47
- await queue.enqueue(mockPod, WatchPhase.Added);
48
- } catch (e) {
49
- expect(e).toBe(error);
50
- }
51
-
52
- // Ensure that the queue is ready to process the next pod
53
- const mockPod2 = {
54
- metadata: { name: "test-pod-2", namespace: "test-namespace-2" },
55
- };
56
- await queue.enqueue(mockPod2, WatchPhase.Modified);
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
  });