pepr 0.13.4 → 0.14.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 (105) hide show
  1. package/README.md +23 -4
  2. package/dist/cli.js +375 -204
  3. package/dist/controller.js +1 -1
  4. package/dist/lib/assets/deploy.d.ts.map +1 -1
  5. package/dist/lib/assets/destroy.d.ts +2 -0
  6. package/dist/lib/assets/destroy.d.ts.map +1 -0
  7. package/dist/lib/assets/index.d.ts +6 -5
  8. package/dist/lib/assets/index.d.ts.map +1 -1
  9. package/dist/lib/assets/networking.d.ts +6 -5
  10. package/dist/lib/assets/networking.d.ts.map +1 -1
  11. package/dist/lib/assets/pods.d.ts +84 -4
  12. package/dist/lib/assets/pods.d.ts.map +1 -1
  13. package/dist/lib/assets/rbac.d.ts +6 -4
  14. package/dist/lib/assets/rbac.d.ts.map +1 -1
  15. package/dist/lib/assets/store.d.ts +7 -0
  16. package/dist/lib/assets/store.d.ts.map +1 -0
  17. package/dist/lib/assets/webhooks.d.ts +2 -2
  18. package/dist/lib/assets/webhooks.d.ts.map +1 -1
  19. package/dist/lib/assets/yaml.d.ts.map +1 -1
  20. package/dist/lib/capability.d.ts +21 -4
  21. package/dist/lib/capability.d.ts.map +1 -1
  22. package/dist/lib/controller/index.d.ts +10 -0
  23. package/dist/lib/controller/index.d.ts.map +1 -0
  24. package/dist/lib/controller/store.d.ts +7 -0
  25. package/dist/lib/controller/store.d.ts.map +1 -0
  26. package/dist/lib/filter.d.ts +2 -2
  27. package/dist/lib/filter.d.ts.map +1 -1
  28. package/dist/lib/{k8s/types.d.ts → k8s.d.ts} +14 -25
  29. package/dist/lib/k8s.d.ts.map +1 -0
  30. package/dist/lib/metrics.d.ts +12 -12
  31. package/dist/lib/metrics.d.ts.map +1 -1
  32. package/dist/lib/module.d.ts +25 -4
  33. package/dist/lib/module.d.ts.map +1 -1
  34. package/dist/lib/mutate-processor.d.ts +3 -3
  35. package/dist/lib/mutate-processor.d.ts.map +1 -1
  36. package/dist/lib/mutate-request.d.ts +11 -10
  37. package/dist/lib/mutate-request.d.ts.map +1 -1
  38. package/dist/lib/storage.d.ts +56 -0
  39. package/dist/lib/storage.d.ts.map +1 -0
  40. package/dist/lib/tls.d.ts.map +1 -0
  41. package/dist/lib/types.d.ts +28 -48
  42. package/dist/lib/types.d.ts.map +1 -1
  43. package/dist/lib/validate-processor.d.ts +2 -2
  44. package/dist/lib/validate-processor.d.ts.map +1 -1
  45. package/dist/lib/validate-request.d.ts +9 -8
  46. package/dist/lib/validate-request.d.ts.map +1 -1
  47. package/dist/lib/watch-processor.d.ts +3 -0
  48. package/dist/lib/watch-processor.d.ts.map +1 -0
  49. package/dist/lib.d.ts +3 -7
  50. package/dist/lib.d.ts.map +1 -1
  51. package/dist/lib.js +484 -807
  52. package/dist/lib.js.map +4 -4
  53. package/package.json +13 -17
  54. package/src/lib/assets/deploy.ts +69 -127
  55. package/src/lib/assets/destroy.ts +33 -0
  56. package/src/lib/assets/index.ts +8 -14
  57. package/src/lib/assets/networking.ts +28 -5
  58. package/src/lib/assets/pods.ts +130 -11
  59. package/src/lib/assets/rbac.ts +42 -4
  60. package/src/lib/assets/store.ts +49 -0
  61. package/src/lib/assets/webhooks.ts +2 -2
  62. package/src/lib/assets/yaml.ts +13 -3
  63. package/src/lib/capability.ts +69 -14
  64. package/src/lib/{controller.ts → controller/index.ts} +25 -23
  65. package/src/lib/controller/store.ts +197 -0
  66. package/src/lib/filter.ts +2 -2
  67. package/src/lib/{k8s/types.ts → k8s.ts} +15 -26
  68. package/src/lib/metrics.ts +22 -38
  69. package/src/lib/module.ts +47 -10
  70. package/src/lib/mutate-processor.ts +6 -6
  71. package/src/lib/mutate-request.ts +18 -26
  72. package/src/lib/storage.ts +128 -0
  73. package/src/lib/types.ts +30 -53
  74. package/src/lib/validate-processor.ts +5 -4
  75. package/src/lib/validate-request.ts +15 -19
  76. package/src/lib/watch-processor.ts +55 -0
  77. package/src/lib.ts +4 -8
  78. package/src/templates/capabilities/hello-pepr.ts +54 -5
  79. package/src/templates/package.json +1 -0
  80. package/dist/lib/controller.d.ts +0 -10
  81. package/dist/lib/controller.d.ts.map +0 -1
  82. package/dist/lib/fetch.d.ts +0 -23
  83. package/dist/lib/fetch.d.ts.map +0 -1
  84. package/dist/lib/k8s/index.d.ts +0 -7
  85. package/dist/lib/k8s/index.d.ts.map +0 -1
  86. package/dist/lib/k8s/kinds.d.ts +0 -12
  87. package/dist/lib/k8s/kinds.d.ts.map +0 -1
  88. package/dist/lib/k8s/tls.d.ts.map +0 -1
  89. package/dist/lib/k8s/types.d.ts.map +0 -1
  90. package/dist/lib/k8s/upstream.d.ts +0 -4
  91. package/dist/lib/k8s/upstream.d.ts.map +0 -1
  92. package/jest.config.json +0 -4
  93. package/journey/before.ts +0 -21
  94. package/journey/k8s.ts +0 -100
  95. package/journey/pepr-build.ts +0 -69
  96. package/journey/pepr-deploy.ts +0 -174
  97. package/journey/pepr-dev.ts +0 -155
  98. package/journey/pepr-format.ts +0 -13
  99. package/journey/pepr-init.ts +0 -12
  100. package/src/lib/fetch.ts +0 -76
  101. package/src/lib/k8s/index.ts +0 -14
  102. package/src/lib/k8s/kinds.ts +0 -531
  103. package/src/lib/k8s/upstream.ts +0 -53
  104. /package/dist/lib/{k8s/tls.d.ts → tls.d.ts} +0 -0
  105. /package/src/lib/{k8s/tls.ts → tls.ts} +0 -0
