pepr 0.0.0-development

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 (58) hide show
  1. package/.prettierignore +1 -0
  2. package/CODE_OF_CONDUCT.md +133 -0
  3. package/LICENSE +201 -0
  4. package/README.md +151 -0
  5. package/SECURITY.md +18 -0
  6. package/SUPPORT.md +16 -0
  7. package/codecov.yaml +19 -0
  8. package/commitlint.config.js +1 -0
  9. package/package.json +70 -0
  10. package/src/cli.ts +48 -0
  11. package/src/lib/assets/deploy.ts +122 -0
  12. package/src/lib/assets/destroy.ts +33 -0
  13. package/src/lib/assets/helm.ts +219 -0
  14. package/src/lib/assets/index.ts +175 -0
  15. package/src/lib/assets/loader.ts +41 -0
  16. package/src/lib/assets/networking.ts +89 -0
  17. package/src/lib/assets/pods.ts +353 -0
  18. package/src/lib/assets/rbac.ts +111 -0
  19. package/src/lib/assets/store.ts +49 -0
  20. package/src/lib/assets/webhooks.ts +147 -0
  21. package/src/lib/assets/yaml.ts +234 -0
  22. package/src/lib/capability.ts +314 -0
  23. package/src/lib/controller/index.ts +326 -0
  24. package/src/lib/controller/store.ts +219 -0
  25. package/src/lib/errors.ts +20 -0
  26. package/src/lib/filter.ts +110 -0
  27. package/src/lib/helpers.ts +342 -0
  28. package/src/lib/included-files.ts +19 -0
  29. package/src/lib/k8s.ts +169 -0
  30. package/src/lib/logger.ts +27 -0
  31. package/src/lib/metrics.ts +120 -0
  32. package/src/lib/module.ts +136 -0
  33. package/src/lib/mutate-processor.ts +160 -0
  34. package/src/lib/mutate-request.ts +153 -0
  35. package/src/lib/queue.ts +89 -0
  36. package/src/lib/schedule.ts +175 -0
  37. package/src/lib/storage.ts +192 -0
  38. package/src/lib/tls.ts +90 -0
  39. package/src/lib/types.ts +215 -0
  40. package/src/lib/utils.ts +57 -0
  41. package/src/lib/validate-processor.ts +80 -0
  42. package/src/lib/validate-request.ts +102 -0
  43. package/src/lib/watch-processor.ts +124 -0
  44. package/src/lib.ts +27 -0
  45. package/src/runtime/controller.ts +75 -0
  46. package/src/sdk/sdk.ts +116 -0
  47. package/src/templates/.eslintrc.template.json +18 -0
  48. package/src/templates/.prettierrc.json +13 -0
  49. package/src/templates/README.md +21 -0
  50. package/src/templates/capabilities/hello-pepr.samples.json +160 -0
  51. package/src/templates/capabilities/hello-pepr.ts +426 -0
  52. package/src/templates/gitignore +4 -0
  53. package/src/templates/package.json +20 -0
  54. package/src/templates/pepr.code-snippets.json +21 -0
  55. package/src/templates/pepr.ts +17 -0
  56. package/src/templates/settings.json +10 -0
  57. package/src/templates/tsconfig.json +9 -0
  58. package/src/templates/tsconfig.module.json +19 -0
