pepr 0.27.0 → 0.28.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/package.json CHANGED
@@ -9,7 +9,7 @@
9
9
  "engines": {
10
10
  "node": ">=18.0.0"
11
11
  },
12
- "version": "0.27.0",
12
+ "version": "0.28.0",
13
13
  "main": "dist/lib.js",
14
14
  "types": "dist/lib.d.ts",
15
15
  "scripts": {
@@ -19,7 +19,7 @@
19
19
  "test": "npm run test:unit && npm run test:journey",
20
20
  "test:unit": "npm run gen-data-json && jest src --coverage --detectOpenHandles --coverageDirectory=./coverage",
21
21
  "test:journey": "npm run test:journey:k3d && npm run test:journey:build && npm run test:journey:image && npm run test:journey:run",
22
- "test:journey:prep": "git clone https://github.com/defenseunicorns/pepr-upgrade-test.git",
22
+ "test:journey:prep": "if [ ! -d ./pepr-upgrade-test ]; then git clone https://github.com/defenseunicorns/pepr-upgrade-test.git ; fi",
23
23
  "test:journey-wasm": "npm run test:journey:k3d && npm run test:journey:build && npm run test:journey:image && npm run test:journey:run-wasm",
24
24
  "test:journey:k3d": "k3d cluster delete pepr-dev && k3d cluster create pepr-dev --k3s-arg '--debug@server:0' --wait && kubectl rollout status deployment -n kube-system",
25
25
  "test:journey:build": "npm run build && npm pack",
@@ -31,20 +31,20 @@
31
31
  "format:fix": "eslint src --fix && prettier src --write"
32
32
  },
33
33
  "dependencies": {
34
- "@types/ramda": "0.29.10",
35
- "express": "4.18.2",
34
+ "@types/ramda": "0.29.11",
35
+ "express": "4.18.3",
36
36
  "fast-json-patch": "3.1.1",
37
- "kubernetes-fluent-client": "2.2.1",
37
+ "kubernetes-fluent-client": "2.2.2",
38
38
  "pino": "8.19.0",
39
39
  "pino-pretty": "10.3.1",
40
40
  "prom-client": "15.1.0",
41
41
  "ramda": "0.29.1"
42
42
  },
