pepr 0.1.26 → 0.1.28

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.
Files changed (73) hide show
  1. package/{index.ts → dist/index.d.ts} +0 -2
  2. package/dist/index.js +5 -0
  3. package/dist/package.json +76 -0
  4. package/dist/src/cli/banner.d.ts +1 -0
  5. package/dist/{pepr-cli.js → src/cli/banner.js} +1 -1251
  6. package/dist/src/cli/build.d.ts +7 -0
  7. package/dist/src/cli/build.js +95 -0
  8. package/dist/src/cli/capability.d.ts +2 -0
  9. package/dist/src/cli/capability.js +12 -0
  10. package/dist/src/cli/deploy.d.ts +2 -0
  11. package/dist/src/cli/deploy.js +49 -0
  12. package/dist/src/cli/dev.d.ts +2 -0
  13. package/dist/src/cli/dev.js +90 -0
  14. package/dist/src/cli/index.d.ts +1 -0
  15. package/dist/src/cli/index.js +28 -0
  16. package/dist/src/cli/init/index.d.ts +2 -0
  17. package/dist/src/cli/init/index.js +48 -0
  18. package/dist/src/cli/init/templates.d.ts +82 -0
  19. package/dist/src/cli/init/templates.js +224 -0
  20. package/dist/src/cli/init/utils.d.ts +20 -0
  21. package/dist/src/cli/init/utils.js +50 -0
  22. package/dist/src/cli/init/walkthrough.d.ts +7 -0
  23. package/dist/src/cli/init/walkthrough.js +76 -0
  24. package/dist/src/cli/root.d.ts +4 -0
  25. package/dist/src/cli/root.js +14 -0
  26. package/dist/src/cli/test.d.ts +2 -0
  27. package/dist/src/cli/test.js +45 -0
  28. package/dist/src/lib/capability.d.ts +26 -0
  29. package/dist/src/lib/capability.js +112 -0
  30. package/dist/src/lib/controller.d.ts +13 -0
  31. package/dist/src/lib/controller.js +77 -0
  32. package/dist/src/lib/filter.d.ts +10 -0
  33. package/dist/src/lib/filter.js +41 -0
  34. package/dist/src/lib/k8s/index.d.ts +4 -0
  35. package/{src/lib/k8s/index.ts → dist/src/lib/k8s/index.js} +0 -3
  36. package/dist/src/lib/k8s/kinds.d.ts +3 -0
  37. package/dist/src/lib/k8s/kinds.js +427 -0
  38. package/dist/src/lib/k8s/tls.d.ts +17 -0
  39. package/dist/src/lib/k8s/tls.js +67 -0
  40. package/dist/src/lib/k8s/types.d.ts +136 -0
  41. package/dist/src/lib/k8s/types.js +9 -0
  42. package/dist/src/lib/k8s/upstream.d.ts +1 -0
  43. package/dist/src/lib/k8s/upstream.js +3 -0
  44. package/dist/src/lib/k8s/webhook.d.ts +33 -0
  45. package/dist/src/lib/k8s/webhook.js +490 -0
  46. package/dist/src/lib/logger.d.ts +54 -0
  47. package/dist/{types-1709b44f.js → src/lib/logger.js} +3 -40
  48. package/dist/src/lib/module.d.ts +22 -0
  49. package/dist/src/lib/module.js +32 -0
  50. package/dist/src/lib/processor.d.ts +4 -0
  51. package/dist/src/lib/processor.js +66 -0
  52. package/dist/src/lib/request.d.ts +77 -0
  53. package/dist/src/lib/request.js +117 -0
  54. package/dist/src/lib/types.d.ts +187 -0
  55. package/dist/src/lib/types.js +31 -0
  56. package/package.json +8 -11
  57. package/tsconfig.build.json +4 -0
  58. package/CODEOWNERS +0 -6
  59. package/dist/pepr-core.js +0 -950
  60. package/src/lib/capability.ts +0 -150
  61. package/src/lib/controller.ts +0 -92
  62. package/src/lib/filter.ts +0 -52
  63. package/src/lib/k8s/kinds.ts +0 -470
  64. package/src/lib/k8s/tls.ts +0 -90
  65. package/src/lib/k8s/types.ts +0 -170
  66. package/src/lib/k8s/upstream.ts +0 -47
  67. package/src/lib/k8s/webhook.ts +0 -540
  68. package/src/lib/logger.ts +0 -131
  69. package/src/lib/module.ts +0 -47
  70. package/src/lib/processor.ts +0 -83
  71. package/src/lib/request.ts +0 -140
  72. package/src/lib/types.ts +0 -211
  73. package/tsconfig.json +0 -15
