pepr 0.6.1 → 0.7.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.6.1",
12
+ "version": "0.7.1",
13
13
  "main": "dist/lib.js",
14
14
  "types": "dist/lib.d.ts",
15
15
  "scripts": {
@@ -34,14 +34,14 @@
34
34
  "ramda": "0.29.0"
35
35
  },
36
36
  "devDependencies": {
37
- "@types/eslint": "8.40.0",
37
+ "@types/eslint": "8.40.2",
38
38
  "@types/express": "4.17.17",
39
39
  "@types/node-fetch": "2.6.4",
40
40
  "@types/node-forge": "1.3.2",
41
41
  "@types/prettier": "2.7.3",
42
42
  "@types/prompts": "2.4.4",
43
43
  "@types/ramda": "0.29.2",
44
- "@types/uuid": "9.0.1",
44
+ "@types/uuid": "9.0.2",
45
45
  "ava": "5.3.0",
46
46
  "nock": "13.3.1"
47
47
  },
package/src/cli.ts CHANGED
@@ -12,6 +12,7 @@ import init from "./cli/init/index";
12
12
  import { version } from "./cli/init/templates";
13
13
  import { RootCmd } from "./cli/root";
14
14
  import update from "./cli/update";
15
+ import { Log } from "./lib";
15
16
 
16
17
  const program = new RootCmd();
17
18
 
@@ -22,6 +23,10 @@ program
22
23
  if (program.args.length < 1) {
23
24
  console.log(banner);
24
25
  program.help();
26
+ } else {
27
+ Log.error(`Invalid command '${program.args.join(" ")}'\n`);
28
+ program.outputHelp();
29
+ process.exitCode = 1;
25
30
  }
26
31
  });
27
32
 