@@ -6,10 +6,10 @@ import {
6
6
  V1LabelSelectorRequirement,
7
7
  V1RuleWithOperations,
8
8
  } from "@kubernetes/client-node";
9
+ import { kind } from "kubernetes-fluent-client";
9
10
  import { concat, equals, uniqWith } from "ramda";
10
11
 
11
12
  import { Assets } from ".";
12
- import { MutatingWebhookConfiguration, ValidatingWebhookConfiguration } from "../k8s/upstream";
13
13
  import { Event } from "../types";
14
14
 
15
15
  const peprIgnoreLabel: V1LabelSelectorRequirement = {
@@ -70,7 +70,7 @@ export async function webhookConfig(
70
70
  assets: Assets,
71
71
  mutateOrValidate: "mutate" | "validate",
72
72
  timeoutSeconds = 10,
73
- ): Promise<MutatingWebhookConfiguration | ValidatingWebhookConfiguration | null> {
73
+ ): Promise<kind.MutatingWebhookConfiguration | kind.ValidatingWebhookConfiguration | null> {
74
74
  const ignore = [peprIgnoreLabel];
75
75
 
76
76
  const { name, tls, config, apiToken, host } = assets;
@@ -6,10 +6,11 @@ import crypto from "crypto";
6
6
  import { promises as fs } from "fs";
7
7
 
8
8
  import { Assets } from ".";
9
- import { apiTokenSecret, service, tlsSecret } from "./networking";
10
- import { deployment, moduleSecret, namespace } from "./pods";
11
- import { clusterRole, clusterRoleBinding, serviceAccount } from "./rbac";
9
+ import { apiTokenSecret, service, tlsSecret, watcherService } from "./networking";
10
+ import { deployment, moduleSecret, namespace, watcher } from "./pods";
11
+ import { clusterRole, clusterRoleBinding, serviceAccount, storeRole, storeRoleBinding } from "./rbac";
12
12
  import { webhookConfig } from "./webhooks";
13
+ import { peprStoreCRD } from "./store";
13
14
 
14
15
  export function zarfYaml({ name, image, config }: Assets, path: string) {
15
16
  const zarfCfg = {
@@ -49,6 +50,7 @@ export async function allYaml(assets: Assets) {
49
50
 
50
51
  const mutateWebhook = await webhookConfig(assets, "mutate");
51
52
  const validateWebhook = await webhookConfig(assets, "validate");
53
+ const watchDeployment = watcher(assets, hash);
52
54
 
53
55
  const resources = [
54
56
  namespace,
@@ -59,7 +61,11 @@ export async function allYaml(assets: Assets) {
59
61
  tlsSecret(name, tls),
60
62
  deployment(assets, hash),
61
63
  service(name),
64
+ watcherService(name),
62
65
  moduleSecret(name, code, hash),
66
+ peprStoreCRD,
67
+ storeRole(name),
68
+ storeRoleBinding(name),
63
69
  ];
64
70
 
65
71
  if (mutateWebhook) {
@@ -70,6 +76,10 @@ export async function allYaml(assets: Assets) {
70
76
  resources.push(validateWebhook);
71
77
  }
72
78
 
79
+ if (watchDeployment) {
80
+ resources.push(watchDeployment);
81
+ }
82
+
73
83
  // Convert the resources to a single YAML string
74
84
  return resources.map(r => dumpYaml(r, { noRefs: true })).join("---\n");
75
85
  }
@@ -1,11 +1,13 @@
1
1
  // SPDX-License-Identifier: Apache-2.0
2
2
  // SPDX-FileCopyrightText: 2023-Present The Pepr Authors
3
3
 
4
+ import { GenericClass, GroupVersionKind, modelToGroupVersionKind } from "kubernetes-fluent-client";
5
+ import { WatchAction } from "kubernetes-fluent-client/dist/fluent/types";
4
6
  import { pickBy } from "ramda";
5
7
 
6
- import { isWatchMode, modelToGroupVersionKind } from "./k8s/index";
7
- import { GroupVersionKind } from "./k8s/types";
8
8
  import Log from "./logger";
9
+ import { isBuildMode, isDevMode, isWatchMode } from "./module";
10
+ import { PeprStore, Storage } from "./storage";
9
11
  import {
10
12
  Binding,
11
13
  BindingFilter,
@@ -13,13 +15,16 @@ import {
13
15
  CapabilityCfg,
14
16
  CapabilityExport,
15
17
  Event,
16
- GenericClass,
17
18
  MutateAction,
18
19
  MutateActionChain,
19
20
  ValidateAction,
21
+ ValidateActionChain,
20
22
  WhenSelector,
21
23
  } from "./types";
22
24
 
25
+ const registerAdmission = isBuildMode() || !isWatchMode();
26
+ const registerWatch = isBuildMode() || isWatchMode() || isDevMode();
27
+
23
28
  /**
24
29
  * A capability is a unit of functionality that can be registered with the Pepr runtime.
25
30
  */
@@ -28,6 +33,24 @@ export class Capability implements CapabilityExport {
28
33
  #description: string;
29
34
  #namespaces?: string[] | undefined;
30
35
  #bindings: Binding[] = [];
36
+ #store = new Storage();
37
+ #registered = false;
38
+
39
+ /**
40
+ * Store is a key-value data store that can be used to persist data that should be shared
41
+ * between requests. Each capability has its own store, and the data is persisted in Kubernetes
42
+ * in the `pepr-system` namespace.
43
+ *
44
+ * Note: You should only access the store from within an action.
45
+ */
46
+ Store: PeprStore = {
47
+ clear: this.#store.clear,
48
+ getItem: this.#store.getItem,
49
+ removeItem: this.#store.removeItem,
50
+ setItem: this.#store.setItem,
51
+ subscribe: this.#store.subscribe,
52
+ onReady: this.#store.onReady,
53
+ };
31
54
 
32
55
  get bindings() {
33
56
  return this.#bindings;
@@ -50,15 +73,32 @@ export class Capability implements CapabilityExport {
50
73
  this.#description = cfg.description;
51
74
  this.#namespaces = cfg.namespaces;
52
75
 
53
- // Bind When() to this instance
54
- this.When = this.When.bind(this);
55
-
56
76
  Log.info(`Capability ${this.#name} registered`);
57
77
  Log.debug(cfg);
58
78
  }
59
79
 
60
80
  /**
61
- * The When method is used to register a capability action to be executed when a Kubernetes resource is
81
+ * Register the store with the capability. This is called automatically by the Pepr controller.
82
+ *
83
+ * @param store
84
+ */
85
+ registerStore = () => {
86
+ Log.info(`Registering store for ${this.#name}`);
87
+
88
+ if (this.#registered) {
89
+ throw new Error(`Store already registered for ${this.#name}`);
90
+ }
91
+
92
+ this.#registered = true;
93
+
94
+ // Pass back any ready callback to the controller
95
+ return {
96
+ store: this.#store,
97
+ };
98
+ };
99
+
100
+ /**
101
+ * The When method is used to register a action to be executed when a Kubernetes resource is
62
102
  * processed by Pepr. The action will be executed if the resource matches the specified kind and any
63
103
  * filters that are applied.
64
104
  *
@@ -66,7 +106,7 @@ export class Capability implements CapabilityExport {
66
106
  * @param kind if using a custom KubernetesObject not available in `a.*`, specify the GroupVersionKind
67
107
  * @returns
68
108
  */
69
- When<T extends GenericClass>(model: T, kind?: GroupVersionKind): WhenSelector<T> {
109
+ When = <T extends GenericClass>(model: T, kind?: GroupVersionKind): WhenSelector<T> => {
70
110
  const matchedKind = modelToGroupVersionKind(model.name);
71
111
 
72
112
  // If the kind is not specified and the model is not a KubernetesObject, throw an error
@@ -75,6 +115,7 @@ export class Capability implements CapabilityExport {
75
115
  }
76
116
 
77
117
  const binding: Binding = {
118
+ model,
78
119
  // If the kind is not specified, use the matched kind from the model
79
120
  kind: kind || matchedKind,
80
121
  event: Event.Any,
@@ -88,7 +129,7 @@ export class Capability implements CapabilityExport {
88
129
 
89
130
  const bindings = this.#bindings;
90
131
  const prefix = `${this.#name}: ${model.name}`;
91
- const commonChain = { WithLabel, WithAnnotation, Mutate, Validate };
132
+ const commonChain = { WithLabel, WithAnnotation, Mutate, Validate, Watch };
92
133
  const isNotEmpty = (value: object) => Object.keys(value).length > 0;
93
134
  const log = (message: string, cbString: string) => {
94
135
  const filteredObj = pickBy(isNotEmpty, binding.filters);
@@ -98,8 +139,8 @@ export class Capability implements CapabilityExport {
98
139
  Log.debug(cbString, prefix);
99
140
  };
100
141
 
101
- function Validate(validateCallback: ValidateAction<T>): void {
102
- if (!isWatchMode) {
142
+ function Validate(validateCallback: ValidateAction<T>): ValidateActionChain<T> {
143
+ if (registerAdmission) {
103
144
  log("Validate Action", validateCallback.toString());
104
145
 
105
146
  // Push the binding to the list of bindings for this capability as a new BindingAction
@@ -110,10 +151,12 @@ export class Capability implements CapabilityExport {
110
151
  validateCallback,
111
152
  });
112
153
  }
154
+
155
+ return { Watch };
113
156
  }
114
157
 
115
158
  function Mutate(mutateCallback: MutateAction<T>): MutateActionChain<T> {
116
- if (!isWatchMode) {
159
+ if (registerAdmission) {
117
160
  log("Mutate Action", mutateCallback.toString());
118
161
 
119
162
  // Push the binding to the list of bindings for this capability as a new BindingAction
@@ -126,7 +169,19 @@ export class Capability implements CapabilityExport {
126
169
  }
127
170
 
128
171
  // Now only allow adding actions to the same binding
129
- return { Validate };
172
+ return { Watch, Validate };
173
+ }
174
+
175
+ function Watch(watchCallback: WatchAction<T>) {
176
+ if (registerWatch) {
177
+ log("Watch Action", watchCallback.toString());
178
+
179
+ bindings.push({
180
+ ...binding,
181
+ isWatch: true,
182
+ watchCallback,
183
+ });
184
+ }
130
185
  }
131
186
 
132
187
  function InNamespace(...namespaces: string[]): BindingWithName<T> {
@@ -168,5 +223,5 @@ export class Capability implements CapabilityExport {
168
223
  IsUpdated: () => bindEvent(Event.Update),
169
224
  IsDeleted: () => bindEvent(Event.Delete),
170
225
  };
171
- }
226
+ };
172
227
  }
@@ -5,14 +5,14 @@ import express, { NextFunction } from "express";
5
5
  import fs from "fs";
6
6
  import https from "https";
7
7
 
8
- import { Capability } from "./capability";
9
- import { isWatchMode } from "./k8s";
10
- import { MutateResponse, Request, ValidateResponse } from "./k8s/types";
11
- import Log from "./logger";
12
- import { MetricsCollector } from "./metrics";
13
- import { mutateProcessor } from "./mutate-processor";
14
- import { ModuleConfig } from "./types";
15
- import { validateProcessor } from "./validate-processor";
8
+ import { Capability } from "../capability";
9
+ import { MutateResponse, AdmissionRequest, ValidateResponse } from "../k8s";
10
+ import Log from "../logger";
11
+ import { MetricsCollector } from "../metrics";
12
+ import { ModuleConfig, isWatchMode } from "../module";
13
+ import { mutateProcessor } from "../mutate-processor";
14
+ import { validateProcessor } from "../validate-processor";
15
+ import { PeprControllerStore } from "./store";
16
16
 
17
17
  export class Controller {
18
18
  // Track whether the server is running
@@ -30,22 +30,25 @@ export class Controller {
30
30
  // Initialized with the constructor
31
31
  readonly #config: ModuleConfig;
32
32
  readonly #capabilities: Capability[];
33
- readonly #beforeHook?: (req: Request) => void;
33
+ readonly #beforeHook?: (req: AdmissionRequest) => void;
34
34
  readonly #afterHook?: (res: MutateResponse) => void;
35
35
 
36
36
  constructor(
37
37
  config: ModuleConfig,
38
38
  capabilities: Capability[],
39
- beforeHook?: (req: Request) => void,
39
+ beforeHook?: (req: AdmissionRequest) => void,
40
40
  afterHook?: (res: MutateResponse) => void,
41
+ onReady?: () => void,
41
42
  ) {
42
43
  this.#config = config;
43
44
  this.#capabilities = capabilities;
44
- this.#beforeHook = beforeHook;
45
- this.#afterHook = afterHook;
46
45
 
47
- // Bind public methods
48
- this.startServer = this.startServer.bind(this);
46
+ // Initialize the Pepr store for each capability
47
+ new PeprControllerStore(config, capabilities, () => {
48
+ this.#bindEndpoints();
49
+ onReady && onReady();
50
+ Log.info("✅ Controller startup complete");
51
+ });
49
52
 
50
53
  // Middleware for logging requests
51
54
  this.#app.use(Controller.#logger);
@@ -55,18 +58,17 @@ export class Controller {
55
58
 
56
59
  if (beforeHook) {
57
60
  Log.info(`Using beforeHook: ${beforeHook}`);
61
+ this.#beforeHook = beforeHook;
58
62
  }
59
63
 
60
64
  if (afterHook) {
61
65
  Log.info(`Using afterHook: ${afterHook}`);
66
+ this.#afterHook = afterHook;
62
67
  }
63
-
64
- // Bind endpoints
65
- this.#bindEndpoints();
66
68
  }
67
69
 
68
70
  /** Start the webhook server */
69
- startServer(port: number) {
71
+ startServer = (port: number) => {
70
72
  if (this.#running) {
71
73
  throw new Error("Cannot start Pepr module: Pepr module was not instantiated with deferStart=true");
72
74
  }
@@ -78,7 +80,7 @@ export class Controller {
78
80
  };
79
81
 
80
82
  // Get the API token if not in watch mode
81
- if (!isWatchMode) {
83
+ if (!isWatchMode()) {
82
84
  // Get the API token from the environment variable or the mounted secret
83
85
  this.#token = process.env.PEPR_API_TOKEN || fs.readFileSync("/app/api-token/value").toString().trim();
84
86
  Log.info(`Using API token: ${this.#token}`);
@@ -119,7 +121,7 @@ export class Controller {
119
121
  process.exit(0);
120
122
  });
121
123
  });
122
- }
124
+ };
123
125
 
124
126
  #bindEndpoints = () => {
125
127
  // Health check endpoint
@@ -128,7 +130,7 @@ export class Controller {
128
130
  // Metrics endpoint
129
131
  this.#app.get("/metrics", this.#metrics);
130
132
 
131
- if (isWatchMode) {
133
+ if (isWatchMode()) {
132
134
  return;
133
135
  }
134
136
 
@@ -190,11 +192,11 @@ export class Controller {
190
192
  // Create the admission request handler
191
193
  return async (req: express.Request, res: express.Response) => {
192
194
  // Start the metrics timer
193
- const startTime = this.#metricsCollector.observeStart();
195
+ const startTime = MetricsCollector.observeStart();
194
196
 
195
197
  try {
196
198
  // Get the request from the body or create an empty request
197
- const request: Request = req.body?.request || ({} as Request);
199
+ const request: AdmissionRequest = req.body?.request || ({} as AdmissionRequest);
198
200
 
199
201
  // Run the before hook if it exists
200
202
  this.#beforeHook && this.#beforeHook(request || {});
@@ -0,0 +1,197 @@
1
+ // SPDX-License-Identifier: Apache-2.0
2
+ // SPDX-FileCopyrightText: 2023-Present The Pepr Authors
3
+
4
+ import { Operation } from "fast-json-patch";
5
+ import { K8s } from "kubernetes-fluent-client";
6
+ import { startsWith } from "ramda";
7
+
8
+ import { Capability } from "../capability";
9
+ import { PeprStore } from "../k8s";
10
+ import Log from "../logger";
11
+ import { ModuleConfig } from "../module";
12
+ import { DataOp, DataSender, DataStore, Storage } from "../storage";
13
+
14
+ const namespace = "pepr-system";
15
+ const debounceBackoff = 5000;
16
+
17
+ export class PeprControllerStore {
18
+ #name: string;
19
+ #stores: Record<string, Storage> = {};
20
+ #sendDebounce: NodeJS.Timeout | undefined;
21
+ #onReady?: () => void;
22
+
23
+ constructor(config: ModuleConfig, capabilities: Capability[], onReady?: () => void) {
24
+ this.#onReady = onReady;
25
+
26
+ // Setup Pepr State bindings
27
+ this.#name = `pepr-${config.uuid}-store`;
28
+
29
+ // Establish the store for each capability
30
+ for (const { name, registerStore } of capabilities) {
31
+ // Register the store with the capability
32
+ const { store } = registerStore();
33
+
34
+ // Bind the store sender to the capability
35
+ store.registerSender(this.#send(name));
36
+
37
+ // Store the storage instance
38
+ this.#stores[name] = store;
39
+ }
40
+
41
+ // Add a jitter to the Store creation to avoid collisions
42
+ setTimeout(
43
+ () =>
44
+ K8s(PeprStore)
45
+ .InNamespace(namespace)
46
+ .Get(this.#name)
47
+ // If the get succeeds, setup the watch
48
+ .then(this.#setupWatch)
49
+ // Otherwise, create the resource
50
+ .catch(this.#createStoreResource),
51
+ Math.random() * 3000,
52
+ );
53
+ }
54
+
55
+ #setupWatch = () => {
56
+ void K8s(PeprStore, { name: this.#name, namespace }).Watch(this.#receive);
57
+ };
58
+
59
+ #receive = (store: PeprStore) => {
60
+ Log.debug(store, "Pepr Store update");
61
+
62
+ // Wrap the update in a debounced function
63
+ const debounced = () => {
64
+ // Base64 decode the data
65
+ const data: DataStore = store.data || {};
66
+
67
+ // Loop over each stored capability
68
+ for (const name of Object.keys(this.#stores)) {
69
+ // Get the prefix offset for the keys
70
+ const offset = `${name}-`.length;
71
+
72
+ // Get any keys that match the capability name prefix
73
+ const filtered: DataStore = {};
74
+
75
+ // Loop over each key in the secret
76
+ for (const key of Object.keys(data)) {
77
+ // Match on the capability name as a prefix
78
+ if (startsWith(name, key)) {
79
+ // Strip the prefix and store the value
80
+ filtered[key.slice(offset)] = data[key];
81
+ }
82
+ }
83
+
84
+ // Send the data to the receiver callback
85
+ this.#stores[name].receive(filtered);
86
+ }
87
+
88
+ // Call the onReady callback if this is the first time the secret has been read
89
+ if (this.#onReady) {
90
+ this.#onReady();
91
+ this.#onReady = undefined;
92
+ }
93
+ };
94
+
95
+ // Debounce the update to 1 second to avoid multiple rapid calls
96
+ clearTimeout(this.#sendDebounce);
97
+ this.#sendDebounce = setTimeout(debounced, debounceBackoff);
98
+ };
99
+
100
+ #send = (capabilityName: string) => {
101
+ const sendCache: Record<string, Operation> = {};
102
+
103
+ // Load the sendCache with patch operations
104
+ const fillCache = (op: DataOp, key: string[], val?: string) => {
105
+ if (op === "add") {
106
+ const path = `/data/${capabilityName}-${key}`;
107
+ const value = val || "";
108
+ const cacheIdx = [op, path, value].join(":");
109
+
110
+ // Add the operation to the cache
111
+ sendCache[cacheIdx] = { op, path, value };
112
+
113
+ return;
114
+ }
115
+
116
+ if (op === "remove") {
117
+ if (key.length < 1) {
118
+ throw new Error(`Key is required for REMOVE operation`);
119
+ }
120
+
121
+ for (const k of key) {
122
+ const path = `/data/${capabilityName}-${k}`;
123
+ const cacheIdx = [op, path].join(":");
124
+
125
+ // Add the operation to the cache
126
+ sendCache[cacheIdx] = { op, path };
127
+ }
128
+
129
+ return;
130
+ }
131
+
132
+ // If we get here, the operation is not supported
133
+ throw new Error(`Unsupported operation: ${op}`);
134
+ };
135
+
136
+ // Send the cached updates to the cluster
137
+ const flushCache = async () => {
138
+ const indexes = Object.keys(sendCache);
139
+ const payload = Object.values(sendCache);
140
+
141
+ // Loop over each key in the cache and delete it to avoid collisions with other sender calls
142
+ for (const idx of indexes) {
143
+ delete sendCache[idx];
144
+ }
145
+
146
+ try {
147
+ // Send the patch to the cluster
148
+ await K8s(PeprStore, { namespace, name: this.#name }).Patch(payload);
149
+ } catch (err) {
150
+ Log.error(err, "Pepr store update failure");
151
+
152
+ // On failure to update, re-add the operations to the cache to be retried
153
+ for (const idx of indexes) {
154
+ sendCache[idx] = payload[Number(idx)];
155
+ }
156
+ }
157
+ };
158
+
159
+ // Create a sender function for the capability to add/remove data from the store
160
+ const sender: DataSender = async (op: DataOp, key: string[], val?: string) => {
161
+ fillCache(op, key, val);
162
+ };
163
+
164
+ // Send any cached updates every debounceBackoff milliseconds
165
+ setInterval(() => {
166
+ if (Object.keys(sendCache).length > 0) {
167
+ Log.debug(sendCache, "Sending updates to Pepr store");
168
+ void flushCache();
169
+ }
170
+ }, debounceBackoff);
171
+
172
+ return sender;
173
+ };
174
+
175
+ #createStoreResource = async (e: unknown) => {
176
+ Log.info(`Pepr store not found, creating...`);
177
+ Log.debug(e);
178
+
179
+ try {
180
+ await K8s(PeprStore).Apply({
181
+ metadata: {
182
+ name: this.#name,
183
+ namespace,
184
+ },
185
+ data: {
186
+ // JSON Patch will die if the data is empty, so we need to add a placeholder
187
+ __pepr_do_not_delete__: "k-thx-bye",
188
+ },
189
+ });
190
+
191
+ // Now that the resource exists, setup the watch
192
+ this.#setupWatch();
193
+ } catch (err) {
194
+ Log.error(err, "Failed to create Pepr store");
195
+ }
196
+ };
197
+ }
package/src/lib/filter.ts CHANGED
@@ -1,7 +1,7 @@
1
1
  // SPDX-License-Identifier: Apache-2.0
2
2
  // SPDX-FileCopyrightText: 2023-Present The Pepr Authors
3
3
 
4
- import { Operation, Request } from "./k8s/types";
4
+ import { AdmissionRequest, Operation } from "./k8s";
5
5
  import logger from "./logger";
6
6
  import { Binding, Event } from "./types";
7
7
 
@@ -12,7 +12,7 @@ import { Binding, Event } from "./types";
12
12
  * @param req the incoming request
13
13
  * @returns
14
14
  */
15
- export function shouldSkipRequest(binding: Binding, req: Request, capabilityNamespaces: string[]) {
15
+ export function shouldSkipRequest(binding: Binding, req: AdmissionRequest, capabilityNamespaces: string[]) {
16
16
  const { group, kind, version } = binding.kind || {};
17
17
  const { namespaces, labels, annotations, name } = binding.filters || {};
18
18
  const operation = req.operation.toUpperCase();
@@ -1,9 +1,7 @@
1
1
  // SPDX-License-Identifier: Apache-2.0
2
2
  // SPDX-FileCopyrightText: 2023-Present The Pepr Authors
3
3
 
4
- import { KubernetesListObject, KubernetesObject, V1ObjectMeta } from "@kubernetes/client-node";
5
-
6
- export { KubernetesListObject, KubernetesObject };
4
+ import { GenericKind, GroupVersionKind, KubernetesObject, RegisterKind } from "kubernetes-fluent-client";
7
5
 
8
6
  export enum Operation {
9
7
  CREATE = "CREATE",
@@ -13,30 +11,21 @@ export enum Operation {
13
11
  }
14
12
 
15
13
  /**
16
- * GenericKind is a generic Kubernetes object that can be used to represent any Kubernetes object
17
- * that is not explicitly supported by Pepr. This can be used on its own or as a base class for
18
- * other types. See the examples in `HelloPepr.ts` for more information.
14
+ * PeprStore for internal use by Pepr. This is used to store arbitrary data in the cluster.
19
15
  */
20
- export class GenericKind {
21
- apiVersion?: string;
22
- kind?: string;
23
- metadata?: V1ObjectMeta;
24
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
25
- [key: string]: any;
16
+ export class PeprStore extends GenericKind {
17
+ declare data: {
18
+ [key: string]: string;
19
+ };
26
20
  }
27
21
 
28
- /**
29
- * GroupVersionKind unambiguously identifies a kind. It doesn't anonymously include GroupVersion
30
- * to avoid automatic coercion. It doesn't use a GroupVersion to avoid custom marshalling
31
- **/
32
- export interface GroupVersionKind {
33
- /** The K8s resource kind, e..g "Pod". */
34
- readonly kind: string;
35
- readonly group: string;
36
- readonly version?: string;
37
- /** Optional, override the plural name for use in Webhook rules generation */
38
- readonly plural?: string;
39
- }
22
+ export const peprStoreGVK = {
23
+ kind: "PeprStore",
24
+ version: "v1",
25
+ group: "pepr.dev",
26
+ };
27
+
28
+ RegisterKind(PeprStore, peprStoreGVK);
40
29
 
41
30
  /**
42
31
  * GroupVersionResource unambiguously identifies a resource. It doesn't anonymously include GroupVersion
@@ -51,7 +40,7 @@ export interface GroupVersionResource {
51
40
  /**
52
41
  * A Kubernetes admission request to be processed by a capability.
53
42
  */
54
- export interface Request<T = KubernetesObject> {
43
+ export interface AdmissionRequest<T = KubernetesObject> {
55
44
  /** UID is an identifier for the individual request/response. */
56
45
  readonly uid: string;
57
46
 
@@ -161,7 +150,7 @@ export interface ValidateResponse extends MutateResponse {
161
150
  /** Status contains extra details into why an admission request was denied. This field IS NOT consulted in any way if "Allowed" is "true". */
162
151
  status?: {
163
152
  /** A machine-readable description of why this operation is in the
164
- "Failure" status. If this value is empty there is no information available. */
153
+ "Failure" status. If this value is empty there is no information available. */
165
154
  code: number;
166
155
 
167
156
  /** A human-readable description of the status of this operation. */