pepr 0.28.4 → 0.28.6
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/SECURITY.md +18 -0
- package/dist/cli.js +8 -8
- package/dist/controller.js +1 -1
- package/dist/lib/helpers.d.ts +4 -5
- package/dist/lib/helpers.d.ts.map +1 -1
- package/dist/lib/queue.d.ts +3 -2
- package/dist/lib/queue.d.ts.map +1 -1
- package/dist/lib/watch-processor.d.ts +5 -0
- package/dist/lib/watch-processor.d.ts.map +1 -1
- package/dist/lib.js +101 -115
- package/dist/lib.js.map +4 -4
- package/package.json +6 -6
- package/src/lib/assets/pods.ts +1 -1
- package/src/lib/assets/yaml.ts +2 -2
- package/src/lib/controller/index.ts +1 -1
- package/src/lib/helpers.ts +10 -10
- package/src/lib/queue.ts +20 -8
- package/src/lib/watch-processor.ts +59 -74
package/package.json
CHANGED
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
"engines": {
|
|
10
10
|
"node": ">=18.0.0"
|
|
11
11
|
},
|
|
12
|
-
"version": "0.28.
|
|
12
|
+
"version": "0.28.6",
|
|
13
13
|
"main": "dist/lib.js",
|
|
14
14
|
"types": "dist/lib.d.ts",
|
|
15
15
|
"scripts": {
|
|
@@ -32,19 +32,19 @@
|
|
|
32
32
|
},
|
|
33
33
|
"dependencies": {
|
|
34
34
|
"@types/ramda": "0.29.11",
|
|
35
|
-
"express": "4.
|
|
35
|
+
"express": "4.19.1",
|
|
36
36
|
"fast-json-patch": "3.1.1",
|
|
37
|
-
"kubernetes-fluent-client": "2.
|
|
37
|
+
"kubernetes-fluent-client": "2.3.0",
|
|
38
38
|
"pino": "8.19.0",
|
|
39
|
-
"pino-pretty": "
|
|
39
|
+
"pino-pretty": "11.0.0",
|
|
40
40
|
"prom-client": "15.1.0",
|
|
41
41
|
"ramda": "0.29.1"
|
|
42
42
|
},
|
|
43
43
|
"devDependencies": {
|
|
44
|
-
"@commitlint/cli": "19.2.
|
|
44
|
+
"@commitlint/cli": "19.2.1",
|
|
45
45
|
"@commitlint/config-conventional": "19.1.0",
|
|
46
46
|
"@jest/globals": "29.7.0",
|
|
47
|
-
"@types/eslint": "8.56.
|
|
47
|
+
"@types/eslint": "8.56.6",
|
|
48
48
|
"@types/express": "4.17.21",
|
|
49
49
|
"@types/node": "18.x.x",
|
|
50
50
|
"@types/node-forge": "1.3.11",
|
package/src/lib/assets/pods.ts
CHANGED
|
@@ -344,7 +344,7 @@ function genEnv(config: ModuleConfig, watchMode = false): V1EnvVar[] {
|
|
|
344
344
|
const def = {
|
|
345
345
|
PEPR_WATCH_MODE: watchMode ? "true" : "false",
|
|
346
346
|
PEPR_PRETTY_LOG: "false",
|
|
347
|
-
LOG_LEVEL: config.logLevel || "
|
|
347
|
+
LOG_LEVEL: config.logLevel || "info",
|
|
348
348
|
};
|
|
349
349
|
const cfg = config.env || {};
|
|
350
350
|
const env = Object.entries({ ...def, ...cfg }).map(([name, value]) => ({ name, value }));
|
package/src/lib/assets/yaml.ts
CHANGED
|
@@ -32,7 +32,7 @@ export async function overridesFile({ hash, name, image, config, apiToken }: Ass
|
|
|
32
32
|
env: [
|
|
33
33
|
{ name: "PEPR_WATCH_MODE", value: "false" },
|
|
34
34
|
{ name: "PEPR_PRETTY_LOG", value: "false" },
|
|
35
|
-
{ name: "LOG_LEVEL", value: "
|
|
35
|
+
{ name: "LOG_LEVEL", value: "info" },
|
|
36
36
|
],
|
|
37
37
|
image,
|
|
38
38
|
annotations: {
|
|
@@ -77,7 +77,7 @@ export async function overridesFile({ hash, name, image, config, apiToken }: Ass
|
|
|
77
77
|
env: [
|
|
78
78
|
{ name: "PEPR_WATCH_MODE", value: "true" },
|
|
79
79
|
{ name: "PEPR_PRETTY_LOG", value: "false" },
|
|
80
|
-
{ name: "LOG_LEVEL", value: "
|
|
80
|
+
{ name: "LOG_LEVEL", value: "info" },
|
|
81
81
|
],
|
|
82
82
|
image,
|
|
83
83
|
annotations: {
|
|
@@ -235,7 +235,7 @@ export class Controller {
|
|
|
235
235
|
responseList.map(res => {
|
|
236
236
|
this.#afterHook && this.#afterHook(res);
|
|
237
237
|
// Log the response
|
|
238
|
-
Log.
|
|
238
|
+
Log.info({ ...reqMetadata, res }, "Check response");
|
|
239
239
|
});
|
|
240
240
|
|
|
241
241
|
let kubeAdmissionResponse: ValidateResponse[] | MutateResponse | ResponseItem;
|
package/src/lib/helpers.ts
CHANGED
|
@@ -1,11 +1,10 @@
|
|
|
1
1
|
// SPDX-License-Identifier: Apache-2.0
|
|
2
2
|
// SPDX-FileCopyrightText: 2023-Present The Pepr Authors
|
|
3
3
|
|
|
4
|
+
import { promises as fs } from "fs";
|
|
4
5
|
import { K8s, KubernetesObject, kind } from "kubernetes-fluent-client";
|
|
5
6
|
import Log from "./logger";
|
|
6
|
-
import { CapabilityExport } from "./types";
|
|
7
|
-
import { promises as fs } from "fs";
|
|
8
|
-
import { Binding } from "./types";
|
|
7
|
+
import { Binding, CapabilityExport } from "./types";
|
|
9
8
|
|
|
10
9
|
type RBACMap = {
|
|
11
10
|
[key: string]: {
|
|
@@ -47,11 +46,11 @@ export function checkOverlap(bindingFilters: Record<string, string>, objectFilte
|
|
|
47
46
|
/**
|
|
48
47
|
* Decide to run callback after the event comes back from API Server
|
|
49
48
|
**/
|
|
50
|
-
export
|
|
49
|
+
export function filterNoMatchReason(
|
|
51
50
|
binding: Partial<Binding>,
|
|
52
51
|
obj: Partial<KubernetesObject>,
|
|
53
52
|
capabilityNamespaces: string[],
|
|
54
|
-
): string
|
|
53
|
+
): string {
|
|
55
54
|
// binding kind is namespace with a InNamespace filter
|
|
56
55
|
if (binding.kind && binding.kind.kind === "Namespace" && binding.filters && binding.filters.namespaces.length !== 0) {
|
|
57
56
|
return `Ignoring Watch Callback: Cannot use a namespace filter in a namespace object.`;
|
|
@@ -116,14 +115,15 @@ export const filterMatcher = (
|
|
|
116
115
|
|
|
117
116
|
// no problems
|
|
118
117
|
return "";
|
|
119
|
-
}
|
|
120
|
-
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
export function addVerbIfNotExists(verbs: string[], verb: string) {
|
|
121
121
|
if (!verbs.includes(verb)) {
|
|
122
122
|
verbs.push(verb);
|
|
123
123
|
}
|
|
124
|
-
}
|
|
124
|
+
}
|
|
125
125
|
|
|
126
|
-
export
|
|
126
|
+
export function createRBACMap(capabilities: CapabilityExport[]): RBACMap {
|
|
127
127
|
return capabilities.reduce((acc: RBACMap, capability: CapabilityExport) => {
|
|
128
128
|
capability.bindings.forEach(binding => {
|
|
129
129
|
const key = `${binding.kind.group}/${binding.kind.version}/${binding.kind.kind}`;
|
|
@@ -148,7 +148,7 @@ export const createRBACMap = (capabilities: CapabilityExport[]): RBACMap => {
|
|
|
148
148
|
|
|
149
149
|
return acc;
|
|
150
150
|
}, {});
|
|
151
|
-
}
|
|
151
|
+
}
|
|
152
152
|
|
|
153
153
|
export async function createDirectoryIfNotExists(path: string) {
|
|
154
154
|
try {
|
package/src/lib/queue.ts
CHANGED
|
@@ -1,10 +1,12 @@
|
|
|
1
1
|
// SPDX-License-Identifier: Apache-2.0
|
|
2
2
|
// SPDX-FileCopyrightText: 2023-Present The Pepr Authors
|
|
3
3
|
import { KubernetesObject } from "@kubernetes/client-node";
|
|
4
|
+
import { WatchPhase } from "kubernetes-fluent-client/dist/fluent/types";
|
|
4
5
|
import Log from "./logger";
|
|
5
6
|
|
|
6
7
|
type QueueItem<K extends KubernetesObject> = {
|
|
7
8
|
item: K;
|
|
9
|
+
type: WatchPhase;
|
|
8
10
|
resolve: (value: void | PromiseLike<void>) => void;
|
|
9
11
|
reject: (reason?: string) => void;
|
|
10
12
|
};
|
|
@@ -15,15 +17,16 @@ type QueueItem<K extends KubernetesObject> = {
|
|
|
15
17
|
export class Queue<K extends KubernetesObject> {
|
|
16
18
|
#queue: QueueItem<K>[] = [];
|
|
17
19
|
#pendingPromise = false;
|
|
18
|
-
#reconcile?: (
|
|
20
|
+
#reconcile?: (obj: KubernetesObject, type: WatchPhase) => Promise<void>;
|
|
19
21
|
|
|
20
22
|
constructor() {
|
|
21
23
|
this.#reconcile = async () => await new Promise(resolve => resolve());
|
|
22
24
|
}
|
|
23
25
|
|
|
24
|
-
setReconcile(reconcile: (
|
|
26
|
+
setReconcile(reconcile: (obj: KubernetesObject, type: WatchPhase) => Promise<void>) {
|
|
25
27
|
this.#reconcile = reconcile;
|
|
26
28
|
}
|
|
29
|
+
|
|
27
30
|
/**
|
|
28
31
|
* Enqueue adds an item to the queue and returns a promise that resolves when the item is
|
|
29
32
|
* reconciled.
|
|
@@ -31,10 +34,10 @@ export class Queue<K extends KubernetesObject> {
|
|
|
31
34
|
* @param item The object to reconcile
|
|
32
35
|
* @returns A promise that resolves when the object is reconciled
|
|
33
36
|
*/
|
|
34
|
-
enqueue(item: K) {
|
|
37
|
+
enqueue(item: K, type: WatchPhase) {
|
|
35
38
|
Log.debug(`Enqueueing ${item.metadata!.namespace}/${item.metadata!.name}`);
|
|
36
39
|
return new Promise<void>((resolve, reject) => {
|
|
37
|
-
this.#queue.push({ item, resolve, reject });
|
|
40
|
+
this.#queue.push({ item, type, resolve, reject });
|
|
38
41
|
return this.#dequeue();
|
|
39
42
|
});
|
|
40
43
|
}
|
|
@@ -46,28 +49,37 @@ export class Queue<K extends KubernetesObject> {
|
|
|
46
49
|
*/
|
|
47
50
|
async #dequeue() {
|
|
48
51
|
// If there is a pending promise, do nothing
|
|
49
|
-
if (this.#pendingPromise)
|
|
52
|
+
if (this.#pendingPromise) {
|
|
53
|
+
Log.debug("Pending promise, not dequeuing");
|
|
54
|
+
return false;
|
|
55
|
+
}
|
|
50
56
|
|
|
51
57
|
// Take the next element from the queue
|
|
52
58
|
const element = this.#queue.shift();
|
|
53
59
|
|
|
54
60
|
// If there is no element, do nothing
|
|
55
|
-
if (!element)
|
|
61
|
+
if (!element) {
|
|
62
|
+
Log.debug("No element, not dequeuing");
|
|
63
|
+
return false;
|
|
64
|
+
}
|
|
56
65
|
|
|
57
66
|
try {
|
|
58
67
|
// Set the pending promise flag to avoid concurrent reconciliations
|
|
59
68
|
this.#pendingPromise = true;
|
|
60
69
|
|
|
61
|
-
// Reconcile the
|
|
70
|
+
// Reconcile the element
|
|
62
71
|
if (this.#reconcile) {
|
|
63
|
-
|
|
72
|
+
Log.debug(`Reconciling ${element.item.metadata!.name}`);
|
|
73
|
+
await this.#reconcile(element.item, element.type);
|
|
64
74
|
}
|
|
65
75
|
|
|
66
76
|
element.resolve();
|
|
67
77
|
} catch (e) {
|
|
78
|
+
Log.debug(`Error reconciling ${element.item.metadata!.name}`, { error: e });
|
|
68
79
|
element.reject(e);
|
|
69
80
|
} finally {
|
|
70
81
|
// Reset the pending promise flag
|
|
82
|
+
Log.debug("Resetting pending promise and dequeuing");
|
|
71
83
|
this.#pendingPromise = false;
|
|
72
84
|
|
|
73
85
|
// After the element is reconciled, dequeue the next element
|
|
@@ -1,19 +1,33 @@
|
|
|
1
1
|
// SPDX-License-Identifier: Apache-2.0
|
|
2
2
|
// SPDX-FileCopyrightText: 2023-Present The Pepr Authors
|
|
3
|
-
|
|
4
|
-
import { createHash } from "crypto";
|
|
5
|
-
import { K8s, WatchCfg, WatchEvent } from "kubernetes-fluent-client";
|
|
3
|
+
import { K8s, KubernetesObject, WatchCfg, WatchEvent } from "kubernetes-fluent-client";
|
|
6
4
|
import { WatchPhase } from "kubernetes-fluent-client/dist/fluent/types";
|
|
7
|
-
import { Queue } from "./queue";
|
|
8
5
|
import { Capability } from "./capability";
|
|
6
|
+
import { filterNoMatchReason } from "./helpers";
|
|
9
7
|
import Log from "./logger";
|
|
8
|
+
import { Queue } from "./queue";
|
|
10
9
|
import { Binding, Event } from "./types";
|
|
11
|
-
import { Watcher } from "kubernetes-fluent-client/dist/fluent/watch";
|
|
12
|
-
import { GenericClass } from "kubernetes-fluent-client";
|
|
13
|
-
import { filterMatcher } from "./helpers";
|
|
14
10
|
|
|
15
|
-
|
|
11
|
+
// Watch configuration
|
|
12
|
+
const watchCfg: WatchCfg = {
|
|
13
|
+
retryMax: 5,
|
|
14
|
+
retryDelaySec: 5,
|
|
15
|
+
};
|
|
16
16
|
|
|
17
|
+
// Map the event to the watch phase
|
|
18
|
+
const eventToPhaseMap = {
|
|
19
|
+
[Event.Create]: [WatchPhase.Added],
|
|
20
|
+
[Event.Update]: [WatchPhase.Modified],
|
|
21
|
+
[Event.CreateOrUpdate]: [WatchPhase.Added, WatchPhase.Modified],
|
|
22
|
+
[Event.Delete]: [WatchPhase.Deleted],
|
|
23
|
+
[Event.Any]: [WatchPhase.Added, WatchPhase.Modified, WatchPhase.Deleted],
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Entrypoint for setting up watches for all capabilities
|
|
28
|
+
*
|
|
29
|
+
* @param capabilities The capabilities to load watches for
|
|
30
|
+
*/
|
|
17
31
|
export function setupWatch(capabilities: Capability[]) {
|
|
18
32
|
capabilities.map(capability =>
|
|
19
33
|
capability.bindings
|
|
@@ -22,73 +36,50 @@ export function setupWatch(capabilities: Capability[]) {
|
|
|
22
36
|
);
|
|
23
37
|
}
|
|
24
38
|
|
|
39
|
+
/**
|
|
40
|
+
* Setup a watch for a binding
|
|
41
|
+
*
|
|
42
|
+
* @param binding the binding to watch
|
|
43
|
+
* @param capabilityNamespaces list of namespaces to filter on
|
|
44
|
+
*/
|
|
25
45
|
async function runBinding(binding: Binding, capabilityNamespaces: string[]) {
|
|
26
|
-
//
|
|
27
|
-
const eventToPhaseMap = {
|
|
28
|
-
[Event.Create]: [WatchPhase.Added],
|
|
29
|
-
[Event.Update]: [WatchPhase.Modified],
|
|
30
|
-
[Event.CreateOrUpdate]: [WatchPhase.Added, WatchPhase.Modified],
|
|
31
|
-
[Event.Delete]: [WatchPhase.Deleted],
|
|
32
|
-
[Event.Any]: [WatchPhase.Added, WatchPhase.Modified, WatchPhase.Deleted],
|
|
33
|
-
};
|
|
34
|
-
|
|
35
|
-
// Get the phases to match, default to any
|
|
46
|
+
// Get the phases to match, fallback to any
|
|
36
47
|
const phaseMatch: WatchPhase[] = eventToPhaseMap[binding.event] || eventToPhaseMap[Event.Any];
|
|
37
48
|
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
// If the type matches the phase, call the watch callback
|
|
51
|
-
if (phaseMatch.includes(type)) {
|
|
52
|
-
try {
|
|
53
|
-
const filterMatch = filterMatcher(binding, obj, capabilityNamespaces);
|
|
54
|
-
if (filterMatch === "") {
|
|
55
|
-
queue.setReconcile(async () => await binding.watchCallback?.(obj, type));
|
|
56
|
-
// Enqueue the object for reconciliation through callback
|
|
57
|
-
await queue.enqueue(obj);
|
|
58
|
-
} else {
|
|
59
|
-
Log.debug(filterMatch);
|
|
60
|
-
}
|
|
61
|
-
} catch (e) {
|
|
62
|
-
// Errors in the watch callback should not crash the controller
|
|
63
|
-
Log.error(e, "Error executing watch callback");
|
|
49
|
+
// The watch callback is run when an object is received or dequeued
|
|
50
|
+
const watchCallback = async (obj: KubernetesObject, type: WatchPhase) => {
|
|
51
|
+
// First, filter the object based on the phase
|
|
52
|
+
if (phaseMatch.includes(type)) {
|
|
53
|
+
try {
|
|
54
|
+
// Then, check if the object matches the filter
|
|
55
|
+
const filterMatch = filterNoMatchReason(binding, obj, capabilityNamespaces);
|
|
56
|
+
if (filterMatch === "") {
|
|
57
|
+
await binding.watchCallback?.(obj, type);
|
|
58
|
+
} else {
|
|
59
|
+
Log.debug(filterMatch);
|
|
64
60
|
}
|
|
61
|
+
} catch (e) {
|
|
62
|
+
// Errors in the watch callback should not crash the controller
|
|
63
|
+
Log.error(e, "Error executing watch callback");
|
|
65
64
|
}
|
|
66
|
-
}
|
|
67
|
-
}
|
|
68
|
-
// Watch the resource
|
|
69
|
-
watcher = K8s(binding.model, binding.filters).Watch(async (obj, type) => {
|
|
70
|
-
Log.debug(obj, `Watch event ${type} received`);
|
|
65
|
+
}
|
|
66
|
+
};
|
|
71
67
|
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
try {
|
|
75
|
-
const filterMatch = filterMatcher(binding, obj, capabilityNamespaces);
|
|
76
|
-
if (filterMatch === "") {
|
|
77
|
-
await binding.watchCallback?.(obj, type);
|
|
78
|
-
} else {
|
|
79
|
-
Log.debug(filterMatch);
|
|
80
|
-
}
|
|
81
|
-
} catch (e) {
|
|
82
|
-
// Errors in the watch callback should not crash the controller
|
|
83
|
-
Log.error(e, "Error executing watch callback");
|
|
84
|
-
}
|
|
85
|
-
}
|
|
86
|
-
}, watchCfg);
|
|
87
|
-
}
|
|
68
|
+
const queue = new Queue();
|
|
69
|
+
queue.setReconcile(watchCallback);
|
|
88
70
|
|
|
89
|
-
//
|
|
90
|
-
const
|
|
91
|
-
|
|
71
|
+
// Setup the resource watch
|
|
72
|
+
const watcher = K8s(binding.model, binding.filters).Watch(async (obj, type) => {
|
|
73
|
+
Log.debug(obj, `Watch event ${type} received`);
|
|
74
|
+
|
|
75
|
+
// If the binding is a queue, enqueue the object
|
|
76
|
+
if (binding.isQueue) {
|
|
77
|
+
await queue.enqueue(obj, type);
|
|
78
|
+
} else {
|
|
79
|
+
// Otherwise, run the watch callback directly
|
|
80
|
+
await watchCallback(obj, type);
|
|
81
|
+
}
|
|
82
|
+
}, watchCfg);
|
|
92
83
|
|
|
93
84
|
// If failure continues, log and exit
|
|
94
85
|
watcher.events.on(WatchEvent.GIVE_UP, err => {
|
|
@@ -98,12 +89,6 @@ async function runBinding(binding: Binding, capabilityNamespaces: string[]) {
|
|
|
98
89
|
|
|
99
90
|
// Start the watch
|
|
100
91
|
try {
|
|
101
|
-
const resourceVersion = store[cacheID];
|
|
102
|
-
if (resourceVersion) {
|
|
103
|
-
Log.debug(`Starting watch ${binding.model.name} from version ${resourceVersion}`);
|
|
104
|
-
watcher.resourceVersion = resourceVersion;
|
|
105
|
-
}
|
|
106
|
-
|
|
107
92
|
await watcher.start();
|
|
108
93
|
} catch (err) {
|
|
109
94
|
Log.error(err, "Error starting watch");
|