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