pepr 0.36.0 → 0.37.1

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 (99) hide show
  1. package/dist/cli/init/index.d.ts.map +1 -1
  2. package/dist/cli/init/templates.d.ts +4 -3
  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/types.d.ts +3 -0
  8. package/dist/cli/types.d.ts.map +1 -0
  9. package/dist/cli.js +253 -31
  10. package/dist/controller.js +138 -1
  11. package/dist/lib/adjudicators.d.ts +63 -0
  12. package/dist/lib/adjudicators.d.ts.map +1 -0
  13. package/dist/lib/adjudicators.test.d.ts +2 -0
  14. package/dist/lib/adjudicators.test.d.ts.map +1 -0
  15. package/dist/lib/assets/loader.d.ts.map +1 -1
  16. package/dist/lib/assets/pods.d.ts +1 -0
  17. package/dist/lib/assets/pods.d.ts.map +1 -1
  18. package/dist/lib/capability.d.ts +1 -0
  19. package/dist/lib/capability.d.ts.map +1 -1
  20. package/dist/lib/capability.test.d.ts +2 -0
  21. package/dist/lib/capability.test.d.ts.map +1 -0
  22. package/dist/lib/controller/index.d.ts.map +1 -1
  23. package/dist/lib/controller/store.d.ts +4 -0
  24. package/dist/lib/controller/store.d.ts.map +1 -1
  25. package/dist/lib/controller/store.test.d.ts +2 -0
  26. package/dist/lib/controller/store.test.d.ts.map +1 -0
  27. package/dist/lib/filter.d.ts +2 -3
  28. package/dist/lib/filter.d.ts.map +1 -1
  29. package/dist/lib/filter.test.d.ts +2 -1
  30. package/dist/lib/filter.test.d.ts.map +1 -1
  31. package/dist/lib/finalizer.d.ts +6 -0
  32. package/dist/lib/finalizer.d.ts.map +1 -0
  33. package/dist/lib/finalizer.test.d.ts +2 -0
  34. package/dist/lib/finalizer.test.d.ts.map +1 -0
  35. package/dist/lib/helpers.d.ts +2 -2
  36. package/dist/lib/helpers.d.ts.map +1 -1
  37. package/dist/lib/helpers.test.d.ts +1 -1
  38. package/dist/lib/helpers.test.d.ts.map +1 -1
  39. package/dist/lib/k8s.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/schedule.d.ts +1 -2
  47. package/dist/lib/schedule.d.ts.map +1 -1
  48. package/dist/lib/storage.d.ts.map +1 -1
  49. package/dist/lib/types.d.ts +113 -6
  50. package/dist/lib/types.d.ts.map +1 -1
  51. package/dist/lib/validate-processor.d.ts +4 -2
  52. package/dist/lib/validate-processor.d.ts.map +1 -1
  53. package/dist/lib/validate-request.d.ts +1 -1
  54. package/dist/lib/validate-request.d.ts.map +1 -1
  55. package/dist/lib/watch-processor.d.ts +1 -1
  56. package/dist/lib/watch-processor.d.ts.map +1 -1
  57. package/dist/lib.js +383 -204
  58. package/dist/lib.js.map +4 -4
  59. package/package.json +13 -12
  60. package/src/cli/build.ts +3 -3
  61. package/src/cli/init/index.ts +20 -11
  62. package/src/cli/init/templates.ts +1 -1
  63. package/src/cli/init/utils.test.ts +11 -20
  64. package/src/cli/init/utils.ts +5 -0
  65. package/src/cli/init/walkthrough.test.ts +92 -11
  66. package/src/cli/init/walkthrough.ts +71 -16
  67. package/src/cli/monitor.ts +1 -1
  68. package/src/cli/types.ts +3 -0
  69. package/src/cli.ts +4 -2
  70. package/src/fixtures/data/create-pod.json +1 -1
  71. package/src/fixtures/data/delete-pod.json +1 -1
  72. package/src/lib/adjudicators.test.ts +1232 -0
  73. package/src/lib/adjudicators.ts +235 -0
  74. package/src/lib/assets/index.ts +1 -1
  75. package/src/lib/assets/loader.ts +1 -0
  76. package/src/lib/assets/webhooks.ts +1 -1
  77. package/src/lib/capability.test.ts +655 -0
  78. package/src/lib/capability.ts +104 -11
  79. package/src/lib/controller/index.ts +7 -4
  80. package/src/lib/controller/store.test.ts +131 -0
  81. package/src/lib/controller/store.ts +43 -5
  82. package/src/lib/filter.test.ts +194 -8
  83. package/src/lib/filter.ts +46 -107
  84. package/src/lib/finalizer.test.ts +236 -0
  85. package/src/lib/finalizer.ts +63 -0
  86. package/src/lib/helpers.test.ts +329 -69
  87. package/src/lib/helpers.ts +141 -100
  88. package/src/lib/k8s.ts +4 -0
  89. package/src/lib/module.ts +3 -3
  90. package/src/lib/mutate-processor.ts +5 -4
  91. package/src/lib/mutate-request.test.ts +1 -2
  92. package/src/lib/mutate-request.ts +1 -3
  93. package/src/lib/schedule.ts +1 -1
  94. package/src/lib/storage.ts +5 -6
  95. package/src/lib/types.ts +148 -5
  96. package/src/lib/validate-processor.ts +5 -2
  97. package/src/lib/validate-request.test.ts +1 -4
  98. package/src/lib/validate-request.ts +1 -1
  99. package/src/lib/watch-processor.ts +19 -5
