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
@@ -1,62 +1,60 @@
1
1
  // SPDX-License-Identifier: Apache-2.0
2
2
  // SPDX-FileCopyrightText: 2023-Present The Pepr Authors
3
3
 
4
- import { modelToGroupVersionKind } from "./k8s/index";
4
+ import { pickBy } from "ramda";
5
+
6
+ import { isWatchMode, modelToGroupVersionKind } from "./k8s/index";
5
7
  import { GroupVersionKind } from "./k8s/types";
6
- import logger from "./logger";
8
+ import Log from "./logger";
7
9
  import {
8
- BindToAction,
9
10
  Binding,
10
11
  BindingFilter,
11
12
  BindingWithName,
12
- CapabilityAction,
13
13
  CapabilityCfg,
14
- DeepPartial,
14
+ CapabilityExport,
15
15
  Event,
16
16
  GenericClass,
17
- HookPhase,
17
+ MutateAction,
18
+ MutateActionChain,
19
+ ValidateAction,
18
20
  WhenSelector,
19
21
  } from "./types";
20
22
 
21
23
  /**
22
24
  * A capability is a unit of functionality that can be registered with the Pepr runtime.
23
25
  */
24
- export class Capability implements CapabilityCfg {
25
- private _name: string;
26
- private _description: string;
27
- private _namespaces?: string[] | undefined;
28
-
29
- // Currently everything is considered a mutation
30
- private _mutateOrValidate = HookPhase.mutate;
31
-
32
- private _bindings: Binding[] = [];
33
-
34
- get bindings(): Binding[] {
35
- return this._bindings;
26
+ export class Capability implements CapabilityExport {
27
+ #name: string;
28
+ #description: string;
29
+ #namespaces?: string[] | undefined;
30
+ #bindings: Binding[] = [];
31
+
32
+ get bindings() {
33
+ return this.#bindings;
36
34
  }
37
35
 
38
36
  get name() {
39
- return this._name;
37
+ return this.#name;
40
38
  }
41
39
 
42
40
  get description() {
43
- return this._description;
41
+ return this.#description;
44
42
  }
45
43
 
46
44
  get namespaces() {
47
- return this._namespaces || [];
48
- }
49
-
50
- get mutateOrValidate() {
51
- return this._mutateOrValidate;
45
+ return this.#namespaces || [];
52
46
  }
53
47
 
54
48
  constructor(cfg: CapabilityCfg) {
55
- this._name = cfg.name;
56
- this._description = cfg.description;
57
- this._namespaces = cfg.namespaces;
58
- logger.info(`Capability ${this._name} registered`);
59
- logger.debug(cfg);
49
+ this.#name = cfg.name;
50
+ this.#description = cfg.description;
51
+ this.#namespaces = cfg.namespaces;
52
+
53
+ // Bind When() to this instance
54
+ this.When = this.When.bind(this);
55
+
56
+ Log.info(`Capability ${this.#name} registered`);
57
+ Log.debug(cfg);
60
58
  }
61
59
 
62
60
  /**
@@ -68,7 +66,7 @@ export class Capability implements CapabilityCfg {
68
66
  * @param kind if using a custom KubernetesObject not available in `a.*`, specify the GroupVersionKind
69
67
  * @returns
70
68
  */
71
- When = <T extends GenericClass>(model: T, kind?: GroupVersionKind): WhenSelector<T> => {
69
+ When<T extends GenericClass>(model: T, kind?: GroupVersionKind): WhenSelector<T> {
72
70
  const matchedKind = modelToGroupVersionKind(model.name);
73
71
 
74
72
  // If the kind is not specified and the model is not a KubernetesObject, throw an error
@@ -86,69 +84,83 @@ export class Capability implements CapabilityCfg {
86
84
  labels: {},
87
85
  annotations: {},
88
86
  },
89
- callback: () => undefined,
90
87
  };
91
88
 
92
- const prefix = `${this._name}: ${model.name}`;
89
+ const bindings = this.#bindings;
90
+ const prefix = `${this.#name}: ${model.name}`;
91
+ const commonChain = { WithLabel, WithAnnotation, Mutate, Validate };
92
+ const isNotEmpty = (value: object) => Object.keys(value).length > 0;
93
+ const log = (message: string, cbString: string) => {
94
+ const filteredObj = pickBy(isNotEmpty, binding.filters);
93
95
 
94
- logger.info(`Binding created`, prefix);
96
+ Log.info(`${message} configured for ${binding.event}`, prefix);
97
+ Log.info(filteredObj, prefix);
98
+ Log.debug(cbString, prefix);
99
+ };
95
100
 
96
- const Then = (cb: CapabilityAction<T>): BindToAction<T> => {
97
- logger.info(`Binding action created`, prefix);
98
- logger.debug(cb.toString(), prefix);
99
- // Push the binding to the list of bindings for this capability as a new BindingAction
100
- // with the callback function to preserve
101
- this._bindings.push({
102
- ...binding,
103
- callback: cb,
104
- });
101
+ function Validate(validateCallback: ValidateAction<T>): void {
102
+ if (!isWatchMode) {
103
+ log("Validate Action", validateCallback.toString());
104
+
105
+ // Push the binding to the list of bindings for this capability as a new BindingAction
106
+ // with the callback function to preserve
107
+ bindings.push({
108
+ ...binding,
109
+ isValidate: true,
110
+ validateCallback,
111
+ });
112
+ }
113
+ }
105
114
 
106
- // Now only allow adding actions to the same binding
107
- return { Then };
108
- };
115
+ function Mutate(mutateCallback: MutateAction<T>): MutateActionChain<T> {
116
+ if (!isWatchMode) {
117
+ log("Mutate Action", mutateCallback.toString());
109
118
 
110
- const ThenSet = (merge: DeepPartial<InstanceType<T>>): BindToAction<T> => {
111
- // Add the new action to the binding
112
- Then(req => req.Merge(merge));
119
+ // Push the binding to the list of bindings for this capability as a new BindingAction
120
+ // with the callback function to preserve
121
+ bindings.push({
122
+ ...binding,
123
+ isMutate: true,
124
+ mutateCallback,
125
+ });
126
+ }
113
127
 
114
- return { Then };
115
- };
128
+ // Now only allow adding actions to the same binding
129
+ return { Validate };
130
+ }
116
131
 
117
132
  function InNamespace(...namespaces: string[]): BindingWithName<T> {
118
- logger.debug(`Add namespaces filter ${namespaces}`, prefix);
133
+ Log.debug(`Add namespaces filter ${namespaces}`, prefix);
119
134
  binding.filters.namespaces.push(...namespaces);
120
- return { WithLabel, WithAnnotation, WithName, Then, ThenSet };
135
+ return { ...commonChain, WithName };
121
136
  }
122
137
 
123
138
  function WithName(name: string): BindingFilter<T> {
124
- logger.debug(`Add name filter ${name}`, prefix);
139
+ Log.debug(`Add name filter ${name}`, prefix);
125
140
  binding.filters.name = name;
126
- return { WithLabel, WithAnnotation, Then, ThenSet };
141
+ return commonChain;
127
142
  }
128
143
 
129
144
  function WithLabel(key: string, value = ""): BindingFilter<T> {
130
- logger.debug(`Add label filter ${key}=${value}`, prefix);
145
+ Log.debug(`Add label filter ${key}=${value}`, prefix);
131
146
  binding.filters.labels[key] = value;
132
- return { WithLabel, WithAnnotation, Then, ThenSet };
147
+ return commonChain;
133
148
  }
134
149
 
135
- const WithAnnotation = (key: string, value = ""): BindingFilter<T> => {
136
- logger.debug(`Add annotation filter ${key}=${value}`, prefix);
150
+ function WithAnnotation(key: string, value = ""): BindingFilter<T> {
151
+ Log.debug(`Add annotation filter ${key}=${value}`, prefix);
137
152
  binding.filters.annotations[key] = value;
138
- return { WithLabel, WithAnnotation, Then, ThenSet };
139
- };
153
+ return commonChain;
154
+ }
140
155
 
141
- const bindEvent = (event: Event) => {
156
+ function bindEvent(event: Event) {
142
157
  binding.event = event;
143
158
  return {
159
+ ...commonChain,
144
160
  InNamespace,
145
- Then,
146
- ThenSet,
147
- WithAnnotation,
148
- WithLabel,
149
161
  WithName,
150
162
  };
151
- };
163
+ }
152
164
 
153
165
  return {
154
166
  IsCreatedOrUpdated: () => bindEvent(Event.CreateOrUpdate),
@@ -156,5 +168,5 @@ export class Capability implements CapabilityCfg {
156
168
  IsUpdated: () => bindEvent(Event.Update),
157
169
  IsDeleted: () => bindEvent(Event.Delete),
158
170
  };
159
- };
171
+ }
160
172
  }
@@ -1,58 +1,73 @@
1
1
  // SPDX-License-Identifier: Apache-2.0
2
2
  // SPDX-FileCopyrightText: 2023-Present The Pepr Authors
3
3
 
4
- import express from "express";
4
+ import express, { NextFunction } from "express";
5
5
  import fs from "fs";
6
6
  import https from "https";
7
7
 
8
8
  import { Capability } from "./capability";
9
- import { Request, Response } from "./k8s/types";
9
+ import { isWatchMode } from "./k8s";
10
+ import { MutateResponse, Request, ValidateResponse } from "./k8s/types";
10
11
  import Log from "./logger";
11
- import { processor } from "./processor";
12
- import { ModuleConfig } from "./types";
13
12
  import { MetricsCollector } from "./metrics";
13
+ import { mutateProcessor } from "./mutate-processor";
14
+ import { ModuleConfig } from "./types";
15
+ import { validateProcessor } from "./validate-processor";
14
16
 
15
17
  export class Controller {
16
- private readonly app = express();
17
- private running = false;
18
- private metricsCollector = new MetricsCollector("pepr");
18
+ // Track whether the server is running
19
+ #running = false;
20
+
21
+ // Metrics collector
22
+ #metricsCollector = new MetricsCollector("pepr");
19
23
 
20
24
  // The token used to authenticate requests
21
- private token = "";
25
+ #token = "";
26
+
27
+ // The express app instance
28
+ readonly #app = express();
29
+
30
+ // Initialized with the constructor
31
+ readonly #config: ModuleConfig;
32
+ readonly #capabilities: Capability[];
33
+ readonly #beforeHook?: (req: Request) => void;
34
+ readonly #afterHook?: (res: MutateResponse) => void;
22
35
 
23
36
  constructor(
24
- private readonly config: ModuleConfig,
25
- private readonly capabilities: Capability[],
26
- private readonly beforeHook?: (req: Request) => void,
27
- private readonly afterHook?: (res: Response) => void
37
+ config: ModuleConfig,
38
+ capabilities: Capability[],
39
+ beforeHook?: (req: Request) => void,
40
+ afterHook?: (res: MutateResponse) => void,
28
41
  ) {
29
- // Middleware for logging requests
30
- this.app.use(this.logger);
31
-
32
- // Middleware for parsing JSON, limit to 2mb vs 100K for K8s compatibility
33
- this.app.use(express.json({ limit: "2mb" }));
42
+ this.#config = config;
43
+ this.#capabilities = capabilities;
44
+ this.#beforeHook = beforeHook;
45
+ this.#afterHook = afterHook;
34
46
 
35
- // Health check endpoint
36
- this.app.get("/healthz", this.healthz);
47
+ // Bind public methods
48
+ this.startServer = this.startServer.bind(this);
37
49
 
38
- // Metrics endpoint
39
- this.app.get("/metrics", this.metrics);
50
+ // Middleware for logging requests
51
+ this.#app.use(Controller.#logger);
40
52
 
41
- // Mutate endpoint
42
- this.app.post("/mutate/:token", this.mutate);
53
+ // Middleware for parsing JSON, limit to 2mb vs 100K for K8s compatibility
54
+ this.#app.use(express.json({ limit: "2mb" }));
43
55
 
44
56
  if (beforeHook) {
45
- console.info(`Using beforeHook: ${beforeHook}`);
57
+ Log.info(`Using beforeHook: ${beforeHook}`);
46
58
  }
47
59
 
48
60
  if (afterHook) {
49
- console.info(`Using afterHook: ${afterHook}`);
61
+ Log.info(`Using afterHook: ${afterHook}`);
50
62
  }
63
+
64
+ // Bind endpoints
65
+ this.#bindEndpoints();
51
66
  }
52
67
 
53
68
  /** Start the webhook server */
54
- public startServer = (port: number) => {
55
- if (this.running) {
69
+ startServer(port: number) {
70
+ if (this.#running) {
56
71
  throw new Error("Cannot start Pepr module: Pepr module was not instantiated with deferStart=true");
57
72
  }
58
73
 
@@ -62,29 +77,32 @@ export class Controller {
62
77
  cert: fs.readFileSync(process.env.SSL_CERT_PATH || "/etc/certs/tls.crt"),
63
78
  };
64
79
 
65
- // Get the API token from the environment variable or the mounted secret
66
- this.token = process.env.PEPR_API_TOKEN || fs.readFileSync("/app/api-token/value").toString().trim();
67
- console.info(`Using API token: ${this.token}`);
80
+ // Get the API token if not in watch mode
81
+ if (!isWatchMode) {
82
+ // Get the API token from the environment variable or the mounted secret
83
+ this.#token = process.env.PEPR_API_TOKEN || fs.readFileSync("/app/api-token/value").toString().trim();
84
+ Log.info(`Using API token: ${this.#token}`);
68
85
 
69
- if (!this.token) {
70
- throw new Error("API token not found");
86
+ if (!this.#token) {
87
+ throw new Error("API token not found");
88
+ }
71
89
  }
72
90
 
73
91
  // Create HTTPS server
74
- const server = https.createServer(options, this.app).listen(port);
92
+ const server = https.createServer(options, this.#app).listen(port);
75
93
 
76
94
  // Handle server listening event
77
95
  server.on("listening", () => {
78
- console.log(`Server listening on port ${port}`);
96
+ Log.info(`Server listening on port ${port}`);
79
97
  // Track that the server is running
80
- this.running = true;
98
+ this.#running = true;
81
99
  });
82
100
 
83
101
  // Handle EADDRINUSE errors
84
102
  server.on("error", (e: { code: string }) => {
85
103
  if (e.code === "EADDRINUSE") {
86
- console.log(
87
- `Address in use, retrying in 2 seconds. If this persists, ensure ${port} is not in use, e.g. "lsof -i :${port}"`
104
+ Log.warn(
105
+ `Address in use, retrying in 2 seconds. If this persists, ensure ${port} is not in use, e.g. "lsof -i :${port}"`,
88
106
  );
89
107
  setTimeout(() => {
90
108
  server.close();
@@ -95,92 +113,174 @@ export class Controller {
95
113
 
96
114
  // Listen for the SIGTERM signal and gracefully close the server
97
115
  process.on("SIGTERM", () => {
98
- console.log("Received SIGTERM, closing server");
116
+ Log.info("Received SIGTERM, closing server");
99
117
  server.close(() => {
100
- console.log("Server closed");
118
+ Log.info("Server closed");
101
119
  process.exit(0);
102
120
  });
103
121
  });
104
- };
122
+ }
105
123
 
106
- private logger = (req: express.Request, res: express.Response, next: express.NextFunction) => {
107
- const startTime = Date.now();
124
+ #bindEndpoints = () => {
125
+ // Health check endpoint
126
+ this.#app.get("/healthz", Controller.#healthz);
108
127
 
109
- res.on("finish", () => {
110
- const now = new Date().toISOString();
111
- const elapsedTime = Date.now() - startTime;
112
- const message = `[${now}] ${req.method} ${req.originalUrl} [${res.statusCode}] ${elapsedTime} ms\n`;
128
+ // Metrics endpoint
129
+ this.#app.get("/metrics", this.#metrics);
113
130
 
114
- res.statusCode >= 400 ? console.error(message) : console.info(message);
115
- });
131
+ if (isWatchMode) {
132
+ return;
133
+ }
116
134
 
117
- next();
135
+ // Require auth for webhook endpoints
136
+ this.#app.use(["/mutate/:token", "/validate/:token"], this.#validateToken);
137
+
138
+ // Mutate endpoint
139
+ this.#app.post("/mutate/:token", this.#admissionReq("Mutate"));
140
+
141
+ // Validate endpoint
142
+ this.#app.post("/validate/:token", this.#admissionReq("Validate"));
118
143
  };
119
144
 
120
- private healthz = (req: express.Request, res: express.Response) => {
121
- try {
122
- res.send("OK");
123
- } catch (err) {
124
- console.error(err);
125
- res.status(500).send("Internal Server Error");
145
+ /**
146
+ * Validate the token in the request path
147
+ *
148
+ * @param req The incoming request
149
+ * @param res The outgoing response
150
+ * @param next The next middleware function
151
+ * @returns
152
+ */
153
+ #validateToken = (req: express.Request, res: express.Response, next: NextFunction) => {
154
+ // Validate the token
155
+ const { token } = req.params;
156
+ if (token !== this.#token) {
157
+ const err = `Unauthorized: invalid token '${token.replace(/[^\w]/g, "_")}'`;
158
+ Log.warn(err);
159
+ res.status(401).send(err);
160
+ this.#metricsCollector.alert();
161
+ return;
126
162
  }
163
+
164
+ // Token is valid, continue
165
+ next();
127
166
  };
128
167
 
129
- private metrics = async (req: express.Request, res: express.Response) => {
168
+ /**
169
+ * Metrics endpoint handler
170
+ *
171
+ * @param req the incoming request
172
+ * @param res the outgoing response
173
+ */
174
+ #metrics = async (req: express.Request, res: express.Response) => {
130
175
  try {
131
- res.send(await this.metricsCollector.getMetrics());
176
+ res.send(await this.#metricsCollector.getMetrics());
132
177
  } catch (err) {
133
- console.error(err);
178
+ Log.error(err);
134
179
  res.status(500).send("Internal Server Error");
135
180
  }
136
181
  };
137
182
 
138
- private mutate = async (req: express.Request, res: express.Response) => {
139
- const startTime = this.metricsCollector.observeStart();
140
-
141
- try {
142
- // Validate the token
143
- const { token } = req.params;
144
- if (token !== this.token) {
145
- const err = `Unauthorized: invalid token '${token.replace(/[^\w]/g, "_")}'`;
146
- console.warn(err);
147
- res.status(401).send(err);
148
- this.metricsCollector.alert();
149
- return;
183
+ /**
184
+ * Admission request handler for both mutate and validate requests
185
+ *
186
+ * @param admissionKind the type of admission request
187
+ * @returns the request handler
188
+ */
189
+ #admissionReq = (admissionKind: "Mutate" | "Validate") => {
190
+ // Create the admission request handler
191
+ return async (req: express.Request, res: express.Response) => {
192
+ // Start the metrics timer
193
+ const startTime = this.#metricsCollector.observeStart();
194
+
195
+ try {
196
+ // Get the request from the body or create an empty request
197
+ const request: Request = req.body?.request || ({} as Request);
198
+
199
+ // Run the before hook if it exists
200
+ this.#beforeHook && this.#beforeHook(request || {});
201
+
202
+ // Setup identifiers for logging
203
+ const name = request?.name ? `/${request.name}` : "";
204
+ const namespace = request?.namespace || "";
205
+ const gvk = request?.kind || { group: "", version: "", kind: "" };
206
+
207
+ const reqMetadata = {
208
+ uid: request.uid,
209
+ namespace,
210
+ name,
211
+ };
212
+
213
+ Log.info({ ...reqMetadata, gvk, operation: request.operation, admissionKind }, "Incoming request");
214
+ Log.debug({ ...reqMetadata, request }, "Incoming request body");
215
+
216
+ // Process the request
217
+ let response: MutateResponse | ValidateResponse;
218
+
219
+ // Call mutate or validate based on the admission kind
220
+ if (admissionKind === "Mutate") {
221
+ response = await mutateProcessor(this.#config, this.#capabilities, request, reqMetadata);
222
+ } else {
223
+ response = await validateProcessor(this.#capabilities, request, reqMetadata);
224
+ }
225
+
226
+ // Run the after hook if it exists
227
+ this.#afterHook && this.#afterHook(response);
228
+
229
+ // Log the response
230
+ Log.debug({ ...reqMetadata, response }, "Outgoing response");
231
+
232
+ // Send a no prob bob response
233
+ res.send({
234
+ apiVersion: "admission.k8s.io/v1",
235
+ kind: "AdmissionReview",
236
+ response,
237
+ });
238
+ this.#metricsCollector.observeEnd(startTime, admissionKind);
239
+ } catch (err) {
240
+ Log.error(err);
241
+ res.status(500).send("Internal Server Error");
242
+ this.#metricsCollector.error();
150
243
  }
244
+ };
245
+ };
151
246
 
152
- const request: Request = req.body?.request || ({} as Request);
153
-
154
- // Run the before hook if it exists
155
- this.beforeHook && this.beforeHook(request || {});
156
-
157
- const name = request?.name ? `/${request.name}` : "";
158
- const namespace = request?.namespace || "";
159
- const gvk = request?.kind || { group: "", version: "", kind: "" };
160
- const prefix = `${request.uid} ${namespace}${name}`;
161
-
162
- Log.info(`Mutate [${request.operation}] ${gvk.group}/${gvk.version}/${gvk.kind}`, prefix);
163
-
164
- // Process the request
165
- const response = await processor(this.config, this.capabilities, request, prefix);
166
-
167
- // Run the after hook if it exists
168
- this.afterHook && this.afterHook(response);
247
+ /**
248
+ * Middleware for logging requests
249
+ *
250
+ * @param req the incoming request
251
+ * @param res the outgoing response
252
+ * @param next the next middleware function
253
+ */
254
+ static #logger(req: express.Request, res: express.Response, next: express.NextFunction) {
255
+ const startTime = Date.now();
169
256
 
170
- // Log the response
171
- Log.debug(response, prefix);
257
+ res.on("finish", () => {
258
+ const elapsedTime = Date.now() - startTime;
259
+ const message = {
260
+ uid: req.body?.request?.uid,
261
+ method: req.method,
262
+ url: req.originalUrl,
263
+ status: res.statusCode,
264
+ duration: `${elapsedTime} ms`,
265
+ };
266
+
267
+ res.statusCode >= 300 ? Log.warn(message) : Log.info(message);
268
+ });
172
269
 
173
- // Send a no prob bob response
174
- res.send({
175
- apiVersion: "admission.k8s.io/v1",
176
- kind: "AdmissionReview",
177
- response,
178
- });
179
- this.metricsCollector.observeEnd(startTime);
270
+ next();
271
+ }
272
+ /**
273
+ * Health check endpoint handler
274
+ *
275
+ * @param req the incoming request
276
+ * @param res the outgoing response
277
+ */
278
+ static #healthz(req: express.Request, res: express.Response) {
279
+ try {
280
+ res.send("OK");
180
281
  } catch (err) {
181
- console.error(err);
282
+ Log.error(err);
182
283
  res.status(500).send("Internal Server Error");
183
- this.metricsCollector.error();
184
284
  }
185
- };
285
+ }
186
286
  }
@@ -0,0 +1,20 @@
1
+ // SPDX-License-Identifier: Apache-2.0
2
+ // SPDX-FileCopyrightText: 2023-Present The Pepr Authors
3
+
4
+ export const Errors = {
5
+ audit: "audit",
6
+ ignore: "ignore",
7
+ reject: "reject",
8
+ };
9
+
10
+ export const ErrorList = Object.values(Errors);
11
+
12
+ /**
13
+ * Validate the error or throw an error
14
+ * @param error
15
+ */
16
+ export function ValidateError(error = "") {
17
+ if (!ErrorList.includes(error)) {
18
+ throw new Error(`Invalid error: ${error}. Must be one of: ${ErrorList.join(", ")}`);
19
+ }
20
+ }
package/src/lib/fetch.ts CHANGED
@@ -30,7 +30,7 @@ export type FetchResponse<T> = {
30
30
  export async function fetch<T>(url: URL | RequestInfo, init?: RequestInit): Promise<FetchResponse<T>> {
31
31
  let data = undefined as unknown as T;
32
32
  try {
33
- logger.debug(`Fetching ${url}`);
33
+ logger.debug(init, `Fetching ${url}`);
34
34
 
35
35
  const resp = await fetchRaw(url, init);
36
36
  const contentType = resp.headers.get("content-type") || "";
package/src/lib/filter.ts CHANGED
@@ -8,7 +8,7 @@ import { Binding, Event } from "./types";
8
8
  /**
9
9
  * shouldSkipRequest determines if a request should be skipped based on the binding filters.
10
10
  *
11
- * @param binding the capability action binding
11
+ * @param binding the action binding
12
12
  * @param req the incoming request
13
13
  * @returns
14
14
  */
@@ -20,8 +20,6 @@ export function shouldSkipRequest(binding: Binding, req: Request) {
20
20
  const srcObject = operation === Operation.DELETE ? req.oldObject : req.object;
21
21
  const { metadata } = srcObject || {};
22
22
 
23
- console.log(metadata);
24
-
25
23
  // Test for matching operation
26
24
  if (!binding.event.includes(operation) && !binding.event.includes(Event.Any)) {
27
25
  return true;
@@ -3,9 +3,12 @@
3
3
 
4
4
  // Export kinds as a single object
5
5
  import * as kind from "./upstream";
6
- /** a is a collection of K8s types to be used within a CapabilityAction: `When(a.Configmap)` */
6
+ /** a is a collection of K8s types to be used within a action: `When(a.Configmap)` */
7
7
  export { kind as a };
8
8
 
9
9
  export { modelToGroupVersionKind, gvkMap, RegisterKind } from "./kinds";
10
10
 
11
+ // If the hostname is pepr-static-test-watcher-0, then we are running in watch mode
12
+ export const isWatchMode = process.env.PEPR_WATCH_MODE === "true";
13
+
11
14
  export * from "./types";