43
43
  "devDependencies": {
44
- "@commitlint/cli": "19.0.1",
45
- "@commitlint/config-conventional": "19.0.0",
44
+ "@commitlint/cli": "19.0.3",
45
+ "@commitlint/config-conventional": "19.0.3",
46
46
  "@jest/globals": "29.7.0",
47
- "@types/eslint": "8.56.4",
47
+ "@types/eslint": "8.56.5",
48
48
  "@types/express": "4.17.21",
49
49
  "@types/node": "18.x.x",
50
50
  "@types/node-forge": "1.3.11",
package/src/cli.ts CHANGED
@@ -14,6 +14,7 @@ import uuid from "./cli/uuid";
14
14
  import { version } from "./cli/init/templates";
15
15
  import { RootCmd } from "./cli/root";
16
16
  import update from "./cli/update";
17
+ import kfc from "./cli/kfc";
17
18
 
18
19
  if (process.env.npm_lifecycle_event !== "npx") {
19
20
  console.warn("Pepr should be run via `npx pepr <command>` instead of `pepr <command>`.");
@@ -43,4 +44,5 @@ update(program);
43
44
  format(program);
44
45
  monitor(program);
45
46
  uuid(program);
47
+ kfc(program);
46
48
  program.parse();
@@ -1,11 +1,11 @@
1
1
  // SPDX-License-Identifier: Apache-2.0
2
2
  // SPDX-FileCopyrightText: 2023-Present The Pepr Authors
3
3
 
4
- import { K8s, kind } from "kubernetes-fluent-client";
4
+ import { K8s, KubernetesObject, 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
+ import { Binding } from "./types";
9
9
 
10
10
  type RBACMap = {
11
11
  [key: string]: {
@@ -14,6 +14,96 @@ type RBACMap = {
14
14
  };
15
15
  };
16
16
 
17
+ // check for overlap with labels and annotations between bindings and kubernetes objects
18
+ export function checkOverlap(record1: Record<string, string>, record2: Record<string, string>) {
19
+ if (Object.keys(record1).length === 0) {
20
+ return true;
21
+ }
22
+ for (const key in record1) {
23
+ if (
24
+ Object.prototype.hasOwnProperty.call(record1, key) &&
25
+ Object.prototype.hasOwnProperty.call(record2, key) &&
26
+ record1[key] === record2[key]
27
+ ) {
28
+ return true;
29
+ }
30
+ }
31
+ return false;
32
+ }
33
+
34
+ /**
35
+ * Decide to run callback after the event comes back from API Server
36
+ **/
37
+ export const filterMatcher = (
38
+ binding: Partial<Binding>,
39
+ obj: Partial<KubernetesObject>,
40
+ capabilityNamespaces: string[],
41
+ ): string => {
42
+ // binding kind is namespace with a InNamespace filter
43
+ if (binding.kind && binding.kind.kind === "Namespace" && binding.filters && binding.filters.namespaces.length !== 0) {
44
+ return `Ignoring Watch Callback: Cannot use a namespace filter in a namespace object.`;
45
+ }
46
+
47
+ if (typeof obj === "object" && obj !== null && "metadata" in obj && obj.metadata !== undefined && binding.filters) {
48
+ // binding labels and object labels dont match
49
+ if (obj.metadata.labels && !checkOverlap(binding.filters.labels, obj.metadata.labels)) {
50
+ return `Ignoring Watch Callback: No overlap between binding and object labels. Binding labels ${JSON.stringify(
51
+ binding.filters.labels,
52
+ )}, Object Labels ${JSON.stringify(obj.metadata.labels)}.`;
53
+ }
54
+
55
+ // binding annotations and object annotations dont match
56
+ if (obj.metadata.annotations && !checkOverlap(binding.filters.annotations, obj.metadata.annotations)) {
57
+ return `Ignoring Watch Callback: No overlap between binding and object annotations. Binding annotations ${JSON.stringify(
58
+ binding.filters.annotations,
59
+ )}, Object annotations ${JSON.stringify(obj.metadata.annotations)}.`;
60
+ }
61
+ }
62
+
63
+ // Check object is in the capability namespace
64
+ if (
65
+ Array.isArray(capabilityNamespaces) &&
66
+ capabilityNamespaces.length > 0 &&
67
+ obj.metadata &&
68
+ obj.metadata.namespace &&
69
+ !capabilityNamespaces.includes(obj.metadata.namespace)
70
+ ) {
71
+ return `Ignoring Watch Callback: Object is not in the capability namespace. Capability namespaces: ${capabilityNamespaces.join(
72
+ ", ",
73
+ )}, Object namespace: ${obj.metadata.namespace}.`;
74
+ }
75
+
76
+ // chceck every filter namespace is a capability namespace
77
+ if (
78
+ Array.isArray(capabilityNamespaces) &&
79
+ capabilityNamespaces.length > 0 &&
80
+ binding.filters &&
81
+ Array.isArray(binding.filters.namespaces) &&
82
+ binding.filters.namespaces.length > 0 &&
83
+ !binding.filters.namespaces.every(ns => capabilityNamespaces.includes(ns))
84
+ ) {
85
+ return `Ignoring Watch Callback: Binding namespace is not part of capability namespaces. Capability namespaces: ${capabilityNamespaces.join(
86
+ ", ",
87
+ )}, Binding namespaces: ${binding.filters.namespaces.join(", ")}.`;
88
+ }
89
+
90
+ // filter namespace is not the same of object namespace
91
+ if (
92
+ binding.filters &&
93
+ Array.isArray(binding.filters.namespaces) &&
94
+ binding.filters.namespaces.length > 0 &&
95
+ obj.metadata &&
96
+ obj.metadata.namespace &&
97
+ !binding.filters.namespaces.includes(obj.metadata.namespace)
98
+ ) {
99
+ return `Ignoring Watch Callback: Binding namespace and object namespace are not the same. Binding namespaces: ${binding.filters.namespaces.join(
100
+ ", ",
101
+ )}, Object namespace: ${obj.metadata.namespace}.`;
102
+ }
103
+
104
+ // no problems
105
+ return "";
106
+ };
17
107
  export const addVerbIfNotExists = (verbs: string[], verb: string) => {
18
108
  if (!verbs.includes(verb)) {
19
109
  verbs.push(verb);
@@ -177,11 +267,11 @@ export const parseTimeout = (value: string, previous: unknown): number => {
177
267
  const parsedValue = parseInt(value, 10);
178
268
  const floatValue = parseFloat(value);
179
269
  if (isNaN(parsedValue)) {
180
- throw new commander.InvalidArgumentError("Not a number.");
270
+ throw new Error("Not a number.");
181
271
  } else if (parsedValue !== floatValue) {
182
- throw new commander.InvalidArgumentError("Value must be an integer.");
272
+ throw new Error("Value must be an integer.");
183
273
  } else if (parsedValue < 1 || parsedValue > 30) {
184
- throw new commander.InvalidArgumentError("Number must be between 1 and 30.");
274
+ throw new Error("Number must be between 1 and 30.");
185
275
  }
186
276
  return parsedValue;
187
277
  };
package/src/lib/k8s.ts CHANGED
@@ -166,15 +166,4 @@ export type WebhookIgnore = {
166
166
  * Note: `kube-system` and `pepr-system` are always ignored.
167
167
  */
168
168
  namespaces?: string[];
169
- /**
170
- * List of Kubernetes labels to always ignore.
171
- * Any resources with these labels will be ignored by Pepr.
172
- *
173
- * The example below will ignore any resources with the label `my-label=ulta-secret`:
174
- * ```
175
- * alwaysIgnore:
176
- * labels: [{ "my-label": "ultra-secret" }]
177
- * ```
178
- */
179
- labels?: Record<string, string>[];
180
169
  };
package/src/lib/module.ts CHANGED
@@ -15,8 +15,6 @@ export interface CustomLabels {
15
15
  }
16
16
  /** Global configuration for the Pepr runtime. */
17
17
  export type ModuleConfig = {
18
- /** The user-defined name for the module */
19
- name: string;
20
18
  /** The Pepr version this module uses */
21
19
  peprVersion?: string;
22
20
  /** The user-defined version of the module */
@@ -11,6 +11,7 @@ import Log from "./logger";
11
11
  import { Binding, Event } from "./types";
12
12
  import { Watcher } from "kubernetes-fluent-client/dist/fluent/watch";
13
13
  import { GenericClass } from "kubernetes-fluent-client";
14
+ import { filterMatcher } from "./helpers";
14
15
 
15
16
  // Track if the store has been updated
16
17
  let storeUpdates = false;
@@ -58,13 +59,14 @@ export async function setupStore(uuid: string) {
58
59
  export async function setupWatch(uuid: string, capabilities: Capability[]) {
59
60
  await setupStore(uuid);
60
61
 
61
- capabilities
62
- .flatMap(c => c.bindings)
63
- .filter(binding => binding.isWatch)
64
- .forEach(runBinding);
62
+ capabilities.map(capability =>
63
+ capability.bindings
64
+ .filter(binding => binding.isWatch)
65
+ .forEach(bindingElement => runBinding(bindingElement, capability.namespaces)),
66
+ );
65
67
  }
66
68
 
67
- async function runBinding(binding: Binding) {
69
+ async function runBinding(binding: Binding, capabilityNamespaces: string[]) {
68
70
  // Map the event to the watch phase
69
71
  const eventToPhaseMap = {
70
72
  [Event.Create]: [WatchPhase.Added],
@@ -92,9 +94,14 @@ async function runBinding(binding: Binding) {
92
94
  // If the type matches the phase, call the watch callback
93
95
  if (phaseMatch.includes(type)) {
94
96
  try {
95
- queue.setReconcile(async () => await binding.watchCallback?.(obj, type));
96
- // Enqueue the object for reconciliation through callback
97
- await queue.enqueue(obj);
97
+ const filterMatch = filterMatcher(binding, obj, capabilityNamespaces);
98
+ if (filterMatch === "") {
99
+ queue.setReconcile(async () => await binding.watchCallback?.(obj, type));
100
+ // Enqueue the object for reconciliation through callback
101
+ await queue.enqueue(obj);
102
+ } else {
103
+ Log.debug(filterMatch);
104
+ }
98
105
  } catch (e) {
99
106
  // Errors in the watch callback should not crash the controller
100
107
  Log.error(e, "Error executing watch callback");
@@ -109,8 +116,12 @@ async function runBinding(binding: Binding) {
109
116
  // If the type matches the phase, call the watch callback
110
117
  if (phaseMatch.includes(type)) {
111
118
  try {
112
- // Perform the watch callback
113
- await binding.watchCallback?.(obj, type);
119
+ const filterMatch = filterMatcher(binding, obj, capabilityNamespaces);
120
+ if (filterMatch === "") {
121
+ await binding.watchCallback?.(obj, type);
122
+ } else {
123
+ Log.debug(filterMatch);
124
+ }
114
125
  } catch (e) {
115
126
  // Errors in the watch callback should not crash the controller
116
127
  Log.error(e, "Error executing watch callback");
@@ -13,8 +13,7 @@
13
13
  }
14
14
  },
15
15
  "alwaysIgnore": {
16
- "namespaces": [],
17
- "labels": []
16
+ "namespaces": []
18
17
  },
19
18
  "includedFiles": []
20
19
  }