pepr 0.1.21 → 0.1.24
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/README.md +8 -6
- package/dist/pepr-cli.js +190 -72
- package/dist/pepr-core.js +71 -80
- package/dist/{types-672dd6e4.js → types-1709b44f.js} +2 -12
- package/index.ts +7 -2
- package/package.json +6 -2
- package/src/lib/k8s/tls.ts +18 -6
- package/src/lib/k8s/webhook.ts +126 -43
- package/src/lib/index.ts +0 -10
package/README.md
CHANGED
|
@@ -2,6 +2,14 @@
|
|
|
2
2
|
|
|
3
3
|
<img align="right" width="40%" src=".images/pepr.png" />
|
|
4
4
|
|
|
5
|
+
Pepr is on a mission to save Kubernetes from the tyranny of YAML, intimidating glue code, bash scripts, and other makeshift solutions. As a Kubernetes controller, Pepr empowers you to define Kubernetes transformations using TypeScript, without software development expertise thanks to plain-english configurations. Pepr transforms a patchwork of forks, scripts, overlays, and other chaos into a cohesive, well-structured, and maintainable system. With Pepr, you can seamlessly transition IT ops tribal knowledge into code, simplifying documentation, testing, validation, and coordination of changes for a more predictable outcome.
|
|
6
|
+
|
|
7
|
+
## Features
|
|
8
|
+
|
|
9
|
+
- Define a set of Kubernetes transformations/actions as Pepr capabilities.
|
|
10
|
+
- Write capabilities in TypeScript and bundle them for in-cluster processing in [NodeJS](https://nodejs.org/).
|
|
11
|
+
- React to cluster resources by mutating them, creating new Kubernetes resources, or performing arbitrary exec/API operations.
|
|
12
|
+
|
|
5
13
|
Pepr is an open-source project that helps IT Ops teams of all skill levels manage and modify resources in a Kubernetes (K8s) cluster using TypeScript. Kubernetes simplifies the management of multiple computers working together to run and scale applications. Pepr acts as a smart assistant, automatically changing or validating parts of the system as needed.
|
|
6
14
|
|
|
7
15
|
TypeScript is used to create Pepr capabilities, benefiting from its error-catching and clean code features, but without requiring specialized software engineering experience or prior Typescript knowledge. Pepr also provides a user-friendly interface for writing commands in plain English in a [Fluent Interface](https://en.wikipedia.org/wiki/Fluent_interface) style.
|
|
@@ -10,12 +18,6 @@ Capabilities are logical groupings of actions, which are the atomic units of cha
|
|
|
10
18
|
|
|
11
19
|
Imagine Pepr as a smart home system where different devices communicate with each other. Pepr provides instructions, simplifying the management of the smart home. The project enables both expert and novice capability authors to improve management and interactions within the Kubernetes environment, making its features accessible to everyone.
|
|
12
20
|
|
|
13
|
-
## Features
|
|
14
|
-
|
|
15
|
-
- Define a set of Kubernetes transformations/actions as Pepr capabilities.
|
|
16
|
-
- Write capabilities in TypeScript and bundle them for in-cluster processing in [NodeJS](https://nodejs.org/).
|
|
17
|
-
- React to cluster resources by mutating them, creating new Kubernetes resources, or performing arbitrary exec/API operations.
|
|
18
|
-
|
|
19
21
|
## Concepts
|
|
20
22
|
|
|
21
23
|
### Module
|
package/dist/pepr-cli.js
CHANGED
|
@@ -7,10 +7,7 @@ var typescript = require('@rollup/plugin-typescript');
|
|
|
7
7
|
var fs = require('fs');
|
|
8
8
|
var path = require('path');
|
|
9
9
|
var rollup = require('rollup');
|
|
10
|
-
var types = require('./types-
|
|
11
|
-
require('@kubernetes/client-node/dist');
|
|
12
|
-
require('ramda');
|
|
13
|
-
require('fast-json-patch');
|
|
10
|
+
var types = require('./types-1709b44f.js');
|
|
14
11
|
var clientNode = require('@kubernetes/client-node');
|
|
15
12
|
var zlib = require('zlib');
|
|
16
13
|
var forge = require('node-forge');
|
|
@@ -21,7 +18,7 @@ var uuid = require('uuid');
|
|
|
21
18
|
var commander = require('commander');
|
|
22
19
|
var chokidar = require('chokidar');
|
|
23
20
|
|
|
24
|
-
var version = "0.1.
|
|
21
|
+
var version = "0.1.24";
|
|
25
22
|
|
|
26
23
|
// SPDX-License-Identifier: Apache-2.0
|
|
27
24
|
// SPDX-FileCopyrightText: 2023-Present The Pepr Authors
|
|
@@ -118,15 +115,21 @@ function genTLS(name) {
|
|
|
118
115
|
]);
|
|
119
116
|
// Generate a new server key pair and create a server certificate signed by the CA
|
|
120
117
|
const serverKeys = forge.pki.rsa.generateKeyPair(2048);
|
|
121
|
-
const serverCert = genCert(serverKeys,
|
|
118
|
+
const serverCert = genCert(serverKeys, name, caCert.subject.attributes);
|
|
122
119
|
// Sign both certificates with the CA private key
|
|
123
120
|
caCert.sign(caKeys.privateKey, forge.md.sha256.create());
|
|
124
121
|
serverCert.sign(caKeys.privateKey, forge.md.sha256.create());
|
|
125
|
-
// Convert the keys and certificates to PEM format
|
|
126
|
-
const
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
122
|
+
// Convert the keys and certificates to PEM format
|
|
123
|
+
const pem = {
|
|
124
|
+
ca: forge.pki.certificateToPem(caCert),
|
|
125
|
+
crt: forge.pki.certificateToPem(serverCert),
|
|
126
|
+
key: forge.pki.privateKeyToPem(serverKeys.privateKey),
|
|
127
|
+
};
|
|
128
|
+
// Base64-encode the PEM strings
|
|
129
|
+
const ca = Buffer.from(pem.ca).toString("base64");
|
|
130
|
+
const key = Buffer.from(pem.key).toString("base64");
|
|
131
|
+
const crt = Buffer.from(pem.crt).toString("base64");
|
|
132
|
+
return { ca, key, crt, pem };
|
|
130
133
|
}
|
|
131
134
|
function genCert(key, name, issuer) {
|
|
132
135
|
const crt = forge.pki.createCertificate();
|
|
@@ -159,12 +162,16 @@ const peprIgnore = {
|
|
|
159
162
|
values: ["ignore"],
|
|
160
163
|
};
|
|
161
164
|
class Webhook {
|
|
162
|
-
|
|
165
|
+
get tls() {
|
|
166
|
+
return this._tls;
|
|
167
|
+
}
|
|
168
|
+
constructor(config, host) {
|
|
163
169
|
this.config = config;
|
|
170
|
+
this.host = host;
|
|
164
171
|
this.name = `pepr-${config.uuid}`;
|
|
165
172
|
this.image = `ghcr.io/defenseunicorns/pepr/controller:${config.version}`;
|
|
166
173
|
// Generate the ephemeral tls things
|
|
167
|
-
this.
|
|
174
|
+
this._tls = genTLS(this.host || `${this.name}.pepr-system.svc`);
|
|
168
175
|
}
|
|
169
176
|
/** Generate the pepr-system namespace */
|
|
170
177
|
namespace() {
|
|
@@ -235,8 +242,8 @@ class Webhook {
|
|
|
235
242
|
},
|
|
236
243
|
type: "kubernetes.io/tls",
|
|
237
244
|
data: {
|
|
238
|
-
"tls.crt": this.
|
|
239
|
-
"tls.key": this.
|
|
245
|
+
"tls.crt": this._tls.crt,
|
|
246
|
+
"tls.key": this._tls.key,
|
|
240
247
|
},
|
|
241
248
|
};
|
|
242
249
|
}
|
|
@@ -251,6 +258,21 @@ class Webhook {
|
|
|
251
258
|
values: this.config.alwaysIgnore.namespaces,
|
|
252
259
|
});
|
|
253
260
|
}
|
|
261
|
+
const clientConfig = {
|
|
262
|
+
caBundle: this._tls.ca,
|
|
263
|
+
};
|
|
264
|
+
// If a host is specified, use that with a port of 3000
|
|
265
|
+
if (this.host) {
|
|
266
|
+
clientConfig.url = `https://${this.host}:3000/mutate`;
|
|
267
|
+
}
|
|
268
|
+
else {
|
|
269
|
+
// Otherwise, use the service
|
|
270
|
+
clientConfig.service = {
|
|
271
|
+
name: this.name,
|
|
272
|
+
namespace: "pepr-system",
|
|
273
|
+
path: "/mutate",
|
|
274
|
+
};
|
|
275
|
+
}
|
|
254
276
|
return {
|
|
255
277
|
apiVersion: "admissionregistration.k8s.io/v1",
|
|
256
278
|
kind: "MutatingWebhookConfiguration",
|
|
@@ -259,14 +281,7 @@ class Webhook {
|
|
|
259
281
|
{
|
|
260
282
|
name: `${name}.pepr.dev`,
|
|
261
283
|
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
|
-
},
|
|
284
|
+
clientConfig,
|
|
270
285
|
failurePolicy: "Ignore",
|
|
271
286
|
matchPolicy: "Equivalent",
|
|
272
287
|
timeoutSeconds: 15,
|
|
@@ -351,6 +366,11 @@ class Webhook {
|
|
|
351
366
|
mountPath: "/etc/certs",
|
|
352
367
|
readOnly: true,
|
|
353
368
|
},
|
|
369
|
+
{
|
|
370
|
+
name: "module",
|
|
371
|
+
mountPath: "/app/module.js.gz",
|
|
372
|
+
readOnly: true,
|
|
373
|
+
},
|
|
354
374
|
],
|
|
355
375
|
},
|
|
356
376
|
],
|
|
@@ -361,12 +381,56 @@ class Webhook {
|
|
|
361
381
|
secretName: `${this.name}-tls`,
|
|
362
382
|
},
|
|
363
383
|
},
|
|
384
|
+
{
|
|
385
|
+
name: "module",
|
|
386
|
+
secret: {
|
|
387
|
+
secretName: `${this.name}-module`,
|
|
388
|
+
},
|
|
389
|
+
},
|
|
364
390
|
],
|
|
365
391
|
},
|
|
366
392
|
},
|
|
367
393
|
},
|
|
368
394
|
};
|
|
369
395
|
}
|
|
396
|
+
/** Only permit the */
|
|
397
|
+
networkPolicy() {
|
|
398
|
+
return {
|
|
399
|
+
apiVersion: "networking.k8s.io/v1",
|
|
400
|
+
kind: "NetworkPolicy",
|
|
401
|
+
metadata: {
|
|
402
|
+
name: this.name,
|
|
403
|
+
namespace: "pepr-system",
|
|
404
|
+
},
|
|
405
|
+
spec: {
|
|
406
|
+
podSelector: {
|
|
407
|
+
matchLabels: {
|
|
408
|
+
app: this.name,
|
|
409
|
+
},
|
|
410
|
+
},
|
|
411
|
+
policyTypes: ["Ingress"],
|
|
412
|
+
ingress: [
|
|
413
|
+
{
|
|
414
|
+
from: [
|
|
415
|
+
{
|
|
416
|
+
namespaceSelector: {
|
|
417
|
+
matchLabels: {
|
|
418
|
+
"kubernetes.io/metadata.name": "kube-system",
|
|
419
|
+
},
|
|
420
|
+
},
|
|
421
|
+
},
|
|
422
|
+
],
|
|
423
|
+
ports: [
|
|
424
|
+
{
|
|
425
|
+
protocol: "TCP",
|
|
426
|
+
port: 443,
|
|
427
|
+
},
|
|
428
|
+
],
|
|
429
|
+
},
|
|
430
|
+
],
|
|
431
|
+
},
|
|
432
|
+
};
|
|
433
|
+
}
|
|
370
434
|
service() {
|
|
371
435
|
return {
|
|
372
436
|
apiVersion: "v1",
|
|
@@ -433,6 +497,7 @@ class Webhook {
|
|
|
433
497
|
allYaml(code) {
|
|
434
498
|
const resources = [
|
|
435
499
|
this.namespace(),
|
|
500
|
+
this.networkPolicy(),
|
|
436
501
|
this.clusterRole(),
|
|
437
502
|
this.clusterRoleBinding(),
|
|
438
503
|
this.serviceAccount(),
|
|
@@ -446,7 +511,7 @@ class Webhook {
|
|
|
446
511
|
return resources.map(r => clientNode.dumpYaml(r, { noRefs: true })).join("---\n");
|
|
447
512
|
}
|
|
448
513
|
async deploy(code) {
|
|
449
|
-
types.
|
|
514
|
+
types.logger.info("Establishing connection to Kubernetes");
|
|
450
515
|
const namespace = "pepr-system";
|
|
451
516
|
// Deploy the resources using the k8s API
|
|
452
517
|
const kubeConfig = new clientNode.KubeConfig();
|
|
@@ -455,106 +520,121 @@ class Webhook {
|
|
|
455
520
|
const rbacApi = kubeConfig.makeApiClient(clientNode.RbacAuthorizationV1Api);
|
|
456
521
|
const appsApi = kubeConfig.makeApiClient(clientNode.AppsV1Api);
|
|
457
522
|
const admissionApi = kubeConfig.makeApiClient(clientNode.AdmissionregistrationV1Api);
|
|
523
|
+
const networkApi = kubeConfig.makeApiClient(clientNode.NetworkingV1Api);
|
|
458
524
|
const ns = this.namespace();
|
|
459
525
|
try {
|
|
460
|
-
types.
|
|
526
|
+
types.logger.info("Checking for namespace");
|
|
461
527
|
await coreV1Api.readNamespace(namespace);
|
|
462
528
|
}
|
|
463
529
|
catch (e) {
|
|
464
|
-
types.
|
|
465
|
-
types.
|
|
530
|
+
types.logger.debug(e.body);
|
|
531
|
+
types.logger.info("Creating namespace");
|
|
466
532
|
await coreV1Api.createNamespace(ns);
|
|
467
533
|
}
|
|
534
|
+
const netpol = this.networkPolicy();
|
|
535
|
+
try {
|
|
536
|
+
types.logger.info("Checking for network policy");
|
|
537
|
+
await networkApi.readNamespacedNetworkPolicy(netpol.metadata.name, namespace);
|
|
538
|
+
}
|
|
539
|
+
catch (e) {
|
|
540
|
+
types.logger.debug(e.body);
|
|
541
|
+
types.logger.info("Creating network policy");
|
|
542
|
+
await networkApi.createNamespacedNetworkPolicy(namespace, netpol);
|
|
543
|
+
}
|
|
468
544
|
const wh = this.mutatingWebhook();
|
|
469
545
|
try {
|
|
470
|
-
types.
|
|
546
|
+
types.logger.info("Creating mutating webhook");
|
|
471
547
|
await admissionApi.createMutatingWebhookConfiguration(wh);
|
|
472
548
|
}
|
|
473
549
|
catch (e) {
|
|
474
|
-
types.
|
|
475
|
-
types.
|
|
550
|
+
types.logger.debug(e.body);
|
|
551
|
+
types.logger.info("Removing and re-creating mutating webhook");
|
|
476
552
|
await admissionApi.deleteMutatingWebhookConfiguration(wh.metadata.name);
|
|
477
553
|
await admissionApi.createMutatingWebhookConfiguration(wh);
|
|
478
554
|
}
|
|
479
555
|
const crb = this.clusterRoleBinding();
|
|
480
556
|
try {
|
|
481
|
-
types.
|
|
557
|
+
types.logger.info("Creating cluster role binding");
|
|
482
558
|
await rbacApi.createClusterRoleBinding(crb);
|
|
483
559
|
}
|
|
484
560
|
catch (e) {
|
|
485
|
-
types.
|
|
486
|
-
types.
|
|
561
|
+
types.logger.debug(e.body);
|
|
562
|
+
types.logger.info("Removing and re-creating cluster role binding");
|
|
487
563
|
await rbacApi.deleteClusterRoleBinding(crb.metadata.name);
|
|
488
564
|
await rbacApi.createClusterRoleBinding(crb);
|
|
489
565
|
}
|
|
490
566
|
const cr = this.clusterRole();
|
|
491
567
|
try {
|
|
492
|
-
types.
|
|
568
|
+
types.logger.info("Creating cluster role");
|
|
493
569
|
await rbacApi.createClusterRole(cr);
|
|
494
570
|
}
|
|
495
571
|
catch (e) {
|
|
496
|
-
types.
|
|
497
|
-
types.
|
|
572
|
+
types.logger.debug(e.body);
|
|
573
|
+
types.logger.info("Removing and re-creating the cluster role");
|
|
498
574
|
try {
|
|
499
575
|
await rbacApi.deleteClusterRole(cr.metadata.name);
|
|
500
576
|
await rbacApi.createClusterRole(cr);
|
|
501
577
|
}
|
|
502
578
|
catch (e) {
|
|
503
|
-
types.
|
|
579
|
+
types.logger.debug(e.body);
|
|
504
580
|
}
|
|
505
581
|
}
|
|
506
582
|
const sa = this.serviceAccount();
|
|
507
583
|
try {
|
|
508
|
-
types.
|
|
584
|
+
types.logger.info("Creating service account");
|
|
509
585
|
await coreV1Api.createNamespacedServiceAccount(namespace, sa);
|
|
510
586
|
}
|
|
511
587
|
catch (e) {
|
|
512
|
-
types.
|
|
513
|
-
types.
|
|
588
|
+
types.logger.debug(e.body);
|
|
589
|
+
types.logger.info("Removing and re-creating service account");
|
|
514
590
|
await coreV1Api.deleteNamespacedServiceAccount(sa.metadata.name, namespace);
|
|
515
591
|
await coreV1Api.createNamespacedServiceAccount(namespace, sa);
|
|
516
592
|
}
|
|
593
|
+
// If a host is specified, we don't need to deploy the rest of the resources
|
|
594
|
+
if (this.host) {
|
|
595
|
+
return;
|
|
596
|
+
}
|
|
517
597
|
const mod = this.moduleSecret(code);
|
|
518
598
|
try {
|
|
519
|
-
types.
|
|
599
|
+
types.logger.info("Creating module secret");
|
|
520
600
|
await coreV1Api.createNamespacedSecret(namespace, mod);
|
|
521
601
|
}
|
|
522
602
|
catch (e) {
|
|
523
|
-
types.
|
|
524
|
-
types.
|
|
603
|
+
types.logger.debug(e.body);
|
|
604
|
+
types.logger.info("Removing and re-creating module secret");
|
|
525
605
|
await coreV1Api.deleteNamespacedSecret(mod.metadata.name, namespace);
|
|
526
606
|
await coreV1Api.createNamespacedSecret(namespace, mod);
|
|
527
607
|
}
|
|
528
608
|
const svc = this.service();
|
|
529
609
|
try {
|
|
530
|
-
types.
|
|
610
|
+
types.logger.info("Creating service");
|
|
531
611
|
await coreV1Api.createNamespacedService(namespace, svc);
|
|
532
612
|
}
|
|
533
613
|
catch (e) {
|
|
534
|
-
types.
|
|
535
|
-
types.
|
|
614
|
+
types.logger.debug(e.body);
|
|
615
|
+
types.logger.info("Removing and re-creating service");
|
|
536
616
|
await coreV1Api.deleteNamespacedService(svc.metadata.name, namespace);
|
|
537
617
|
await coreV1Api.createNamespacedService(namespace, svc);
|
|
538
618
|
}
|
|
539
619
|
const tls = this.tlsSecret();
|
|
540
620
|
try {
|
|
541
|
-
types.
|
|
621
|
+
types.logger.info("Creating TLS secret");
|
|
542
622
|
await coreV1Api.createNamespacedSecret(namespace, tls);
|
|
543
623
|
}
|
|
544
624
|
catch (e) {
|
|
545
|
-
types.
|
|
546
|
-
types.
|
|
625
|
+
types.logger.debug(e.body);
|
|
626
|
+
types.logger.info("Removing and re-creating TLS secret");
|
|
547
627
|
await coreV1Api.deleteNamespacedSecret(tls.metadata.name, namespace);
|
|
548
628
|
await coreV1Api.createNamespacedSecret(namespace, tls);
|
|
549
629
|
}
|
|
550
630
|
const dep = this.deployment();
|
|
551
631
|
try {
|
|
552
|
-
types.
|
|
632
|
+
types.logger.info("Creating deployment");
|
|
553
633
|
await appsApi.createNamespacedDeployment(namespace, dep);
|
|
554
634
|
}
|
|
555
635
|
catch (e) {
|
|
556
|
-
types.
|
|
557
|
-
types.
|
|
636
|
+
types.logger.debug(e.body);
|
|
637
|
+
types.logger.info("Removing and re-creating deployment");
|
|
558
638
|
await appsApi.deleteNamespacedDeployment(dep.metadata.name, namespace);
|
|
559
639
|
await appsApi.createNamespacedDeployment(namespace, dep);
|
|
560
640
|
}
|
|
@@ -584,8 +664,8 @@ function build (program) {
|
|
|
584
664
|
const zarf = webhook.zarfYaml(yamlFile);
|
|
585
665
|
await fs.promises.writeFile(yamlPath, yaml);
|
|
586
666
|
await fs.promises.writeFile(zarfPath, zarf);
|
|
587
|
-
types.
|
|
588
|
-
types.
|
|
667
|
+
types.logger.debug(`Module compiled successfully at ${path$1}`);
|
|
668
|
+
types.logger.info(`K8s resource for the module saved to ${yamlPath}`);
|
|
589
669
|
});
|
|
590
670
|
}
|
|
591
671
|
const externalLibs = [
|
|
@@ -644,8 +724,8 @@ async function buildModule(moduleDir) {
|
|
|
644
724
|
}
|
|
645
725
|
catch (e) {
|
|
646
726
|
// On any other error, exit with a non-zero exit code
|
|
647
|
-
types.
|
|
648
|
-
types.
|
|
727
|
+
types.logger.debug(e);
|
|
728
|
+
types.logger.error(e.message);
|
|
649
729
|
process.exit(1);
|
|
650
730
|
}
|
|
651
731
|
}
|
|
@@ -698,10 +778,51 @@ function deploy (program) {
|
|
|
698
778
|
}
|
|
699
779
|
try {
|
|
700
780
|
await webhook.deploy(code);
|
|
701
|
-
types.
|
|
781
|
+
types.logger.info(`Module deployed successfully`);
|
|
702
782
|
}
|
|
703
783
|
catch (e) {
|
|
704
|
-
types.
|
|
784
|
+
types.logger.error(`Error deploying module: ${e}`);
|
|
785
|
+
process.exit(1);
|
|
786
|
+
}
|
|
787
|
+
});
|
|
788
|
+
}
|
|
789
|
+
|
|
790
|
+
// SPDX-License-Identifier: Apache-2.0
|
|
791
|
+
function dev (program) {
|
|
792
|
+
program
|
|
793
|
+
.command("dev")
|
|
794
|
+
.description("Setup a local webhook development environment")
|
|
795
|
+
.option("-d, --dir [directory]", "Pepr module directory", ".")
|
|
796
|
+
.option("-h, --host [host]", "Host to listen on", "host.docker.internal")
|
|
797
|
+
.action(async (opts) => {
|
|
798
|
+
// Prompt the user to confirm
|
|
799
|
+
const confirm = await prompt.prompt({
|
|
800
|
+
type: "confirm",
|
|
801
|
+
name: "confirm",
|
|
802
|
+
message: "This will remove and redeploy the module. Continue?",
|
|
803
|
+
});
|
|
804
|
+
// Exit if the user doesn't confirm
|
|
805
|
+
if (!confirm.confirm) {
|
|
806
|
+
process.exit(0);
|
|
807
|
+
}
|
|
808
|
+
// Build the module
|
|
809
|
+
const { cfg, path } = await buildModule(opts.dir);
|
|
810
|
+
// Read the compiled module code
|
|
811
|
+
const code = await fs.promises.readFile(path, { encoding: "utf-8" });
|
|
812
|
+
// Generate a secret for the module
|
|
813
|
+
const webhook = new Webhook({
|
|
814
|
+
...cfg.pepr,
|
|
815
|
+
description: cfg.description,
|
|
816
|
+
}, opts.host);
|
|
817
|
+
// Write the TLS cert and key to disk
|
|
818
|
+
await fs.promises.writeFile("insecure-tls.crt", webhook.tls.pem.crt);
|
|
819
|
+
await fs.promises.writeFile("insecure-tls.key", webhook.tls.pem.key);
|
|
820
|
+
try {
|
|
821
|
+
await webhook.deploy(code);
|
|
822
|
+
types.logger.info(`Module deployed successfully`);
|
|
823
|
+
}
|
|
824
|
+
catch (e) {
|
|
825
|
+
types.logger.error(`Error deploying module: ${e}`);
|
|
705
826
|
process.exit(1);
|
|
706
827
|
}
|
|
707
828
|
});
|
|
@@ -1027,11 +1148,7 @@ function walkthrough() {
|
|
|
1027
1148
|
},
|
|
1028
1149
|
],
|
|
1029
1150
|
};
|
|
1030
|
-
return prompt([
|
|
1031
|
-
askName,
|
|
1032
|
-
askDescription,
|
|
1033
|
-
askErrorBehavior,
|
|
1034
|
-
]);
|
|
1151
|
+
return prompt([askName, askDescription, askErrorBehavior]);
|
|
1035
1152
|
}
|
|
1036
1153
|
async function confirm(dirName, packageJSON, peprTSPath) {
|
|
1037
1154
|
console.log(`
|
|
@@ -1090,8 +1207,8 @@ function init (program) {
|
|
|
1090
1207
|
console.log(`Open VSCode or your editor of choice in ${dirName} to get started!`);
|
|
1091
1208
|
}
|
|
1092
1209
|
catch (e) {
|
|
1093
|
-
types.
|
|
1094
|
-
types.
|
|
1210
|
+
types.logger.debug(e);
|
|
1211
|
+
types.logger.error(e.message);
|
|
1095
1212
|
process.exit(1);
|
|
1096
1213
|
}
|
|
1097
1214
|
}
|
|
@@ -1104,7 +1221,7 @@ class RootCmd extends commander.Command {
|
|
|
1104
1221
|
const cmd = new commander.Command(name);
|
|
1105
1222
|
cmd.option("-l, --log-level [level]", "Log level: debug, info, warn, error", "info");
|
|
1106
1223
|
cmd.hook("preAction", run => {
|
|
1107
|
-
types.
|
|
1224
|
+
types.logger.SetLogLevel(run.opts().logLevel);
|
|
1108
1225
|
});
|
|
1109
1226
|
return cmd;
|
|
1110
1227
|
}
|
|
@@ -1119,15 +1236,15 @@ function test (program) {
|
|
|
1119
1236
|
.option("-d, --dir [directory]", "Pepr module directory", ".")
|
|
1120
1237
|
.option("-w, --watch", "Watch for changes and re-run the test")
|
|
1121
1238
|
.action(async (opts) => {
|
|
1122
|
-
types.
|
|
1239
|
+
types.logger.info("Test Module");
|
|
1123
1240
|
await buildAndTest(opts.dir);
|
|
1124
1241
|
if (opts.watch) {
|
|
1125
1242
|
const moduleFiles = path.resolve(opts.dir, "**", "*.ts");
|
|
1126
1243
|
const watcher = chokidar.watch(moduleFiles);
|
|
1127
1244
|
watcher.on("ready", () => {
|
|
1128
|
-
types.
|
|
1245
|
+
types.logger.info(`Watching for changes in ${moduleFiles}`);
|
|
1129
1246
|
watcher.on("all", async (event, path) => {
|
|
1130
|
-
types.
|
|
1247
|
+
types.logger.debug({ event, path }, "File changed");
|
|
1131
1248
|
await buildAndTest(opts.dir);
|
|
1132
1249
|
});
|
|
1133
1250
|
});
|
|
@@ -1136,15 +1253,15 @@ function test (program) {
|
|
|
1136
1253
|
}
|
|
1137
1254
|
async function buildAndTest(dir) {
|
|
1138
1255
|
const { path } = await buildModule(dir);
|
|
1139
|
-
types.
|
|
1256
|
+
types.logger.info(`Module built successfully at ${path}`);
|
|
1140
1257
|
try {
|
|
1141
1258
|
const { stdout, stderr } = await exec(`node ${path}`);
|
|
1142
1259
|
console.log(stdout);
|
|
1143
1260
|
console.log(stderr);
|
|
1144
1261
|
}
|
|
1145
1262
|
catch (e) {
|
|
1146
|
-
types.
|
|
1147
|
-
types.
|
|
1263
|
+
types.logger.debug(e);
|
|
1264
|
+
types.logger.error(`Error running module: ${e}`);
|
|
1148
1265
|
process.exit(1);
|
|
1149
1266
|
}
|
|
1150
1267
|
}
|
|
@@ -1165,4 +1282,5 @@ build(program);
|
|
|
1165
1282
|
capability(program);
|
|
1166
1283
|
test(program);
|
|
1167
1284
|
deploy(program);
|
|
1285
|
+
dev(program);
|
|
1168
1286
|
program.parse();
|
package/dist/pepr-core.js
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
'use strict';
|
|
3
3
|
|
|
4
|
-
var types = require('./types-672dd6e4.js');
|
|
5
4
|
var dist = require('@kubernetes/client-node/dist');
|
|
5
|
+
var types = require('./types-1709b44f.js');
|
|
6
6
|
var R = require('ramda');
|
|
7
7
|
var fastJsonPatch = require('fast-json-patch');
|
|
8
8
|
|
|
@@ -501,6 +501,16 @@ function modelToGroupVersionKind(key) {
|
|
|
501
501
|
return gvkMap[key];
|
|
502
502
|
}
|
|
503
503
|
|
|
504
|
+
// SPDX-License-Identifier: Apache-2.0
|
|
505
|
+
// SPDX-FileCopyrightText: 2023-Present The Pepr Authors
|
|
506
|
+
var Operation;
|
|
507
|
+
(function (Operation) {
|
|
508
|
+
Operation["CREATE"] = "CREATE";
|
|
509
|
+
Operation["UPDATE"] = "UPDATE";
|
|
510
|
+
Operation["DELETE"] = "DELETE";
|
|
511
|
+
Operation["CONNECT"] = "CONNECT";
|
|
512
|
+
})(Operation || (Operation = {}));
|
|
513
|
+
|
|
504
514
|
// SPDX-License-Identifier: Apache-2.0
|
|
505
515
|
/**
|
|
506
516
|
* A capability is a unit of functionality that can be registered with the Pepr runtime.
|
|
@@ -553,10 +563,10 @@ class Capability {
|
|
|
553
563
|
callback: () => null,
|
|
554
564
|
};
|
|
555
565
|
const prefix = `${this._name}: ${model.name}`;
|
|
556
|
-
types.
|
|
566
|
+
types.logger.info(`Binding created`, prefix);
|
|
557
567
|
const Then = (cb) => {
|
|
558
|
-
types.
|
|
559
|
-
types.
|
|
568
|
+
types.logger.info(`Binding action created`, prefix);
|
|
569
|
+
types.logger.debug(cb.toString(), prefix);
|
|
560
570
|
// Push the binding to the list of bindings for this capability as a new BindingAction
|
|
561
571
|
// with the callback function to preserve
|
|
562
572
|
this._bindings.push({
|
|
@@ -572,22 +582,22 @@ class Capability {
|
|
|
572
582
|
return { Then };
|
|
573
583
|
};
|
|
574
584
|
function InNamespace(...namespaces) {
|
|
575
|
-
types.
|
|
585
|
+
types.logger.debug(`Add namespaces filter ${namespaces}`, prefix);
|
|
576
586
|
binding.filters.namespaces.push(...namespaces);
|
|
577
587
|
return { WithLabel, WithAnnotation, WithName, Then, ThenSet };
|
|
578
588
|
}
|
|
579
589
|
function WithName(name) {
|
|
580
|
-
types.
|
|
590
|
+
types.logger.debug(`Add name filter ${name}`, prefix);
|
|
581
591
|
binding.filters.name = name;
|
|
582
592
|
return { WithLabel, WithAnnotation, Then, ThenSet };
|
|
583
593
|
}
|
|
584
594
|
function WithLabel(key, value = "") {
|
|
585
|
-
types.
|
|
595
|
+
types.logger.debug(`Add label filter ${key}=${value}`, prefix);
|
|
586
596
|
binding.filters.labels[key] = value;
|
|
587
597
|
return { WithLabel, WithAnnotation, Then, ThenSet };
|
|
588
598
|
}
|
|
589
599
|
const WithAnnotation = (key, value = "") => {
|
|
590
|
-
types.
|
|
600
|
+
types.logger.debug(`Add annotation filter ${key}=${value}`, prefix);
|
|
591
601
|
binding.filters.annotations[key] = value;
|
|
592
602
|
return { WithLabel, WithAnnotation, Then, ThenSet };
|
|
593
603
|
};
|
|
@@ -612,11 +622,54 @@ class Capability {
|
|
|
612
622
|
this._name = cfg.name;
|
|
613
623
|
this._description = cfg.description;
|
|
614
624
|
this._namespaces = cfg.namespaces;
|
|
615
|
-
types.
|
|
616
|
-
types.
|
|
625
|
+
types.logger.info(`Capability ${this._name} registered`);
|
|
626
|
+
types.logger.debug(cfg);
|
|
617
627
|
}
|
|
618
628
|
}
|
|
619
629
|
|
|
630
|
+
// SPDX-License-Identifier: Apache-2.0
|
|
631
|
+
/**
|
|
632
|
+
* shouldSkipRequest determines if a request should be skipped based on the binding filters.
|
|
633
|
+
*
|
|
634
|
+
* @param binding the capability action binding
|
|
635
|
+
* @param req the incoming request
|
|
636
|
+
* @returns
|
|
637
|
+
*/
|
|
638
|
+
function shouldSkipRequest(binding, req) {
|
|
639
|
+
const { group, kind, version } = binding.kind;
|
|
640
|
+
const { namespaces, labels, annotations } = binding.filters;
|
|
641
|
+
const { metadata } = req.object;
|
|
642
|
+
if (kind !== req.kind.kind) {
|
|
643
|
+
types.logger.debug(`${req.kind.kind} does not match ${kind}`);
|
|
644
|
+
return true;
|
|
645
|
+
}
|
|
646
|
+
if (group && group !== req.kind.group) {
|
|
647
|
+
types.logger.debug(`${req.kind.group} does not match ${group}`);
|
|
648
|
+
return true;
|
|
649
|
+
}
|
|
650
|
+
if (version && version !== req.kind.version) {
|
|
651
|
+
types.logger.debug(`${req.kind.version} does not match ${version}`);
|
|
652
|
+
return true;
|
|
653
|
+
}
|
|
654
|
+
if (namespaces.length && !namespaces.includes(req.namespace || "")) {
|
|
655
|
+
types.logger.debug(`${req.namespace} is not in ${namespaces}`);
|
|
656
|
+
return true;
|
|
657
|
+
}
|
|
658
|
+
for (const [key, value] of Object.entries(labels)) {
|
|
659
|
+
if (metadata?.labels?.[key] !== value) {
|
|
660
|
+
types.logger.debug(`${metadata?.labels?.[key]} does not match ${value}`);
|
|
661
|
+
return true;
|
|
662
|
+
}
|
|
663
|
+
}
|
|
664
|
+
for (const [key, value] of Object.entries(annotations)) {
|
|
665
|
+
if (metadata?.annotations?.[key] !== value) {
|
|
666
|
+
types.logger.debug(`${metadata?.annotations?.[key]} does not match ${value}`);
|
|
667
|
+
return true;
|
|
668
|
+
}
|
|
669
|
+
}
|
|
670
|
+
return false;
|
|
671
|
+
}
|
|
672
|
+
|
|
620
673
|
// SPDX-License-Identifier: Apache-2.0
|
|
621
674
|
/**
|
|
622
675
|
* The RequestWrapper class provides methods to modify Kubernetes objects in the context
|
|
@@ -733,49 +786,6 @@ class RequestWrapper {
|
|
|
733
786
|
}
|
|
734
787
|
}
|
|
735
788
|
|
|
736
|
-
// SPDX-License-Identifier: Apache-2.0
|
|
737
|
-
/**
|
|
738
|
-
* shouldSkipRequest determines if a request should be skipped based on the binding filters.
|
|
739
|
-
*
|
|
740
|
-
* @param binding the capability action binding
|
|
741
|
-
* @param req the incoming request
|
|
742
|
-
* @returns
|
|
743
|
-
*/
|
|
744
|
-
function shouldSkipRequest(binding, req) {
|
|
745
|
-
const { group, kind, version } = binding.kind;
|
|
746
|
-
const { namespaces, labels, annotations } = binding.filters;
|
|
747
|
-
const { metadata } = req.object;
|
|
748
|
-
if (kind !== req.kind.kind) {
|
|
749
|
-
types.Log.debug(`${req.kind.kind} does not match ${kind}`);
|
|
750
|
-
return true;
|
|
751
|
-
}
|
|
752
|
-
if (group && group !== req.kind.group) {
|
|
753
|
-
types.Log.debug(`${req.kind.group} does not match ${group}`);
|
|
754
|
-
return true;
|
|
755
|
-
}
|
|
756
|
-
if (version && version !== req.kind.version) {
|
|
757
|
-
types.Log.debug(`${req.kind.version} does not match ${version}`);
|
|
758
|
-
return true;
|
|
759
|
-
}
|
|
760
|
-
if (namespaces.length && !namespaces.includes(req.namespace || "")) {
|
|
761
|
-
types.Log.debug(`${req.namespace} is not in ${namespaces}`);
|
|
762
|
-
return true;
|
|
763
|
-
}
|
|
764
|
-
for (const [key, value] of Object.entries(labels)) {
|
|
765
|
-
if (metadata?.labels?.[key] !== value) {
|
|
766
|
-
types.Log.debug(`${metadata?.labels?.[key]} does not match ${value}`);
|
|
767
|
-
return true;
|
|
768
|
-
}
|
|
769
|
-
}
|
|
770
|
-
for (const [key, value] of Object.entries(annotations)) {
|
|
771
|
-
if (metadata?.annotations?.[key] !== value) {
|
|
772
|
-
types.Log.debug(`${metadata?.annotations?.[key]} does not match ${value}`);
|
|
773
|
-
return true;
|
|
774
|
-
}
|
|
775
|
-
}
|
|
776
|
-
return false;
|
|
777
|
-
}
|
|
778
|
-
|
|
779
789
|
// SPDX-License-Identifier: Apache-2.0
|
|
780
790
|
function processor(config, capabilities, req) {
|
|
781
791
|
const wrapped = new RequestWrapper(req);
|
|
@@ -785,16 +795,16 @@ function processor(config, capabilities, req) {
|
|
|
785
795
|
warnings: [],
|
|
786
796
|
allowed: false,
|
|
787
797
|
};
|
|
788
|
-
types.
|
|
798
|
+
types.logger.info(`Processing '${req.uid}' for '${req.kind.kind}' '${req.name}'`);
|
|
789
799
|
for (const { name, bindings } of capabilities) {
|
|
790
800
|
const prefix = `${req.uid} ${req.name}: ${name}`;
|
|
791
|
-
types.
|
|
801
|
+
types.logger.info(`Processing capability ${name}`, prefix);
|
|
792
802
|
for (const action of bindings) {
|
|
793
803
|
// Continue to the next action without doing anything if this one should be skipped
|
|
794
804
|
if (shouldSkipRequest(action, req)) {
|
|
795
805
|
continue;
|
|
796
806
|
}
|
|
797
|
-
types.
|
|
807
|
+
types.logger.info(`Processing matched action ${action.kind.kind}`, prefix);
|
|
798
808
|
// Add annotations to the request to indicate that the capability started processing
|
|
799
809
|
// this will allow tracking of failed mutations that were permitted to continue
|
|
800
810
|
const { metadata } = wrapped.Raw;
|
|
@@ -811,12 +821,12 @@ function processor(config, capabilities, req) {
|
|
|
811
821
|
response.warnings.push(`Action failed: ${e}`);
|
|
812
822
|
// If errors are not allowed, note the failure in the Reponse
|
|
813
823
|
if (config.onError) {
|
|
814
|
-
types.
|
|
824
|
+
types.logger.error(`Action failed: ${e}`, prefix);
|
|
815
825
|
response.result = "Pepr module configured to reject on error";
|
|
816
826
|
return response;
|
|
817
827
|
}
|
|
818
828
|
else {
|
|
819
|
-
types.
|
|
829
|
+
types.logger.warn(`Action failed: ${e}`, prefix);
|
|
820
830
|
metadata.annotations[identifier] = "warning";
|
|
821
831
|
}
|
|
822
832
|
}
|
|
@@ -834,7 +844,7 @@ function processor(config, capabilities, req) {
|
|
|
834
844
|
if (response.warnings.length < 1) {
|
|
835
845
|
delete response.warnings;
|
|
836
846
|
}
|
|
837
|
-
types.
|
|
847
|
+
types.logger.debug(patches);
|
|
838
848
|
return response;
|
|
839
849
|
}
|
|
840
850
|
|
|
@@ -859,7 +869,7 @@ class PeprModule {
|
|
|
859
869
|
this._state = [];
|
|
860
870
|
this._kinds = [];
|
|
861
871
|
this.Register = (capability) => {
|
|
862
|
-
types.
|
|
872
|
+
types.logger.info(`Registering capability ${capability.name}`);
|
|
863
873
|
// Add the kinds to the list of kinds (ignoring duplicates for now)
|
|
864
874
|
this._kinds = capability.bindings.map(({ kind }) => kind);
|
|
865
875
|
// Add the capability to the state
|
|
@@ -873,26 +883,7 @@ class PeprModule {
|
|
|
873
883
|
}
|
|
874
884
|
}
|
|
875
885
|
|
|
876
|
-
|
|
877
|
-
enumerable: true,
|
|
878
|
-
get: function () { return types.ErrorBehavior; }
|
|
879
|
-
});
|
|
880
|
-
Object.defineProperty(exports, 'Event', {
|
|
881
|
-
enumerable: true,
|
|
882
|
-
get: function () { return types.Event; }
|
|
883
|
-
});
|
|
884
|
-
Object.defineProperty(exports, 'HookPhase', {
|
|
885
|
-
enumerable: true,
|
|
886
|
-
get: function () { return types.HookPhase; }
|
|
887
|
-
});
|
|
888
|
-
exports.Log = types.Log;
|
|
889
|
-
Object.defineProperty(exports, 'Operation', {
|
|
890
|
-
enumerable: true,
|
|
891
|
-
get: function () { return types.Operation; }
|
|
892
|
-
});
|
|
886
|
+
exports.Log = types.logger;
|
|
893
887
|
exports.Capability = Capability;
|
|
894
888
|
exports.PeprModule = PeprModule;
|
|
895
|
-
exports.RequestWrapper = RequestWrapper;
|
|
896
889
|
exports.a = upstream;
|
|
897
|
-
exports.gvkMap = gvkMap;
|
|
898
|
-
exports.modelToGroupVersionKind = modelToGroupVersionKind;
|
|
@@ -116,17 +116,7 @@ class Logger {
|
|
|
116
116
|
return color + text + ConsoleColors.Reset;
|
|
117
117
|
}
|
|
118
118
|
}
|
|
119
|
-
var
|
|
120
|
-
|
|
121
|
-
// SPDX-License-Identifier: Apache-2.0
|
|
122
|
-
// SPDX-FileCopyrightText: 2023-Present The Pepr Authors
|
|
123
|
-
exports.Operation = void 0;
|
|
124
|
-
(function (Operation) {
|
|
125
|
-
Operation["CREATE"] = "CREATE";
|
|
126
|
-
Operation["UPDATE"] = "UPDATE";
|
|
127
|
-
Operation["DELETE"] = "DELETE";
|
|
128
|
-
Operation["CONNECT"] = "CONNECT";
|
|
129
|
-
})(exports.Operation || (exports.Operation = {}));
|
|
119
|
+
var logger = new Logger(LogLevel.info);
|
|
130
120
|
|
|
131
121
|
// SPDX-License-Identifier: Apache-2.0
|
|
132
122
|
// SPDX-FileCopyrightText: 2023-Present The Pepr Authors
|
|
@@ -160,4 +150,4 @@ exports.Event = void 0;
|
|
|
160
150
|
Event["CreateOrUpdate"] = "createOrUpdate";
|
|
161
151
|
})(exports.Event || (exports.Event = {}));
|
|
162
152
|
|
|
163
|
-
exports.
|
|
153
|
+
exports.logger = logger;
|
package/index.ts
CHANGED
|
@@ -1,2 +1,7 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
1
|
+
import { Capability } from './src/lib/capability';
|
|
2
|
+
import { a } from './src/lib/k8s';
|
|
3
|
+
import Log from './src/lib/logger';
|
|
4
|
+
import { PeprModule } from './src/lib/module';
|
|
5
|
+
|
|
6
|
+
export { a, PeprModule, Capability, Log };
|
|
7
|
+
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pepr",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.24",
|
|
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": "
|
|
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"
|
package/src/lib/k8s/tls.ts
CHANGED
|
@@ -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,
|
|
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
|
|
46
|
-
const
|
|
47
|
-
|
|
48
|
-
|
|
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
|
-
|
|
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[]) {
|
package/src/lib/k8s/webhook.ts
CHANGED
|
@@ -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,13 +15,14 @@ import {
|
|
|
13
15
|
V1LabelSelectorRequirement,
|
|
14
16
|
V1MutatingWebhookConfiguration,
|
|
15
17
|
V1Namespace,
|
|
18
|
+
V1NetworkPolicy,
|
|
16
19
|
V1Secret,
|
|
17
20
|
V1Service,
|
|
18
21
|
V1ServiceAccount,
|
|
19
22
|
dumpYaml,
|
|
20
23
|
} from "@kubernetes/client-node";
|
|
21
24
|
import { gzipSync } from "zlib";
|
|
22
|
-
import
|
|
25
|
+
import Log from "../logger";
|
|
23
26
|
import { ModuleConfig } from "../types";
|
|
24
27
|
import { TLSOut, genTLS } from "./tls";
|
|
25
28
|
|
|
@@ -31,17 +34,21 @@ const peprIgnore: V1LabelSelectorRequirement = {
|
|
|
31
34
|
|
|
32
35
|
export class Webhook {
|
|
33
36
|
private name: string;
|
|
34
|
-
private
|
|
37
|
+
private _tls: TLSOut;
|
|
35
38
|
|
|
36
39
|
public image: string;
|
|
37
40
|
|
|
38
|
-
|
|
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.
|
|
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.
|
|
121
|
-
"tls.key": this.
|
|
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(),
|
|
@@ -339,7 +406,7 @@ export class Webhook {
|
|
|
339
406
|
}
|
|
340
407
|
|
|
341
408
|
async deploy(code: string) {
|
|
342
|
-
|
|
409
|
+
Log.info("Establishing connection to Kubernetes");
|
|
343
410
|
|
|
344
411
|
const namespace = "pepr-system";
|
|
345
412
|
|
|
@@ -351,105 +418,121 @@ 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 {
|
|
357
|
-
|
|
425
|
+
Log.info("Checking for namespace");
|
|
358
426
|
await coreV1Api.readNamespace(namespace);
|
|
359
427
|
} catch (e) {
|
|
360
|
-
|
|
361
|
-
|
|
428
|
+
Log.debug(e.body);
|
|
429
|
+
Log.info("Creating namespace");
|
|
362
430
|
await coreV1Api.createNamespace(ns);
|
|
363
431
|
}
|
|
364
432
|
|
|
433
|
+
const netpol = this.networkPolicy();
|
|
434
|
+
try {
|
|
435
|
+
Log.info("Checking for network policy");
|
|
436
|
+
await networkApi.readNamespacedNetworkPolicy(netpol.metadata.name, namespace);
|
|
437
|
+
} catch (e) {
|
|
438
|
+
Log.debug(e.body);
|
|
439
|
+
Log.info("Creating network policy");
|
|
440
|
+
await networkApi.createNamespacedNetworkPolicy(namespace, netpol);
|
|
441
|
+
}
|
|
442
|
+
|
|
365
443
|
const wh = this.mutatingWebhook();
|
|
366
444
|
try {
|
|
367
|
-
|
|
445
|
+
Log.info("Creating mutating webhook");
|
|
368
446
|
await admissionApi.createMutatingWebhookConfiguration(wh);
|
|
369
447
|
} catch (e) {
|
|
370
|
-
|
|
371
|
-
|
|
448
|
+
Log.debug(e.body);
|
|
449
|
+
Log.info("Removing and re-creating mutating webhook");
|
|
372
450
|
await admissionApi.deleteMutatingWebhookConfiguration(wh.metadata.name);
|
|
373
451
|
await admissionApi.createMutatingWebhookConfiguration(wh);
|
|
374
452
|
}
|
|
375
453
|
|
|
376
454
|
const crb = this.clusterRoleBinding();
|
|
377
455
|
try {
|
|
378
|
-
|
|
456
|
+
Log.info("Creating cluster role binding");
|
|
379
457
|
await rbacApi.createClusterRoleBinding(crb);
|
|
380
458
|
} catch (e) {
|
|
381
|
-
|
|
382
|
-
|
|
459
|
+
Log.debug(e.body);
|
|
460
|
+
Log.info("Removing and re-creating cluster role binding");
|
|
383
461
|
await rbacApi.deleteClusterRoleBinding(crb.metadata.name);
|
|
384
462
|
await rbacApi.createClusterRoleBinding(crb);
|
|
385
463
|
}
|
|
386
464
|
|
|
387
465
|
const cr = this.clusterRole();
|
|
388
466
|
try {
|
|
389
|
-
|
|
467
|
+
Log.info("Creating cluster role");
|
|
390
468
|
await rbacApi.createClusterRole(cr);
|
|
391
469
|
} catch (e) {
|
|
392
|
-
|
|
393
|
-
|
|
470
|
+
Log.debug(e.body);
|
|
471
|
+
Log.info("Removing and re-creating the cluster role");
|
|
394
472
|
try {
|
|
395
473
|
await rbacApi.deleteClusterRole(cr.metadata.name);
|
|
396
474
|
await rbacApi.createClusterRole(cr);
|
|
397
475
|
} catch (e) {
|
|
398
|
-
|
|
476
|
+
Log.debug(e.body);
|
|
399
477
|
}
|
|
400
478
|
}
|
|
401
479
|
|
|
402
480
|
const sa = this.serviceAccount();
|
|
403
481
|
try {
|
|
404
|
-
|
|
482
|
+
Log.info("Creating service account");
|
|
405
483
|
await coreV1Api.createNamespacedServiceAccount(namespace, sa);
|
|
406
484
|
} catch (e) {
|
|
407
|
-
|
|
408
|
-
|
|
485
|
+
Log.debug(e.body);
|
|
486
|
+
Log.info("Removing and re-creating service account");
|
|
409
487
|
await coreV1Api.deleteNamespacedServiceAccount(sa.metadata.name, namespace);
|
|
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
|
+
Log.info("Creating module secret");
|
|
416
499
|
await coreV1Api.createNamespacedSecret(namespace, mod);
|
|
417
500
|
} catch (e) {
|
|
418
|
-
|
|
419
|
-
|
|
501
|
+
Log.debug(e.body);
|
|
502
|
+
Log.info("Removing and re-creating module secret");
|
|
420
503
|
await coreV1Api.deleteNamespacedSecret(mod.metadata.name, namespace);
|
|
421
504
|
await coreV1Api.createNamespacedSecret(namespace, mod);
|
|
422
505
|
}
|
|
423
506
|
|
|
424
507
|
const svc = this.service();
|
|
425
508
|
try {
|
|
426
|
-
|
|
509
|
+
Log.info("Creating service");
|
|
427
510
|
await coreV1Api.createNamespacedService(namespace, svc);
|
|
428
511
|
} catch (e) {
|
|
429
|
-
|
|
430
|
-
|
|
512
|
+
Log.debug(e.body);
|
|
513
|
+
Log.info("Removing and re-creating service");
|
|
431
514
|
await coreV1Api.deleteNamespacedService(svc.metadata.name, namespace);
|
|
432
515
|
await coreV1Api.createNamespacedService(namespace, svc);
|
|
433
516
|
}
|
|
434
517
|
|
|
435
518
|
const tls = this.tlsSecret();
|
|
436
519
|
try {
|
|
437
|
-
|
|
520
|
+
Log.info("Creating TLS secret");
|
|
438
521
|
await coreV1Api.createNamespacedSecret(namespace, tls);
|
|
439
522
|
} catch (e) {
|
|
440
|
-
|
|
441
|
-
|
|
523
|
+
Log.debug(e.body);
|
|
524
|
+
Log.info("Removing and re-creating TLS secret");
|
|
442
525
|
await coreV1Api.deleteNamespacedSecret(tls.metadata.name, namespace);
|
|
443
526
|
await coreV1Api.createNamespacedSecret(namespace, tls);
|
|
444
527
|
}
|
|
445
528
|
|
|
446
529
|
const dep = this.deployment();
|
|
447
530
|
try {
|
|
448
|
-
|
|
531
|
+
Log.info("Creating deployment");
|
|
449
532
|
await appsApi.createNamespacedDeployment(namespace, dep);
|
|
450
533
|
} catch (e) {
|
|
451
|
-
|
|
452
|
-
|
|
534
|
+
Log.debug(e.body);
|
|
535
|
+
Log.info("Removing and re-creating deployment");
|
|
453
536
|
await appsApi.deleteNamespacedDeployment(dep.metadata.name, namespace);
|
|
454
537
|
await appsApi.createNamespacedDeployment(namespace, dep);
|
|
455
538
|
}
|
package/src/lib/index.ts
DELETED
|
@@ -1,10 +0,0 @@
|
|
|
1
|
-
// SPDX-License-Identifier: Apache-2.0
|
|
2
|
-
// SPDX-FileCopyrightText: 2023-Present The Pepr Authors
|
|
3
|
-
|
|
4
|
-
import logger from "./logger";
|
|
5
|
-
|
|
6
|
-
export { logger as Log };
|
|
7
|
-
export * from "./capability";
|
|
8
|
-
export * from "./request";
|
|
9
|
-
export * from "./module";
|
|
10
|
-
export * from "./types";
|