pepr 0.1.7 → 0.1.9

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/dist/pepr-cli.js CHANGED
@@ -1,7 +1,6 @@
1
1
  #!/usr/bin/env node
2
2
  'use strict';
3
3
 
4
- var clientNode = require('@kubernetes/client-node');
5
4
  var json = require('@rollup/plugin-json');
6
5
  var nodeResolve = require('@rollup/plugin-node-resolve');
7
6
  var typescript = require('@rollup/plugin-typescript');
@@ -11,7 +10,9 @@ var rollup = require('rollup');
11
10
  require('@kubernetes/client-node/dist');
12
11
  require('ramda');
13
12
  require('fast-json-patch');
13
+ var clientNode = require('@kubernetes/client-node');
14
14
  var zlib = require('zlib');
15
+ var forge = require('node-forge');
15
16
  var util = require('util');
16
17
  var uuid = require('uuid');
17
18
  var prompt = require('prompts');
@@ -19,7 +20,7 @@ var commander = require('commander');
19
20
  var child_process = require('child_process');
20
21
  var chokidar = require('chokidar');
21
22
 
22
- var version = "0.1.7";
23
+ var version = "0.1.9";
23
24
  var dependencies = {
24
25
  "@kubernetes/client-node": "^0.18.1",
25
26
  "@rollup/plugin-json": "^6.0.0",
@@ -30,21 +31,23 @@ var dependencies = {
30
31
  commander: "^10.0.0",
31
32
  express: "^4.18.2",
32
33
  "fast-json-patch": "^3.1.1",
34
+ "node-forge": "^1.3.1",
35
+ prettier: "^2.8.7",
33
36
  prompts: "^2.4.2",
34
37
  ramda: "^0.28.0",
35
38
  rollup: "^3.20.2",
36
- uuid: "^9.0.0",
37
39
  tslib: "^2.5.0",
38
- typescript: "^5.0.2"
40
+ typescript: "^5.0.2",
41
+ uuid: "^9.0.0"
39
42
  };
40
43
  var devDependencies = {
44
+ "@types/node-forge": "^1.3.2",
41
45
  "@types/prompts": "^2.4.4",
42
46
  "@types/uuid": "^9.0.1",
43
47
  "@typescript-eslint/eslint-plugin": "^5.57.0",
44
48
  "@typescript-eslint/parser": "^5.57.0",
45
49
  ava: "^5.2.0",
46
50
  eslint: "^8.37.0",
47
- prettier: "^2.8.7",
48
51
  "rollup-plugin-visualizer": "^5.9.0",
49
52
  "ts-node": "^10.9.1",
50
53
  "tsconfig-paths": "^4.1.2"
@@ -278,21 +281,341 @@ var Event;
278
281
  })(Event || (Event = {}));
279
282
 
280
283
  // SPDX-License-Identifier: Apache-2.0
