pepr 0.0.0-development

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 (58) hide show
  1. package/.prettierignore +1 -0
  2. package/CODE_OF_CONDUCT.md +133 -0
  3. package/LICENSE +201 -0
  4. package/README.md +151 -0
  5. package/SECURITY.md +18 -0
  6. package/SUPPORT.md +16 -0
  7. package/codecov.yaml +19 -0
  8. package/commitlint.config.js +1 -0
  9. package/package.json +70 -0
  10. package/src/cli.ts +48 -0
  11. package/src/lib/assets/deploy.ts +122 -0
  12. package/src/lib/assets/destroy.ts +33 -0
  13. package/src/lib/assets/helm.ts +219 -0
  14. package/src/lib/assets/index.ts +175 -0
  15. package/src/lib/assets/loader.ts +41 -0
  16. package/src/lib/assets/networking.ts +89 -0
  17. package/src/lib/assets/pods.ts +353 -0
  18. package/src/lib/assets/rbac.ts +111 -0
  19. package/src/lib/assets/store.ts +49 -0
  20. package/src/lib/assets/webhooks.ts +147 -0
  21. package/src/lib/assets/yaml.ts +234 -0
  22. package/src/lib/capability.ts +314 -0
  23. package/src/lib/controller/index.ts +326 -0
  24. package/src/lib/controller/store.ts +219 -0
  25. package/src/lib/errors.ts +20 -0
  26. package/src/lib/filter.ts +110 -0
  27. package/src/lib/helpers.ts +342 -0
  28. package/src/lib/included-files.ts +19 -0
  29. package/src/lib/k8s.ts +169 -0
  30. package/src/lib/logger.ts +27 -0
  31. package/src/lib/metrics.ts +120 -0
  32. package/src/lib/module.ts +136 -0
  33. package/src/lib/mutate-processor.ts +160 -0
  34. package/src/lib/mutate-request.ts +153 -0
  35. package/src/lib/queue.ts +89 -0
  36. package/src/lib/schedule.ts +175 -0
  37. package/src/lib/storage.ts +192 -0
  38. package/src/lib/tls.ts +90 -0
  39. package/src/lib/types.ts +215 -0
  40. package/src/lib/utils.ts +57 -0
  41. package/src/lib/validate-processor.ts +80 -0
  42. package/src/lib/validate-request.ts +102 -0
  43. package/src/lib/watch-processor.ts +124 -0
  44. package/src/lib.ts +27 -0
  45. package/src/runtime/controller.ts +75 -0
  46. package/src/sdk/sdk.ts +116 -0
  47. package/src/templates/.eslintrc.template.json +18 -0
  48. package/src/templates/.prettierrc.json +13 -0
  49. package/src/templates/README.md +21 -0
  50. package/src/templates/capabilities/hello-pepr.samples.json +160 -0
  51. package/src/templates/capabilities/hello-pepr.ts +426 -0
  52. package/src/templates/gitignore +4 -0
  53. package/src/templates/package.json +20 -0
  54. package/src/templates/pepr.code-snippets.json +21 -0
  55. package/src/templates/pepr.ts +17 -0
  56. package/src/templates/settings.json +10 -0
  57. package/src/templates/tsconfig.json +9 -0
  58. package/src/templates/tsconfig.module.json +19 -0
