pepr 0.13.3 → 0.14.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 (106) hide show
  1. package/README.md +23 -4
  2. package/dist/cli.js +455 -266
  3. package/dist/controller.js +1 -1
  4. package/dist/lib/assets/deploy.d.ts.map +1 -1
  5. package/dist/lib/assets/destroy.d.ts +2 -0
  6. package/dist/lib/assets/destroy.d.ts.map +1 -0
  7. package/dist/lib/assets/index.d.ts +6 -5
  8. package/dist/lib/assets/index.d.ts.map +1 -1
  9. package/dist/lib/assets/networking.d.ts +6 -5
  10. package/dist/lib/assets/networking.d.ts.map +1 -1
  11. package/dist/lib/assets/pods.d.ts +84 -4
  12. package/dist/lib/assets/pods.d.ts.map +1 -1
  13. package/dist/lib/assets/rbac.d.ts +6 -4
  14. package/dist/lib/assets/rbac.d.ts.map +1 -1
  15. package/dist/lib/assets/store.d.ts +7 -0
  16. package/dist/lib/assets/store.d.ts.map +1 -0
  17. package/dist/lib/assets/webhooks.d.ts +2 -2
  18. package/dist/lib/assets/webhooks.d.ts.map +1 -1
  19. package/dist/lib/assets/yaml.d.ts.map +1 -1
  20. package/dist/lib/capability.d.ts +21 -4
  21. package/dist/lib/capability.d.ts.map +1 -1
  22. package/dist/lib/controller/index.d.ts +10 -0
  23. package/dist/lib/controller/index.d.ts.map +1 -0
  24. package/dist/lib/controller/store.d.ts +7 -0
  25. package/dist/lib/controller/store.d.ts.map +1 -0
  26. package/dist/lib/filter.d.ts +2 -2
  27. package/dist/lib/filter.d.ts.map +1 -1
  28. package/dist/lib/{k8s/types.d.ts → k8s.d.ts} +14 -25
  29. package/dist/lib/k8s.d.ts.map +1 -0
  30. package/dist/lib/metrics.d.ts +12 -12
  31. package/dist/lib/metrics.d.ts.map +1 -1
  32. package/dist/lib/module.d.ts +25 -4
  33. package/dist/lib/module.d.ts.map +1 -1
  34. package/dist/lib/mutate-processor.d.ts +3 -3
  35. package/dist/lib/mutate-processor.d.ts.map +1 -1
  36. package/dist/lib/mutate-request.d.ts +11 -10
  37. package/dist/lib/mutate-request.d.ts.map +1 -1
  38. package/dist/lib/storage.d.ts +56 -0
  39. package/dist/lib/storage.d.ts.map +1 -0
  40. package/dist/lib/tls.d.ts.map +1 -0
  41. package/dist/lib/types.d.ts +28 -48
  42. package/dist/lib/types.d.ts.map +1 -1
  43. package/dist/lib/validate-processor.d.ts +2 -2
  44. package/dist/lib/validate-processor.d.ts.map +1 -1
  45. package/dist/lib/validate-request.d.ts +9 -8
  46. package/dist/lib/validate-request.d.ts.map +1 -1
  47. package/dist/lib/watch-processor.d.ts +3 -0
  48. package/dist/lib/watch-processor.d.ts.map +1 -0
  49. package/dist/lib.d.ts +3 -7
  50. package/dist/lib.d.ts.map +1 -1
  51. package/dist/lib.js +484 -807
  52. package/dist/lib.js.map +4 -4
  53. package/package.json +16 -20
  54. package/src/lib/assets/deploy.ts +69 -127
  55. package/src/lib/assets/destroy.ts +33 -0
  56. package/src/lib/assets/index.ts +8 -14
  57. package/src/lib/assets/networking.ts +28 -5
  58. package/src/lib/assets/pods.ts +130 -11
  59. package/src/lib/assets/rbac.ts +42 -4
  60. package/src/lib/assets/store.ts +49 -0
  61. package/src/lib/assets/webhooks.ts +2 -2
  62. package/src/lib/assets/yaml.ts +13 -3
  63. package/src/lib/capability.ts +69 -14
  64. package/src/lib/{controller.ts → controller/index.ts} +25 -23
  65. package/src/lib/controller/store.ts +197 -0
  66. package/src/lib/filter.ts +2 -2
  67. package/src/lib/{k8s/types.ts → k8s.ts} +15 -26
  68. package/src/lib/metrics.ts +22 -38
  69. package/src/lib/module.ts +47 -10
  70. package/src/lib/mutate-processor.ts +6 -6
  71. package/src/lib/mutate-request.ts +18 -26
  72. package/src/lib/storage.ts +128 -0
  73. package/src/lib/types.ts +30 -53
  74. package/src/lib/validate-processor.ts +5 -4
  75. package/src/lib/validate-request.ts +15 -19
  76. package/src/lib/watch-processor.ts +55 -0
  77. package/src/lib.ts +4 -8
  78. package/src/templates/.eslintrc.template.json +18 -0
  79. package/src/templates/capabilities/hello-pepr.ts +54 -5
  80. package/src/templates/package.json +1 -0
  81. package/dist/lib/controller.d.ts +0 -10
  82. package/dist/lib/controller.d.ts.map +0 -1
  83. package/dist/lib/fetch.d.ts +0 -23
  84. package/dist/lib/fetch.d.ts.map +0 -1
  85. package/dist/lib/k8s/index.d.ts +0 -7
  86. package/dist/lib/k8s/index.d.ts.map +0 -1
  87. package/dist/lib/k8s/kinds.d.ts +0 -12
  88. package/dist/lib/k8s/kinds.d.ts.map +0 -1
  89. package/dist/lib/k8s/tls.d.ts.map +0 -1
  90. package/dist/lib/k8s/types.d.ts.map +0 -1
  91. package/dist/lib/k8s/upstream.d.ts +0 -4
  92. package/dist/lib/k8s/upstream.d.ts.map +0 -1
  93. package/jest.config.json +0 -4
  94. package/journey/before.ts +0 -21
  95. package/journey/k8s.ts +0 -100
  96. package/journey/pepr-build.ts +0 -69
  97. package/journey/pepr-deploy.ts +0 -174
  98. package/journey/pepr-dev.ts +0 -155
  99. package/journey/pepr-format.ts +0 -13
  100. package/journey/pepr-init.ts +0 -12
  101. package/src/lib/fetch.ts +0 -76
  102. package/src/lib/k8s/index.ts +0 -14
  103. package/src/lib/k8s/kinds.ts +0 -531
  104. package/src/lib/k8s/upstream.ts +0 -53
  105. /package/dist/lib/{k8s/tls.d.ts → tls.d.ts} +0 -0
  106. /package/src/lib/{k8s/tls.ts → tls.ts} +0 -0