@@ -0,0 +1,50 @@
1
+ // SPDX-License-Identifier: Apache-2.0
2
+ // SPDX-FileCopyrightText: 2023-Present The Pepr Authors
3
+ import { promises as fs } from "fs";
4
+ /**
5
+ * Sanitize a user input name to be used as a pepr module directory name
6
+ *
7
+ * @param name the user input name
8
+ * @returns the sanitized name
9
+ */
10
+ export function sanitizeName(name) {
11
+ // Replace any characters outside of [^a-z0-9-] with "-"
12
+ let sanitized = name.toLowerCase().replace(/[^a-z0-9-]+/gi, "-");
13
+ // Remove any leading or trailing hyphens
14
+ sanitized = sanitized.replace(/^-+|-+$/g, "");
15
+ // Replace multiple hyphens with a single hyphen
16
+ sanitized = sanitized.replace(/--+/g, "-");
17
+ return sanitized;
18
+ }
19
+ /**
20
+ * Creates a directory and throws an error if it already exists
21
+ *
22
+ * @param dir - The directory to create
23
+ */
24
+ export async function createDir(dir) {
25
+ try {
26
+ await fs.mkdir(dir);
27
+ }
28
+ catch (err) {
29
+ // The directory already exists
30
+ if (err.code === "EEXIST") {
31
+ throw new Error(`Directory ${dir} already exists`);
32
+ }
33
+ else {
34
+ throw err;
35
+ }
36
+ }
37
+ }
38
+ /**
39
+ * Write data to a file on disk
40
+ * @param path - The path to the file
41
+ * @param data - The data to write
42
+ * @returns A promise that resolves when the file has been written
43
+ */
44
+ export function write(path, data) {
45
+ // If the data is not a string, stringify it
46
+ if (typeof data !== "string") {
47
+ data = JSON.stringify(data, null, 2);
48
+ }
49
+ return fs.writeFile(path, data);
50
+ }
@@ -0,0 +1,7 @@
1
+ import { Answers } from "prompts";
2
+ export type InitOptions = Answers<"name" | "description" | "errorBehavior">;
3
+ export declare function walkthrough(): Promise<InitOptions>;
4
+ export declare function confirm(dirName: string, packageJSON: {
5
+ path: string;
6
+ print: string;
7
+ }, peprTSPath: string): Promise<any>;
@@ -0,0 +1,76 @@
1
+ // SPDX-License-Identifier: Apache-2.0
2
+ // SPDX-FileCopyrightText: 2023-Present The Pepr Authors
3
+ import { promises as fs } from "fs";
4
+ import prompt from "prompts";
5
+ import { ErrorBehavior } from "../../lib/types";
6
+ import { gitIgnore, prettierRC, readme, tsConfig } from "./templates";
7
+ import { sanitizeName } from "./utils";
8
+ export function walkthrough() {
9
+ const askName = {
10
+ type: "text",
11
+ name: "name",
12
+ message: "Enter a name for the new Pepr module. This will create a new directory based on the name.\n",
13
+ validate: async (val) => {
14
+ try {
15
+ const name = sanitizeName(val);
16
+ await fs.access(name, fs.constants.F_OK);
17
+ return "A directory with this name already exists";
18
+ }
19
+ catch (e) {
20
+ return val.length > 2 || "The name must be at least 3 characters long";
21
+ }
22
+ },
23
+ };
24
+ const askDescription = {
25
+ type: "text",
26
+ name: "description",
27
+ message: "(Recommended) Enter a description for the new Pepr module.\n",
28
+ };
29
+ const askErrorBehavior = {
30
+ type: "select",
31
+ name: "errorBehavior",
32
+ validate: val => ErrorBehavior[val],
33
+ message: "How do you want Pepr to handle errors encountered during K8s operations?",
34
+ choices: [
35
+ {
36
+ title: "Ignore",
37
+ value: ErrorBehavior.ignore,
38
+ description: "Pepr will continue processing and generate an entry in the Pepr Controller log.",
39
+ selected: true,
40
+ },
41
+ {
42
+ title: "Log an audit event",
43
+ value: ErrorBehavior.audit,
44
+ description: "Pepr will continue processing and generate an entry in the Pepr Controller log as well as an audit event in the cluster.",
45
+ },
46
+ {
47
+ title: "Reject the operation",
48
+ value: ErrorBehavior.reject,
49
+ description: "Pepr will reject the operation and return an error to the client.",
50
+ },
51
+ ],
52
+ };
53
+ return prompt([askName, askDescription, askErrorBehavior]);
54
+ }
55
+ export async function confirm(dirName, packageJSON, peprTSPath) {
56
+ console.log(`
57
+ To be generated:
58
+
59
+ \x1b[1m${dirName}\x1b[0m
60
+ ├── \x1b[1m${gitIgnore.path}\x1b[0m
61
+ ├── \x1b[1m${prettierRC.path}\x1b[0m
62
+ ├── \x1b[1mcapabilties\x1b[0m
63
+ | └── \x1b[1mhello-pepr.ts\x1b[0m
64
+ ├── \x1b[1m${packageJSON.path}\x1b[0m
65
+ ${packageJSON.print.replace(/^/gm, " │ ")}
66
+ ├── \x1b[1m${peprTSPath}\x1b[0m
67
+ ├── \x1b[1m${readme.path}\x1b[0m
68
+ └── \x1b[1m${tsConfig.path}\x1b[0m
69
+ `);
70
+ const confirm = await prompt({
71
+ type: "confirm",
72
+ name: "confirm",
73
+ message: "Create the new Pepr module?",
74
+ });
75
+ return confirm.confirm;
76
+ }
@@ -0,0 +1,4 @@
1
+ import { Command } from "commander";
2
+ export declare class RootCmd extends Command {
3
+ createCommand(name: string): Command;
4
+ }
@@ -0,0 +1,14 @@
1
+ // SPDX-License-Identifier: Apache-2.0
2
+ // SPDX-FileCopyrightText: 2023-Present The Pepr Authors
3
+ import { Command } from "commander";
4
+ import Log from "../lib/logger";
5
+ export class RootCmd extends Command {
6
+ createCommand(name) {
7
+ const cmd = new Command(name);
8
+ cmd.option("-l, --log-level [level]", "Log level: debug, info, warn, error", "info");
9
+ cmd.hook("preAction", run => {
10
+ Log.SetLogLevel(run.opts().logLevel);
11
+ });
12
+ return cmd;
13
+ }
14
+ }
@@ -0,0 +1,2 @@
1
+ import { RootCmd } from "./root";
2
+ export default function (program: RootCmd): void;
@@ -0,0 +1,45 @@
1
+ // SPDX-License-Identifier: Apache-2.0
2
+ // SPDX-FileCopyrightText: 2023-Present The Pepr Authors
3
+ import { exec as execCallback } from "child_process";
4
+ import { watch } from "chokidar";
5
+ import { resolve } from "path";
6
+ import { promisify } from "util";
7
+ import Log from "../lib/logger";
8
+ import { buildModule } from "./build";
9
+ const exec = promisify(execCallback);
10
+ export default function (program) {
11
+ program
12
+ .command("test")
13
+ .description("Test a Pepr Module locally")
14
+ .option("-d, --dir [directory]", "Pepr module directory", ".")
15
+ .option("-w, --watch", "Watch for changes and re-run the test")
16
+ .action(async (opts) => {
17
+ Log.info("Test Module");
18
+ await buildAndTest(opts.dir);
19
+ if (opts.watch) {
20
+ const moduleFiles = resolve(opts.dir, "**", "*.ts");
21
+ const watcher = watch(moduleFiles);
22
+ watcher.on("ready", () => {
23
+ Log.info(`Watching for changes in ${moduleFiles}`);
24
+ watcher.on("all", async (event, path) => {
25
+ Log.debug({ event, path }, "File changed");
26
+ await buildAndTest(opts.dir);
27
+ });
28
+ });
29
+ }
30
+ });
31
+ }
32
+ async function buildAndTest(dir) {
33
+ const { path } = await buildModule(dir);
34
+ Log.info(`Module built successfully at ${path}`);
35
+ try {
36
+ const { stdout, stderr } = await exec(`node ${path}`);
37
+ console.log(stdout);
38
+ console.log(stderr);
39
+ }
40
+ catch (e) {
41
+ Log.debug(e);
42
+ Log.error(`Error running module: ${e}`);
43
+ process.exit(1);
44
+ }
45
+ }
@@ -0,0 +1,26 @@
1
+ import { Binding, CapabilityCfg, GenericClass, HookPhase, WhenSelector } from "./types";
2
+ /**
3
+ * A capability is a unit of functionality that can be registered with the Pepr runtime.
4
+ */
5
+ export declare class Capability implements CapabilityCfg {
6
+ private _name;
7
+ private _description;
8
+ private _namespaces?;
9
+ private _mutateOrValidate;
10
+ private _bindings;
11
+ get bindings(): Binding[];
12
+ get name(): string;
13
+ get description(): string;
14
+ get namespaces(): string[];
15
+ get mutateOrValidate(): HookPhase;
16
+ constructor(cfg: CapabilityCfg);
17
+ /**
18
+ * The When method is used to register a capability action to be executed when a Kubernetes resource is
19
+ * processed by Pepr. The action will be executed if the resource matches the specified kind and any
20
+ * filters that are applied.
21
+ *
22
+ * @param model if using a custom KubernetesObject not available in `a.*`, specify the GroupVersionKind
23
+ * @returns
24
+ */
25
+ When: <T extends GenericClass>(model: T) => WhenSelector<T>;
26
+ }
@@ -0,0 +1,112 @@
1
+ // SPDX-License-Identifier: Apache-2.0
2
+ // SPDX-FileCopyrightText: 2023-Present The Pepr Authors
3
+ import { modelToGroupVersionKind } from "./k8s";
4
+ import logger from "./logger";
5
+ import { Event, HookPhase, } from "./types";
6
+ /**
7
+ * A capability is a unit of functionality that can be registered with the Pepr runtime.
8
+ */
9
+ export class Capability {
10
+ get bindings() {
11
+ return this._bindings;
12
+ }
13
+ get name() {
14
+ return this._name;
15
+ }
16
+ get description() {
17
+ return this._description;
18
+ }
19
+ get namespaces() {
20
+ return this._namespaces || [];
21
+ }
22
+ get mutateOrValidate() {
23
+ return this._mutateOrValidate;
24
+ }
25
+ constructor(cfg) {
26
+ // Currently everything is considered a mutation
27
+ this._mutateOrValidate = HookPhase.mutate;
28
+ this._bindings = [];
29
+ /**
30
+ * The When method is used to register a capability action to be executed when a Kubernetes resource is
31
+ * processed by Pepr. The action will be executed if the resource matches the specified kind and any
32
+ * filters that are applied.
33
+ *
34
+ * @param model if using a custom KubernetesObject not available in `a.*`, specify the GroupVersionKind
35
+ * @returns
36
+ */
37
+ this.When = (model) => {
38
+ const binding = {
39
+ // If the kind is not specified, use the default KubernetesObject
40
+ kind: modelToGroupVersionKind(model.name),
41
+ filters: {
42
+ name: "",
43
+ namespaces: [],
44
+ labels: {},
45
+ annotations: {},
46
+ },
47
+ callback: () => null,
48
+ };
49
+ const prefix = `${this._name}: ${model.name}`;
50
+ logger.info(`Binding created`, prefix);
51
+ const Then = (cb) => {
52
+ logger.info(`Binding action created`, prefix);
53
+ logger.debug(cb.toString(), prefix);
54
+ // Push the binding to the list of bindings for this capability as a new BindingAction
55
+ // with the callback function to preserve
56
+ this._bindings.push({
57
+ ...binding,
58
+ callback: cb,
59
+ });
60
+ // Now only allow adding actions to the same binding
61
+ return { Then };
62
+ };
63
+ const ThenSet = (merge) => {
64
+ // Add the new action to the binding
65
+ Then(req => req.Merge(merge));
66
+ return { Then };
67
+ };
68
+ function InNamespace(...namespaces) {
69
+ logger.debug(`Add namespaces filter ${namespaces}`, prefix);
70
+ binding.filters.namespaces.push(...namespaces);
71
+ return { WithLabel, WithAnnotation, WithName, Then, ThenSet };
72
+ }
73
+ function WithName(name) {
74
+ logger.debug(`Add name filter ${name}`, prefix);
75
+ binding.filters.name = name;
76
+ return { WithLabel, WithAnnotation, Then, ThenSet };
77
+ }
78
+ function WithLabel(key, value = "") {
79
+ logger.debug(`Add label filter ${key}=${value}`, prefix);
80
+ binding.filters.labels[key] = value;
81
+ return { WithLabel, WithAnnotation, Then, ThenSet };
82
+ }
83
+ const WithAnnotation = (key, value = "") => {
84
+ logger.debug(`Add annotation filter ${key}=${value}`, prefix);
85
+ binding.filters.annotations[key] = value;
86
+ return { WithLabel, WithAnnotation, Then, ThenSet };
87
+ };
88
+ const bindEvent = (event) => {
89
+ binding.event = event;
90
+ return {
91
+ InNamespace,
92
+ Then,
93
+ ThenSet,
94
+ WithAnnotation,
95
+ WithLabel,
96
+ WithName,
97
+ };
98
+ };
99
+ return {
100
+ IsCreatedOrUpdated: () => bindEvent(Event.CreateOrUpdate),
101
+ IsCreated: () => bindEvent(Event.Create),
102
+ IsUpdated: () => bindEvent(Event.Update),
103
+ IsDeleted: () => bindEvent(Event.Delete),
104
+ };
105
+ };
106
+ this._name = cfg.name;
107
+ this._description = cfg.description;
108
+ this._namespaces = cfg.namespaces;
109
+ logger.info(`Capability ${this._name} registered`);
110
+ logger.debug(cfg);
111
+ }
112
+ }
@@ -0,0 +1,13 @@
1
+ import { ModuleConfig } from "./types";
2
+ import { Capability } from "./capability";
3
+ export declare class Controller {
4
+ private readonly config;
5
+ private readonly capabilities;
6
+ private readonly app;
7
+ constructor(config: ModuleConfig, capabilities: Capability[]);
8
+ /** Start the webhook server */
9
+ startServer: (port: number) => void;
10
+ private logger;
11
+ private healthz;
12
+ private mutate;
13
+ }
@@ -0,0 +1,77 @@
1
+ // SPDX-License-Identifier: Apache-2.0
2
+ // SPDX-FileCopyrightText: 2023-Present The Pepr Authors
3
+ import express from "express";
4
+ import fs from "fs";
5
+ import https from "https";
6
+ import { processor } from "./processor";
7
+ // Load SSL certificate and key
8
+ const options = {
9
+ key: fs.readFileSync(process.env.SSL_KEY_PATH || "/etc/certs/tls.key"),
10
+ cert: fs.readFileSync(process.env.SSL_CERT_PATH || "/etc/certs/tls.crt"),
11
+ };
12
+ export class Controller {
13
+ constructor(config, capabilities) {
14
+ this.config = config;
15
+ this.capabilities = capabilities;
16
+ this.app = express();
17
+ /** Start the webhook server */
18
+ this.startServer = (port) => {
19
+ // Create HTTPS server
20
+ https.createServer(options, this.app).listen(port, () => {
21
+ console.log(`Server listening on port ${port}`);
22
+ });
23
+ };
24
+ this.logger = (req, res, next) => {
25
+ const startTime = Date.now();
26
+ res.on("finish", () => {
27
+ const now = new Date().toISOString();
28
+ const elapsedTime = Date.now() - startTime;
29
+ const message = `[${now}] ${req.method} ${req.originalUrl} - ${res.statusCode} - ${elapsedTime} ms\n`;
30
+ res.statusCode >= 400 ? console.error(message) : console.info(message);
31
+ });
32
+ next();
33
+ };
34
+ this.healthz = (req, res) => {
35
+ try {
36
+ res.send("OK");
37
+ }
38
+ catch (err) {
39
+ console.error(err);
40
+ res.status(500).send("Internal Server Error");
41
+ }
42
+ };
43
+ this.mutate = (req, res) => {
44
+ try {
45
+ const name = req.body?.request?.name || "";
46
+ const namespace = req.body?.request?.namespace || "";
47
+ const gvk = req.body?.request?.kind || { group: "", version: "", kind: "" };
48
+ console.log(`Mutate request: ${gvk.group}/${gvk.version}/${gvk.kind}`);
49
+ name && console.log(` ${namespace}/${name}\n`);
50
+ // @todo: make this actually do something
51
+ const response = processor(this.config, this.capabilities, req.body.request);
52
+ console.debug(response);
53
+ // Send a no prob bob response
54
+ res.send({
55
+ apiVersion: "admission.k8s.io/v1",
56
+ kind: "AdmissionReview",
57
+ response: {
58
+ uid: req.body.request.uid,
59
+ allowed: true,
60
+ },
61
+ });
62
+ }
63
+ catch (err) {
64
+ console.error(err);
65
+ res.status(500).send("Internal Server Error");
66
+ }
67
+ };
68
+ // Middleware for logging requests
69
+ this.app.use(this.logger);
70
+ // Middleware for parsing JSON
71
+ this.app.use(express.json());
72
+ // Health check endpoint
73
+ this.app.get("/healthz", this.healthz);
74
+ // Mutate endpoint
75
+ this.app.post("/mutate", this.mutate);
76
+ }
77
+ }
@@ -0,0 +1,10 @@
1
+ import { Request } from "./k8s";
2
+ import { Binding } from "./types";
3
+ /**
4
+ * shouldSkipRequest determines if a request should be skipped based on the binding filters.
5
+ *
6
+ * @param binding the capability action binding
7
+ * @param req the incoming request
8
+ * @returns
9
+ */
10
+ export declare function shouldSkipRequest(binding: Binding, req: Request): boolean;
@@ -0,0 +1,41 @@
1
+ // SPDX-License-Identifier: Apache-2.0
2
+ // SPDX-FileCopyrightText: 2023-Present The Pepr Authors
3
+ import logger from "./logger";
4
+ /**
5
+ * shouldSkipRequest determines if a request should be skipped based on the binding filters.
6
+ *
7
+ * @param binding the capability action binding
8
+ * @param req the incoming request
9
+ * @returns
10
+ */
11
+ export function shouldSkipRequest(binding, req) {
12
+ const { group, kind, version } = binding.kind;
13
+ const { namespaces, labels, annotations } = binding.filters;
14
+ const { metadata } = req.object;
15
+ if (kind !== req.kind.kind) {
16
+ return true;
17
+ }
18
+ if (group && group !== req.kind.group) {
19
+ return true;
20
+ }
21
+ if (version && version !== req.kind.version) {
22
+ return true;
23
+ }
24
+ if (namespaces.length && !namespaces.includes(req.namespace || "")) {
25
+ logger.debug("Namespace does not match");
26
+ return true;
27
+ }
28
+ for (const [key, value] of Object.entries(labels)) {
29
+ if (metadata?.labels?.[key] !== value) {
30
+ logger.debug(`${metadata?.labels?.[key]} does not match ${value}`);
31
+ return true;
32
+ }
33
+ }
34
+ for (const [key, value] of Object.entries(annotations)) {
35
+ if (metadata?.annotations?.[key] !== value) {
36
+ logger.debug(`${metadata?.annotations?.[key]} does not match ${value}`);
37
+ return true;
38
+ }
39
+ }
40
+ return false;
41
+ }
@@ -0,0 +1,4 @@
1
+ import * as kind from "./upstream";
2
+ export { kind as a };
3
+ export { modelToGroupVersionKind, gvkMap } from "./kinds";
4
+ export * from "./types";
@@ -1,10 +1,7 @@
1
1
  // SPDX-License-Identifier: Apache-2.0
2
2
  // SPDX-FileCopyrightText: 2023-Present The Pepr Authors
3
-
4
3
  // Export kinds as a single object
5
4
  import * as kind from "./upstream";
6
5
  export { kind as a };
7
-
8
6
  export { modelToGroupVersionKind, gvkMap } from "./kinds";
9
-
10
7
  export * from "./types";
@@ -0,0 +1,3 @@
1
+ import { GroupVersionKind } from "./types";
2
+ export declare const gvkMap: Record<string, GroupVersionKind>;
3
+ export declare function modelToGroupVersionKind(key: string): GroupVersionKind;