@@ -0,0 +1,353 @@
1
+ // SPDX-License-Identifier: Apache-2.0
2
+ // SPDX-FileCopyrightText: 2023-Present The Pepr Authors
3
+
4
+ import { V1EnvVar } from "@kubernetes/client-node";
5
+ import { kind } from "kubernetes-fluent-client";
6
+ import { gzipSync } from "zlib";
7
+ import { secretOverLimit } from "../helpers";
8
+ import { Assets } from ".";
9
+ import { ModuleConfig } from "../module";
10
+ import { Binding } from "../types";
11
+
12
+ /** Generate the pepr-system namespace */
13
+ export function namespace(namespaceLabels?: Record<string, string>) {
14
+ if (namespaceLabels) {
15
+ return {
16
+ apiVersion: "v1",
17
+ kind: "Namespace",
18
+ metadata: {
19
+ name: "pepr-system",
20
+ labels: namespaceLabels ?? {},
21
+ },
22
+ };
23
+ } else {
24
+ return {
25
+ apiVersion: "v1",
26
+ kind: "Namespace",
27
+ metadata: {
28
+ name: "pepr-system",
29
+ },
30
+ };
31
+ }
32
+ }
33
+
34
+ export function watcher(assets: Assets, hash: string, buildTimestamp: string) {
35
+ const { name, image, capabilities, config } = assets;
36
+
37
+ let hasSchedule = false;
38
+
39
+ // Append the watcher suffix
40
+ const app = `${name}-watcher`;
41
+ const bindings: Binding[] = [];
42
+
43
+ // Loop through the capabilities and find any Watch Actions
44
+ for (const capability of capabilities) {
45
+ if (capability.hasSchedule) {
46
+ hasSchedule = true;
47
+ }
48
+ const watchers = capability.bindings.filter(binding => binding.isWatch);
49
+ bindings.push(...watchers);
50
+ }
51
+
52
+ // If there are no watchers, don't deploy the watcher
53
+ if (bindings.length < 1 && !hasSchedule) {
54
+ return null;
55
+ }
56
+
57
+ return {
58
+ apiVersion: "apps/v1",
59
+ kind: "Deployment",
60
+ metadata: {
61
+ name: app,
62
+ namespace: "pepr-system",
63
+ annotations: {
64
+ "pepr.dev/description": config.description || "",
65
+ },
66
+ labels: {
67
+ app,
68
+ "pepr.dev/controller": "watcher",
69
+ "pepr.dev/uuid": config.uuid,
70
+ },
71
+ },
72
+ spec: {
73
+ replicas: 1,
74
+ strategy: {
75
+ type: "Recreate",
76
+ },
77
+ selector: {
78
+ matchLabels: {
79
+ app,
80
+ "pepr.dev/controller": "watcher",
81
+ },
82
+ },
83
+ template: {
84
+ metadata: {
85
+ annotations: {
86
+ buildTimestamp: `${buildTimestamp}`,
87
+ },
88
+ labels: {
89
+ app,
90
+ "pepr.dev/controller": "watcher",
91
+ },
92
+ },
93
+ spec: {
94
+ terminationGracePeriodSeconds: 5,
95
+ serviceAccountName: name,
96
+ securityContext: {
97
+ runAsUser: 65532,
98
+ runAsGroup: 65532,
99
+ runAsNonRoot: true,
100
+ fsGroup: 65532,
101
+ },
102
+ containers: [
103
+ {
104
+ name: "watcher",
105
+ image,
106
+ imagePullPolicy: "IfNotPresent",
107
+ command: ["node", "/app/node_modules/pepr/dist/controller.js", hash],
108
+ readinessProbe: {
109
+ httpGet: {
110
+ path: "/healthz",
111
+ port: 3000,
112
+ scheme: "HTTPS",
113
+ },
114
+ },
115
+ livenessProbe: {
116
+ httpGet: {
117
+ path: "/healthz",
118
+ port: 3000,
119
+ scheme: "HTTPS",
120
+ },
121
+ },
122
+ ports: [
123
+ {
124
+ containerPort: 3000,
125
+ },
126
+ ],
127
+ resources: {
128
+ requests: {
129
+ memory: "64Mi",
130
+ cpu: "100m",
131
+ },
132
+ limits: {
133
+ memory: "256Mi",
134
+ cpu: "500m",
135
+ },
136
+ },
137
+ securityContext: {
138
+ runAsUser: 65532,
139
+ runAsGroup: 65532,
140
+ runAsNonRoot: true,
141
+ allowPrivilegeEscalation: false,
142
+ capabilities: {
143
+ drop: ["ALL"],
144
+ },
145
+ },
146
+ volumeMounts: [
147
+ {
148
+ name: "tls-certs",
149
+ mountPath: "/etc/certs",
150
+ readOnly: true,
151
+ },
152
+ {
153
+ name: "module",
154
+ mountPath: `/app/load`,
155
+ readOnly: true,
156
+ },
157
+ ],
158
+ env: genEnv(config, true),
159
+ },
160
+ ],
161
+ volumes: [
162
+ {
163
+ name: "tls-certs",
164
+ secret: {
165
+ secretName: `${name}-tls`,
166
+ },
167
+ },
168
+ {
169
+ name: "module",
170
+ secret: {
171
+ secretName: `${name}-module`,
172
+ },
173
+ },
174
+ ],
175
+ },
176
+ },
177
+ },
178
+ };
179
+ }
180
+
181
+ export function deployment(assets: Assets, hash: string, buildTimestamp: string): kind.Deployment {
182
+ const { name, image, config } = assets;
183
+ const app = name;
184
+
185
+ return {
186
+ apiVersion: "apps/v1",
187
+ kind: "Deployment",
188
+ metadata: {
189
+ name,
190
+ namespace: "pepr-system",
191
+ annotations: {
192
+ "pepr.dev/description": config.description || "",
193
+ },
194
+ labels: {
195
+ app,
196
+ "pepr.dev/controller": "admission",
197
+ "pepr.dev/uuid": config.uuid,
198
+ },
199
+ },
200
+ spec: {
201
+ replicas: 2,
202
+ selector: {
203
+ matchLabels: {
204
+ app,
205
+ "pepr.dev/controller": "admission",
206
+ },
207
+ },
208
+ template: {
209
+ metadata: {
210
+ annotations: {
211
+ buildTimestamp: `${buildTimestamp}`,
212
+ },
213
+ labels: {
214
+ app,
215
+ "pepr.dev/controller": "admission",
216
+ },
217
+ },
218
+ spec: {
219
+ terminationGracePeriodSeconds: 5,
220
+ priorityClassName: "system-node-critical",
221
+ serviceAccountName: name,
222
+ securityContext: {
223
+ runAsUser: 65532,
224
+ runAsGroup: 65532,
225
+ runAsNonRoot: true,
226
+ fsGroup: 65532,
227
+ },
228
+ containers: [
229
+ {
230
+ name: "server",
231
+ image,
232
+ imagePullPolicy: "IfNotPresent",
233
+ command: ["node", "/app/node_modules/pepr/dist/controller.js", hash],
234
+ readinessProbe: {
235
+ httpGet: {
236
+ path: "/healthz",
237
+ port: 3000,
238
+ scheme: "HTTPS",
239
+ },
240
+ },
241
+ livenessProbe: {
242
+ httpGet: {
243
+ path: "/healthz",
244
+ port: 3000,
245
+ scheme: "HTTPS",
246
+ },
247
+ },
248
+ ports: [
249
+ {
250
+ containerPort: 3000,
251
+ },
252
+ ],
253
+ resources: {
254
+ requests: {
255
+ memory: "64Mi",
256
+ cpu: "100m",
257
+ },
258
+ limits: {
259
+ memory: "256Mi",
260
+ cpu: "500m",
261
+ },
262
+ },
263
+ env: genEnv(config),
264
+ securityContext: {
265
+ runAsUser: 65532,
266
+ runAsGroup: 65532,
267
+ runAsNonRoot: true,
268
+ allowPrivilegeEscalation: false,
269
+ capabilities: {
270
+ drop: ["ALL"],
271
+ },
272
+ },
273
+ volumeMounts: [
274
+ {
275
+ name: "tls-certs",
276
+ mountPath: "/etc/certs",
277
+ readOnly: true,
278
+ },
279
+ {
280
+ name: "api-token",
281
+ mountPath: "/app/api-token",
282
+ readOnly: true,
283
+ },
284
+ {
285
+ name: "module",
286
+ mountPath: `/app/load`,
287
+ readOnly: true,
288
+ },
289
+ ],
290
+ },
291
+ ],
292
+ volumes: [
293
+ {
294
+ name: "tls-certs",
295
+ secret: {
296
+ secretName: `${name}-tls`,
297
+ },
298
+ },
299
+ {
300
+ name: "api-token",
301
+ secret: {
302
+ secretName: `${name}-api-token`,
303
+ },
304
+ },
305
+ {
306
+ name: "module",
307
+ secret: {
308
+ secretName: `${name}-module`,
309
+ },
310
+ },
311
+ ],
312
+ },
313
+ },
314
+ },
315
+ };
316
+ }
317
+
318
+ export function moduleSecret(name: string, data: Buffer, hash: string): kind.Secret {
319
+ // Compress the data
320
+ const compressed = gzipSync(data);
321
+ const path = `module-${hash}.js.gz`;
322
+ const compressedData = compressed.toString("base64");
323
+ if (secretOverLimit(compressedData)) {
324
+ const error = new Error(`Module secret for ${name} is over the 1MB limit`);
325
+ console.error("Uncaught Exception:", error);
326
+ process.exit(1);
327
+ } else {
328
+ return {
329
+ apiVersion: "v1",
330
+ kind: "Secret",
331
+ metadata: {
332
+ name: `${name}-module`,
333
+ namespace: "pepr-system",
334
+ },
335
+ type: "Opaque",
336
+ data: {
337
+ [path]: compressed.toString("base64"),
338
+ },
339
+ };
340
+ }
341
+ }
342
+
343
+ function genEnv(config: ModuleConfig, watchMode = false): V1EnvVar[] {
344
+ const def = {
345
+ PEPR_WATCH_MODE: watchMode ? "true" : "false",
346
+ PEPR_PRETTY_LOG: "false",
347
+ LOG_LEVEL: config.logLevel || "info",
348
+ };
349
+ const cfg = config.env || {};
350
+ const env = Object.entries({ ...def, ...cfg }).map(([name, value]) => ({ name, value }));
351
+
352
+ return env;
353
+ }
@@ -0,0 +1,111 @@
1
+ // SPDX-License-Identifier: Apache-2.0
2
+ // SPDX-FileCopyrightText: 2023-Present The Pepr Authors
3
+
4
+ import { kind } from "kubernetes-fluent-client";
5
+ import { CapabilityExport } from "../types";
6
+ import { createRBACMap } from "../helpers";
7
+ /**
8
+ * Grants the controller access to cluster resources beyond the mutating webhook.
9
+ *
10
+ * @todo: should dynamically generate this based on resources used by the module. will also need to explore how this should work for multiple modules.
11
+ * @returns
12
+ */
13
+ export function clusterRole(name: string, capabilities: CapabilityExport[], rbacMode: string = ""): kind.ClusterRole {
14
+ const rbacMap = createRBACMap(capabilities);
15
+ return {
16
+ apiVersion: "rbac.authorization.k8s.io/v1",
17
+ kind: "ClusterRole",
18
+ metadata: { name },
19
+ rules:
20
+ rbacMode === "scoped"
21
+ ? [
22
+ ...Object.keys(rbacMap).map(key => {
23
+ // let group:string, version:string, kind:string;
24
+ let group: string;
25
+ key.split("/").length < 3 ? (group = "") : (group = key.split("/")[0]);
26
+
27
+ return {
28
+ apiGroups: [group],
29
+ resources: [rbacMap[key].plural],
30
+ verbs: rbacMap[key].verbs,
31
+ };
32
+ }),
33
+ ]
34
+ : [
35
+ {
36
+ apiGroups: ["*"],
37
+ resources: ["*"],
38
+ verbs: ["create", "delete", "get", "list", "patch", "update", "watch"],
39
+ },
40
+ ],
41
+ };
42
+ }
43
+
44
+ export function clusterRoleBinding(name: string): kind.ClusterRoleBinding {
45
+ return {
46
+ apiVersion: "rbac.authorization.k8s.io/v1",
47
+ kind: "ClusterRoleBinding",
48
+ metadata: { name },
49
+ roleRef: {
50
+ apiGroup: "rbac.authorization.k8s.io",
51
+ kind: "ClusterRole",
52
+ name,
53
+ },
54
+ subjects: [
55
+ {
56
+ kind: "ServiceAccount",
57
+ name,
58
+ namespace: "pepr-system",
59
+ },
60
+ ],
61
+ };
62
+ }
63
+
64
+ export function serviceAccount(name: string): kind.ServiceAccount {
65
+ return {
66
+ apiVersion: "v1",
67
+ kind: "ServiceAccount",
68
+ metadata: {
69
+ name,
70
+ namespace: "pepr-system",
71
+ },
72
+ };
73
+ }
74
+
75
+ export function storeRole(name: string): kind.Role {
76
+ name = `${name}-store`;
77
+ return {
78
+ apiVersion: "rbac.authorization.k8s.io/v1",
79
+ kind: "Role",
80
+ metadata: { name, namespace: "pepr-system" },
81
+ rules: [
82
+ {
83
+ apiGroups: ["pepr.dev"],
84
+ resources: ["peprstores"],
85
+ resourceNames: [""],
86
+ verbs: ["create", "get", "patch", "watch"],
87
+ },
88
+ ],
89
+ };
90
+ }
91
+
92
+ export function storeRoleBinding(name: string): kind.RoleBinding {
93
+ name = `${name}-store`;
94
+ return {
95
+ apiVersion: "rbac.authorization.k8s.io/v1",
96
+ kind: "RoleBinding",
97
+ metadata: { name, namespace: "pepr-system" },
98
+ roleRef: {
99
+ apiGroup: "rbac.authorization.k8s.io",
100
+ kind: "Role",
101
+ name,
102
+ },
103
+ subjects: [
104
+ {
105
+ kind: "ServiceAccount",
106
+ name,
107
+ namespace: "pepr-system",
108
+ },
109
+ ],
110
+ };
111
+ }
@@ -0,0 +1,49 @@
1
+ // SPDX-License-Identifier: Apache-2.0
2
+ // SPDX-FileCopyrightText: 2023-Present The Pepr Authors
3
+
4
+ import { kind as k } from "kubernetes-fluent-client";
5
+
6
+ import { peprStoreGVK } from "../k8s";
7
+
8
+ export const { group, version, kind } = peprStoreGVK;
9
+ export const singular = kind.toLocaleLowerCase();
10
+ export const plural = `${singular}s`;
11
+ export const name = `${plural}.${group}`;
12
+
13
+ export const peprStoreCRD: k.CustomResourceDefinition = {
14
+ apiVersion: "apiextensions.k8s.io/v1",
15
+ kind: "CustomResourceDefinition",
16
+ metadata: {
17
+ name,
18
+ },
19
+ spec: {
20
+ group,
21
+ versions: [
22
+ {
23
+ // typescript doesn't know this is really already set, which is kind of annoying
24
+ name: version || "v1",
25
+ served: true,
26
+ storage: true,
27
+ schema: {
28
+ openAPIV3Schema: {
29
+ type: "object",
30
+ properties: {
31
+ data: {
32
+ type: "object",
33
+ additionalProperties: {
34
+ type: "string",
35
+ },
36
+ },
37
+ },
38
+ },
39
+ },
40
+ },
41
+ ],
42
+ scope: "Namespaced",
43
+ names: {
44
+ plural,
45
+ singular,
46
+ kind,
47
+ },
48
+ },
49
+ };
@@ -0,0 +1,147 @@
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 { kind } from "kubernetes-fluent-client";
10
+ import { concat, equals, uniqWith } from "ramda";
11
+
12
+ import { Assets } from ".";
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
+ const ruleObject = {
57
+ apiGroups: [kind.group],
58
+ apiVersions: [kind.version || "*"],
59
+ operations,
60
+ resources: [resource],
61
+ };
62
+
63
+ // If the resource is pods, add ephemeralcontainers as well
64
+ if (resource === "pods") {
65
+ ruleObject.resources.push("pods/ephemeralcontainers");
66
+ }
67
+
68
+ // Add the rule to the rules array
69
+ rules.push(ruleObject);
70
+ }
71
+ }
72
+
73
+ // Return the rules with duplicates removed
74
+ return uniqWith(equals, rules);
75
+ }
76
+
77
+ export async function webhookConfig(
78
+ assets: Assets,
79
+ mutateOrValidate: "mutate" | "validate",
80
+ timeoutSeconds = 10,
81
+ ): Promise<kind.MutatingWebhookConfiguration | kind.ValidatingWebhookConfiguration | null> {
82
+ const ignore = [peprIgnoreLabel];
83
+
84
+ const { name, tls, config, apiToken, host } = assets;
85
+ const ignoreNS = concat(peprIgnoreNamespaces, config.alwaysIgnore.namespaces || []);
86
+
87
+ // Add any namespaces to ignore
88
+ if (ignoreNS) {
89
+ ignore.push({
90
+ key: "kubernetes.io/metadata.name",
91
+ operator: "NotIn",
92
+ values: ignoreNS,
93
+ });
94
+ }
95
+
96
+ const clientConfig: AdmissionRegnV1WebhookClientCfg = {
97
+ caBundle: tls.ca,
98
+ };
99
+
100
+ // The URL must include the API Token
101
+ const apiPath = `/${mutateOrValidate}/${apiToken}`;
102
+
103
+ // If a host is specified, use that with a port of 3000
104
+ if (host) {
105
+ clientConfig.url = `https://${host}:3000${apiPath}`;
106
+ } else {
107
+ // Otherwise, use the service
108
+ clientConfig.service = {
109
+ name: name,
110
+ namespace: "pepr-system",
111
+ path: apiPath,
112
+ };
113
+ }
114
+
115
+ const isMutate = mutateOrValidate === "mutate";
116
+ const rules = await generateWebhookRules(assets, isMutate);
117
+
118
+ // If there are no rules, return null
119
+ if (rules.length < 1) {
120
+ return null;
121
+ }
122
+
123
+ return {
124
+ apiVersion: "admissionregistration.k8s.io/v1",
125
+ kind: isMutate ? "MutatingWebhookConfiguration" : "ValidatingWebhookConfiguration",
126
+ metadata: { name },
127
+ webhooks: [
128
+ {
129
+ name: `${name}.pepr.dev`,
130
+ admissionReviewVersions: ["v1", "v1beta1"],
131
+ clientConfig,
132
+ failurePolicy: config.onError === "reject" ? "Fail" : "Ignore",
133
+ matchPolicy: "Equivalent",
134
+ timeoutSeconds,
135
+ namespaceSelector: {
136
+ matchExpressions: ignore,
137
+ },
138
+ objectSelector: {
139
+ matchExpressions: ignore,
140
+ },
141
+ rules,
142
+ // @todo: track side effects state
143
+ sideEffects: "None",
144
+ },
145
+ ],
146
+ };
147
+ }