pepr 0.1.24 → 0.1.26

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
@@ -13,12 +13,12 @@ var zlib = require('zlib');
13
13
  var forge = require('node-forge');
14
14
  var prompt = require('prompts');
15
15
  var child_process = require('child_process');
16
+ var chokidar = require('chokidar');
16
17
  var util = require('util');
17
18
  var uuid = require('uuid');
18
19
  var commander = require('commander');
19
- var chokidar = require('chokidar');
20
20
 
21
- var version = "0.1.24";
21
+ var version = "0.1.26";
22
22
 
23
23
  // SPDX-License-Identifier: Apache-2.0
24
24
  // SPDX-FileCopyrightText: 2023-Present The Pepr Authors
@@ -806,9 +806,9 @@ function dev (program) {
806
806
  process.exit(0);
807
807
  }
808
808
  // Build the module
809
- const { cfg, path } = await buildModule(opts.dir);
809
+ const { cfg, path: path$1 } = await buildModule(opts.dir);
810
810
  // Read the compiled module code
811
- const code = await fs.promises.readFile(path, { encoding: "utf-8" });
811
+ const code = await fs.promises.readFile(path$1, { encoding: "utf-8" });
812
812
  // Generate a secret for the module
813
813
  const webhook = new Webhook({
814
814
  ...cfg.pepr,
@@ -820,6 +820,25 @@ function dev (program) {
820
820
  try {
821
821
  await webhook.deploy(code);
822
822
  types.logger.info(`Module deployed successfully`);
823
+ const moduleFiles = path.resolve(opts.dir, "**", "*.ts");
824
+ const watcher = chokidar.watch(moduleFiles);
825
+ const peprTS = path.resolve(opts.dir, "pepr.ts");
826
+ let program;
827
+ // Run the module once to start the server
828
+ runDev(peprTS);
829
+ // Watch for changes
830
+ watcher.on("ready", () => {
831
+ types.logger.info(`Watching for changes in ${moduleFiles}`);
832
+ watcher.on("all", async (event, path) => {
833
+ types.logger.debug({ event, path }, "File changed");
834
+ // Kill the running process
835
+ if (program) {
836
+ program.kill("SIGKILL");
837
+ }
838
+ // Start the process again
839
+ program = runDev(peprTS);
840
+ });
841
+ });
823
842
  }
824
843
  catch (e) {
825
844
  types.logger.error(`Error deploying module: ${e}`);
@@ -827,6 +846,28 @@ function dev (program) {
827
846
  }
828
847
  });
829
848
  }
849
+ function runDev(path) {
850
+ try {
851
+ const program = child_process.spawn("./node_modules/.bin/ts-node", [path], {
852
+ env: {
853
+ ...process.env,
854
+ SSL_KEY_PATH: "insecure-tls.key",
855
+ SSL_CERT_PATH: "insecure-tls.crt",
856
+ },
857
+ });
858
+ program.stdout.on("data", data => console.log(data.toString()));
859
+ program.stderr.on("data", data => console.error(data.toString()));
860
+ program.on("close", code => {
861
+ types.logger.info(`Process exited with code ${code}`);
862
+ });
863
+ return program;
864
+ }
865
+ catch (e) {
866
+ types.logger.debug(e);
867
+ types.logger.error(`Error running module: ${e}`);
868
+ process.exit(1);
869
+ }
870
+ }
830
871
 
831
872
  // SPDX-License-Identifier: Apache-2.0
832
873
  /**
@@ -883,28 +924,18 @@ function genPeprTS() {
883
924
  path: "pepr.ts",
884
925
  data: `import { PeprModule } from "pepr";
885
926
  import cfg from "./package.json";
886
- // import { HelloPepr } from "./capabilities/hello-pepr";
887
-
888
- // This initializes the Pepr module with the configuration from package.json
889
- export const { ProcessRequest, Register } = new PeprModule(cfg);
927
+ import { HelloPepr } from "./capabilities/hello-pepr";
890
928
 
891
929
  /**
892
- * Each Pepr Capability is registered with the module using the Register function.
893
- * This will be automatically generated by the Pepr CLI when creating a new capability:
894
- * \`pepr new <capability name>\`
895
- *
896
- * Example:
897
- * import { Capability1 } from "./capabilities/capability1";
898
- * import { Capability2 } from "./capabilities/capability2";
899
- *
900
- * Capability1(Register);
901
- * Capability2(Register);
902
- *
903
- * Uncomment the line below and the import above to use the hello-pepr demo capability
930
+ * This is the main entrypoint for the Pepr module. It is the file that is run when the module is started.
931
+ * This is where you register your configurations and capabilities with the module.
904
932
  */
933
+ new PeprModule(cfg, [
934
+ // "HelloPepr" is a demo capability that is included with Pepr. You can remove it if you want.
935
+ HelloPepr,
905
936
 
906
- // HelloPepr(Register);
907
-
937
+ // Your additional capabilities go here
938
+ ]);
908
939
  `,
909
940
  };
910
941
  }
@@ -1009,14 +1040,15 @@ const capabilityHelloPeprTS = {
1009
1040
  path: "hello-pepr.ts",
1010
1041
  data: `import { Capability, a } from "pepr";
1011
1042
 
1012
- const { Register, When } = new Capability({
1043
+ export const HelloPepr = new Capability({
1013
1044
  name: "hello-pepr",
1014
1045
  description: "A simple example capability to show how things work.",
1015
1046
  namespaces: ["pepr-demo"],
1016
1047
  });
1017
1048
 
1018
- export { Register as HelloPepr };
1019
-
1049
+ // Use the 'When' function to create a new Capability Action
1050
+ const { When } = HelloPepr;
1051
+
1020
1052
  /**
1021
1053
  * This is a single Capability Action. They can be in the same file or put imported from other files.
1022
1054
  * In this exmaple, when a ConfigMap is created with the name \`example-1\`, then add a label and annotation.
@@ -1079,27 +1111,26 @@ When(a.ConfigMap)
1079
1111
  const capabilitySnippet = {
1080
1112
  path: "pepr.code-snippets",
1081
1113
  data: `{
1082
- "Create a new Pepr capability": {
1083
- "prefix": "create pepr capability",
1084
- "body": [
1085
- "import { Capability, a } from 'pepr';",
1086
- "",
1087
- "const { Register, When } = new Capability({",
1088
- "\tname: '$\{TM_FILENAME_BASE}',",
1089
- "\tdescription: '$\{1:A brief description of this capability.}',",
1090
- "\tnamespaces: [$\{2:}],",
1091
- "});",
1092
- "",
1093
- "// Export the capability so it can be imported in the pepr.ts file.",
1094
- "export { Register as $\{TM_FILENAME_BASE/(.*)/$\{1:/pascalcase}/} };",
1095
- "",
1096
- "// When(a.<Kind>).Is<Event>().Then(change => change.<changes>",
1097
- "When($\{3:})"
1098
- ],
1099
- "description": "Creates a new Pepr capability with a specified description, and optional namespaces, and adds a When statement for the specified value."
1100
- }
1114
+ "Create a new Pepr capability": {
1115
+ "prefix": "create pepr capability",
1116
+ "body": [
1117
+ "import { Capability, a } from 'pepr';",
1118
+ "",
1119
+ "export const $\{TM_FILENAME_BASE/(.*)/$\{1:/pascalcase}/} = new Capability({",
1120
+ "\tname: '$\{TM_FILENAME_BASE}',",
1121
+ "\tdescription: '$\{1:A brief description of this capability.}',",
1122
+ "\tnamespaces: [$\{2:}],",
1123
+ "});",
1124
+ "",
1125
+ "// Use the 'When' function to create a new Capability Action",
1126
+ "const { When } = $\{TM_FILENAME_BASE/(.*)/$\{1:/pascalcase}/};",
1127
+ "",
1128
+ "// When(a.<Kind>).Is<Event>().Then(change => change.<changes>",
1129
+ "When($\{3:})"
1130
+ ],
1131
+ "description": "Creates a new Pepr capability with a specified description, and optional namespaces, and adds a When statement for the specified value."
1101
1132
  }
1102
- `,
1133
+ }`,
1103
1134
  };
1104
1135
 
1105
1136
  // SPDX-License-Identifier: Apache-2.0
package/dist/pepr-core.js CHANGED
@@ -4,6 +4,9 @@
4
4
  var dist = require('@kubernetes/client-node/dist');
5
5
  var types = require('./types-1709b44f.js');
6
6
  var R = require('ramda');
7
+ var express = require('express');
8
+ var fs = require('fs');
9
+ var https = require('https');
7
10
  var fastJsonPatch = require('fast-json-patch');
8
11
 
9
12
  function _interopNamespaceDefault(e) {
@@ -535,13 +538,6 @@ class Capability {
535
538
  // Currently everything is considered a mutation
536
539
  this._mutateOrValidate = types.HookPhase.mutate;
537
540
  this._bindings = [];
538
- /**
539
- * The Register method is used to register a capability with the Pepr runtime. This method is
540
- * called in the order that the capabilities should be executed.
541
- *
542
- * @param callback the state register method to call, passing the capability as an argument
543
- */
544
- this.Register = (register) => register(this);
545
541
  /**
546
542
  * The When method is used to register a capability action to be executed when a Kubernetes resource is
547
543
  * processed by Pepr. The action will be executed if the resource matches the specified kind and any
@@ -640,19 +636,16 @@ function shouldSkipRequest(binding, req) {
640
636
  const { namespaces, labels, annotations } = binding.filters;
641
637
  const { metadata } = req.object;
642
638
  if (kind !== req.kind.kind) {
643
- types.logger.debug(`${req.kind.kind} does not match ${kind}`);
644
639
  return true;
645
640
  }
646
641
  if (group && group !== req.kind.group) {
647
- types.logger.debug(`${req.kind.group} does not match ${group}`);
648
642
  return true;
649
643
  }
650
644
  if (version && version !== req.kind.version) {
651
- types.logger.debug(`${req.kind.version} does not match ${version}`);
652
645
  return true;
653
646
  }
654
647
  if (namespaces.length && !namespaces.includes(req.namespace || "")) {
655
- types.logger.debug(`${req.namespace} is not in ${namespaces}`);
648
+ types.logger.debug("Namespace does not match");
656
649
  return true;
657
650
  }
658
651
  for (const [key, value] of Object.entries(labels)) {
@@ -848,38 +841,106 @@ function processor(config, capabilities, req) {
848
841
  return response;
849
842
  }
850
843
 
844
+ // SPDX-License-Identifier: Apache-2.0
845
+ // Load SSL certificate and key
846
+ const options = {
847
+ key: fs.readFileSync(process.env.SSL_KEY_PATH || "/etc/certs/tls.key"),
848
+ cert: fs.readFileSync(process.env.SSL_CERT_PATH || "/etc/certs/tls.crt"),
849
+ };
850
+ class Controller {
851
+ constructor(config, capabilities) {
852
+ this.config = config;
853
+ this.capabilities = capabilities;
854
+ this.app = express();
855
+ /** Start the webhook server */
856
+ this.startServer = (port) => {
857
+ // Create HTTPS server
858
+ https.createServer(options, this.app).listen(port, () => {
859
+ console.log(`Server listening on port ${port}`);
860
+ });
861
+ };
862
+ this.logger = (req, res, next) => {
863
+ const startTime = Date.now();
864
+ res.on("finish", () => {
865
+ const now = new Date().toISOString();
866
+ const elapsedTime = Date.now() - startTime;
867
+ const message = `[${now}] ${req.method} ${req.originalUrl} - ${res.statusCode} - ${elapsedTime} ms\n`;
868
+ res.statusCode >= 400 ? console.error(message) : console.info(message);
869
+ });
870
+ next();
871
+ };
872
+ this.healthz = (req, res) => {
873
+ try {
874
+ res.send("OK");
875
+ }
876
+ catch (err) {
877
+ console.error(err);
878
+ res.status(500).send("Internal Server Error");
879
+ }
880
+ };
881
+ this.mutate = (req, res) => {
882
+ try {
883
+ const name = req.body?.request?.name || "";
884
+ const namespace = req.body?.request?.namespace || "";
885
+ const gvk = req.body?.request?.kind || { group: "", version: "", kind: "" };
886
+ console.log(`Mutate request: ${gvk.group}/${gvk.version}/${gvk.kind}`);
887
+ name && console.log(` ${namespace}/${name}\n`);
888
+ // @todo: make this actually do something
889
+ const response = processor(this.config, this.capabilities, req.body.request);
890
+ console.debug(response);
891
+ // Send a no prob bob response
892
+ res.send({
893
+ apiVersion: "admission.k8s.io/v1",
894
+ kind: "AdmissionReview",
895
+ response: {
896
+ uid: req.body.request.uid,
897
+ allowed: true,
898
+ },
899
+ });
900
+ }
901
+ catch (err) {
902
+ console.error(err);
903
+ res.status(500).send("Internal Server Error");
904
+ }
905
+ };
906
+ // Middleware for logging requests
907
+ this.app.use(this.logger);
908
+ // Middleware for parsing JSON
909
+ this.app.use(express.json());
910
+ // Health check endpoint
911
+ this.app.get("/healthz", this.healthz);
912
+ // Mutate endpoint
913
+ this.app.post("/mutate", this.mutate);
914
+ }
915
+ }
916
+
851
917
  // SPDX-License-Identifier: Apache-2.0
852
918
  const alwaysIgnore = {
853
919
  namespaces: ["kube-system", "pepr-system"],
854
920
  labels: [{ "pepr.dev": "ignore" }],
855
921
  };
856
922
  class PeprModule {
857
- get kinds() {
858
- return this._kinds;
859
- }
860
- get UUID() {
861
- return this._config.uuid;
862
- }
863
923
  /**
864
924
  * Create a new Pepr runtime
865
925
  *
866
926
  * @param config The configuration for the Pepr runtime
867
927
  */
868
- constructor({ description, pepr }) {
869
- this._state = [];
870
- this._kinds = [];
871
- this.Register = (capability) => {
872
- types.logger.info(`Registering capability ${capability.name}`);
873
- // Add the kinds to the list of kinds (ignoring duplicates for now)
874
- this._kinds = capability.bindings.map(({ kind }) => kind);
875
- // Add the capability to the state
876
- this._state.push(capability);
877
- };
878
- this.ProcessRequest = (req) => {
879
- return processor(this._config, this._state, req);
880
- };
881
- pepr.description = description;
882
- this._config = R.mergeDeepWith(R.concat, pepr, alwaysIgnore);
928
+ constructor({ description, pepr }, capabilities = [], deferStart = false) {
929
+ const config = R.mergeDeepWith(R.concat, pepr, alwaysIgnore);
930
+ config.description = description;
931
+ this._controller = new Controller(config, capabilities);
932
+ if (!deferStart) {
933
+ this.start();
934
+ }
935
+ }
936
+ /**
937
+ * Start the Pepr runtime manually.
938
+ * Normally this is called automatically when the Pepr module is instantiated, but can be called manually if `deferStart` is set to `true` in the constructor.
939
+ *
940
+ * @param port
941
+ */
942
+ start(port = 3000) {
943
+ this._controller.startServer(port);
883
944
  }
884
945
  }
885
946
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pepr",
3
- "version": "0.1.24",
3
+ "version": "0.1.26",
4
4
  "description": "Kubernetes application engine",
5
5
  "author": "Defense Unicorns",
6
6
  "homepage": "https://github.com/defenseunicorns/pepr",
@@ -22,6 +22,7 @@
22
22
  }
23
23
  },
