pepr 0.32.0 → 0.32.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 (42) hide show
  1. package/package.json +2 -2
  2. package/dist/cli.d.ts.map +0 -1
  3. package/dist/cli.js +0 -2849
  4. package/dist/controller.js +0 -164
  5. package/dist/lib/assets/deploy.d.ts.map +0 -1
  6. package/dist/lib/assets/destroy.d.ts.map +0 -1
  7. package/dist/lib/assets/helm.d.ts.map +0 -1
  8. package/dist/lib/assets/index.d.ts.map +0 -1
  9. package/dist/lib/assets/loader.d.ts.map +0 -1
  10. package/dist/lib/assets/networking.d.ts.map +0 -1
  11. package/dist/lib/assets/pods.d.ts.map +0 -1
  12. package/dist/lib/assets/rbac.d.ts.map +0 -1
  13. package/dist/lib/assets/store.d.ts.map +0 -1
  14. package/dist/lib/assets/webhooks.d.ts.map +0 -1
  15. package/dist/lib/assets/yaml.d.ts.map +0 -1
  16. package/dist/lib/capability.d.ts.map +0 -1
  17. package/dist/lib/controller/index.d.ts.map +0 -1
  18. package/dist/lib/controller/store.d.ts.map +0 -1
  19. package/dist/lib/errors.d.ts.map +0 -1
  20. package/dist/lib/filter.d.ts.map +0 -1
  21. package/dist/lib/helpers.d.ts.map +0 -1
  22. package/dist/lib/included-files.d.ts.map +0 -1
  23. package/dist/lib/k8s.d.ts.map +0 -1
  24. package/dist/lib/logger.d.ts.map +0 -1
  25. package/dist/lib/metrics.d.ts.map +0 -1
  26. package/dist/lib/module.d.ts.map +0 -1
  27. package/dist/lib/mutate-processor.d.ts.map +0 -1
  28. package/dist/lib/mutate-request.d.ts.map +0 -1
  29. package/dist/lib/queue.d.ts.map +0 -1
  30. package/dist/lib/schedule.d.ts.map +0 -1
  31. package/dist/lib/storage.d.ts.map +0 -1
  32. package/dist/lib/tls.d.ts.map +0 -1
  33. package/dist/lib/types.d.ts.map +0 -1
  34. package/dist/lib/utils.d.ts.map +0 -1
  35. package/dist/lib/validate-processor.d.ts.map +0 -1
  36. package/dist/lib/validate-request.d.ts.map +0 -1
  37. package/dist/lib/watch-processor.d.ts.map +0 -1
  38. package/dist/lib.d.ts.map +0 -1
  39. package/dist/lib.js +0 -1808
  40. package/dist/lib.js.map +0 -7
  41. package/dist/runtime/controller.d.ts.map +0 -1
  42. package/dist/sdk/sdk.d.ts.map +0 -1
