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,102 @@
1
+ // SPDX-License-Identifier: Apache-2.0
2
+ // SPDX-FileCopyrightText: 2023-Present The Pepr Authors
3
+
4
+ /* eslint-disable class-methods-use-this */
5
+
6
+ import { KubernetesObject } from "kubernetes-fluent-client";
7
+
8
+ import { clone } from "ramda";
9
+ import { Operation, AdmissionRequest } from "./k8s";
10
+ import { ValidateActionResponse } from "./types";
11
+
12
+ /**
13
+ * The RequestWrapper class provides methods to modify Kubernetes objects in the context
14
+ * of a mutating webhook request.
15
+ */
16
+ export class PeprValidateRequest<T extends KubernetesObject> {
17
+ Raw: T;
18
+
19
+ #input: AdmissionRequest<T>;
20
+
21
+ /**
22
+ * Provides access to the old resource in the request if available.
23
+ * @returns The old Kubernetes resource object or null if not available.
24
+ */
25
+ get OldResource() {
26
+ return this.#input.oldObject;
27
+ }
28
+
29
+ /**
30
+ * Provides access to the request object.
31
+ * @returns The request object containing the Kubernetes resource.
32
+ */
33
+ get Request() {
34
+ return this.#input;
35
+ }
36
+
37
+ /**
38
+ * Creates a new instance of the Action class.
39
+ * @param input - The request object containing the Kubernetes resource to modify.
40
+ */
41
+ constructor(input: AdmissionRequest<T>) {
42
+ this.#input = input;
43
+
44
+ // If this is a DELETE operation, use the oldObject instead
45
+ if (input.operation.toUpperCase() === Operation.DELETE) {
46
+ this.Raw = clone(input.oldObject as T);
47
+ } else {
48
+ // Otherwise, use the incoming object
49
+ this.Raw = clone(input.object);
50
+ }
51
+
52
+ if (!this.Raw) {
53
+ throw new Error("unable to load the request object into PeprRequest.Raw");
54
+ }
55
+ }
56
+
57
+ /**
58
+ * Check if a label exists on the Kubernetes resource.
59
+ *
60
+ * @param key the label key to check
61
+ * @returns
62
+ */
63
+ HasLabel = (key: string) => {
64
+ return this.Raw.metadata?.labels?.[key] !== undefined;
65
+ };
66
+
67
+ /**
68
+ * Check if an annotation exists on the Kubernetes resource.
69
+ *
70
+ * @param key the annotation key to check
71
+ * @returns
72
+ */
73
+ HasAnnotation = (key: string) => {
74
+ return this.Raw.metadata?.annotations?.[key] !== undefined;
75
+ };
76
+
77
+ /**
78
+ * Create a validation response that allows the request.
79
+ *
80
+ * @returns The validation response.
81
+ */
82
+ Approve = (): ValidateActionResponse => {
83
+ return {
84
+ allowed: true,
85
+ };
86
+ };
87
+
88
+ /**
89
+ * Create a validation response that denies the request.
90
+ *
91
+ * @param statusMessage Optional status message to return to the user.
92
+ * @param statusCode Optional status code to return to the user.
93
+ * @returns The validation response.
94
+ */
95
+ Deny = (statusMessage?: string, statusCode?: number): ValidateActionResponse => {
96
+ return {
97
+ allowed: false,
98
+ statusCode,
99
+ statusMessage,
100
+ };
101
+ };
102
+ }
@@ -0,0 +1,124 @@
1
+ // SPDX-License-Identifier: Apache-2.0
2
+ // SPDX-FileCopyrightText: 2023-Present The Pepr Authors
3
+ import { K8s, KubernetesObject, WatchCfg, WatchEvent } from "kubernetes-fluent-client";
4
+ import { WatchPhase } from "kubernetes-fluent-client/dist/fluent/types";
5
+ import { Capability } from "./capability";
6
+ import { filterNoMatchReason } from "./helpers";
7
+ import Log from "./logger";
8
+ import { Queue } from "./queue";
9
+ import { Binding, Event } from "./types";
10
+
11
+ // Watch configuration
12
+ const watchCfg: WatchCfg = {
13
+ retryMax: process.env.PEPR_RETRYMAX ? parseInt(process.env.PEPR_RETRYMAX, 10) : 5,
14
+ retryDelaySec: process.env.PEPR_RETRYDELAYSECONDS ? parseInt(process.env.PEPR_RETRYDELAYSECONDS, 10) : 5,
15
+ resyncIntervalSec: process.env.PEPR_RESYNCINTERVALSECONDS
16
+ ? parseInt(process.env.PEPR_RESYNCINTERVALSECONDS, 10)
17
+ : 300,
18
+ };
19
+
20
+ // Map the event to the watch phase
21
+ const eventToPhaseMap = {
22
+ [Event.Create]: [WatchPhase.Added],
23
+ [Event.Update]: [WatchPhase.Modified],
24
+ [Event.CreateOrUpdate]: [WatchPhase.Added, WatchPhase.Modified],
25
+ [Event.Delete]: [WatchPhase.Deleted],
26
+ [Event.Any]: [WatchPhase.Added, WatchPhase.Modified, WatchPhase.Deleted],
27
+ };
28
+
29
+ /**
30
+ * Entrypoint for setting up watches for all capabilities
31
+ *
32
+ * @param capabilities The capabilities to load watches for
33
+ */
34
+ export function setupWatch(capabilities: Capability[]) {
35
+ capabilities.map(capability =>
36
+ capability.bindings
37
+ .filter(binding => binding.isWatch)
38
+ .forEach(bindingElement => runBinding(bindingElement, capability.namespaces)),
39
+ );
40
+ }
41
+
42
+ /**
43
+ * Setup a watch for a binding
44
+ *
45
+ * @param binding the binding to watch
46
+ * @param capabilityNamespaces list of namespaces to filter on
47
+ */
48
+ async function runBinding(binding: Binding, capabilityNamespaces: string[]) {
49
+ // Get the phases to match, fallback to any
50
+ const phaseMatch: WatchPhase[] = eventToPhaseMap[binding.event] || eventToPhaseMap[Event.Any];
51
+
52
+ // The watch callback is run when an object is received or dequeued
53
+
54
+ Log.debug({ watchCfg }, "Effective WatchConfig");
55
+ const watchCallback = async (obj: KubernetesObject, type: WatchPhase) => {
56
+ // First, filter the object based on the phase
57
+ if (phaseMatch.includes(type)) {
58
+ try {
59
+ // Then, check if the object matches the filter
60
+ const filterMatch = filterNoMatchReason(binding, obj, capabilityNamespaces);
61
+ if (filterMatch === "") {
62
+ await binding.watchCallback?.(obj, type);
63
+ } else {
64
+ Log.debug(filterMatch);
65
+ }
66
+ } catch (e) {
67
+ // Errors in the watch callback should not crash the controller
68
+ Log.error(e, "Error executing watch callback");
69
+ }
70
+ }
71
+ };
72
+
73
+ const queue = new Queue();
74
+ queue.setReconcile(watchCallback);
75
+
76
+ // Setup the resource watch
77
+ const watcher = K8s(binding.model, binding.filters).Watch(async (obj, type) => {
78
+ Log.debug(obj, `Watch event ${type} received`);
79
+
80
+ // If the binding is a queue, enqueue the object
81
+ if (binding.isQueue) {
82
+ await queue.enqueue(obj, type);
83
+ } else {
84
+ // Otherwise, run the watch callback directly
85
+ await watchCallback(obj, type);
86
+ }
87
+ }, watchCfg);
88
+
89
+ // If failure continues, log and exit
90
+ watcher.events.on(WatchEvent.GIVE_UP, err => {
91
+ Log.error(err, "Watch failed after 5 attempts, giving up");
92
+ process.exit(1);
93
+ });
94
+
95
+ watcher.events.on(WatchEvent.CONNECT, url => logEvent(WatchEvent.CONNECT, url));
96
+
97
+ watcher.events.on(WatchEvent.DATA_ERROR, err => logEvent(WatchEvent.DATA_ERROR, err.message));
98
+ watcher.events.on(WatchEvent.RECONNECT, (err, retryCount) =>
99
+ logEvent(WatchEvent.RECONNECT, err ? `Reconnecting after ${retryCount} attempts` : ""),
100
+ );
101
+ watcher.events.on(WatchEvent.RECONNECT_PENDING, () => logEvent(WatchEvent.RECONNECT_PENDING));
102
+ watcher.events.on(WatchEvent.GIVE_UP, err => logEvent(WatchEvent.GIVE_UP, err.message));
103
+ watcher.events.on(WatchEvent.ABORT, err => logEvent(WatchEvent.ABORT, err.message));
104
+ watcher.events.on(WatchEvent.OLD_RESOURCE_VERSION, err => logEvent(WatchEvent.OLD_RESOURCE_VERSION, err));
105
+ watcher.events.on(WatchEvent.NETWORK_ERROR, err => logEvent(WatchEvent.NETWORK_ERROR, err.message));
106
+ watcher.events.on(WatchEvent.LIST_ERROR, err => logEvent(WatchEvent.LIST_ERROR, err.message));
107
+ watcher.events.on(WatchEvent.LIST, list => logEvent(WatchEvent.LIST, JSON.stringify(list, undefined, 2)));
108
+ // Start the watch
109
+ try {
110
+ await watcher.start();
111
+ } catch (err) {
112
+ Log.error(err, "Error starting watch");
113
+ process.exit(1);
114
+ }
115
+ }
116
+
117
+ export function logEvent(type: WatchEvent, message: string = "", obj?: KubernetesObject) {
118
+ const logMessage = `Watch event ${type} received${message ? `. ${message}.` : "."}`;
119
+ if (obj) {
120
+ Log.debug(obj, logMessage);
121
+ } else {
122
+ Log.debug(logMessage);
123
+ }
124
+ }
package/src/lib.ts ADDED
@@ -0,0 +1,27 @@
1
+ import { K8s, RegisterKind, kind as a, fetch, fetchStatus, kind } from "kubernetes-fluent-client";
2
+ import * as R from "ramda";
3
+
4
+ import { Capability } from "./lib/capability";
5
+ import Log from "./lib/logger";
6
+ import { PeprModule } from "./lib/module";
7
+ import { PeprMutateRequest } from "./lib/mutate-request";
8
+ import * as PeprUtils from "./lib/utils";
9
+ import { PeprValidateRequest } from "./lib/validate-request";
10
+ import * as sdk from "./sdk/sdk";
11
+
12
+ export {
13
+ Capability,
14
+ K8s,
15
+ Log,
16
+ PeprModule,
17
+ PeprMutateRequest,
18
+ PeprUtils,
19
+ PeprValidateRequest,
20
+ R,
21
+ RegisterKind,
22
+ a,
23
+ fetch,
24
+ fetchStatus,
25
+ kind,
26
+ sdk,
27
+ };
@@ -0,0 +1,75 @@
1
+ #!/usr/bin/env node
2
+
3
+ // SPDX-License-Identifier: Apache-2.0
4
+ // SPDX-FileCopyrightText: 2023-Present The Pepr Authors
5
+
6
+ import { fork } from "child_process";
7
+ import crypto from "crypto";
8
+ import fs from "fs";
9
+ import { gunzipSync } from "zlib";
10
+ import { K8s, kind } from "kubernetes-fluent-client";
11
+ import Log from "../lib/logger";
12
+ import { packageJSON } from "../templates/data.json";
13
+ import { peprStoreCRD } from "../lib/assets/store";
14
+ import { validateHash } from "../lib/helpers";
15
+ const { version } = packageJSON;
16
+
17
+ function runModule(expectedHash: string) {
18
+ const gzPath = `/app/load/module-${expectedHash}.js.gz`;
19
+ const jsPath = `/app/module-${expectedHash}.js`;
20
+
21
+ // Set the log level
22
+ Log.level = "info";
23
+
24
+ // Check if the path is a valid file
25
+ if (!fs.existsSync(gzPath)) {
26
+ throw new Error(`File not found: ${gzPath}`);
27
+ }
28
+
29
+ try {
30
+ Log.info(`Loading module ${gzPath}`);
31
+
32
+ // Extract the code from the file
33
+ const codeGZ = fs.readFileSync(gzPath);
34
+ const code = gunzipSync(codeGZ);
35
+
36
+ // Get the hash of the extracted code
37
+ const actualHash = crypto.createHash("sha256").update(code).digest("hex");
38
+
39
+ // If the hash doesn't match, exit
40
+ // This is a timing safe comparison to prevent timing attacks
41
+ // https://en.wikipedia.org/wiki/Timing_attack
42
+ if (!crypto.timingSafeEqual(Buffer.from(expectedHash, "hex"), Buffer.from(actualHash, "hex"))) {
43
+ throw new Error(`File hash does not match, expected ${expectedHash} but got ${actualHash}`);
44
+ }
45
+
46
+ Log.info(`File hash matches, running module`);
47
+
48
+ // Write the code to a file
49
+ fs.writeFileSync(jsPath, code);
50
+
51
+ // Run the module
52
+ fork(jsPath);
53
+ } catch (e) {
54
+ throw new Error(`Failed to decompress module: ${e}`);
55
+ }
56
+ }
57
+
58
+ Log.info(`Pepr Controller (v${version})`);
59
+
60
+ const hash = process.argv[2];
61
+
62
+ const startup = async () => {
63
+ try {
64
+ Log.info("Applying the Pepr Store CRD if it doesn't exist");
65
+ await K8s(kind.CustomResourceDefinition).Apply(peprStoreCRD, { force: true });
66
+
67
+ validateHash(hash);
68
+ runModule(hash);
69
+ } catch (err) {
70
+ Log.error(err, `Error starting Pepr Store CRD`);
71
+ process.exit(1);
72
+ }
73
+ };
74
+
75
+ startup().catch(err => Log.error(err, `Error starting Pepr Controller`));
package/src/sdk/sdk.ts ADDED
@@ -0,0 +1,116 @@
1
+ // SPDX-License-Identifier: Apache-2.0
2
+ // SPDX-FileCopyrightText: 2023-Present The Pepr Authors
3
+
4
+ import { PeprValidateRequest } from "../lib/validate-request";
5
+ import { PeprMutateRequest } from "../lib/mutate-request";
6
+ import { a } from "../lib";
7
+ import { V1OwnerReference } from "@kubernetes/client-node";
8
+ import { GenericKind } from "kubernetes-fluent-client";
9
+ import { K8s, kind } from "kubernetes-fluent-client";
10
+ import Log from "../lib/logger";
11
+
12
+ /**
13
+ * Returns all containers in a pod
14
+ * @param request the request/pod to get the containers from
15
+ * @param containerType the type of container to get
16
+ * @returns the list of containers in the pod
17
+ */
18
+ export function containers(
19
+ request: PeprValidateRequest<a.Pod> | PeprMutateRequest<a.Pod>,
20
+ containerType?: "containers" | "initContainers" | "ephemeralContainers",
21
+ ) {
22
+ const containers = request.Raw.spec?.containers || [];
23
+ const initContainers = request.Raw.spec?.initContainers || [];
24
+ const ephemeralContainers = request.Raw.spec?.ephemeralContainers || [];
25
+
26
+ if (containerType === "containers") {
27
+ return containers;
28
+ }
29
+ if (containerType === "initContainers") {
30
+ return initContainers;
31
+ }
32
+ if (containerType === "ephemeralContainers") {
33
+ return ephemeralContainers;
34
+ }
35
+ return [...containers, ...initContainers, ...ephemeralContainers];
36
+ }
37
+
38
+ /**
39
+ * Write a K8s event for a CRD
40
+ *
41
+ * @param cr The custom resource to write the event for
42
+ * @param event The event to write, should contain a human-readable message for the event
43
+ * @param eventType The type of event to write, for example "Warning"
44
+ * @param eventReason The reason for the event, for example "ReconciliationFailed"
45
+ * @param reportingComponent The component that is reporting the event, for example "uds.dev/operator"
46
+ * @param reportingInstance The instance of the component that is reporting the event, for example process.env.HOSTNAME
47
+ */
48
+ export async function writeEvent(
49
+ cr: GenericKind,
50
+ event: Partial<kind.CoreEvent>,
51
+ eventType: string,
52
+ eventReason: string,
53
+ reportingComponent: string,
54
+ reportingInstance: string,
55
+ ) {
56
+ Log.debug(cr.metadata, `Writing event: ${event.message}`);
57
+
58
+ await K8s(kind.CoreEvent).Create({
59
+ type: eventType,
60
+ reason: eventReason,
61
+ ...event,
62
+ // Fixed values
63
+ metadata: {
64
+ namespace: cr.metadata!.namespace,
65
+ generateName: cr.metadata!.name,
66
+ },
67
+ involvedObject: {
68
+ apiVersion: cr.apiVersion,
69
+ kind: cr.kind,
70
+ name: cr.metadata!.name,
71
+ namespace: cr.metadata!.namespace,
72
+ uid: cr.metadata!.uid,
73
+ },
74
+ firstTimestamp: new Date(),
75
+ reportingComponent: reportingComponent,
76
+ reportingInstance: reportingInstance,
77
+ });
78
+ }
79
+
80
+ /**
81
+ * Get the owner reference for a custom resource
82
+ * @param cr the custom resource to get the owner reference for
83
+ * @returns the owner reference for the custom resource
84
+ */
85
+ export function getOwnerRefFrom(cr: GenericKind): V1OwnerReference[] {
86
+ const { name, uid } = cr.metadata!;
87
+
88
+ return [
89
+ {
90
+ apiVersion: cr.apiVersion!,
91
+ kind: cr.kind!,
92
+ uid: uid!,
93
+ name: name!,
94
+ },
95
+ ];
96
+ }
97
+
98
+ /**
99
+ * Sanitize a resource name to make it a valid Kubernetes resource name.
100
+ *
101
+ * @param name the name of the resource to sanitize
102
+ * @returns the sanitized resource name
103
+ */
104
+ export function sanitizeResourceName(name: string) {
105
+ return (
106
+ name
107
+ // The name must be lowercase
108
+ .toLowerCase()
109
+ // Replace sequences of non-alphanumeric characters with a single '-'
110
+ .replace(/[^a-z0-9]+/g, "-")
111
+ // Truncate the name to 250 characters
112
+ .slice(0, 250)
113
+ // Remove leading and trailing non-letter characters
114
+ .replace(/^[^a-z]+|[^a-z]+$/g, "")
115
+ );
116
+ }
@@ -0,0 +1,18 @@
1
+ {
2
+ "env": {
3
+ "browser": false,
4
+ "es2021": true
5
+ },
6
+ "extends": ["eslint:recommended", "plugin:@typescript-eslint/recommended"],
7
+ "parser": "@typescript-eslint/parser",
8
+ "parserOptions": {
9
+ "project": ["./tsconfig.json"],
10
+ "ecmaVersion": 2022
11
+ },
12
+ "plugins": ["@typescript-eslint"],
13
+ "ignorePatterns": ["node_modules", "dist"],
14
+ "root": true,
15
+ "rules": {
16
+ "@typescript-eslint/no-floating-promises": ["error"]
17
+ }
18
+ }
@@ -0,0 +1,13 @@
1
+ {
2
+ "arrowParens": "avoid",
3
+ "bracketSameLine": false,
4
+ "bracketSpacing": true,
5
+ "embeddedLanguageFormatting": "auto",
6
+ "insertPragma": false,
7
+ "printWidth": 80,
8
+ "quoteProps": "as-needed",
9
+ "requirePragma": false,
10
+ "semi": true,
11
+ "tabWidth": 2,
12
+ "useTabs": false
13
+ }
@@ -0,0 +1,21 @@
1
+ # Pepr Module
2
+
3
+ This is a Pepr Module. [Pepr](https://github.com/defenseunicorns/pepr) is a type-safe Kubernetes middleware system.
4
+
5
+ The `capabilities` directory contains all the capabilities for this module. By default,
6
+ a capability is a single typescript file in the format of `capability-name.ts` that is
7
+ imported in the root `pepr.ts` file as `import { HelloPepr } from "./capabilities/hello-pepr";`.
8
+ Because this is typescript, you can organize this however you choose, e.g. creating a sub-folder
9
+ per-capability or common logic in shared files or folders.
10
+
11
+ Example Structure:
12
+
13
+ ```
14
+ Module Root
15
+ ├── package.json
16
+ ├── pepr.ts
17
+ └── capabilities
18
+ ├── example-one.ts
19
+ ├── example-three.ts
20
+ └── example-two.ts
21
+ ```
@@ -0,0 +1,160 @@
1
+ [
2
+ {
3
+ "apiVersion": "v1",
4
+ "kind": "Namespace",
5
+ "metadata": {
6
+ "name": "pepr-demo",
7
+ "labels": {
8
+ "keep-me": "please",
9
+ "remove-me": "please"
10
+ }
11
+ }
12
+ },
13
+ {
14
+ "apiVersion": "v1",
15
+ "kind": "Namespace",
16
+ "metadata": {
17
+ "name": "pepr-demo-2"
18
+ }
19
+ },
20
+ {
21
+ "apiVersion": "v1",
22
+ "kind": "Secret",
23
+ "metadata": {
24
+ "name": "secret-1",
25
+ "namespace": "pepr-demo"
26
+ },
27
+ "data": {
28
+ "example": "dW5pY29ybiBtYWdpYw==",
29
+ "binary-data": "iCZQUg8xYucNUqD+8lyl2YcKjYYygvTtiDSEV9b9WKUkxSSLFJTgIWMJ9GcFFYs4T9JCdda51u74jfq8yHzRuEASl60EdTS/NfWgIIFTGqcNRfqMw+vgpyTMmCyJVaJEDFq6AA==",
30
+ "ascii-with-white-space": "VGhpcyBpcyBzb21lIHJhbmRvbSB0ZXh0OgoKICAgIC0gd2l0aCBsaW5lIGJyZWFrcwogICAgLSBhbmQgdGFicw=="
31
+ }
32
+ },
33
+ {
34
+ "apiVersion": "v1",
35
+ "kind": "ConfigMap",
36
+ "metadata": {
37
+ "name": "example-1",
38
+ "namespace": "pepr-demo"
39
+ },
40
+ "data": {
41
+ "key": "ex-1-val"
42
+ }
43
+ },
44
+ {
45
+ "apiVersion": "v1",
46
+ "kind": "ConfigMap",
47
+ "metadata": {
48
+ "name": "example-2",
49
+ "namespace": "pepr-demo"
50
+ },
51
+ "data": {
52
+ "key": "ex-2-val"
53
+ }
54
+ },
55
+ {
56
+ "apiVersion": "v1",
57
+ "kind": "ConfigMap",
58
+ "metadata": {
59
+ "name": "example-evil-cm",
60
+ "namespace": "pepr-demo",
61
+ "annotations": {
62
+ "evil": "true"
63
+ }
64
+ },
65
+ "data": {
66
+ "key": "ex-evil-cm-val"
67
+ }
68
+ },
69
+ {
70
+ "apiVersion": "v1",
71
+ "kind": "ConfigMap",
72
+ "metadata": {
73
+ "name": "example-3",
74
+ "namespace": "pepr-demo",
75
+ "labels": {
76
+ "change": "by-label"
77
+ }
78
+ },
79
+ "data": {
80
+ "key": "ex-3-val"
81
+ }
82
+ },
83
+ {
84
+ "apiVersion": "v1",
85
+ "kind": "ConfigMap",
86
+ "metadata": {
87
+ "name": "example-4",
88
+ "namespace": "pepr-demo"
89
+ },
90
+ "data": {
91
+ "key": "ex-4-val"
92
+ }
93
+ },
94
+ {
95
+ "apiVersion": "v1",
96
+ "kind": "ConfigMap",
97
+ "metadata": {
98
+ "name": "example-4a",
99
+ "namespace": "pepr-demo-2"
100
+ },
101
+ "data": {
102
+ "key": "ex-4-val"
103
+ }
104
+ },
105
+ {
106
+ "apiVersion": "v1",
107
+ "kind": "ConfigMap",
108
+ "metadata": {
109
+ "name": "example-5",
110
+ "namespace": "pepr-demo",
111
+ "labels": {
112
+ "chuck-norris": "test"
113
+ }
114
+ },
115
+ "data": {
116
+ "key": "ex-5-val"
117
+ }
118
+ },
119
+ {
120
+ "apiVersion": "apiextensions.k8s.io/v1",
121
+ "kind": "CustomResourceDefinition",
122
+ "metadata": {
123
+ "name": "unicorns.pepr.dev"
124
+ },
125
+ "spec": {
126
+ "group": "pepr.dev",
127
+ "versions": [
128
+ {
129
+ "name": "v1",
130
+ "served": true,
131
+ "storage": true,
132
+ "schema": {
133
+ "openAPIV3Schema": {
134
+ "type": "object",
135
+ "properties": {
136
+ "spec": {
137
+ "type": "object",
138
+ "properties": {
139
+ "message": {
140
+ "type": "string"
141
+ },
142
+ "counter": {
143
+ "type": "number"
144
+ }
145
+ }
146
+ }
147
+ }
148
+ }
149
+ }
150
+ }
151
+ ],
152
+ "scope": "Namespaced",
153
+ "names": {
154
+ "plural": "unicorns",
155
+ "singular": "unicorn",
156
+ "kind": "Unicorn"
157
+ }
158
+ }
159
+ }
160
+ ]