pepr 0.12.2 → 0.13.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 (99) hide show
  1. package/CODE_OF_CONDUCT.md +83 -0
  2. package/CONTRIBUTING.md +70 -0
  3. package/README.md +28 -30
  4. package/dist/cli.js +666 -692
  5. package/dist/controller.js +13 -81
  6. package/dist/lib/assets/deploy.d.ts +3 -0
  7. package/dist/lib/assets/deploy.d.ts.map +1 -0
  8. package/dist/lib/assets/index.d.ts +17 -0
  9. package/dist/lib/assets/index.d.ts.map +1 -0
  10. package/dist/lib/assets/loader.d.ts +8 -0
  11. package/dist/lib/assets/loader.d.ts.map +1 -0
  12. package/dist/lib/assets/networking.d.ts +6 -0
  13. package/dist/lib/assets/networking.d.ts.map +1 -0
  14. package/dist/lib/assets/pods.d.ts +8 -0
  15. package/dist/lib/assets/pods.d.ts.map +1 -0
  16. package/dist/lib/assets/rbac.d.ts +11 -0
  17. package/dist/lib/assets/rbac.d.ts.map +1 -0
  18. package/dist/lib/assets/webhooks.d.ts +6 -0
  19. package/dist/lib/assets/webhooks.d.ts.map +1 -0
  20. package/dist/lib/assets/yaml.d.ts +4 -0
  21. package/dist/lib/assets/yaml.d.ts.map +1 -0
  22. package/dist/lib/capability.d.ts +4 -9
  23. package/dist/lib/capability.d.ts.map +1 -1
  24. package/dist/lib/controller.d.ts +4 -15
  25. package/dist/lib/controller.d.ts.map +1 -1
  26. package/dist/lib/errors.d.ts +12 -0
  27. package/dist/lib/errors.d.ts.map +1 -0
  28. package/dist/lib/filter.d.ts +1 -1
  29. package/dist/lib/filter.d.ts.map +1 -1
  30. package/dist/lib/k8s/index.d.ts +2 -1
  31. package/dist/lib/k8s/index.d.ts.map +1 -1
  32. package/dist/lib/k8s/kinds.d.ts.map +1 -1
  33. package/dist/lib/k8s/types.d.ts +18 -14
  34. package/dist/lib/k8s/types.d.ts.map +1 -1
  35. package/dist/lib/k8s/upstream.d.ts +2 -2
  36. package/dist/lib/k8s/upstream.d.ts.map +1 -1
  37. package/dist/lib/logger.d.ts +8 -54
  38. package/dist/lib/logger.d.ts.map +1 -1
  39. package/dist/lib/metrics.d.ts +10 -9
  40. package/dist/lib/metrics.d.ts.map +1 -1
  41. package/dist/lib/module.d.ts +4 -4
  42. package/dist/lib/module.d.ts.map +1 -1
  43. package/dist/lib/mutate-processor.d.ts +5 -0
  44. package/dist/lib/mutate-processor.d.ts.map +1 -0
  45. package/dist/lib/{request.d.ts → mutate-request.d.ts} +7 -7
  46. package/dist/lib/mutate-request.d.ts.map +1 -0
  47. package/dist/lib/types.d.ts +48 -55
  48. package/dist/lib/types.d.ts.map +1 -1
  49. package/dist/lib/validate-processor.d.ts +4 -0
  50. package/dist/lib/validate-processor.d.ts.map +1 -0
  51. package/dist/lib/validate-request.d.ts +54 -0
  52. package/dist/lib/validate-request.d.ts.map +1 -0
  53. package/dist/lib.d.ts +3 -2
  54. package/dist/lib.d.ts.map +1 -1
  55. package/dist/lib.js +610 -354
  56. package/dist/lib.js.map +4 -4
  57. package/jest.config.json +4 -0
  58. package/journey/before.ts +21 -0
  59. package/journey/k8s.ts +81 -0
  60. package/journey/pepr-build.ts +69 -0
  61. package/journey/pepr-deploy.ts +133 -0
  62. package/journey/pepr-dev.ts +155 -0
  63. package/journey/pepr-format.ts +13 -0
  64. package/journey/pepr-init.ts +12 -0
  65. package/package.json +29 -27
  66. package/src/cli.ts +2 -11
  67. package/src/lib/assets/deploy.ts +179 -0
  68. package/src/lib/assets/index.ts +53 -0
  69. package/src/lib/assets/loader.ts +41 -0
  70. package/src/lib/assets/networking.ts +58 -0
  71. package/src/lib/assets/pods.ts +148 -0
  72. package/src/lib/assets/rbac.ts +57 -0
  73. package/src/lib/assets/webhooks.ts +139 -0
  74. package/src/lib/assets/yaml.ts +75 -0
  75. package/src/lib/capability.ts +80 -68
  76. package/src/lib/controller.ts +199 -99
  77. package/src/lib/errors.ts +20 -0
  78. package/src/lib/fetch.ts +1 -1
  79. package/src/lib/filter.ts +1 -3
  80. package/src/lib/k8s/index.ts +4 -1
  81. package/src/lib/k8s/kinds.ts +40 -0
  82. package/src/lib/k8s/types.ts +21 -15
  83. package/src/lib/k8s/upstream.ts +5 -1
  84. package/src/lib/logger.ts +14 -125
  85. package/src/lib/metrics.ts +86 -29
  86. package/src/lib/module.ts +32 -16
  87. package/src/lib/{processor.ts → mutate-processor.ts} +39 -28
  88. package/src/lib/{request.ts → mutate-request.ts} +26 -13
  89. package/src/lib/types.ts +54 -60
  90. package/src/lib/validate-processor.ts +76 -0
  91. package/src/lib/validate-request.ts +106 -0
  92. package/src/lib.ts +4 -2
  93. package/src/runtime/controller.ts +1 -1
  94. package/dist/lib/k8s/webhook.d.ts +0 -37
  95. package/dist/lib/k8s/webhook.d.ts.map +0 -1
  96. package/dist/lib/processor.d.ts +0 -5
  97. package/dist/lib/processor.d.ts.map +0 -1
  98. package/dist/lib/request.d.ts.map +0 -1
  99. package/src/lib/k8s/webhook.ts +0 -643
