pepr 0.6.1 → 0.7.0

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.
@@ -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;