@@ -0,0 +1,326 @@
1
+ // SPDX-License-Identifier: Apache-2.0
2
+ // SPDX-FileCopyrightText: 2023-Present The Pepr Authors
3
+
4
+ import express, { NextFunction } from "express";
5
+ import fs from "fs";
6
+ import https from "https";
7
+
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
+ import { ResponseItem } from "../types";
17
+
18
+ export class Controller {
19
+ // Track whether the server is running
20
+ #running = false;
21
+
22
+ // Metrics collector
23
+ #metricsCollector = new MetricsCollector("pepr");
24
+
25
+ // The token used to authenticate requests
26
+ #token = "";
27
+
28
+ // The express app instance
29
+ readonly #app = express();
30
+
31
+ // Initialized with the constructor
32
+ readonly #config: ModuleConfig;
33
+ readonly #capabilities: Capability[];
34
+ readonly #beforeHook?: (req: AdmissionRequest) => void;
35
+ readonly #afterHook?: (res: MutateResponse | ValidateResponse) => void;
36
+
37
+ constructor(
38
+ config: ModuleConfig,
39
+ capabilities: Capability[],
40
+ beforeHook?: (req: AdmissionRequest) => void,
41
+ afterHook?: (res: MutateResponse | ValidateResponse) => void,
42
+ onReady?: () => void,
43
+ ) {
44
+ this.#config = config;
45
+ this.#capabilities = capabilities;
46
+
47
+ // Initialize the Pepr store for each capability
48
+ new PeprControllerStore(capabilities, `pepr-${config.uuid}-store`, () => {
49
+ this.#bindEndpoints();
50
+ onReady && onReady();
51
+ Log.info("✅ Controller startup complete");
52
+ // Initialize the schedule store for each capability
53
+ new PeprControllerStore(capabilities, `pepr-${config.uuid}-schedule`, () => {
54
+ Log.info("✅ Scheduling processed");
55
+ });
56
+ });
57
+
58
+ // Middleware for logging requests
59
+ this.#app.use(Controller.#logger);
60
+
61
+ // Middleware for parsing JSON, limit to 2mb vs 100K for K8s compatibility
62
+ this.#app.use(express.json({ limit: "2mb" }));
63
+
64
+ if (beforeHook) {
65
+ Log.info(`Using beforeHook: ${beforeHook}`);
66
+ this.#beforeHook = beforeHook;
67
+ }
68
+
69
+ if (afterHook) {
70
+ Log.info(`Using afterHook: ${afterHook}`);
71
+ this.#afterHook = afterHook;
72
+ }
73
+ }
74
+
75
+ /** Start the webhook server */
76
+ startServer = (port: number) => {
77
+ if (this.#running) {
78
+ throw new Error("Cannot start Pepr module: Pepr module was not instantiated with deferStart=true");
79
+ }
80
+
81
+ // Load SSL certificate and key
82
+ const options = {
83
+ key: fs.readFileSync(process.env.SSL_KEY_PATH || "/etc/certs/tls.key"),
84
+ cert: fs.readFileSync(process.env.SSL_CERT_PATH || "/etc/certs/tls.crt"),
85
+ };
86
+
87
+ // Get the API token if not in watch mode
88
+ if (!isWatchMode()) {
89
+ // Get the API token from the environment variable or the mounted secret
90
+ this.#token = process.env.PEPR_API_TOKEN || fs.readFileSync("/app/api-token/value").toString().trim();
91
+ Log.info(`Using API token: ${this.#token}`);
92
+
93
+ if (!this.#token) {
94
+ throw new Error("API token not found");
95
+ }
96
+ }
97
+
98
+ // Create HTTPS server
99
+ const server = https.createServer(options, this.#app).listen(port);
100
+
101
+ // Handle server listening event
102
+ server.on("listening", () => {
103
+ Log.info(`Server listening on port ${port}`);
104
+ // Track that the server is running
105
+ this.#running = true;
106
+ });
107
+
108
+ // Handle EADDRINUSE errors
109
+ server.on("error", (e: { code: string }) => {
110
+ if (e.code === "EADDRINUSE") {
111
+ Log.warn(
112
+ `Address in use, retrying in 2 seconds. If this persists, ensure ${port} is not in use, e.g. "lsof -i :${port}"`,
113
+ );
114
+ setTimeout(() => {
115
+ server.close();
116
+ server.listen(port);
117
+ }, 2000);
118
+ }
119
+ });
120
+
121
+ // Listen for the SIGTERM signal and gracefully close the server
122
+ process.on("SIGTERM", () => {
123
+ Log.info("Received SIGTERM, closing server");
124
+ server.close(() => {
125
+ Log.info("Server closed");
126
+ process.exit(0);
127
+ });
128
+ });
129
+ };
130
+
131
+ #bindEndpoints = () => {
132
+ // Health check endpoint
133
+ this.#app.get("/healthz", Controller.#healthz);
134
+
135
+ // Metrics endpoint
136
+ this.#app.get("/metrics", this.#metrics);
137
+
138
+ if (isWatchMode()) {
139
+ return;
140
+ }
141
+
142
+ // Require auth for webhook endpoints
143
+ this.#app.use(["/mutate/:token", "/validate/:token"], this.#validateToken);
144
+
145
+ // Mutate endpoint
146
+ this.#app.post("/mutate/:token", this.#admissionReq("Mutate"));
147
+
148
+ // Validate endpoint
149
+ this.#app.post("/validate/:token", this.#admissionReq("Validate"));
150
+ };
151
+
152
+ /**
153
+ * Validate the token in the request path
154
+ *
155
+ * @param req The incoming request
156
+ * @param res The outgoing response
157
+ * @param next The next middleware function
158
+ * @returns
159
+ */
160
+ #validateToken = (req: express.Request, res: express.Response, next: NextFunction) => {
161
+ // Validate the token
162
+ const { token } = req.params;
163
+ if (token !== this.#token) {
164
+ const err = `Unauthorized: invalid token '${token.replace(/[^\w]/g, "_")}'`;
165
+ Log.warn(err);
166
+ res.status(401).send(err);
167
+ this.#metricsCollector.alert();
168
+ return;
169
+ }
170
+
171
+ // Token is valid, continue
172
+ next();
173
+ };
174
+
175
+ /**
176
+ * Metrics endpoint handler
177
+ *
178
+ * @param req the incoming request
179
+ * @param res the outgoing response
180
+ */
181
+ #metrics = async (req: express.Request, res: express.Response) => {
182
+ try {
183
+ res.send(await this.#metricsCollector.getMetrics());
184
+ } catch (err) {
185
+ Log.error(err, `Error getting metrics`);
186
+ res.status(500).send("Internal Server Error");
187
+ }
188
+ };
189
+
190
+ /**
191
+ * Admission request handler for both mutate and validate requests
192
+ *
193
+ * @param admissionKind the type of admission request
194
+ * @returns the request handler
195
+ */
196
+ #admissionReq = (admissionKind: "Mutate" | "Validate") => {
197
+ // Create the admission request handler
198
+ return async (req: express.Request, res: express.Response) => {
199
+ // Start the metrics timer
200
+ const startTime = MetricsCollector.observeStart();
201
+
202
+ try {
203
+ // Get the request from the body or create an empty request
204
+ const request: AdmissionRequest = req.body?.request || ({} as AdmissionRequest);
205
+
206
+ // Run the before hook if it exists
207
+ this.#beforeHook && this.#beforeHook(request || {});
208
+
209
+ // Setup identifiers for logging
210
+ const name = request?.name ? `/${request.name}` : "";
211
+ const namespace = request?.namespace || "";
212
+ const gvk = request?.kind || { group: "", version: "", kind: "" };
213
+
214
+ const reqMetadata = {
215
+ uid: request.uid,
216
+ namespace,
217
+ name,
218
+ };
219
+
220
+ Log.info({ ...reqMetadata, gvk, operation: request.operation, admissionKind }, "Incoming request");
221
+ Log.debug({ ...reqMetadata, request }, "Incoming request body");
222
+
223
+ // Process the request
224
+ let response: MutateResponse | ValidateResponse[];
225
+
226
+ // Call mutate or validate based on the admission kind
227
+ if (admissionKind === "Mutate") {
228
+ response = await mutateProcessor(this.#config, this.#capabilities, request, reqMetadata);
229
+ } else {
230
+ response = await validateProcessor(this.#capabilities, request, reqMetadata);
231
+ }
232
+
233
+ // Run the after hook if it exists
234
+ const responseList: ValidateResponse[] | MutateResponse[] = Array.isArray(response) ? response : [response];
235
+ responseList.map(res => {
236
+ this.#afterHook && this.#afterHook(res);
237
+ // Log the response
238
+ Log.info({ ...reqMetadata, res }, "Check response");
239
+ });
240
+
241
+ let kubeAdmissionResponse: ValidateResponse[] | MutateResponse | ResponseItem;
242
+
243
+ if (admissionKind === "Mutate") {
244
+ kubeAdmissionResponse = response;
245
+ Log.debug({ ...reqMetadata, response }, "Outgoing response");
246
+ res.send({
247
+ apiVersion: "admission.k8s.io/v1",
248
+ kind: "AdmissionReview",
249
+ response: kubeAdmissionResponse,
250
+ });
251
+ } else {
252
+ kubeAdmissionResponse =
253
+ responseList.length === 0
254
+ ? {
255
+ uid: request.uid,
256
+ allowed: true,
257
+ status: { message: "no in-scope validations -- allowed!" },
258
+ }
259
+ : {
260
+ uid: responseList[0].uid,
261
+ allowed: responseList.filter(r => !r.allowed).length === 0,
262
+ status: {
263
+ message: (responseList as ValidateResponse[])
264
+ .filter(rl => !rl.allowed)
265
+ .map(curr => curr.status?.message)
266
+ .join("; "),
267
+ },
268
+ };
269
+ res.send({
270
+ apiVersion: "admission.k8s.io/v1",
271
+ kind: "AdmissionReview",
272
+ response: kubeAdmissionResponse,
273
+ });
274
+ }
275
+
276
+ Log.debug({ ...reqMetadata, kubeAdmissionResponse }, "Outgoing response");
277
+
278
+ this.#metricsCollector.observeEnd(startTime, admissionKind);
279
+ } catch (err) {
280
+ Log.error(err, `Error processing ${admissionKind} request`);
281
+ res.status(500).send("Internal Server Error");
282
+ this.#metricsCollector.error();
283
+ }
284
+ };
285
+ };
286
+
287
+ /**
288
+ * Middleware for logging requests
289
+ *
290
+ * @param req the incoming request
291
+ * @param res the outgoing response
292
+ * @param next the next middleware function
293
+ */
294
+ static #logger(req: express.Request, res: express.Response, next: express.NextFunction) {
295
+ const startTime = Date.now();
296
+
297
+ res.on("finish", () => {
298
+ const elapsedTime = Date.now() - startTime;
299
+ const message = {
300
+ uid: req.body?.request?.uid,
301
+ method: req.method,
302
+ url: req.originalUrl,
303
+ status: res.statusCode,
304
+ duration: `${elapsedTime} ms`,
305
+ };
306
+
307
+ res.statusCode >= 300 ? Log.warn(message) : Log.info(message);
308
+ });
309
+
310
+ next();
311
+ }
312
+ /**
313
+ * Health check endpoint handler
314
+ *
315
+ * @param req the incoming request
316
+ * @param res the outgoing response
317
+ */
318
+ static #healthz(req: express.Request, res: express.Response) {
319
+ try {
320
+ res.send("OK");
321
+ } catch (err) {
322
+ Log.error(err, `Error processing health check`);
323
+ res.status(500).send("Internal Server Error");
324
+ }
325
+ }
326
+ }
@@ -0,0 +1,219 @@
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 { DataOp, DataSender, DataStore, Storage } from "../storage";
12
+
13
+ const namespace = "pepr-system";
14
+ export const debounceBackoff = 5000;
15
+
16
+ export class PeprControllerStore {
17
+ #name: string;
18
+ #stores: Record<string, Storage> = {};
19
+ #sendDebounce: NodeJS.Timeout | undefined;
20
+ #onReady?: () => void;
21
+
22
+ constructor(capabilities: Capability[], name: string, onReady?: () => void) {
23
+ this.#onReady = onReady;
24
+
25
+ // Setup Pepr State bindings
26
+ this.#name = name;
27
+
28
+ if (name.includes("schedule")) {
29
+ // Establish the store for each capability
30
+ for (const { name, registerScheduleStore, hasSchedule } of capabilities) {
31
+ // Guard Clause to exit early
32
+ if (hasSchedule !== true) {
33
+ continue;
34
+ }
35
+ // Register the scheduleStore with the capability
36
+ const { scheduleStore } = registerScheduleStore();
37
+
38
+ // Bind the store sender to the capability
39
+ scheduleStore.registerSender(this.#send(name));
40
+
41
+ // Store the storage instance
42
+ this.#stores[name] = scheduleStore;
43
+ }
44
+ } else {
45
+ // Establish the store for each capability
46
+ for (const { name, registerStore } of capabilities) {
47
+ // Register the store with the capability
48
+ const { store } = registerStore();
49
+
50
+ // Bind the store sender to the capability
51
+ store.registerSender(this.#send(name));
52
+
53
+ // Store the storage instance
54
+ this.#stores[name] = store;
55
+ }
56
+ }
57
+
58
+ // Add a jitter to the Store creation to avoid collisions
59
+ setTimeout(
60
+ () =>
61
+ K8s(PeprStore)
62
+ .InNamespace(namespace)
63
+ .Get(this.#name)
64
+ // If the get succeeds, setup the watch
65
+ .then(this.#setupWatch)
66
+ // Otherwise, create the resource
67
+ .catch(this.#createStoreResource),
68
+ Math.random() * 3000,
69
+ );
70
+ }
71
+
72
+ #setupWatch = () => {
73
+ const watcher = K8s(PeprStore, { name: this.#name, namespace }).Watch(this.#receive);
74
+ watcher.start().catch(e => Log.error(e, "Error starting Pepr store watch"));
75
+ };
76
+
77
+ #receive = (store: PeprStore) => {
78
+ Log.debug(store, "Pepr Store update");
79
+
80
+ // Wrap the update in a debounced function
81
+ const debounced = () => {
82
+ // Base64 decode the data
83
+ const data: DataStore = store.data || {};
84
+
85
+ // Loop over each stored capability
86
+ for (const name of Object.keys(this.#stores)) {
87
+ // Get the prefix offset for the keys
88
+ const offset = `${name}-`.length;
89
+
90
+ // Get any keys that match the capability name prefix
91
+ const filtered: DataStore = {};
92
+
93
+ // Loop over each key in the secret
94
+ for (const key of Object.keys(data)) {
95
+ // Match on the capability name as a prefix
96
+ if (startsWith(name, key)) {
97
+ // Strip the prefix and store the value
98
+ filtered[key.slice(offset)] = data[key];
99
+ }
100
+ }
101
+
102
+ // Send the data to the receiver callback
103
+ this.#stores[name].receive(filtered);
104
+ }
105
+
106
+ // Call the onReady callback if this is the first time the secret has been read
107
+ if (this.#onReady) {
108
+ this.#onReady();
109
+ this.#onReady = undefined;
110
+ }
111
+ };
112
+
113
+ // Debounce the update to 1 second to avoid multiple rapid calls
114
+ clearTimeout(this.#sendDebounce);
115
+ this.#sendDebounce = setTimeout(debounced, this.#onReady ? 0 : debounceBackoff);
116
+ };
117
+
118
+ #send = (capabilityName: string) => {
119
+ const sendCache: Record<string, Operation> = {};
120
+
121
+ // Load the sendCache with patch operations
122
+ const fillCache = (op: DataOp, key: string[], val?: string) => {
123
+ if (op === "add") {
124
+ const path = `/data/${capabilityName}-${key}`;
125
+ const value = val || "";
126
+ const cacheIdx = [op, path, value].join(":");
127
+
128
+ // Add the operation to the cache
129
+ sendCache[cacheIdx] = { op, path, value };
130
+
131
+ return;
132
+ }
133
+
134
+ if (op === "remove") {
135
+ if (key.length < 1) {
136
+ throw new Error(`Key is required for REMOVE operation`);
137
+ }
138
+
139
+ for (const k of key) {
140
+ const path = `/data/${capabilityName}-${k}`;
141
+ const cacheIdx = [op, path].join(":");
142
+
143
+ // Add the operation to the cache
144
+ sendCache[cacheIdx] = { op, path };
145
+ }
146
+
147
+ return;
148
+ }
149
+
150
+ // If we get here, the operation is not supported
151
+ throw new Error(`Unsupported operation: ${op}`);
152
+ };
153
+
154
+ // Send the cached updates to the cluster
155
+ const flushCache = async () => {
156
+ const indexes = Object.keys(sendCache);
157
+ const payload = Object.values(sendCache);
158
+
159
+ // Loop over each key in the cache and delete it to avoid collisions with other sender calls
160
+ for (const idx of indexes) {
161
+ delete sendCache[idx];
162
+ }
163
+
164
+ try {
165
+ // Send the patch to the cluster
166
+ await K8s(PeprStore, { namespace, name: this.#name }).Patch(payload);
167
+ } catch (err) {
168
+ Log.error(err, "Pepr store update failure");
169
+
170
+ if (err.status === 422) {
171
+ Object.keys(sendCache).forEach(key => delete sendCache[key]);
172
+ } else {
173
+ // On failure to update, re-add the operations to the cache to be retried
174
+ for (const idx of indexes) {
175
+ sendCache[idx] = payload[Number(idx)];
176
+ }
177
+ }
178
+ }
179
+ };
180
+
181
+ // Create a sender function for the capability to add/remove data from the store
182
+ const sender: DataSender = async (op: DataOp, key: string[], val?: string) => {
183
+ fillCache(op, key, val);
184
+ };
185
+
186
+ // Send any cached updates every debounceBackoff milliseconds
187
+ setInterval(() => {
188
+ if (Object.keys(sendCache).length > 0) {
189
+ Log.debug(sendCache, "Sending updates to Pepr store");
190
+ void flushCache();
191
+ }
192
+ }, debounceBackoff);
193
+
194
+ return sender;
195
+ };
196
+
197
+ #createStoreResource = async (e: unknown) => {
198
+ Log.info(`Pepr store not found, creating...`);
199
+ Log.debug(e);
200
+
201
+ try {
202
+ await K8s(PeprStore).Apply({
203
+ metadata: {
204
+ name: this.#name,
205
+ namespace,
206
+ },
207
+ data: {
208
+ // JSON Patch will die if the data is empty, so we need to add a placeholder
209
+ __pepr_do_not_delete__: "k-thx-bye",
210
+ },
211
+ });
212
+
213
+ // Now that the resource exists, setup the watch
214
+ this.#setupWatch();
215
+ } catch (err) {
216
+ Log.error(err, "Failed to create Pepr store");
217
+ }
218
+ };
219
+ }
@@ -0,0 +1,20 @@
1
+ // SPDX-License-Identifier: Apache-2.0
2
+ // SPDX-FileCopyrightText: 2023-Present The Pepr Authors
3
+
4
+ export const Errors = {
5
+ audit: "audit",
6
+ ignore: "ignore",
7
+ reject: "reject",
8
+ };
9
+
10
+ export const ErrorList = Object.values(Errors);
11
+
12
+ /**
13
+ * Validate the error or throw an error
14
+ * @param error
15
+ */
16
+ export function ValidateError(error = "") {
17
+ if (!ErrorList.includes(error)) {
18
+ throw new Error(`Invalid error: ${error}. Must be one of: ${ErrorList.join(", ")}`);
19
+ }
20
+ }
@@ -0,0 +1,110 @@
1
+ // SPDX-License-Identifier: Apache-2.0
2
+ // SPDX-FileCopyrightText: 2023-Present The Pepr Authors
3
+
4
+ import { AdmissionRequest, Operation } from "./k8s";
5
+ import logger from "./logger";
6
+ import { Binding, Event } from "./types";
7
+
8
+ /**
9
+ * shouldSkipRequest determines if a request should be skipped based on the binding filters.
10
+ *
11
+ * @param binding the action binding
12
+ * @param req the incoming request
13
+ * @returns
14
+ */
15
+ export function shouldSkipRequest(binding: Binding, req: AdmissionRequest, capabilityNamespaces: string[]) {
16
+ const { group, kind, version } = binding.kind || {};
17
+ const { namespaces, labels, annotations, name } = binding.filters || {};
18
+ const operation = req.operation.toUpperCase();
19
+ const uid = req.uid;
20
+ // Use the old object if the request is a DELETE operation
21
+ const srcObject = operation === Operation.DELETE ? req.oldObject : req.object;
22
+ const { metadata } = srcObject || {};
23
+ const combinedNamespaces = [...namespaces, ...capabilityNamespaces];
24
+
25
+ // Test for matching operation
26
+ if (!binding.event.includes(operation) && !binding.event.includes(Event.Any)) {
27
+ return true;
28
+ }
29
+
30
+ // Test name first, since it's the most specific
31
+ if (name && name !== req.name) {
32
+ return true;
33
+ }
34
+
35
+ // Test for matching kinds
36
+ if (kind !== req.kind.kind) {
37
+ return true;
38
+ }
39
+
40
+ // Test for matching groups
41
+ if (group && group !== req.kind.group) {
42
+ return true;
43
+ }
44
+
45
+ // Test for matching versions
46
+ if (version && version !== req.kind.version) {
47
+ return true;
48
+ }
49
+
50
+ // Test for matching namespaces
51
+ if (
52
+ (combinedNamespaces.length && !combinedNamespaces.includes(req.namespace || "")) ||
53
+ (!namespaces.includes(req.namespace || "") && capabilityNamespaces.length !== 0 && namespaces.length !== 0)
54
+ ) {
55
+ let type = "";
56
+ let label = "";
57
+
58
+ if (binding.isMutate) {
59
+ type = "Mutate";
60
+ label = binding.mutateCallback!.name;
61
+ } else if (binding.isValidate) {
62
+ type = "Validate";
63
+ label = binding.validateCallback!.name;
64
+ } else if (binding.isWatch) {
65
+ type = "Watch";
66
+ label = binding.watchCallback!.name;
67
+ }
68
+
69
+ logger.debug({ uid }, `${type} binding (${label}) does not match request namespace "${req.namespace}"`);
70
+
71
+ return true;
72
+ }
73
+
74
+ // Test for matching labels
75
+ for (const [key, value] of Object.entries(labels)) {
76
+ const testKey = metadata?.labels?.[key];
77
+
78
+ // First check if the label exists
79
+ if (!testKey) {
80
+ logger.debug({ uid }, `Label ${key} does not exist`);
81
+ return true;
82
+ }
83
+
84
+ // Then check if the value matches, if specified
85
+ if (value && testKey !== value) {
86
+ logger.debug({ uid }, `${testKey} does not match ${value}`);
87
+ return true;
88
+ }
89
+ }
90
+
91
+ // Test for matching annotations
92
+ for (const [key, value] of Object.entries(annotations)) {
93
+ const testKey = metadata?.annotations?.[key];
94
+
95
+ // First check if the annotation exists
96
+ if (!testKey) {
97
+ logger.debug({ uid }, `Annotation ${key} does not exist`);
98
+ return true;
99
+ }
100
+
101
+ // Then check if the value matches, if specified
102
+ if (value && testKey !== value) {
103
+ logger.debug({ uid }, `${testKey} does not match ${value}`);
104
+ return true;
105
+ }
106
+ }
107
+
108
+ // No failed filters, so we should not skip this request
109
+ return false;
110
+ }