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/package.json CHANGED
@@ -9,7 +9,7 @@
9
9
  "engines": {
10
10
  "node": ">=18.0.0"
11
11
  },
12
- "version": "0.28.4",
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.18.3",
35
+ "express": "4.19.1",
36
36
  "fast-json-patch": "3.1.1",
37
- "kubernetes-fluent-client": "2.2.3",
37
+ "kubernetes-fluent-client": "2.3.0",
38
38
  "pino": "8.19.0",
39
- "pino-pretty": "10.3.1",
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.0",
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.5",
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",
@@ -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 || "debug",
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 }));
@@ -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: "debug" },
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: "debug" },
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.debug({ ...reqMetadata, res }, "Check response");
238
+ Log.info({ ...reqMetadata, res }, "Check response");
239
239
  });
240
240
 
241
241
  let kubeAdmissionResponse: ValidateResponse[] | MutateResponse | ResponseItem;
@@ -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 const filterMatcher = (
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
- export const addVerbIfNotExists = (verbs: string[], verb: string) => {
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 const createRBACMap = (capabilities: CapabilityExport[]): RBACMap => {
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?: (...args: unknown[]) => Promise<void>;
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: (...args: unknown[]) => Promise<void>) {
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) return false;
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) return false;
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 webapp
70
+ // Reconcile the element
62
71
  if (this.#reconcile) {
63
- await this.#reconcile(element.item);
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
- const store: Record<string, string> = {};
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
- // Map the event to the watch phase
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
- const watchCfg: WatchCfg = {
39
- retryMax: 5,
40
- retryDelaySec: 5,
41
- };
42
-
43
- let watcher: Watcher<GenericClass>;
44
- if (binding.isQueue) {
45
- const queue = new Queue();
46
- // Watch the resource
47
- watcher = K8s(binding.model, binding.filters).Watch(async (obj, type) => {
48
- Log.debug(obj, `Watch event ${type} received`);
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
- }, watchCfg);
67
- } else {
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
- // If the type matches the phase, call the watch callback
73
- if (phaseMatch.includes(type)) {
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
- // Create a unique cache ID for this watch binding in case multiple bindings are watching the same resource
90
- const cacheSuffix = createHash("sha224").update(binding.watchCallback!.toString()).digest("hex").substring(0, 5);
91
- const cacheID = [watcher.getCacheID(), cacheSuffix].join("-");
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");