pepr 0.12.1 → 0.13.0

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 (90) hide show
  1. package/CODE_OF_CONDUCT.md +83 -0
  2. package/CONTRIBUTING.md +70 -0
  3. package/README.md +28 -30
  4. package/dist/cli.js +644 -679
  5. package/dist/controller.js +13 -81
  6. package/dist/lib/assets/deploy.d.ts +3 -0
  7. package/dist/lib/assets/deploy.d.ts.map +1 -0
  8. package/dist/lib/assets/index.d.ts +18 -0
  9. package/dist/lib/assets/index.d.ts.map +1 -0
  10. package/dist/lib/assets/loader.d.ts +14 -0
  11. package/dist/lib/assets/loader.d.ts.map +1 -0
  12. package/dist/lib/assets/networking.d.ts +6 -0
  13. package/dist/lib/assets/networking.d.ts.map +1 -0
  14. package/dist/lib/assets/pods.d.ts +8 -0
  15. package/dist/lib/assets/pods.d.ts.map +1 -0
  16. package/dist/lib/assets/rbac.d.ts +11 -0
  17. package/dist/lib/assets/rbac.d.ts.map +1 -0
  18. package/dist/lib/assets/webhooks.d.ts +6 -0
  19. package/dist/lib/assets/webhooks.d.ts.map +1 -0
  20. package/dist/lib/assets/yaml.d.ts +4 -0
  21. package/dist/lib/assets/yaml.d.ts.map +1 -0
  22. package/dist/lib/capability.d.ts +1 -3
  23. package/dist/lib/capability.d.ts.map +1 -1
  24. package/dist/lib/controller.d.ts +45 -10
  25. package/dist/lib/controller.d.ts.map +1 -1
  26. package/dist/lib/filter.d.ts +1 -1
  27. package/dist/lib/filter.d.ts.map +1 -1
  28. package/dist/lib/k8s/index.d.ts +2 -1
  29. package/dist/lib/k8s/index.d.ts.map +1 -1
  30. package/dist/lib/k8s/kinds.d.ts.map +1 -1
  31. package/dist/lib/k8s/types.d.ts +13 -13
  32. package/dist/lib/k8s/types.d.ts.map +1 -1
  33. package/dist/lib/k8s/upstream.d.ts +2 -2
  34. package/dist/lib/k8s/upstream.d.ts.map +1 -1
  35. package/dist/lib/logger.d.ts +8 -54
  36. package/dist/lib/logger.d.ts.map +1 -1
  37. package/dist/lib/metrics.d.ts +11 -4
  38. package/dist/lib/metrics.d.ts.map +1 -1
  39. package/dist/lib/module.d.ts +2 -2
  40. package/dist/lib/module.d.ts.map +1 -1
  41. package/dist/lib/mutate-processor.d.ts +5 -0
  42. package/dist/lib/mutate-processor.d.ts.map +1 -0
  43. package/dist/lib/{request.d.ts → mutate-request.d.ts} +5 -5
  44. package/dist/lib/mutate-request.d.ts.map +1 -0
  45. package/dist/lib/types.d.ts +45 -46
  46. package/dist/lib/types.d.ts.map +1 -1
  47. package/dist/lib/utils.d.ts.map +1 -1
  48. package/dist/lib/validate-processor.d.ts +4 -0
  49. package/dist/lib/validate-processor.d.ts.map +1 -0
  50. package/dist/lib/validate-request.d.ts +54 -0
  51. package/dist/lib/validate-request.d.ts.map +1 -0
  52. package/dist/lib.d.ts +3 -2
  53. package/dist/lib.d.ts.map +1 -1
  54. package/dist/lib.js +510 -306
  55. package/dist/lib.js.map +4 -4
  56. package/package.json +15 -12
  57. package/src/cli.ts +2 -11
  58. package/src/lib/assets/deploy.ts +179 -0
  59. package/src/lib/assets/index.ts +46 -0
  60. package/src/lib/assets/loader.ts +49 -0
  61. package/src/lib/assets/networking.ts +58 -0
  62. package/src/lib/assets/pods.ts +148 -0
  63. package/src/lib/assets/rbac.ts +57 -0
  64. package/src/lib/assets/webhooks.ts +139 -0
  65. package/src/lib/assets/yaml.ts +75 -0
  66. package/src/lib/capability.ts +54 -44
  67. package/src/lib/controller.ts +171 -89
  68. package/src/lib/fetch.ts +1 -1
  69. package/src/lib/filter.ts +1 -3
  70. package/src/lib/k8s/index.ts +4 -1
  71. package/src/lib/k8s/kinds.ts +40 -0
  72. package/src/lib/k8s/types.ts +16 -14
  73. package/src/lib/k8s/upstream.ts +5 -1
  74. package/src/lib/logger.ts +14 -125
  75. package/src/lib/metrics.ts +67 -23
  76. package/src/lib/module.ts +13 -11
  77. package/src/lib/{processor.ts → mutate-processor.ts} +37 -28
  78. package/src/lib/{request.ts → mutate-request.ts} +4 -4
  79. package/src/lib/types.ts +51 -51
  80. package/src/lib/utils.ts +9 -7
  81. package/src/lib/validate-processor.ts +68 -0
  82. package/src/lib/validate-request.ts +94 -0
  83. package/src/lib.ts +4 -2
  84. package/src/runtime/controller.ts +1 -1
  85. package/dist/lib/k8s/webhook.d.ts +0 -37
  86. package/dist/lib/k8s/webhook.d.ts.map +0 -1
  87. package/dist/lib/processor.d.ts +0 -5
  88. package/dist/lib/processor.d.ts.map +0 -1
  89. package/dist/lib/request.d.ts.map +0 -1
  90. package/src/lib/k8s/webhook.ts +0 -643