package/dist/lib.js DELETED
@@ -1,1808 +0,0 @@
1
- "use strict";
2
- var __create = Object.create;
3
- var __defProp = Object.defineProperty;
4
- var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
5
- var __getOwnPropNames = Object.getOwnPropertyNames;
6
- var __getProtoOf = Object.getPrototypeOf;
7
- var __hasOwnProp = Object.prototype.hasOwnProperty;
8
- var __export = (target, all) => {
9
- for (var name in all)
10
- __defProp(target, name, { get: all[name], enumerable: true });
11
- };
12
- var __copyProps = (to, from, except, desc) => {
13
- if (from && typeof from === "object" || typeof from === "function") {
14
- for (let key of __getOwnPropNames(from))
15
- if (!__hasOwnProp.call(to, key) && key !== except)
16
- __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
17
- }
18
- return to;
19
- };
20
- var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
21
- // If the importer is in node compatibility mode or this is not an ESM
22
- // file that has been converted to a CommonJS file using a Babel-
23
- // compatible transform (i.e. "__esModule" has not been set), then set
24
- // "default" to the CommonJS "module.exports" for node compatibility.
25
- isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
26
- mod
27
- ));
28
- var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
29
-
30
- // src/lib.ts
31
- var lib_exports = {};
32
- __export(lib_exports, {
33
- Capability: () => Capability,
34
- K8s: () => import_kubernetes_fluent_client7.K8s,
35
- Log: () => logger_default,
36
- PeprModule: () => PeprModule,
37
- PeprMutateRequest: () => PeprMutateRequest,
38
- PeprUtils: () => utils_exports,
39
- PeprValidateRequest: () => PeprValidateRequest,
40
- R: () => R,
41
- RegisterKind: () => import_kubernetes_fluent_client7.RegisterKind,
42
- a: () => import_kubernetes_fluent_client7.kind,
43
- fetch: () => import_kubernetes_fluent_client7.fetch,
44
- fetchStatus: () => import_kubernetes_fluent_client7.fetchStatus,
45
- kind: () => import_kubernetes_fluent_client7.kind,
46
- sdk: () => sdk_exports
47
- });
48
- module.exports = __toCommonJS(lib_exports);
49
- var import_kubernetes_fluent_client7 = require("kubernetes-fluent-client");
50
- var R = __toESM(require("ramda"));
51
-
52
- // src/lib/capability.ts
53
- var import_kubernetes_fluent_client6 = require("kubernetes-fluent-client");
54
- var import_ramda6 = require("ramda");
55
-
56
- // src/lib/logger.ts
57
- var import_pino = require("pino");
58
- var isPrettyLog = process.env.PEPR_PRETTY_LOGS === "true";
59
- var pretty = {
60
- target: "pino-pretty",
61
- options: {
62
- colorize: true
63
- }
64
- };
65
- var transport = isPrettyLog ? pretty : void 0;
66
- var pinoTimeFunction = process.env.PINO_TIME_STAMP === "iso" ? () => import_pino.stdTimeFunctions.isoTime() : () => import_pino.stdTimeFunctions.epochTime();
67
- var Log = (0, import_pino.pino)({
68
- transport,
69
- timestamp: pinoTimeFunction
70
- });
71
- if (process.env.LOG_LEVEL) {
72
- Log.level = process.env.LOG_LEVEL;
73
- }
74
- var logger_default = Log;
75
-
76
- // src/lib/module.ts
77
- var import_ramda4 = require("ramda");
78
-
79
- // src/lib/controller/index.ts
80
- var import_express = __toESM(require("express"));
81
- var import_fs = __toESM(require("fs"));
82
- var import_https = __toESM(require("https"));
83
-
84
- // src/lib/metrics.ts
85
- var import_perf_hooks = require("perf_hooks");
86
- var import_prom_client = __toESM(require("prom-client"));
87
- var loggingPrefix = "MetricsCollector";
88
- var MetricsCollector = class {
89
- #registry;
90
- #counters = /* @__PURE__ */ new Map();
91
- #summaries = /* @__PURE__ */ new Map();
92
- #prefix;
93
- #metricNames = {
94
- errors: "errors",
95
- alerts: "alerts",
96
- mutate: "Mutate",
97
- validate: "Validate"
98
- };
99
- /**
100
- * Creates a MetricsCollector instance with prefixed metrics.
101
- * @param [prefix='pepr'] - The prefix for the metric names.
102
- */
103
- constructor(prefix = "pepr") {
104
- this.#registry = new import_prom_client.Registry();
105
- this.#prefix = prefix;
106
- this.addCounter(this.#metricNames.errors, "Mutation/Validate errors encountered");
107
- this.addCounter(this.#metricNames.alerts, "Mutation/Validate bad api token received");
108
- this.addSummary(this.#metricNames.mutate, "Mutation operation summary");
109
- this.addSummary(this.#metricNames.validate, "Validation operation summary");
110
- }
111
- #getMetricName = (name) => `${this.#prefix}_${name}`;
112
- #addMetric = (collection, MetricType, name, help) => {
113
- if (collection.has(this.#getMetricName(name))) {
114
- logger_default.debug(`Metric for ${name} already exists`, loggingPrefix);
115
- return;
116
- }
117
- const metric = new MetricType({
118
- name: this.#getMetricName(name),
119
- help,
120
- registers: [this.#registry]
121
- });
122
- collection.set(this.#getMetricName(name), metric);
123
- };
124
- addCounter = (name, help) => {
125
- this.#addMetric(this.#counters, import_prom_client.default.Counter, name, help);
126
- };
127
- addSummary = (name, help) => {
128
- this.#addMetric(this.#summaries, import_prom_client.default.Summary, name, help);
129
- };
130
- incCounter = (name) => {
131
- this.#counters.get(this.#getMetricName(name))?.inc();
132
- };
133
- /**
134
- * Increments the error counter.
135
- */
136
- error = () => this.incCounter(this.#metricNames.errors);
137
- /**
138
- * Increments the alerts counter.
139
- */
140
- alert = () => this.incCounter(this.#metricNames.alerts);
141
- /**
142
- * Observes the duration since the provided start time and updates the summary.
143
- * @param startTime - The start time.
144
- * @param name - The metrics summary to increment.
145
- */
146
- observeEnd = (startTime, name = this.#metricNames.mutate) => {
147
- this.#summaries.get(this.#getMetricName(name))?.observe(import_perf_hooks.performance.now() - startTime);
148
- };
149
- /**
150
- * Fetches the current metrics from the registry.
151
- * @returns The metrics.
152
- */
153
- getMetrics = () => this.#registry.metrics();
154
- /**
155
- * Returns the current timestamp from performance.now() method. Useful for start timing an operation.
156
- * @returns The timestamp.
157
- */
158
- static observeStart() {
159
- return import_perf_hooks.performance.now();
160
- }
161
- };
162
-
163
- // src/lib/mutate-processor.ts
164
- var import_fast_json_patch = __toESM(require("fast-json-patch"));
165
-
166
- // src/lib/errors.ts
167
- var Errors = {
168
- audit: "audit",
169
- ignore: "ignore",
170
- reject: "reject"
171
- };
172
- var ErrorList = Object.values(Errors);
173
- function ValidateError(error = "") {
174
- if (!ErrorList.includes(error)) {
175
- throw new Error(`Invalid error: ${error}. Must be one of: ${ErrorList.join(", ")}`);
176
- }
177
- }
178
-
179
- // src/lib/k8s.ts
180
- var import_kubernetes_fluent_client = require("kubernetes-fluent-client");
181
- var PeprStore = class extends import_kubernetes_fluent_client.GenericKind {
182
- };
183
- var peprStoreGVK = {
184
- kind: "PeprStore",
185
- version: "v1",
186
- group: "pepr.dev"
187
- };
188
- (0, import_kubernetes_fluent_client.RegisterKind)(PeprStore, peprStoreGVK);
189
-
190
- // src/lib/filter.ts
191
- function shouldSkipRequest(binding, req, capabilityNamespaces) {
192
- const { group, kind: kind4, version } = binding.kind || {};
193
- const { namespaces, labels, annotations, name } = binding.filters || {};
194
- const operation = req.operation.toUpperCase();
195
- const uid = req.uid;
196
- const srcObject = operation === "DELETE" /* DELETE */ ? req.oldObject : req.object;
197
- const { metadata } = srcObject || {};
198
- const combinedNamespaces = [...namespaces, ...capabilityNamespaces];
199
- if (!binding.event.includes(operation) && !binding.event.includes("*" /* Any */)) {
200
- return true;
201
- }
202
- if (name && name !== req.name) {
203
- return true;
204
- }
205
- if (kind4 !== req.kind.kind) {
206
- return true;
207
- }
208
- if (group && group !== req.kind.group) {
209
- return true;
210
- }
211
- if (version && version !== req.kind.version) {
212
- return true;
213
- }
214
- if (combinedNamespaces.length && !combinedNamespaces.includes(req.namespace || "") || !namespaces.includes(req.namespace || "") && capabilityNamespaces.length !== 0 && namespaces.length !== 0) {
215
- let type = "";
216
- let label = "";
217
- if (binding.isMutate) {
218
- type = "Mutate";
219
- label = binding.mutateCallback.name;
220
- } else if (binding.isValidate) {
221
- type = "Validate";
222
- label = binding.validateCallback.name;
223
- } else if (binding.isWatch) {
224
- type = "Watch";
225
- label = binding.watchCallback.name;
226
- }
227
- logger_default.debug({ uid }, `${type} binding (${label}) does not match request namespace "${req.namespace}"`);
228
- return true;
229
- }
230
- for (const [key, value] of Object.entries(labels)) {
231
- const testKey = metadata?.labels?.[key];
232
- if (!testKey) {
233
- logger_default.debug({ uid }, `Label ${key} does not exist`);
234
- return true;
235
- }
236
- if (value && testKey !== value) {
237
- logger_default.debug({ uid }, `${testKey} does not match ${value}`);
238
- return true;
239
- }
240
- }
241
- for (const [key, value] of Object.entries(annotations)) {
242
- const testKey = metadata?.annotations?.[key];
243
- if (!testKey) {
244
- logger_default.debug({ uid }, `Annotation ${key} does not exist`);
245
- return true;
246
- }
247
- if (value && testKey !== value) {
248
- logger_default.debug({ uid }, `${testKey} does not match ${value}`);
249
- return true;
250
- }
251
- }
252
- return false;
253
- }
254
-
255
- // src/lib/mutate-request.ts
256
- var import_ramda = require("ramda");
257
- var PeprMutateRequest = class {
258
- Raw;
259
- #input;
260
- get PermitSideEffects() {
261
- return !this.#input.dryRun;
262
- }
263
- /**
264
- * Indicates whether the request is a dry run.
265
- * @returns true if the request is a dry run, false otherwise.
266
- */
267
- get IsDryRun() {
268
- return this.#input.dryRun;
269
- }
270
- /**
271
- * Provides access to the old resource in the request if available.
272
- * @returns The old Kubernetes resource object or null if not available.
273
- */
274
- get OldResource() {
275
- return this.#input.oldObject;
276
- }
277
- /**
278
- * Provides access to the request object.
279
- * @returns The request object containing the Kubernetes resource.
280
- */
281
- get Request() {
282
- return this.#input;
283
- }
284
- /**
285
- * Creates a new instance of the action class.
286
- * @param input - The request object containing the Kubernetes resource to modify.
287
- */
288
- constructor(input) {
289
- this.#input = input;
290
- if (input.operation.toUpperCase() === "DELETE" /* DELETE */) {
291
- this.Raw = (0, import_ramda.clone)(input.oldObject);
292
- } else {
293
- this.Raw = (0, import_ramda.clone)(input.object);
294
- }
295
- if (!this.Raw) {
296
- throw new Error("unable to load the request object into PeprRequest.RawP");
297
- }
298
- }
299
- /**
300
- * Deep merges the provided object with the current resource.
301
- *
302
- * @param obj - The object to merge with the current resource.
303
- */
304
- Merge = (obj) => {
305
- this.Raw = (0, import_ramda.mergeDeepRight)(this.Raw, obj);
306
- };
307
- /**
308
- * Updates a label on the Kubernetes resource.
309
- * @param key - The key of the label to update.
310
- * @param value - The value of the label.
311
- * @returns The current action instance for method chaining.
312
- */
313
- SetLabel = (key, value) => {
314
- const ref = this.Raw;
315
- ref.metadata = ref.metadata ?? {};
316
- ref.metadata.labels = ref.metadata.labels ?? {};
317
- ref.metadata.labels[key] = value;
318
- return this;
319
- };
320
- /**
321
- * Updates an annotation on the Kubernetes resource.
322
- * @param key - The key of the annotation to update.
323
- * @param value - The value of the annotation.
324
- * @returns The current action instance for method chaining.
325
- */
326
- SetAnnotation = (key, value) => {
327
- const ref = this.Raw;
328
- ref.metadata = ref.metadata ?? {};
329
- ref.metadata.annotations = ref.metadata.annotations ?? {};
330
- ref.metadata.annotations[key] = value;
331
- return this;
332
- };
333
- /**
334
- * Removes a label from the Kubernetes resource.
335
- * @param key - The key of the label to remove.
336
- * @returns The current Action instance for method chaining.
337
- */
338
- RemoveLabel = (key) => {
339
- if (this.Raw.metadata?.labels?.[key]) {
340
- delete this.Raw.metadata.labels[key];
341
- }
342
- return this;
343
- };
344
- /**
345
- * Removes an annotation from the Kubernetes resource.
346
- * @param key - The key of the annotation to remove.
347
- * @returns The current Action instance for method chaining.
348
- */
349
- RemoveAnnotation = (key) => {
350
- if (this.Raw.metadata?.annotations?.[key]) {
351
- delete this.Raw.metadata.annotations[key];
352
- }
353
- return this;
354
- };
355
- /**
356
- * Check if a label exists on the Kubernetes resource.
357
- *
358
- * @param key the label key to check
359
- * @returns
360
- */
361
- HasLabel = (key) => {
362
- return this.Raw.metadata?.labels?.[key] !== void 0;
363
- };
364
- /**
365
- * Check if an annotation exists on the Kubernetes resource.
366
- *
367
- * @param key the annotation key to check
368
- * @returns
369
- */
370
- HasAnnotation = (key) => {
371
- return this.Raw.metadata?.annotations?.[key] !== void 0;
372
- };
373
- };
374
-
375
- // src/lib/utils.ts
376
- var utils_exports = {};
377
- __export(utils_exports, {
378
- base64Decode: () => base64Decode,
379
- base64Encode: () => base64Encode,
380
- convertFromBase64Map: () => convertFromBase64Map,
381
- convertToBase64Map: () => convertToBase64Map,
382
- isAscii: () => isAscii
383
- });
384
- var isAscii = /^[\s\x20-\x7E]*$/;
385
- function convertToBase64Map(obj, skip) {
386
- obj.data = obj.data ?? {};
387
- for (const key in obj.data) {
388
- const value = obj.data[key];
389
- obj.data[key] = skip.includes(key) ? value : base64Encode(value);
390
- }
391
- }
392
- function convertFromBase64Map(obj) {
393
- const skip = [];
394
- obj.data = obj.data ?? {};
395
- for (const key in obj.data) {
396
- if (obj.data[key] == void 0) {
397
- obj.data[key] = "";
398
- } else {
399
- const decoded = base64Decode(obj.data[key]);
400
- if (isAscii.test(decoded)) {
401
- obj.data[key] = decoded;
402
- } else {
403
- skip.push(key);
404
- }
405
- }
406
- }
407
- logger_default.debug(`Non-ascii data detected in keys: ${skip}, skipping automatic base64 decoding`);
408
- return skip;
409
- }
410
- function base64Decode(data) {
411
- return Buffer.from(data, "base64").toString("utf-8");
412
- }
413
- function base64Encode(data) {
414
- return Buffer.from(data).toString("base64");
415
- }
416
-
417
- // src/lib/mutate-processor.ts
418
- async function mutateProcessor(config, capabilities, req, reqMetadata) {
419
- const wrapped = new PeprMutateRequest(req);
420
- const response = {
421
- uid: req.uid,
422
- warnings: [],
423
- allowed: false
424
- };
425
- let matchedAction = false;
426
- let skipDecode = [];
427
- const isSecret = req.kind.version == "v1" && req.kind.kind == "Secret";
428
- if (isSecret) {
429
- skipDecode = convertFromBase64Map(wrapped.Raw);
430
- }
431
- logger_default.info(reqMetadata, `Processing request`);
432
- for (const { name, bindings, namespaces } of capabilities) {
433
- const actionMetadata = { ...reqMetadata, name };
434
- for (const action of bindings) {
435
- if (!action.mutateCallback) {
436
- continue;
437
- }
438
- if (shouldSkipRequest(action, req, namespaces)) {
439
- continue;
440
- }
441
- const label = action.mutateCallback.name;
442
- logger_default.info(actionMetadata, `Processing mutation action (${label})`);
443
- matchedAction = true;
444
- const updateStatus = (status) => {
445
- if (req.operation == "DELETE") {
446
- return;
447
- }
448
- const identifier = `${config.uuid}.pepr.dev/${name}`;
449
- wrapped.Raw.metadata = wrapped.Raw.metadata || {};
450
- wrapped.Raw.metadata.annotations = wrapped.Raw.metadata.annotations || {};
451
- wrapped.Raw.metadata.annotations[identifier] = status;
452
- };
453
- updateStatus("started");
454
- try {
455
- await action.mutateCallback(wrapped);
456
- logger_default.info(actionMetadata, `Mutation action succeeded (${label})`);
457
- updateStatus("succeeded");
458
- } catch (e) {
459
- updateStatus("warning");
460
- response.warnings = response.warnings || [];
461
- let errorMessage = "";
462
- try {
463
- if (e.message && e.message !== "[object Object]") {
464
- errorMessage = e.message;
465
- } else {
466
- throw new Error("An error occurred in the mutate action.");
467
- }
468
- } catch (e2) {
469
- errorMessage = "An error occurred with the mutate action.";
470
- }
471
- logger_default.error(actionMetadata, `Action failed: ${errorMessage}`);
472
- response.warnings.push(`Action failed: ${errorMessage}`);
473
- switch (config.onError) {
474
- case Errors.reject:
475
- logger_default.error(actionMetadata, `Action failed: ${errorMessage}`);
476
- response.result = "Pepr module configured to reject on error";
477
- return response;
478
- case Errors.audit:
479
- response.auditAnnotations = response.auditAnnotations || {};
480
- response.auditAnnotations[Date.now()] = `Action failed: ${errorMessage}`;
481
- break;
482
- }
483
- }
484
- }
485
- }
486
- response.allowed = true;
487
- if (!matchedAction) {
488
- logger_default.info(reqMetadata, `No matching actions found`);
489
- return response;
490
- }
491
- if (req.operation == "DELETE") {
492
- return response;
493
- }
494
- const transformed = wrapped.Raw;
495
- if (isSecret) {
496
- convertToBase64Map(transformed, skipDecode);
497
- }
498
- const patches = import_fast_json_patch.default.compare(req.object, transformed);
499
- if (patches.length > 0) {
500
- response.patchType = "JSONPatch";
501
- response.patch = base64Encode(JSON.stringify(patches));
502
- }
503
- if (response.warnings && response.warnings.length < 1) {
504
- delete response.warnings;
505
- }
506
- logger_default.debug({ ...reqMetadata, patches }, `Patches generated`);
507
- return response;
508
- }
509
-
510
- // src/lib/validate-request.ts
511
- var import_ramda2 = require("ramda");
512
- var PeprValidateRequest = class {
513
- Raw;
514
- #input;
515
- /**
516
- * Provides access to the old resource in the request if available.
517
- * @returns The old Kubernetes resource object or null if not available.
518
- */
519
- get OldResource() {
520
- return this.#input.oldObject;
521
- }
522
- /**
523
- * Provides access to the request object.
524
- * @returns The request object containing the Kubernetes resource.
525
- */
526
- get Request() {
527
- return this.#input;
528
- }
529
- /**
530
- * Creates a new instance of the Action class.
531
- * @param input - The request object containing the Kubernetes resource to modify.
532
- */
533
- constructor(input) {
534
- this.#input = input;
535
- if (input.operation.toUpperCase() === "DELETE" /* DELETE */) {
536
- this.Raw = (0, import_ramda2.clone)(input.oldObject);
537
- } else {
538
- this.Raw = (0, import_ramda2.clone)(input.object);
539
- }
540
- if (!this.Raw) {
541
- throw new Error("unable to load the request object into PeprRequest.Raw");
542
- }
543
- }
544
- /**
545
- * Check if a label exists on the Kubernetes resource.
546
- *
547
- * @param key the label key to check
548
- * @returns
549
- */
550
- HasLabel = (key) => {
551
- return this.Raw.metadata?.labels?.[key] !== void 0;
552
- };
553
- /**
554
- * Check if an annotation exists on the Kubernetes resource.
555
- *
556
- * @param key the annotation key to check
557
- * @returns
558
- */
559
- HasAnnotation = (key) => {
560
- return this.Raw.metadata?.annotations?.[key] !== void 0;
561
- };
562
- /**
563
- * Create a validation response that allows the request.
564
- *
565
- * @returns The validation response.
566
- */
567
- Approve = () => {
568
- return {
569
- allowed: true
570
- };
571
- };
572
- /**
573
- * Create a validation response that denies the request.
574
- *
575
- * @param statusMessage Optional status message to return to the user.
576
- * @param statusCode Optional status code to return to the user.
577
- * @returns The validation response.
578
- */
579
- Deny = (statusMessage, statusCode) => {
580
- return {
581
- allowed: false,
582
- statusCode,
583
- statusMessage
584
- };
585
- };
586
- };
587
-
588
- // src/lib/validate-processor.ts
589
- async function validateProcessor(capabilities, req, reqMetadata) {
590
- const wrapped = new PeprValidateRequest(req);
591
- const response = [];
592
- const isSecret = req.kind.version == "v1" && req.kind.kind == "Secret";
593
- if (isSecret) {
594
- convertFromBase64Map(wrapped.Raw);
595
- }
596
- logger_default.info(reqMetadata, `Processing validation request`);
597
- for (const { name, bindings, namespaces } of capabilities) {
598
- const actionMetadata = { ...reqMetadata, name };
599
- for (const action of bindings) {
600
- if (!action.validateCallback) {
601
- continue;
602
- }
603
- const localResponse = {
604
- uid: req.uid,
605
- allowed: true
606
- // Assume it's allowed until a validation check fails
607
- };
608
- if (shouldSkipRequest(action, req, namespaces)) {
609
- continue;
610
- }
611
- const label = action.validateCallback.name;
612
- logger_default.info(actionMetadata, `Processing validation action (${label})`);
613
- try {
614
- const resp = await action.validateCallback(wrapped);
615
- localResponse.allowed = resp.allowed;
616
- if (resp.statusCode || resp.statusMessage) {
617
- localResponse.status = {
618
- code: resp.statusCode || 400,
619
- message: resp.statusMessage || `Validation failed for ${name}`
620
- };
621
- }
622
- logger_default.info(actionMetadata, `Validation action complete (${label}): ${resp.allowed ? "allowed" : "denied"}`);
623
- } catch (e) {
624
- logger_default.error(actionMetadata, `Action failed: ${JSON.stringify(e)}`);
625
- localResponse.allowed = false;
626
- localResponse.status = {
627
- code: 500,
628
- message: `Action failed with error: ${JSON.stringify(e)}`
629
- };
630
- return [localResponse];
631
- }
632
- response.push(localResponse);
633
- }
634
- }
635
- return response;
636
- }
637
-
638
- // src/lib/controller/store.ts
639
- var import_kubernetes_fluent_client2 = require("kubernetes-fluent-client");
640
- var import_ramda3 = require("ramda");
641
- var namespace = "pepr-system";
642
- var debounceBackoff = 5e3;
643
- var PeprControllerStore = class {
644
- #name;
645
- #stores = {};
646
- #sendDebounce;
647
- #onReady;
648
- constructor(capabilities, name, onReady) {
649
- this.#onReady = onReady;
650
- this.#name = name;
651
- if (name.includes("schedule")) {
652
- for (const { name: name2, registerScheduleStore, hasSchedule } of capabilities) {
653
- if (hasSchedule !== true) {
654
- continue;
655
- }
656
- const { scheduleStore } = registerScheduleStore();
657
- scheduleStore.registerSender(this.#send(name2));
658
- this.#stores[name2] = scheduleStore;
659
- }
660
- } else {
661
- for (const { name: name2, registerStore } of capabilities) {
662
- const { store } = registerStore();
663
- store.registerSender(this.#send(name2));
664
- this.#stores[name2] = store;
665
- }
666
- }
667
- setTimeout(
668
- () => (0, import_kubernetes_fluent_client2.K8s)(PeprStore).InNamespace(namespace).Get(this.#name).then(this.#setupWatch).catch(this.#createStoreResource),
669
- Math.random() * 3e3
670
- );
671
- }
672
- #setupWatch = () => {
673
- const watcher = (0, import_kubernetes_fluent_client2.K8s)(PeprStore, { name: this.#name, namespace }).Watch(this.#receive);
674
- watcher.start().catch((e) => logger_default.error(e, "Error starting Pepr store watch"));
675
- };
676
- #receive = (store) => {
677
- logger_default.debug(store, "Pepr Store update");
678
- const debounced = () => {
679
- const data = store.data || {};
680
- for (const name of Object.keys(this.#stores)) {
681
- const offset = `${name}-`.length;
682
- const filtered = {};
683
- for (const key of Object.keys(data)) {
684
- if ((0, import_ramda3.startsWith)(name, key)) {
685
- filtered[key.slice(offset)] = data[key];
686
- }
687
- }
688
- this.#stores[name].receive(filtered);
689
- }
690
- if (this.#onReady) {
691
- this.#onReady();
692
- this.#onReady = void 0;
693
- }
694
- };
695
- clearTimeout(this.#sendDebounce);
696
- this.#sendDebounce = setTimeout(debounced, this.#onReady ? 0 : debounceBackoff);
697
- };
698
- #send = (capabilityName) => {
699
- const sendCache = {};
700
- const fillCache = (op, key, val) => {
701
- if (op === "add") {
702
- const path = `/data/${capabilityName}-${key}`;
703
- const value = val || "";
704
- const cacheIdx = [op, path, value].join(":");
705
- sendCache[cacheIdx] = { op, path, value };
706
- return;
707
- }
708
- if (op === "remove") {
709
- if (key.length < 1) {
710
- throw new Error(`Key is required for REMOVE operation`);
711
- }
712
- for (const k of key) {
713
- const path = `/data/${capabilityName}-${k}`;
714
- const cacheIdx = [op, path].join(":");
715
- sendCache[cacheIdx] = { op, path };
716
- }
717
- return;
718
- }
719
- throw new Error(`Unsupported operation: ${op}`);
720
- };
721
- const flushCache = async () => {
722
- const indexes = Object.keys(sendCache);
723
- const payload = Object.values(sendCache);
724
- for (const idx of indexes) {
725
- delete sendCache[idx];
726
- }
727
- try {
728
- await (0, import_kubernetes_fluent_client2.K8s)(PeprStore, { namespace, name: this.#name }).Patch(payload);
729
- } catch (err) {
730
- logger_default.error(err, "Pepr store update failure");
731
- if (err.status === 422) {
732
- Object.keys(sendCache).forEach((key) => delete sendCache[key]);
733
- } else {
734
- for (const idx of indexes) {
735
- sendCache[idx] = payload[Number(idx)];
736
- }
737
- }
738
- }
739
- };
740
- const sender = async (op, key, val) => {
741
- fillCache(op, key, val);
742
- };
743
- setInterval(() => {
744
- if (Object.keys(sendCache).length > 0) {
745
- logger_default.debug(sendCache, "Sending updates to Pepr store");
746
- void flushCache();
747
- }
748
- }, debounceBackoff);
749
- return sender;
750
- };
751
- #createStoreResource = async (e) => {
752
- logger_default.info(`Pepr store not found, creating...`);
753
- logger_default.debug(e);
754
- try {
755
- await (0, import_kubernetes_fluent_client2.K8s)(PeprStore).Apply({
756
- metadata: {
757
- name: this.#name,
758
- namespace
759
- },
760
- data: {
761
- // JSON Patch will die if the data is empty, so we need to add a placeholder
762
- __pepr_do_not_delete__: "k-thx-bye"
763
- }
764
- });
765
- this.#setupWatch();
766
- } catch (err) {
767
- logger_default.error(err, "Failed to create Pepr store");
768
- }
769
- };
770
- };
771
-
772
- // src/lib/controller/index.ts
773
- var Controller = class _Controller {
774
- // Track whether the server is running
775
- #running = false;
776
- // Metrics collector
777
- #metricsCollector = new MetricsCollector("pepr");
778
- // The token used to authenticate requests
779
- #token = "";
780
- // The express app instance
781
- #app = (0, import_express.default)();
782
- // Initialized with the constructor
783
- #config;
784
- #capabilities;
785
- #beforeHook;
786
- #afterHook;
787
- constructor(config, capabilities, beforeHook, afterHook, onReady) {
788
- this.#config = config;
789
- this.#capabilities = capabilities;
790
- new PeprControllerStore(capabilities, `pepr-${config.uuid}-store`, () => {
791
- this.#bindEndpoints();
792
- onReady && onReady();
793
- logger_default.info("\u2705 Controller startup complete");
794
- new PeprControllerStore(capabilities, `pepr-${config.uuid}-schedule`, () => {
795
- logger_default.info("\u2705 Scheduling processed");
796
- });
797
- });
798
- this.#app.use(_Controller.#logger);
799
- this.#app.use(import_express.default.json({ limit: "2mb" }));
800
- if (beforeHook) {
801
- logger_default.info(`Using beforeHook: ${beforeHook}`);
802
- this.#beforeHook = beforeHook;
803
- }
804
- if (afterHook) {
805
- logger_default.info(`Using afterHook: ${afterHook}`);
806
- this.#afterHook = afterHook;
807
- }
808
- }
809
- /** Start the webhook server */
810
- startServer = (port) => {
811
- if (this.#running) {
812
- throw new Error("Cannot start Pepr module: Pepr module was not instantiated with deferStart=true");
813
- }
814
- const options = {
815
- key: import_fs.default.readFileSync(process.env.SSL_KEY_PATH || "/etc/certs/tls.key"),
816
- cert: import_fs.default.readFileSync(process.env.SSL_CERT_PATH || "/etc/certs/tls.crt")
817
- };
818
- if (!isWatchMode()) {
819
- this.#token = process.env.PEPR_API_TOKEN || import_fs.default.readFileSync("/app/api-token/value").toString().trim();
820
- logger_default.info(`Using API token: ${this.#token}`);
821
- if (!this.#token) {
822
- throw new Error("API token not found");
823
- }
824
- }
825
- const server = import_https.default.createServer(options, this.#app).listen(port);
826
- server.on("listening", () => {
827
- logger_default.info(`Server listening on port ${port}`);
828
- this.#running = true;
829
- });
830
- server.on("error", (e) => {
831
- if (e.code === "EADDRINUSE") {
832
- logger_default.warn(
833
- `Address in use, retrying in 2 seconds. If this persists, ensure ${port} is not in use, e.g. "lsof -i :${port}"`
834
- );
835
- setTimeout(() => {
836
- server.close();
837
- server.listen(port);
838
- }, 2e3);
839
- }
840
- });
841
- process.on("SIGTERM", () => {
842
- logger_default.info("Received SIGTERM, closing server");
843
- server.close(() => {
844
- logger_default.info("Server closed");
845
- process.exit(0);
846
- });
847
- });
848
- };
849
- #bindEndpoints = () => {
850
- this.#app.get("/healthz", _Controller.#healthz);
851
- this.#app.get("/metrics", this.#metrics);
852
- if (isWatchMode()) {
853
- return;
854
- }
855
- this.#app.use(["/mutate/:token", "/validate/:token"], this.#validateToken);
856
- this.#app.post("/mutate/:token", this.#admissionReq("Mutate"));
857
- this.#app.post("/validate/:token", this.#admissionReq("Validate"));
858
- };
859
- /**
860
- * Validate the token in the request path
861
- *
862
- * @param req The incoming request
863
- * @param res The outgoing response
864
- * @param next The next middleware function
865
- * @returns
866
- */
867
- #validateToken = (req, res, next) => {
868
- const { token } = req.params;
869
- if (token !== this.#token) {
870
- const err = `Unauthorized: invalid token '${token.replace(/[^\w]/g, "_")}'`;
871
- logger_default.warn(err);
872
- res.status(401).send(err);
873
- this.#metricsCollector.alert();
874
- return;
875
- }
876
- next();
877
- };
878
- /**
879
- * Metrics endpoint handler
880
- *
881
- * @param req the incoming request
882
- * @param res the outgoing response
883
- */
884
- #metrics = async (req, res) => {
885
- try {
886
- res.send(await this.#metricsCollector.getMetrics());
887
- } catch (err) {
888
- logger_default.error(err, `Error getting metrics`);
889
- res.status(500).send("Internal Server Error");
890
- }
891
- };
892
- /**
893
- * Admission request handler for both mutate and validate requests
894
- *
895
- * @param admissionKind the type of admission request
896
- * @returns the request handler
897
- */
898
- #admissionReq = (admissionKind) => {
899
- return async (req, res) => {
900
- const startTime = MetricsCollector.observeStart();
901
- try {
902
- const request = req.body?.request || {};
903
- this.#beforeHook && this.#beforeHook(request || {});
904
- const name = request?.name ? `/${request.name}` : "";
905
- const namespace2 = request?.namespace || "";
906
- const gvk = request?.kind || { group: "", version: "", kind: "" };
907
- const reqMetadata = {
908
- uid: request.uid,
909
- namespace: namespace2,
910
- name
911
- };
912
- logger_default.info({ ...reqMetadata, gvk, operation: request.operation, admissionKind }, "Incoming request");
913
- logger_default.debug({ ...reqMetadata, request }, "Incoming request body");
914
- let response;
915
- if (admissionKind === "Mutate") {
916
- response = await mutateProcessor(this.#config, this.#capabilities, request, reqMetadata);
917
- } else {
918
- response = await validateProcessor(this.#capabilities, request, reqMetadata);
919
- }
920
- const responseList = Array.isArray(response) ? response : [response];
921
- responseList.map((res2) => {
922
- this.#afterHook && this.#afterHook(res2);
923
- logger_default.info({ ...reqMetadata, res: res2 }, "Check response");
924
- });
925
- let kubeAdmissionResponse;
926
- if (admissionKind === "Mutate") {
927
- kubeAdmissionResponse = response;
928
- logger_default.debug({ ...reqMetadata, response }, "Outgoing response");
929
- res.send({
930
- apiVersion: "admission.k8s.io/v1",
931
- kind: "AdmissionReview",
932
- response: kubeAdmissionResponse
933
- });
934
- } else {
935
- kubeAdmissionResponse = responseList.length === 0 ? {
936
- uid: request.uid,
937
- allowed: true,
938
- status: { message: "no in-scope validations -- allowed!" }
939
- } : {
940
- uid: responseList[0].uid,
941
- allowed: responseList.filter((r) => !r.allowed).length === 0,
942
- status: {
943
- message: responseList.filter((rl) => !rl.allowed).map((curr) => curr.status?.message).join("; ")
944
- }
945
- };
946
- res.send({
947
- apiVersion: "admission.k8s.io/v1",
948
- kind: "AdmissionReview",
949
- response: kubeAdmissionResponse
950
- });
951
- }
952
- logger_default.debug({ ...reqMetadata, kubeAdmissionResponse }, "Outgoing response");
953
- this.#metricsCollector.observeEnd(startTime, admissionKind);
954
- } catch (err) {
955
- logger_default.error(err, `Error processing ${admissionKind} request`);
956
- res.status(500).send("Internal Server Error");
957
- this.#metricsCollector.error();
958
- }
959
- };
960
- };
961
- /**
962
- * Middleware for logging requests
963
- *
964
- * @param req the incoming request
965
- * @param res the outgoing response
966
- * @param next the next middleware function
967
- */
968
- static #logger(req, res, next) {
969
- const startTime = Date.now();
970
- res.on("finish", () => {
971
- const elapsedTime = Date.now() - startTime;
972
- const message = {
973
- uid: req.body?.request?.uid,
974
- method: req.method,
975
- url: req.originalUrl,
976
- status: res.statusCode,
977
- duration: `${elapsedTime} ms`
978
- };
979
- res.statusCode >= 300 ? logger_default.warn(message) : logger_default.info(message);
980
- });
981
- next();
982
- }
983
- /**
984
- * Health check endpoint handler
985
- *
986
- * @param req the incoming request
987
- * @param res the outgoing response
988
- */
989
- static #healthz(req, res) {
990
- try {
991
- res.send("OK");
992
- } catch (err) {
993
- logger_default.error(err, `Error processing health check`);
994
- res.status(500).send("Internal Server Error");
995
- }
996
- }
997
- };
998
-
999
- // src/lib/watch-processor.ts
1000
- var import_kubernetes_fluent_client5 = require("kubernetes-fluent-client");
1001
- var import_types2 = require("kubernetes-fluent-client/dist/fluent/types");
1002
-
1003
- // src/lib/helpers.ts
1004
- var import_kubernetes_fluent_client4 = require("kubernetes-fluent-client");
1005
-
1006
- // src/sdk/sdk.ts
1007
- var sdk_exports = {};
1008
- __export(sdk_exports, {
1009
- containers: () => containers,
1010
- getOwnerRefFrom: () => getOwnerRefFrom,
1011
- sanitizeResourceName: () => sanitizeResourceName,
1012
- writeEvent: () => writeEvent
1013
- });
1014
- var import_kubernetes_fluent_client3 = require("kubernetes-fluent-client");
1015
- function containers(request, containerType) {
1016
- const containers2 = request.Raw.spec?.containers || [];
1017
- const initContainers = request.Raw.spec?.initContainers || [];
1018
- const ephemeralContainers = request.Raw.spec?.ephemeralContainers || [];
1019
- if (containerType === "containers") {
1020
- return containers2;
1021
- }
1022
- if (containerType === "initContainers") {
1023
- return initContainers;
1024
- }
1025
- if (containerType === "ephemeralContainers") {
1026
- return ephemeralContainers;
1027
- }
1028
- return [...containers2, ...initContainers, ...ephemeralContainers];
1029
- }
1030
- async function writeEvent(cr, event, eventType, eventReason, reportingComponent, reportingInstance) {
1031
- logger_default.debug(cr.metadata, `Writing event: ${event.message}`);
1032
- await (0, import_kubernetes_fluent_client3.K8s)(import_kubernetes_fluent_client3.kind.CoreEvent).Create({
1033
- type: eventType,
1034
- reason: eventReason,
1035
- ...event,
1036
- // Fixed values
1037
- metadata: {
1038
- namespace: cr.metadata.namespace,
1039
- generateName: cr.metadata.name
1040
- },
1041
- involvedObject: {
1042
- apiVersion: cr.apiVersion,
1043
- kind: cr.kind,
1044
- name: cr.metadata.name,
1045
- namespace: cr.metadata.namespace,
1046
- uid: cr.metadata.uid
1047
- },
1048
- firstTimestamp: /* @__PURE__ */ new Date(),
1049
- reportingComponent,
1050
- reportingInstance
1051
- });
1052
- }
1053
- function getOwnerRefFrom(cr) {
1054
- const { name, uid } = cr.metadata;
1055
- return [
1056
- {
1057
- apiVersion: cr.apiVersion,
1058
- kind: cr.kind,
1059
- uid,
1060
- name
1061
- }
1062
- ];
1063
- }
1064
- function sanitizeResourceName(name) {
1065
- return name.toLowerCase().replace(/[^a-z0-9]+/g, "-").slice(0, 250).replace(/^[^a-z]+|[^a-z]+$/g, "");
1066
- }
1067
-
1068
- // src/lib/helpers.ts
1069
- function checkOverlap(bindingFilters, objectFilters) {
1070
- if (Object.keys(bindingFilters).length === 0) {
1071
- return true;
1072
- }
1073
- let matchCount = 0;
1074
- for (const key in bindingFilters) {
1075
- if (Object.prototype.hasOwnProperty.call(objectFilters, key)) {
1076
- const val1 = bindingFilters[key];
1077
- const val2 = objectFilters[key];
1078
- if (val1 === "" && key in objectFilters) {
1079
- matchCount++;
1080
- } else if (val1 !== "" && val1 === val2) {
1081
- matchCount++;
1082
- }
1083
- }
1084
- }
1085
- return matchCount === Object.keys(bindingFilters).length;
1086
- }
1087
- function filterNoMatchReason(binding, obj, capabilityNamespaces) {
1088
- if (binding.kind && binding.kind.kind === "Namespace" && binding.filters && binding.filters.namespaces.length !== 0) {
1089
- return `Ignoring Watch Callback: Cannot use a namespace filter in a namespace object.`;
1090
- }
1091
- if (typeof obj === "object" && obj !== null && "metadata" in obj && obj.metadata !== void 0 && binding.filters) {
1092
- if (obj.metadata.labels && !checkOverlap(binding.filters.labels, obj.metadata.labels)) {
1093
- return `Ignoring Watch Callback: No overlap between binding and object labels. Binding labels ${JSON.stringify(
1094
- binding.filters.labels
1095
- )}, Object Labels ${JSON.stringify(obj.metadata.labels)}.`;
1096
- }
1097
- if (obj.metadata.annotations && !checkOverlap(binding.filters.annotations, obj.metadata.annotations)) {
1098
- return `Ignoring Watch Callback: No overlap between binding and object annotations. Binding annotations ${JSON.stringify(
1099
- binding.filters.annotations
1100
- )}, Object annotations ${JSON.stringify(obj.metadata.annotations)}.`;
1101
- }
1102
- }
1103
- if (Array.isArray(capabilityNamespaces) && capabilityNamespaces.length > 0 && obj.metadata && obj.metadata.namespace && !capabilityNamespaces.includes(obj.metadata.namespace)) {
1104
- return `Ignoring Watch Callback: Object is not in the capability namespace. Capability namespaces: ${capabilityNamespaces.join(
1105
- ", "
1106
- )}, Object namespace: ${obj.metadata.namespace}.`;
1107
- }
1108
- if (Array.isArray(capabilityNamespaces) && capabilityNamespaces.length > 0 && binding.filters && Array.isArray(binding.filters.namespaces) && binding.filters.namespaces.length > 0 && !binding.filters.namespaces.every((ns) => capabilityNamespaces.includes(ns))) {
1109
- return `Ignoring Watch Callback: Binding namespace is not part of capability namespaces. Capability namespaces: ${capabilityNamespaces.join(
1110
- ", "
1111
- )}, Binding namespaces: ${binding.filters.namespaces.join(", ")}.`;
1112
- }
1113
- if (binding.filters && Array.isArray(binding.filters.namespaces) && binding.filters.namespaces.length > 0 && obj.metadata && obj.metadata.namespace && !binding.filters.namespaces.includes(obj.metadata.namespace)) {
1114
- return `Ignoring Watch Callback: Binding namespace and object namespace are not the same. Binding namespaces: ${binding.filters.namespaces.join(
1115
- ", "
1116
- )}, Object namespace: ${obj.metadata.namespace}.`;
1117
- }
1118
- return "";
1119
- }
1120
-
1121
- // src/lib/queue.ts
1122
- var Queue = class {
1123
- #queue = [];
1124
- #pendingPromise = false;
1125
- #reconcile;
1126
- constructor() {
1127
- this.#reconcile = async () => await new Promise((resolve) => resolve());
1128
- }
1129
- setReconcile(reconcile) {
1130
- this.#reconcile = reconcile;
1131
- }
1132
- /**
1133
- * Enqueue adds an item to the queue and returns a promise that resolves when the item is
1134
- * reconciled.
1135
- *
1136
- * @param item The object to reconcile
1137
- * @returns A promise that resolves when the object is reconciled
1138
- */
1139
- enqueue(item, type) {
1140
- logger_default.debug(`Enqueueing ${item.metadata.namespace}/${item.metadata.name}`);
1141
- return new Promise((resolve, reject) => {
1142
- this.#queue.push({ item, type, resolve, reject });
1143
- return this.#dequeue();
1144
- });
1145
- }
1146
- /**
1147
- * Dequeue reconciles the next item in the queue
1148
- *
1149
- * @returns A promise that resolves when the webapp is reconciled
1150
- */
1151
- async #dequeue() {
1152
- if (this.#pendingPromise) {
1153
- logger_default.debug("Pending promise, not dequeuing");
1154
- return false;
1155
- }
1156
- const element = this.#queue.shift();
1157
- if (!element) {
1158
- logger_default.debug("No element, not dequeuing");
1159
- return false;
1160
- }
1161
- try {
1162
- this.#pendingPromise = true;
1163
- if (this.#reconcile) {
1164
- logger_default.debug(`Reconciling ${element.item.metadata.name}`);
1165
- await this.#reconcile(element.item, element.type);
1166
- }
1167
- element.resolve();
1168
- } catch (e) {
1169
- logger_default.debug(`Error reconciling ${element.item.metadata.name}`, { error: e });
1170
- element.reject(e);
1171
- } finally {
1172
- logger_default.debug("Resetting pending promise and dequeuing");
1173
- this.#pendingPromise = false;
1174
- await this.#dequeue();
1175
- }
1176
- }
1177
- };
1178
-
1179
- // src/lib/watch-processor.ts
1180
- var watchCfg = {
1181
- retryMax: process.env.PEPR_RETRYMAX ? parseInt(process.env.PEPR_RETRYMAX, 10) : 5,
1182
- retryDelaySec: process.env.PEPR_RETRYDELAYSECONDS ? parseInt(process.env.PEPR_RETRYDELAYSECONDS, 10) : 5,
1183
- resyncIntervalSec: process.env.PEPR_RESYNCINTERVALSECONDS ? parseInt(process.env.PEPR_RESYNCINTERVALSECONDS, 10) : 300
1184
- };
1185
- var eventToPhaseMap = {
1186
- ["CREATE" /* Create */]: [import_types2.WatchPhase.Added],
1187
- ["UPDATE" /* Update */]: [import_types2.WatchPhase.Modified],
1188
- ["CREATEORUPDATE" /* CreateOrUpdate */]: [import_types2.WatchPhase.Added, import_types2.WatchPhase.Modified],
1189
- ["DELETE" /* Delete */]: [import_types2.WatchPhase.Deleted],
1190
- ["*" /* Any */]: [import_types2.WatchPhase.Added, import_types2.WatchPhase.Modified, import_types2.WatchPhase.Deleted]
1191
- };
1192
- function setupWatch(capabilities) {
1193
- capabilities.map(
1194
- (capability) => capability.bindings.filter((binding) => binding.isWatch).forEach((bindingElement) => runBinding(bindingElement, capability.namespaces))
1195
- );
1196
- }
1197
- async function runBinding(binding, capabilityNamespaces) {
1198
- const phaseMatch = eventToPhaseMap[binding.event] || eventToPhaseMap["*" /* Any */];
1199
- logger_default.debug({ watchCfg }, "Effective WatchConfig");
1200
- const watchCallback = async (obj, type) => {
1201
- if (phaseMatch.includes(type)) {
1202
- try {
1203
- const filterMatch = filterNoMatchReason(binding, obj, capabilityNamespaces);
1204
- if (filterMatch === "") {
1205
- await binding.watchCallback?.(obj, type);
1206
- } else {
1207
- logger_default.debug(filterMatch);
1208
- }
1209
- } catch (e) {
1210
- logger_default.error(e, "Error executing watch callback");
1211
- }
1212
- }
1213
- };
1214
- const queue = new Queue();
1215
- queue.setReconcile(watchCallback);
1216
- const watcher = (0, import_kubernetes_fluent_client5.K8s)(binding.model, binding.filters).Watch(async (obj, type) => {
1217
- logger_default.debug(obj, `Watch event ${type} received`);
1218
- if (binding.isQueue) {
1219
- await queue.enqueue(obj, type);
1220
- } else {
1221
- await watchCallback(obj, type);
1222
- }
1223
- }, watchCfg);
1224
- watcher.events.on(import_kubernetes_fluent_client5.WatchEvent.GIVE_UP, (err) => {
1225
- logger_default.error(err, "Watch failed after 5 attempts, giving up");
1226
- process.exit(1);
1227
- });
1228
- watcher.events.on(import_kubernetes_fluent_client5.WatchEvent.CONNECT, (url) => logEvent(import_kubernetes_fluent_client5.WatchEvent.CONNECT, url));
1229
- watcher.events.on(import_kubernetes_fluent_client5.WatchEvent.DATA_ERROR, (err) => logEvent(import_kubernetes_fluent_client5.WatchEvent.DATA_ERROR, err.message));
1230
- watcher.events.on(
1231
- import_kubernetes_fluent_client5.WatchEvent.RECONNECT,
1232
- (err, retryCount) => logEvent(import_kubernetes_fluent_client5.WatchEvent.RECONNECT, err ? `Reconnecting after ${retryCount} attempts` : "")
1233
- );
1234
- watcher.events.on(import_kubernetes_fluent_client5.WatchEvent.RECONNECT_PENDING, () => logEvent(import_kubernetes_fluent_client5.WatchEvent.RECONNECT_PENDING));
1235
- watcher.events.on(import_kubernetes_fluent_client5.WatchEvent.GIVE_UP, (err) => logEvent(import_kubernetes_fluent_client5.WatchEvent.GIVE_UP, err.message));
1236
- watcher.events.on(import_kubernetes_fluent_client5.WatchEvent.ABORT, (err) => logEvent(import_kubernetes_fluent_client5.WatchEvent.ABORT, err.message));
1237
- watcher.events.on(import_kubernetes_fluent_client5.WatchEvent.OLD_RESOURCE_VERSION, (err) => logEvent(import_kubernetes_fluent_client5.WatchEvent.OLD_RESOURCE_VERSION, err));
1238
- watcher.events.on(import_kubernetes_fluent_client5.WatchEvent.NETWORK_ERROR, (err) => logEvent(import_kubernetes_fluent_client5.WatchEvent.NETWORK_ERROR, err.message));
1239
- watcher.events.on(import_kubernetes_fluent_client5.WatchEvent.LIST_ERROR, (err) => logEvent(import_kubernetes_fluent_client5.WatchEvent.LIST_ERROR, err.message));
1240
- watcher.events.on(import_kubernetes_fluent_client5.WatchEvent.LIST, (list) => logEvent(import_kubernetes_fluent_client5.WatchEvent.LIST, JSON.stringify(list, void 0, 2)));
1241
- try {
1242
- await watcher.start();
1243
- } catch (err) {
1244
- logger_default.error(err, "Error starting watch");
1245
- process.exit(1);
1246
- }
1247
- }
1248
- function logEvent(type, message = "", obj) {
1249
- const logMessage = `Watch event ${type} received${message ? `. ${message}.` : "."}`;
1250
- if (obj) {
1251
- logger_default.debug(obj, logMessage);
1252
- } else {
1253
- logger_default.debug(logMessage);
1254
- }
1255
- }
1256
-
1257
- // src/lib/module.ts
1258
- var isWatchMode = () => process.env.PEPR_WATCH_MODE === "true";
1259
- var isBuildMode = () => process.env.PEPR_MODE === "build";
1260
- var isDevMode = () => process.env.PEPR_MODE === "dev";
1261
- var PeprModule = class {
1262
- #controller;
1263
- /**
1264
- * Create a new Pepr runtime
1265
- *
1266
- * @param config The configuration for the Pepr runtime
1267
- * @param capabilities The capabilities to be loaded into the Pepr runtime
1268
- * @param opts Options for the Pepr runtime
1269
- */
1270
- constructor({ description, pepr }, capabilities = [], opts = {}) {
1271
- const config = (0, import_ramda4.clone)(pepr);
1272
- config.description = description;
1273
- ValidateError(config.onError);
1274
- if (isBuildMode()) {
1275
- if (!process.send) {
1276
- throw new Error("process.send is not defined");
1277
- }
1278
- const exportedCapabilities = [];
1279
- for (const capability of capabilities) {
1280
- exportedCapabilities.push({
1281
- name: capability.name,
1282
- description: capability.description,
1283
- namespaces: capability.namespaces,
1284
- bindings: capability.bindings,
1285
- hasSchedule: capability.hasSchedule
1286
- });
1287
- }
1288
- process.send(exportedCapabilities);
1289
- return;
1290
- }
1291
- this.#controller = new Controller(config, capabilities, opts.beforeHook, opts.afterHook, () => {
1292
- if (isWatchMode() || isDevMode()) {
1293
- try {
1294
- setupWatch(capabilities);
1295
- } catch (e) {
1296
- logger_default.error(e, "Error setting up watch");
1297
- process.exit(1);
1298
- }
1299
- }
1300
- });
1301
- if (opts.deferStart) {
1302
- return;
1303
- }
1304
- this.start();
1305
- }
1306
- /**
1307
- * Start the Pepr runtime manually.
1308
- * Normally this is called automatically when the Pepr module is instantiated, but can be called manually if `deferStart` is set to `true` in the constructor.
1309
- *
1310
- * @param port
1311
- */
1312
- start = (port = 3e3) => {
1313
- this.#controller.startServer(port);
1314
- };
1315
- };
1316
-
1317
- // src/lib/storage.ts
1318
- var import_ramda5 = require("ramda");
1319
- var MAX_WAIT_TIME = 15e3;
1320
- var Storage = class {
1321
- #store = {};
1322
- #send;
1323
- #subscribers = {};
1324
- #subscriberId = 0;
1325
- #readyHandlers = [];
1326
- registerSender = (send) => {
1327
- this.#send = send;
1328
- };
1329
- receive = (data) => {
1330
- logger_default.debug(data, `Pepr store data received`);
1331
- this.#store = data || {};
1332
- this.#onReady();
1333
- for (const idx in this.#subscribers) {
1334
- this.#subscribers[idx]((0, import_ramda5.clone)(this.#store));
1335
- }
1336
- };
1337
- getItem = (key) => {
1338
- return this.#store[key] || null;
1339
- };
1340
- clear = () => {
1341
- this.#dispatchUpdate("remove", Object.keys(this.#store));
1342
- };
1343
- removeItem = (key) => {
1344
- this.#dispatchUpdate("remove", [key]);
1345
- };
1346
- setItem = (key, value) => {
1347
- this.#dispatchUpdate("add", [key], value);
1348
- };
1349
- /**
1350
- * Creates a promise and subscribes to the store, the promise resolves when
1351
- * the key and value are seen in the store.
1352
- *
1353
- * @param key - The key to add into the store
1354
- * @param value - The value of the key
1355
- * @returns
1356
- */
1357
- setItemAndWait = (key, value) => {
1358
- this.#dispatchUpdate("add", [key], value);
1359
- return new Promise((resolve, reject) => {
1360
- const unsubscribe = this.subscribe((data) => {
1361
- if (data[key] === value) {
1362
- unsubscribe();
1363
- resolve();
1364
- }
1365
- });
1366
- setTimeout(() => {
1367
- unsubscribe();
1368
- return reject();
1369
- }, MAX_WAIT_TIME);
1370
- });
1371
- };
1372
- /**
1373
- * Creates a promise and subscribes to the store, the promise resolves when
1374
- * the key is removed from the store.
1375
- *
1376
- * @param key - The key to add into the store
1377
- * @returns
1378
- */
1379
- removeItemAndWait = (key) => {
1380
- this.#dispatchUpdate("remove", [key]);
1381
- return new Promise((resolve, reject) => {
1382
- const unsubscribe = this.subscribe((data) => {
1383
- if (!Object.hasOwn(data, key)) {
1384
- unsubscribe();
1385
- resolve();
1386
- }
1387
- });
1388
- setTimeout(() => {
1389
- unsubscribe();
1390
- return reject();
1391
- }, MAX_WAIT_TIME);
1392
- });
1393
- };
1394
- subscribe = (subscriber) => {
1395
- const idx = this.#subscriberId++;
1396
- this.#subscribers[idx] = subscriber;
1397
- return () => this.unsubscribe(idx);
1398
- };
1399
- onReady = (callback) => {
1400
- this.#readyHandlers.push(callback);
1401
- };
1402
- /**
1403
- * Remove a subscriber from the list of subscribers.
1404
- * @param idx - The index of the subscriber to remove.
1405
- */
1406
- unsubscribe = (idx) => {
1407
- delete this.#subscribers[idx];
1408
- };
1409
- #onReady = () => {
1410
- for (const handler of this.#readyHandlers) {
1411
- handler((0, import_ramda5.clone)(this.#store));
1412
- }
1413
- this.#onReady = () => {
1414
- };
1415
- };
1416
- /**
1417
- * Dispatch an update to the store and notify all subscribers.
1418
- * @param op - The type of operation to perform.
1419
- * @param keys - The keys to update.
1420
- * @param [value] - The new value.
1421
- */
1422
- #dispatchUpdate = (op, keys, value) => {
1423
- this.#send(op, keys, value);
1424
- };
1425
- };
1426
-
1427
- // src/lib/schedule.ts
1428
- var OnSchedule = class {
1429
- intervalId = null;
1430
- store;
1431
- name;
1432
- completions;
1433
- every;
1434
- unit;
1435
- run;
1436
- startTime;
1437
- duration;
1438
- lastTimestamp;
1439
- constructor(schedule) {
1440
- this.name = schedule.name;
1441
- this.run = schedule.run;
1442
- this.every = schedule.every;
1443
- this.unit = schedule.unit;
1444
- this.startTime = schedule?.startTime;
1445
- this.completions = schedule?.completions;
1446
- }
1447
- setStore(store) {
1448
- this.store = store;
1449
- this.startInterval();
1450
- }
1451
- startInterval() {
1452
- this.checkStore();
1453
- this.getDuration();
1454
- this.setupInterval();
1455
- }
1456
- /**
1457
- * Checks the store for this schedule and sets the values if it exists
1458
- * @returns
1459
- */
1460
- checkStore() {
1461
- const result = this.store && this.store.getItem(this.name);
1462
- if (result) {
1463
- const storedSchedule = JSON.parse(result);
1464
- this.completions = storedSchedule?.completions;
1465
- this.startTime = storedSchedule?.startTime;
1466
- this.lastTimestamp = storedSchedule?.lastTimestamp;
1467
- }
1468
- }
1469
- /**
1470
- * Saves the schedule to the store
1471
- * @returns
1472
- */
1473
- saveToStore() {
1474
- const schedule = {
1475
- completions: this.completions,
1476
- startTime: this.startTime,
1477
- lastTimestamp: /* @__PURE__ */ new Date(),
1478
- name: this.name
1479
- };
1480
- this.store && this.store.setItem(this.name, JSON.stringify(schedule));
1481
- }
1482
- /**
1483
- * Gets the durations in milliseconds
1484
- */
1485
- getDuration() {
1486
- switch (this.unit) {
1487
- case "seconds":
1488
- if (this.every < 10)
1489
- throw new Error("10 Seconds in the smallest interval allowed");
1490
- this.duration = 1e3 * this.every;
1491
- break;
1492
- case "minutes":
1493
- case "minute":
1494
- this.duration = 1e3 * 60 * this.every;
1495
- break;
1496
- case "hours":
1497
- case "hour":
1498
- this.duration = 1e3 * 60 * 60 * this.every;
1499
- break;
1500
- default:
1501
- throw new Error("Invalid time unit");
1502
- }
1503
- }
1504
- /**
1505
- * Sets up the interval
1506
- */
1507
- setupInterval() {
1508
- const now = /* @__PURE__ */ new Date();
1509
- let delay;
1510
- if (this.lastTimestamp && this.startTime) {
1511
- this.startTime = void 0;
1512
- }
1513
- if (this.startTime) {
1514
- delay = this.startTime.getTime() - now.getTime();
1515
- } else if (this.lastTimestamp && this.duration) {
1516
- const lastTimestamp = new Date(this.lastTimestamp);
1517
- delay = this.duration - (now.getTime() - lastTimestamp.getTime());
1518
- }
1519
- if (delay === void 0 || delay <= 0) {
1520
- this.start();
1521
- } else {
1522
- setTimeout(() => {
1523
- this.start();
1524
- }, delay);
1525
- }
1526
- }
1527
- /**
1528
- * Starts the interval
1529
- */
1530
- start() {
1531
- this.intervalId = setInterval(() => {
1532
- if (this.completions === 0) {
1533
- this.stop();
1534
- return;
1535
- } else {
1536
- this.run();
1537
- if (this.completions && this.completions !== 0) {
1538
- this.completions -= 1;
1539
- }
1540
- this.saveToStore();
1541
- }
1542
- }, this.duration);
1543
- }
1544
- /**
1545
- * Stops the interval
1546
- */
1547
- stop() {
1548
- if (this.intervalId) {
1549
- clearInterval(this.intervalId);
1550
- this.intervalId = null;
1551
- }
1552
- this.store && this.store.removeItem(this.name);
1553
- }
1554
- };
1555
-
1556
- // src/lib/capability.ts
1557
- var registerAdmission = isBuildMode() || !isWatchMode();
1558
- var registerWatch = isBuildMode() || isWatchMode() || isDevMode();
1559
- var Capability = class {
1560
- #name;
1561
- #description;
1562
- #namespaces;
1563
- #bindings = [];
1564
- #store = new Storage();
1565
- #scheduleStore = new Storage();
1566
- #registered = false;
1567
- #scheduleRegistered = false;
1568
- hasSchedule;
1569
- /**
1570
- * Run code on a schedule with the capability.
1571
- *
1572
- * @param schedule The schedule to run the code on
1573
- * @returns
1574
- */
1575
- OnSchedule = (schedule) => {
1576
- const { name, every, unit, run, startTime, completions } = schedule;
1577
- this.hasSchedule = true;
1578
- if (process.env.PEPR_WATCH_MODE === "true" || process.env.PEPR_MODE === "dev") {
1579
- const newSchedule = {
1580
- name,
1581
- every,
1582
- unit,
1583
- run,
1584
- startTime,
1585
- completions
1586
- };
1587
- this.#scheduleStore.onReady(() => {
1588
- new OnSchedule(newSchedule).setStore(this.#scheduleStore);
1589
- });
1590
- }
1591
- };
1592
- /**
1593
- * Store is a key-value data store that can be used to persist data that should be shared
1594
- * between requests. Each capability has its own store, and the data is persisted in Kubernetes
1595
- * in the `pepr-system` namespace.
1596
- *
1597
- * Note: You should only access the store from within an action.
1598
- */
1599
- Store = {
1600
- clear: this.#store.clear,
1601
- getItem: this.#store.getItem,
1602
- removeItem: this.#store.removeItem,
1603
- removeItemAndWait: this.#store.removeItemAndWait,
1604
- setItem: this.#store.setItem,
1605
- subscribe: this.#store.subscribe,
1606
- onReady: this.#store.onReady,
1607
- setItemAndWait: this.#store.setItemAndWait
1608
- };
1609
- /**
1610
- * ScheduleStore is a key-value data store used to persist schedule data that should be shared
1611
- * between intervals. Each Schedule shares store, and the data is persisted in Kubernetes
1612
- * in the `pepr-system` namespace.
1613
- *
1614
- * Note: There is no direct access to schedule store
1615
- */
1616
- ScheduleStore = {
1617
- clear: this.#scheduleStore.clear,
1618
- getItem: this.#scheduleStore.getItem,
1619
- removeItemAndWait: this.#scheduleStore.removeItemAndWait,
1620
- removeItem: this.#scheduleStore.removeItem,
1621
- setItemAndWait: this.#scheduleStore.setItemAndWait,
1622
- setItem: this.#scheduleStore.setItem,
1623
- subscribe: this.#scheduleStore.subscribe,
1624
- onReady: this.#scheduleStore.onReady
1625
- };
1626
- get bindings() {
1627
- return this.#bindings;
1628
- }
1629
- get name() {
1630
- return this.#name;
1631
- }
1632
- get description() {
1633
- return this.#description;
1634
- }
1635
- get namespaces() {
1636
- return this.#namespaces || [];
1637
- }
1638
- constructor(cfg) {
1639
- this.#name = cfg.name;
1640
- this.#description = cfg.description;
1641
- this.#namespaces = cfg.namespaces;
1642
- this.hasSchedule = false;
1643
- logger_default.info(`Capability ${this.#name} registered`);
1644
- logger_default.debug(cfg);
1645
- }
1646
- /**
1647
- * Register the store with the capability. This is called automatically by the Pepr controller.
1648
- *
1649
- * @param store
1650
- */
1651
- registerScheduleStore = () => {
1652
- logger_default.info(`Registering schedule store for ${this.#name}`);
1653
- if (this.#scheduleRegistered) {
1654
- throw new Error(`Schedule store already registered for ${this.#name}`);
1655
- }
1656
- this.#scheduleRegistered = true;
1657
- return {
1658
- scheduleStore: this.#scheduleStore
1659
- };
1660
- };
1661
- /**
1662
- * Register the store with the capability. This is called automatically by the Pepr controller.
1663
- *
1664
- * @param store
1665
- */
1666
- registerStore = () => {
1667
- logger_default.info(`Registering store for ${this.#name}`);
1668
- if (this.#registered) {
1669
- throw new Error(`Store already registered for ${this.#name}`);
1670
- }
1671
- this.#registered = true;
1672
- return {
1673
- store: this.#store
1674
- };
1675
- };
1676
- /**
1677
- * The When method is used to register a action to be executed when a Kubernetes resource is
1678
- * processed by Pepr. The action will be executed if the resource matches the specified kind and any
1679
- * filters that are applied.
1680
- *
1681
- * @param model the KubernetesObject model to match
1682
- * @param kind if using a custom KubernetesObject not available in `a.*`, specify the GroupVersionKind
1683
- * @returns
1684
- */
1685
- When = (model, kind4) => {
1686
- const matchedKind = (0, import_kubernetes_fluent_client6.modelToGroupVersionKind)(model.name);
1687
- if (!matchedKind && !kind4) {
1688
- throw new Error(`Kind not specified for ${model.name}`);
1689
- }
1690
- const binding = {
1691
- model,
1692
- // If the kind is not specified, use the matched kind from the model
1693
- kind: kind4 || matchedKind,
1694
- event: "*" /* Any */,
1695
- filters: {
1696
- name: "",
1697
- namespaces: [],
1698
- labels: {},
1699
- annotations: {}
1700
- }
1701
- };
1702
- const bindings = this.#bindings;
1703
- const prefix = `${this.#name}: ${model.name}`;
1704
- const commonChain = { WithLabel, WithAnnotation, Mutate, Validate, Watch, Reconcile };
1705
- const isNotEmpty = (value) => Object.keys(value).length > 0;
1706
- const log = (message, cbString) => {
1707
- const filteredObj = (0, import_ramda6.pickBy)(isNotEmpty, binding.filters);
1708
- logger_default.info(`${message} configured for ${binding.event}`, prefix);
1709
- logger_default.info(filteredObj, prefix);
1710
- logger_default.debug(cbString, prefix);
1711
- };
1712
- function Validate(validateCallback) {
1713
- if (registerAdmission) {
1714
- log("Validate Action", validateCallback.toString());
1715
- bindings.push({
1716
- ...binding,
1717
- isValidate: true,
1718
- validateCallback
1719
- });
1720
- }
1721
- return { Watch, Reconcile };
1722
- }
1723
- function Mutate(mutateCallback) {
1724
- if (registerAdmission) {
1725
- log("Mutate Action", mutateCallback.toString());
1726
- bindings.push({
1727
- ...binding,
1728
- isMutate: true,
1729
- mutateCallback
1730
- });
1731
- }
1732
- return { Watch, Validate, Reconcile };
1733
- }
1734
- function Watch(watchCallback) {
1735
- if (registerWatch) {
1736
- log("Watch Action", watchCallback.toString());
1737
- bindings.push({
1738
- ...binding,
1739
- isWatch: true,
1740
- watchCallback
1741
- });
1742
- }
1743
- }
1744
- function Reconcile(watchCallback) {
1745
- if (registerWatch) {
1746
- log("Reconcile Action", watchCallback.toString());
1747
- bindings.push({
1748
- ...binding,
1749
- isWatch: true,
1750
- isQueue: true,
1751
- watchCallback
1752
- });
1753
- }
1754
- }
1755
- function InNamespace(...namespaces) {
1756
- logger_default.debug(`Add namespaces filter ${namespaces}`, prefix);
1757
- binding.filters.namespaces.push(...namespaces);
1758
- return { ...commonChain, WithName };
1759
- }
1760
- function WithName(name) {
1761
- logger_default.debug(`Add name filter ${name}`, prefix);
1762
- binding.filters.name = name;
1763
- return commonChain;
1764
- }
1765
- function WithLabel(key, value = "") {
1766
- logger_default.debug(`Add label filter ${key}=${value}`, prefix);
1767
- binding.filters.labels[key] = value;
1768
- return commonChain;
1769
- }
1770
- function WithAnnotation(key, value = "") {
1771
- logger_default.debug(`Add annotation filter ${key}=${value}`, prefix);
1772
- binding.filters.annotations[key] = value;
1773
- return commonChain;
1774
- }
1775
- function bindEvent(event) {
1776
- binding.event = event;
1777
- return {
1778
- ...commonChain,
1779
- InNamespace,
1780
- WithName
1781
- };
1782
- }
1783
- return {
1784
- IsCreatedOrUpdated: () => bindEvent("CREATEORUPDATE" /* CreateOrUpdate */),
1785
- IsCreated: () => bindEvent("CREATE" /* Create */),
1786
- IsUpdated: () => bindEvent("UPDATE" /* Update */),
1787
- IsDeleted: () => bindEvent("DELETE" /* Delete */)
1788
- };
1789
- };
1790
- };
1791
- // Annotate the CommonJS export names for ESM import in node:
1792
- 0 && (module.exports = {
1793
- Capability,
1794
- K8s,
1795
- Log,
1796
- PeprModule,
1797
- PeprMutateRequest,
1798
- PeprUtils,
1799
- PeprValidateRequest,
1800
- R,
1801
- RegisterKind,
1802
- a,
1803
- fetch,
1804
- fetchStatus,
1805
- kind,
1806
- sdk
1807
- });
1808
- //# sourceMappingURL=lib.js.map