pepr 0.40.1 → 0.42.1

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 (107) hide show
  1. package/README.md +11 -5
  2. package/dist/cli/build.d.ts +1 -0
  3. package/dist/cli/build.d.ts.map +1 -1
  4. package/dist/cli/build.helpers.d.ts +66 -0
  5. package/dist/cli/build.helpers.d.ts.map +1 -1
  6. package/dist/cli/deploy.d.ts.map +1 -1
  7. package/dist/cli/init/templates.d.ts +2 -2
  8. package/dist/cli/monitor.d.ts +23 -0
  9. package/dist/cli/monitor.d.ts.map +1 -1
  10. package/dist/cli.js +536 -429
  11. package/dist/controller.js +52 -27
  12. package/dist/lib/assets/destroy.d.ts.map +1 -1
  13. package/dist/lib/assets/helm.d.ts +1 -1
  14. package/dist/lib/assets/helm.d.ts.map +1 -1
  15. package/dist/lib/assets/index.d.ts.map +1 -1
  16. package/dist/lib/assets/pods.d.ts +5 -19
  17. package/dist/lib/assets/pods.d.ts.map +1 -1
  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.map +1 -1
  21. package/dist/lib/controller/index.d.ts.map +1 -1
  22. package/dist/lib/controller/index.util.d.ts +10 -0
  23. package/dist/lib/controller/index.util.d.ts.map +1 -0
  24. package/dist/lib/controller/store.d.ts +0 -1
  25. package/dist/lib/controller/store.d.ts.map +1 -1
  26. package/dist/lib/controller/storeCache.d.ts +1 -0
  27. package/dist/lib/controller/storeCache.d.ts.map +1 -1
  28. package/dist/lib/deploymentChecks.d.ts +3 -0
  29. package/dist/lib/deploymentChecks.d.ts.map +1 -0
  30. package/dist/lib/enums.d.ts +5 -5
  31. package/dist/lib/enums.d.ts.map +1 -1
  32. package/dist/lib/filesystemService.d.ts +2 -0
  33. package/dist/lib/filesystemService.d.ts.map +1 -0
  34. package/dist/lib/filter/adjudicators/adjudicators.d.ts +73 -0
  35. package/dist/lib/filter/adjudicators/adjudicators.d.ts.map +1 -0
  36. package/dist/lib/filter/adjudicators/defaultTestObjects.d.ts +7 -0
  37. package/dist/lib/filter/adjudicators/defaultTestObjects.d.ts.map +1 -0
  38. package/dist/lib/helpers.d.ts +1 -4
  39. package/dist/lib/helpers.d.ts.map +1 -1
  40. package/dist/lib/mutate-request.d.ts +2 -2
  41. package/dist/lib/mutate-request.d.ts.map +1 -1
  42. package/dist/lib/queue.d.ts.map +1 -1
  43. package/dist/lib/schedule.d.ts.map +1 -1
  44. package/dist/lib/storage.d.ts +5 -5
  45. package/dist/lib/storage.d.ts.map +1 -1
  46. package/dist/lib/{logger.d.ts → telemetry/logger.d.ts} +1 -1
  47. package/dist/lib/telemetry/logger.d.ts.map +1 -0
  48. package/dist/lib/{metrics.d.ts → telemetry/metrics.d.ts} +3 -1
  49. package/dist/lib/telemetry/metrics.d.ts.map +1 -0
  50. package/dist/lib/types.d.ts +10 -9
  51. package/dist/lib/types.d.ts.map +1 -1
  52. package/dist/lib/utils.d.ts.map +1 -1
  53. package/dist/lib/validate-processor.d.ts +4 -1
  54. package/dist/lib/validate-processor.d.ts.map +1 -1
  55. package/dist/lib/watch-processor.d.ts.map +1 -1
  56. package/dist/lib.d.ts +1 -1
  57. package/dist/lib.d.ts.map +1 -1
  58. package/dist/lib.js +283 -231
  59. package/dist/lib.js.map +4 -4
  60. package/dist/sdk/sdk.d.ts +3 -4
  61. package/dist/sdk/sdk.d.ts.map +1 -1
  62. package/package.json +5 -5
  63. package/src/cli/build.helpers.ts +180 -0
  64. package/src/cli/build.ts +85 -132
  65. package/src/cli/deploy.ts +2 -1
  66. package/src/cli/init/templates.ts +1 -1
  67. package/src/cli/monitor.ts +108 -65
  68. package/src/lib/assets/deploy.ts +7 -7
  69. package/src/lib/assets/destroy.ts +2 -2
  70. package/src/lib/assets/helm.ts +6 -6
  71. package/src/lib/assets/index.ts +110 -89
  72. package/src/lib/assets/pods.ts +10 -5
  73. package/src/lib/assets/webhooks.ts +3 -3
  74. package/src/lib/assets/yaml.ts +12 -9
  75. package/src/lib/capability.ts +29 -19
  76. package/src/lib/controller/index.ts +41 -69
  77. package/src/lib/controller/index.util.ts +47 -0
  78. package/src/lib/controller/store.ts +24 -11
  79. package/src/lib/controller/storeCache.ts +11 -2
  80. package/src/lib/deploymentChecks.ts +43 -0
  81. package/src/lib/enums.ts +5 -5
  82. package/src/lib/filesystemService.ts +16 -0
  83. package/src/lib/filter/{adjudicators.ts → adjudicators/adjudicators.ts} +67 -35
  84. package/src/lib/filter/adjudicators/defaultTestObjects.ts +46 -0
  85. package/src/lib/filter/filter.ts +1 -1
  86. package/src/lib/finalizer.ts +1 -1
  87. package/src/lib/helpers.ts +31 -88
  88. package/src/lib/mutate-processor.ts +1 -1
  89. package/src/lib/mutate-request.ts +11 -11
  90. package/src/lib/queue.ts +13 -5
  91. package/src/lib/schedule.ts +8 -8
  92. package/src/lib/storage.ts +48 -39
  93. package/src/lib/{logger.ts → telemetry/logger.ts} +1 -1
  94. package/src/lib/{metrics.ts → telemetry/metrics.ts} +18 -17
  95. package/src/lib/types.ts +12 -9
  96. package/src/lib/utils.ts +6 -6
  97. package/src/lib/validate-processor.ts +48 -40
  98. package/src/lib/watch-processor.ts +19 -15
  99. package/src/lib.ts +1 -1
  100. package/src/runtime/controller.ts +1 -1
  101. package/src/sdk/cosign.ts +4 -4
  102. package/src/sdk/sdk.ts +6 -9
  103. package/src/templates/capabilities/hello-pepr.ts +19 -9
  104. package/dist/lib/filter/adjudicators.d.ts +0 -69
  105. package/dist/lib/filter/adjudicators.d.ts.map +0 -1
  106. package/dist/lib/logger.d.ts.map +0 -1
  107. package/dist/lib/metrics.d.ts.map +0 -1