@@ -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,76 +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 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
-
81
- // binding kind is namespace with a InNamespace filter
82
- if (binding.kind && binding.kind.kind === "Namespace" && binding.filters && binding.filters.namespaces.length !== 0) {
83
- return `Ignoring Watch Callback: Cannot use a namespace filter in a namespace object.`;
84
- }
85
-
86
- if (typeof obj === "object" && obj !== null && "metadata" in obj && obj.metadata !== undefined && binding.filters) {
87
- // binding labels and object labels dont match
88
- if (obj.metadata.labels && !checkOverlap(binding.filters.labels, obj.metadata.labels)) {
89
- return `Ignoring Watch Callback: No overlap between binding and object labels. Binding labels ${JSON.stringify(
90
- binding.filters.labels,
91
- )}, Object Labels ${JSON.stringify(obj.metadata.labels)}.`;
92
- }
93
-
94
- // binding annotations and object annotations dont match
95
- if (obj.metadata.annotations && !checkOverlap(binding.filters.annotations, obj.metadata.annotations)) {
96
- return `Ignoring Watch Callback: No overlap between binding and object annotations. Binding annotations ${JSON.stringify(
97
- binding.filters.annotations,
98
- )}, Object annotations ${JSON.stringify(obj.metadata.annotations)}.`;
99
- }
100
- }
101
-
102
- // Check object is in the capability namespace
103
- if (
104
- Array.isArray(capabilityNamespaces) &&
105
- capabilityNamespaces.length > 0 &&
106
- obj.metadata &&
107
- obj.metadata.namespace &&
108
- !capabilityNamespaces.includes(obj.metadata.namespace)
109
- ) {
110
- return `Ignoring Watch Callback: Object is not in the capability namespace. Capability namespaces: ${capabilityNamespaces.join(
111
- ", ",
112
- )}, Object namespace: ${obj.metadata.namespace}.`;
113
- }
114
-
115
- // chceck every filter namespace is a capability namespace
116
- if (
117
- Array.isArray(capabilityNamespaces) &&
118
- capabilityNamespaces.length > 0 &&
119
- binding.filters &&
120
- Array.isArray(binding.filters.namespaces) &&
121
- binding.filters.namespaces.length > 0 &&
122
- !binding.filters.namespaces.every(ns => capabilityNamespaces.includes(ns))
123
- ) {
124
- return `Ignoring Watch Callback: Binding namespace is not part of capability namespaces. Capability namespaces: ${capabilityNamespaces.join(
125
- ", ",
126
- )}, Binding namespaces: ${binding.filters.namespaces.join(", ")}.`;
127
- }
128
-
129
- // filter namespace is not the same of object namespace
130
- if (
131
- binding.filters &&
132
- Array.isArray(binding.filters.namespaces) &&
133
- binding.filters.namespaces.length > 0 &&
134
- obj.metadata &&
135
- obj.metadata.namespace &&
136
- !binding.filters.namespaces.includes(obj.metadata.namespace)
137
- ) {
138
- return `Ignoring Watch Callback: Binding namespace and object namespace are not the same. Binding namespaces: ${binding.filters.namespaces.join(
139
- ", ",
140
- )}, Object namespace: ${obj.metadata.namespace}.`;
141
- }
142
-
143
- // no problems
144
- 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
+ );
145
144
  }
146
145
 