281
- function moduleSecret(uuid, data) {
282
- // Compress the data
283
- const compressed = zlib.gzipSync(data);
284
- return {
285
- apiVersion: "v1",
286
- kind: "Secret",
287
- metadata: {
288
- name: `module-${uuid}`,
289
- namespace: "pepr-system",
284
+ function genTLS(name) {
285
+ // Generate a new CA key pair
286
+ const caKeys = forge.pki.rsa.generateKeyPair(2048);
287
+ const caCert = forge.pki.createCertificate();
288
+ caCert.publicKey = caKeys.publicKey;
289
+ caCert.serialNumber = "01";
290
+ caCert.validity.notBefore = new Date();
291
+ caCert.validity.notAfter = new Date();
292
+ caCert.validity.notAfter.setFullYear(caCert.validity.notBefore.getFullYear() + 1);
293
+ const caAttrs = [
294
+ {
295
+ name: "commonName",
296
+ value: "Pepr Ephemeral CA",
290
297
  },
291
- type: "Opaque",
292
- data: {
293
- module: compressed.toString("base64"),
298
+ ];
299
+ caCert.setSubject(caAttrs);
300
+ caCert.setIssuer(caAttrs);
301
+ caCert.sign(caKeys.privateKey, forge.md.sha256.create());
302
+ // Generate a new key pair
303
+ const keys = forge.pki.rsa.generateKeyPair(2048);
304
+ const cert = forge.pki.createCertificate();
305
+ cert.publicKey = keys.publicKey;
306
+ cert.serialNumber = "01";
307
+ cert.validity.notBefore = new Date();
308
+ cert.validity.notAfter = new Date();
309
+ cert.validity.notAfter.setFullYear(cert.validity.notBefore.getFullYear() + 1);
310
+ const attrs = [
311
+ {
312
+ name: "commonName",
313
+ value: `${name}.pepr-system.svc`,
294
314
  },
295
- };
315
+ ];
316
+ cert.setSubject(attrs);
317
+ cert.setIssuer(caCert.subject.attributes);
318
+ cert.sign(caKeys.privateKey, forge.md.sha256.create());
319
+ // Convert the keys and certificates to PEM format
320
+ const ca = Buffer.from(forge.pki.certificateToPem(caCert)).toString("base64");
321
+ const key = Buffer.from(forge.pki.privateKeyToPem(keys.privateKey)).toString("base64");
322
+ const crt = Buffer.from(forge.pki.certificateToPem(cert)).toString("base64");
323
+ return { ca, key, crt };
324
+ }
325
+
326
+ // SPDX-License-Identifier: Apache-2.0
327
+ const peprIgnore = {
328
+ key: "pepr.dev",
329
+ operator: "NotIn",
330
+ values: ["ignore"],
331
+ };
332
+ class Webhook {
333
+ constructor(config) {
334
+ this.config = config;
335
+ this.name = `pepr-${config.uuid}`;
336
+ this.image = `ghcr.io/defenseunicorns/pepr-controller:${config.version}`;
337
+ // Generate the ephemeral tls things
338
+ this.tls = genTLS(this.name);
339
+ }
340
+ /** Generate the pepr-system namespace */
341
+ namespace() {
342
+ return {
343
+ apiVersion: "v1",
344
+ kind: "Namespace",
345
+ metadata: { name: "pepr-system" },
346
+ };
347
+ }
348
+ /**
349
+ * Grants the controller access to cluster resources beyond the mutating webhook.
350
+ *
351
+ * @todo: should dynamically generate this based on resources used by the module. will also need to explore how this should work for multiple modules.
352
+ * @returns
353
+ */
354
+ clusterRole() {
355
+ return {
356
+ apiVersion: "rbac.authorization.k8s.io/v1",
357
+ kind: "ClusterRole",
358
+ metadata: { name: this.name },
359
+ rules: [
360
+ {
361
+ // @todo: make this configurable
362
+ apiGroups: ["*"],
363
+ resources: ["*"],
364
+ verbs: ["create", "delete", "get", "list", "patch", "update", "watch"],
365
+ },
366
+ ],
367
+ };
368
+ }
369
+ clusterRoleBinding() {
370
+ const name = this.name;
371
+ return {
372
+ apiVersion: "rbac.authorization.k8s.io/v1",
373
+ kind: "ClusterRoleBinding",
374
+ metadata: { name },
375
+ roleRef: {
376
+ apiGroup: "rbac.authorization.k8s.io",
377
+ kind: "ClusterRole",
378
+ name,
379
+ },
380
+ subjects: [
381
+ {
382
+ kind: "ServiceAccount",
383
+ name,
384
+ namespace: "pepr-system",
385
+ },
386
+ ],
387
+ };
388
+ }
389
+ serviceAccount() {
390
+ return {
391
+ apiVersion: "v1",
392
+ kind: "ServiceAccount",
393
+ metadata: {
394
+ name: this.name,
395
+ namespace: "pepr-system",
396
+ },
397
+ };
398
+ }
399
+ tlsSecret() {
400
+ return {
401
+ apiVersion: "v1",
402
+ kind: "Secret",
403
+ metadata: {
404
+ name: `${this.name}-tls`,
405
+ namespace: "pepr-system",
406
+ },
407
+ type: "kubernetes.io/tls",
408
+ data: {
409
+ "tls.crt": this.tls.crt,
410
+ "tls.key": this.tls.key,
411
+ },
412
+ };
413
+ }
414
+ mutatingWebhook() {
415
+ const { name } = this;
416
+ const ignore = [peprIgnore];
417
+ // Add any namespaces to ignore
418
+ if (this.config.alwaysIgnore.namespaces.length > 0) {
419
+ ignore.push({
420
+ key: "kubernetes.io/metadata.name",
421
+ operator: "NotIn",
422
+ values: this.config.alwaysIgnore.namespaces,
423
+ });
424
+ }
425
+ return {
426
+ apiVersion: "admissionregistration.k8s.io/v1",
427
+ kind: "MutatingWebhookConfiguration",
428
+ metadata: { name },
429
+ webhooks: [
430
+ {
431
+ name: `${name}.pepr.dev`,
432
+ admissionReviewVersions: ["v1", "v1beta1"],
433
+ clientConfig: {
434
+ caBundle: this.tls.ca,
435
+ service: {
436
+ name: this.name,
437
+ namespace: "pepr-system",
438
+ path: "/mutate",
439
+ },
440
+ },
441
+ failurePolicy: "Ignore",
442
+ matchPolicy: "Equivalent",
443
+ timeoutSeconds: 15,
444
+ namespaceSelector: {
445
+ matchExpressions: ignore,
446
+ },
447
+ objectSelector: {
448
+ matchExpressions: ignore,
449
+ },
450
+ // @todo: make this configurable
451
+ rules: [
452
+ {
453
+ apiGroups: ["*"],
454
+ apiVersions: ["*"],
455
+ operations: ["CREATE", "UPDATE", "DELETE"],
456
+ resources: ["*"],
457
+ },
458
+ ],
459
+ // @todo: track side effects state
460
+ sideEffects: "None",
461
+ },
462
+ ],
463
+ };
464
+ }
465
+ deployment() {
466
+ return {
467
+ apiVersion: "apps/v1",
468
+ kind: "Deployment",
469
+ metadata: {
470
+ name: this.name,
471
+ namespace: "pepr-system",
472
+ labels: {
473
+ app: this.name,
474
+ },
475
+ },
476
+ spec: {
477
+ replicas: 2,
478
+ selector: {
479
+ matchLabels: {
480
+ app: this.name,
481
+ },
482
+ },
483
+ template: {
484
+ metadata: {
485
+ labels: {
486
+ app: this.name,
487
+ },
488
+ },
489
+ spec: {
490
+ priorityClassName: "system-node-critical",
491
+ serviceAccountName: this.name,
492
+ containers: [
493
+ {
494
+ name: "server",
495
+ image: this.image,
496
+ imagePullPolicy: "IfNotPresent",
497
+ livenessProbe: {
498
+ httpGet: {
499
+ path: "/healthz",
500
+ port: 3000,
501
+ scheme: "HTTPS",
502
+ },
503
+ },
504
+ ports: [
505
+ {
506
+ containerPort: 3000,
507
+ },
508
+ ],
509
+ resources: {
510
+ requests: {
511
+ memory: "64Mi",
512
+ cpu: "100m",
513
+ },
514
+ limits: {
515
+ memory: "256Mi",
516
+ cpu: "500m",
517
+ },
518
+ },
519
+ volumeMounts: [
520
+ {
521
+ name: "tls-certs",
522
+ mountPath: "/etc/certs",
523
+ readOnly: true,
524
+ },
525
+ ],
526
+ },
527
+ ],
528
+ volumes: [
529
+ {
530
+ name: "tls-certs",
531
+ secret: {
532
+ secretName: `${this.name}-tls`,
533
+ },
534
+ },
535
+ ],
536
+ },
537
+ },
538
+ },
539
+ };
540
+ }
541
+ service() {
542
+ return {
543
+ apiVersion: "v1",
544
+ kind: "Service",
545
+ metadata: {
546
+ name: this.name,
547
+ namespace: "pepr-system",
548
+ },
549
+ spec: {
550
+ selector: {
551
+ app: this.name,
552
+ },
553
+ ports: [
554
+ {
555
+ port: 443,
556
+ targetPort: 3000,
557
+ },
558
+ ],
559
+ },
560
+ };
561
+ }
562
+ moduleSecret(data) {
563
+ // Compress the data
564
+ const compressed = zlib.gzipSync(data);
565
+ return {
566
+ apiVersion: "v1",
567
+ kind: "Secret",
568
+ metadata: {
569
+ name: `${this.name}-module`,
570
+ namespace: "pepr-system",
571
+ },
572
+ type: "Opaque",
573
+ data: {
574
+ module: compressed.toString("base64"),
575
+ },
576
+ };
577
+ }
578
+ zarfYaml(path) {
579
+ const zarfCfg = {
580
+ kind: "ZarfPackageConfig",
581
+ metadata: {
582
+ name: this.name,
583
+ description: `Pepr Module: ${this.config.description}`,
584
+ url: "https://github.com/defenseunicorns/pepr",
585
+ version: this.config.version,
586
+ },
587
+ components: [
588
+ {
589
+ name: "module",
590
+ required: true,
591
+ manifests: [
592
+ {
593
+ name: "module",
594
+ namespace: "pepr-system",
595
+ files: [path],
596
+ },
597
+ ],
598
+ images: [this.image],
599
+ },
600
+ ],
601
+ };
602
+ return clientNode.dumpYaml(zarfCfg, { noRefs: true });
603
+ }
604
+ allYaml(code) {
605
+ const resources = [
606
+ this.namespace(),
607
+ this.clusterRole(),
608
+ this.clusterRoleBinding(),
609
+ this.serviceAccount(),
610
+ this.tlsSecret(),
611
+ this.mutatingWebhook(),
612
+ this.deployment(),
613
+ this.service(),
614
+ this.moduleSecret(code),
615
+ ];
616
+ // Convert the resources to a single YAML string
617
+ return resources.map(r => clientNode.dumpYaml(r, { noRefs: true })).join("---\n");
618
+ }
296
619
  }
297
620
 
298
621
  // SPDX-License-Identifier: Apache-2.0
@@ -303,14 +626,21 @@ function build (program) {
303
626
  .option("-d, --dir [directory]", "Pepr module directory", ".")
304
627
  .action(async (opts) => {
305
628
  // Build the module
306
- const { path: path$1, uuid } = await buildModule(opts.dir);
629
+ const { cfg, path: path$1, uuid } = await buildModule(opts.dir);
307
630
  // Read the compiled module code
308
631
  const code = await fs.promises.readFile(path$1, { encoding: "utf-8" });
309
632
  // Generate a secret for the module
310
- const secret = moduleSecret(uuid, code);
311
- const yaml = clientNode.dumpYaml(secret);
312
- const yamlPath = path.resolve("dist", `pepr-module-${uuid}.yaml`);
633
+ const webhook = new Webhook({
634
+ ...cfg.pepr,
635
+ description: cfg.description,
636
+ });
637
+ const yamlFile = `pepr-module-${uuid}.yaml`;
638
+ const yamlPath = path.resolve("dist", yamlFile);
639
+ const yaml = webhook.allYaml(code);
640
+ const zarfPath = path.resolve("dist", "zarf.yaml");
641
+ const zarf = webhook.zarfYaml(yamlFile);
313
642
  await fs.promises.writeFile(yamlPath, yaml);
643
+ await fs.promises.writeFile(zarfPath, zarf);
314
644
  Log.debug(`Module compiled successfully at ${path$1}`);
315
645
  Log.info(`K8s resource for the module saved to ${yamlPath}`);
316
646
  });
@@ -320,6 +650,7 @@ const externalLibs = [
320
650
  "commander",
321
651
  "express",
322
652
  "fast-json-patch",
653
+ "pepr",
323
654
  "ramda",
324
655
  ];
325
656
  async function buildModule(moduleDir) {
@@ -329,7 +660,8 @@ async function buildModule(moduleDir) {
329
660
  const input = path.resolve(moduleDir, "pepr.ts");
330
661
  // Read the module's UUID from the package.json filel
331
662
  const moduleText = await fs.promises.readFile(cfgPath, { encoding: "utf-8" });
332
- const uuid = JSON.parse(moduleText).pepr.uuid;
663
+ const cfg = JSON.parse(moduleText);
664
+ const { uuid } = cfg.pepr;
333
665
  const name = `pepr-${uuid}.js`;
334
666
  // Exit if the module's UUID could not be found
335
667
  if (!uuid) {
@@ -363,7 +695,8 @@ async function buildModule(moduleDir) {
363
695
  });
364
696
  return {
365
697
  path: path.resolve("dist", name),
366
- uuid: uuid,
698
+ cfg,
699
+ uuid,
367
700
  };
368
701
  }
369
702
  catch (e) {
@@ -445,12 +778,7 @@ import cfg from "./package.json";
445
778
  // import { HelloPepr } from "./capabilities/hello-pepr";
446
779
 
447
780
  // This initializes the Pepr module with the configuration from package.json
448
- export const { ProcessRequest, Register } = new PeprModule(cfg, {
449
- alwaysIgnore: {
450
- namespaces: [],
451
- labels: [],
452
- },
453
- });
781
+ export const { ProcessRequest, Register } = new PeprModule(cfg);
454
782
 
455
783
  /**
456
784
  * Each Pepr Capability is registered with the module using the Register function.
@@ -490,6 +818,10 @@ function genPkgJSON(opts) {
490
818
  version,
491
819
  uuid: uuid$1,
492
820
  onError: opts.errorBehavior,
821
+ alwaysIgnore: {
822
+ namespaces: [],
823
+ labels: [],
824
+ },
493
825
  },
494
826
  scripts: {
495
827
  build: "pepr build",
@@ -576,72 +908,72 @@ const capabilityHelloPeprTS = {
576
908
  path: "hello-pepr.ts",
577
909
  data: `import { Capability, a } from "pepr";
578
910
 
579
- const { Register, When } = new Capability({
580
- name: "hello-pepr",
581
- description: "A simple example capability to show how things work.",
582
- namespaces: ["pepr-demo"],
583
- });
584
-
585
- export { Register as HelloPepr };
586
-
587
- /**
588
- * This is a single Capability Action. They can be in the same file or put imported from other files.
589
- * In this exmaple, when a ConfigMap is created with the name \`example-1\`, then add a label and annotation.
590
- *
591
- * Equivelant to manually running:
592
- * \`kubectl label configmap example-1 pepr=was-here\`
593
- * \`kubectl annotate configmap example-1 pepr.dev=annotations-work-too\`
594
- */
595
- When(a.ConfigMap)
596
- .IsCreated()
597
- .WithName("example-1")
598
- .Then(request =>
599
- request
600
- .SetLabel("pepr", "was-here")
601
- .SetAnnotation("pepr.dev", "annotations-work-too")
602
- );
603
-
604
- /**
605
- * This Capabiility Action does the exact same changes for example-2, except this time it uses the \`.ThenSet()\` feature.
606
- * You can stack multiple \`.Then()\` calls, but only a single \`.ThenSet()\`
607
- */
608
- When(a.ConfigMap)
609
- .IsCreated()
610
- .WithName("example-2")
611
- .ThenSet({
612
- metadata: {
613
- labels: {
614
- pepr: "was-here",
615
- },
616
- annotations: {
617
- "pepr.dev": "annotations-work-too",
618
- },
911
+ const { Register, When } = new Capability({
912
+ name: "hello-pepr",
913
+ description: "A simple example capability to show how things work.",
914
+ namespaces: ["pepr-demo"],
915
+ });
916
+
917
+ export { Register as HelloPepr };
918
+
919
+ /**
920
+ * This is a single Capability Action. They can be in the same file or put imported from other files.
921
+ * In this exmaple, when a ConfigMap is created with the name \`example-1\`, then add a label and annotation.
922
+ *
923
+ * Equivelant to manually running:
924
+ * \`kubectl label configmap example-1 pepr=was-here\`
925
+ * \`kubectl annotate configmap example-1 pepr.dev=annotations-work-too\`
926
+ */
927
+ When(a.ConfigMap)
928
+ .IsCreated()
929
+ .WithName("example-1")
930
+ .Then(request =>
931
+ request
932
+ .SetLabel("pepr", "was-here")
933
+ .SetAnnotation("pepr.dev", "annotations-work-too")
934
+ );
935
+
936
+ /**
937
+ * This Capabiility Action does the exact same changes for example-2, except this time it uses the \`.ThenSet()\` feature.
938
+ * You can stack multiple \`.Then()\` calls, but only a single \`.ThenSet()\`
939
+ */
940
+ When(a.ConfigMap)
941
+ .IsCreated()
942
+ .WithName("example-2")
943
+ .ThenSet({
944
+ metadata: {
945
+ labels: {
946
+ pepr: "was-here",
619
947
  },
620
- });
621
-
622
- /**
623
- * This Capability Action combines different styles. Unlike the previous actions, this one will look for any ConfigMap
624
- * in the \`pepr-demo\` namespace that has the label \`change=by-label\`. Note that all conditions added such as \`WithName()\`,
625
- * \`WithLabel()\`, \`InNamespace()\`, are ANDs so all conditions must be true for the request to be procssed.
626
- */
627
- When(a.ConfigMap)
628
- .IsCreated()
629
- .WithLabel("change", "by-label")
630
- .Then(request => {
631
- // The K8s object e are going to mutate
632
- const cm = request.Raw;
633
-
634
- // Get the username and uid of the K8s reuest
635
- const { username, uid } = request.Request.userInfo;
636
-
637
- // Store some data about the request in the configmap
638
- cm.data["username"] = username;
639
- cm.data["uid"] = uid;
640
-
641
- // You can still mix other ways of making changes too
642
- request.SetAnnotation("pepr.dev", "making-waves");
643
- });
644
- `,
948
+ annotations: {
949
+ "pepr.dev": "annotations-work-too",
950
+ },
951
+ },
952
+ });
953
+
954
+ /**
955
+ * This Capability Action combines different styles. Unlike the previous actions, this one will look for any ConfigMap
956
+ * in the \`pepr-demo\` namespace that has the label \`change=by-label\`. Note that all conditions added such as \`WithName()\`,
957
+ * \`WithLabel()\`, \`InNamespace()\`, are ANDs so all conditions must be true for the request to be procssed.
958
+ */
959
+ When(a.ConfigMap)
960
+ .IsCreated()
961
+ .WithLabel("change", "by-label")
962
+ .Then(request => {
963
+ // The K8s object e are going to mutate
964
+ const cm = request.Raw;
965
+
966
+ // Get the username and uid of the K8s reuest
967
+ const { username, uid } = request.Request.userInfo;
968
+
969
+ // Store some data about the request in the configmap
970
+ cm.data["username"] = username;
971
+ cm.data["uid"] = uid;
972
+
973
+ // You can still mix other ways of making changes too
974
+ request.SetAnnotation("pepr.dev", "making-waves");
975
+ });
976
+ `,
645
977
  };
646
978
  const capabilitySnippet = {
647
979
  path: "pepr.code-snippets",