24
24
  "scripts": {
25
+ "preversion": "npm run prepublishOnly",
25
26
  "build": "rollup -c",
26
27
  "test": "ava",
27
28
  "lint": "npx eslint src",
@@ -40,7 +41,6 @@
40
41
  "@rollup/plugin-node-resolve": "^15.0.1",
41
42
  "@rollup/plugin-typescript": "^11.0.0",
42
43
  "@types/ramda": "^0.28.23",
43
- "body-parser": "^1.20.2",
44
44
  "chokidar": "^3.5.3",
45
45
  "commander": "^10.0.0",
46
46
  "express": "^4.18.2",
@@ -58,14 +58,6 @@ export class Capability implements CapabilityCfg {
58
58
  logger.debug(cfg);
59
59
  }
60
60
 
61
- /**
62
- * The Register method is used to register a capability with the Pepr runtime. This method is
63
- * called in the order that the capabilities should be executed.
64
- *
65
- * @param callback the state register method to call, passing the capability as an argument
66
- */
67
- Register = (register: (capability: Capability) => void) => register(this);
68
-
69
61
  /**
70
62
  * The When method is used to register a capability action to be executed when a Kubernetes resource is
71
63
  * processed by Pepr. The action will be executed if the resource matches the specified kind and any
@@ -0,0 +1,92 @@
1
+ // SPDX-License-Identifier: Apache-2.0
2
+ // SPDX-FileCopyrightText: 2023-Present The Pepr Authors
3
+
4
+ import express from "express";
5
+ import fs from "fs";
6
+ import https from "https";
7
+ import { ModuleConfig } from "./types";
8
+ import { Capability } from "./capability";
9
+ import { processor } from "./processor";
10
+
11
+ // Load SSL certificate and key
12
+ const options = {
13
+ key: fs.readFileSync(process.env.SSL_KEY_PATH || "/etc/certs/tls.key"),
14
+ cert: fs.readFileSync(process.env.SSL_CERT_PATH || "/etc/certs/tls.crt"),
15
+ };
16
+
17
+ export class Controller {
18
+ private readonly app = express();
19
+
20
+ constructor(private readonly config: ModuleConfig, private readonly capabilities: Capability[]) {
21
+ // Middleware for logging requests
22
+ this.app.use(this.logger);
23
+
24
+ // Middleware for parsing JSON
25
+ this.app.use(express.json());
26
+
27
+ // Health check endpoint
28
+ this.app.get("/healthz", this.healthz);
29
+
30
+ // Mutate endpoint
31
+ this.app.post("/mutate", this.mutate);
32
+ }
33
+
34
+ /** Start the webhook server */
35
+ public startServer = (port: number) => {
36
+ // Create HTTPS server
37
+ https.createServer(options, this.app).listen(port, () => {
38
+ console.log(`Server listening on port ${port}`);
39
+ });
40
+ };
41
+
42
+ private logger = (req: express.Request, res: express.Response, next: express.NextFunction) => {
43
+ const startTime = Date.now();
44
+
45
+ res.on("finish", () => {
46
+ const now = new Date().toISOString();
47
+ const elapsedTime = Date.now() - startTime;
48
+ const message = `[${now}] ${req.method} ${req.originalUrl} - ${res.statusCode} - ${elapsedTime} ms\n`;
49
+
50
+ res.statusCode >= 400 ? console.error(message) : console.info(message);
51
+ });
52
+
53
+ next();
54
+ };
55
+
56
+ private healthz = (req: express.Request, res: express.Response) => {
57
+ try {
58
+ res.send("OK");
59
+ } catch (err) {
60
+ console.error(err);
61
+ res.status(500).send("Internal Server Error");
62
+ }
63
+ };
64
+
65
+ private mutate = (req: express.Request, res: express.Response) => {
66
+ try {
67
+ const name = req.body?.request?.name || "";
68
+ const namespace = req.body?.request?.namespace || "";
69
+ const gvk = req.body?.request?.kind || { group: "", version: "", kind: "" };
70
+
71
+ console.log(`Mutate request: ${gvk.group}/${gvk.version}/${gvk.kind}`);
72
+ name && console.log(` ${namespace}/${name}\n`);
73
+
74
+ // @todo: make this actually do something
75
+ const response = processor(this.config, this.capabilities, req.body.request);
76
+ console.debug(response);
77
+
78
+ // Send a no prob bob response
79
+ res.send({
80
+ apiVersion: "admission.k8s.io/v1",
81
+ kind: "AdmissionReview",
82
+ response: {
83
+ uid: req.body.request.uid,
84
+ allowed: true,
85
+ },
86
+ });
87
+ } catch (err) {
88
+ console.error(err);
89
+ res.status(500).send("Internal Server Error");
90
+ }
91
+ };
92
+ }
package/src/lib/filter.ts CHANGED
@@ -18,22 +18,19 @@ export function shouldSkipRequest(binding: Binding, req: Request) {
18
18
  const { metadata } = req.object;
19
19
 
20
20
  if (kind !== req.kind.kind) {
21
- logger.debug(`${req.kind.kind} does not match ${kind}`);
22
21
  return true;
23
22
  }
24
23
 
25
24
  if (group && group !== req.kind.group) {
26
- logger.debug(`${req.kind.group} does not match ${group}`);
27
25
  return true;
28
26
  }
29
27
 
30
28
  if (version && version !== req.kind.version) {
31
- logger.debug(`${req.kind.version} does not match ${version}`);
32
29
  return true;
33
30
  }
34
31
 
35
32
  if (namespaces.length && !namespaces.includes(req.namespace || "")) {
36
- logger.debug(`${req.namespace} is not in ${namespaces}`);
33
+ logger.debug("Namespace does not match");
37
34
  return true;
38
35
  }
39
36
 
package/src/lib/module.ts CHANGED
@@ -3,9 +3,7 @@
3
3
 
4
4
  import R from "ramda";
5
5
  import { Capability } from "./capability";
6
- import { GroupVersionKind, Request, Response } from "./k8s";
7
- import logger from "./logger";
8
- import { processor } from "./processor";
6
+ import { Controller } from "./controller";
9
7
  import { ModuleConfig } from "./types";
10
8
 
11
9
  const alwaysIgnore = {
@@ -19,39 +17,31 @@ export type PackageJSON = {
19
17
  };
20
18
 
21
19
  export class PeprModule {
22
- private _config: ModuleConfig;
23
- private _state: Capability[] = [];
24
- private _kinds: GroupVersionKind[] = [];
25
-
26
- get kinds(): GroupVersionKind[] {
27
- return this._kinds;
28
- }
29
-
30
- get UUID(): string {
31
- return this._config.uuid;
32
- }
20
+ private _controller: Controller;
33
21
 
34
22
  /**
35
23
  * Create a new Pepr runtime
36
24
  *
37
25
  * @param config The configuration for the Pepr runtime
38
26
  */
39
- constructor({ description, pepr }: PackageJSON) {
40
- pepr.description = description;
41
- this._config = R.mergeDeepWith(R.concat, pepr, alwaysIgnore);
42
- }
43
-
44
- Register = (capability: Capability) => {
45
- logger.info(`Registering capability ${capability.name}`);
27
+ constructor({ description, pepr }: PackageJSON, capabilities: Capability[] = [], deferStart = false) {
28
+ const config: ModuleConfig = R.mergeDeepWith(R.concat, pepr, alwaysIgnore);
29
+ config.description = description;
46
30
 
47
- // Add the kinds to the list of kinds (ignoring duplicates for now)
48
- this._kinds = capability.bindings.map(({ kind }) => kind);
31
+ this._controller = new Controller(config, capabilities);
49
32
 
50
- // Add the capability to the state
51
- this._state.push(capability);
52
- };
33
+ if (!deferStart) {
34
+ this.start();
35
+ }
36
+ }
53
37
 
54
- ProcessRequest = (req: Request): Response => {
55
- return processor(this._config, this._state, req);
56
- };
38
+ /**
39
+ * Start the Pepr runtime manually.
40
+ * Normally this is called automatically when the Pepr module is instantiated, but can be called manually if `deferStart` is set to `true` in the constructor.
41
+ *
42
+ * @param port
43
+ */
44
+ start(port = 3000) {
45
+ this._controller.startServer(port);
46
+ }
57
47
  }