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,234 @@
1
+ // SPDX-License-Identifier: Apache-2.0
2
+ // SPDX-FileCopyrightText: 2023-Present The Pepr Authors
3
+
4
+ import { dumpYaml } from "@kubernetes/client-node";
5
+ import crypto from "crypto";
6
+ import { promises as fs } from "fs";
7
+
8
+ import { Assets } from ".";
9
+ import { apiTokenSecret, service, tlsSecret, watcherService } from "./networking";
10
+ import { deployment, moduleSecret, namespace, watcher } from "./pods";
11
+ import { clusterRole, clusterRoleBinding, serviceAccount, storeRole, storeRoleBinding } from "./rbac";
12
+ import { webhookConfig } from "./webhooks";
13
+
14
+ // Helm Chart overrides file (values.yaml) generated from assets
15
+ export async function overridesFile({ hash, name, image, config, apiToken }: Assets, path: string) {
16
+ const overrides = {
17
+ secrets: {
18
+ apiToken: Buffer.from(apiToken).toString("base64"),
19
+ },
20
+ hash,
21
+ namespace: {
22
+ annotations: {},
23
+ labels: {
24
+ "pepr.dev": "",
25
+ },
26
+ },
27
+ uuid: name,
28
+ admission: {
29
+ terminationGracePeriodSeconds: 5,
30
+ failurePolicy: config.onError === "reject" ? "Fail" : "Ignore",
31
+ webhookTimeout: config.webhookTimeout,
32
+ env: [
33
+ { name: "PEPR_WATCH_MODE", value: "false" },
34
+ { name: "PEPR_PRETTY_LOG", value: "false" },
35
+ { name: "LOG_LEVEL", value: "info" },
36
+ ],
37
+ image,
38
+ annotations: {
39
+ "pepr.dev/description": `${config.description}` || "",
40
+ },
41
+ labels: {
42
+ app: name,
43
+ "pepr.dev/controller": "admission",
44
+ "pepr.dev/uuid": config.uuid,
45
+ },
46
+ securityContext: {
47
+ runAsUser: 65532,
48
+ runAsGroup: 65532,
49
+ runAsNonRoot: true,
50
+ fsGroup: 65532,
51
+ },
52
+ resources: {
53
+ requests: {
54
+ memory: "64Mi",
55
+ cpu: "100m",
56
+ },
57
+ limits: {
58
+ memory: "256Mi",
59
+ cpu: "500m",
60
+ },
61
+ },
62
+ containerSecurityContext: {
63
+ runAsUser: 65532,
64
+ runAsGroup: 65532,
65
+ runAsNonRoot: true,
66
+ allowPrivilegeEscalation: false,
67
+ capabilities: {
68
+ drop: ["ALL"],
69
+ },
70
+ },
71
+ podAnnotations: {},
72
+ nodeSelector: {},
73
+ tolerations: [],
74
+ extraVolumeMounts: [],
75
+ extraVolumes: [],
76
+ affinity: {},
77
+ },
78
+ watcher: {
79
+ terminationGracePeriodSeconds: 5,
80
+ env: [
81
+ { name: "PEPR_WATCH_MODE", value: "true" },
82
+ { name: "PEPR_PRETTY_LOG", value: "false" },
83
+ { name: "LOG_LEVEL", value: "info" },
84
+ ],
85
+ image,
86
+ annotations: {
87
+ "pepr.dev/description": `${config.description}` || "",
88
+ },
89
+ labels: {
90
+ app: `${name}-watcher`,
91
+ "pepr.dev/controller": "watcher",
92
+ "pepr.dev/uuid": config.uuid,
93
+ },
94
+ securityContext: {
95
+ runAsUser: 65532,
96
+ runAsGroup: 65532,
97
+ runAsNonRoot: true,
98
+ fsGroup: 65532,
99
+ },
100
+ resources: {
101
+ requests: {
102
+ memory: "64Mi",
103
+ cpu: "100m",
104
+ },
105
+ limits: {
106
+ memory: "256Mi",
107
+ cpu: "500m",
108
+ },
109
+ },
110
+ containerSecurityContext: {
111
+ runAsUser: 65532,
112
+ runAsGroup: 65532,
113
+ runAsNonRoot: true,
114
+ allowPrivilegeEscalation: false,
115
+ capabilities: {
116
+ drop: ["ALL"],
117
+ },
118
+ },
119
+ nodeSelector: {},
120
+ tolerations: [],
121
+ extraVolumeMounts: [],
122
+ extraVolumes: [],
123
+ affinity: {},
124
+ podAnnotations: {},
125
+ },
126
+ };
127
+ if (process.env.PEPR_MODE === "dev") {
128
+ overrides.admission.env.push({ name: "ZARF_VAR", value: "###ZARF_VAR_THING###" });
129
+ overrides.watcher.env.push({ name: "ZARF_VAR", value: "###ZARF_VAR_THING###" });
130
+ overrides.admission.env.push({ name: "MY_CUSTOM_VAR", value: "example-value" });
131
+ overrides.watcher.env.push({ name: "MY_CUSTOM_VAR", value: "example-value" });
132
+ }
133
+
134
+ await fs.writeFile(path, dumpYaml(overrides, { noRefs: true, forceQuotes: true }));
135
+ }
136
+ export function zarfYaml({ name, image, config }: Assets, path: string) {
137
+ const zarfCfg = {
138
+ kind: "ZarfPackageConfig",
139
+ metadata: {
140
+ name,
141
+ description: `Pepr Module: ${config.description}`,
142
+ url: "https://github.com/defenseunicorns/pepr",
143
+ version: `${config.appVersion || "0.0.1"}`,
144
+ },
145
+ components: [
146
+ {
147
+ name: "module",
148
+ required: true,
149
+ manifests: [
150
+ {
151
+ name: "module",
152
+ namespace: "pepr-system",
153
+ files: [path],
154
+ },
155
+ ],
156
+ images: [image],
157
+ },
158
+ ],
159
+ };
160
+
161
+ return dumpYaml(zarfCfg, { noRefs: true });
162
+ }
163
+
164
+ export function zarfYamlChart({ name, image, config }: Assets, path: string) {
165
+ const zarfCfg = {
166
+ kind: "ZarfPackageConfig",
167
+ metadata: {
168
+ name,
169
+ description: `Pepr Module: ${config.description}`,
170
+ url: "https://github.com/defenseunicorns/pepr",
171
+ version: `${config.appVersion || "0.0.1"}`,
172
+ },
173
+ components: [
174
+ {
175
+ name: "module",
176
+ required: true,
177
+ charts: [
178
+ {
179
+ name: "module",
180
+ namespace: "pepr-system",
181
+ version: `${config.appVersion || "0.0.1"}`,
182
+ localPath: path,
183
+ },
184
+ ],
185
+ images: [image],
186
+ },
187
+ ],
188
+ };
189
+
190
+ return dumpYaml(zarfCfg, { noRefs: true });
191
+ }
192
+
193
+ export async function allYaml(assets: Assets, rbacMode: string) {
194
+ const { name, tls, apiToken, path } = assets;
195
+
196
+ const code = await fs.readFile(path);
197
+
198
+ // Generate a hash of the code
199
+ assets.hash = crypto.createHash("sha256").update(code).digest("hex");
200
+
201
+ const mutateWebhook = await webhookConfig(assets, "mutate", assets.config.webhookTimeout);
202
+ const validateWebhook = await webhookConfig(assets, "validate", assets.config.webhookTimeout);
203
+ const watchDeployment = watcher(assets, assets.hash, assets.buildTimestamp);
204
+
205
+ const resources = [
206
+ namespace(assets.config.customLabels?.namespace),
207
+ clusterRole(name, assets.capabilities, rbacMode),
208
+ clusterRoleBinding(name),
209
+ serviceAccount(name),
210
+ apiTokenSecret(name, apiToken),
211
+ tlsSecret(name, tls),
212
+ deployment(assets, assets.hash, assets.buildTimestamp),
213
+ service(name),
214
+ watcherService(name),
215
+ moduleSecret(name, code, assets.hash),
216
+ storeRole(name),
217
+ storeRoleBinding(name),
218
+ ];
219
+
220
+ if (mutateWebhook) {
221
+ resources.push(mutateWebhook);
222
+ }
223
+
224
+ if (validateWebhook) {
225
+ resources.push(validateWebhook);
226
+ }
227
+
228
+ if (watchDeployment) {
229
+ resources.push(watchDeployment);
230
+ }
231
+
232
+ // Convert the resources to a single YAML string
233
+ return resources.map(r => dumpYaml(r, { noRefs: true })).join("---\n");
234
+ }
@@ -0,0 +1,314 @@
1
+ // SPDX-License-Identifier: Apache-2.0
2
+ // SPDX-FileCopyrightText: 2023-Present The Pepr Authors
3
+
4
+ import { GenericClass, GroupVersionKind, modelToGroupVersionKind } from "kubernetes-fluent-client";
5
+ import { WatchAction } from "kubernetes-fluent-client/dist/fluent/types";
6
+ import { pickBy } from "ramda";
7
+
8
+ import Log from "./logger";
9
+ import { isBuildMode, isDevMode, isWatchMode } from "./module";
10
+ import { PeprStore, Storage } from "./storage";
11
+ import { OnSchedule, Schedule } from "./schedule";
12
+ import {
13
+ Binding,
14
+ BindingFilter,
15
+ BindingWithName,
16
+ CapabilityCfg,
17
+ CapabilityExport,
18
+ Event,
19
+ MutateAction,
20
+ MutateActionChain,
21
+ ValidateAction,
22
+ ValidateActionChain,
23
+ WhenSelector,
24
+ } from "./types";
25
+
26
+ const registerAdmission = isBuildMode() || !isWatchMode();
27
+ const registerWatch = isBuildMode() || isWatchMode() || isDevMode();
28
+
29
+ /**
30
+ * A capability is a unit of functionality that can be registered with the Pepr runtime.
31
+ */
32
+ export class Capability implements CapabilityExport {
33
+ #name: string;
34
+ #description: string;
35
+ #namespaces?: string[] | undefined;
36
+ #bindings: Binding[] = [];
37
+ #store = new Storage();
38
+ #scheduleStore = new Storage();
39
+ #registered = false;
40
+ #scheduleRegistered = false;
41
+ hasSchedule: boolean;
42
+
43
+ /**
44
+ * Run code on a schedule with the capability.
45
+ *
46
+ * @param schedule The schedule to run the code on
47
+ * @returns
48
+ */
49
+ OnSchedule: (schedule: Schedule) => void = (schedule: Schedule) => {
50
+ const { name, every, unit, run, startTime, completions } = schedule;
51
+ this.hasSchedule = true;
52
+
53
+ if (process.env.PEPR_WATCH_MODE === "true" || process.env.PEPR_MODE === "dev") {
54
+ // Only create/watch schedule store if necessary
55
+
56
+ // Create a new schedule
57
+ const newSchedule: Schedule = {
58
+ name,
59
+ every,
60
+ unit,
61
+ run,
62
+ startTime,
63
+ completions,
64
+ };
65
+
66
+ this.#scheduleStore.onReady(() => {
67
+ new OnSchedule(newSchedule).setStore(this.#scheduleStore);
68
+ });
69
+ }
70
+ };
71
+
72
+ /**
73
+ * Store is a key-value data store that can be used to persist data that should be shared
74
+ * between requests. Each capability has its own store, and the data is persisted in Kubernetes
75
+ * in the `pepr-system` namespace.
76
+ *
77
+ * Note: You should only access the store from within an action.
78
+ */
79
+ Store: PeprStore = {
80
+ clear: this.#store.clear,
81
+ getItem: this.#store.getItem,
82
+ removeItem: this.#store.removeItem,
83
+ removeItemAndWait: this.#store.removeItemAndWait,
84
+ setItem: this.#store.setItem,
85
+ subscribe: this.#store.subscribe,
86
+ onReady: this.#store.onReady,
87
+ setItemAndWait: this.#store.setItemAndWait,
88
+ };
89
+
90
+ /**
91
+ * ScheduleStore is a key-value data store used to persist schedule data that should be shared
92
+ * between intervals. Each Schedule shares store, and the data is persisted in Kubernetes
93
+ * in the `pepr-system` namespace.
94
+ *
95
+ * Note: There is no direct access to schedule store
96
+ */
97
+ ScheduleStore: PeprStore = {
98
+ clear: this.#scheduleStore.clear,
99
+ getItem: this.#scheduleStore.getItem,
100
+ removeItemAndWait: this.#scheduleStore.removeItemAndWait,
101
+ removeItem: this.#scheduleStore.removeItem,
102
+ setItemAndWait: this.#scheduleStore.setItemAndWait,
103
+ setItem: this.#scheduleStore.setItem,
104
+ subscribe: this.#scheduleStore.subscribe,
105
+ onReady: this.#scheduleStore.onReady,
106
+ };
107
+
108
+ get bindings() {
109
+ return this.#bindings;
110
+ }
111
+
112
+ get name() {
113
+ return this.#name;
114
+ }
115
+
116
+ get description() {
117
+ return this.#description;
118
+ }
119
+
120
+ get namespaces() {
121
+ return this.#namespaces || [];
122
+ }
123
+
124
+ constructor(cfg: CapabilityCfg) {
125
+ this.#name = cfg.name;
126
+ this.#description = cfg.description;
127
+ this.#namespaces = cfg.namespaces;
128
+ this.hasSchedule = false;
129
+
130
+ Log.info(`Capability ${this.#name} registered`);
131
+ Log.debug(cfg);
132
+ }
133
+
134
+ /**
135
+ * Register the store with the capability. This is called automatically by the Pepr controller.
136
+ *
137
+ * @param store
138
+ */
139
+ registerScheduleStore = () => {
140
+ Log.info(`Registering schedule store for ${this.#name}`);
141
+
142
+ if (this.#scheduleRegistered) {
143
+ throw new Error(`Schedule store already registered for ${this.#name}`);
144
+ }
145
+
146
+ this.#scheduleRegistered = true;
147
+
148
+ // Pass back any ready callback to the controller
149
+ return {
150
+ scheduleStore: this.#scheduleStore,
151
+ };
152
+ };
153
+
154
+ /**
155
+ * Register the store with the capability. This is called automatically by the Pepr controller.
156
+ *
157
+ * @param store
158
+ */
159
+ registerStore = () => {
160
+ Log.info(`Registering store for ${this.#name}`);
161
+
162
+ if (this.#registered) {
163
+ throw new Error(`Store already registered for ${this.#name}`);
164
+ }
165
+
166
+ this.#registered = true;
167
+
168
+ // Pass back any ready callback to the controller
169
+ return {
170
+ store: this.#store,
171
+ };
172
+ };
173
+
174
+ /**
175
+ * The When method is used to register a action to be executed when a Kubernetes resource is
176
+ * processed by Pepr. The action will be executed if the resource matches the specified kind and any
177
+ * filters that are applied.
178
+ *
179
+ * @param model the KubernetesObject model to match
180
+ * @param kind if using a custom KubernetesObject not available in `a.*`, specify the GroupVersionKind
181
+ * @returns
182
+ */
183
+ When = <T extends GenericClass>(model: T, kind?: GroupVersionKind): WhenSelector<T> => {
184
+ const matchedKind = modelToGroupVersionKind(model.name);
185
+
186
+ // If the kind is not specified and the model is not a KubernetesObject, throw an error
187
+ if (!matchedKind && !kind) {
188
+ throw new Error(`Kind not specified for ${model.name}`);
189
+ }
190
+
191
+ const binding: Binding = {
192
+ model,
193
+ // If the kind is not specified, use the matched kind from the model
194
+ kind: kind || matchedKind,
195
+ event: Event.Any,
196
+ filters: {
197
+ name: "",
198
+ namespaces: [],
199
+ labels: {},
200
+ annotations: {},
201
+ },
202
+ };
203
+
204
+ const bindings = this.#bindings;
205
+ const prefix = `${this.#name}: ${model.name}`;
206
+ const commonChain = { WithLabel, WithAnnotation, Mutate, Validate, Watch, Reconcile };
207
+ const isNotEmpty = (value: object) => Object.keys(value).length > 0;
208
+ const log = (message: string, cbString: string) => {
209
+ const filteredObj = pickBy(isNotEmpty, binding.filters);
210
+
211
+ Log.info(`${message} configured for ${binding.event}`, prefix);
212
+ Log.info(filteredObj, prefix);
213
+ Log.debug(cbString, prefix);
214
+ };
215
+
216
+ function Validate(validateCallback: ValidateAction<T>): ValidateActionChain<T> {
217
+ if (registerAdmission) {
218
+ log("Validate Action", validateCallback.toString());
219
+
220
+ // Push the binding to the list of bindings for this capability as a new BindingAction
221
+ // with the callback function to preserve
222
+ bindings.push({
223
+ ...binding,
224
+ isValidate: true,
225
+ validateCallback,
226
+ });
227
+ }
228
+
229
+ return { Watch, Reconcile };
230
+ }
231
+
232
+ function Mutate(mutateCallback: MutateAction<T>): MutateActionChain<T> {
233
+ if (registerAdmission) {
234
+ log("Mutate Action", mutateCallback.toString());
235
+
236
+ // Push the binding to the list of bindings for this capability as a new BindingAction
237
+ // with the callback function to preserve
238
+ bindings.push({
239
+ ...binding,
240
+ isMutate: true,
241
+ mutateCallback,
242
+ });
243
+ }
244
+
245
+ // Now only allow adding actions to the same binding
246
+ return { Watch, Validate, Reconcile };
247
+ }
248
+
249
+ function Watch(watchCallback: WatchAction<T>) {
250
+ if (registerWatch) {
251
+ log("Watch Action", watchCallback.toString());
252
+
253
+ bindings.push({
254
+ ...binding,
255
+ isWatch: true,
256
+ watchCallback,
257
+ });
258
+ }
259
+ }
260
+
261
+ function Reconcile(watchCallback: WatchAction<T>) {
262
+ if (registerWatch) {
263
+ log("Reconcile Action", watchCallback.toString());
264
+
265
+ bindings.push({
266
+ ...binding,
267
+ isWatch: true,
268
+ isQueue: true,
269
+ watchCallback,
270
+ });
271
+ }
272
+ }
273
+
274
+ function InNamespace(...namespaces: string[]): BindingWithName<T> {
275
+ Log.debug(`Add namespaces filter ${namespaces}`, prefix);
276
+ binding.filters.namespaces.push(...namespaces);
277
+ return { ...commonChain, WithName };
278
+ }
279
+
280
+ function WithName(name: string): BindingFilter<T> {
281
+ Log.debug(`Add name filter ${name}`, prefix);
282
+ binding.filters.name = name;
283
+ return commonChain;
284
+ }
285
+
286
+ function WithLabel(key: string, value = ""): BindingFilter<T> {
287
+ Log.debug(`Add label filter ${key}=${value}`, prefix);
288
+ binding.filters.labels[key] = value;
289
+ return commonChain;
290
+ }
291
+
292
+ function WithAnnotation(key: string, value = ""): BindingFilter<T> {
293
+ Log.debug(`Add annotation filter ${key}=${value}`, prefix);
294
+ binding.filters.annotations[key] = value;
295
+ return commonChain;
296
+ }
297
+
298
+ function bindEvent(event: Event) {
299
+ binding.event = event;
300
+ return {
301
+ ...commonChain,
302
+ InNamespace,
303
+ WithName,
304
+ };
305
+ }
306
+
307
+ return {
308
+ IsCreatedOrUpdated: () => bindEvent(Event.CreateOrUpdate),
309
+ IsCreated: () => bindEvent(Event.Create),
310
+ IsUpdated: () => bindEvent(Event.Update),
311
+ IsDeleted: () => bindEvent(Event.Delete),
312
+ };
313
+ };
314
+ }