pepr 0.18.1 → 0.20.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.
Files changed (40) hide show
  1. package/README.md +1 -1
  2. package/dist/cli.js +169 -26
  3. package/dist/controller.js +1 -1
  4. package/dist/lib/assets/index.d.ts +2 -0
  5. package/dist/lib/assets/index.d.ts.map +1 -1
  6. package/dist/lib/assets/networking.d.ts.map +1 -1
  7. package/dist/lib/assets/pods.d.ts +5 -4
  8. package/dist/lib/assets/pods.d.ts.map +1 -1
  9. package/dist/lib/controller/index.d.ts.map +1 -1
  10. package/dist/lib/helpers.d.ts +6 -0
  11. package/dist/lib/helpers.d.ts.map +1 -1
  12. package/dist/lib/logger.d.ts +1 -8
  13. package/dist/lib/logger.d.ts.map +1 -1
  14. package/dist/lib/module-helpers.d.ts +5 -0
  15. package/dist/lib/module-helpers.d.ts.map +1 -0
  16. package/dist/lib/module.d.ts +2 -0
  17. package/dist/lib/module.d.ts.map +1 -1
  18. package/dist/lib/types.d.ts +10 -0
  19. package/dist/lib/types.d.ts.map +1 -1
  20. package/dist/lib.d.ts +2 -1
  21. package/dist/lib.d.ts.map +1 -1
  22. package/dist/lib.js +34 -10
  23. package/dist/lib.js.map +4 -4
  24. package/package.json +4 -4
  25. package/src/cli.ts +2 -0
  26. package/src/lib/assets/index.ts +10 -1
  27. package/src/lib/assets/networking.ts +8 -0
  28. package/src/lib/assets/pods.ts +26 -9
  29. package/src/lib/assets/webhooks.ts +1 -1
  30. package/src/lib/controller/index.ts +20 -13
  31. package/src/lib/controller/store.ts +1 -1
  32. package/src/lib/helpers.ts +69 -0
  33. package/src/lib/module-helpers.ts +27 -0
  34. package/src/lib/module.ts +2 -0
  35. package/src/lib/types.ts +10 -0
  36. package/src/lib.ts +2 -0
  37. package/website/assets/img/Pepr.Horizontal.white.svg +144 -0
  38. package/website/assets/scss/_styles_project.scss +4 -0
  39. package/website/content/en/docs/cli.md +16 -0
  40. package/website/content/en/docs/metrics.md +39 -0
package/package.json CHANGED
@@ -9,7 +9,7 @@
9
9
  "engines": {
10
10
  "node": ">=18.0.0"
11
11
  },
12
- "version": "0.18.1",
12
+ "version": "0.20.1",
13
13
  "main": "dist/lib.js",
14
14
  "types": "dist/lib.d.ts",
15
15
  "scripts": {
@@ -33,8 +33,8 @@
33
33
  "express": "4.18.2",
34
34
  "fast-json-patch": "3.1.1",
35
35
  "kubernetes-fluent-client": "1.9.0",
36
- "pino": "8.16.2",
37
- "pino-pretty": "10.2.3",
36
+ "pino": "8.17.1",
37
+ "pino-pretty": "10.3.0",
38
38
  "prom-client": "15.0.0",
39
39
  "ramda": "0.29.1"
40
40
  },
@@ -42,7 +42,7 @@
42
42
  "@commitlint/cli": "18.4.3",
43
43
  "@commitlint/config-conventional": "18.4.3",
44
44
  "@jest/globals": "29.7.0",
45
- "@types/eslint": "8.44.8",
45
+ "@types/eslint": "8.44.9",
46
46
  "@types/express": "4.17.21",
47
47
  "@types/node": "18.x.x",
48
48
  "@types/node-forge": "1.3.10",
package/src/cli.ts CHANGED
@@ -8,6 +8,7 @@ import build from "./cli/build";
8
8
  import deploy from "./cli/deploy";
9
9
  import dev from "./cli/dev";
10
10
  import format from "./cli/format";
11
+ import monitor from "./cli/monitor";
11
12
  import init from "./cli/init/index";
12
13
  import { version } from "./cli/init/templates";
13
14
  import { RootCmd } from "./cli/root";