@@ -51,29 +51,19 @@ export class MetricsCollector {
51
51
  this.addSummary(this.#metricNames.validate, "Validation operation summary");
52
52
  }
53
53
 
54
- #getMetricName(name: string) {
55
- return `${this.#prefix}_${name}`;
56
- }
54
+ #getMetricName = (name: string) => `${this.#prefix}_${name}`;
57
55
 
58
- #addMetric<T extends Counter<string> | Summary<string>>(
56
+ #addMetric = <T extends Counter<string> | Summary<string>>(
59
57
  collection: Map<string, T>,
60
58
  MetricType: new (args: MetricArgs) => T,
61
59
  name: string,
62
60
  help: string,
63
- ) {
61
+ ) => {
64
62
  if (collection.has(this.#getMetricName(name))) {
65
63
  Log.debug(`Metric for ${name} already exists`, loggingPrefix);
66
64
  return;
67
65
  }
68
66
 
69
- // Bind public methods
70
- this.incCounter = this.incCounter.bind(this);
71
- this.error = this.error.bind(this);
72
- this.alert = this.alert.bind(this);
73
- this.observeStart = this.observeStart.bind(this);
74
- this.observeEnd = this.observeEnd.bind(this);
75
- this.getMetrics = this.getMetrics.bind(this);
76
-
77
67
  const metric = new MetricType({
78
68
  name: this.#getMetricName(name),
79
69
  help,
@@ -81,56 +71,50 @@ export class MetricsCollector {
81
71
  });
82
72
 
83
73
  collection.set(this.#getMetricName(name), metric);
84
- }
74
+ };
85
75
 
86
- addCounter(name: string, help: string) {
76
+ addCounter = (name: string, help: string) => {
87
77
  this.#addMetric(this.#counters, promClient.Counter, name, help);
88
- }
78
+ };
89
79
 
90
- addSummary(name: string, help: string) {
80
+ addSummary = (name: string, help: string) => {
91
81
  this.#addMetric(this.#summaries, promClient.Summary, name, help);
92
- }
82
+ };
93
83
 
94
- incCounter(name: string) {
84
+ incCounter = (name: string) => {
95
85
  this.#counters.get(this.#getMetricName(name))?.inc();
96
- }
86
+ };
97
87
 
98
88
  /**
99
89
  * Increments the error counter.
100
90
  */
101
- error() {
102
- this.incCounter(this.#metricNames.errors);
103
- }
91
+ error = () => this.incCounter(this.#metricNames.errors);
104
92
 
105
93
  /**
106
94
  * Increments the alerts counter.
107
95
  */
108
- alert() {
109
- this.incCounter(this.#metricNames.alerts);
110
- }
111
-
112
- /**
113
- * Returns the current timestamp from performance.now() method. Useful for start timing an operation.
114
- * @returns The timestamp.
115
- */
116
- observeStart() {
117
- return performance.now();
118
- }
96
+ alert = () => this.incCounter(this.#metricNames.alerts);
119
97
 
120
98
  /**
121
99
  * Observes the duration since the provided start time and updates the summary.
122
100
  * @param startTime - The start time.
123
101
  * @param name - The metrics summary to increment.
124
102
  */
125
- observeEnd(startTime: number, name: string = this.#metricNames.mutate) {
103
+ observeEnd = (startTime: number, name: string = this.#metricNames.mutate) => {
126
104
  this.#summaries.get(this.#getMetricName(name))?.observe(performance.now() - startTime);
127
- }
105
+ };
128
106
 
129
107
  /**
130
108
  * Fetches the current metrics from the registry.
131
109
  * @returns The metrics.
132
110
  */
133
- async getMetrics() {
134
- return this.#registry.metrics();
111
+ getMetrics = () => this.#registry.metrics();
112
+
113
+ /**
114
+ * Returns the current timestamp from performance.now() method. Useful for start timing an operation.
115
+ * @returns The timestamp.
116
+ */
117
+ static observeStart() {
118
+ return performance.now();
135
119
  }
136
120
  }
package/src/lib/module.ts CHANGED
@@ -6,8 +6,29 @@ import { clone } from "ramda";
6
6
  import { Capability } from "./capability";
7
7
  import { Controller } from "./controller";
8
8
  import { ValidateError } from "./errors";
9
- import { MutateResponse, Request, ValidateResponse } from "./k8s/types";
10
- import { CapabilityExport, ModuleConfig } from "./types";
9
+ import { AdmissionRequest, MutateResponse, ValidateResponse, WebhookIgnore } from "./k8s";
10
+ import { CapabilityExport } from "./types";
11
+ import { setupWatch } from "./watch-processor";
12
+
13
+ /** Global configuration for the Pepr runtime. */
14
+ export type ModuleConfig = {
15
+ /** The user-defined name for the module */
16
+ name: string;
17
+ /** The Pepr version this module uses */
18
+ peprVersion?: string;
19
+ /** The user-defined version of the module */
20
+ appVersion?: string;
21
+ /** A unique identifier for this Pepr module. This is automatically generated by Pepr. */
22
+ uuid: string;
23
+ /** A description of the Pepr module and what it does. */
24
+ description?: string;
25
+ /** Reject K8s resource AdmissionRequests on error. */
26
+ onError?: string;
27
+ /** Configure global exclusions that will never be processed by Pepr. */
28
+ alwaysIgnore: WebhookIgnore;
29
+ /** Define the log level for the in-cluster controllers */
30
+ logLevel?: string;
31
+ };
11
32
 
12
33
  export type PackageJSON = {
13
34
  description: string;
@@ -18,12 +39,20 @@ export type PeprModuleOptions = {
18
39
  deferStart?: boolean;
19
40
 
20
41
  /** A user-defined callback to pre-process or intercept a Pepr request from K8s immediately before it is processed */
21
- beforeHook?: (req: Request) => void;
42
+ beforeHook?: (req: AdmissionRequest) => void;
22
43
 
23
44
  /** A user-defined callback to post-process or intercept a Pepr response just before it is returned to K8s */
24
45
  afterHook?: (res: MutateResponse | ValidateResponse) => void;
25
46
  };
26
47
 
48
+ // Track if this is a watch mode controller
49
+ export const isWatchMode = () => process.env.PEPR_WATCH_MODE === "true";
50
+
51
+ // Track if Pepr is running in build mode
52
+ export const isBuildMode = () => process.env.PEPR_MODE === "build";
53
+
54
+ export const isDevMode = () => process.env.PEPR_MODE === "dev";
55
+
27
56
  export class PeprModule {
28
57
  #controller!: Controller;
29
58
 
@@ -41,11 +70,13 @@ export class PeprModule {
41
70
  // Need to validate at runtime since TS gets sad about parsing the package.json
42
71
  ValidateError(config.onError);
43
72
 
44
- // Bind public methods
45
- this.start = this.start.bind(this);
46
-
47
73
  // Handle build mode
48
- if (process.env.PEPR_MODE === "build" && process.send) {
74
+ if (isBuildMode()) {
75
+ // Fail if process.send is not defined
76
+ if (!process.send) {
77
+ throw new Error("process.send is not defined");
78
+ }
79
+
49
80
  const exportedCapabilities: CapabilityExport[] = [];
50
81
 
51
82
  // Send capability map to parent process
@@ -59,12 +90,18 @@ export class PeprModule {
59
90
  });
60
91
  }
61
92
 
93
+ // Send the capabilities back to the parent process
62
94
  process.send(exportedCapabilities);
63
95
 
64
96
  return;
65
97
  }
66
98
 
67
- this.#controller = new Controller(config, capabilities, opts.beforeHook, opts.afterHook);
99
+ this.#controller = new Controller(config, capabilities, opts.beforeHook, opts.afterHook, () => {
100
+ // Wait for the controller to be ready before setting up watches
101
+ if (isWatchMode() || isDevMode()) {
102
+ setupWatch(capabilities);
103
+ }
104
+ });
68
105
 
69
106
  // Stop processing if deferStart is set to true
70
107
  if (opts.deferStart) {
@@ -80,7 +117,7 @@ export class PeprModule {
80
117
  *
81
118
  * @param port
82
119
  */
83
- start(port = 3000) {
120
+ start = (port = 3000) => {
84
121
  this.#controller.startServer(port);
85
- }
122
+ };
86
123
  }
@@ -2,21 +2,21 @@
2
2
  // SPDX-FileCopyrightText: 2023-Present The Pepr Authors
3
3
 
4
4
  import jsonPatch from "fast-json-patch";
5
+ import { kind } from "kubernetes-fluent-client";
5
6
 
6
7
  import { Capability } from "./capability";
7
8
  import { Errors } from "./errors";
8
9
  import { shouldSkipRequest } from "./filter";
9
- import { MutateResponse, Request } from "./k8s/types";
10
- import { Secret } from "./k8s/upstream";
10
+ import { MutateResponse, AdmissionRequest } from "./k8s";
11
11
  import Log from "./logger";
12
+ import { ModuleConfig } from "./module";
12
13
  import { PeprMutateRequest } from "./mutate-request";
13
- import { ModuleConfig } from "./types";
14
14
  import { base64Encode, convertFromBase64Map, convertToBase64Map } from "./utils";
15
15
 
16
16
  export async function mutateProcessor(
17
17
  config: ModuleConfig,
18
18
  capabilities: Capability[],
19
- req: Request,
19
+ req: AdmissionRequest,
20
20
  reqMetadata: Record<string, string>,
21
21
  ): Promise<MutateResponse> {
22
22
  const wrapped = new PeprMutateRequest(req);
@@ -35,7 +35,7 @@ export async function mutateProcessor(
35
35
  // If the resource is a secret, decode the data
36
36
  const isSecret = req.kind.version == "v1" && req.kind.kind == "Secret";
37
37
  if (isSecret) {
38
- skipDecode = convertFromBase64Map(wrapped.Raw as unknown as Secret);
38
+ skipDecode = convertFromBase64Map(wrapped.Raw as unknown as kind.Secret);
39
39
  }
40
40
 
41
41
  Log.info(reqMetadata, `Processing request`);
@@ -124,7 +124,7 @@ export async function mutateProcessor(
124
124
 
125
125
  // Post-process the Secret requests to convert it back to the original format
126
126
  if (isSecret) {
127
- convertToBase64Map(transformed as unknown as Secret, skipDecode);
127
+ convertToBase64Map(transformed as unknown as kind.Secret, skipDecode);
128
128
  }
129
129
 
130
130
  // Compare the original request to the modified request to get the patches
@@ -1,9 +1,10 @@
1
1
  // SPDX-License-Identifier: Apache-2.0
2
2
  // SPDX-FileCopyrightText: 2023-Present The Pepr Authors
3
3
 
4
+ import { KubernetesObject } from "kubernetes-fluent-client";
4
5
  import { clone, mergeDeepRight } from "ramda";
5
6
 
6
- import { KubernetesObject, Operation, Request } from "./k8s/types";
7
+ import { AdmissionRequest, Operation } from "./k8s";
7
8
  import { DeepPartial } from "./types";
8
9
 
9
10
  /**
@@ -13,7 +14,7 @@ import { DeepPartial } from "./types";
13
14
  export class PeprMutateRequest<T extends KubernetesObject> {
14
15
  Raw: T;
15
16
 
16
- #input: Request<T>;
17
+ #input: AdmissionRequest<T>;
17
18
 
18
19
  get PermitSideEffects() {
19
20
  return !this.#input.dryRun;
@@ -47,18 +48,9 @@ export class PeprMutateRequest<T extends KubernetesObject> {
47
48
  * Creates a new instance of the action class.
48
49
  * @param input - The request object containing the Kubernetes resource to modify.
49
50
  */
50
- constructor(input: Request<T>) {
51
+ constructor(input: AdmissionRequest<T>) {
51
52
  this.#input = input;
52
53
 
53
- // Bind public methods
54
- this.Merge = this.Merge.bind(this);
55
- this.SetLabel = this.SetLabel.bind(this);
56
- this.SetAnnotation = this.SetAnnotation.bind(this);
57
- this.RemoveLabel = this.RemoveLabel.bind(this);
58
- this.RemoveAnnotation = this.RemoveAnnotation.bind(this);
59
- this.HasLabel = this.HasLabel.bind(this);
60
- this.HasAnnotation = this.HasAnnotation.bind(this);
61
-
62
54
  // If this is a DELETE operation, use the oldObject instead
63
55
  if (input.operation.toUpperCase() === Operation.DELETE) {
64
56
  this.Raw = clone(input.oldObject as T);
@@ -77,9 +69,9 @@ export class PeprMutateRequest<T extends KubernetesObject> {
77
69
  *
78
70
  * @param obj - The object to merge with the current resource.
79
71
  */
80
- Merge(obj: DeepPartial<T>) {
72
+ Merge = (obj: DeepPartial<T>) => {
81
73
  this.Raw = mergeDeepRight(this.Raw, obj) as unknown as T;
82
- }
74
+ };
83
75
 
84
76
  /**
85
77
  * Updates a label on the Kubernetes resource.
@@ -87,7 +79,7 @@ export class PeprMutateRequest<T extends KubernetesObject> {
87
79
  * @param value - The value of the label.
88
80
  * @returns The current action instance for method chaining.
89
81
  */
90
- SetLabel(key: string, value: string) {
82
+ SetLabel = (key: string, value: string) => {
91
83
  const ref = this.Raw;
92
84
 
93
85
  ref.metadata = ref.metadata ?? {};
@@ -95,7 +87,7 @@ export class PeprMutateRequest<T extends KubernetesObject> {
95
87
  ref.metadata.labels[key] = value;
96
88
 
97
89
  return this;
98
- }
90
+ };
99
91
 
100
92
  /**
101
93
  * Updates an annotation on the Kubernetes resource.
@@ -103,7 +95,7 @@ export class PeprMutateRequest<T extends KubernetesObject> {
103
95
  * @param value - The value of the annotation.
104
96
  * @returns The current action instance for method chaining.
105
97
  */
106
- SetAnnotation(key: string, value: string) {
98
+ SetAnnotation = (key: string, value: string) => {
107
99
  const ref = this.Raw;
108
100
 
109
101
  ref.metadata = ref.metadata ?? {};
@@ -111,33 +103,33 @@ export class PeprMutateRequest<T extends KubernetesObject> {
111
103
  ref.metadata.annotations[key] = value;
112
104
 
113
105
  return this;
114
- }
106
+ };
115
107
 
116
108
  /**
117
109
  * Removes a label from the Kubernetes resource.
118
110
  * @param key - The key of the label to remove.
119
111
  * @returns The current Action instance for method chaining.
120
112
  */
121
- RemoveLabel(key: string) {
113
+ RemoveLabel = (key: string) => {
122
114
  if (this.Raw.metadata?.labels?.[key]) {
123
115
  delete this.Raw.metadata.labels[key];
124
116
  }
125
117
 
126
118
  return this;
127
- }
119
+ };
128
120
 
129
121
  /**
130
122
  * Removes an annotation from the Kubernetes resource.
131
123
  * @param key - The key of the annotation to remove.
132
124
  * @returns The current Action instance for method chaining.
133
125
  */
134
- RemoveAnnotation(key: string) {
126
+ RemoveAnnotation = (key: string) => {
135
127
  if (this.Raw.metadata?.annotations?.[key]) {
136
128
  delete this.Raw.metadata.annotations[key];
137
129
  }
138
130
 
139
131
  return this;
140
- }
132
+ };
141
133
 
142
134
  /**
143
135
  * Check if a label exists on the Kubernetes resource.
@@ -145,9 +137,9 @@ export class PeprMutateRequest<T extends KubernetesObject> {
145
137
  * @param key the label key to check
146
138
  * @returns
147
139
  */
148
- HasLabel(key: string) {
140
+ HasLabel = (key: string) => {
149
141
  return this.Raw.metadata?.labels?.[key] !== undefined;
150
- }
142
+ };
151
143
 
152
144
  /**
153
145
  * Check if an annotation exists on the Kubernetes resource.
@@ -155,7 +147,7 @@ export class PeprMutateRequest<T extends KubernetesObject> {
155
147
  * @param key the annotation key to check
156
148
  * @returns
157
149
  */
158
- HasAnnotation(key: string) {
150
+ HasAnnotation = (key: string) => {
159
151
  return this.Raw.metadata?.annotations?.[key] !== undefined;
160
- }
152
+ };
161
153
  }
@@ -0,0 +1,128 @@
1
+ // SPDX-License-Identifier: Apache-2.0
2
+ // SPDX-FileCopyrightText: 2023-Present The Pepr Authors
3
+
4
+ import { clone } from "ramda";
5
+ import Log from "./logger";
6
+
7
+ export type DataOp = "add" | "remove";
8
+ export type DataStore = Record<string, string>;
9
+ export type DataSender = (op: DataOp, keys: string[], value?: string) => void;
10
+ export type DataReceiver = (data: DataStore) => void;
11
+ export type Unsubscribe = () => void;
12
+
13
+ export interface PeprStore {
14
+ /**
15
+ * Returns the current value associated with the given key, or null if the given key does not exist.
16
+ */
17
+ getItem(key: string): string | null;
18
+ /**
19
+ * Removes all key/value pairs, if there are any.
20
+ */
21
+ clear(): void;
22
+ /**
23
+ * Removes the key/value pair with the given key, if a key/value pair with the given key exists.
24
+ */
25
+ removeItem(key: string): void;
26
+ /**
27
+ * Sets the value of the pair identified by key to value, creating a new key/value pair if none existed for key previously.
28
+ */
29
+ setItem(key: string, value: string): void;
30
+
31
+ /**
32
+ * Subscribe to changes in the store. This API behaves similarly to the [Svelte Store API](https://vercel.com/docs/beginner-sveltekit/svelte-stores#using-the-store).
33
+ *
34
+ * @param listener - The callback to be invoked when the store changes.
35
+ * @returns A function to unsubscribe from the listener.
36
+ */
37
+ subscribe(listener: DataReceiver): Unsubscribe;
38
+
39
+ /**
40
+ * Register a function to be called when the store is ready.
41
+ */
42
+ onReady(callback: DataReceiver): void;
43
+ }
44
+
45
+ /**
46
+ * A key-value data store that can be used to persist data that should be shared across Pepr controllers and capabilities.
47
+ *
48
+ * The API is similar to the [Storage API](https://developer.mozilla.org/docs/Web/API/Storage)
49
+ */
50
+ export class Storage implements PeprStore {
51
+ #store: DataStore = {};
52
+ #send!: DataSender;
53
+ #subscribers: Record<number, DataReceiver> = {};
54
+ #subscriberId = 0;
55
+ #readyHandlers: DataReceiver[] = [];
56
+
57
+ registerSender = (send: DataSender) => {
58
+ this.#send = send;
59
+ };
60
+
61
+ receive = (data: DataStore) => {
62
+ Log.debug(data, `Pepr store data received`);
63
+ this.#store = data || {};
64
+
65
+ this.#onReady();
66
+
67
+ // Notify all subscribers
68
+ for (const idx in this.#subscribers) {
69
+ // Send a unique clone of the store to each subscriber
70
+ this.#subscribers[idx](clone(this.#store));
71
+ }
72
+ };
73
+
74
+ getItem = (key: string) => {
75
+ // Return null if the value is the empty string
76
+ return this.#store[key] || null;
77
+ };
78
+
79
+ clear = () => {
80
+ this.#dispatchUpdate("remove", Object.keys(this.#store));
81
+ };
82
+
83
+ removeItem = (key: string) => {
84
+ this.#dispatchUpdate("remove", [key]);
85
+ };
86
+
87
+ setItem = (key: string, value: string) => {
88
+ this.#dispatchUpdate("add", [key], value);
89
+ };
90
+
91
+ subscribe = (subscriber: DataReceiver) => {
92
+ const idx = this.#subscriberId++;
93
+ this.#subscribers[idx] = subscriber;
94
+ return () => this.unsubscribe(idx);
95
+ };
96
+
97
+ onReady = (callback: DataReceiver) => {
98
+ this.#readyHandlers.push(callback);
99
+ };
100
+
101
+ /**
102
+ * Remove a subscriber from the list of subscribers.
103
+ * @param idx - The index of the subscriber to remove.
104
+ */
105
+ unsubscribe = (idx: number) => {
106
+ delete this.#subscribers[idx];
107
+ };
108
+
109
+ #onReady = () => {
110
+ // Notify all ready handlers with a clone of the store
111
+ for (const handler of this.#readyHandlers) {
112
+ handler(clone(this.#store));
113
+ }
114
+
115
+ // Make this a noop so that it can't be called again
116
+ this.#onReady = () => {};
117
+ };
118
+
119
+ /**
120
+ * Dispatch an update to the store and notify all subscribers.
121
+ * @param op - The type of operation to perform.
122
+ * @param keys - The keys to update.
123
+ * @param [value] - The new value.
124
+ */
125
+ #dispatchUpdate = (op: DataOp, keys: string[], value?: string) => {
126
+ this.#send(op, keys, value);
127
+ };
128
+ }
package/src/lib/types.ts CHANGED
@@ -1,15 +1,12 @@
1
1
  // SPDX-License-Identifier: Apache-2.0
2
2
  // SPDX-FileCopyrightText: 2023-Present The Pepr Authors
3
3
 
4
- import { GroupVersionKind, KubernetesObject, WebhookIgnore } from "./k8s/types";
4
+ import { GenericClass, GroupVersionKind, KubernetesObject } from "kubernetes-fluent-client";
5
+ import { WatchAction } from "kubernetes-fluent-client/dist/fluent/types";
6
+
5
7
  import { PeprMutateRequest } from "./mutate-request";
6
8
  import { PeprValidateRequest } from "./validate-request";
7
9
 
8
- export type PackageJSON = {
9
- description: string;
10
- pepr: ModuleConfig;
11
- };
12
-
13
10
  /**
14
11
  * Recursively make all properties in T optional.
15
12
  */
@@ -48,49 +45,6 @@ export interface CapabilityExport extends CapabilityCfg {
48
45
  bindings: Binding[];
49
46
  }
50
47
 
51
- export type ModuleSigning = {
52
- /**
53
- * Specifies the signing policy.
54
- * "requireAuthorizedKey" - only authorized keys are accepted.
55
- * "requireAnyKey" - any key is accepted, as long as it's valid.
56
- * "none" - no signing required.
57
- */
58
- signingPolicy?: "requireAuthorizedKey" | "requireAnyKey" | "none";
59
- /**
60
- * List of authorized keys for the "requireAuthorizedKey" policy.
61
- * These keys are allowed to sign Pepr capabilities.
62
- */
63
- authorizedKeys?: string[];
64
- };
65
-
66
- /** Global configuration for the Pepr runtime. */
67
- export type ModuleConfig = {
68
- /** The user-defined name for the module */
69
- name: string;
70
- /** The Pepr version this module uses */
71
- peprVersion?: string;
72
- /** The user-defined version of the module */
73
- appVersion?: string;
74
- /** A unique identifier for this Pepr module. This is automatically generated by Pepr. */
75
- uuid: string;
76
- /** A description of the Pepr module and what it does. */
77
- description?: string;
78
- /** Reject K8s resource AdmissionRequests on error. */
79
- onError?: string;
80
- /** Configure global exclusions that will never be processed by Pepr. */
81
- alwaysIgnore: WebhookIgnore;
82
- /**
83
- * FUTURE USE.
84
- *
85
- * Configure the signing policy for Pepr capabilities.
86
- * This setting determines the requirements for signing keys in Pepr.
87
- */
88
- signing?: ModuleSigning;
89
- };
90
-
91
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
92
- export type GenericClass = abstract new () => any;
93
-
94
48
  export type WhenSelector<T extends GenericClass> = {
95
49
  /** Register an action to be executed when a Kubernetes resource is created or updated. */
96
50
  IsCreatedOrUpdated: () => BindingAll<T>;
@@ -106,6 +60,8 @@ export type Binding = {
106
60
  event: Event;
107
61
  isMutate?: boolean;
108
62
  isValidate?: boolean;
63
+ isWatch?: boolean;
64
+ readonly model: GenericClass;
109
65
  readonly kind: GroupVersionKind;
110
66
  readonly filters: {
111
67
  name: string;
@@ -115,6 +71,7 @@ export type Binding = {
115
71
  };
116
72
  readonly mutateCallback?: MutateAction<GenericClass, InstanceType<GenericClass>>;
117
73
  readonly validateCallback?: ValidateAction<GenericClass, InstanceType<GenericClass>>;
74
+ readonly watchCallback?: WatchAction<GenericClass, InstanceType<GenericClass>>;
118
75
  };
119
76
 
120
77
  export type BindingFilter<T extends GenericClass> = CommonActionChain<T> & {
@@ -170,16 +127,36 @@ export type CommonActionChain<T extends GenericClass> = MutateActionChain<T> & {
170
127
  /**
171
128
  * Create a new MUTATE action with the specified callback function and previously specified
172
129
  * filters.
130
+ *
131
+ * @since 0.13.0
132
+ *
173
133
  * @param action The action to be executed when the Kubernetes resource is processed by the AdmissionController.
174
134
  */
175
135
  Mutate: (action: MutateAction<T, InstanceType<T>>) => MutateActionChain<T>;
176
136
  };
177
137
 
178
- export type MutateActionChain<T extends GenericClass> = {
138
+ export type ValidateActionChain<T extends GenericClass> = {
139
+ /**
140
+ * Establish a watcher for the specified resource. The callback function will be executed after the admission controller has
141
+ * processed the resource and the request has been persisted to the cluster.
142
+ *
143
+ * **Beta Function**: This method is still in early testing and edge cases may still exist.
144
+ *
145
+ * @since 0.14.0
146
+ *
147
+ * @param action
148
+ * @returns
149
+ */
150
+ Watch: (action: WatchAction<T, InstanceType<T>>) => void;
151
+ };
152
+
153
+ export type MutateActionChain<T extends GenericClass> = ValidateActionChain<T> & {
179
154
  /**
180
155
  * Create a new VALIDATE action with the specified callback function and previously specified
181
156
  * filters. Return the `request.Approve()` or `Request.Deny()` methods to approve or deny the request:
182
157
  *
158
+ * @since 0.13.0
159
+ *
183
160
  * @example
184
161
  * ```ts
185
162
  * When(a.Deployment)
@@ -195,7 +172,7 @@ export type MutateActionChain<T extends GenericClass> = {
195
172
  *
196
173
  * @param action The action to be executed when the Kubernetes resource is processed by the AdmissionController.
197
174
  */
198
- Validate: (action: ValidateAction<T, InstanceType<T>>) => void;
175
+ Validate: (action: ValidateAction<T, InstanceType<T>>) => ValidateActionChain<T>;
199
176
  };
200
177
 
201
178
  export type MutateAction<T extends GenericClass, K extends KubernetesObject = InstanceType<T>> = (
@@ -204,9 +181,9 @@ export type MutateAction<T extends GenericClass, K extends KubernetesObject = In
204
181
 
205
182
  export type ValidateAction<T extends GenericClass, K extends KubernetesObject = InstanceType<T>> = (
206
183
  req: PeprValidateRequest<K>,
207
- ) => Promise<ValidateResponse> | ValidateResponse;
184
+ ) => Promise<ValidateActionResponse> | ValidateActionResponse;
208
185
 
209
- export type ValidateResponse = {
186
+ export type ValidateActionResponse = {
210
187
  allowed: boolean;
211
188
  statusCode?: number;
212
189
  statusMessage?: string;
@@ -1,17 +1,18 @@
1
1
  // SPDX-License-Identifier: Apache-2.0
2
2
  // SPDX-FileCopyrightText: 2023-Present The Pepr Authors
3
3
 
4
+ import { kind } from "kubernetes-fluent-client";
5
+
4
6
  import { Capability } from "./capability";
5
7
  import { shouldSkipRequest } from "./filter";
6
- import { Request, ValidateResponse } from "./k8s/types";
7
- import { Secret } from "./k8s/upstream";
8
+ import { AdmissionRequest, ValidateResponse } from "./k8s";
8
9
  import Log from "./logger";
9
10
  import { convertFromBase64Map } from "./utils";
10
11
  import { PeprValidateRequest } from "./validate-request";
11
12
 
12
13
  export async function validateProcessor(
13
14
  capabilities: Capability[],
14
- req: Request,
15
+ req: AdmissionRequest,
15
16
  reqMetadata: Record<string, string>,
16
17
  ): Promise<ValidateResponse> {
17
18
  const wrapped = new PeprValidateRequest(req);
@@ -23,7 +24,7 @@ export async function validateProcessor(
23
24
  // If the resource is a secret, decode the data
24
25
  const isSecret = req.kind.version == "v1" && req.kind.kind == "Secret";
25
26
  if (isSecret) {
26
- convertFromBase64Map(wrapped.Raw as unknown as Secret);
27
+ convertFromBase64Map(wrapped.Raw as unknown as kind.Secret);
27
28
  }
28
29
 
29
30
  Log.info(reqMetadata, `Processing validation request`);