147
146
  export function addVerbIfNotExists(verbs: string[], verb: string) {
@@ -244,7 +243,8 @@ export function generateWatchNamespaceError(
244
243
  // namespaceComplianceValidator ensures that capability bindinds respect ignored and capability namespaces
245
244
  export function namespaceComplianceValidator(capability: CapabilityExport, ignoredNamespaces?: string[]) {
246
245
  const { namespaces: capabilityNamespaces, bindings, name } = capability;
247
- 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 || []);
248
248
 
249
249
  const namespaceError = generateWatchNamespaceError(
250
250
  ignoredNamespaces ? ignoredNamespaces : [],
@@ -256,6 +256,47 @@ export function namespaceComplianceValidator(capability: CapabilityExport, ignor
256
256
  `Error in ${name} capability. A binding violates namespace rules. Please check ignoredNamespaces and capability namespaces: ${namespaceError}`,
257
257
  );
258
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
+ }
259
300
  }
260
301
 
261
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
@@ -3,7 +3,7 @@
3
3
 
4
4
  import { PeprStore } from "./storage";
5
5
 
6
- type Unit = "seconds" | "second" | "minute" | "minutes" | "hours" | "hour";
6
+ export type Unit = "seconds" | "second" | "minute" | "minutes" | "hours" | "hour";
7
7
 
8
8
  export interface Schedule {
9
9
  /**
@@ -2,7 +2,6 @@
2
2
  // SPDX-FileCopyrightText: 2023-Present The Pepr Authors
3
3
 
4
4
  import { clone } from "ramda";
5
- import Log from "./logger";
6
5
  import pointer from "json-pointer";
7
6
  export type DataOp = "add" | "remove";
8
7
  export type DataStore = Record<string, string>;
@@ -86,7 +85,6 @@ export class Storage implements PeprStore {
86
85
  };
87
86
 
88
87
  receive = (data: DataStore) => {
89
- Log.debug(data, `Pepr store data received`);
90
88
  this.#store = data || {};
91
89
 
92
90
  this.#onReady();
@@ -107,10 +105,11 @@ export class Storage implements PeprStore {
107
105
  };
108
106
 
109
107
  clear = () => {
110
- this.#dispatchUpdate(
111
- "remove",
112
- Object.keys(this.#store).map(key => pointer.escape(key)),
113
- );
108
+ Object.keys(this.#store).length > 0 &&
109
+ this.#dispatchUpdate(
110
+ "remove",
111
+ Object.keys(this.#store).map(key => pointer.escape(key)),
112
+ );
114
113
  };
115
114
 
116
115
  removeItem = (key: string) => {
package/src/lib/types.ts CHANGED
@@ -2,11 +2,20 @@
2
2
  // SPDX-FileCopyrightText: 2023-Present The Pepr Authors
3
3
 
4
4
  import { GenericClass, GroupVersionKind, KubernetesObject } from "kubernetes-fluent-client";
5
- import { WatchAction } from "kubernetes-fluent-client/dist/fluent/types";
5
+
6
+ import { WatchPhase } from "kubernetes-fluent-client/dist/fluent/types";
6
7
 
7
8
  import { PeprMutateRequest } from "./mutate-request";
8
9
  import { PeprValidateRequest } from "./validate-request";
9
10
 
11
+ import { Logger } from "pino";
12
+
13
+ export enum Operation {
14
+ CREATE = "CREATE",
15
+ UPDATE = "UPDATE",
16
+ DELETE = "DELETE",
17
+ CONNECT = "CONNECT",
18
+ }
10
19
  /**
11
20
  * Specifically for deploying images with a private registry
12
21
  */
@@ -80,25 +89,33 @@ export type WhenSelector<T extends GenericClass> = {
80
89
  /** Register an action to be executed when a Kubernetes resource is deleted. */
81
90
  IsDeleted: () => BindingAll<T>;
82
91
  };
83
-
92
+ export interface RegExpFilter {
93
+ obj: RegExp;
94
+ source: string;
95
+ }
84
96
  export type Binding = {
85
97
  event: Event;
86
98
  isMutate?: boolean;
87
99
  isValidate?: boolean;
88
100
  isWatch?: boolean;
89
101
  isQueue?: boolean;
102
+ isFinalize?: boolean;
90
103
  readonly model: GenericClass;
91
104
  readonly kind: GroupVersionKind;
92
105
  readonly filters: {
93
106
  name: string;
107
+ regexName: string;
94
108
  namespaces: string[];
109
+ regexNamespaces: string[];
95
110
  labels: Record<string, string>;
96
111
  annotations: Record<string, string>;
97
112
  deletionTimestamp: boolean;
98
113
  };
114
+ alias?: string;
99
115
  readonly mutateCallback?: MutateAction<GenericClass, InstanceType<GenericClass>>;
100
116
  readonly validateCallback?: ValidateAction<GenericClass, InstanceType<GenericClass>>;
101
- readonly watchCallback?: WatchAction<GenericClass, InstanceType<GenericClass>>;
117
+ readonly watchCallback?: WatchLogAction<GenericClass, InstanceType<GenericClass>>;
118
+ readonly finalizeCallback?: FinalizeAction<GenericClass, InstanceType<GenericClass>>;
102
119
  };
103
120
 
104
121
  export type BindingFilter<T extends GenericClass> = CommonActionChain<T> & {
@@ -145,11 +162,15 @@ export type BindingFilter<T extends GenericClass> = CommonActionChain<T> & {
145
162
  export type BindingWithName<T extends GenericClass> = BindingFilter<T> & {
146
163
  /** Only apply the action if the resource name matches the specified name. */
147
164
  WithName: (name: string) => BindingFilter<T>;
165
+ /** Only apply the action if the resource name matches the specified regex name. */
166
+ WithNameRegex: (name: RegExp) => BindingFilter<T>;
148
167
  };
149
168
 
150
169
  export type BindingAll<T extends GenericClass> = BindingWithName<T> & {
151
170
  /** Only apply the action if the resource is in one of the specified namespaces.*/
152
171
  InNamespace: (...namespaces: string[]) => BindingWithName<T>;
172
+ /** Only apply the action if the resource is in one of the specified regex namespaces.*/
173
+ InNamespaceRegex: (...namespaces: RegExp[]) => BindingWithName<T>;
153
174
  };
154
175
 
155
176
  export type CommonActionChain<T extends GenericClass> = MutateActionChain<T> & {
@@ -162,6 +183,7 @@ export type CommonActionChain<T extends GenericClass> = MutateActionChain<T> & {
162
183
  * @param action The action to be executed when the Kubernetes resource is processed by the AdmissionController.
163
184
  */
164
185
  Mutate: (action: MutateAction<T, InstanceType<T>>) => MutateActionChain<T>;
186
+ Alias: (alias: string) => BindingFilter<T>;
165
187
  };
166
188
 
167
189
  export type ValidateActionChain<T extends GenericClass> = {
@@ -176,7 +198,8 @@ export type ValidateActionChain<T extends GenericClass> = {
176
198
  * @param action
177
199
  * @returns
178
200
  */
179
- Watch: (action: WatchAction<T, InstanceType<T>>) => void;
201
+
202
+ Watch: (action: WatchLogAction<T, InstanceType<T>>) => FinalizeActionChain<T>;
180
203
 
181
204
  /**
182
205
  * Establish a reconcile for the specified resource. The callback function will be executed after the admission controller has
@@ -189,7 +212,8 @@ export type ValidateActionChain<T extends GenericClass> = {
189
212
  * @param action
190
213
  * @returns
191
214
  */
192
- Reconcile: (action: WatchAction<T, InstanceType<T>>) => void;
215
+
216
+ Reconcile: (action: WatchLogAction<T, InstanceType<T>>) => FinalizeActionChain<T>;
193
217
  };
194
218
 
195
219
  export type MutateActionChain<T extends GenericClass> = ValidateActionChain<T> & {
@@ -219,14 +243,133 @@ export type MutateActionChain<T extends GenericClass> = ValidateActionChain<T> &
219
243
 
220
244
  export type MutateAction<T extends GenericClass, K extends KubernetesObject = InstanceType<T>> = (
221
245
  req: PeprMutateRequest<K>,
246
+ logger?: Logger,
222
247
  ) => Promise<void> | void | Promise<PeprMutateRequest<K>> | PeprMutateRequest<K>;
223
248
 
224
249
  export type ValidateAction<T extends GenericClass, K extends KubernetesObject = InstanceType<T>> = (
225
250
  req: PeprValidateRequest<K>,
251
+ logger?: Logger,
226
252
  ) => Promise<ValidateActionResponse> | ValidateActionResponse;
227
253
 
254
+ // Define WatchLogAction by adding an optional logger parameter to the WatchAction
255
+ export type WatchLogAction<T extends GenericClass, K extends KubernetesObject = InstanceType<T>> = (
256
+ update: K,
257
+ phase: WatchPhase,
258
+ logger?: Logger,
259
+ ) => Promise<void> | void;
260
+
228
261
  export type ValidateActionResponse = {
229
262
  allowed: boolean;
230
263
  statusCode?: number;
231
264
  statusMessage?: string;
232
265
  };
266
+
267
+ export type FinalizeAction<T extends GenericClass, K extends KubernetesObject = InstanceType<T>> = (
268
+ update: K,
269
+ logger?: Logger,
270
+ ) => Promise<void> | void;
271
+
272
+ export type FinalizeActionChain<T extends GenericClass> = {
273
+ /**
274
+ * Establish a finalizer for the specified resource. The callback given will be executed by the watch
275
+ * controller after it has received notification of an update adding a deletionTimestamp.
276
+ *
277
+ * **Beta Function**: This method is still in early testing and edge cases may still exist.
278
+ *
279
+ * @since 0.35.0
280
+ *
281
+ * @param action
282
+ * @returns
283
+ */
284
+ Finalize: (action: FinalizeAction<T, InstanceType<T>>) => void;
285
+ };
286
+
287
+ /**
288
+ * A Kubernetes admission request to be processed by a capability.
289
+ */
290
+ export interface AdmissionRequest<T = KubernetesObject> {
291
+ /** UID is an identifier for the individual request/response. */
292
+ readonly uid: string;
293
+
294
+ /** Kind is the fully-qualified type of object being submitted (for example, v1.Pod or autoscaling.v1.Scale) */
295
+ readonly kind: GroupVersionKind;
296
+
297
+ /** Resource is the fully-qualified resource being requested (for example, v1.pods) */
298
+ readonly resource: GroupVersionResource;
299
+
300
+ /** SubResource is the sub-resource being requested, if any (for example, "status" or "scale") */
301
+ readonly subResource?: string;
302
+
303
+ /** RequestKind is the fully-qualified type of the original API request (for example, v1.Pod or autoscaling.v1.Scale). */
304
+ readonly requestKind?: GroupVersionKind;
305
+
306
+ /** RequestResource is the fully-qualified resource of the original API request (for example, v1.pods). */
307
+ readonly requestResource?: GroupVersionResource;
308
+
309
+ /** RequestSubResource is the sub-resource of the original API request, if any (for example, "status" or "scale"). */
310
+ readonly requestSubResource?: string;
311
+
312
+ /**
313
+ * Name is the name of the object as presented in the request. On a CREATE operation, the client may omit name and
314
+ * rely on the server to generate the name. If that is the case, this method will return the empty string.
315
+ */
316
+ readonly name: string;
317
+
318
+ /** Namespace is the namespace associated with the request (if any). */
319
+ readonly namespace?: string;
320
+
321
+ /**
322
+ * Operation is the operation being performed. This may be different than the operation
323
+ * requested. e.g. a patch can result in either a CREATE or UPDATE Operation.
324
+ */
325
+ readonly operation: Operation;
326
+
327
+ /** UserInfo is information about the requesting user */
328
+ readonly userInfo: {
329
+ /** The name that uniquely identifies this user among all active users. */
330
+ username?: string;
331
+
332
+ /**
333
+ * A unique value that identifies this user across time. If this user is deleted
334
+ * and another user by the same name is added, they will have different UIDs.
335
+ */
336
+ uid?: string;
337
+
338
+ /** The names of groups this user is a part of. */
339
+ groups?: string[];
340
+
341
+ /** Any additional information provided by the authenticator. */
342
+ extra?: {
343
+ [key: string]: string[];
344
+ };
345
+ };
346
+
347
+ /** Object is the object from the incoming request prior to default values being applied */
348
+ readonly object: T;
349
+
350
+ /** OldObject is the existing object. Only populated for UPDATE or DELETE requests. */
351
+ readonly oldObject?: T;
352
+
353
+ /** DryRun indicates that modifications will definitely not be persisted for this request. Defaults to false. */
354
+ readonly dryRun?: boolean;
355
+
356
+ /**
357
+ * Options contains the options for the operation being performed.
358
+ * e.g. `meta.k8s.io/v1.DeleteOptions` or `meta.k8s.io/v1.CreateOptions`. This may be
359
+ * different than the options the caller provided. e.g. for a patch request the performed
360
+ * Operation might be a CREATE, in which case the Options will a
361
+ * `meta.k8s.io/v1.CreateOptions` even though the caller provided `meta.k8s.io/v1.PatchOptions`.
362
+ */
363
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
364
+ readonly options?: any;
365
+ }
366
+
367
+ /**
368
+ * GroupVersionResource unambiguously identifies a resource. It doesn't anonymously include GroupVersion
369
+ * to avoid automatic coercion. It doesn't use a GroupVersion to avoid custom marshalling
370
+ */
371
+ export interface GroupVersionResource {
372
+ readonly group: string;
373
+ readonly version: string;
374
+ readonly resource: string;
375
+ }