@@ -79,6 +79,7 @@ export class Capability implements CapabilityCfg {
79
79
  const binding: Binding = {
80
80
  // If the kind is not specified, use the matched kind from the model
81
81
  kind: kind || matchedKind,
82
+ event: Event.Any,
82
83
  filters: {
83
84
  name: "",
84
85
  namespaces: [],
@@ -10,12 +10,6 @@ import { processor } from "./processor";
10
10
  import { ModuleConfig } from "./types";
11
11
  import Log from "./logger";
12
12
 
13
- // Load SSL certificate and key
14
- const options = {
15
- key: fs.readFileSync(process.env.SSL_KEY_PATH || "/etc/certs/tls.key"),
16
- cert: fs.readFileSync(process.env.SSL_CERT_PATH || "/etc/certs/tls.crt"),
17
- };
18
-
19
13
  export class Controller {
20
14
  private readonly app = express();
21
15
  private running = false;
@@ -53,6 +47,12 @@ export class Controller {
53
47
  throw new Error("Cannot start Pepr module: Pepr module was not instantiated with deferStart=true");
54
48
  }
55
49
 
50
+ // Load SSL certificate and key
51
+ const options = {
52
+ key: fs.readFileSync(process.env.SSL_KEY_PATH || "/etc/certs/tls.key"),
53
+ cert: fs.readFileSync(process.env.SSL_CERT_PATH || "/etc/certs/tls.crt"),
54
+ };
55
+
56
56
  // Create HTTPS server
57
57
  const server = https.createServer(options, this.app).listen(port);
58
58
 
@@ -66,7 +66,9 @@ export class Controller {
66
66
  // Handle EADDRINUSE errors
67
67
  server.on("error", (e: { code: string }) => {
68
68
  if (e.code === "EADDRINUSE") {
69
- console.log("Address in use, retrying...");
69
+ console.log(
70
+ `Address in use, retrying in 2 seconds. If this persists, ensure ${port} is not in use, e.g. "lsof -i :${port}"`
71
+ );
70
72
  setTimeout(() => {
71
73
  server.close();
72
74
  server.listen(port);
package/src/lib/filter.ts CHANGED
@@ -3,7 +3,7 @@
3
3
 
4
4
  import { Request } from "./k8s/types";
5
5
  import logger from "./logger";
6
- import { Binding } from "./types";
6
+ import { Binding, Event } from "./types";
7
7
 
8
8
  /**
9
9
  * shouldSkipRequest determines if a request should be skipped based on the binding filters.
@@ -18,7 +18,7 @@ export function shouldSkipRequest(binding: Binding, req: Request) {
18
18
  const { metadata } = req.object || {};
19
19
 
20
20
  // Test for matching operation
21
- if (binding.event && !binding.event.includes(req.operation)) {
21
+ if (!binding.event.includes(req.operation) && !binding.event.includes(Event.Any)) {
22
22
  return true;
23
23
  }
24
24
 
@@ -25,6 +25,7 @@ export const gvkMap: Record<string, GroupVersionKind> = {
25
25
  kind: "Endpoints",
26
26
  version: "v1",
27
27
  group: "",
28
+ plural: "endpoints",
28
29
  },
29
30
 
30
31
  /**
@@ -386,6 +387,7 @@ export const gvkMap: Record<string, GroupVersionKind> = {
386
387
  kind: "Ingress",
387
388
  version: "v1",
388
389
  group: "networking.k8s.io",
390
+ plural: "ingresses",
389
391
  },
390
392
 
391
393
  /**
@@ -44,6 +44,8 @@ export interface GroupVersionKind {
44
44
  readonly kind: string;
45
45
  readonly group: string;
46
46
  readonly version?: string;
47
+ /** Optional, override the plural name for use in Webhook rules generation */
48
+ readonly plural?: string;
47
49
  }
48
50
 
49
51
  /**
@@ -2,8 +2,8 @@
2
2
  // SPDX-FileCopyrightText: 2023-Present The Pepr Authors
3
3
 
4
4
  import {
5
- AdmissionregistrationV1Api,
6
- AdmissionregistrationV1WebhookClientConfig,
5
+ AdmissionregistrationV1Api as AdmissionRegV1API,
6
+ AdmissionregistrationV1WebhookClientConfig as AdmissionRegnV1WebhookClientCfg,
7
7
  AppsV1Api,
8
8
  CoreV1Api,
9
9
  HttpError,
@@ -17,15 +17,20 @@ import {
17
17
  V1MutatingWebhookConfiguration,
18
18
  V1Namespace,
19
19
  V1NetworkPolicy,
20
+ V1RuleWithOperations,
20
21
  V1Secret,
21
22
  V1Service,
22
23
  V1ServiceAccount,
23
24
  dumpYaml,
24
25
  } from "@kubernetes/client-node";
26
+ import { fork } from "child_process";
25
27
  import crypto from "crypto";
28
+ import { promises as fs } from "fs";
29
+ import { equals, uniqWith } from "ramda";
26
30
  import { gzipSync } from "zlib";
31
+
27
32
  import Log from "../logger";
28
- import { ModuleConfig } from "../types";
33
+ import { Binding, Event, HookPhase, ModuleConfig } from "../types";
29
34
  import { TLSOut, genTLS } from "./tls";
30
35
 
31
36
  const peprIgnore: V1LabelSelectorRequirement = {
@@ -132,7 +137,95 @@ export class Webhook {
132
137
  };
133
138
  }
134
139
 
135
- mutatingWebhook(timeoutSeconds = 10): V1MutatingWebhookConfiguration {
140
+ generateWebhookRules(path: string): Promise<V1RuleWithOperations[]> {
141
+ return new Promise((resolve, reject) => {
142
+ const rules: V1RuleWithOperations[] = [];
143
+
144
+ // Add a default rule that allows all resources as a fallback
145
+ const defaultRule = {
146
+ apiGroups: ["*"],
147
+ apiVersions: ["*"],
148
+ operations: ["CREATE", "UPDATE", "DELETE"],
149
+ resources: ["*/*"],
150
+ };
151
+
152
+ // Fork is needed with the PEPR_MODE env var to ensure the module is loaded in build mode and will send back the capabilities
153
+ const program = fork(path, {
154
+ env: {
155
+ ...process.env,
156
+ LOG_LEVEL: "warn",
157
+ PEPR_MODE: "build",
158
+ },
159
+ });
160
+
161
+ // We are receiving javscript so the private fields are now public
162
+ interface ModuleCapabilities {
163
+ capabilities: {
164
+ _name: string;
165
+ _description: string;
166
+ _namespaces: string[];
167
+ _mutateOrValidate: HookPhase;
168
+ _bindings: Binding[];
169
+ }[];
170
+ }
171
+
172
+ // Wait for the module to send back the capabilities
173
+ program.on("message", message => {
174
+ // Cast the message to the ModuleCapabilities type
175
+ const { capabilities } = message.valueOf() as ModuleCapabilities;
176
+
177
+ for (const capability of capabilities) {
178
+ Log.info(`Module ${this.config.uuid} has capability: ${capability._name}`);
179
+
180
+ const { _bindings } = capability;
181
+
182
+ // Read the bindings and generate the rules
183
+ for (const binding of _bindings) {
184
+ const { event, kind } = binding;
185
+
186
+ const operations: string[] = [];
187
+
188
+ // CreateOrUpdate is a Pepr-specific event that is translated to Create and Update
189
+ if (event === Event.CreateOrUpdate) {
190
+ operations.push(Event.Create, Event.Update);
191
+ } else {
192
+ operations.push(event);
193
+ }
194
+
195
+ // Use the plural property if it exists, otherwise use lowercase kind + s
196
+ const resource = kind.plural || `${kind.kind.toLowerCase()}s`;
197
+
198
+ rules.push({
199
+ apiGroups: [kind.group],
200
+ apiVersions: [kind.version || "*"],
201
+ operations,
202
+ resources: [resource],
203
+ });
204
+ }
205
+ }
206
+ });
207
+
208
+ program.on("exit", code => {
209
+ if (code !== 0) {
210
+ reject(new Error(`Child process exited with code ${code}`));
211
+ } else {
212
+ // If there are no rules, add a catch-all
213
+ if (rules.length < 1) {
214
+ resolve([defaultRule]);
215
+ } else {
216
+ const reducedRules = uniqWith(equals, rules);
217
+ resolve(reducedRules);
218
+ }
219
+ }
220
+ });
221
+
222
+ program.on("error", error => {
223
+ reject(error);
224
+ });
225
+ });
226
+ }
227
+
228
+ async mutatingWebhook(path: string, timeoutSeconds = 10): Promise<V1MutatingWebhookConfiguration> {
136
229
  const { name } = this;
137
230
  const ignore = [peprIgnore];
138
231
 
@@ -145,7 +238,7 @@ export class Webhook {
145
238
  });
146
239
  }
147
240
 
148
- const clientConfig: AdmissionregistrationV1WebhookClientConfig = {
241
+ const clientConfig: AdmissionRegnV1WebhookClientCfg = {
149
242
  caBundle: this._tls.ca,
150
243
  };
151
244
 
@@ -161,6 +254,8 @@ export class Webhook {
161
254
  };
162
255
  }
163
256
 
257
+ const rules = await this.generateWebhookRules(path);
258
+
164
259
  return {
165
260
  apiVersion: "admissionregistration.k8s.io/v1",
166
261
  kind: "MutatingWebhookConfiguration",
@@ -179,15 +274,7 @@ export class Webhook {
179
274
  objectSelector: {
180
275
  matchExpressions: ignore,
181
276
  },
182
- // @todo: make this configurable
183
- rules: [
184
- {
185
- apiGroups: ["*"],
186
- apiVersions: ["*"],
187
- operations: ["CREATE", "UPDATE", "DELETE"],
188
- resources: ["*/*"],
189
- },
190
- ],
277
+ rules,
191
278
  // @todo: track side effects state
192
279
  sideEffects: "None",
193
280
  },
@@ -390,10 +477,14 @@ export class Webhook {
390
477
  return dumpYaml(zarfCfg, { noRefs: true });
391
478
  }
392
479
 
393
- allYaml(code: Buffer) {
480
+ async allYaml(path: string) {
481
+ const code = await fs.readFile(path);
482
+
394
483
  // Generate a hash of the code
395
484
  const hash = crypto.createHash("sha256").update(code).digest("hex");
396
485
 
486
+ const webhook = await this.mutatingWebhook(path);
487
+
397
488
  const resources = [
398
489
  this.namespace(),
399
490
  this.networkPolicy(),
@@ -401,7 +492,7 @@ export class Webhook {
401
492
  this.clusterRoleBinding(),
402
493
  this.serviceAccount(),
403
494
  this.tlsSecret(),
404
- this.mutatingWebhook(),
495
+ webhook,
405
496
  this.deployment(hash),
406
497
  this.service(),
407
498
  this.moduleSecret(code, hash),
@@ -411,7 +502,7 @@ export class Webhook {
411
502
  return resources.map(r => dumpYaml(r, { noRefs: true })).join("---\n");
412
503
  }
413
504
 
414
- async deploy(code?: Buffer, webhookTimeout?: number) {
505
+ async deploy(path: string, webhookTimeout?: number) {
415
506
  Log.info("Establishing connection to Kubernetes");
416
507
 
417
508
  const namespace = "pepr-system";
@@ -421,10 +512,7 @@ export class Webhook {
421
512
  kubeConfig.loadFromDefault();
422
513
 
423
514
  const coreV1Api = kubeConfig.makeApiClient(CoreV1Api);
424
- const rbacApi = kubeConfig.makeApiClient(RbacAuthorizationV1Api);
425
- const appsApi = kubeConfig.makeApiClient(AppsV1Api);
426
- const admissionApi = kubeConfig.makeApiClient(AdmissionregistrationV1Api);
427
- const networkApi = kubeConfig.makeApiClient(NetworkingV1Api);
515
+ const admissionApi = kubeConfig.makeApiClient(AdmissionRegV1API);
428
516
 
429
517
  const ns = this.namespace();
430
518
  try {
@@ -436,7 +524,7 @@ export class Webhook {
436
524
  await coreV1Api.createNamespace(ns);
437
525
  }
438
526
 
439
- const wh = this.mutatingWebhook(webhookTimeout);
527
+ const wh = await this.mutatingWebhook(path, webhookTimeout);
440
528
  try {
441
529
  Log.info("Creating mutating webhook");
442
530
  await admissionApi.createMutatingWebhookConfiguration(wh);
@@ -452,20 +540,26 @@ export class Webhook {
452
540
  return;
453
541
  }
454
542
 
455
- if (!code) {
543
+ if (!path) {
456
544
  throw new Error("No code provided");
457
545
  }
458
546
 
547
+ const code = await fs.readFile(path);
548
+
459
549
  const hash = crypto.createHash("sha256").update(code).digest("hex");
460
550
 
461
- const netpol = this.networkPolicy();
551
+ const appsApi = kubeConfig.makeApiClient(AppsV1Api);
552
+ const rbacApi = kubeConfig.makeApiClient(RbacAuthorizationV1Api);
553
+ const networkApi = kubeConfig.makeApiClient(NetworkingV1Api);
554
+
555
+ const networkPolicy = this.networkPolicy();
462
556
  try {
463
557
  Log.info("Checking for network policy");
464
- await networkApi.readNamespacedNetworkPolicy(netpol.metadata?.name ?? "", namespace);
558
+ await networkApi.readNamespacedNetworkPolicy(networkPolicy.metadata?.name ?? "", namespace);
465
559
  } catch (e) {
466
560
  Log.debug(e instanceof HttpError ? e.body : e);
467
561
  Log.info("Creating network policy");
468
- await networkApi.createNamespacedNetworkPolicy(namespace, netpol);
562
+ await networkApi.createNamespacedNetworkPolicy(namespace, networkPolicy);
469
563
  }
470
564
 
471
565
  const crb = this.clusterRoleBinding();
package/src/lib/module.ts CHANGED
@@ -29,7 +29,7 @@ export type PeprModuleOptions = {
29
29
  };
30
30
 
31
31
  export class PeprModule {
32
- private _controller: Controller;
32
+ private _controller!: Controller;
33
33
 
34
34
  /**
35
35
  * Create a new Pepr runtime
@@ -42,6 +42,12 @@ export class PeprModule {
42
42
  const config: ModuleConfig = mergeDeepWith(concat, pepr, alwaysIgnore);
43
43
  config.description = description;
44
44
 
45
+ // Handle build mode
46
+ if (process.env.PEPR_MODE === "build") {
47
+ process.send?.({ capabilities });
48
+ return;
49
+ }
50
+
45
51
  this._controller = new Controller(config, capabilities, opts.beforeHook, opts.afterHook);
46
52
 
47
53
  // Stop processing if deferStart is set to true
package/src/lib/types.ts CHANGED
@@ -44,6 +44,7 @@ export enum Event {
44
44
  Update = "UPDATE",
45
45
  Delete = "DELETE",
46
46
  CreateOrUpdate = "CREATEORUPDATE",
47
+ Any = "*",
47
48
  }
48
49
 
49
50
  export interface CapabilityCfg {
@@ -123,7 +124,7 @@ export type WhenSelector<T extends GenericClass> = {
123
124
  };
124
125
 
125
126
  export type Binding = {
126
- event?: Event;
127
+ event: Event;
127
128
  readonly kind: GroupVersionKind;
128
129
  readonly filters: {
129
130
  name: string;