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,192 @@
1
+ // SPDX-License-Identifier: Apache-2.0
2
+ // SPDX-FileCopyrightText: 2023-Present The Pepr Authors
3
+
4
+ import { clone } from "ramda";
5
+ import Log from "./logger";
6
+
7
+ export type DataOp = "add" | "remove";
8
+ export type DataStore = Record<string, string>;
9
+ export type DataSender = (op: DataOp, keys: string[], value?: string) => void;
10
+ export type DataReceiver = (data: DataStore) => void;
11
+ export type Unsubscribe = () => void;
12
+
13
+ const MAX_WAIT_TIME = 15000;
14
+ export interface PeprStore {
15
+ /**
16
+ * Returns the current value associated with the given key, or null if the given key does not exist.
17
+ */
18
+ getItem(key: string): string | null;
19
+ /**
20
+ * Removes all key/value pairs, if there are any.
21
+ */
22
+ clear(): void;
23
+ /**
24
+ * Removes the key/value pair with the given key, if a key/value pair with the given key exists.
25
+ */
26
+ removeItem(key: string): void;
27
+ /**
28
+ * Sets the value of the pair identified by key to value, creating a new key/value pair if none existed for key previously.
29
+ */
30
+ setItem(key: string, value: string): void;
31
+
32
+ /**
33
+ * Subscribe to changes in the store. This API behaves similarly to the [Svelte Store API](https://vercel.com/docs/beginner-sveltekit/svelte-stores#using-the-store).
34
+ *
35
+ * @param listener - The callback to be invoked when the store changes.
36
+ * @returns A function to unsubscribe from the listener.
37
+ */
38
+ subscribe(listener: DataReceiver): Unsubscribe;
39
+
40
+ /**
41
+ * Register a function to be called when the store is ready.
42
+ */
43
+ onReady(callback: DataReceiver): void;
44
+
45
+ /**
46
+ * Sets the value of the pair identified by key to value, creating a new key/value pair if none existed for key previously.
47
+ * Resolves when the key/value show up in the store.
48
+ */
49
+ setItemAndWait(key: string, value: string): Promise<void>;
50
+
51
+ /**
52
+ * Remove the value of the key.
53
+ * Resolves when the key does not show up in the store.
54
+ */
55
+ removeItemAndWait(key: string): Promise<void>;
56
+ }
57
+
58
+ /**
59
+ * A key-value data store that can be used to persist data that should be shared across Pepr controllers and capabilities.
60
+ *
61
+ * The API is similar to the [Storage API](https://developer.mozilla.org/docs/Web/API/Storage)
62
+ */
63
+ export class Storage implements PeprStore {
64
+ #store: DataStore = {};
65
+ #send!: DataSender;
66
+ #subscribers: Record<number, DataReceiver> = {};
67
+ #subscriberId = 0;
68
+ #readyHandlers: DataReceiver[] = [];
69
+
70
+ registerSender = (send: DataSender) => {
71
+ this.#send = send;
72
+ };
73
+
74
+ receive = (data: DataStore) => {
75
+ Log.debug(data, `Pepr store data received`);
76
+ this.#store = data || {};
77
+
78
+ this.#onReady();
79
+
80
+ // Notify all subscribers
81
+ for (const idx in this.#subscribers) {
82
+ // Send a unique clone of the store to each subscriber
83
+ this.#subscribers[idx](clone(this.#store));
84
+ }
85
+ };
86
+
87
+ getItem = (key: string) => {
88
+ // Return null if the value is the empty string
89
+ return this.#store[key] || null;
90
+ };
91
+
92
+ clear = () => {
93
+ this.#dispatchUpdate("remove", Object.keys(this.#store));
94
+ };
95
+
96
+ removeItem = (key: string) => {
97
+ this.#dispatchUpdate("remove", [key]);
98
+ };
99
+
100
+ setItem = (key: string, value: string) => {
101
+ this.#dispatchUpdate("add", [key], value);
102
+ };
103
+
104
+ /**
105
+ * Creates a promise and subscribes to the store, the promise resolves when
106
+ * the key and value are seen in the store.
107
+ *
108
+ * @param key - The key to add into the store
109
+ * @param value - The value of the key
110
+ * @returns
111
+ */
112
+ setItemAndWait = (key: string, value: string) => {
113
+ this.#dispatchUpdate("add", [key], value);
114
+ return new Promise<void>((resolve, reject) => {
115
+ const unsubscribe = this.subscribe(data => {
116
+ if (data[key] === value) {
117
+ unsubscribe();
118
+ resolve();
119
+ }
120
+ });
121
+
122
+ // If promise has not resolved before MAX_WAIT_TIME reject
123
+ setTimeout(() => {
124
+ unsubscribe();
125
+ return reject();
126
+ }, MAX_WAIT_TIME);
127
+ });
128
+ };
129
+
130
+ /**
131
+ * Creates a promise and subscribes to the store, the promise resolves when
132
+ * the key is removed from the store.
133
+ *
134
+ * @param key - The key to add into the store
135
+ * @returns
136
+ */
137
+ removeItemAndWait = (key: string) => {
138
+ this.#dispatchUpdate("remove", [key]);
139
+ return new Promise<void>((resolve, reject) => {
140
+ const unsubscribe = this.subscribe(data => {
141
+ if (!Object.hasOwn(data, key)) {
142
+ unsubscribe();
143
+ resolve();
144
+ }
145
+ });
146
+
147
+ // If promise has not resolved before MAX_WAIT_TIME reject
148
+ setTimeout(() => {
149
+ unsubscribe();
150
+ return reject();
151
+ }, MAX_WAIT_TIME);
152
+ });
153
+ };
154
+
155
+ subscribe = (subscriber: DataReceiver) => {
156
+ const idx = this.#subscriberId++;
157
+ this.#subscribers[idx] = subscriber;
158
+ return () => this.unsubscribe(idx);
159
+ };
160
+
161
+ onReady = (callback: DataReceiver) => {
162
+ this.#readyHandlers.push(callback);
163
+ };
164
+
165
+ /**
166
+ * Remove a subscriber from the list of subscribers.
167
+ * @param idx - The index of the subscriber to remove.
168
+ */
169
+ unsubscribe = (idx: number) => {
170
+ delete this.#subscribers[idx];
171
+ };
172
+
173
+ #onReady = () => {
174
+ // Notify all ready handlers with a clone of the store
175
+ for (const handler of this.#readyHandlers) {
176
+ handler(clone(this.#store));
177
+ }
178
+
179
+ // Make this a noop so that it can't be called again
180
+ this.#onReady = () => {};
181
+ };
182
+
183
+ /**
184
+ * Dispatch an update to the store and notify all subscribers.
185
+ * @param op - The type of operation to perform.
186
+ * @param keys - The keys to update.
187
+ * @param [value] - The new value.
188
+ */
189
+ #dispatchUpdate = (op: DataOp, keys: string[], value?: string) => {
190
+ this.#send(op, keys, value);
191
+ };
192
+ }
package/src/lib/tls.ts ADDED
@@ -0,0 +1,90 @@
1
+ import forge from "node-forge";
2
+
3
+ const caName = "Pepr Ephemeral CA";
4
+
5
+ export interface TLSOut {
6
+ ca: string;
7
+ crt: string;
8
+ key: string;
9
+ pem: {
10
+ ca: string;
11
+ crt: string;
12
+ key: string;
13
+ };
14
+ }
15
+
16
+ /**
17
+ * Generates a self-signed CA and server certificate with Subject Alternative Names (SANs) for the K8s webhook.
18
+ *
19
+ * @param {string} name - The name to use for the server certificate's Common Name and SAN DNS entry.
20
+ * @returns {TLSOut} - An object containing the Base64-encoded CA, server certificate, and server private key.
21
+ */
22
+ export function genTLS(name: string): TLSOut {
23
+ // Generate a new CA key pair and create a self-signed CA certificate
24
+ const caKeys = forge.pki.rsa.generateKeyPair(2048);
25
+ const caCert = genCert(caKeys, caName, [{ name: "commonName", value: caName }]);
26
+
27
+ caCert.setExtensions([
28
+ {
29
+ name: "basicConstraints",
30
+ cA: true,
31
+ },
32
+ {
33
+ name: "keyUsage",
34
+ keyCertSign: true,
35
+ digitalSignature: true,
36
+ nonRepudiation: true,
37
+ keyEncipherment: true,
38
+ dataEncipherment: true,
39
+ },
40
+ ]);
41
+
42
+ // Generate a new server key pair and create a server certificate signed by the CA
43
+ const serverKeys = forge.pki.rsa.generateKeyPair(2048);
44
+ const serverCert = genCert(serverKeys, name, caCert.subject.attributes);
45
+
46
+ // Sign both certificates with the CA private key
47
+ caCert.sign(caKeys.privateKey, forge.md.sha256.create());
48
+ serverCert.sign(caKeys.privateKey, forge.md.sha256.create());
49
+
50
+ // Convert the keys and certificates to PEM format
51
+ const pem = {
52
+ ca: forge.pki.certificateToPem(caCert),
53
+ crt: forge.pki.certificateToPem(serverCert),
54
+ key: forge.pki.privateKeyToPem(serverKeys.privateKey),
55
+ };
56
+
57
+ // Base64-encode the PEM strings
58
+ const ca = Buffer.from(pem.ca).toString("base64");
59
+ const key = Buffer.from(pem.key).toString("base64");
60
+ const crt = Buffer.from(pem.crt).toString("base64");
61
+
62
+ return { ca, key, crt, pem };
63
+ }
64
+
65
+ function genCert(key: forge.pki.rsa.KeyPair, name: string, issuer: forge.pki.CertificateField[]) {
66
+ const crt = forge.pki.createCertificate();
67
+ crt.publicKey = key.publicKey;
68
+ crt.serialNumber = "01";
69
+ crt.validity.notBefore = new Date();
70
+ crt.validity.notAfter = new Date();
71
+ crt.validity.notAfter.setFullYear(crt.validity.notBefore.getFullYear() + 1);
72
+
73
+ // Add SANs to the server certificate
74
+ crt.setExtensions([
75
+ {
76
+ name: "subjectAltName",
77
+ altNames: [
78
+ {
79
+ type: 2, // DNS
80
+ value: name,
81
+ },
82
+ ],
83
+ },
84
+ ]);
85
+
86
+ // Set the server certificate's issuer to the CA
87
+ crt.setIssuer(issuer);
88
+
89
+ return crt;
90
+ }
@@ -0,0 +1,215 @@
1
+ // SPDX-License-Identifier: Apache-2.0
2
+ // SPDX-FileCopyrightText: 2023-Present The Pepr Authors
3
+
4
+ import { GenericClass, GroupVersionKind, KubernetesObject } from "kubernetes-fluent-client";
5
+ import { WatchAction } from "kubernetes-fluent-client/dist/fluent/types";
6
+
7
+ import { PeprMutateRequest } from "./mutate-request";
8
+ import { PeprValidateRequest } from "./validate-request";
9
+
10
+ /**
11
+ * Specifically for parsing logs in monitor mode
12
+ */
13
+ export interface ResponseItem {
14
+ uid?: string;
15
+ allowed: boolean;
16
+ status: {
17
+ message: string;
18
+ };
19
+ }
20
+ /**
21
+ * Recursively make all properties in T optional.
22
+ */
23
+ export type DeepPartial<T> = {
24
+ [P in keyof T]?: T[P] extends object ? DeepPartial<T[P]> : T[P];
25
+ };
26
+
27
+ /**
28
+ * The type of Kubernetes mutating webhook event that the action is registered for.
29
+ */
30
+ export enum Event {
31
+ Create = "CREATE",
32
+ Update = "UPDATE",
33
+ Delete = "DELETE",
34
+ CreateOrUpdate = "CREATEORUPDATE",
35
+ Any = "*",
36
+ }
37
+
38
+ export interface CapabilityCfg {
39
+ /**
40
+ * The name of the capability. This should be unique.
41
+ */
42
+ name: string;
43
+ /**
44
+ * A description of the capability and what it does.
45
+ */
46
+ description: string;
47
+ /**
48
+ * List of namespaces that this capability applies to, if empty, applies to all namespaces (cluster-wide).
49
+ * This does not supersede the `alwaysIgnore` global configuration.
50
+ */
51
+ namespaces?: string[];
52
+ }
53
+
54
+ export interface CapabilityExport extends CapabilityCfg {
55
+ bindings: Binding[];
56
+ hasSchedule: boolean;
57
+ }
58
+
59
+ export type WhenSelector<T extends GenericClass> = {
60
+ /** Register an action to be executed when a Kubernetes resource is created or updated. */
61
+ IsCreatedOrUpdated: () => BindingAll<T>;
62
+ /** Register an action to be executed when a Kubernetes resource is created. */
63
+ IsCreated: () => BindingAll<T>;
64
+ /** Register ann action to be executed when a Kubernetes resource is updated. */
65
+ IsUpdated: () => BindingAll<T>;
66
+ /** Register an action to be executed when a Kubernetes resource is deleted. */
67
+ IsDeleted: () => BindingAll<T>;
68
+ };
69
+
70
+ export type Binding = {
71
+ event: Event;
72
+ isMutate?: boolean;
73
+ isValidate?: boolean;
74
+ isWatch?: boolean;
75
+ isQueue?: boolean;
76
+ readonly model: GenericClass;
77
+ readonly kind: GroupVersionKind;
78
+ readonly filters: {
79
+ name: string;
80
+ namespaces: string[];
81
+ labels: Record<string, string>;
82
+ annotations: Record<string, string>;
83
+ };
84
+ readonly mutateCallback?: MutateAction<GenericClass, InstanceType<GenericClass>>;
85
+ readonly validateCallback?: ValidateAction<GenericClass, InstanceType<GenericClass>>;
86
+ readonly watchCallback?: WatchAction<GenericClass, InstanceType<GenericClass>>;
87
+ };
88
+
89
+ export type BindingFilter<T extends GenericClass> = CommonActionChain<T> & {
90
+ /**
91
+ * Only apply the action if the resource has the specified label. If no value is specified, the label must exist.
92
+ * Note multiple calls to this method will result in an AND condition. e.g.
93
+ *
94
+ * ```ts
95
+ * When(a.Deployment)
96
+ * .IsCreated()
97
+ * .WithLabel("foo", "bar")
98
+ * .WithLabel("baz", "qux")
99
+ * .Mutate(...)
100
+ * ```
101
+ *
102
+ * Will only apply the action if the resource has both the `foo=bar` and `baz=qux` labels.
103
+ *
104
+ * @param key
105
+ * @param value
106
+ */
107
+ WithLabel: (key: string, value?: string) => BindingFilter<T>;
108
+ /**
109
+ * Only apply the action if the resource has the specified annotation. If no value is specified, the annotation must exist.
110
+ * Note multiple calls to this method will result in an AND condition. e.g.
111
+ *
112
+ * ```ts
113
+ * When(a.Deployment)
114
+ * .IsCreated()
115
+ * .WithAnnotation("foo", "bar")
116
+ * .WithAnnotation("baz", "qux")
117
+ * .Mutate(...)
118
+ * ```
119
+ *
120
+ * Will only apply the action if the resource has both the `foo=bar` and `baz=qux` annotations.
121
+ *
122
+ * @param key
123
+ * @param value
124
+ */
125
+ WithAnnotation: (key: string, value?: string) => BindingFilter<T>;
126
+ };
127
+
128
+ export type BindingWithName<T extends GenericClass> = BindingFilter<T> & {
129
+ /** Only apply the action if the resource name matches the specified name. */
130
+ WithName: (name: string) => BindingFilter<T>;
131
+ };
132
+
133
+ export type BindingAll<T extends GenericClass> = BindingWithName<T> & {
134
+ /** Only apply the action if the resource is in one of the specified namespaces.*/
135
+ InNamespace: (...namespaces: string[]) => BindingWithName<T>;
136
+ };
137
+
138
+ export type CommonActionChain<T extends GenericClass> = MutateActionChain<T> & {
139
+ /**
140
+ * Create a new MUTATE action with the specified callback function and previously specified
141
+ * filters.
142
+ *
143
+ * @since 0.13.0
144
+ *
145
+ * @param action The action to be executed when the Kubernetes resource is processed by the AdmissionController.
146
+ */
147
+ Mutate: (action: MutateAction<T, InstanceType<T>>) => MutateActionChain<T>;
148
+ };
149
+
150
+ export type ValidateActionChain<T extends GenericClass> = {
151
+ /**
152
+ * Establish a watcher for the specified resource. The callback function will be executed after the admission controller has
153
+ * processed the resource and the request has been persisted to the cluster.
154
+ *
155
+ * **Beta Function**: This method is still in early testing and edge cases may still exist.
156
+ *
157
+ * @since 0.14.0
158
+ *
159
+ * @param action
160
+ * @returns
161
+ */
162
+ Watch: (action: WatchAction<T, InstanceType<T>>) => void;
163
+
164
+ /**
165
+ * Establish a reconcile for the specified resource. The callback function will be executed after the admission controller has
166
+ * processed the resource and the request has been persisted to the cluster.
167
+ *
168
+ * **Beta Function**: This method is still in early testing and edge cases may still exist.
169
+ *
170
+ * @since 0.14.0
171
+ *
172
+ * @param action
173
+ * @returns
174
+ */
175
+ Reconcile: (action: WatchAction<T, InstanceType<T>>) => void;
176
+ };
177
+
178
+ export type MutateActionChain<T extends GenericClass> = ValidateActionChain<T> & {
179
+ /**
180
+ * Create a new VALIDATE action with the specified callback function and previously specified
181
+ * filters. Return the `request.Approve()` or `Request.Deny()` methods to approve or deny the request:
182
+ *
183
+ * @since 0.13.0
184
+ *
185
+ * @example
186
+ * ```ts
187
+ * When(a.Deployment)
188
+ * .IsCreated()
189
+ * .Validate(request => {
190
+ * if (request.HasLabel("foo")) {
191
+ * return request.Approve();
192
+ * }
193
+ *
194
+ * return request.Deny("Deployment must have label foo");
195
+ * });
196
+ * ```
197
+ *
198
+ * @param action The action to be executed when the Kubernetes resource is processed by the AdmissionController.
199
+ */
200
+ Validate: (action: ValidateAction<T, InstanceType<T>>) => ValidateActionChain<T>;
201
+ };
202
+
203
+ export type MutateAction<T extends GenericClass, K extends KubernetesObject = InstanceType<T>> = (
204
+ req: PeprMutateRequest<K>,
205
+ ) => Promise<void> | void | Promise<PeprMutateRequest<K>> | PeprMutateRequest<K>;
206
+
207
+ export type ValidateAction<T extends GenericClass, K extends KubernetesObject = InstanceType<T>> = (
208
+ req: PeprValidateRequest<K>,
209
+ ) => Promise<ValidateActionResponse> | ValidateActionResponse;
210
+
211
+ export type ValidateActionResponse = {
212
+ allowed: boolean;
213
+ statusCode?: number;
214
+ statusMessage?: string;
215
+ };
@@ -0,0 +1,57 @@
1
+ // SPDX-License-Identifier: Apache-2.0
2
+ // SPDX-FileCopyrightText: 2023-Present The Pepr Authors
3
+
4
+ import Log from "./logger";
5
+
6
+ /** Test if a string is ascii or not */
7
+ export const isAscii = /^[\s\x20-\x7E]*$/;
8
+
9
+ /**
10
+ * Encode all ascii values in a map to base64
11
+ * @param obj The object to encode
12
+ * @param skip A list of keys to skip encoding
13
+ */
14
+ export function convertToBase64Map(obj: { data?: Record<string, string> }, skip: string[]) {
15
+ obj.data = obj.data ?? {};
16
+ for (const key in obj.data) {
17
+ const value = obj.data[key];
18
+ // Only encode ascii values
19
+ obj.data[key] = skip.includes(key) ? value : base64Encode(value);
20
+ }
21
+ }
22
+
23
+ /**
24
+ * Decode all ascii values in a map from base64 to utf-8
25
+ * @param obj The object to decode
26
+ * @returns A list of keys that were skipped
27
+ */
28
+ export function convertFromBase64Map(obj: { data?: Record<string, string> }) {
29
+ const skip: string[] = [];
30
+
31
+ obj.data = obj.data ?? {};
32
+ for (const key in obj.data) {
33
+ if (obj.data[key] == undefined) {
34
+ obj.data[key] = "";
35
+ } else {
36
+ const decoded = base64Decode(obj.data[key]);
37
+ if (isAscii.test(decoded)) {
38
+ // Only decode ascii values
39
+ obj.data[key] = decoded;
40
+ } else {
41
+ skip.push(key);
42
+ }
43
+ }
44
+ }
45
+ Log.debug(`Non-ascii data detected in keys: ${skip}, skipping automatic base64 decoding`);
46
+ return skip;
47
+ }
48
+
49
+ /** Decode a base64 string */
50
+ export function base64Decode(data: string) {
51
+ return Buffer.from(data, "base64").toString("utf-8");
52
+ }
53
+
54
+ /** Encode a string to base64 */
55
+ export function base64Encode(data: string) {
56
+ return Buffer.from(data).toString("base64");
57
+ }
@@ -0,0 +1,80 @@
1
+ // SPDX-License-Identifier: Apache-2.0
2
+ // SPDX-FileCopyrightText: 2023-Present The Pepr Authors
3
+
4
+ import { kind } from "kubernetes-fluent-client";
5
+
6
+ import { Capability } from "./capability";
7
+ import { shouldSkipRequest } from "./filter";
8
+ import { AdmissionRequest, ValidateResponse } from "./k8s";
9
+ import Log from "./logger";
10
+ import { convertFromBase64Map } from "./utils";
11
+ import { PeprValidateRequest } from "./validate-request";
12
+
13
+ export async function validateProcessor(
14
+ capabilities: Capability[],
15
+ req: AdmissionRequest,
16
+ reqMetadata: Record<string, string>,
17
+ ): Promise<ValidateResponse[]> {
18
+ const wrapped = new PeprValidateRequest(req);
19
+ const response: ValidateResponse[] = [];
20
+
21
+ // If the resource is a secret, decode the data
22
+ const isSecret = req.kind.version == "v1" && req.kind.kind == "Secret";
23
+ if (isSecret) {
24
+ convertFromBase64Map(wrapped.Raw as unknown as kind.Secret);
25
+ }
26
+
27
+ Log.info(reqMetadata, `Processing validation request`);
28
+
29
+ for (const { name, bindings, namespaces } of capabilities) {
30
+ const actionMetadata = { ...reqMetadata, name };
31
+
32
+ for (const action of bindings) {
33
+ // Skip this action if it's not a validation action
34
+ if (!action.validateCallback) {
35
+ continue;
36
+ }
37
+
38
+ const localResponse: ValidateResponse = {
39
+ uid: req.uid,
40
+ allowed: true, // Assume it's allowed until a validation check fails
41
+ };
42
+
43
+ // Continue to the next action without doing anything if this one should be skipped
44
+ if (shouldSkipRequest(action, req, namespaces)) {
45
+ continue;
46
+ }
47
+
48
+ const label = action.validateCallback.name;
49
+ Log.info(actionMetadata, `Processing validation action (${label})`);
50
+
51
+ try {
52
+ // Run the validation callback, if it fails set allowed to false
53
+ const resp = await action.validateCallback(wrapped);
54
+ localResponse.allowed = resp.allowed;
55
+
56
+ // If the validation callback returned a status code or message, set it in the Response
57
+ if (resp.statusCode || resp.statusMessage) {
58
+ localResponse.status = {
59
+ code: resp.statusCode || 400,
60
+ message: resp.statusMessage || `Validation failed for ${name}`,
61
+ };
62
+ }
63
+
64
+ Log.info(actionMetadata, `Validation action complete (${label}): ${resp.allowed ? "allowed" : "denied"}`);
65
+ } catch (e) {
66
+ // If any validation throws an error, note the failure in the Response
67
+ Log.error(actionMetadata, `Action failed: ${JSON.stringify(e)}`);
68
+ localResponse.allowed = false;
69
+ localResponse.status = {
70
+ code: 500,
71
+ message: `Action failed with error: ${JSON.stringify(e)}`,
72
+ };
73
+ return [localResponse];
74
+ }
75
+ response.push(localResponse);
76
+ }
77
+ }
78
+
79
+ return response;
80
+ }