@@ -0,0 +1,179 @@
1
+ // SPDX-License-Identifier: Apache-2.0
2
+ // SPDX-FileCopyrightText: 2023-Present The Pepr Authors
3
+
4
+ import {
5
+ AdmissionregistrationV1Api as AdmissionRegV1API,
6
+ AppsV1Api,
7
+ CoreV1Api,
8
+ HttpError,
9
+ KubeConfig,
10
+ RbacAuthorizationV1Api,
11
+ } from "@kubernetes/client-node";
12
+ import crypto from "crypto";
13
+ import { promises as fs } from "fs";
14
+
15
+ import { Assets } from ".";
16
+ import Log from "../logger";
17
+ import { apiTokenSecret, service, tlsSecret } from "./networking";
18
+ import { deployment, moduleSecret, namespace } from "./pods";
19
+ import { clusterRole, clusterRoleBinding, serviceAccount } from "./rbac";
20
+ import { webhookConfig } from "./webhooks";
21
+
22
+ export async function deploy(assets: Assets, webhookTimeout?: number) {
23
+ Log.info("Establishing connection to Kubernetes");
24
+
25
+ const peprNS = "pepr-system";
26
+ const { name, host, path } = assets;
27
+
28
+ // Deploy the resources using the k8s API
29
+ const kubeConfig = new KubeConfig();
30
+ kubeConfig.loadFromDefault();
31
+
32
+ const coreV1Api = kubeConfig.makeApiClient(CoreV1Api);
33
+ const admissionApi = kubeConfig.makeApiClient(AdmissionRegV1API);
34
+
35
+ try {
36
+ Log.info("Checking for namespace");
37
+ await coreV1Api.readNamespace(peprNS);
38
+ } catch (e) {
39
+ Log.debug(e instanceof HttpError ? e.body : e);
40
+ Log.info("Creating namespace");
41
+ await coreV1Api.createNamespace(namespace);
42
+ }
43
+
44
+ // Create the mutating webhook configuration if it is needed
45
+ const mutateWebhook = await webhookConfig(assets, "mutate", webhookTimeout);
46
+ if (mutateWebhook) {
47
+ try {
48
+ Log.info("Creating mutating webhook");
49
+ await admissionApi.createMutatingWebhookConfiguration(mutateWebhook);
50
+ } catch (e) {
51
+ Log.debug(e instanceof HttpError ? e.body : e);
52
+ Log.info("Removing and re-creating mutating webhook");
53
+ await admissionApi.deleteMutatingWebhookConfiguration(mutateWebhook.metadata?.name ?? "");
54
+ await admissionApi.createMutatingWebhookConfiguration(mutateWebhook);
55
+ }
56
+ }
57
+
58
+ // Create the validating webhook configuration if it is needed
59
+ const validateWebhook = await webhookConfig(assets, "validate", webhookTimeout);
60
+ if (validateWebhook) {
61
+ try {
62
+ Log.info("Creating validating webhook");
63
+ await admissionApi.createValidatingWebhookConfiguration(validateWebhook);
64
+ } catch (e) {
65
+ Log.debug(e instanceof HttpError ? e.body : e);
66
+ Log.info("Removing and re-creating validating webhook");
67
+ await admissionApi.deleteValidatingWebhookConfiguration(validateWebhook.metadata?.name ?? "");
68
+ await admissionApi.createValidatingWebhookConfiguration(validateWebhook);
69
+ }
70
+ }
71
+
72
+ // If a host is specified, we don't need to deploy the rest of the resources
73
+ if (host) {
74
+ return;
75
+ }
76
+
77
+ if (!path) {
78
+ throw new Error("No code provided");
79
+ }
80
+
81
+ const code = await fs.readFile(path);
82
+
83
+ const hash = crypto.createHash("sha256").update(code).digest("hex");
84
+
85
+ const appsApi = kubeConfig.makeApiClient(AppsV1Api);
86
+ const rbacApi = kubeConfig.makeApiClient(RbacAuthorizationV1Api);
87
+
88
+ const crb = clusterRoleBinding(name);
89
+ try {
90
+ Log.info("Creating cluster role binding");
91
+ await rbacApi.createClusterRoleBinding(crb);
92
+ } catch (e) {
93
+ Log.debug(e instanceof HttpError ? e.body : e);
94
+ Log.info("Removing and re-creating cluster role binding");
95
+ await rbacApi.deleteClusterRoleBinding(crb.metadata?.name ?? "");
96
+ await rbacApi.createClusterRoleBinding(crb);
97
+ }
98
+
99
+ const cr = clusterRole(name);
100
+ try {
101
+ Log.info("Creating cluster role");
102
+ await rbacApi.createClusterRole(cr);
103
+ } catch (e) {
104
+ Log.debug(e instanceof HttpError ? e.body : e);
105
+ Log.info("Removing and re-creating the cluster role");
106
+ try {
107
+ await rbacApi.deleteClusterRole(cr.metadata?.name ?? "");
108
+ await rbacApi.createClusterRole(cr);
109
+ } catch (e) {
110
+ Log.debug(e instanceof HttpError ? e.body : e);
111
+ }
112
+ }
113
+
114
+ const sa = serviceAccount(name);
115
+ try {
116
+ Log.info("Creating service account");
117
+ await coreV1Api.createNamespacedServiceAccount(peprNS, sa);
118
+ } catch (e) {
119
+ Log.debug(e instanceof HttpError ? e.body : e);
120
+ Log.info("Removing and re-creating service account");
121
+ await coreV1Api.deleteNamespacedServiceAccount(sa.metadata?.name ?? "", peprNS);
122
+ await coreV1Api.createNamespacedServiceAccount(peprNS, sa);
123
+ }
124
+
125
+ const mod = moduleSecret(name, code, hash);
126
+ try {
127
+ Log.info("Creating module secret");
128
+ await coreV1Api.createNamespacedSecret(peprNS, mod);
129
+ } catch (e) {
130
+ Log.debug(e instanceof HttpError ? e.body : e);
131
+ Log.info("Removing and re-creating module secret");
132
+ await coreV1Api.deleteNamespacedSecret(mod.metadata?.name ?? "", peprNS);
133
+ await coreV1Api.createNamespacedSecret(peprNS, mod);
134
+ }
135
+
136
+ const svc = service(name);
137
+ try {
138
+ Log.info("Creating service");
139
+ await coreV1Api.createNamespacedService(peprNS, svc);
140
+ } catch (e) {
141
+ Log.debug(e instanceof HttpError ? e.body : e);
142
+ Log.info("Removing and re-creating service");
143
+ await coreV1Api.deleteNamespacedService(svc.metadata?.name ?? "", peprNS);
144
+ await coreV1Api.createNamespacedService(peprNS, svc);
145
+ }
146
+
147
+ const tls = tlsSecret(name, assets.tls);
148
+ try {
149
+ Log.info("Creating TLS secret");
150
+ await coreV1Api.createNamespacedSecret(peprNS, tls);
151
+ } catch (e) {
152
+ Log.debug(e instanceof HttpError ? e.body : e);
153
+ Log.info("Removing and re-creating TLS secret");
154
+ await coreV1Api.deleteNamespacedSecret(tls.metadata?.name ?? "", peprNS);
155
+ await coreV1Api.createNamespacedSecret(peprNS, tls);
156
+ }
157
+
158
+ const apiToken = apiTokenSecret(name, assets.apiToken);
159
+ try {
160
+ Log.info("Creating API token secret");
161
+ await coreV1Api.createNamespacedSecret(peprNS, apiToken);
162
+ } catch (e) {
163
+ Log.debug(e instanceof HttpError ? e.body : e);
164
+ Log.info("Removing and re-creating API token secret");
165
+ await coreV1Api.deleteNamespacedSecret(apiToken.metadata?.name ?? "", peprNS);
166
+ await coreV1Api.createNamespacedSecret(peprNS, apiToken);
167
+ }
168
+
169
+ const dep = deployment(assets, hash);
170
+ try {
171
+ Log.info("Creating deployment");
172
+ await appsApi.createNamespacedDeployment(peprNS, dep);
173
+ } catch (e) {
174
+ Log.debug(e instanceof HttpError ? e.body : e);
175
+ Log.info("Removing and re-creating deployment");
176
+ await appsApi.deleteNamespacedDeployment(dep.metadata?.name ?? "", peprNS);
177
+ await appsApi.createNamespacedDeployment(peprNS, dep);
178
+ }
179
+ }
@@ -0,0 +1,46 @@
1
+ // SPDX-License-Identifier: Apache-2.0
2
+ // SPDX-FileCopyrightText: 2023-Present The Pepr Authors
3
+
4
+ import crypto from "crypto";
5
+
6
+ import { TLSOut, genTLS } from "../k8s/tls";
7
+ import { ModuleConfig } from "../types";
8
+ import { deploy } from "./deploy";
9
+ import { ModuleCapabilities, loadCapabilities } from "./loader";
10
+ import { allYaml, zarfYaml } from "./yaml";
11
+
12
+ export class Assets {
13
+ readonly name: string;
14
+ readonly tls: TLSOut;
15
+ readonly apiToken: string;
16
+ capabilities!: ModuleCapabilities[];
17
+ image: string;
18
+
19
+ constructor(
20
+ readonly config: ModuleConfig,
21
+ readonly path: string,
22
+ readonly host?: string,
23
+ ) {
24
+ this.name = `pepr-${config.uuid}`;
25
+
26
+ this.image = `ghcr.io/defenseunicorns/pepr/controller:v${config.peprVersion}`;
27
+
28
+ // Generate the ephemeral tls things
29
+ this.tls = genTLS(this.host || `${this.name}.pepr-system.svc`);
30
+
31
+ // Generate the api token for the controller / webhook
32
+ this.apiToken = crypto.randomBytes(32).toString("hex");
33
+ }
34
+
35
+ deploy = async (webhookTimeout?: number) => {
36
+ this.capabilities = await loadCapabilities(this.path);
37
+ await deploy(this, webhookTimeout);
38
+ };
39
+
40
+ zarfYaml = (path: string) => zarfYaml(this, path);
41
+
42
+ allYaml = async () => {
43
+ this.capabilities = await loadCapabilities(this.path);
44
+ return allYaml(this);
45
+ };
46
+ }
@@ -0,0 +1,49 @@
1
+ // SPDX-License-Identifier: Apache-2.0
2
+ // SPDX-FileCopyrightText: 2023-Present The Pepr Authors
3
+
4
+ import { fork } from "child_process";
5
+
6
+ import { Binding } from "../types";
7
+
8
+ // We are receiving javascript so the private fields are now public
9
+ export interface ModuleCapabilities {
10
+ _name: string;
11
+ _description: string;
12
+ _namespaces: string[];
13
+ _bindings: Binding[];
14
+ }
15
+
16
+ /**
17
+ * Read the capabilities from the module by running it in build mode
18
+ * @param path
19
+ * @returns
20
+ */
21
+ export function loadCapabilities(path: string): Promise<ModuleCapabilities[]> {
22
+ return new Promise((resolve, reject) => {
23
+ // Fork is needed with the PEPR_MODE env var to ensure the module is loaded in build mode and will send back the capabilities
24
+ const program = fork(path, {
25
+ env: {
26
+ ...process.env,
27
+ LOG_LEVEL: "warn",
28
+ PEPR_MODE: "build",
29
+ },
30
+ });
31
+
32
+ // Wait for the module to send back the capabilities
33
+ program.on("message", message => {
34
+ // Cast the message to the ModuleCapabilities type
35
+ const capabilities = message.valueOf() as ModuleCapabilities[];
36
+
37
+ // Iterate through the capabilities and generate the rules
38
+ for (const capability of capabilities) {
39
+ console.info(`Registered Pepr Capability "${capability._name}"`);
40
+ }
41
+
42
+ resolve(capabilities);
43
+ });
44
+
45
+ program.on("error", error => {
46
+ reject(error);
47
+ });
48
+ });
49
+ }
@@ -0,0 +1,58 @@
1
+ // SPDX-License-Identifier: Apache-2.0
2
+ // SPDX-FileCopyrightText: 2023-Present The Pepr Authors
3
+
4
+ import { TLSOut } from "../k8s/tls";
5
+ import { Secret, Service } from "../k8s/upstream";
6
+
7
+ export function apiTokenSecret(name: string, apiToken: string): Secret {
8
+ return {
9
+ apiVersion: "v1",
10
+ kind: "Secret",
11
+ metadata: {
12
+ name: `${name}-api-token`,
13
+ namespace: "pepr-system",
14
+ },
15
+ type: "Opaque",
16
+ data: {
17
+ value: Buffer.from(apiToken).toString("base64"),
18
+ },
19
+ };
20
+ }
21
+
22
+ export function tlsSecret(name: string, tls: TLSOut): Secret {
23
+ return {
24
+ apiVersion: "v1",
25
+ kind: "Secret",
26
+ metadata: {
27
+ name: `${name}-tls`,
28
+ namespace: "pepr-system",
29
+ },
30
+ type: "kubernetes.io/tls",
31
+ data: {
32
+ "tls.crt": tls.crt,
33
+ "tls.key": tls.key,
34
+ },
35
+ };
36
+ }
37
+
38
+ export function service(name: string): Service {
39
+ return {
40
+ apiVersion: "v1",
41
+ kind: "Service",
42
+ metadata: {
43
+ name,
44
+ namespace: "pepr-system",
45
+ },
46
+ spec: {
47
+ selector: {
48
+ app: name,
49
+ },
50
+ ports: [
51
+ {
52
+ port: 443,
53
+ targetPort: 3000,
54
+ },
55
+ ],
56
+ },
57
+ };
58
+ }
@@ -0,0 +1,148 @@
1
+ // SPDX-License-Identifier: Apache-2.0
2
+ // SPDX-FileCopyrightText: 2023-Present The Pepr Authors
3
+
4
+ import { gzipSync } from "zlib";
5
+
6
+ import { Assets } from ".";
7
+ import { Deployment, Namespace, Secret } from "../k8s/upstream";
8
+
9
+ /** Generate the pepr-system namespace */
10
+ export const namespace: Namespace = {
11
+ apiVersion: "v1",
12
+ kind: "Namespace",
13
+ metadata: { name: "pepr-system" },
14
+ };
15
+
16
+ export function deployment(assets: Assets, hash: string): Deployment {
17
+ const { name, image } = assets;
18
+ const app = name;
19
+
20
+ return {
21
+ apiVersion: "apps/v1",
22
+ kind: "Deployment",
23
+ metadata: {
24
+ name,
25
+ namespace: "pepr-system",
26
+ labels: {
27
+ app,
28
+ },
29
+ },
30
+ spec: {
31
+ replicas: 2,
32
+ selector: {
33
+ matchLabels: {
34
+ app,
35
+ },
36
+ },
37
+ template: {
38
+ metadata: {
39
+ labels: {
40
+ app,
41
+ },
42
+ },
43
+ spec: {
44
+ priorityClassName: "system-node-critical",
45
+ serviceAccountName: name,
46
+ containers: [
47
+ {
48
+ name: "server",
49
+ image,
50
+ imagePullPolicy: "IfNotPresent",
51
+ command: ["node", "/app/node_modules/pepr/dist/controller.js", hash],
52
+ readinessProbe: {
53
+ httpGet: {
54
+ path: "/healthz",
55
+ port: 3000,
56
+ scheme: "HTTPS",
57
+ },
58
+ },
59
+ livenessProbe: {
60
+ httpGet: {
61
+ path: "/healthz",
62
+ port: 3000,
63
+ scheme: "HTTPS",
64
+ },
65
+ },
66
+ ports: [
67
+ {
68
+ containerPort: 3000,
69
+ },
70
+ ],
71
+ env: [
72
+ {
73
+ name: "PEPR_PRETTY_LOG",
74
+ value: "false",
75
+ },
76
+ ],
77
+ resources: {
78
+ requests: {
79
+ memory: "64Mi",
80
+ cpu: "100m",
81
+ },
82
+ limits: {
83
+ memory: "256Mi",
84
+ cpu: "500m",
85
+ },
86
+ },
87
+ volumeMounts: [
88
+ {
89
+ name: "tls-certs",
90
+ mountPath: "/etc/certs",
91
+ readOnly: true,
92
+ },
93
+ {
94
+ name: "api-token",
95
+ mountPath: "/app/api-token",
96
+ readOnly: true,
97
+ },
98
+ {
99
+ name: "module",
100
+ mountPath: `/app/load`,
101
+ readOnly: true,
102
+ },
103
+ ],
104
+ },
105
+ ],
106
+ volumes: [
107
+ {
108
+ name: "tls-certs",
109
+ secret: {
110
+ secretName: `${name}-tls`,
111
+ },
112
+ },
113
+ {
114
+ name: "api-token",
115
+ secret: {
116
+ secretName: `${name}-api-token`,
117
+ },
118
+ },
119
+ {
120
+ name: "module",
121
+ secret: {
122
+ secretName: `${name}-module`,
123
+ },
124
+ },
125
+ ],
126
+ },
127
+ },
128
+ },
129
+ };
130
+ }
131
+
132
+ export function moduleSecret(name: string, data: Buffer, hash: string): Secret {
133
+ // Compress the data
134
+ const compressed = gzipSync(data);
135
+ const path = `module-${hash}.js.gz`;
136
+ return {
137
+ apiVersion: "v1",
138
+ kind: "Secret",
139
+ metadata: {
140
+ name: `${name}-module`,
141
+ namespace: "pepr-system",
142
+ },
143
+ type: "Opaque",
144
+ data: {
145
+ [path]: compressed.toString("base64"),
146
+ },
147
+ };
148
+ }
@@ -0,0 +1,57 @@
1
+ // SPDX-License-Identifier: Apache-2.0
2
+ // SPDX-FileCopyrightText: 2023-Present The Pepr Authors
3
+
4
+ import { ClusterRole, ClusterRoleBinding, ServiceAccount } from "../k8s/upstream";
5
+
6
+ /**
7
+ * Grants the controller access to cluster resources beyond the mutating webhook.
8
+ *
9
+ * @todo: should dynamically generate this based on resources used by the module. will also need to explore how this should work for multiple modules.
10
+ * @returns
11
+ */
12
+ export function clusterRole(name: string): ClusterRole {
13
+ return {
14
+ apiVersion: "rbac.authorization.k8s.io/v1",
15
+ kind: "ClusterRole",
16
+ metadata: { name },
17
+ rules: [
18
+ {
19
+ // @todo: make this configurable
20
+ apiGroups: ["*"],
21
+ resources: ["*"],
22
+ verbs: ["create", "delete", "get", "list", "patch", "update", "watch"],
23
+ },
24
+ ],
25
+ };
26
+ }
27
+
28
+ export function clusterRoleBinding(name: string): ClusterRoleBinding {
29
+ return {
30
+ apiVersion: "rbac.authorization.k8s.io/v1",
31
+ kind: "ClusterRoleBinding",
32
+ metadata: { name },
33
+ roleRef: {
34
+ apiGroup: "rbac.authorization.k8s.io",
35
+ kind: "ClusterRole",
36
+ name,
37
+ },
38
+ subjects: [
39
+ {
40
+ kind: "ServiceAccount",
41
+ name,
42
+ namespace: "pepr-system",
43
+ },
44
+ ],
45
+ };
46
+ }
47
+
48
+ export function serviceAccount(name: string): ServiceAccount {
49
+ return {
50
+ apiVersion: "v1",
51
+ kind: "ServiceAccount",
52
+ metadata: {
53
+ name,
54
+ namespace: "pepr-system",
55
+ },
56
+ };
57
+ }
@@ -0,0 +1,139 @@
1
+ // SPDX-License-Identifier: Apache-2.0
2
+ // SPDX-FileCopyrightText: 2023-Present The Pepr Authors
3
+
4
+ import {
5
+ AdmissionregistrationV1WebhookClientConfig as AdmissionRegnV1WebhookClientCfg,
6
+ V1LabelSelectorRequirement,
7
+ V1RuleWithOperations,
8
+ } from "@kubernetes/client-node";
9
+ import { concat, equals, uniqWith } from "ramda";
10
+
11
+ import { Assets } from ".";
12
+ import { MutatingWebhookConfiguration, ValidatingWebhookConfiguration } from "../k8s/upstream";
13
+ import { Event } from "../types";
14
+
15
+ const peprIgnoreLabel: V1LabelSelectorRequirement = {
16
+ key: "pepr.dev",
17
+ operator: "NotIn",
18
+ values: ["ignore"],
19
+ };
20
+
21
+ const peprIgnoreNamespaces: string[] = ["kube-system", "pepr-system"];
22
+
23
+ export async function generateWebhookRules(assets: Assets, isMutateWebhook: boolean) {
24
+ const { config, capabilities } = assets;
25
+ const rules: V1RuleWithOperations[] = [];
26
+
27
+ // Iterate through the capabilities and generate the rules
28
+ for (const capability of capabilities) {
29
+ console.info(`Module ${config.uuid} has capability: ${capability._name}`);
30
+
31
+ // Read the bindings and generate the rules
32
+ for (const binding of capability._bindings) {
33
+ const { event, kind, isMutate, isValidate } = binding;
34
+
35
+ // If the module doesn't have a callback for the event, skip it
36
+ if (isMutateWebhook && !isMutate) {
37
+ continue;
38
+ }
39
+
40
+ if (!isMutateWebhook && !isValidate) {
41
+ continue;
42
+ }
43
+
44
+ const operations: string[] = [];
45
+
46
+ // CreateOrUpdate is a Pepr-specific event that is translated to Create and Update
47
+ if (event === Event.CreateOrUpdate) {
48
+ operations.push(Event.Create, Event.Update);
49
+ } else {
50
+ operations.push(event);
51
+ }
52
+
53
+ // Use the plural property if it exists, otherwise use lowercase kind + s
54
+ const resource = kind.plural || `${kind.kind.toLowerCase()}s`;
55
+
56
+ rules.push({
57
+ apiGroups: [kind.group],
58
+ apiVersions: [kind.version || "*"],
59
+ operations,
60
+ resources: [resource],
61
+ });
62
+ }
63
+ }
64
+
65
+ // Return the rules with duplicates removed
66
+ return uniqWith(equals, rules);
67
+ }
68
+
69
+ export async function webhookConfig(
70
+ assets: Assets,
71
+ mutateOrValidate: "mutate" | "validate",
72
+ timeoutSeconds = 10,
73
+ ): Promise<MutatingWebhookConfiguration | ValidatingWebhookConfiguration | null> {
74
+ const ignore = [peprIgnoreLabel];
75
+
76
+ const { name, tls, config, apiToken, host } = assets;
77
+ const ignoreNS = concat(peprIgnoreNamespaces, config.alwaysIgnore.namespaces || []);
78
+
79
+ // Add any namespaces to ignore
80
+ if (ignoreNS) {
81
+ ignore.push({
82
+ key: "kubernetes.io/metadata.name",
83
+ operator: "NotIn",
84
+ values: ignoreNS,
85
+ });
86
+ }
87
+
88
+ const clientConfig: AdmissionRegnV1WebhookClientCfg = {
89
+ caBundle: tls.ca,
90
+ };
91
+
92
+ // The URL must include the API Token
93
+ const apiPath = `/${mutateOrValidate}/${apiToken}`;
94
+
95
+ // If a host is specified, use that with a port of 3000
96
+ if (host) {
97
+ clientConfig.url = `https://${host}:3000${apiPath}`;
98
+ } else {
99
+ // Otherwise, use the service
100
+ clientConfig.service = {
101
+ name: name,
102
+ namespace: "pepr-system",
103
+ path: apiPath,
104
+ };
105
+ }
106
+
107
+ const isMutate = mutateOrValidate === "mutate";
108
+ const rules = await generateWebhookRules(assets, isMutate);
109
+
110
+ // If there are no rules, return null
111
+ if (rules.length < 1) {
112
+ return null;
113
+ }
114
+
115
+ return {
116
+ apiVersion: "admissionregistration.k8s.io/v1",
117
+ kind: isMutate ? "MutatingWebhookConfiguration" : "ValidatingWebhookConfiguration",
118
+ metadata: { name },
119
+ webhooks: [
120
+ {
121
+ name: `${name}.pepr.dev`,
122
+ admissionReviewVersions: ["v1", "v1beta1"],
123
+ clientConfig,
124
+ failurePolicy: "Ignore",
125
+ matchPolicy: "Equivalent",
126
+ timeoutSeconds,
127
+ namespaceSelector: {
128
+ matchExpressions: ignore,
129
+ },
130
+ objectSelector: {
131
+ matchExpressions: ignore,
132
+ },
133
+ rules,
134
+ // @todo: track side effects state
135
+ sideEffects: "None",
136
+ },
137
+ ],
138
+ };
139
+ }