pepr 0.28.0 → 0.28.2

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.0",
12
+ "version": "0.28.2",
13
13
  "main": "dist/lib.js",
14
14
  "types": "dist/lib.d.ts",
15
15
  "scripts": {
@@ -34,15 +34,15 @@
34
34
  "@types/ramda": "0.29.11",
35
35
  "express": "4.18.3",
36
36
  "fast-json-patch": "3.1.1",
37
- "kubernetes-fluent-client": "2.2.2",
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.3",
45
- "@commitlint/config-conventional": "19.0.3",
44
+ "@commitlint/cli": "19.1.0",
45
+ "@commitlint/config-conventional": "19.1.0",
46
46
  "@jest/globals": "29.7.0",
47
47
  "@types/eslint": "8.56.5",
48
48
  "@types/express": "4.17.21",
@@ -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) => {
package/src/lib/filter.ts CHANGED
@@ -51,7 +51,22 @@ export function shouldSkipRequest(binding: Binding, req: AdmissionRequest, capab
51
51
  (combinedNamespaces.length && !combinedNamespaces.includes(req.namespace || "")) ||
52
52
  (!namespaces.includes(req.namespace || "") && capabilityNamespaces.length !== 0 && namespaces.length !== 0)
53
53
  ) {
54
- logger.debug("Namespace does not match");
54
+ let type = "";
55
+ let label = "";
56
+
57
+ if (binding.isMutate) {
58
+ type = "Mutate";
59
+ label = binding.mutateCallback!.name;
60
+ } else if (binding.isValidate) {
61
+ type = "Validate";
62
+ label = binding.validateCallback!.name;
63
+ } else if (binding.isWatch) {
64
+ type = "Watch";
65
+ label = binding.watchCallback!.name;
66
+ }
67
+
68
+ logger.debug(`${type} binding (${label}) does not match request namespace "${req.namespace}"`);
69
+
55
70
  return true;
56
71
  }
57
72
 
@@ -15,20 +15,33 @@ type RBACMap = {
15
15
  };
16
16
 
17
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) {
18
+ export function checkOverlap(bindingFilters: Record<string, string>, objectFilters: Record<string, string>): boolean {
19
+ // True if labels/annotations are empty
20
+ if (Object.keys(bindingFilters).length === 0) {
20
21
  return true;
21
22
  }
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;
23
+
24
+ let matchCount = 0;
25
+
26
+ for (const key in bindingFilters) {
27
+ // object must have label/annotation
28
+ if (Object.prototype.hasOwnProperty.call(objectFilters, key)) {
29
+ const val1 = bindingFilters[key];
30
+ const val2 = objectFilters[key];
31
+
32
+ // If bindingFilter has empty value for this key, only need to ensure objectFilter has this key
33
+ if (val1 === "" && key in objectFilters) {
34
+ matchCount++;
35
+ }
36
+ // If bindingFilter has a value, it must match the value in objectFilter
37
+ else if (val1 !== "" && val1 === val2) {
38
+ matchCount++;
39
+ }
29
40
  }
30
41
  }
31
- return false;
42
+
43
+ // For single-key objects in bindingFilter or matching all keys in multiple-keys scenario
44
+ return matchCount === Object.keys(bindingFilters).length;
32
45
  }
33
46
 
34
47
  /**
@@ -189,7 +202,7 @@ export function generateWatchNamespaceError(
189
202
  if (bindingAndCapabilityNSConflict(bindingNamespaces, capabilityNamespaces)) {
190
203
  err += `Binding uses namespace not governed by capability: bindingNamespaces: [${bindingNamespaces.join(
191
204
  ", ",
192
- )}] capabilityNamespaces:$[${capabilityNamespaces.join(", ")}].`;
205
+ )}] capabilityNamespaces: [${capabilityNamespaces.join(", ")}].`;
193
206
  }
194
207
 
195
208
  // add a space if there is a period in the middle of the string
package/src/lib/module.ts CHANGED
@@ -107,10 +107,12 @@ export class PeprModule {
107
107
  this.#controller = new Controller(config, capabilities, opts.beforeHook, opts.afterHook, () => {
108
108
  // Wait for the controller to be ready before setting up watches
109
109
  if (isWatchMode() || isDevMode()) {
110
- setupWatch(config.uuid, capabilities).catch(e => {
110
+ try {
111
+ setupWatch(capabilities);
112
+ } catch (e) {
111
113
  Log.error(e, "Error setting up watch");
112
114
  process.exit(1);
113
- });
115
+ }
114
116
  }
115
117
  });
116
118
 
@@ -55,7 +55,7 @@ export async function mutateProcessor(
55
55
  }
56
56
 
57
57
  const label = action.mutateCallback.name;
58
- Log.info(actionMetadata, `Processing matched action ${label}`);
58
+ Log.info(actionMetadata, `Processing mutation action (${label})`);
59
59
 
60
60
  matchedAction = true;
61
61
 
@@ -79,7 +79,7 @@ export async function mutateProcessor(
79
79
  // Run the action
80
80
  await action.mutateCallback(wrapped);
81
81
 
82
- Log.info(actionMetadata, `Action succeeded`);
82
+ Log.info(actionMetadata, `Mutation action succeeded (${label})`);
83
83
 
84
84
  // Add annotations to the request to indicate that the capability succeeded
85
85
  updateStatus("succeeded");
@@ -46,7 +46,7 @@ export async function validateProcessor(
46
46
  }
47
47
 
48
48
  const label = action.validateCallback.name;
49
- Log.info(actionMetadata, `Processing matched action ${label}`);
49
+ Log.info(actionMetadata, `Processing validation action (${label})`);
50
50
 
51
51
  try {
52
52
  // Run the validation callback, if it fails set allowed to false
@@ -61,7 +61,7 @@ export async function validateProcessor(
61
61
  };
62
62
  }
63
63
 
64
- Log.info(actionMetadata, `Validation Action completed: ${resp.allowed ? "allowed" : "denied"}`);
64
+ Log.info(actionMetadata, `Validation action complete (${label}): ${resp.allowed ? "allowed" : "denied"}`);
65
65
  } catch (e) {
66
66
  // If any validation throws an error, note the failure in the Response
67
67
  Log.error(actionMetadata, `Action failed: ${JSON.stringify(e)}`);
@@ -6,59 +6,15 @@ 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
13
  import { filterMatcher } from "./helpers";
15
14
 
16
- // Track if the store has been updated
17
- let storeUpdates = false;
18
-
19
15
  const store: Record<string, string> = {};
20
16
 
21
- export async function setupStore(uuid: string) {
22
- const name = `pepr-${uuid}-watch`;
23
- const namespace = "pepr-system";
24
-
25
- try {
26
- // Try to read the watch store if it exists
27
- const k8sStore = await K8s(PeprStore).InNamespace(namespace).Get(name);
28
-
29
- // Iterate over the store and add the values to the local store
30
- Object.entries(k8sStore.data).forEach(([key, value]) => {
31
- store[key] = value;
32
- });
33
- } catch (e) {
34
- // A store not existing is expected behavior on the first run
35
- Log.debug(e, "Watch store does not exist yet");
36
- }
37
-
38
- // Update the store every 10 seconds if there are changes
39
- setInterval(() => {
40
- if (storeUpdates) {
41
- K8s(PeprStore)
42
- .Apply({
43
- metadata: {
44
- name,
45
- namespace,
46
- },
47
- data: store,
48
- })
49
- // Reset the store updates flag
50
- .then(() => (storeUpdates = false))
51
- // Log the error if the store update fails, but don't reset the store updates flag
52
- .catch(e => {
53
- Log.error(e, "Error updating watch store");
54
- });
55
- }
56
- }, 10 * 1000);
57
- }
58
-
59
- export async function setupWatch(uuid: string, capabilities: Capability[]) {
60
- await setupStore(uuid);
61
-
17
+ export function setupWatch(capabilities: Capability[]) {
62
18
  capabilities.map(capability =>
63
19
  capability.bindings
64
20
  .filter(binding => binding.isWatch)
@@ -134,16 +90,6 @@ async function runBinding(binding: Binding, capabilityNamespaces: string[]) {
134
90
  const cacheSuffix = createHash("sha224").update(binding.watchCallback!.toString()).digest("hex").substring(0, 5);
135
91
  const cacheID = [watcher.getCacheID(), cacheSuffix].join("-");
136
92
 
137
- // Track the resource version in the local store
138
- watcher.events.on(WatchEvent.RESOURCE_VERSION, version => {
139
- Log.debug(`Received watch cache: ${cacheID}:${version}`);
140
- if (store[cacheID] !== version) {
141
- Log.debug(`Updating watch cache: ${cacheID}: ${store[cacheID]} => ${version}`);
142
- store[cacheID] = version;
143
- storeUpdates = true;
144
- }
145
- });
146
-
147
93
  // If failure continues, log and exit
148
94
  watcher.events.on(WatchEvent.GIVE_UP, err => {
149
95
  Log.error(err, "Watch failed after 5 attempts, giving up");