pepr 0.27.0 → 0.28.1

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.1",
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.3",
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.1.0",
45
+ "@commitlint/config-conventional": "19.1.0",
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();
@@ -74,6 +74,7 @@ export function watcherDeployTemplate(buildTimestamp: string) {
74
74
  app: {{ .Values.uuid }}-watcher
75
75
  pepr.dev/controller: watcher
76
76
  spec:
77
+ terminationGracePeriodSeconds: {{ .Values.watcher.terminationGracePeriodSeconds }}
77
78
  serviceAccountName: {{ .Values.uuid }}
78
79
  securityContext:
79
80
  {{- toYaml .Values.admission.securityContext | nindent 8 }}
@@ -145,6 +146,7 @@ export function admissionDeployTemplate(buildTimestamp: string) {
145
146
  app: {{ .Values.uuid }}
146
147
  pepr.dev/controller: admission
147
148
  spec:
149
+ terminationGracePeriodSeconds: {{ .Values.admission.terminationGracePeriodSeconds }}
148
150
  priorityClassName: system-node-critical
149
151
  serviceAccountName: {{ .Values.uuid }}
150
152
  securityContext:
@@ -91,6 +91,7 @@ export function watcher(assets: Assets, hash: string, buildTimestamp: string) {
91
91
  },
92
92
  },
93
93
  spec: {
94
+ terminationGracePeriodSeconds: 5,
94
95
  serviceAccountName: name,
95
96
  securityContext: {
96
97
  runAsUser: 65532,
@@ -215,6 +216,7 @@ export function deployment(assets: Assets, hash: string, buildTimestamp: string)
215
216
  },
216
217
  },
217
218
  spec: {
219
+ terminationGracePeriodSeconds: 5,
218
220
  priorityClassName: "system-node-critical",
219
221
  serviceAccountName: name,
220
222
  securityContext: {
@@ -26,14 +26,13 @@ export async function overridesFile({ hash, name, image, config, apiToken }: Ass
26
26
  },
27
27
  uuid: name,
28
28
  admission: {
29
+ terminationGracePeriodSeconds: 5,
29
30
  failurePolicy: config.onError === "reject" ? "Fail" : "Ignore",
30
31
  webhookTimeout: config.webhookTimeout,
31
32
  env: [
32
33
  { name: "PEPR_WATCH_MODE", value: "false" },
33
34
  { name: "PEPR_PRETTY_LOG", value: "false" },
34
35
  { name: "LOG_LEVEL", value: "debug" },
35
- process.env.PEPR_MODE === "dev" && { name: "MY_CUSTOM_VAR", value: "example-value" },
36
- process.env.PEPR_MODE === "dev" && { name: "ZARF_VAR", value: "###ZARF_VAR_THING###" },
37
36
  ],
38
37
  image,
39
38
  annotations: {
@@ -74,12 +73,11 @@ export async function overridesFile({ hash, name, image, config, apiToken }: Ass
74
73
  affinity: {},
75
74
  },
76
75
  watcher: {
76
+ terminationGracePeriodSeconds: 5,
77
77
  env: [
78
78
  { name: "PEPR_WATCH_MODE", value: "true" },
79
79
  { name: "PEPR_PRETTY_LOG", value: "false" },
80
80
  { name: "LOG_LEVEL", value: "debug" },
81
- process.env.PEPR_MODE === "dev" && { name: "MY_CUSTOM_VAR", value: "example-value" },
82
- process.env.PEPR_MODE === "dev" && { name: "ZARF_VAR", value: "###ZARF_VAR_THING###" },
83
81
  ],
84
82
  image,
85
83
  annotations: {
@@ -120,6 +118,12 @@ export async function overridesFile({ hash, name, image, config, apiToken }: Ass
120
118
  affinity: {},
121
119
  },
122
120
  };
121
+ if (process.env.PEPR_MODE === "dev") {
122
+ overrides.admission.env.push({ name: "ZARF_VAR", value: "###ZARF_VAR_THING###" });
123
+ overrides.watcher.env.push({ name: "ZARF_VAR", value: "###ZARF_VAR_THING###" });
124
+ overrides.admission.env.push({ name: "MY_CUSTOM_VAR", value: "example-value" });
125
+ overrides.watcher.env.push({ name: "MY_CUSTOM_VAR", value: "example-value" });
126
+ }
123
127
 
124
128
  await fs.writeFile(path, dumpYaml(overrides, { noRefs: true, forceQuotes: true }));
125
129
  }
@@ -32,13 +32,13 @@ export class Controller {
32
32
  readonly #config: ModuleConfig;
33
33
  readonly #capabilities: Capability[];
34
34
  readonly #beforeHook?: (req: AdmissionRequest) => void;
35
- readonly #afterHook?: (res: MutateResponse) => void;
35
+ readonly #afterHook?: (res: MutateResponse | ValidateResponse) => void;
36
36
 
37
37
  constructor(
38
38
  config: ModuleConfig,
39
39
  capabilities: Capability[],
40
40
  beforeHook?: (req: AdmissionRequest) => void,
41
- afterHook?: (res: MutateResponse) => void,
41
+ afterHook?: (res: MutateResponse | ValidateResponse) => void,
42
42
  onReady?: () => void,
43
43
  ) {
44
44
  this.#config = config;
@@ -112,7 +112,7 @@ export class PeprControllerStore {
112
112
 
113
113
  // Debounce the update to 1 second to avoid multiple rapid calls
114
114
  clearTimeout(this.#sendDebounce);
115
- this.#sendDebounce = setTimeout(debounced, debounceBackoff);
115
+ this.#sendDebounce = setTimeout(debounced, this.#onReady ? 0 : debounceBackoff);
116
116
  };
117
117
 
118
118
  #send = (capabilityName: string) => {
@@ -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);
@@ -99,7 +189,7 @@ export function generateWatchNamespaceError(
99
189
  if (bindingAndCapabilityNSConflict(bindingNamespaces, capabilityNamespaces)) {
100
190
  err += `Binding uses namespace not governed by capability: bindingNamespaces: [${bindingNamespaces.join(
101
191
  ", ",
102
- )}] capabilityNamespaces:$[${capabilityNamespaces.join(", ")}].`;
192
+ )}] capabilityNamespaces: [${capabilityNamespaces.join(", ")}].`;
103
193
  }
104
194
 
105
195
  // add a space if there is a period in the middle of the string
@@ -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 */
@@ -109,10 +107,12 @@ export class PeprModule {
109
107
  this.#controller = new Controller(config, capabilities, opts.beforeHook, opts.afterHook, () => {
110
108
  // Wait for the controller to be ready before setting up watches
111
109
  if (isWatchMode() || isDevMode()) {
112
- setupWatch(config.uuid, capabilities).catch(e => {
110
+ try {
111
+ setupWatch(capabilities);
112
+ } catch (e) {
113
113
  Log.error(e, "Error setting up watch");
114
114
  process.exit(1);
115
- });
115
+ }
116
116
  }
117
117
  });
118
118
 
@@ -6,65 +6,23 @@ 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
- import { PeprStore } from "./k8s";
10
9
  import Log from "./logger";
11
10
  import { Binding, Event } from "./types";
12
11
  import { Watcher } from "kubernetes-fluent-client/dist/fluent/watch";
13
12
  import { GenericClass } from "kubernetes-fluent-client";
14
-
15
- // Track if the store has been updated
16
- let storeUpdates = false;
13
+ import { filterMatcher } from "./helpers";
17
14
 
18
15
  const store: Record<string, string> = {};
19
16
 
20
- export async function setupStore(uuid: string) {
21
- const name = `pepr-${uuid}-watch`;
22
- const namespace = "pepr-system";
23
-
24
- try {
25
- // Try to read the watch store if it exists
26
- const k8sStore = await K8s(PeprStore).InNamespace(namespace).Get(name);
27
-
28
- // Iterate over the store and add the values to the local store
29
- Object.entries(k8sStore.data).forEach(([key, value]) => {
30
- store[key] = value;
31
- });
32
- } catch (e) {
33
- // A store not existing is expected behavior on the first run
34
- Log.debug(e, "Watch store does not exist yet");
35
- }
36
-
37
- // Update the store every 10 seconds if there are changes
38
- setInterval(() => {
39
- if (storeUpdates) {
40
- K8s(PeprStore)
41
- .Apply({
42
- metadata: {
43
- name,
44
- namespace,
45
- },
46
- data: store,
47
- })
48
- // Reset the store updates flag
49
- .then(() => (storeUpdates = false))
50
- // Log the error if the store update fails, but don't reset the store updates flag
51
- .catch(e => {
52
- Log.error(e, "Error updating watch store");
53
- });
54
- }
55
- }, 10 * 1000);
17
+ export function setupWatch(capabilities: Capability[]) {
18
+ capabilities.map(capability =>
19
+ capability.bindings
20
+ .filter(binding => binding.isWatch)
21
+ .forEach(bindingElement => runBinding(bindingElement, capability.namespaces)),
22
+ );
56
23
  }
57
24
 
58
- export async function setupWatch(uuid: string, capabilities: Capability[]) {
59
- await setupStore(uuid);
60
-
61
- capabilities
62
- .flatMap(c => c.bindings)
63
- .filter(binding => binding.isWatch)
64
- .forEach(runBinding);
65
- }
66
-
67
- async function runBinding(binding: Binding) {
25
+ async function runBinding(binding: Binding, capabilityNamespaces: string[]) {
68
26
  // Map the event to the watch phase
69
27
  const eventToPhaseMap = {
70
28
  [Event.Create]: [WatchPhase.Added],
@@ -92,9 +50,14 @@ async function runBinding(binding: Binding) {
92
50
  // If the type matches the phase, call the watch callback
93
51
  if (phaseMatch.includes(type)) {
94
52
  try {
95
- queue.setReconcile(async () => await binding.watchCallback?.(obj, type));
96
- // Enqueue the object for reconciliation through callback
97
- await queue.enqueue(obj);
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
+ }
98
61
  } catch (e) {
99
62
  // Errors in the watch callback should not crash the controller
100
63
  Log.error(e, "Error executing watch callback");
@@ -109,8 +72,12 @@ async function runBinding(binding: Binding) {
109
72
  // If the type matches the phase, call the watch callback
110
73
  if (phaseMatch.includes(type)) {
111
74
  try {
112
- // Perform the watch callback
113
- await binding.watchCallback?.(obj, type);
75
+ const filterMatch = filterMatcher(binding, obj, capabilityNamespaces);
76
+ if (filterMatch === "") {
77
+ await binding.watchCallback?.(obj, type);
78
+ } else {
79
+ Log.debug(filterMatch);
80
+ }
114
81
  } catch (e) {
115
82
  // Errors in the watch callback should not crash the controller
116
83
  Log.error(e, "Error executing watch callback");
@@ -123,16 +90,6 @@ async function runBinding(binding: Binding) {
123
90
  const cacheSuffix = createHash("sha224").update(binding.watchCallback!.toString()).digest("hex").substring(0, 5);
124
91
  const cacheID = [watcher.getCacheID(), cacheSuffix].join("-");
125
92
 
126
- // Track the resource version in the local store
127
- watcher.events.on(WatchEvent.RESOURCE_VERSION, version => {
128
- Log.debug(`Received watch cache: ${cacheID}:${version}`);
129
- if (store[cacheID] !== version) {
130
- Log.debug(`Updating watch cache: ${cacheID}: ${store[cacheID]} => ${version}`);
131
- store[cacheID] = version;
132
- storeUpdates = true;
133
- }
134
- });
135
-
136
93
  // If failure continues, log and exit
137
94
  watcher.events.on(WatchEvent.GIVE_UP, err => {
138
95
  Log.error(err, "Watch failed after 5 attempts, giving up");
@@ -13,8 +13,7 @@
13
13
  }
14
14
  },
15
15
  "alwaysIgnore": {
16
- "namespaces": [],
17
- "labels": []
16
+ "namespaces": []
18
17
  },
19
18
  "includedFiles": []
20
19
  }