@@ -39,5 +40,6 @@ deploy(program);
39
40
  dev(program);
40
41
  update(program);
41
42
  format(program);
43
+ monitor(program);
42
44
 
43
45
  program.parse();
@@ -6,15 +6,19 @@ import crypto from "crypto";
6
6
  import { ModuleConfig } from "../module";
7
7
  import { TLSOut, genTLS } from "../tls";
8
8
  import { CapabilityExport } from "../types";
9
+ import { WebhookIgnore } from "../k8s";
9
10
  import { deploy } from "./deploy";
10
11
  import { loadCapabilities } from "./loader";
11
12
  import { allYaml, zarfYaml } from "./yaml";
13
+ import { namespaceComplianceValidator } from "../helpers";
12
14
 
13
15
  export class Assets {
14
16
  readonly name: string;
15
17
  readonly tls: TLSOut;
16
18
  readonly apiToken: string;
19
+ readonly alwaysIgnore!: WebhookIgnore;
17
20
  capabilities!: CapabilityExport[];
21
+
18
22
  image: string;
19
23
 
20
24
  constructor(
@@ -23,7 +27,7 @@ export class Assets {
23
27
  readonly host?: string,
24
28
  ) {
25
29
  this.name = `pepr-${config.uuid}`;
26
-
30
+ this.alwaysIgnore = config.alwaysIgnore;
27
31
  this.image = `ghcr.io/defenseunicorns/pepr/controller:v${config.peprVersion}`;
28
32
 
29
33
  // Generate the ephemeral tls things
@@ -42,6 +46,11 @@ export class Assets {
42
46
 
43
47
  allYaml = async (rbacMode: string) => {
44
48
  this.capabilities = await loadCapabilities(this.path);
49
+ // give error if namespaces are not respected
50
+ for (const capability of this.capabilities) {
51
+ namespaceComplianceValidator(capability, this.alwaysIgnore.namespaces);
52
+ }
53
+
45
54
  return allYaml(this, rbacMode);
46
55
  };
47
56
  }
@@ -43,10 +43,14 @@ export function service(name: string): kind.Service {
43
43
  metadata: {
44
44
  name,
45
45
  namespace: "pepr-system",
46
+ labels: {
47
+ "pepr.dev/controller": "admission",
48
+ },
46
49
  },
47
50
  spec: {
48
51
  selector: {
49
52
  app: name,
53
+ "pepr.dev/controller": "admission",
50
54
  },
51
55
  ports: [
52
56
  {
@@ -65,10 +69,14 @@ export function watcherService(name: string): kind.Service {
65
69
  metadata: {
66
70
  name: `${name}-watcher`,
67
71
  namespace: "pepr-system",
72
+ labels: {
73
+ "pepr.dev/controller": "watcher",
74
+ },
68
75
  },
69
76
  spec: {
70
77
  selector: {
71
78
  app: `${name}-watcher`,
79
+ "pepr.dev/controller": "watcher",
72
80
  },
73
81
  ports: [
74
82
  {
@@ -1,10 +1,12 @@
1
1
  // SPDX-License-Identifier: Apache-2.0
2
2
  // SPDX-FileCopyrightText: 2023-Present The Pepr Authors
3
3
 
4
+ import { V1EnvVar } from "@kubernetes/client-node";
4
5
  import { kind } from "kubernetes-fluent-client";
5
6
  import { gzipSync } from "zlib";
6
7
 
7
8
  import { Assets } from ".";
9
+ import { ModuleConfig } from "../module";
8
10
  import { Binding } from "../types";
9
11
 
10
12
  /** Generate the pepr-system namespace */
@@ -45,6 +47,7 @@ export function watcher(assets: Assets, hash: string) {
45
47
  namespace: "pepr-system",
46
48
  labels: {
47
49
  app,
50
+ "pepr.dev/controller": "watcher",
48
51
  },
49
52
  },
50
53
  spec: {
@@ -55,12 +58,14 @@ export function watcher(assets: Assets, hash: string) {
55
58
  selector: {
56
59
  matchLabels: {
57
60
  app,
61
+ "pepr.dev/controller": "watcher",
58
62
  },
59
63
  },
60
64
  template: {
61
65
  metadata: {
62
66
  labels: {
63
67
  app,
68
+ "pepr.dev/controller": "watcher",
64
69
  },
65
70
  },
66
71
  spec: {
@@ -112,11 +117,7 @@ export function watcher(assets: Assets, hash: string) {
112
117
  readOnly: true,
113
118
  },
114
119
  ],
115
- env: [
116
- { name: "PEPR_WATCH_MODE", value: "true" },
117
- { name: "PEPR_PRETTY_LOG", value: "false" },
118
- { name: "LOG_LEVEL", value: config.logLevel || "debug" },
119
- ],
120
+ env: genEnv(config, true),
120
121
  },
121
122
  ],
122
123
  volumes: [
@@ -151,6 +152,7 @@ export function deployment(assets: Assets, hash: string): kind.Deployment {
151
152
  namespace: "pepr-system",
152
153
  labels: {
153
154
  app,
155
+ "pepr.dev/controller": "admission",
154
156
  },
155
157
  },
156
158
  spec: {
@@ -158,12 +160,14 @@ export function deployment(assets: Assets, hash: string): kind.Deployment {
158
160
  selector: {
159
161
  matchLabels: {
160
162
  app,
163
+ "pepr.dev/controller": "admission",
161
164
  },
162
165
  },
163
166
  template: {
164
167
  metadata: {
165
168
  labels: {
166
169
  app,
170
+ "pepr.dev/controller": "admission",
167
171
  },
168
172
  },
169
173
  spec: {
@@ -204,10 +208,7 @@ export function deployment(assets: Assets, hash: string): kind.Deployment {
204
208
  cpu: "500m",
205
209
  },
206
210
  },
207
- env: [
208
- { name: "PEPR_PRETTY_LOG", value: "false" },
209
- { name: "LOG_LEVEL", value: config.logLevel || "debug" },
210
- ],
211
+ env: genEnv(config),
211
212
  volumeMounts: [
212
213
  {
213
214
  name: "tls-certs",
@@ -270,3 +271,19 @@ export function moduleSecret(name: string, data: Buffer, hash: string): kind.Sec
270
271
  },
271
272
  };
272
273
  }
274
+
275
+ function genEnv(config: ModuleConfig, watchMode = false): V1EnvVar[] {
276
+ const env = [
277
+ { name: "PEPR_WATCH_MODE", value: watchMode ? "true" : "false" },
278
+ { name: "PEPR_PRETTY_LOG", value: "false" },
279
+ { name: "LOG_LEVEL", value: config.logLevel || "debug" },
280
+ ];
281
+
282
+ if (config.env) {
283
+ for (const [name, value] of Object.entries(config.env)) {
284
+ env.push({ name, value });
285
+ }
286
+ }
287
+
288
+ return env;
289
+ }
@@ -129,7 +129,7 @@ export async function webhookConfig(
129
129
  name: `${name}.pepr.dev`,
130
130
  admissionReviewVersions: ["v1", "v1beta1"],
131
131
  clientConfig,
132
- failurePolicy: "Ignore",
132
+ failurePolicy: config.onError === "reject" ? "Fail" : "Ignore",
133
133
  matchPolicy: "Equivalent",
134
134
  timeoutSeconds,
135
135
  namespaceSelector: {
@@ -13,6 +13,7 @@ import { ModuleConfig, isWatchMode } from "../module";
13
13
  import { mutateProcessor } from "../mutate-processor";
14
14
  import { validateProcessor } from "../validate-processor";
15
15
  import { PeprControllerStore } from "./store";
16
+ import { ResponseItem } from "../types";
16
17
 
17
18
  export class Controller {
18
19
  // Track whether the server is running
@@ -233,34 +234,40 @@ export class Controller {
233
234
  const responseList: ValidateResponse[] | MutateResponse[] = Array.isArray(response) ? response : [response];
234
235
  responseList.map(res => {
235
236
  this.#afterHook && this.#afterHook(res);
237
+ // Log the response
238
+ Log.debug({ ...reqMetadata, res }, "Check response");
236
239
  });
237
240
 
238
- // Log the response
239
- Log.debug({ ...reqMetadata, response }, "Outgoing response");
241
+ let kubeAdmissionResponse: ValidateResponse[] | MutateResponse | ResponseItem;
240
242
 
241
243
  if (admissionKind === "Mutate") {
244
+ kubeAdmissionResponse = response;
245
+ Log.debug({ ...reqMetadata, response }, "Outgoing response");
242
246
  res.send({
243
247
  apiVersion: "admission.k8s.io/v1",
244
248
  kind: "AdmissionReview",
245
- response,
249
+ response: kubeAdmissionResponse,
246
250
  });
247
251
  } else {
252
+ kubeAdmissionResponse = {
253
+ uid: responseList[0].uid,
254
+ allowed: responseList.filter(r => !r.allowed).length === 0,
255
+ status: {
256
+ message: (responseList as ValidateResponse[])
257
+ .filter(rl => !rl.allowed)
258
+ .map(curr => curr.status?.message)
259
+ .join("; "),
260
+ },
261
+ };
248
262
  res.send({
249
263
  apiVersion: "admission.k8s.io/v1",
250
264
  kind: "AdmissionReview",
251
- response: {
252
- uid: responseList[0].uid,
253
- allowed: responseList.filter(r => !r.allowed).length === 0,
254
- status: {
255
- message: (responseList as ValidateResponse[])
256
- .filter(rl => !rl.allowed)
257
- .map(curr => curr.status?.message)
258
- .join("; "),
259
- },
260
- },
265
+ response: kubeAdmissionResponse,
261
266
  });
262
267
  }
263
268
 
269
+ Log.debug({ ...reqMetadata, kubeAdmissionResponse }, "Outgoing response");
270
+
264
271
  this.#metricsCollector.observeEnd(startTime, admissionKind);
265
272
  } catch (err) {
266
273
  Log.error(err);
@@ -31,7 +31,7 @@ export class PeprControllerStore {
31
31
  for (const { name, registerScheduleStore, hasSchedule } of capabilities) {
32
32
  // Guard Clause to exit early
33
33
  if (hasSchedule !== true) {
34
- return;
34
+ continue;
35
35
  }
36
36
  // Register the scheduleStore with the capability
37
37
  const { scheduleStore } = registerScheduleStore();
@@ -50,3 +50,72 @@ export async function createDirectoryIfNotExists(path: string) {
50
50
  }
51
51
  }
52
52
  }
53
+
54
+ export function hasEveryOverlap<T>(array1: T[], array2: T[]): boolean {
55
+ if (!Array.isArray(array1) || !Array.isArray(array2)) {
56
+ return false;
57
+ }
58
+
59
+ return array1.every(element => array2.includes(element));
60
+ }
61
+
62
+ export function hasAnyOverlap<T>(array1: T[], array2: T[]): boolean {
63
+ if (!Array.isArray(array1) || !Array.isArray(array2)) {
64
+ return false;
65
+ }
66
+
67
+ return array1.some(element => array2.includes(element));
68
+ }
69
+
70
+ export function ignoredNamespaceConflict(ignoreNamespaces: string[], bindingNamespaces: string[]) {
71
+ return hasAnyOverlap(bindingNamespaces, ignoreNamespaces);
72
+ }
73
+
74
+ export function bindingAndCapabilityNSConflict(bindingNamespaces: string[], capabilityNamespaces: string[]) {
75
+ if (!capabilityNamespaces) {
76
+ return false;
77
+ }
78
+ return capabilityNamespaces.length !== 0 && !hasEveryOverlap(bindingNamespaces, capabilityNamespaces);
79
+ }
80
+
81
+ export function generateWatchNamespaceError(
82
+ ignoredNamespaces: string[],
83
+ bindingNamespaces: string[],
84
+ capabilityNamespaces: string[],
85
+ ) {
86
+ let err = "";
87
+
88
+ // check if binding uses an ignored namespace
89
+ if (ignoredNamespaceConflict(ignoredNamespaces, bindingNamespaces)) {
90
+ err += `Binding uses a Pepr ignored namespace: ignoredNamespaces: [${ignoredNamespaces.join(
91
+ ", ",
92
+ )}] bindingNamespaces: [${bindingNamespaces.join(", ")}].`;
93
+ }
94
+
95
+ // ensure filter namespaces are part of capability namespaces
96
+ if (bindingAndCapabilityNSConflict(bindingNamespaces, capabilityNamespaces)) {
97
+ err += `Binding uses namespace not governed by capability: bindingNamespaces: [${bindingNamespaces.join(
98
+ ", ",
99
+ )}] capabilityNamespaces:$[${capabilityNamespaces.join(", ")}].`;
100
+ }
101
+
102
+ // add a space if there is a period in the middle of the string
103
+ return err.replace(/\.([^ ])/g, ". $1");
104
+ }
105
+
106
+ // namespaceComplianceValidator ensures that capability bindinds respect ignored and capability namespaces
107
+ export function namespaceComplianceValidator(capability: CapabilityExport, ignoredNamespaces?: string[]) {
108
+ const { namespaces: capabilityNamespaces, bindings, name } = capability;
109
+ const bindingNamespaces = bindings.flatMap(binding => binding.filters.namespaces);
110
+
111
+ const namespaceError = generateWatchNamespaceError(
112
+ ignoredNamespaces ? ignoredNamespaces : [],
113
+ bindingNamespaces,
114
+ capabilityNamespaces ? capabilityNamespaces : [],
115
+ );
116
+ if (namespaceError !== "") {
117
+ throw new Error(
118
+ `Error in ${name} capability. A binding violates namespace rules. Please check ignoredNamespaces and capability namespaces: ${namespaceError}`,
119
+ );
120
+ }
121
+ }
@@ -0,0 +1,27 @@
1
+ // SPDX-License-Identifier: Apache-2.0
2
+ // SPDX-FileCopyrightText: 2023-Present The Pepr Authors
3
+
4
+ import { PeprValidateRequest } from "./validate-request";
5
+ import { PeprMutateRequest } from "./mutate-request";
6
+ import { a } from "../lib";
7
+
8
+ // Returns all containers in the pod
9
+ export function containers(
10
+ request: PeprValidateRequest<a.Pod> | PeprMutateRequest<a.Pod>,
11
+ containerType?: "containers" | "initContainers" | "ephemeralContainers",
12
+ ) {
13
+ const containers = request.Raw.spec?.containers || [];
14
+ const initContainers = request.Raw.spec?.initContainers || [];
15
+ const ephemeralContainers = request.Raw.spec?.ephemeralContainers || [];
16
+
17
+ if (containerType === "containers") {
18
+ return containers;
19
+ }
20
+ if (containerType === "initContainers") {
21
+ return initContainers;
22
+ }
23
+ if (containerType === "ephemeralContainers") {
24
+ return ephemeralContainers;
25
+ }
26
+ return [...containers, ...initContainers, ...ephemeralContainers];
27
+ }
package/src/lib/module.ts CHANGED
@@ -28,6 +28,8 @@ export type ModuleConfig = {
28
28
  alwaysIgnore: WebhookIgnore;
29
29
  /** Define the log level for the in-cluster controllers */
30
30
  logLevel?: string;
31
+ /** Propagate env variables to in-cluster controllers */
32
+ env?: Record<string, string>;
31
33
  };
32
34
 
33
35
  export type PackageJSON = {
package/src/lib/types.ts CHANGED
@@ -7,6 +7,16 @@ import { WatchAction } from "kubernetes-fluent-client/dist/fluent/types";
7
7
  import { PeprMutateRequest } from "./mutate-request";
8
8
  import { PeprValidateRequest } from "./validate-request";
9
9
 
10
+ /**
11
+ * Specifically for parsing logs in monitor mode
12
+ */
13
+ export interface ResponseItem {
14
+ uid?: string;
15
+ allowed: boolean;
16
+ status: {
17
+ message: string;
18
+ };
19
+ }
10
20
  /**
11
21
  * Recursively make all properties in T optional.
12
22
  */
package/src/lib.ts CHANGED
@@ -7,6 +7,7 @@ import { PeprModule } from "./lib/module";
7
7
  import { PeprMutateRequest } from "./lib/mutate-request";
8
8
  import * as PeprUtils from "./lib/utils";
9
9
  import { PeprValidateRequest } from "./lib/validate-request";
10
+ import { containers } from "./lib/module-helpers";
10
11
 
11
12
  export {
12
13
  Capability,
@@ -22,4 +23,5 @@ export {
22
23
  fetch,
23
24
  fetchStatus,
24
25
  kind,
26
+ containers,
25
27
  };