@@ -5,6 +5,46 @@ import { GenericClass } from "../types";
5
5
  import { GroupVersionKind } from "./types";
6
6
 
7
7
  export const gvkMap: Record<string, GroupVersionKind> = {
8
+ /**
9
+ * Represents a K8s ClusterRole resource.
10
+ * ClusterRole is a set of permissions that can be bound to a user or group in a cluster-wide scope.
11
+ * @see {@link https://kubernetes.io/docs/reference/access-authn-authz/rbac/#role-and-clusterrole}
12
+ */
13
+ V1ClusterRole: {
14
+ kind: "ClusterRole",
15
+ version: "v1",
16
+ group: "rbac.authorization.k8s.io",
17
+ },
18
+ /**
19
+ * Represents a K8s ClusterRoleBinding resource.
20
+ * ClusterRoleBinding binds a ClusterRole to a user or group in a cluster-wide scope.
21
+ * @see {@link https://kubernetes.io/docs/reference/access-authn-authz/rbac/#rolebinding-and-clusterrolebinding}
22
+ */
23
+ V1ClusterRoleBinding: {
24
+ kind: "ClusterRoleBinding",
25
+ version: "v1",
26
+ group: "rbac.authorization.k8s.io",
27
+ },
28
+ /**
29
+ * Represents a K8s Role resource.
30
+ * Role is a set of permissions that can be bound to a user or group in a namespace scope.
31
+ * @see {@link https://kubernetes.io/docs/reference/access-authn-authz/rbac/#role-and-clusterrole}
32
+ */
33
+ V1Role: {
34
+ kind: "Role",
35
+ version: "v1",
36
+ group: "rbac.authorization.k8s.io",
37
+ },
38
+ /**
39
+ * Represents a K8s RoleBinding resource.
40
+ * RoleBinding binds a Role to a user or group in a namespace scope.
41
+ * @see {@link https://kubernetes.io/docs/reference/access-authn-authz/rbac/#rolebinding-and-clusterrolebinding}
42
+ */
43
+ V1RoleBinding: {
44
+ kind: "RoleBinding",
45
+ version: "v1",
46
+ group: "rbac.authorization.k8s.io",
47
+ },
8
48
  /**
9
49
  * Represents a K8s ConfigMap resource.
10
50
  * ConfigMap holds configuration data for pods to consume.
@@ -1,7 +1,9 @@
1
1
  // SPDX-License-Identifier: Apache-2.0
2
2
  // SPDX-FileCopyrightText: 2023-Present The Pepr Authors
3
3
 
4
- import { V1ListMeta, V1ObjectMeta } from "@kubernetes/client-node";
4
+ import { KubernetesListObject, KubernetesObject, V1ObjectMeta } from "@kubernetes/client-node";
5
+
6
+ export { KubernetesListObject, KubernetesObject };
5
7
 
6
8
  export enum Operation {
7
9
  CREATE = "CREATE",
@@ -10,18 +12,6 @@ export enum Operation {
10
12
  CONNECT = "CONNECT",
11
13
  }
12
14
 
13
- export interface KubernetesObject {
14
- apiVersion?: string;
15
- kind?: string;
16
- metadata?: V1ObjectMeta;
17
- }
18
- export interface KubernetesListObject<T extends KubernetesObject> {
19
- apiVersion?: string;
20
- kind?: string;
21
- metadata?: V1ListMeta;
22
- items: T[];
23
- }
24
-
25
15
  /**
26
16
  * GenericKind is a generic Kubernetes object that can be used to represent any Kubernetes object
27
17
  * that is not explicitly supported by Pepr. This can be used on its own or as a base class for
@@ -138,7 +128,7 @@ export interface Request<T = KubernetesObject> {
138
128
  readonly options?: any;
139
129
  }
140
130
 
141
- export interface Response {
131
+ export interface MutateResponse {
142
132
  /** UID is an identifier for the individual request/response. This must be copied over from the corresponding AdmissionRequest. */
143
133
  uid: string;
144
134
 
@@ -154,7 +144,11 @@ export interface Response {
154
144
  /** The type of Patch. Currently we only allow "JSONPatch". */
155
145
  patchType?: "JSONPatch";
156
146
 
157
- /** AuditAnnotations is an unstructured key value map set by remote admission controller (e.g. error=image-blacklisted). */
147
+ /**
148
+ * AuditAnnotations is an unstructured key value map set by remote admission controller (e.g. error=image-blacklisted).
149
+ *
150
+ * See https://kubernetes.io/docs/reference/labels-annotations-taints/audit-annotations/ for more information
151
+ */
158
152
  auditAnnotations?: {
159
153
  [key: string]: string;
160
154
  };
@@ -163,6 +157,18 @@ export interface Response {
163
157
  warnings?: string[];
164
158
  }
165
159
 
160
+ export interface ValidateResponse extends MutateResponse {
161
+ /** Status contains extra details into why an admission request was denied. This field IS NOT consulted in any way if "Allowed" is "true". */
162
+ status?: {
163
+ /** A machine-readable description of why this operation is in the
164
+ "Failure" status. If this value is empty there is no information available. */
165
+ code: number;
166
+
167
+ /** A human-readable description of the status of this operation. */
168
+ message: string;
169
+ };
170
+ }
171
+
166
172
  export type WebhookIgnore = {
167
173
  /**
168
174
  * List of Kubernetes namespaces to always ignore.
@@ -1,10 +1,12 @@
1
1
  // SPDX-License-Identifier: Apache-2.0
2
2
  // SPDX-FileCopyrightText: 2023-Present The Pepr Authors
3
3
 
4
- /** a is a collection of K8s types to be used within a CapabilityAction: `When(a.Configmap)` */
4
+ /** a is a collection of K8s types to be used within an action: `When(a.Configmap)` */
5
5
  export {
6
6
  V1APIService as APIService,
7
7
  V1CertificateSigningRequest as CertificateSigningRequest,
8
+ V1ClusterRole as ClusterRole,
9
+ V1ClusterRoleBinding as ClusterRoleBinding,
8
10
  V1ConfigMap as ConfigMap,
9
11
  V1ControllerRevision as ControllerRevision,
10
12
  V1CronJob as CronJob,
@@ -32,6 +34,8 @@ export {
32
34
  V1ReplicaSet as ReplicaSet,
33
35
  V1ReplicationController as ReplicationController,
34
36
  V1ResourceQuota as ResourceQuota,
37
+ V1Role as Role,
38
+ V1RoleBinding as RoleBinding,
35
39
  V1RuntimeClass as RuntimeClass,
36
40
  V1Secret as Secret,
37
41
  V1SelfSubjectAccessReview as SelfSubjectAccessReview,
package/src/lib/logger.ts CHANGED
@@ -1,136 +1,25 @@
1
1
  // SPDX-License-Identifier: Apache-2.0
2
2
  // SPDX-FileCopyrightText: 2023-Present The Pepr Authors
3
3
 
4
- /**
5
- * Enumeration representing different logging levels.
6
- */
7
- export enum LogLevel {
8
- debug = 0,
9
- info = 1,
10
- warn = 2,
11
- error = 3,
12
- }
13
-
14
- enum ConsoleColors {
15
- Reset = "\x1b[0m",
16
- Bright = "\x1b[1m",
17
- Dim = "\x1b[2m",
18
- Underscore = "\x1b[4m",
19
- Blink = "\x1b[5m",
20
- Reverse = "\x1b[7m",
21
- Hidden = "\x1b[8m",
22
-
23
- FgBlack = "\x1b[30m",
24
- FgRed = "\x1b[31m",
25
- FgGreen = "\x1b[32m",
26
- FgYellow = "\x1b[33m",
27
- FgBlue = "\x1b[34m",
28
- FgMagenta = "\x1b[35m",
29
- FgCyan = "\x1b[36m",
30
- FgWhite = "\x1b[37m",
31
-
32
- BgBlack = "\x1b[40m",
33
- BgRed = "\x1b[41m",
34
- BgGreen = "\x1b[42m",
35
- BgYellow = "\x1b[43m",
36
- BgBlue = "\x1b[44m",
37
- BgMagenta = "\x1b[45m",
38
- BgCyan = "\x1b[46m",
39
- BgWhite = "\x1b[47m",
40
- }
41
-
42
- /**
43
- * Simple logger class that logs messages at different log levels.
44
- */
45
- export class Logger {
46
- private _logLevel: LogLevel;
47
-
48
- /**
49
- * Create a new logger instance.
50
- * @param logLevel - The minimum log level to log messages for.
51
- */
52
- constructor(logLevel: LogLevel) {
53
- this._logLevel = logLevel;
54
- }
55
-
56
- /**
57
- * Change the log level of the logger.
58
- * @param logLevel - The log level to log the message at.
59
- */
60
- public SetLogLevel(logLevel: string): void {
61
- this._logLevel = LogLevel[logLevel as keyof typeof LogLevel];
62
- this.debug(`Log level set to ${logLevel}`);
63
- }
4
+ import { pino } from "pino";
64
5
 
65
- /**
66
- * Log a debug message.
67
- * @param message - The message to log.
68
- */
69
- public debug<T>(message: T, prefix?: string): void {
70
- this.log(LogLevel.debug, message, prefix);
71
- }
6
+ const isPrettyLog = process.env.PEPR_PRETTY_LOGS === "true";
72
7
 
73
- /**
74
- * Log an info message.
75
- * @param message - The message to log.
76
- */
77
- public info<T>(message: T, prefix?: string): void {
78
- this.log(LogLevel.info, message, prefix);
79
- }
8
+ const pretty = {
9
+ target: "pino-pretty",
10
+ options: {
11
+ colorize: true,
12
+ },
13
+ };
80
14
 
81
- /**
82
- * Log a warning message.
83
- * @param message - The message to log.
84
- */
85
- public warn<T>(message: T, prefix?: string): void {
86
- this.log(LogLevel.warn, message, prefix);
87
- }
15
+ const transport = isPrettyLog ? pretty : undefined;
88
16
 
89
- /**
90
- * Log an error message.
91
- * @param message - The message to log.
92
- */
93
- public error<T>(message: T, prefix?: string): void {
94
- this.log(LogLevel.error, message, prefix);
95
- }
17
+ const Log = pino({
18
+ transport,
19
+ });
96
20
 
97
- /**
98
- * Log a message at the specified log level.
99
- * @param logLevel - The log level of the message.
100
- * @param message - The message to log.
101
- */
102
- private log<T>(logLevel: LogLevel, message: T, callerPrefix = ""): void {
103
- const color = {
104
- [LogLevel.debug]: ConsoleColors.FgBlack,
105
- [LogLevel.info]: ConsoleColors.FgCyan,
106
- [LogLevel.warn]: ConsoleColors.FgYellow,
107
- [LogLevel.error]: ConsoleColors.FgRed,
108
- };
109
-
110
- if (logLevel >= this._logLevel) {
111
- // Prefix the message with the colored log level.
112
- let prefix = "[" + LogLevel[logLevel] + "]\t" + callerPrefix;
113
-
114
- prefix = this.colorize(prefix, color[logLevel]);
115
-
116
- // If the message is not a string, use the debug method to log the object.
117
- if (typeof message !== "string") {
118
- console.log(prefix);
119
- console.debug("%o", message);
120
- } else {
121
- console.log(prefix + "\t" + message);
122
- }
123
- }
124
- }
125
-
126
- private colorize(text: string, color: ConsoleColors): string {
127
- return color + text + ConsoleColors.Reset;
128
- }
129
- }
130
-
131
- /** Log is an instance of Logger used to generate log entries. */
132
- const Log = new Logger(LogLevel.info);
133
21
  if (process.env.LOG_LEVEL) {
134
- Log.SetLogLevel(process.env.LOG_LEVEL);
22
+ Log.level = process.env.LOG_LEVEL;
135
23
  }
24
+
136
25
  export default Log;
@@ -1,61 +1,117 @@
1
1
  // SPDX-License-Identifier: Apache-2.0
2
2
  // SPDX-FileCopyrightText: 2023-Present The Pepr Authors
3
3
 
4
- import promClient from "prom-client";
4
+ /* eslint-disable class-methods-use-this */
5
+
5
6
  import { performance } from "perf_hooks";
7
+ import promClient, { Counter, Registry, Summary } from "prom-client";
8
+ import Log from "./logger";
9
+
10
+ const loggingPrefix = "MetricsCollector";
11
+
12
+ interface MetricNames {
13
+ errors: string;
14
+ alerts: string;
15
+ mutate: string;
16
+ validate: string;
17
+ }
18
+
19
+ interface MetricArgs {
20
+ name: string;
21
+ help: string;
22
+ registers: Registry[];
23
+ }
6
24
 
7
25
  /**
8
26
  * MetricsCollector class handles metrics collection using prom-client and performance hooks.
9
27
  */
10
28
  export class MetricsCollector {
11
- private _registry: promClient.Registry;
12
- private _errors: promClient.Counter<string>;
13
- private _alerts: promClient.Counter<string>;
14
- private _summary: promClient.Summary<string>;
29
+ #registry: Registry;
30
+ #counters: Map<string, Counter<string>> = new Map();
31
+ #summaries: Map<string, Summary<string>> = new Map();
32
+ #prefix: string;
33
+
34
+ #metricNames: MetricNames = {
35
+ errors: "errors",
36
+ alerts: "alerts",
37
+ mutate: "Mutate",
38
+ validate: "Validate",
39
+ };
15
40
 
16
41
  /**
17
42
  * Creates a MetricsCollector instance with prefixed metrics.
18
- * @param {string} [prefix='pepr'] - The prefix for the metric names.
43
+ * @param [prefix='pepr'] - The prefix for the metric names.
19
44
  */
20
45
  constructor(prefix = "pepr") {
21
- this._registry = new promClient.Registry();
46
+ this.#registry = new Registry();
47
+ this.#prefix = prefix;
48
+ this.addCounter(this.#metricNames.errors, "Mutation/Validate errors encountered");
49
+ this.addCounter(this.#metricNames.alerts, "Mutation/Validate bad api token received");
50
+ this.addSummary(this.#metricNames.mutate, "Mutation operation summary");
51
+ this.addSummary(this.#metricNames.validate, "Validation operation summary");
52
+ }
22
53
 
23
- this._errors = new promClient.Counter({
24
- name: `${prefix}_errors`,
25
- help: "error counter",
26
- registers: [this._registry],
27
- });
54
+ #getMetricName(name: string) {
55
+ return `${this.#prefix}_${name}`;
56
+ }
28
57
 
29
- this._alerts = new promClient.Counter({
30
- name: `${prefix}_alerts`,
31
- help: "alerts counter",
32
- registers: [this._registry],
33
- });
58
+ #addMetric<T extends Counter<string> | Summary<string>>(
59
+ collection: Map<string, T>,
60
+ MetricType: new (args: MetricArgs) => T,
61
+ name: string,
62
+ help: string,
63
+ ) {
64
+ if (collection.has(this.#getMetricName(name))) {
65
+ Log.debug(`Metric for ${name} already exists`, loggingPrefix);
66
+ return;
67
+ }
68
+
69
+ // Bind public methods
70
+ this.incCounter = this.incCounter.bind(this);
71
+ this.error = this.error.bind(this);
72
+ this.alert = this.alert.bind(this);
73
+ this.observeStart = this.observeStart.bind(this);
74
+ this.observeEnd = this.observeEnd.bind(this);
75
+ this.getMetrics = this.getMetrics.bind(this);
34
76
 
35
- this._summary = new promClient.Summary({
36
- name: `${prefix}_summary`,
37
- help: "summary",
38
- registers: [this._registry],
77
+ const metric = new MetricType({
78
+ name: this.#getMetricName(name),
79
+ help,
80
+ registers: [this.#registry],
39
81
  });
82
+
83
+ collection.set(this.#getMetricName(name), metric);
84
+ }
85
+
86
+ addCounter(name: string, help: string) {
87
+ this.#addMetric(this.#counters, promClient.Counter, name, help);
88
+ }
89
+
90
+ addSummary(name: string, help: string) {
91
+ this.#addMetric(this.#summaries, promClient.Summary, name, help);
92
+ }
93
+
94
+ incCounter(name: string) {
95
+ this.#counters.get(this.#getMetricName(name))?.inc();
40
96
  }
41
97
 
42
98
  /**
43
99
  * Increments the error counter.
44
100
  */
45
101
  error() {
46
- this._errors.inc();
102
+ this.incCounter(this.#metricNames.errors);
47
103
  }
48
104
 
49
105
  /**
50
106
  * Increments the alerts counter.
51
107
  */
52
108
  alert() {
53
- this._alerts.inc();
109
+ this.incCounter(this.#metricNames.alerts);
54
110
  }
55
111
 
56
112
  /**
57
113
  * Returns the current timestamp from performance.now() method. Useful for start timing an operation.
58
- * @returns {number} The timestamp.
114
+ * @returns The timestamp.
59
115
  */
60
116
  observeStart() {
61
117
  return performance.now();
@@ -63,17 +119,18 @@ export class MetricsCollector {
63
119
 
64
120
  /**
65
121
  * Observes the duration since the provided start time and updates the summary.
66
- * @param {number} startTime - The start time.
122
+ * @param startTime - The start time.
123
+ * @param name - The metrics summary to increment.
67
124
  */
68
- observeEnd(startTime: number) {
69
- this._summary.observe(performance.now() - startTime);
125
+ observeEnd(startTime: number, name: string = this.#metricNames.mutate) {
126
+ this.#summaries.get(this.#getMetricName(name))?.observe(performance.now() - startTime);
70
127
  }
71
128
 
72
129
  /**
73
130
  * Fetches the current metrics from the registry.
74
- * @returns {Promise<string>} The metrics.
131
+ * @returns The metrics.
75
132
  */
76
133
  async getMetrics() {
77
- return this._registry.metrics();
134
+ return this.#registry.metrics();
78
135
  }
79
136
  }
package/src/lib/module.ts CHANGED
@@ -1,17 +1,13 @@
1
1
  // SPDX-License-Identifier: Apache-2.0
2
2
  // SPDX-FileCopyrightText: 2023-Present The Pepr Authors
3
3
 
4
- import { concat, mergeDeepWith } from "ramda";
4
+ import { clone } from "ramda";
5
5
 
6
6
  import { Capability } from "./capability";
7
7
  import { Controller } from "./controller";
8
- import { Request, Response } from "./k8s/types";
9
- import { ModuleConfig } from "./types";
10
-
11
- const alwaysIgnore = {
12
- namespaces: ["kube-system", "pepr-system"],
13
- labels: [{ "pepr.dev": "ignore" }],
14
- };
8
+ import { ValidateError } from "./errors";
9
+ import { MutateResponse, Request, ValidateResponse } from "./k8s/types";
10
+ import { CapabilityExport, ModuleConfig } from "./types";
15
11
 
16
12
  export type PackageJSON = {
17
13
  description: string;
@@ -25,30 +21,50 @@ export type PeprModuleOptions = {
25
21
  beforeHook?: (req: Request) => void;
26
22
 
27
23
  /** A user-defined callback to post-process or intercept a Pepr response just before it is returned to K8s */
28
- afterHook?: (res: Response) => void;
24
+ afterHook?: (res: MutateResponse | ValidateResponse) => void;
29
25
  };
30
26
 
31
27
  export class PeprModule {
32
- private _controller!: Controller;
28
+ #controller!: Controller;
33
29
 
34
30
  /**
35
31
  * Create a new Pepr runtime
36
32
  *
37
33
  * @param config The configuration for the Pepr runtime
38
34
  * @param capabilities The capabilities to be loaded into the Pepr runtime
39
- * @param _deferStart (optional) If set to `true`, the Pepr runtime will not be started automatically. This can be used to start the Pepr runtime manually with `start()`.
35
+ * @param opts Options for the Pepr runtime
40
36
  */
41
37
  constructor({ description, pepr }: PackageJSON, capabilities: Capability[] = [], opts: PeprModuleOptions = {}) {
42
- const config: ModuleConfig = mergeDeepWith(concat, pepr, alwaysIgnore);
38
+ const config: ModuleConfig = clone(pepr);
43
39
  config.description = description;
44
40
 
41
+ // Need to validate at runtime since TS gets sad about parsing the package.json
42
+ ValidateError(config.onError);
43
+
44
+ // Bind public methods
45
+ this.start = this.start.bind(this);
46
+
45
47
  // Handle build mode
46
- if (process.env.PEPR_MODE === "build") {
47
- process.send?.({ capabilities });
48
+ if (process.env.PEPR_MODE === "build" && process.send) {
49
+ const exportedCapabilities: CapabilityExport[] = [];
50
+
51
+ // Send capability map to parent process
52
+ for (const capability of capabilities) {
53
+ // Convert the capability to a capability config
54
+ exportedCapabilities.push({
55
+ name: capability.name,
56
+ description: capability.description,
57
+ namespaces: capability.namespaces,
58
+ bindings: capability.bindings,
59
+ });
60
+ }
61
+
62
+ process.send(exportedCapabilities);
63
+
48
64
  return;
49
65
  }
50
66
 
51
- this._controller = new Controller(config, capabilities, opts.beforeHook, opts.afterHook);
67
+ this.#controller = new Controller(config, capabilities, opts.beforeHook, opts.afterHook);
52
68
 
53
69
  // Stop processing if deferStart is set to true
54
70
  if (opts.deferStart) {
@@ -65,6 +81,6 @@ export class PeprModule {
65
81
  * @param port
66
82
  */
67
83
  start(port = 3000) {
68
- this._controller.startServer(port);
84
+ this.#controller.startServer(port);
69
85
  }
70
86
  }
@@ -4,29 +4,30 @@
4
4
  import jsonPatch from "fast-json-patch";
5
5
 
6
6
  import { Capability } from "./capability";
7
+ import { Errors } from "./errors";
7
8
  import { shouldSkipRequest } from "./filter";
8
- import { Request, Response } from "./k8s/types";
9
+ import { MutateResponse, Request } from "./k8s/types";
9
10
  import { Secret } from "./k8s/upstream";
10
11
  import Log from "./logger";
11
- import { PeprRequest } from "./request";
12
+ import { PeprMutateRequest } from "./mutate-request";
12
13
  import { ModuleConfig } from "./types";
13
- import { convertFromBase64Map, convertToBase64Map } from "./utils";
14
+ import { base64Encode, convertFromBase64Map, convertToBase64Map } from "./utils";
14
15
 
15
- export async function processor(
16
+ export async function mutateProcessor(
16
17
  config: ModuleConfig,
17
18
  capabilities: Capability[],
18
19
  req: Request,
19
- parentPrefix: string
20
- ): Promise<Response> {
21
- const wrapped = new PeprRequest(req);
22
- const response: Response = {
20
+ reqMetadata: Record<string, string>,
21
+ ): Promise<MutateResponse> {
22
+ const wrapped = new PeprMutateRequest(req);
23
+ const response: MutateResponse = {
23
24
  uid: req.uid,
24
25
  warnings: [],
25
26
  allowed: false,
26
27
  };
27
28
 
28
29
  // Track whether any capability matched the request
29
- let matchedCapabilityAction = false;
30
+ let matchedAction = false;
30
31
 
31
32
  // Track data fields that should be skipped during decoding
32
33
  let skipDecode: string[] = [];
@@ -37,21 +38,26 @@ export async function processor(
37
38
  skipDecode = convertFromBase64Map(wrapped.Raw as unknown as Secret);
38
39
  }
39
40
 
40
- Log.info(`Processing request`, parentPrefix);
41
+ Log.info(reqMetadata, `Processing request`);
41
42
 
42
43
  for (const { name, bindings } of capabilities) {
43
- const prefix = `${parentPrefix} ${name}:`;
44
+ const actionMetadata = { ...reqMetadata, name };
44
45
 
45
46
  for (const action of bindings) {
47
+ // Skip this action if it's not a mutate action
48
+ if (!action.mutateCallback) {
49
+ continue;
50
+ }
51
+
46
52
  // Continue to the next action without doing anything if this one should be skipped
47
53
  if (shouldSkipRequest(action, req)) {
48
54
  continue;
49
55
  }
50
56
 
51
- const label = action.callback.name;
52
- Log.info(`Processing matched action ${label}`, prefix);
57
+ const label = action.mutateCallback.name;
58
+ Log.info(actionMetadata, `Processing matched action ${label}`);
53
59
 
54
- matchedCapabilityAction = true;
60
+ matchedAction = true;
55
61
 
56
62
  // Add annotations to the request to indicate that the capability started processing
57
63
  // this will allow tracking of failed mutations that were permitted to continue
@@ -71,25 +77,30 @@ export async function processor(
71
77
 
72
78
  try {
73
79
  // Run the action
74
- await action.callback(wrapped);
80
+ await action.mutateCallback(wrapped);
75
81
 
76
- Log.info(`Action succeeded`, prefix);
82
+ Log.info(actionMetadata, `Action succeeded`);
77
83
 
78
84
  // Add annotations to the request to indicate that the capability succeeded
79
85
  updateStatus("succeeded");
80
86
  } catch (e) {
87
+ Log.warn(actionMetadata, `Action failed: ${e}`);
88
+ updateStatus("warning");
89
+
81
90
  // Annoying ts false positive
82
91
  response.warnings = response.warnings || [];
83
92
  response.warnings.push(`Action failed: ${e}`);
84
93
 
85
- // If errors are not allowed, note the failure in the Response
86
- if (config.onError) {
87
- Log.error(`Action failed: ${e}`, prefix);
88
- response.result = "Pepr module configured to reject on error";
89
- return response;
90
- } else {
91
- Log.warn(`Action failed: ${e}`, prefix);
92
- updateStatus("warning");
94
+ switch (config.onError) {
95
+ case Errors.reject:
96
+ Log.error(actionMetadata, `Action failed: ${e}`);
97
+ response.result = "Pepr module configured to reject on error";
98
+ return response;
99
+
100
+ case Errors.audit:
101
+ response.auditAnnotations = response.auditAnnotations || {};
102
+ response.auditAnnotations[Date.now()] = e;
103
+ break;
93
104
  }
94
105
  }
95
106
  }
@@ -99,8 +110,8 @@ export async function processor(
99
110
  response.allowed = true;
100
111
 
101
112
  // If no capability matched the request, exit early
102
- if (!matchedCapabilityAction) {
103
- Log.info(`No matching capability action found`, parentPrefix);
113
+ if (!matchedAction) {
114
+ Log.info(reqMetadata, `No matching actions found`);
104
115
  return response;
105
116
  }
106
117
 
@@ -124,7 +135,7 @@ export async function processor(
124
135
  response.patchType = "JSONPatch";
125
136
  // Webhook must be base64-encoded
126
137
  // https://kubernetes.io/docs/reference/access-authn-authz/extensible-admission-controllers/#response
127
- response.patch = Buffer.from(JSON.stringify(patches)).toString("base64");
138
+ response.patch = base64Encode(JSON.stringify(patches));
128
139
  }
129
140
 
130
141
  // Remove the warnings array if it's empty
@@ -132,7 +143,7 @@ export async function processor(
132
143
  delete response.warnings;
133
144
  }
134
145
 
135
- Log.debug(patches, parentPrefix);
146
+ Log.debug({ ...reqMetadata, patches }, `Patches generated`);
136
147
 
137
148
  return response;
138
149
  }