pepr 0.1.21 → 0.1.22

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
@@ -21,7 +21,7 @@ var uuid = require('uuid');
21
21
  var commander = require('commander');
22
22
  var chokidar = require('chokidar');
23
23
 
24
- var version = "0.1.21";
24
+ var version = "0.1.22";
25
25
 
26
26
  // SPDX-License-Identifier: Apache-2.0
27
27
  // SPDX-FileCopyrightText: 2023-Present The Pepr Authors
@@ -118,15 +118,21 @@ function genTLS(name) {
118
118
  ]);
119
119
  // Generate a new server key pair and create a server certificate signed by the CA
120
120
  const serverKeys = forge.pki.rsa.generateKeyPair(2048);
121
- const serverCert = genCert(serverKeys, `${name}.pepr-system.svc`, caCert.subject.attributes);
121
+ const serverCert = genCert(serverKeys, name, caCert.subject.attributes);
122
122
  // Sign both certificates with the CA private key
123
123
  caCert.sign(caKeys.privateKey, forge.md.sha256.create());
124
124
  serverCert.sign(caKeys.privateKey, forge.md.sha256.create());
125
- // Convert the keys and certificates to PEM format and Base64-encode them
126
- const ca = Buffer.from(forge.pki.certificateToPem(caCert)).toString("base64");
127
- const key = Buffer.from(forge.pki.privateKeyToPem(serverKeys.privateKey)).toString("base64");
128
- const crt = Buffer.from(forge.pki.certificateToPem(serverCert)).toString("base64");
129
- return { ca, key, crt };
125
+ // Convert the keys and certificates to PEM format
126
+ const pem = {
127
+ ca: forge.pki.certificateToPem(caCert),
128
+ crt: forge.pki.certificateToPem(serverCert),
129
+ key: forge.pki.privateKeyToPem(serverKeys.privateKey),
130
+ };
131
+ // Base64-encode the PEM strings
132
+ const ca = Buffer.from(pem.ca).toString("base64");
133
+ const key = Buffer.from(pem.key).toString("base64");
134
+ const crt = Buffer.from(pem.crt).toString("base64");
135
+ return { ca, key, crt, pem };
130
136
  }
131
137
  function genCert(key, name, issuer) {
132
138
  const crt = forge.pki.createCertificate();
@@ -159,12 +165,16 @@ const peprIgnore = {
159
165
  values: ["ignore"],
160
166
  };
161
167
  class Webhook {
162
- constructor(config) {
168
+ get tls() {
169
+ return this._tls;
170
+ }
171
+ constructor(config, host) {
163
172
  this.config = config;
173
+ this.host = host;
164
174
  this.name = `pepr-${config.uuid}`;
165
175
  this.image = `ghcr.io/defenseunicorns/pepr/controller:${config.version}`;
166
176
  // Generate the ephemeral tls things
167
- this.tls = genTLS(this.name);
177
+ this._tls = genTLS(this.host || `${this.name}.pepr-system.svc`);
168
178
  }
169
179
  /** Generate the pepr-system namespace */
170
180
  namespace() {
@@ -235,8 +245,8 @@ class Webhook {
235
245
  },
236
246
  type: "kubernetes.io/tls",
237
247
  data: {
238
- "tls.crt": this.tls.crt,
239
- "tls.key": this.tls.key,
248
+ "tls.crt": this._tls.crt,
249
+ "tls.key": this._tls.key,
240
250
  },
241
251
  };
242
252
  }
@@ -251,6 +261,21 @@ class Webhook {
251
261
  values: this.config.alwaysIgnore.namespaces,
252
262
  });
253
263
  }
264
+ const clientConfig = {
265
+ caBundle: this._tls.ca,
266
+ };
267
+ // If a host is specified, use that with a port of 3000
268
+ if (this.host) {
269
+ clientConfig.url = `https://${this.host}:3000/mutate`;
270
+ }
271
+ else {
272
+ // Otherwise, use the service
273
+ clientConfig.service = {
274
+ name: this.name,
275
+ namespace: "pepr-system",
276
+ path: "/mutate",
277
+ };
278
+ }
254
279
  return {
255
280
  apiVersion: "admissionregistration.k8s.io/v1",
256
281
  kind: "MutatingWebhookConfiguration",
@@ -259,14 +284,7 @@ class Webhook {
259
284
  {
260
285
  name: `${name}.pepr.dev`,
261
286
  admissionReviewVersions: ["v1", "v1beta1"],
262
- clientConfig: {
263
- caBundle: this.tls.ca,
264
- service: {
265
- name: this.name,
266
- namespace: "pepr-system",
267
- path: "/mutate",
268
- },
269
- },
287
+ clientConfig,
270
288
  failurePolicy: "Ignore",
271
289
  matchPolicy: "Equivalent",
272
290
  timeoutSeconds: 15,
@@ -351,6 +369,11 @@ class Webhook {
351
369
  mountPath: "/etc/certs",
352
370
  readOnly: true,
353
371
  },
372
+ {
373
+ name: "module",
374
+ mountPath: "/app/module.js.gz",
375
+ readOnly: true,
376
+ },
354
377
  ],
355
378
  },
356
379
  ],