@@ -6,13 +6,16 @@ import crypto from "crypto";
6
6
  import { promises as fs } from "fs";
7
7
  import { Assets } from ".";
8
8
  import { apiTokenSecret, service, tlsSecret, watcherService } from "./networking";
9
- import { deployment, moduleSecret, namespace, watcher } from "./pods";
9
+ import { getDeployment, getModuleSecret, getNamespace, getWatcher } from "./pods";
10
10
  import { clusterRole, clusterRoleBinding, serviceAccount, storeRole, storeRoleBinding } from "./rbac";
11
11
  import { webhookConfig } from "./webhooks";
12
12
  import { genEnv } from "./pods";
13
13
 
14
14
  // Helm Chart overrides file (values.yaml) generated from assets
15
- export async function overridesFile({ hash, name, image, config, apiToken, capabilities }: Assets, path: string) {
15
+ export async function overridesFile(
16
+ { hash, name, image, config, apiToken, capabilities }: Assets,
17
+ path: string,
18
+ ): Promise<void> {
16
19
  const rbacOverrides = clusterRole(name, capabilities, config.rbacMode, config.rbac).rules;
17
20
 
18
21
  const overrides = {
@@ -166,7 +169,7 @@ export async function overridesFile({ hash, name, image, config, apiToken, capab
166
169
 
167
170
  await fs.writeFile(path, dumpYaml(overrides, { noRefs: true, forceQuotes: true }));
168
171
  }
169
- export function zarfYaml({ name, image, config }: Assets, path: string) {
172
+ export function zarfYaml({ name, image, config }: Assets, path: string): string {
170
173
  const zarfCfg = {
171
174
  kind: "ZarfPackageConfig",
172
175
  metadata: {
@@ -194,7 +197,7 @@ export function zarfYaml({ name, image, config }: Assets, path: string) {
194
197
  return dumpYaml(zarfCfg, { noRefs: true });
195
198
  }
196
199
 
197
- export function zarfYamlChart({ name, image, config }: Assets, path: string) {
200
+ export function zarfYamlChart({ name, image, config }: Assets, path: string): string {
198
201
  const zarfCfg = {
199
202
  kind: "ZarfPackageConfig",
200
203
  metadata: {
@@ -223,7 +226,7 @@ export function zarfYamlChart({ name, image, config }: Assets, path: string) {
223
226
  return dumpYaml(zarfCfg, { noRefs: true });
224
227
  }
225
228
 
226
- export async function allYaml(assets: Assets, imagePullSecret?: string) {
229
+ export async function allYaml(assets: Assets, imagePullSecret?: string): Promise<string> {
227
230
  const { name, tls, apiToken, path, config } = assets;
228
231
  const code = await fs.readFile(path);
229
232
 
@@ -232,19 +235,19 @@ export async function allYaml(assets: Assets, imagePullSecret?: string) {
232
235
 
233
236
  const mutateWebhook = await webhookConfig(assets, "mutate", assets.config.webhookTimeout);
234
237
  const validateWebhook = await webhookConfig(assets, "validate", assets.config.webhookTimeout);
235
- const watchDeployment = watcher(assets, assets.hash, assets.buildTimestamp, imagePullSecret);
238
+ const watchDeployment = getWatcher(assets, assets.hash, assets.buildTimestamp, imagePullSecret);
236
239
 
237
240
  const resources = [
238
- namespace(assets.config.customLabels?.namespace),
241
+ getNamespace(assets.config.customLabels?.namespace),
239
242
  clusterRole(name, assets.capabilities, config.rbacMode, config.rbac),
240
243
  clusterRoleBinding(name),
241
244
  serviceAccount(name),
242
245
  apiTokenSecret(name, apiToken),
243
246
  tlsSecret(name, tls),
244
- deployment(assets, assets.hash, assets.buildTimestamp, imagePullSecret),
247
+ getDeployment(assets, assets.hash, assets.buildTimestamp, imagePullSecret),
245
248
  service(name),
246
249
  watcherService(name),
247
- moduleSecret(name, code, assets.hash),
250
+ getModuleSecret(name, code, assets.hash),
248
251
  storeRole(name),
249
252
  storeRoleBinding(name),
250
253
  ];
@@ -1,10 +1,9 @@
1
- /* eslint-disable max-statements */
2
1
  // SPDX-License-Identifier: Apache-2.0
3
2
  // SPDX-FileCopyrightText: 2023-Present The Pepr Authors
4
3
 
5
4
  import { GenericClass, GroupVersionKind, modelToGroupVersionKind } from "kubernetes-fluent-client";
6
5
  import { pickBy } from "ramda";
7
- import Log from "./logger";
6
+ import Log from "./telemetry/logger";
8
7
  import { isBuildMode, isDevMode, isWatchMode } from "./module";
9
8
  import { PeprStore, Storage } from "./storage";
10
9
  import { OnSchedule, Schedule } from "./schedule";
@@ -72,7 +71,7 @@ export class Capability implements CapabilityExport {
72
71
  }
73
72
  };
74
73
 
75
- public getScheduleStore() {
74
+ public getScheduleStore(): Storage {
76
75
  return this.#scheduleStore;
77
76
  }
78
77
 
@@ -112,19 +111,19 @@ export class Capability implements CapabilityExport {
112
111
  onReady: this.#scheduleStore.onReady,
113
112
  };
114
113
 
115
- get bindings() {
114
+ get bindings(): Binding[] {
116
115
  return this.#bindings;
117
116
  }
118
117
 
119
- get name() {
118
+ get name(): string {
120
119
  return this.#name;
121
120
  }
122
121
 
123
- get description() {
122
+ get description(): string {
124
123
  return this.#description;
125
124
  }
126
125
 
127
- get namespaces() {
126
+ get namespaces(): string[] {
128
127
  return this.#namespaces || [];
129
128
  }
130
129
 
@@ -193,7 +192,7 @@ export class Capability implements CapabilityExport {
193
192
  model,
194
193
  // If the kind is not specified, use the matched kind from the model
195
194
  kind: kind || matchedKind,
196
- event: Event.Any,
195
+ event: Event.ANY,
197
196
  filters: {
198
197
  name: "",
199
198
  namespaces: [],
@@ -208,8 +207,19 @@ export class Capability implements CapabilityExport {
208
207
  const bindings = this.#bindings;
209
208
  const prefix = `${this.#name}: ${model.name}`;
210
209
  const commonChain = { WithLabel, WithAnnotation, WithDeletionTimestamp, Mutate, Validate, Watch, Reconcile, Alias };
211
- const isNotEmpty = (value: object) => Object.keys(value).length > 0;
212
- const log = (message: string, cbString: string) => {
210
+
211
+ type CommonChainType = typeof commonChain;
212
+ type ExtendedCommonChainType = CommonChainType & {
213
+ Alias: (alias: string) => CommonChainType;
214
+ InNamespace: (...namespaces: string[]) => BindingWithName<T>;
215
+ InNamespaceRegex: (...namespaces: RegExp[]) => BindingWithName<T>;
216
+ WithName: (name: string) => BindingFilter<T>;
217
+ WithNameRegex: (regexName: RegExp) => BindingFilter<T>;
218
+ WithDeletionTimestamp: () => BindingFilter<T>;
219
+ };
220
+
221
+ const isNotEmpty = (value: object): boolean => Object.keys(value).length > 0;
222
+ const log = (message: string, cbString: string): void => {
213
223
  const filteredObj = pickBy(isNotEmpty, binding.filters);
214
224
 
215
225
  Log.info(`${message} configured for ${binding.event}`, prefix);
@@ -317,7 +327,7 @@ export class Capability implements CapabilityExport {
317
327
  ...binding,
318
328
  isMutate: true,
319
329
  isFinalize: true,
320
- event: Event.Any,
330
+ event: Event.ANY,
321
331
  mutateCallback: addFinalizer,
322
332
  };
323
333
  bindings.push(mutateBinding);
@@ -329,8 +339,8 @@ export class Capability implements CapabilityExport {
329
339
  ...binding,
330
340
  isWatch: true,
331
341
  isFinalize: true,
332
- event: Event.Update,
333
- finalizeCallback: async (update: InstanceType<T>, logger = aliasLogger) => {
342
+ event: Event.UPDATE,
343
+ finalizeCallback: async (update: InstanceType<T>, logger = aliasLogger): Promise<boolean | void> => {
334
344
  Log.info(`Executing finalize action with alias: ${binding.alias || "no alias provided"}`);
335
345
  return await finalizeCallback(update, logger);
336
346
  },
@@ -381,13 +391,13 @@ export class Capability implements CapabilityExport {
381
391
  return commonChain;
382
392
  }
383
393
 
384
- function Alias(alias: string) {
394
+ function Alias(alias: string): CommonChainType {
385
395
  Log.debug(`Adding prefix alias ${alias}`, prefix);
386
396
  binding.alias = alias;
387
397
  return commonChain;
388
398
  }
389
399
 
390
- function bindEvent(event: Event) {
400
+ function bindEvent(event: Event): ExtendedCommonChainType {
391
401
  binding.event = event;
392
402
  return {
393
403
  ...commonChain,
@@ -401,10 +411,10 @@ export class Capability implements CapabilityExport {
401
411
  }
402
412
 
403
413
  return {
404
- IsCreatedOrUpdated: () => bindEvent(Event.CreateOrUpdate),
405
- IsCreated: () => bindEvent(Event.Create),
406
- IsUpdated: () => bindEvent(Event.Update),
407
- IsDeleted: () => bindEvent(Event.Delete),
414
+ IsCreatedOrUpdated: () => bindEvent(Event.CREATE_OR_UPDATE),
415
+ IsCreated: () => bindEvent(Event.CREATE),
416
+ IsUpdated: () => bindEvent(Event.UPDATE),
417
+ IsDeleted: () => bindEvent(Event.DELETE),
408
418
  };
409
419
  };
410
420
  }
@@ -7,17 +7,19 @@ import https from "https";
7
7
 
8
8
  import { Capability } from "../capability";
9
9
  import { MutateResponse, ValidateResponse } from "../k8s";
10
- import Log from "../logger";
11
- import { metricsCollector, MetricsCollector } from "../metrics";
10
+ import Log from "../telemetry/logger";
11
+ import { metricsCollector, MetricsCollector } from "../telemetry/metrics";
12
12
  import { ModuleConfig, isWatchMode } from "../module";
13
13
  import { mutateProcessor } from "../mutate-processor";
14
14
  import { validateProcessor } from "../validate-processor";
15
15
  import { StoreController } from "./store";
16
- import { ResponseItem, AdmissionRequest } from "../types";
16
+ import { AdmissionRequest } from "../types";
17
+ import { karForMutate, karForValidate, KubeAdmissionReview } from "./index.util";
17
18
 
18
19
  if (!process.env.PEPR_NODE_WARNINGS) {
19
20
  process.removeAllListeners("warning");
20
21
  }
22
+
21
23
  export class Controller {
22
24
  // Track whether the server is running
23
25
  #running = false;
@@ -76,7 +78,7 @@ export class Controller {
76
78
  }
77
79
 
78
80
  /** Start the webhook server */
79
- startServer = (port: number) => {
81
+ startServer = (port: number): void => {
80
82
  if (this.#running) {
81
83
  throw new Error("Cannot start Pepr module: Pepr module was not instantiated with deferStart=true");
82
84
  }
@@ -131,7 +133,7 @@ export class Controller {
131
133
  });
132
134
  };
133
135
 
134
- #bindEndpoints = () => {
136
+ #bindEndpoints = (): void => {
135
137
  // Health check endpoint
136
138
  this.#app.get("/healthz", Controller.#healthz);
137
139
 
@@ -160,7 +162,7 @@ export class Controller {
160
162
  * @param next The next middleware function
161
163
  * @returns
162
164
  */
163
- #validateToken = (req: express.Request, res: express.Response, next: NextFunction) => {
165
+ #validateToken = (req: express.Request, res: express.Response, next: NextFunction): void => {
164
166
  // Validate the token
165
167
  const { token } = req.params;
166
168
  if (token !== this.#token) {
@@ -181,8 +183,10 @@ export class Controller {
181
183
  * @param req the incoming request
182
184
  * @param res the outgoing response
183
185
  */
184
- #metrics = async (req: express.Request, res: express.Response) => {
186
+ #metrics = async (req: express.Request, res: express.Response): Promise<void> => {
185
187
  try {
188
+ // https://github.com/prometheus/docs/blob/main/content/docs/instrumenting/exposition_formats.md#basic-info
189
+ res.set("Content-Type", "text/plain; version=0.0.4");
186
190
  res.send(await this.#metricsCollector.getMetrics());
187
191
  } catch (err) {
188
192
  Log.error(err, `Error getting metrics`);
@@ -196,7 +200,9 @@ export class Controller {
196
200
  * @param admissionKind the type of admission request
197
201
  * @returns the request handler
198
202
  */
199
- #admissionReq = (admissionKind: "Mutate" | "Validate") => {
203
+ #admissionReq = (
204
+ admissionKind: "Mutate" | "Validate",
205
+ ): ((req: express.Request, res: express.Response) => Promise<void>) => {
200
206
  // Create the admission request handler
201
207
  return async (req: express.Request, res: express.Response) => {
202
208
  // Start the metrics timer
@@ -206,77 +212,38 @@ export class Controller {
206
212
  // Get the request from the body or create an empty request
207
213
  const request: AdmissionRequest = req.body?.request || ({} as AdmissionRequest);
208
214
 
209
- // Run the before hook if it exists
210
- this.#beforeHook && this.#beforeHook(request || {});
211
-
212
- // Setup identifiers for logging
213
- const name = request?.name ? `/${request.name}` : "";
214
- const namespace = request?.namespace || "";
215
- const gvk = request?.kind || { group: "", version: "", kind: "" };
216
-
217
- const reqMetadata = {
218
- uid: request.uid,
219
- namespace,
220
- name,
215
+ const { name, namespace, gvk } = {
216
+ name: request?.name ? `/${request.name}` : "",
217
+ namespace: request?.namespace || "",
218
+ gvk: request?.kind || { group: "", version: "", kind: "" },
221
219
  };
222
220
 
221
+ const reqMetadata = { uid: request.uid, namespace, name };
223
222
  Log.info({ ...reqMetadata, gvk, operation: request.operation, admissionKind }, "Incoming request");
224
223
  Log.debug({ ...reqMetadata, request }, "Incoming request body");
225
224
 
226
- // Process the request
227
- let response: MutateResponse | ValidateResponse[];
225
+ // Run the before hook if it exists
226
+ this.#beforeHook && this.#beforeHook(request || {});
228
227
 
229
- // Call mutate or validate based on the admission kind
230
- if (admissionKind === "Mutate") {
231
- response = await mutateProcessor(this.#config, this.#capabilities, request, reqMetadata);
232
- } else {
233
- response = await validateProcessor(this.#config, this.#capabilities, request, reqMetadata);
234
- }
228
+ // Process the request
229
+ const response: MutateResponse | ValidateResponse[] =
230
+ admissionKind === "Mutate"
231
+ ? await mutateProcessor(this.#config, this.#capabilities, request, reqMetadata)
232
+ : await validateProcessor(this.#config, this.#capabilities, request, reqMetadata);
235
233
 
236
234
  // Run the after hook if it exists
237
- const responseList: ValidateResponse[] | MutateResponse[] = Array.isArray(response) ? response : [response];
238
- responseList.map(res => {
235
+ [response].flat().map(res => {
239
236
  this.#afterHook && this.#afterHook(res);
240
- // Log the response
241
237
  Log.info({ ...reqMetadata, res }, "Check response");
242
238
  });
243
239
 
244
- let kubeAdmissionResponse: ValidateResponse[] | MutateResponse | ResponseItem;
245
-
246
- if (admissionKind === "Mutate") {
247
- kubeAdmissionResponse = response;
248
- Log.debug({ ...reqMetadata, response }, "Outgoing response");
249
- res.send({
250
- apiVersion: "admission.k8s.io/v1",
251
- kind: "AdmissionReview",
252
- response: kubeAdmissionResponse,
253
- });
254
- } else {
255
- kubeAdmissionResponse =
256
- responseList.length === 0
257
- ? {
258
- uid: request.uid,
259
- allowed: true,
260
- status: { message: "no in-scope validations -- allowed!" },
261
- }
262
- : {
263
- uid: responseList[0].uid,
264
- allowed: responseList.filter(r => !r.allowed).length === 0,
265
- status: {
266
- message: (responseList as ValidateResponse[])
267
- .filter(rl => !rl.allowed)
268
- .map(curr => curr.status?.message)
269
- .join("; "),
270
- },
271
- };
272
- res.send({
273
- apiVersion: "admission.k8s.io/v1",
274
- kind: "AdmissionReview",
275
- response: kubeAdmissionResponse,
276
- });
277
- }
278
-
279
- Log.debug({ ...reqMetadata, kubeAdmissionResponse }, "Outgoing response");
240
+ const kar: KubeAdmissionReview =
241
+ admissionKind === "Mutate"
242
+ ? karForMutate(response as MutateResponse)
243
+ : karForValidate(request, response as ValidateResponse[]);
244
+
245
+ Log.debug({ ...reqMetadata, kubeAdmissionResponse: kar.response }, "Outgoing response");
246
+ res.send(kar);
280
247
 
281
248
  this.#metricsCollector.observeEnd(startTime, admissionKind);
282
249
  } catch (err) {
@@ -294,10 +261,15 @@ export class Controller {
294
261
  * @param res the outgoing response
295
262
  * @param next the next middleware function
296
263
  */
297
- static #logger(req: express.Request, res: express.Response, next: express.NextFunction) {
264
+ static #logger(req: express.Request, res: express.Response, next: express.NextFunction): void {
298
265
  const startTime = Date.now();
299
266
 
300
267
  res.on("finish", () => {
268
+ const excludedRoutes = ["/healthz", "/metrics"];
269
+ if (excludedRoutes.includes(req.originalUrl)) {
270
+ return;
271
+ }
272
+
301
273
  const elapsedTime = Date.now() - startTime;
302
274
  const message = {
303
275
  uid: req.body?.request?.uid,
@@ -318,7 +290,7 @@ export class Controller {
318
290
  * @param req the incoming request
319
291
  * @param res the outgoing response
320
292
  */
321
- static #healthz(req: express.Request, res: express.Response) {
293
+ static #healthz(req: express.Request, res: express.Response): void {
322
294
  try {
323
295
  res.send("OK");
324
296
  } catch (err) {
@@ -0,0 +1,47 @@
1
+ // SPDX-License-Identifier: Apache-2.0
2
+ // SPDX-FileCopyrightText: 2023-Present The Pepr Authors
3
+
4
+ import { MutateResponse, ValidateResponse } from "../k8s";
5
+ import { ResponseItem, AdmissionRequest } from "../types";
6
+
7
+ export interface KubeAdmissionReview {
8
+ apiVersion: string;
9
+ kind: string;
10
+ response: ValidateResponse[] | MutateResponse | ResponseItem;
11
+ }
12
+
13
+ export function karForMutate(mr: MutateResponse): KubeAdmissionReview {
14
+ return {
15
+ apiVersion: "admission.k8s.io/v1",
16
+ kind: "AdmissionReview",
17
+ response: mr,
18
+ };
19
+ }
20
+
21
+ export function karForValidate(ar: AdmissionRequest, vr: ValidateResponse[]): KubeAdmissionReview {
22
+ const isAllowed = vr.filter(r => !r.allowed).length === 0;
23
+
24
+ const resp: ValidateResponse =
25
+ vr.length === 0
26
+ ? {
27
+ uid: ar.uid,
28
+ allowed: true,
29
+ status: { code: 200, message: "no in-scope validations -- allowed!" },
30
+ }
31
+ : {
32
+ uid: vr[0].uid,
33
+ allowed: isAllowed,
34
+ status: {
35
+ code: isAllowed ? 200 : 422,
36
+ message: vr
37
+ .filter(rl => !rl.allowed)
38
+ .map(curr => curr.status?.message)
39
+ .join("; "),
40
+ },
41
+ };
42
+ return {
43
+ apiVersion: "admission.k8s.io/v1",
44
+ kind: "AdmissionReview",
45
+ response: resp,
46
+ };
47
+ }
@@ -7,12 +7,13 @@ import { startsWith } from "ramda";
7
7
 
8
8
  import { Capability } from "../capability";
9
9
  import { Store } from "../k8s";
10
- import Log, { redactedPatch, redactedStore } from "../logger";
10
+ import Log, { redactedPatch, redactedStore } from "../telemetry/logger";
11
11
  import { DataOp, DataSender, DataStore, Storage } from "../storage";
12
12
  import { fillStoreCache, sendUpdatesAndFlushCache } from "./storeCache";
13
13
 
14
14
  const namespace = "pepr-system";
15
- export const debounceBackoff = 5000;
15
+ const debounceBackoffReceive = 1000;
16
+ const debounceBackoffSend = 4000;
16
17
 
17
18
  export class StoreController {
18
19
  #name: string;
@@ -25,7 +26,7 @@ export class StoreController {
25
26
 
26
27
  this.#name = name;
27
28
 
28
- const setStorageInstance = (registrationFunction: () => Storage, name: string) => {
29
+ const setStorageInstance = (registrationFunction: () => Storage, name: string): void => {
29
30
  const scheduleStore = registrationFunction();
30
31
 
31
32
  // Bind the store sender to the capability
@@ -61,13 +62,22 @@ export class StoreController {
61
62
  );
62
63
  }
63
64
 
64
- #setupWatch = () => {
65
+ #setupWatch = (): void => {
65
66
  const watcher = K8s(Store, { name: this.#name, namespace }).Watch(this.#receive);
66
67
  watcher.start().catch(e => Log.error(e, "Error starting Pepr store watch"));
67
68
  };
68
69
 
69
- #migrateAndSetupWatch = async (store: Store) => {
70
+ #migrateAndSetupWatch = async (store: Store): Promise<void> => {
70
71
  Log.debug(redactedStore(store), "Pepr Store migration");
72
+ // Add cacheID label to store
73
+ await K8s(Store, { namespace, name: this.#name }).Patch([
74
+ {
75
+ op: "add",
76
+ path: "/metadata/labels/pepr.dev-cacheID",
77
+ value: `${Date.now()}`,
78
+ },
79
+ ]);
80
+
71
81
  const data: DataStore = store.data || {};
72
82
  let storeCache: Record<string, Operation> = {};
73
83
 
@@ -96,11 +106,11 @@ export class StoreController {
96
106
  this.#setupWatch();
97
107
  };
98
108
 
99
- #receive = (store: Store) => {
109
+ #receive = (store: Store): void => {
100
110
  Log.debug(redactedStore(store), "Pepr Store update");
101
111
 
102
112
  // Wrap the update in a debounced function
103
- const debounced = () => {
113
+ const debounced = (): void => {
104
114
  // Base64 decode the data
105
115
  const data: DataStore = store.data || {};
106
116
 
@@ -134,10 +144,10 @@ export class StoreController {
134
144
 
135
145
  // Debounce the update to 1 second to avoid multiple rapid calls
136
146
  clearTimeout(this.#sendDebounce);
137
- this.#sendDebounce = setTimeout(debounced, this.#onReady ? 0 : debounceBackoff);
147
+ this.#sendDebounce = setTimeout(debounced, this.#onReady ? 0 : debounceBackoffReceive);
138
148
  };
139
149
 
140
- #send = (capabilityName: string) => {
150
+ #send = (capabilityName: string): DataSender => {
141
151
  let storeCache: Record<string, Operation> = {};
142
152
 
143
153
  // Create a sender function for the capability to add/remove data from the store
@@ -151,12 +161,12 @@ export class StoreController {
151
161
  Log.debug(redactedPatch(storeCache), "Sending updates to Pepr store");
152
162
  void sendUpdatesAndFlushCache(storeCache, namespace, this.#name);
153
163
  }
154
- }, debounceBackoff);
164
+ }, debounceBackoffSend);
155
165
 
156
166
  return sender;
157
167
  };
158
168
 
159
- #createStoreResource = async (e: unknown) => {
169
+ #createStoreResource = async (e: unknown): Promise<void> => {
160
170
  Log.info(`Pepr store not found, creating...`);
161
171
  Log.debug(e);
162
172
 
@@ -165,6 +175,9 @@ export class StoreController {
165
175
  metadata: {
166
176
  name: this.#name,
167
177
  namespace,
178
+ labels: {
179
+ "pepr.dev-cacheID": `${Date.now()}`,
180
+ },
168
181
  },
169
182
  data: {
170
183
  // JSON Patch will die if the data is empty, so we need to add a placeholder
@@ -1,5 +1,5 @@
1
1
  import { DataOp } from "../storage";
2
- import Log from "../logger";
2
+ import Log from "../telemetry/logger";
3
3
  import { K8s } from "kubernetes-fluent-client";
4
4
  import { Store } from "../k8s";
5
5
  import { StatusCodes } from "http-status-codes";
@@ -11,7 +11,7 @@ export const sendUpdatesAndFlushCache = async (cache: Record<string, Operation>,
11
11
 
12
12
  try {
13
13
  if (payload.length > 0) {
14
- await K8s(Store, { namespace, name }).Patch(payload); // Send patch to cluster
14
+ await K8s(Store, { namespace, name }).Patch(updateCacheID(payload)); // Send patch to cluster
15
15
  Object.keys(cache).forEach(key => delete cache[key]);
16
16
  }
17
17
  } catch (err) {
@@ -61,3 +61,12 @@ export const fillStoreCache = (
61
61
  }
62
62
  return cache;
63
63
  };
64
+
65
+ export function updateCacheID(payload: Operation[]): Operation[] {
66
+ payload.push({
67
+ op: "replace",
68
+ path: "/metadata/labels/pepr.dev-cacheID",
69
+ value: `${Date.now()}`,
70
+ });
71
+ return payload;
72
+ }
@@ -0,0 +1,43 @@
1
+ // check to see if all replicas are ready for all deployments in the pepr-system namespace
2
+
3
+ import { K8s, kind } from "kubernetes-fluent-client";
4
+ import Log from "./telemetry/logger";
5
+
6
+ // returns true if all deployments are ready, false otherwise
7
+ export async function checkDeploymentStatus(namespace: string) {
8
+ const deployments = await K8s(kind.Deployment).InNamespace(namespace).Get();
9
+ let status = false;
10
+ let readyCount = 0;
11
+
12
+ for (const deployment of deployments.items) {
13
+ const readyReplicas = deployment.status?.readyReplicas ? deployment.status?.readyReplicas : 0;
14
+ if (deployment.status?.readyReplicas !== deployment.spec?.replicas) {
15
+ Log.info(
16
+ `Waiting for deployment ${deployment.metadata?.name} rollout to finish: ${readyReplicas} of ${deployment.spec?.replicas} replicas are available`,
17
+ );
18
+ } else {
19
+ Log.info(
20
+ `Deployment ${deployment.metadata?.name} rolled out: ${readyReplicas} of ${deployment.spec?.replicas} replicas are available`,
21
+ );
22
+ readyCount++;
23
+ }
24
+ }
25
+ if (readyCount === deployments.items.length) {
26
+ status = true;
27
+ }
28
+ return status;
29
+ }
30
+
31
+ // wait for all deployments in the pepr-system namespace to be ready
32
+ export async function namespaceDeploymentsReady(namespace: string = "pepr-system") {
33
+ Log.info(`Checking ${namespace} deployments status...`);
34
+ let ready = false;
35
+ while (!ready) {
36
+ ready = await checkDeploymentStatus(namespace);
37
+ if (ready) {
38
+ return ready;
39
+ }
40
+ await new Promise(resolve => setTimeout(resolve, 1000));
41
+ }
42
+ Log.info(`All ${namespace} deployments are ready`);
43
+ }
package/src/lib/enums.ts CHANGED
@@ -13,9 +13,9 @@ export enum Operation {
13
13
  * The type of Kubernetes mutating webhook event that the action is registered for.
14
14
  */
15
15
  export enum Event {
16
- Create = "CREATE",
17
- Update = "UPDATE",
18
- Delete = "DELETE",
19
- CreateOrUpdate = "CREATEORUPDATE",
20
- Any = "*",
16
+ CREATE = "CREATE",
17
+ UPDATE = "UPDATE",
18
+ DELETE = "DELETE",
19
+ CREATE_OR_UPDATE = "CREATEORUPDATE",
20
+ ANY = "*",
21
21
  }
@@ -0,0 +1,16 @@
1
+ // SPDX-License-Identifier: Apache-2.0
2
+ // SPDX-FileCopyrightText: 2023-Present The Pepr Authors
3
+
4
+ import { promises } from "fs";
5
+
6
+ export async function createDirectoryIfNotExists(path: string) {
7
+ try {
8
+ await promises.access(path);
9
+ } catch (error) {
10
+ if (error.code === "ENOENT") {
11
+ await promises.mkdir(path, { recursive: true });
12
+ } else {
13
+ throw error;
14
+ }
15
+ }
16
+ }