pepr 0.24.0 → 0.25.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.
- package/dist/cli.js +216 -144
- package/dist/controller.js +1 -1
- package/dist/lib/assets/pods.d.ts +4 -0
- package/dist/lib/assets/pods.d.ts.map +1 -1
- package/dist/lib/capability.d.ts.map +1 -1
- package/dist/lib/helpers.d.ts +2 -0
- package/dist/lib/helpers.d.ts.map +1 -1
- package/dist/lib/module.d.ts +2 -0
- package/dist/lib/module.d.ts.map +1 -1
- package/dist/lib/queue.d.ts +18 -0
- package/dist/lib/queue.d.ts.map +1 -0
- package/dist/lib/types.d.ts +13 -0
- package/dist/lib/types.d.ts.map +1 -1
- package/dist/lib/watch-processor.d.ts.map +1 -1
- package/dist/lib.js +92 -12
- package/dist/lib.js.map +3 -3
- package/package.json +1 -1
- package/src/cli.ts +2 -1
- package/src/lib/assets/pods.ts +28 -13
- package/src/lib/assets/yaml.ts +2 -2
- package/src/lib/capability.ts +16 -3
- package/src/lib/helpers.ts +24 -0
- package/src/lib/module.ts +2 -0
- package/src/lib/queue.ts +77 -0
- package/src/lib/types.ts +14 -0
- package/src/lib/watch-processor.ts +38 -15
package/src/lib/assets/pods.ts
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
import { V1EnvVar } from "@kubernetes/client-node";
|
|
5
5
|
import { kind } from "kubernetes-fluent-client";
|
|
6
6
|
import { gzipSync } from "zlib";
|
|
7
|
-
|
|
7
|
+
import { secretOverLimit } from "../helpers";
|
|
8
8
|
import { Assets } from ".";
|
|
9
9
|
import { ModuleConfig } from "../module";
|
|
10
10
|
import { Binding } from "../types";
|
|
@@ -45,9 +45,13 @@ export function watcher(assets: Assets, hash: string) {
|
|
|
45
45
|
metadata: {
|
|
46
46
|
name: app,
|
|
47
47
|
namespace: "pepr-system",
|
|
48
|
+
annotations: {
|
|
49
|
+
"pepr.dev/description": config.description || "",
|
|
50
|
+
},
|
|
48
51
|
labels: {
|
|
49
52
|
app,
|
|
50
53
|
"pepr.dev/controller": "watcher",
|
|
54
|
+
"pepr.dev/uuid": config.uuid,
|
|
51
55
|
},
|
|
52
56
|
},
|
|
53
57
|
spec: {
|
|
@@ -168,9 +172,13 @@ export function deployment(assets: Assets, hash: string): kind.Deployment {
|
|
|
168
172
|
metadata: {
|
|
169
173
|
name,
|
|
170
174
|
namespace: "pepr-system",
|
|
175
|
+
annotations: {
|
|
176
|
+
"pepr.dev/description": config.description || "",
|
|
177
|
+
},
|
|
171
178
|
labels: {
|
|
172
179
|
app,
|
|
173
180
|
"pepr.dev/controller": "admission",
|
|
181
|
+
"pepr.dev/uuid": config.uuid,
|
|
174
182
|
},
|
|
175
183
|
},
|
|
176
184
|
spec: {
|
|
@@ -294,18 +302,25 @@ export function moduleSecret(name: string, data: Buffer, hash: string): kind.Sec
|
|
|
294
302
|
// Compress the data
|
|
295
303
|
const compressed = gzipSync(data);
|
|
296
304
|
const path = `module-${hash}.js.gz`;
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
305
|
+
const compressedData = compressed.toString("base64");
|
|
306
|
+
if (secretOverLimit(compressedData)) {
|
|
307
|
+
const error = new Error(`Module secret for ${name} is over the 1MB limit`);
|
|
308
|
+
console.error("Uncaught Exception:", error);
|
|
309
|
+
process.exit(1);
|
|
310
|
+
} else {
|
|
311
|
+
return {
|
|
312
|
+
apiVersion: "v1",
|
|
313
|
+
kind: "Secret",
|
|
314
|
+
metadata: {
|
|
315
|
+
name: `${name}-module`,
|
|
316
|
+
namespace: "pepr-system",
|
|
317
|
+
},
|
|
318
|
+
type: "Opaque",
|
|
319
|
+
data: {
|
|
320
|
+
[path]: compressed.toString("base64"),
|
|
321
|
+
},
|
|
322
|
+
};
|
|
323
|
+
}
|
|
309
324
|
}
|
|
310
325
|
|
|
311
326
|
function genEnv(config: ModuleConfig, watchMode = false): V1EnvVar[] {
|
package/src/lib/assets/yaml.ts
CHANGED
|
@@ -48,8 +48,8 @@ export async function allYaml(assets: Assets, rbacMode: string) {
|
|
|
48
48
|
// Generate a hash of the code
|
|
49
49
|
const hash = crypto.createHash("sha256").update(code).digest("hex");
|
|
50
50
|
|
|
51
|
-
const mutateWebhook = await webhookConfig(assets, "mutate");
|
|
52
|
-
const validateWebhook = await webhookConfig(assets, "validate");
|
|
51
|
+
const mutateWebhook = await webhookConfig(assets, "mutate", assets.config.webhookTimeout);
|
|
52
|
+
const validateWebhook = await webhookConfig(assets, "validate", assets.config.webhookTimeout);
|
|
53
53
|
const watchDeployment = watcher(assets, hash);
|
|
54
54
|
|
|
55
55
|
const resources = [
|
package/src/lib/capability.ts
CHANGED
|
@@ -203,7 +203,7 @@ export class Capability implements CapabilityExport {
|
|
|
203
203
|
|
|
204
204
|
const bindings = this.#bindings;
|
|
205
205
|
const prefix = `${this.#name}: ${model.name}`;
|
|
206
|
-
const commonChain = { WithLabel, WithAnnotation, Mutate, Validate, Watch };
|
|
206
|
+
const commonChain = { WithLabel, WithAnnotation, Mutate, Validate, Watch, Reconcile };
|
|
207
207
|
const isNotEmpty = (value: object) => Object.keys(value).length > 0;
|
|
208
208
|
const log = (message: string, cbString: string) => {
|
|
209
209
|
const filteredObj = pickBy(isNotEmpty, binding.filters);
|
|
@@ -226,7 +226,7 @@ export class Capability implements CapabilityExport {
|
|
|
226
226
|
});
|
|
227
227
|
}
|
|
228
228
|
|
|
229
|
-
return { Watch };
|
|
229
|
+
return { Watch, Reconcile };
|
|
230
230
|
}
|
|
231
231
|
|
|
232
232
|
function Mutate(mutateCallback: MutateAction<T>): MutateActionChain<T> {
|
|
@@ -243,7 +243,7 @@ export class Capability implements CapabilityExport {
|
|
|
243
243
|
}
|
|
244
244
|
|
|
245
245
|
// Now only allow adding actions to the same binding
|
|
246
|
-
return { Watch, Validate };
|
|
246
|
+
return { Watch, Validate, Reconcile };
|
|
247
247
|
}
|
|
248
248
|
|
|
249
249
|
function Watch(watchCallback: WatchAction<T>) {
|
|
@@ -258,6 +258,19 @@ export class Capability implements CapabilityExport {
|
|
|
258
258
|
}
|
|
259
259
|
}
|
|
260
260
|
|
|
261
|
+
function Reconcile(watchCallback: WatchAction<T>) {
|
|
262
|
+
if (registerWatch) {
|
|
263
|
+
log("Reconcile Action", watchCallback.toString());
|
|
264
|
+
|
|
265
|
+
bindings.push({
|
|
266
|
+
...binding,
|
|
267
|
+
isWatch: true,
|
|
268
|
+
isQueue: true,
|
|
269
|
+
watchCallback,
|
|
270
|
+
});
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
|
|
261
274
|
function InNamespace(...namespaces: string[]): BindingWithName<T> {
|
|
262
275
|
Log.debug(`Add namespaces filter ${namespaces}`, prefix);
|
|
263
276
|
binding.filters.namespaces.push(...namespaces);
|
package/src/lib/helpers.ts
CHANGED
|
@@ -5,6 +5,7 @@ import { K8s, kind } from "kubernetes-fluent-client";
|
|
|
5
5
|
import Log from "./logger";
|
|
6
6
|
import { CapabilityExport } from "./types";
|
|
7
7
|
import { promises as fs } from "fs";
|
|
8
|
+
import commander from "commander";
|
|
8
9
|
|
|
9
10
|
type RBACMap = {
|
|
10
11
|
[key: string]: {
|
|
@@ -161,3 +162,26 @@ export async function namespaceDeploymentsReady(namespace: string = "pepr-system
|
|
|
161
162
|
}
|
|
162
163
|
Log.info(`All ${namespace} deployments are ready`);
|
|
163
164
|
}
|
|
165
|
+
|
|
166
|
+
// check if secret is over the size limit
|
|
167
|
+
export function secretOverLimit(str: string): boolean {
|
|
168
|
+
const encoder = new TextEncoder();
|
|
169
|
+
const encoded = encoder.encode(str);
|
|
170
|
+
const sizeInBytes = encoded.length;
|
|
171
|
+
const oneMiBInBytes = 1048576;
|
|
172
|
+
return sizeInBytes > oneMiBInBytes;
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
/* eslint-disable @typescript-eslint/no-unused-vars */
|
|
176
|
+
export const parseTimeout = (value: string, previous: unknown): number => {
|
|
177
|
+
const parsedValue = parseInt(value, 10);
|
|
178
|
+
const floatValue = parseFloat(value);
|
|
179
|
+
if (isNaN(parsedValue)) {
|
|
180
|
+
throw new commander.InvalidArgumentError("Not a number.");
|
|
181
|
+
} else if (parsedValue !== floatValue) {
|
|
182
|
+
throw new commander.InvalidArgumentError("Value must be an integer.");
|
|
183
|
+
} else if (parsedValue < 1 || parsedValue > 30) {
|
|
184
|
+
throw new commander.InvalidArgumentError("Number must be between 1 and 30.");
|
|
185
|
+
}
|
|
186
|
+
return parsedValue;
|
|
187
|
+
};
|
package/src/lib/module.ts
CHANGED
|
@@ -23,6 +23,8 @@ export type ModuleConfig = {
|
|
|
23
23
|
uuid: string;
|
|
24
24
|
/** A description of the Pepr module and what it does. */
|
|
25
25
|
description?: string;
|
|
26
|
+
/** The webhookTimeout */
|
|
27
|
+
webhookTimeout?: number;
|
|
26
28
|
/** Reject K8s resource AdmissionRequests on error. */
|
|
27
29
|
onError?: string;
|
|
28
30
|
/** Configure global exclusions that will never be processed by Pepr. */
|
package/src/lib/queue.ts
ADDED
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
// SPDX-License-Identifier: Apache-2.0
|
|
2
|
+
// SPDX-FileCopyrightText: 2023-Present The Pepr Authors
|
|
3
|
+
import { KubernetesObject } from "@kubernetes/client-node";
|
|
4
|
+
import Log from "./logger";
|
|
5
|
+
|
|
6
|
+
type QueueItem<K extends KubernetesObject> = {
|
|
7
|
+
item: K;
|
|
8
|
+
resolve: (value: void | PromiseLike<void>) => void;
|
|
9
|
+
reject: (reason?: string) => void;
|
|
10
|
+
};
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Queue is a FIFO queue for reconciling
|
|
14
|
+
*/
|
|
15
|
+
export class Queue<K extends KubernetesObject> {
|
|
16
|
+
#queue: QueueItem<K>[] = [];
|
|
17
|
+
#pendingPromise = false;
|
|
18
|
+
#reconcile?: (...args: unknown[]) => Promise<void>;
|
|
19
|
+
|
|
20
|
+
constructor() {
|
|
21
|
+
this.#reconcile = async () => await new Promise(resolve => resolve());
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
setReconcile(reconcile: (...args: unknown[]) => Promise<void>) {
|
|
25
|
+
this.#reconcile = reconcile;
|
|
26
|
+
}
|
|
27
|
+
/**
|
|
28
|
+
* Enqueue adds an item to the queue and returns a promise that resolves when the item is
|
|
29
|
+
* reconciled.
|
|
30
|
+
*
|
|
31
|
+
* @param item The object to reconcile
|
|
32
|
+
* @returns A promise that resolves when the object is reconciled
|
|
33
|
+
*/
|
|
34
|
+
enqueue(item: K) {
|
|
35
|
+
Log.debug(`Enqueueing ${item.metadata!.namespace}/${item.metadata!.name}`);
|
|
36
|
+
return new Promise<void>((resolve, reject) => {
|
|
37
|
+
this.#queue.push({ item, resolve, reject });
|
|
38
|
+
return this.#dequeue();
|
|
39
|
+
});
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Dequeue reconciles the next item in the queue
|
|
44
|
+
*
|
|
45
|
+
* @returns A promise that resolves when the webapp is reconciled
|
|
46
|
+
*/
|
|
47
|
+
async #dequeue() {
|
|
48
|
+
// If there is a pending promise, do nothing
|
|
49
|
+
if (this.#pendingPromise) return false;
|
|
50
|
+
|
|
51
|
+
// Take the next element from the queue
|
|
52
|
+
const element = this.#queue.shift();
|
|
53
|
+
|
|
54
|
+
// If there is no element, do nothing
|
|
55
|
+
if (!element) return false;
|
|
56
|
+
|
|
57
|
+
try {
|
|
58
|
+
// Set the pending promise flag to avoid concurrent reconciliations
|
|
59
|
+
this.#pendingPromise = true;
|
|
60
|
+
|
|
61
|
+
// Reconcile the webapp
|
|
62
|
+
if (this.#reconcile) {
|
|
63
|
+
await this.#reconcile(element.item);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
element.resolve();
|
|
67
|
+
} catch (e) {
|
|
68
|
+
element.reject(e);
|
|
69
|
+
} finally {
|
|
70
|
+
// Reset the pending promise flag
|
|
71
|
+
this.#pendingPromise = false;
|
|
72
|
+
|
|
73
|
+
// After the element is reconciled, dequeue the next element
|
|
74
|
+
await this.#dequeue();
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
}
|
package/src/lib/types.ts
CHANGED
|
@@ -72,6 +72,7 @@ export type Binding = {
|
|
|
72
72
|
isMutate?: boolean;
|
|
73
73
|
isValidate?: boolean;
|
|
74
74
|
isWatch?: boolean;
|
|
75
|
+
isQueue?: boolean;
|
|
75
76
|
readonly model: GenericClass;
|
|
76
77
|
readonly kind: GroupVersionKind;
|
|
77
78
|
readonly filters: {
|
|
@@ -159,6 +160,19 @@ export type ValidateActionChain<T extends GenericClass> = {
|
|
|
159
160
|
* @returns
|
|
160
161
|
*/
|
|
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;
|
|
162
176
|
};
|
|
163
177
|
|
|
164
178
|
export type MutateActionChain<T extends GenericClass> = ValidateActionChain<T> & {
|
|
@@ -4,11 +4,13 @@
|
|
|
4
4
|
import { createHash } from "crypto";
|
|
5
5
|
import { K8s, WatchCfg, WatchEvent } from "kubernetes-fluent-client";
|
|
6
6
|
import { WatchPhase } from "kubernetes-fluent-client/dist/fluent/types";
|
|
7
|
-
|
|
7
|
+
import { Queue } from "./queue";
|
|
8
8
|
import { Capability } from "./capability";
|
|
9
9
|
import { PeprStore } from "./k8s";
|
|
10
10
|
import Log from "./logger";
|
|
11
11
|
import { Binding, Event } from "./types";
|
|
12
|
+
import { Watcher } from "kubernetes-fluent-client/dist/fluent/watch";
|
|
13
|
+
import { GenericClass } from "kubernetes-fluent-client";
|
|
12
14
|
|
|
13
15
|
// Track if the store has been updated
|
|
14
16
|
let storeUpdates = false;
|
|
@@ -80,21 +82,42 @@ async function runBinding(binding: Binding) {
|
|
|
80
82
|
retryDelaySec: 5,
|
|
81
83
|
};
|
|
82
84
|
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
85
|
+
let watcher: Watcher<GenericClass>;
|
|
86
|
+
if (binding.isQueue) {
|
|
87
|
+
const queue = new Queue();
|
|
88
|
+
// Watch the resource
|
|
89
|
+
watcher = K8s(binding.model, binding.filters).Watch(async (obj, type) => {
|
|
90
|
+
Log.debug(obj, `Watch event ${type} received`);
|
|
91
|
+
|
|
92
|
+
// If the type matches the phase, call the watch callback
|
|
93
|
+
if (phaseMatch.includes(type)) {
|
|
94
|
+
try {
|
|
95
|
+
queue.setReconcile(async () => await binding.watchCallback?.(obj, type));
|
|
96
|
+
// Enqueue the object for reconciliation through callback
|
|
97
|
+
await queue.enqueue(obj);
|
|
98
|
+
} catch (e) {
|
|
99
|
+
// Errors in the watch callback should not crash the controller
|
|
100
|
+
Log.error(e, "Error executing watch callback");
|
|
101
|
+
}
|
|
95
102
|
}
|
|
96
|
-
}
|
|
97
|
-
}
|
|
103
|
+
}, watchCfg);
|
|
104
|
+
} else {
|
|
105
|
+
// Watch the resource
|
|
106
|
+
watcher = K8s(binding.model, binding.filters).Watch(async (obj, type) => {
|
|
107
|
+
Log.debug(obj, `Watch event ${type} received`);
|
|
108
|
+
|
|
109
|
+
// If the type matches the phase, call the watch callback
|
|
110
|
+
if (phaseMatch.includes(type)) {
|
|
111
|
+
try {
|
|
112
|
+
// Perform the watch callback
|
|
113
|
+
await binding.watchCallback?.(obj, type);
|
|
114
|
+
} catch (e) {
|
|
115
|
+
// Errors in the watch callback should not crash the controller
|
|
116
|
+
Log.error(e, "Error executing watch callback");
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
}, watchCfg);
|
|
120
|
+
}
|
|
98
121
|
|
|
99
122
|
// Create a unique cache ID for this watch binding in case multiple bindings are watching the same resource
|
|
100
123
|
const cacheSuffix = createHash("sha224").update(binding.watchCallback!.toString()).digest("hex").substring(0, 5);
|