@@ -361,12 +384,56 @@ class Webhook {
361
384
  secretName: `${this.name}-tls`,
362
385
  },
363
386
  },
387
+ {
388
+ name: "module",
389
+ secret: {
390
+ secretName: `${this.name}-module`,
391
+ },
392
+ },
364
393
  ],
365
394
  },
366
395
  },
367
396
  },
368
397
  };
369
398
  }
399
+ /** Only permit the */
400
+ networkPolicy() {
401
+ return {
402
+ apiVersion: "networking.k8s.io/v1",
403
+ kind: "NetworkPolicy",
404
+ metadata: {
405
+ name: this.name,
406
+ namespace: "pepr-system",
407
+ },
408
+ spec: {
409
+ podSelector: {
410
+ matchLabels: {
411
+ app: this.name,
412
+ },
413
+ },
414
+ policyTypes: ["Ingress"],
415
+ ingress: [
416
+ {
417
+ from: [
418
+ {
419
+ namespaceSelector: {
420
+ matchLabels: {
421
+ "kubernetes.io/metadata.name": "kube-system",
422
+ },
423
+ },
424
+ },
425
+ ],
426
+ ports: [
427
+ {
428
+ protocol: "TCP",
429
+ port: 443,
430
+ },
431
+ ],
432
+ },
433
+ ],
434
+ },
435
+ };
436
+ }
370
437
  service() {
371
438
  return {
372
439
  apiVersion: "v1",
@@ -433,6 +500,7 @@ class Webhook {
433
500
  allYaml(code) {
434
501
  const resources = [
435
502
  this.namespace(),
503
+ this.networkPolicy(),
436
504
  this.clusterRole(),
437
505
  this.clusterRoleBinding(),
438
506
  this.serviceAccount(),
@@ -455,6 +523,7 @@ class Webhook {
455
523
  const rbacApi = kubeConfig.makeApiClient(clientNode.RbacAuthorizationV1Api);
456
524
  const appsApi = kubeConfig.makeApiClient(clientNode.AppsV1Api);
457
525
  const admissionApi = kubeConfig.makeApiClient(clientNode.AdmissionregistrationV1Api);
526
+ const networkApi = kubeConfig.makeApiClient(clientNode.NetworkingV1Api);
458
527
  const ns = this.namespace();
459
528
  try {
460
529
  types.Log.info("Checking for namespace");
@@ -465,6 +534,16 @@ class Webhook {
465
534
  types.Log.info("Creating namespace");
466
535
  await coreV1Api.createNamespace(ns);
467
536
  }
537
+ const netpol = this.networkPolicy();
538
+ try {
539
+ types.Log.info("Checking for network policy");
540
+ await networkApi.readNamespacedNetworkPolicy(netpol.metadata.name, namespace);
541
+ }
542
+ catch (e) {
543
+ types.Log.debug(e.body);
544
+ types.Log.info("Creating network policy");
545
+ await networkApi.createNamespacedNetworkPolicy(namespace, netpol);
546
+ }
468
547
  const wh = this.mutatingWebhook();
469
548
  try {
470
549
  types.Log.info("Creating mutating webhook");
@@ -514,6 +593,10 @@ class Webhook {
514
593
  await coreV1Api.deleteNamespacedServiceAccount(sa.metadata.name, namespace);
515
594
  await coreV1Api.createNamespacedServiceAccount(namespace, sa);
516
595
  }
596
+ // If a host is specified, we don't need to deploy the rest of the resources
597
+ if (this.host) {
598
+ return;
599
+ }
517
600
  const mod = this.moduleSecret(code);
518
601
  try {
519
602
  types.Log.info("Creating module secret");
@@ -707,6 +790,47 @@ function deploy (program) {
707
790
  });
708
791
  }
709
792
 
793
+ // SPDX-License-Identifier: Apache-2.0
794
+ function dev (program) {
795
+ program
796
+ .command("dev")
797
+ .description("Setup a local webhook development environment")
798
+ .option("-d, --dir [directory]", "Pepr module directory", ".")
799
+ .option("-h, --host [host]", "Host to listen on", "host.docker.internal")
800
+ .action(async (opts) => {
801
+ // Prompt the user to confirm
802
+ const confirm = await prompt.prompt({
803
+ type: "confirm",
804
+ name: "confirm",
805
+ message: "This will remove and redeploy the module. Continue?",
806
+ });
807
+ // Exit if the user doesn't confirm
808
+ if (!confirm.confirm) {
809
+ process.exit(0);
810
+ }
811
+ // Build the module
812
+ const { cfg, path } = await buildModule(opts.dir);
813
+ // Read the compiled module code
814
+ const code = await fs.promises.readFile(path, { encoding: "utf-8" });
815
+ // Generate a secret for the module
816
+ const webhook = new Webhook({
817
+ ...cfg.pepr,
818
+ description: cfg.description,
819
+ }, opts.host);
820
+ // Write the TLS cert and key to disk
821
+ await fs.promises.writeFile("insecure-tls.crt", webhook.tls.pem.crt);
822
+ await fs.promises.writeFile("insecure-tls.key", webhook.tls.pem.key);
823
+ try {
824
+ await webhook.deploy(code);
825
+ types.Log.info(`Module deployed successfully`);
826
+ }
827
+ catch (e) {
828
+ types.Log.error(`Error deploying module: ${e}`);
829
+ process.exit(1);
830
+ }
831
+ });
832
+ }
833
+
710
834
  // SPDX-License-Identifier: Apache-2.0
711
835
  /**
712
836
  * Sanitize a user input name to be used as a pepr module directory name
@@ -1027,11 +1151,7 @@ function walkthrough() {
1027
1151
  },
1028
1152
  ],
1029
1153
  };
1030
- return prompt([
1031
- askName,
1032
- askDescription,
1033
- askErrorBehavior,
1034
- ]);
1154
+ return prompt([askName, askDescription, askErrorBehavior]);
1035
1155
  }
1036
1156
  async function confirm(dirName, packageJSON, peprTSPath) {
1037
1157
  console.log(`
@@ -1165,4 +1285,5 @@ build(program);
1165
1285
  capability(program);
1166
1286
  test(program);
1167
1287
  deploy(program);
1288
+ dev(program);
1168
1289
  program.parse();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pepr",
3
- "version": "0.1.21",
3
+ "version": "0.1.22",
4
4
  "description": "Kubernetes application engine",
5
5
  "author": "Defense Unicorns",
6
6
  "homepage": "https://github.com/defenseunicorns/pepr",
@@ -29,7 +29,10 @@
29
29
  "prettier": "npx prettier src --check",
30
30
  "prettier:fix": "npm run prettier -- --write",
31
31
  "prepublishOnly": "rm -fr dist/* && npm run lint:fix && npm run prettier:fix && npm run test && npm run build",
32
- "e2e-dev": "npm run build && docker buildx build --tag pepr:dev . && k3d image import pepr:dev && node dist/pepr-cli.js deploy -f -i pepr:dev"
32
+ "e2e-dev-setup": "k3d cluster delete pepr-dev && k3d cluster create pepr-dev",
33
+ "e2e-dev": "npm run build && docker buildx build --tag pepr:dev . && k3d image import pepr:dev -c pepr-dev && node dist/pepr-cli.js deploy -f -i pepr:dev",
34
+ "prestart": "ts-node src/cli dev",
35
+ "start": "chokidar 'src/**/*.ts' -c 'SSL_KEY_PATH=insecure-tls.key SSL_CERT_PATH=insecure-tls.crt ts-node src/controller/index.ts' --initial --silent"
33
36
  },
34
37
  "dependencies": {
35
38
  "@kubernetes/client-node": "^0.18.1",
@@ -59,6 +62,7 @@
59
62
  "@typescript-eslint/eslint-plugin": "^5.57.0",
60
63
  "@typescript-eslint/parser": "^5.57.0",
61
64
  "ava": "^5.2.0",
65
+ "chokidar-cli": "^3.0.0",
62
66
  "eslint": "^8.37.0",
63
67
  "ts-node": "^10.9.1",
64
68
  "tsconfig-paths": "^4.1.2"
@@ -6,6 +6,11 @@ export interface TLSOut {
6
6
  ca: string;
7
7
  crt: string;
8
8
  key: string;
9
+ pem: {
10
+ ca: string;
11
+ crt: string;
12
+ key: string;
13
+ };
9
14
  }
10
15
 
11
16
  /**
@@ -36,18 +41,25 @@ export function genTLS(name: string): TLSOut {
36
41
 
37
42
  // Generate a new server key pair and create a server certificate signed by the CA
38
43
  const serverKeys = forge.pki.rsa.generateKeyPair(2048);
39
- const serverCert = genCert(serverKeys, `${name}.pepr-system.svc`, caCert.subject.attributes);
44
+ const serverCert = genCert(serverKeys, name, caCert.subject.attributes);
40
45
 
41
46
  // Sign both certificates with the CA private key
42
47
  caCert.sign(caKeys.privateKey, forge.md.sha256.create());
43
48
  serverCert.sign(caKeys.privateKey, forge.md.sha256.create());
44
49
 
45
- // Convert the keys and certificates to PEM format and Base64-encode them
46
- const ca = Buffer.from(forge.pki.certificateToPem(caCert)).toString("base64");
47
- const key = Buffer.from(forge.pki.privateKeyToPem(serverKeys.privateKey)).toString("base64");
48
- const crt = Buffer.from(forge.pki.certificateToPem(serverCert)).toString("base64");
50
+ // Convert the keys and certificates to PEM format
51
+ const pem = {
52
+ ca: forge.pki.certificateToPem(caCert),
53
+ crt: forge.pki.certificateToPem(serverCert),
54
+ key: forge.pki.privateKeyToPem(serverKeys.privateKey),
55
+ };
49
56
 
50
- return { ca, key, crt };
57
+ // Base64-encode the PEM strings
58
+ const ca = Buffer.from(pem.ca).toString("base64");
59
+ const key = Buffer.from(pem.key).toString("base64");
60
+ const crt = Buffer.from(pem.crt).toString("base64");
61
+
62
+ return { ca, key, crt, pem };
51
63
  }
52
64
 
53
65
  function genCert(key: forge.pki.rsa.KeyPair, name: string, issuer: forge.pki.CertificateField[]) {
@@ -3,9 +3,11 @@
3
3
 
4
4
  import {
5
5
  AdmissionregistrationV1Api,
6
+ AdmissionregistrationV1WebhookClientConfig,
6
7
  AppsV1Api,
7
8
  CoreV1Api,
8
9
  KubeConfig,
10
+ NetworkingV1Api,
9
11
  RbacAuthorizationV1Api,
10
12
  V1ClusterRole,
11
13
  V1ClusterRoleBinding,
@@ -13,6 +15,7 @@ import {
13
15
  V1LabelSelectorRequirement,
14
16
  V1MutatingWebhookConfiguration,
15
17
  V1Namespace,
18
+ V1NetworkPolicy,
16
19
  V1Secret,
17
20
  V1Service,
18
21
  V1ServiceAccount,
@@ -31,17 +34,21 @@ const peprIgnore: V1LabelSelectorRequirement = {
31
34
 
32
35
  export class Webhook {
33
36
  private name: string;
34
- private tls: TLSOut;
37
+ private _tls: TLSOut;
35
38
 
36
39
  public image: string;
37
40
 
38
- constructor(private readonly config: ModuleConfig) {
41
+ public get tls(): TLSOut {
42
+ return this._tls;
43
+ }
44
+
45
+ constructor(private readonly config: ModuleConfig, private readonly host?: string) {
39
46
  this.name = `pepr-${config.uuid}`;
40
47
 
41
48
  this.image = `ghcr.io/defenseunicorns/pepr/controller:${config.version}`;
42
49
 
43
50
  // Generate the ephemeral tls things
44
- this.tls = genTLS(this.name);
51
+ this._tls = genTLS(this.host || `${this.name}.pepr-system.svc`);
45
52
  }
46
53
 
47
54
  /** Generate the pepr-system namespace */
@@ -117,8 +124,8 @@ export class Webhook {
117
124
  },
118
125
  type: "kubernetes.io/tls",
119
126
  data: {
120
- "tls.crt": this.tls.crt,
121
- "tls.key": this.tls.key,
127
+ "tls.crt": this._tls.crt,
128
+ "tls.key": this._tls.key,
122
129
  },
123
130
  };
124
131
  }
@@ -136,6 +143,22 @@ export class Webhook {
136
143
  });
137
144
  }
138
145
 
146
+ const clientConfig: AdmissionregistrationV1WebhookClientConfig = {
147
+ caBundle: this._tls.ca,
148
+ };
149
+
150
+ // If a host is specified, use that with a port of 3000
151
+ if (this.host) {
152
+ clientConfig.url = `https://${this.host}:3000/mutate`;
153
+ } else {
154
+ // Otherwise, use the service
155
+ clientConfig.service = {
156
+ name: this.name,
157
+ namespace: "pepr-system",
158
+ path: "/mutate",
159
+ };
160
+ }
161
+
139
162
  return {
140
163
  apiVersion: "admissionregistration.k8s.io/v1",
141
164
  kind: "MutatingWebhookConfiguration",
@@ -144,14 +167,7 @@ export class Webhook {
144
167
  {
145
168
  name: `${name}.pepr.dev`,
146
169
  admissionReviewVersions: ["v1", "v1beta1"],
147
- clientConfig: {
148
- caBundle: this.tls.ca,
149
- service: {
150
- name: this.name,
151
- namespace: "pepr-system",
152
- path: "/mutate",
153
- },
154
- },
170
+ clientConfig,
155
171
  failurePolicy: "Ignore",
156
172
  matchPolicy: "Equivalent",
157
173
  timeoutSeconds: 15,
@@ -237,6 +253,11 @@ export class Webhook {
237
253
  mountPath: "/etc/certs",
238
254
  readOnly: true,
239
255
  },
256
+ {
257
+ name: "module",
258
+ mountPath: "/app/module.js.gz",
259
+ readOnly: true,
260
+ },
240
261
  ],
241
262
  },
242
263
  ],
@@ -247,6 +268,12 @@ export class Webhook {
247
268
  secretName: `${this.name}-tls`,
248
269
  },
249
270
  },
271
+ {
272
+ name: "module",
273
+ secret: {
274
+ secretName: `${this.name}-module`,
275
+ },
276
+ },
250
277
  ],
251
278
  },
252
279
  },
@@ -254,6 +281,45 @@ export class Webhook {
254
281
  };
255
282
  }
256
283
 
284
+ /** Only permit the */
285
+ networkPolicy(): V1NetworkPolicy {
286
+ return {
287
+ apiVersion: "networking.k8s.io/v1",
288
+ kind: "NetworkPolicy",
289
+ metadata: {
290
+ name: this.name,
291
+ namespace: "pepr-system",
292
+ },
293
+ spec: {
294
+ podSelector: {
295
+ matchLabels: {
296
+ app: this.name,
297
+ },
298
+ },
299
+ policyTypes: ["Ingress"],
300
+ ingress: [
301
+ {
302
+ from: [
303
+ {
304
+ namespaceSelector: {
305
+ matchLabels: {
306
+ "kubernetes.io/metadata.name": "kube-system",
307
+ },
308
+ },
309
+ },
310
+ ],
311
+ ports: [
312
+ {
313
+ protocol: "TCP",
314
+ port: 443,
315
+ },
316
+ ],
317
+ },
318
+ ],
319
+ },
320
+ };
321
+ }
322
+
257
323
  service(): V1Service {
258
324
  return {
259
325
  apiVersion: "v1",
@@ -324,6 +390,7 @@ export class Webhook {
324
390
  allYaml(code: string) {
325
391
  const resources = [
326
392
  this.namespace(),
393
+ this.networkPolicy(),
327
394
  this.clusterRole(),
328
395
  this.clusterRoleBinding(),
329
396
  this.serviceAccount(),
@@ -351,6 +418,7 @@ export class Webhook {
351
418
  const rbacApi = kubeConfig.makeApiClient(RbacAuthorizationV1Api);
352
419
  const appsApi = kubeConfig.makeApiClient(AppsV1Api);
353
420
  const admissionApi = kubeConfig.makeApiClient(AdmissionregistrationV1Api);
421
+ const networkApi = kubeConfig.makeApiClient(NetworkingV1Api);
354
422
 
355
423
  const ns = this.namespace();
356
424
  try {
@@ -362,6 +430,16 @@ export class Webhook {
362
430
  await coreV1Api.createNamespace(ns);
363
431
  }
364
432
 
433
+ const netpol = this.networkPolicy();
434
+ try {
435
+ logger.info("Checking for network policy");
436
+ await networkApi.readNamespacedNetworkPolicy(netpol.metadata.name, namespace);
437
+ } catch (e) {
438
+ logger.debug(e.body);
439
+ logger.info("Creating network policy");
440
+ await networkApi.createNamespacedNetworkPolicy(namespace, netpol);
441
+ }
442
+
365
443
  const wh = this.mutatingWebhook();
366
444
  try {
367
445
  logger.info("Creating mutating webhook");
@@ -410,6 +488,11 @@ export class Webhook {
410
488
  await coreV1Api.createNamespacedServiceAccount(namespace, sa);
411
489
  }
412
490
 
491
+ // If a host is specified, we don't need to deploy the rest of the resources
492
+ if (this.host) {
493
+ return;
494
+ }
495
+
413
496
  const mod = this.moduleSecret(code);
414
497
  try {
415
498
  logger.info("Creating module secret");