pepr 0.45.0 → 0.45.1-nightly.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (63) hide show
  1. package/README.md +15 -7
  2. package/dist/cli/build.d.ts +1 -1
  3. package/dist/cli/build.d.ts.map +1 -1
  4. package/dist/cli/build.helpers.d.ts +1 -1
  5. package/dist/cli/init/templates.d.ts +5 -2
  6. package/dist/cli/init/templates.d.ts.map +1 -1
  7. package/dist/cli.js +31 -37
  8. package/dist/controller.js +1 -1
  9. package/dist/lib/assets/assets.d.ts +3 -3
  10. package/dist/lib/assets/assets.d.ts.map +1 -1
  11. package/dist/lib/assets/index.d.ts +1 -1
  12. package/dist/lib/assets/index.d.ts.map +1 -1
  13. package/dist/lib/assets/networking.d.ts +1 -1
  14. package/dist/lib/assets/networking.d.ts.map +1 -1
  15. package/dist/lib/assets/pods.d.ts +1 -1
  16. package/dist/lib/assets/pods.d.ts.map +1 -1
  17. package/dist/lib/assets/yaml/overridesFile.d.ts +3 -3
  18. package/dist/lib/assets/yaml/overridesFile.d.ts.map +1 -1
  19. package/dist/lib/controller/index.d.ts +1 -1
  20. package/dist/lib/controller/index.d.ts.map +1 -1
  21. package/dist/lib/core/envChecks.d.ts +4 -0
  22. package/dist/lib/core/envChecks.d.ts.map +1 -0
  23. package/dist/lib/core/module.d.ts +1 -51
  24. package/dist/lib/core/module.d.ts.map +1 -1
  25. package/dist/lib/processors/mutate-processor.d.ts +1 -1
  26. package/dist/lib/processors/mutate-processor.d.ts.map +1 -1
  27. package/dist/lib/processors/validate-processor.d.ts +1 -1
  28. package/dist/lib/processors/validate-processor.d.ts.map +1 -1
  29. package/dist/lib/processors/watch-processor.d.ts +22 -1
  30. package/dist/lib/processors/watch-processor.d.ts.map +1 -1
  31. package/dist/lib/telemetry/metrics.d.ts +2 -3
  32. package/dist/lib/telemetry/metrics.d.ts.map +1 -1
  33. package/dist/lib/types.d.ts +45 -0
  34. package/dist/lib/types.d.ts.map +1 -1
  35. package/dist/lib.js +2019 -2019
  36. package/dist/lib.js.map +4 -4
  37. package/dist/sdk/sdk.d.ts +6 -1
  38. package/dist/sdk/sdk.d.ts.map +1 -1
  39. package/package.json +11 -8
  40. package/src/cli/build.helpers.ts +1 -1
  41. package/src/cli/build.ts +3 -11
  42. package/src/cli/dev.ts +1 -1
  43. package/src/cli/init/templates.ts +1 -1
  44. package/src/lib/assets/assets.ts +8 -8
  45. package/src/lib/assets/deploy.ts +4 -4
  46. package/src/lib/assets/destroy.ts +1 -1
  47. package/src/lib/assets/helm.ts +4 -4
  48. package/src/lib/assets/index.ts +2 -3
  49. package/src/lib/assets/networking.ts +3 -3
  50. package/src/lib/assets/pods.ts +5 -5
  51. package/src/lib/assets/webhooks.ts +5 -5
  52. package/src/lib/assets/yaml/generateAllYaml.ts +3 -3
  53. package/src/lib/assets/yaml/overridesFile.ts +4 -4
  54. package/src/lib/controller/index.ts +20 -19
  55. package/src/lib/core/capability.ts +1 -1
  56. package/src/lib/core/envChecks.ts +6 -0
  57. package/src/lib/core/module.ts +3 -62
  58. package/src/lib/processors/mutate-processor.ts +1 -1
  59. package/src/lib/processors/validate-processor.ts +1 -1
  60. package/src/lib/processors/watch-processor.ts +58 -33
  61. package/src/lib/telemetry/metrics.ts +4 -4
  62. package/src/lib/types.ts +48 -0
  63. package/src/sdk/sdk.ts +8 -4
package/dist/lib.js CHANGED
@@ -50,8 +50,8 @@ var import_kubernetes_fluent_client8 = require("kubernetes-fluent-client");
50
50
  var R = __toESM(require("ramda"));
51
51
 
52
52
  // src/lib/core/capability.ts
53
- var import_kubernetes_fluent_client6 = require("kubernetes-fluent-client");
54
- var import_ramda9 = require("ramda");
53
+ var import_kubernetes_fluent_client2 = require("kubernetes-fluent-client");
54
+ var import_ramda2 = require("ramda");
55
55
 
56
56
  // src/lib/telemetry/logger.ts
57
57
  var import_pino = require("pino");
@@ -101,2222 +101,2221 @@ function redactedPatch(patch = {}) {
101
101
  }
102
102
  var logger_default = Log;
103
103
 
104
- // src/lib/core/module.ts
105
- var import_ramda7 = require("ramda");
106
-
107
- // src/lib/controller/index.ts
108
- var import_express = __toESM(require("express"));
109
- var import_fs = __toESM(require("fs"));
110
- var import_https = __toESM(require("https"));
104
+ // src/lib/core/envChecks.ts
105
+ var isWatchMode = () => process.env.PEPR_WATCH_MODE === "true";
106
+ var isBuildMode = () => process.env.PEPR_MODE === "build";
107
+ var isDevMode = () => process.env.PEPR_MODE === "dev";
111
108
 
112
- // src/lib/telemetry/metrics.ts
113
- var import_perf_hooks = require("perf_hooks");
114
- var import_prom_client = __toESM(require("prom-client"));
115
- var loggingPrefix = "MetricsCollector";
116
- var MetricsCollector = class {
117
- #registry;
118
- #counters = /* @__PURE__ */ new Map();
119
- #gauges = /* @__PURE__ */ new Map();
120
- #summaries = /* @__PURE__ */ new Map();
121
- #prefix;
122
- #cacheMissWindows = /* @__PURE__ */ new Map();
123
- #metricNames = {
124
- errors: "errors",
125
- alerts: "alerts",
126
- mutate: "mutate",
127
- validate: "validate",
128
- cacheMiss: "cache_miss",
129
- resyncFailureCount: "resync_failure_count"
109
+ // src/lib/core/storage.ts
110
+ var import_ramda = require("ramda");
111
+ var import_json_pointer = __toESM(require("json-pointer"));
112
+ var MAX_WAIT_TIME = 15e3;
113
+ var STORE_VERSION_PREFIX = "v2";
114
+ function v2StoreKey(key) {
115
+ return `${STORE_VERSION_PREFIX}-${import_json_pointer.default.escape(key)}`;
116
+ }
117
+ function v2UnescapedStoreKey(key) {
118
+ return `${STORE_VERSION_PREFIX}-${key}`;
119
+ }
120
+ var Storage = class {
121
+ #store = {};
122
+ #send;
123
+ #subscribers = {};
124
+ #subscriberId = 0;
125
+ #readyHandlers = [];
126
+ registerSender = (send) => {
127
+ this.#send = send;
130
128
  };
131
- /**
132
- * Creates a MetricsCollector instance with prefixed metrics.
133
- * @param [prefix='pepr'] - The prefix for the metric names.
134
- */
135
- constructor(prefix = "pepr") {
136
- this.#registry = new import_prom_client.Registry();
137
- this.#prefix = prefix;
138
- this.addCounter(this.#metricNames.errors, "Mutation/Validate errors encountered");
139
- this.addCounter(this.#metricNames.alerts, "Mutation/Validate bad api token received");
140
- this.addSummary(this.#metricNames.mutate, "Mutation operation summary");
141
- this.addSummary(this.#metricNames.validate, "Validation operation summary");
142
- this.addGauge(this.#metricNames.cacheMiss, "Number of cache misses per window", ["window"]);
143
- this.addGauge(this.#metricNames.resyncFailureCount, "Number of failures per resync operation", ["count"]);
144
- }
145
- #getMetricName = (name2) => `${this.#prefix}_${name2}`;
146
- #addMetric = (collection, MetricType, { name: name2, help, labelNames }) => {
147
- if (collection.has(this.#getMetricName(name2))) {
148
- logger_default.debug(`Metric for ${name2} already exists`, loggingPrefix);
149
- return;
129
+ receive = (data) => {
130
+ this.#store = data || {};
131
+ this.#onReady();
132
+ for (const idx in this.#subscribers) {
133
+ this.#subscribers[idx]((0, import_ramda.clone)(this.#store));
150
134
  }
151
- const metric = new MetricType({
152
- name: this.#getMetricName(name2),
153
- help,
154
- registers: [this.#registry],
155
- labelNames
156
- });
157
- collection.set(this.#getMetricName(name2), metric);
158
135
  };
159
- addCounter = (name2, help) => {
160
- this.#addMetric(this.#counters, import_prom_client.default.Counter, { name: name2, help, labelNames: [] });
161
- };
162
- addSummary = (name2, help) => {
163
- this.#addMetric(this.#summaries, import_prom_client.default.Summary, { name: name2, help, labelNames: [] });
136
+ getItem = (key) => {
137
+ const result = this.#store[v2UnescapedStoreKey(key)] || null;
138
+ if (result !== null && typeof result !== "function" && typeof result !== "object") {
139
+ return result;
140
+ }
141
+ return null;
164
142
  };
165
- addGauge = (name2, help, labelNames) => {
166
- this.#addMetric(this.#gauges, import_prom_client.default.Gauge, { name: name2, help, labelNames });
143
+ clear = () => {
144
+ if (Object.keys(this.#store).length > 0) {
145
+ this.#dispatchUpdate(
146
+ "remove",
147
+ Object.keys(this.#store).map((key) => import_json_pointer.default.escape(key))
148
+ );
149
+ }
167
150
  };
168
- incCounter = (name2) => {
169
- this.#counters.get(this.#getMetricName(name2))?.inc();
151
+ removeItem = (key) => {
152
+ this.#dispatchUpdate("remove", [v2StoreKey(key)]);
170
153
  };
171
- incGauge = (name2, labels, value = 1) => {
172
- this.#gauges.get(this.#getMetricName(name2))?.inc(labels || {}, value);
154
+ setItem = (key, value) => {
155
+ this.#dispatchUpdate("add", [v2StoreKey(key)], value);
173
156
  };
174
157
  /**
175
- * Increments the error counter.
158
+ * Creates a promise and subscribes to the store, the promise resolves when
159
+ * the key and value are seen in the store.
160
+ *
161
+ * @param key - The key to add into the store
162
+ * @param value - The value of the key
163
+ * @returns
176
164
  */
177
- error = () => this.incCounter(this.#metricNames.errors);
165
+ setItemAndWait = (key, value) => {
166
+ this.#dispatchUpdate("add", [v2StoreKey(key)], value);
167
+ const record = {};
168
+ return new Promise((resolve, reject) => {
169
+ record.timeout = setTimeout(() => {
170
+ record.unsubscribe();
171
+ return reject(`MAX_WAIT_TIME elapsed: Key ${key} not seen in ${MAX_WAIT_TIME / 1e3}s`);
172
+ }, MAX_WAIT_TIME);
173
+ record.unsubscribe = this.subscribe((data) => {
174
+ if (data[`${v2UnescapedStoreKey(key)}`] === value) {
175
+ record.unsubscribe();
176
+ clearTimeout(record.timeout);
177
+ resolve("ok");
178
+ }
179
+ });
180
+ });
181
+ };
178
182
  /**
179
- * Increments the alerts counter.
183
+ * Creates a promise and subscribes to the store, the promise resolves when
184
+ * the key is removed from the store.
185
+ *
186
+ * @param key - The key to add into the store
187
+ * @returns
180
188
  */
181
- alert = () => this.incCounter(this.#metricNames.alerts);
189
+ removeItemAndWait = (key) => {
190
+ this.#dispatchUpdate("remove", [v2StoreKey(key)]);
191
+ const record = {};
192
+ return new Promise((resolve, reject) => {
193
+ record.timeout = setTimeout(() => {
194
+ record.unsubscribe();
195
+ return reject(`MAX_WAIT_TIME elapsed: Key ${key} still seen after ${MAX_WAIT_TIME / 1e3}s`);
196
+ }, MAX_WAIT_TIME);
197
+ record.unsubscribe = this.subscribe((data) => {
198
+ if (!Object.hasOwn(data, `${v2UnescapedStoreKey(key)}`)) {
199
+ record.unsubscribe();
200
+ clearTimeout(record.timeout);
201
+ resolve("ok");
202
+ }
203
+ });
204
+ });
205
+ };
206
+ subscribe = (subscriber) => {
207
+ const idx = this.#subscriberId++;
208
+ this.#subscribers[idx] = subscriber;
209
+ return () => this.unsubscribe(idx);
210
+ };
211
+ onReady = (callback) => {
212
+ this.#readyHandlers.push(callback);
213
+ };
182
214
  /**
183
- * Observes the duration since the provided start time and updates the summary.
184
- * @param startTime - The start time.
185
- * @param name - The metrics summary to increment.
215
+ * Remove a subscriber from the list of subscribers.
216
+ * @param idx - The index of the subscriber to remove.
186
217
  */
187
- observeEnd = (startTime, name2 = this.#metricNames.mutate) => {
188
- this.#summaries.get(this.#getMetricName(name2))?.observe(import_perf_hooks.performance.now() - startTime);
218
+ unsubscribe = (idx) => {
219
+ delete this.#subscribers[idx];
220
+ };
221
+ #onReady = () => {
222
+ for (const handler of this.#readyHandlers) {
223
+ handler((0, import_ramda.clone)(this.#store));
224
+ }
225
+ this.#onReady = () => {
226
+ };
189
227
  };
190
228
  /**
191
- * Fetches the current metrics from the registry.
192
- * @returns The metrics.
229
+ * Dispatch an update to the store and notify all subscribers.
230
+ * @param op - The type of operation to perform.
231
+ * @param keys - The keys to update.
232
+ * @param [value] - The new value.
193
233
  */
194
- getMetrics = () => this.#registry.metrics();
234
+ #dispatchUpdate = (op, keys, value) => {
235
+ this.#send(op, keys, value);
236
+ };
237
+ };
238
+
239
+ // src/lib/core/schedule.ts
240
+ var OnSchedule = class {
241
+ intervalId = null;
242
+ store;
243
+ name;
244
+ completions;
245
+ every;
246
+ unit;
247
+ run;
248
+ startTime;
249
+ duration;
250
+ lastTimestamp;
251
+ constructor(schedule) {
252
+ this.name = schedule.name;
253
+ this.run = schedule.run;
254
+ this.every = schedule.every;
255
+ this.unit = schedule.unit;
256
+ this.startTime = schedule?.startTime;
257
+ this.completions = schedule?.completions;
258
+ }
259
+ setStore(store) {
260
+ this.store = store;
261
+ this.startInterval();
262
+ }
263
+ startInterval() {
264
+ this.checkStore();
265
+ this.getDuration();
266
+ this.setupInterval();
267
+ }
195
268
  /**
196
- * Returns the current timestamp from performance.now() method. Useful for start timing an operation.
197
- * @returns The timestamp.
269
+ * Checks the store for this schedule and sets the values if it exists
270
+ * @returns
198
271
  */
199
- static observeStart() {
200
- return import_perf_hooks.performance.now();
272
+ checkStore() {
273
+ const result = this.store && this.store.getItem(this.name);
274
+ if (result) {
275
+ const storedSchedule = JSON.parse(result);
276
+ this.completions = storedSchedule?.completions;
277
+ this.startTime = storedSchedule?.startTime;
278
+ this.lastTimestamp = storedSchedule?.lastTimestamp;
279
+ }
201
280
  }
202
281
  /**
203
- * Increments the cache miss gauge for a given label.
204
- * @param label - The label for the cache miss.
282
+ * Saves the schedule to the store
283
+ * @returns
205
284
  */
206
- incCacheMiss = (window) => {
207
- this.incGauge(this.#metricNames.cacheMiss, { window });
208
- };
285
+ saveToStore() {
286
+ const schedule = {
287
+ completions: this.completions,
288
+ startTime: this.startTime,
289
+ lastTimestamp: /* @__PURE__ */ new Date(),
290
+ name: this.name
291
+ };
292
+ if (this.store) this.store.setItem(this.name, JSON.stringify(schedule));
293
+ }
209
294
  /**
210
- * Increments the retry count gauge.
211
- * @param count - The count to increment by.
295
+ * Gets the durations in milliseconds
212
296
  */
213
- incRetryCount = (count) => {
214
- this.incGauge(this.#metricNames.resyncFailureCount, { count });
215
- };
297
+ getDuration() {
298
+ switch (this.unit) {
299
+ case "seconds":
300
+ if (this.every < 10) throw new Error("10 Seconds in the smallest interval allowed");
301
+ this.duration = 1e3 * this.every;
302
+ break;
303
+ case "minutes":
304
+ case "minute":
305
+ this.duration = 1e3 * 60 * this.every;
306
+ break;
307
+ case "hours":
308
+ case "hour":
309
+ this.duration = 1e3 * 60 * 60 * this.every;
310
+ break;
311
+ default:
312
+ throw new Error("Invalid time unit");
313
+ }
314
+ }
216
315
  /**
217
- * Initializes the cache miss gauge for a given label.
218
- * @param label - The label for the cache miss.
316
+ * Sets up the interval
219
317
  */
220
- initCacheMissWindow = (window) => {
221
- this.#rollCacheMissWindows();
222
- this.#gauges.get(this.#getMetricName(this.#metricNames.cacheMiss))?.set({ window }, 0);
223
- this.#cacheMissWindows.set(window, 0);
224
- };
318
+ setupInterval() {
319
+ const now = /* @__PURE__ */ new Date();
320
+ let delay;
321
+ if (this.lastTimestamp && this.startTime) {
322
+ this.startTime = void 0;
323
+ }
324
+ if (this.startTime) {
325
+ delay = this.startTime.getTime() - now.getTime();
326
+ } else if (this.lastTimestamp && this.duration) {
327
+ const lastTimestamp = new Date(this.lastTimestamp);
328
+ delay = this.duration - (now.getTime() - lastTimestamp.getTime());
329
+ }
330
+ if (delay === void 0 || delay <= 0) {
331
+ this.start();
332
+ } else {
333
+ setTimeout(() => {
334
+ this.start();
335
+ }, delay);
336
+ }
337
+ }
225
338
  /**
226
- * Manages the size of the cache miss gauge map.
339
+ * Starts the interval
227
340
  */
228
- #rollCacheMissWindows = () => {
229
- const maxCacheMissWindows = process.env.PEPR_MAX_CACHE_MISS_WINDOWS ? parseInt(process.env.PEPR_MAX_CACHE_MISS_WINDOWS, 10) : void 0;
230
- if (maxCacheMissWindows !== void 0 && this.#cacheMissWindows.size >= maxCacheMissWindows) {
231
- const firstKey = this.#cacheMissWindows.keys().next().value;
232
- if (firstKey !== void 0) {
233
- this.#cacheMissWindows.delete(firstKey);
341
+ start() {
342
+ this.intervalId = setInterval(() => {
343
+ if (this.completions === 0) {
344
+ this.stop();
345
+ return;
346
+ } else {
347
+ this.run();
348
+ if (this.completions && this.completions !== 0) {
349
+ this.completions -= 1;
350
+ }
351
+ this.saveToStore();
234
352
  }
235
- this.#gauges.get(this.#getMetricName(this.#metricNames.cacheMiss))?.remove({ window: firstKey });
236
- }
237
- };
238
- };
239
- var metricsCollector = new MetricsCollector("pepr");
240
-
241
- // src/lib/processors/mutate-processor.ts
242
- var import_fast_json_patch = __toESM(require("fast-json-patch"));
243
- var import_ramda4 = require("ramda");
244
-
245
- // src/lib/telemetry/timeUtils.ts
246
- var getNow = () => performance.now();
247
-
248
- // src/lib/telemetry/webhookTimeouts.ts
249
- var MeasureWebhookTimeout = class {
250
- #startTime = null;
251
- #webhookType;
252
- timeout = 0;
253
- constructor(webhookType) {
254
- this.#webhookType = webhookType;
255
- metricsCollector.addCounter(`${webhookType}_timeouts`, `Number of ${webhookType} webhook timeouts`);
256
- }
257
- start(timeout = 10) {
258
- this.#startTime = getNow();
259
- this.timeout = timeout;
260
- logger_default.info(`Starting timer at ${this.#startTime}`);
353
+ }, this.duration);
261
354
  }
355
+ /**
356
+ * Stops the interval
357
+ */
262
358
  stop() {
263
- if (this.#startTime === null) {
264
- throw new Error("Timer was not started before calling stop.");
265
- }
266
- const elapsedTime = getNow() - this.#startTime;
267
- logger_default.info(`Webhook ${this.#startTime} took ${elapsedTime}ms`);
268
- this.#startTime = null;
269
- if (elapsedTime > this.timeout) {
270
- metricsCollector.incCounter(`${this.#webhookType}_timeouts`);
359
+ if (this.intervalId) {
360
+ clearInterval(this.intervalId);
361
+ this.intervalId = null;
271
362
  }
363
+ if (this.store) this.store.removeItem(this.name);
272
364
  }
273
365
  };
274
366
 
275
- // src/lib/filter/adjudicators/adjudicators.ts
276
- var import_ramda = require("ramda");
277
- var declaredOperation = (0, import_ramda.pipe)(
278
- (request) => request?.operation,
279
- (0, import_ramda.defaultTo)("")
280
- );
281
- var declaredGroup = (0, import_ramda.pipe)(
282
- (request) => request?.kind?.group,
283
- (0, import_ramda.defaultTo)("")
284
- );
285
- var declaredVersion = (0, import_ramda.pipe)(
286
- (request) => request?.kind?.version,
287
- (0, import_ramda.defaultTo)("")
288
- );
289
- var declaredKind = (0, import_ramda.pipe)(
290
- (request) => request?.kind?.kind,
291
- (0, import_ramda.defaultTo)("")
292
- );
293
- var declaredUid = (0, import_ramda.pipe)((request) => request?.uid, (0, import_ramda.defaultTo)(""));
294
- var carriesDeletionTimestamp = (0, import_ramda.pipe)(
295
- (kubernetesObject) => !!kubernetesObject.metadata?.deletionTimestamp,
296
- (0, import_ramda.defaultTo)(false)
297
- );
298
- var missingDeletionTimestamp = (0, import_ramda.complement)(carriesDeletionTimestamp);
299
- var carriedKind = (0, import_ramda.pipe)(
300
- (kubernetesObject) => kubernetesObject?.kind,
301
- (0, import_ramda.defaultTo)("not set")
302
- );
303
- var carriedVersion = (0, import_ramda.pipe)(
304
- (kubernetesObject) => kubernetesObject?.metadata?.resourceVersion,
305
- (0, import_ramda.defaultTo)("not set")
306
- );
307
- var carriedName = (0, import_ramda.pipe)(
308
- (kubernetesObject) => kubernetesObject?.metadata?.name,
309
- (0, import_ramda.defaultTo)("")
310
- );
311
- var carriesName = (0, import_ramda.pipe)(carriedName, (0, import_ramda.equals)(""), import_ramda.not);
312
- var missingName = (0, import_ramda.complement)(carriesName);
313
- var carriedNamespace = (0, import_ramda.pipe)(
314
- (kubernetesObject) => kubernetesObject?.metadata?.namespace,
315
- (0, import_ramda.defaultTo)("")
316
- );
317
- var carriesNamespace = (0, import_ramda.pipe)(carriedNamespace, (0, import_ramda.equals)(""), import_ramda.not);
318
- var carriedAnnotations = (0, import_ramda.pipe)(
319
- (kubernetesObject) => kubernetesObject?.metadata?.annotations,
320
- (0, import_ramda.defaultTo)({})
321
- );
322
- var carriesAnnotations = (0, import_ramda.pipe)(carriedAnnotations, (0, import_ramda.equals)({}), import_ramda.not);
323
- var carriedLabels = (0, import_ramda.pipe)(
324
- (kubernetesObject) => kubernetesObject?.metadata?.labels,
325
- (0, import_ramda.defaultTo)({})
326
- );
327
- var carriesLabels = (0, import_ramda.pipe)(carriedLabels, (0, import_ramda.equals)({}), import_ramda.not);
328
- var definesDeletionTimestamp = (0, import_ramda.pipe)(
329
- (binding) => binding?.filters?.deletionTimestamp ?? false,
330
- (0, import_ramda.defaultTo)(false)
331
- );
332
- var ignoresDeletionTimestamp = (0, import_ramda.complement)(definesDeletionTimestamp);
333
- var definedName = (0, import_ramda.pipe)((binding) => {
334
- return binding.filters.name;
335
- }, (0, import_ramda.defaultTo)(""));
336
- var definesName = (0, import_ramda.pipe)(definedName, (0, import_ramda.equals)(""), import_ramda.not);
337
- var ignoresName = (0, import_ramda.complement)(definesName);
338
- var definedNameRegex = (0, import_ramda.pipe)(
339
- (binding) => binding.filters?.regexName,
340
- (0, import_ramda.defaultTo)("")
341
- );
342
- var definesNameRegex = (0, import_ramda.pipe)(definedNameRegex, (0, import_ramda.equals)(""), import_ramda.not);
343
- var definedNamespaces = (0, import_ramda.pipe)((binding) => binding?.filters?.namespaces, (0, import_ramda.defaultTo)([]));
344
- var definesNamespaces = (0, import_ramda.pipe)(definedNamespaces, (0, import_ramda.equals)([]), import_ramda.not);
345
- var definedNamespaceRegexes = (0, import_ramda.pipe)((binding) => binding?.filters?.regexNamespaces, (0, import_ramda.defaultTo)([]));
346
- var definesNamespaceRegexes = (0, import_ramda.pipe)(definedNamespaceRegexes, (0, import_ramda.equals)([]), import_ramda.not);
347
- var definedAnnotations = (0, import_ramda.pipe)((binding) => binding?.filters?.annotations, (0, import_ramda.defaultTo)({}));
348
- var definesAnnotations = (0, import_ramda.pipe)(definedAnnotations, (0, import_ramda.equals)({}), import_ramda.not);
349
- var definedLabels = (0, import_ramda.pipe)((binding) => binding?.filters?.labels, (0, import_ramda.defaultTo)({}));
350
- var definesLabels = (0, import_ramda.pipe)(definedLabels, (0, import_ramda.equals)({}), import_ramda.not);
351
- var definedEvent = (binding) => {
352
- return binding.event;
353
- };
354
- var definesDelete = (0, import_ramda.pipe)(definedEvent, (0, import_ramda.equals)("DELETE" /* DELETE */));
355
- var definedGroup = (0, import_ramda.pipe)((binding) => binding?.kind?.group, (0, import_ramda.defaultTo)(""));
356
- var definesGroup = (0, import_ramda.pipe)(definedGroup, (0, import_ramda.equals)(""), import_ramda.not);
357
- var definedVersion = (0, import_ramda.pipe)(
358
- (binding) => binding?.kind?.version,
359
- (0, import_ramda.defaultTo)("")
360
- );
361
- var definesVersion = (0, import_ramda.pipe)(definedVersion, (0, import_ramda.equals)(""), import_ramda.not);
362
- var definedKind = (0, import_ramda.pipe)((binding) => binding?.kind?.kind, (0, import_ramda.defaultTo)(""));
363
- var definesKind = (0, import_ramda.pipe)(definedKind, (0, import_ramda.equals)(""), import_ramda.not);
364
- var definedCallback = (binding) => {
365
- return binding.isFinalize ? binding.finalizeCallback : binding.isWatch ? binding.watchCallback : binding.isMutate ? binding.mutateCallback : binding.isValidate ? binding.validateCallback : null;
366
- };
367
- var definedCallbackName = (0, import_ramda.pipe)(definedCallback, (0, import_ramda.defaultTo)({ name: "" }), (callback) => callback.name);
368
- var mismatchedDeletionTimestamp = (0, import_ramda.allPass)([
369
- (0, import_ramda.pipe)((0, import_ramda.nthArg)(0), definesDeletionTimestamp),
370
- (0, import_ramda.pipe)((0, import_ramda.nthArg)(1), missingDeletionTimestamp)
371
- ]);
372
- var mismatchedName = (0, import_ramda.allPass)([
373
- (0, import_ramda.pipe)((0, import_ramda.nthArg)(0), definesName),
374
- (0, import_ramda.pipe)((binding, kubernetesObject) => definedName(binding) !== carriedName(kubernetesObject))
375
- ]);
376
- var mismatchedNameRegex = (0, import_ramda.allPass)([
377
- (0, import_ramda.pipe)((0, import_ramda.nthArg)(0), definesNameRegex),
378
- (0, import_ramda.pipe)((binding, kubernetesObject) => new RegExp(definedNameRegex(binding)).test(carriedName(kubernetesObject)), import_ramda.not)
379
- ]);
380
- var bindsToKind = (0, import_ramda.curry)(
381
- (0, import_ramda.allPass)([(0, import_ramda.pipe)((0, import_ramda.nthArg)(0), definedKind, (0, import_ramda.equals)(""), import_ramda.not), (0, import_ramda.pipe)((binding, kind3) => definedKind(binding) === kind3)])
382
- );
383
- var bindsToNamespace = (0, import_ramda.curry)((0, import_ramda.pipe)(bindsToKind(import_ramda.__, "Namespace")));
384
- var misboundNamespace = (0, import_ramda.allPass)([bindsToNamespace, definesNamespaces]);
385
- var mismatchedNamespace = (0, import_ramda.allPass)([
386
- (0, import_ramda.pipe)((0, import_ramda.nthArg)(0), definesNamespaces),
387
- (0, import_ramda.pipe)((binding, kubernetesObject) => definedNamespaces(binding).includes(carriedNamespace(kubernetesObject)), import_ramda.not)
388
- ]);
389
- var mismatchedNamespaceRegex = (0, import_ramda.allPass)([
390
- (0, import_ramda.pipe)((0, import_ramda.nthArg)(0), definesNamespaceRegexes),
391
- (0, import_ramda.pipe)(
392
- (binding, kubernetesObject) => (0, import_ramda.pipe)(
393
- (0, import_ramda.any)((regEx) => new RegExp(regEx).test(carriedNamespace(kubernetesObject))),
394
- import_ramda.not
395
- )(definedNamespaceRegexes(binding))
396
- )
397
- ]);
398
- var metasMismatch = (0, import_ramda.pipe)(
399
- (defined, carried) => {
400
- const result = { defined, carried, unalike: {} };
401
- result.unalike = Object.entries(result.defined).map(([key, value]) => {
402
- const keyMissing = !Object.hasOwn(result.carried, key);
403
- const noValue = !value;
404
- const valMissing = !result.carried[key];
405
- const valDiffers = result.carried[key] !== result.defined[key];
406
- return keyMissing ? { [key]: value } : noValue ? {} : valMissing ? { [key]: value } : valDiffers ? { [key]: value } : {};
407
- }).reduce((acc, cur) => ({ ...acc, ...cur }), {});
408
- return result.unalike;
409
- },
410
- (unalike) => Object.keys(unalike).length > 0
411
- );
412
- var mismatchedAnnotations = (0, import_ramda.allPass)([
413
- (0, import_ramda.pipe)((0, import_ramda.nthArg)(0), definesAnnotations),
414
- (0, import_ramda.pipe)((binding, kubernetesObject) => metasMismatch(definedAnnotations(binding), carriedAnnotations(kubernetesObject)))
415
- ]);
416
- var mismatchedLabels = (0, import_ramda.allPass)([
417
- (0, import_ramda.pipe)((0, import_ramda.nthArg)(0), definesLabels),
418
- (0, import_ramda.pipe)((binding, kubernetesObject) => metasMismatch(definedLabels(binding), carriedLabels(kubernetesObject)))
419
- ]);
420
- var uncarryableNamespace = (0, import_ramda.allPass)([
421
- (0, import_ramda.pipe)((0, import_ramda.nthArg)(0), import_ramda.length, (0, import_ramda.gt)(import_ramda.__, 0)),
422
- (0, import_ramda.pipe)((namespaceSelector, kubernetesObject) => {
423
- if (kubernetesObject?.kind === "Namespace") {
424
- return namespaceSelector.includes(kubernetesObject?.metadata?.name);
425
- }
426
- if (carriesNamespace(kubernetesObject)) {
427
- return namespaceSelector.includes(carriedNamespace(kubernetesObject));
428
- }
429
- return true;
430
- }, import_ramda.not)
431
- ]);
432
- var missingCarriableNamespace = (0, import_ramda.allPass)([
433
- (0, import_ramda.pipe)((0, import_ramda.nthArg)(0), import_ramda.length, (0, import_ramda.gt)(import_ramda.__, 0)),
434
- (0, import_ramda.pipe)(
435
- (namespaceSelector, kubernetesObject) => kubernetesObject.kind === "Namespace" ? !namespaceSelector.includes(kubernetesObject.metadata.name) : !carriesNamespace(kubernetesObject)
436
- )
437
- ]);
438
- var carriesIgnoredNamespace = (0, import_ramda.allPass)([
439
- (0, import_ramda.pipe)((0, import_ramda.nthArg)(0), import_ramda.length, (0, import_ramda.gt)(import_ramda.__, 0)),
440
- (0, import_ramda.pipe)((namespaceSelector, kubernetesObject) => {
441
- if (kubernetesObject?.kind === "Namespace") {
442
- return namespaceSelector.includes(kubernetesObject?.metadata?.name);
367
+ // src/lib/finalizer.ts
368
+ var import_kubernetes_fluent_client = require("kubernetes-fluent-client");
369
+ function addFinalizer(request) {
370
+ if (request.Request.operation === "DELETE" /* DELETE */) {
371
+ return;
372
+ }
373
+ if (request.Request.operation === "UPDATE" /* UPDATE */ && request.Raw.metadata?.deletionTimestamp) {
374
+ return;
375
+ }
376
+ const peprFinal = "pepr.dev/finalizer";
377
+ const finalizers = request.Raw.metadata?.finalizers || [];
378
+ if (!finalizers.includes(peprFinal)) {
379
+ finalizers.push(peprFinal);
380
+ }
381
+ request.Merge({ metadata: { finalizers } });
382
+ }
383
+ async function removeFinalizer(binding, obj) {
384
+ const peprFinal = "pepr.dev/finalizer";
385
+ const meta = obj.metadata;
386
+ const resource = `${meta.namespace || "ClusterScoped"}/${meta.name}`;
387
+ logger_default.debug({ obj }, `Removing finalizer '${peprFinal}' from '${resource}'`);
388
+ const { model, kind: kind3 } = binding;
389
+ try {
390
+ (0, import_kubernetes_fluent_client.RegisterKind)(model, kind3);
391
+ } catch (e) {
392
+ const expected = e.message === `GVK ${model.name} already registered`;
393
+ if (!expected) {
394
+ logger_default.error({ model, kind: kind3, error: e }, `Error registering "${kind3}" during finalization.`);
395
+ return;
443
396
  }
444
- if (carriesNamespace(kubernetesObject)) {
445
- return namespaceSelector.includes(carriedNamespace(kubernetesObject));
397
+ }
398
+ const finalizers = meta.finalizers?.filter((f) => f !== peprFinal) || [];
399
+ obj = await (0, import_kubernetes_fluent_client.K8s)(model, meta).Patch([
400
+ {
401
+ op: "replace",
402
+ path: `/metadata/finalizers`,
403
+ value: finalizers
446
404
  }
447
- return false;
448
- })
449
- ]);
450
- var unbindableNamespaces = (0, import_ramda.allPass)([
451
- (0, import_ramda.pipe)((0, import_ramda.nthArg)(0), import_ramda.length, (0, import_ramda.gt)(import_ramda.__, 0)),
452
- (0, import_ramda.pipe)((0, import_ramda.nthArg)(1), definesNamespaces),
453
- (0, import_ramda.pipe)(
454
- (namespaceSelector, binding) => (0, import_ramda.difference)(definedNamespaces(binding), namespaceSelector),
455
- import_ramda.length,
456
- (0, import_ramda.equals)(0),
457
- import_ramda.not
458
- )
459
- ]);
460
- var misboundDeleteWithDeletionTimestamp = (0, import_ramda.allPass)([definesDelete, definesDeletionTimestamp]);
461
- var operationMatchesEvent = (0, import_ramda.anyPass)([
462
- (0, import_ramda.pipe)((0, import_ramda.nthArg)(1), (0, import_ramda.equals)("*" /* ANY */)),
463
- (0, import_ramda.pipe)((operation, event) => operation.valueOf() === event.valueOf()),
464
- (0, import_ramda.pipe)((operation, event) => operation ? event.includes(operation) : false)
465
- ]);
466
- var mismatchedEvent = (0, import_ramda.pipe)(
467
- (binding, request) => operationMatchesEvent(declaredOperation(request), definedEvent(binding)),
468
- import_ramda.not
469
- );
470
- var mismatchedGroup = (0, import_ramda.allPass)([
471
- (0, import_ramda.pipe)((0, import_ramda.nthArg)(0), definesGroup),
472
- (0, import_ramda.pipe)((binding, request) => definedGroup(binding) !== declaredGroup(request))
473
- ]);
474
- var mismatchedVersion = (0, import_ramda.allPass)([
475
- (0, import_ramda.pipe)((0, import_ramda.nthArg)(0), definesVersion),
476
- (0, import_ramda.pipe)((binding, request) => definedVersion(binding) !== declaredVersion(request))
477
- ]);
478
- var mismatchedKind = (0, import_ramda.allPass)([
479
- (0, import_ramda.pipe)((0, import_ramda.nthArg)(0), definesKind),
480
- (0, import_ramda.pipe)((binding, request) => definedKind(binding) !== declaredKind(request))
481
- ]);
405
+ ]);
406
+ logger_default.debug({ obj }, `Removed finalizer '${peprFinal}' from '${resource}'`);
407
+ }
482
408
 
483
- // src/lib/filter/filter.ts
484
- function shouldSkipRequest(binding, req, capabilityNamespaces, ignoredNamespaces) {
485
- const obj = req.operation === "DELETE" /* DELETE */ ? req.oldObject : req.object;
486
- const prefix = "Ignoring Admission Callback:";
487
- const adjudicators = [
488
- () => adjudicateMisboundDeleteWithDeletionTimestamp(binding),
489
- () => adjudicateMismatchedDeletionTimestamp(binding, obj),
490
- () => adjudicateMismatchedEvent(binding, req),
491
- () => adjudicateMismatchedName(binding, obj),
492
- () => adjudicateMismatchedGroup(binding, req),
493
- () => adjudicateMismatchedVersion(binding, req),
494
- () => adjudicateMismatchedKind(binding, req),
495
- () => adjudicateUnbindableNamespaces(capabilityNamespaces, binding),
496
- () => adjudicateUncarryableNamespace(capabilityNamespaces, obj),
497
- () => adjudicateMismatchedNamespace(binding, obj),
498
- () => adjudicateMismatchedLabels(binding, obj),
499
- () => adjudicateMismatchedAnnotations(binding, obj),
500
- () => adjudicateMismatchedNamespaceRegex(binding, obj),
501
- () => adjudicateMismatchedNameRegex(binding, obj),
502
- () => adjudicateCarriesIgnoredNamespace(ignoredNamespaces, obj),
503
- () => adjudicateMissingCarriableNamespace(capabilityNamespaces, obj)
504
- ];
505
- for (const adjudicator of adjudicators) {
506
- const result = adjudicator();
507
- if (result) {
508
- return `${prefix} ${result}`;
409
+ // src/lib/core/capability.ts
410
+ var registerAdmission = isBuildMode() || !isWatchMode();
411
+ var registerWatch = isBuildMode() || isWatchMode() || isDevMode();
412
+ var Capability = class {
413
+ #name;
414
+ #description;
415
+ #namespaces;
416
+ #bindings = [];
417
+ #store = new Storage();
418
+ #scheduleStore = new Storage();
419
+ #registered = false;
420
+ #scheduleRegistered = false;
421
+ hasSchedule;
422
+ /**
423
+ * Run code on a schedule with the capability.
424
+ *
425
+ * @param schedule The schedule to run the code on
426
+ * @returns
427
+ */
428
+ OnSchedule = (schedule) => {
429
+ const { name: name2, every, unit, run, startTime, completions } = schedule;
430
+ this.hasSchedule = true;
431
+ if (process.env.PEPR_WATCH_MODE === "true" || process.env.PEPR_MODE === "dev") {
432
+ const newSchedule = {
433
+ name: name2,
434
+ every,
435
+ unit,
436
+ run,
437
+ startTime,
438
+ completions
439
+ };
440
+ this.#scheduleStore.onReady(() => {
441
+ new OnSchedule(newSchedule).setStore(this.#scheduleStore);
442
+ });
509
443
  }
444
+ };
445
+ getScheduleStore() {
446
+ return this.#scheduleStore;
510
447
  }
511
- return "";
512
- }
513
- function filterNoMatchReason(binding, obj, capabilityNamespaces, ignoredNamespaces) {
514
- const prefix = "Ignoring Watch Callback:";
515
- const adjudicators = [
516
- () => adjudicateMismatchedDeletionTimestamp(binding, obj),
517
- () => adjudicateMismatchedName(binding, obj),
518
- () => adjudicateMisboundNamespace(binding),
519
- () => adjudicateMismatchedLabels(binding, obj),
520
- () => adjudicateMismatchedAnnotations(binding, obj),
521
- () => adjudicateUncarryableNamespace(capabilityNamespaces, obj),
522
- () => adjudicateUnbindableNamespaces(capabilityNamespaces, binding),
523
- () => adjudicateMismatchedNamespace(binding, obj),
524
- () => adjudicateMismatchedNamespaceRegex(binding, obj),
525
- () => adjudicateMismatchedNameRegex(binding, obj),
526
- () => adjudicateCarriesIgnoredNamespace(ignoredNamespaces, obj),
527
- () => adjudicateMissingCarriableNamespace(capabilityNamespaces, obj)
528
- ];
529
- for (const adjudicator of adjudicators) {
530
- const result = adjudicator();
531
- if (result) {
532
- return `${prefix} ${result}`;
533
- }
448
+ /**
449
+ * Store is a key-value data store that can be used to persist data that should be shared
450
+ * between requests. Each capability has its own store, and the data is persisted in Kubernetes
451
+ * in the `pepr-system` namespace.
452
+ *
453
+ * Note: You should only access the store from within an action.
454
+ */
455
+ Store = {
456
+ clear: this.#store.clear,
457
+ getItem: this.#store.getItem,
458
+ removeItem: this.#store.removeItem,
459
+ removeItemAndWait: this.#store.removeItemAndWait,
460
+ setItem: this.#store.setItem,
461
+ subscribe: this.#store.subscribe,
462
+ onReady: this.#store.onReady,
463
+ setItemAndWait: this.#store.setItemAndWait
464
+ };
465
+ /**
466
+ * ScheduleStore is a key-value data store used to persist schedule data that should be shared
467
+ * between intervals. Each Schedule shares store, and the data is persisted in Kubernetes
468
+ * in the `pepr-system` namespace.
469
+ *
470
+ * Note: There is no direct access to schedule store
471
+ */
472
+ ScheduleStore = {
473
+ clear: this.#scheduleStore.clear,
474
+ getItem: this.#scheduleStore.getItem,
475
+ removeItemAndWait: this.#scheduleStore.removeItemAndWait,
476
+ removeItem: this.#scheduleStore.removeItem,
477
+ setItemAndWait: this.#scheduleStore.setItemAndWait,
478
+ setItem: this.#scheduleStore.setItem,
479
+ subscribe: this.#scheduleStore.subscribe,
480
+ onReady: this.#scheduleStore.onReady
481
+ };
482
+ get bindings() {
483
+ return this.#bindings;
534
484
  }
535
- return "";
536
- }
537
- function adjudicateMisboundNamespace(binding) {
538
- return misboundNamespace(binding) ? "Cannot use namespace filter on a namespace object." : null;
539
- }
540
- function adjudicateMisboundDeleteWithDeletionTimestamp(binding) {
541
- return misboundDeleteWithDeletionTimestamp(binding) ? "Cannot use deletionTimestamp filter on a DELETE operation." : null;
542
- }
543
- function adjudicateMismatchedDeletionTimestamp(binding, obj) {
544
- return mismatchedDeletionTimestamp(binding, obj) ? "Binding defines deletionTimestamp but Object does not carry it." : null;
545
- }
546
- function adjudicateMismatchedEvent(binding, req) {
547
- return mismatchedEvent(binding, req) ? `Binding defines event '${definedEvent(binding)}' but Request declares '${declaredOperation(req)}'.` : null;
548
- }
549
- function adjudicateMismatchedName(binding, obj) {
550
- return mismatchedName(binding, obj) ? `Binding defines name '${definedName(binding)}' but Object carries '${carriedName(obj)}'.` : null;
551
- }
552
- function adjudicateMismatchedGroup(binding, req) {
553
- return mismatchedGroup(binding, req) ? `Binding defines group '${definedGroup(binding)}' but Request declares '${declaredGroup(req)}'.` : null;
554
- }
555
- function adjudicateMismatchedVersion(binding, req) {
556
- return mismatchedVersion(binding, req) ? `Binding defines version '${definedVersion(binding)}' but Request declares '${declaredVersion(req)}'.` : null;
557
- }
558
- function adjudicateMismatchedKind(binding, req) {
559
- return mismatchedKind(binding, req) ? `Binding defines kind '${definedKind(binding)}' but Request declares '${declaredKind(req)}'.` : null;
560
- }
561
- function adjudicateUnbindableNamespaces(capabilityNamespaces, binding) {
562
- return unbindableNamespaces(capabilityNamespaces, binding) ? `Binding defines namespaces ${JSON.stringify(definedNamespaces(binding))} but namespaces allowed by Capability are '${JSON.stringify(capabilityNamespaces)}'.` : null;
563
- }
564
- function adjudicateUncarryableNamespace(capabilityNamespaces, obj) {
565
- return uncarryableNamespace(capabilityNamespaces, obj) ? `Object carries namespace '${obj.kind && obj.kind === "Namespace" ? obj.metadata?.name : carriedNamespace(obj)}' but namespaces allowed by Capability are '${JSON.stringify(capabilityNamespaces)}'.` : null;
566
- }
567
- function adjudicateMismatchedNamespace(binding, obj) {
568
- return mismatchedNamespace(binding, obj) ? `Binding defines namespaces '${JSON.stringify(definedNamespaces(binding))}' but Object carries '${carriedNamespace(obj)}'.` : null;
569
- }
570
- function adjudicateMismatchedLabels(binding, obj) {
571
- return mismatchedLabels(binding, obj) ? `Binding defines labels '${JSON.stringify(definedLabels(binding))}' but Object carries '${JSON.stringify(carriedLabels(obj))}'.` : null;
572
- }
573
- function adjudicateMismatchedAnnotations(binding, obj) {
574
- return mismatchedAnnotations(binding, obj) ? `Binding defines annotations '${JSON.stringify(definedAnnotations(binding))}' but Object carries '${JSON.stringify(carriedAnnotations(obj))}'.` : null;
575
- }
576
- function adjudicateMismatchedNamespaceRegex(binding, obj) {
577
- return mismatchedNamespaceRegex(binding, obj) ? `Binding defines namespace regexes '${JSON.stringify(definedNamespaceRegexes(binding))}' but Object carries '${carriedNamespace(obj)}'.` : null;
578
- }
579
- function adjudicateMismatchedNameRegex(binding, obj) {
580
- return mismatchedNameRegex(binding, obj) ? `Binding defines name regex '${definedNameRegex(binding)}' but Object carries '${carriedName(obj)}'.` : null;
581
- }
582
- function adjudicateCarriesIgnoredNamespace(ignoredNamespaces, obj) {
583
- return carriesIgnoredNamespace(ignoredNamespaces, obj) ? `Object carries namespace '${obj.kind && obj.kind === "Namespace" ? obj.metadata?.name : carriedNamespace(obj)}' but ignored namespaces include '${JSON.stringify(ignoredNamespaces)}'.` : null;
584
- }
585
- function adjudicateMissingCarriableNamespace(capabilityNamespaces, obj) {
586
- return missingCarriableNamespace(capabilityNamespaces, obj) ? `Object does not carry a namespace but namespaces allowed by Capability are '${JSON.stringify(capabilityNamespaces)}'.` : null;
587
- }
588
-
589
- // src/lib/mutate-request.ts
590
- var import_ramda2 = require("ramda");
591
- var PeprMutateRequest = class {
592
- Raw;
593
- #input;
594
- get PermitSideEffects() {
595
- return !this.#input.dryRun;
485
+ get name() {
486
+ return this.#name;
596
487
  }
597
- get IsDryRun() {
598
- return this.#input.dryRun;
488
+ get description() {
489
+ return this.#description;
599
490
  }
600
- get OldResource() {
601
- return this.#input.oldObject;
491
+ get namespaces() {
492
+ return this.#namespaces || [];
602
493
  }
603
- get Request() {
604
- return this.#input;
494
+ constructor(cfg) {
495
+ this.#name = cfg.name;
496
+ this.#description = cfg.description;
497
+ this.#namespaces = cfg.namespaces;
498
+ this.hasSchedule = false;
499
+ logger_default.info(`Capability ${this.#name} registered`);
500
+ logger_default.debug(cfg);
605
501
  }
606
- constructor(input) {
607
- this.#input = input;
608
- if (input.operation.toUpperCase() === "DELETE" /* DELETE */) {
609
- this.Raw = (0, import_ramda2.clone)(input.oldObject);
610
- } else {
611
- this.Raw = (0, import_ramda2.clone)(input.object);
612
- }
613
- if (!this.Raw) {
614
- throw new Error("Unable to load the request object into PeprRequest.Raw");
502
+ /**
503
+ * Register the store with the capability. This is called automatically by the Pepr controller.
504
+ */
505
+ registerScheduleStore = () => {
506
+ logger_default.info(`Registering schedule store for ${this.#name}`);
507
+ if (this.#scheduleRegistered) {
508
+ throw new Error(`Schedule store already registered for ${this.#name}`);
615
509
  }
616
- }
617
- Merge = (obj) => {
618
- this.Raw = (0, import_ramda2.mergeDeepRight)(this.Raw, obj);
619
- };
620
- SetLabel = (key, value) => {
621
- const ref = this.Raw;
622
- ref.metadata = ref.metadata ?? {};
623
- ref.metadata.labels = ref.metadata.labels ?? {};
624
- ref.metadata.labels[key] = value;
625
- return this;
626
- };
627
- SetAnnotation = (key, value) => {
628
- const ref = this.Raw;
629
- ref.metadata = ref.metadata ?? {};
630
- ref.metadata.annotations = ref.metadata.annotations ?? {};
631
- ref.metadata.annotations[key] = value;
632
- return this;
510
+ this.#scheduleRegistered = true;
511
+ return this.#scheduleStore;
633
512
  };
634
- RemoveLabel = (key) => {
635
- if (this.Raw.metadata?.labels?.[key]) {
636
- delete this.Raw.metadata.labels[key];
513
+ /**
514
+ * Register the store with the capability. This is called automatically by the Pepr controller.
515
+ *
516
+ * @param store
517
+ */
518
+ registerStore = () => {
519
+ logger_default.info(`Registering store for ${this.#name}`);
520
+ if (this.#registered) {
521
+ throw new Error(`Store already registered for ${this.#name}`);
637
522
  }
638
- return this;
523
+ this.#registered = true;
524
+ return this.#store;
639
525
  };
640
- RemoveAnnotation = (key) => {
641
- if (this.Raw.metadata?.annotations?.[key]) {
642
- delete this.Raw.metadata.annotations[key];
526
+ /**
527
+ * The When method is used to register a action to be executed when a Kubernetes resource is
528
+ * processed by Pepr. The action will be executed if the resource matches the specified kind and any
529
+ * filters that are applied.
530
+ *
531
+ * @param model the KubernetesObject model to match
532
+ * @param kind if using a custom KubernetesObject not available in `a.*`, specify the GroupVersionKind
533
+ * @returns
534
+ */
535
+ When = (model, kind3) => {
536
+ const matchedKind = (0, import_kubernetes_fluent_client2.modelToGroupVersionKind)(model.name);
537
+ if (!matchedKind && !kind3) {
538
+ throw new Error(`Kind not specified for ${model.name}`);
643
539
  }
644
- return this;
645
- };
646
- HasLabel = (key) => {
647
- return this.Raw.metadata?.labels?.[key] !== void 0;
648
- };
649
- HasAnnotation = (key) => {
650
- return this.Raw.metadata?.annotations?.[key] !== void 0;
651
- };
652
- };
653
-
654
- // src/lib/utils.ts
655
- var utils_exports = {};
656
- __export(utils_exports, {
657
- base64Decode: () => base64Decode,
658
- base64Encode: () => base64Encode,
659
- convertFromBase64Map: () => convertFromBase64Map,
660
- convertToBase64Map: () => convertToBase64Map,
661
- isAscii: () => isAscii
662
- });
663
- var isAscii = /^[\s\x20-\x7E]*$/;
664
- function convertToBase64Map(obj, skip) {
665
- obj.data = obj.data ?? {};
666
- for (const key in obj.data) {
667
- const value = obj.data[key];
668
- obj.data[key] = skip.includes(key) ? value : base64Encode(value);
669
- }
670
- }
671
- function convertFromBase64Map(obj) {
672
- const skip = [];
673
- obj.data = obj.data ?? {};
674
- for (const key in obj.data) {
675
- if (obj.data[key] === void 0) {
676
- obj.data[key] = "";
677
- } else {
678
- const decoded = base64Decode(obj.data[key]);
679
- if (isAscii.test(decoded)) {
680
- obj.data[key] = decoded;
681
- } else {
682
- skip.push(key);
540
+ const binding = {
541
+ model,
542
+ // If the kind is not specified, use the matched kind from the model
543
+ kind: kind3 || matchedKind,
544
+ event: "*" /* ANY */,
545
+ filters: {
546
+ name: "",
547
+ namespaces: [],
548
+ regexNamespaces: [],
549
+ regexName: "",
550
+ labels: {},
551
+ annotations: {},
552
+ deletionTimestamp: false
553
+ }
554
+ };
555
+ const bindings = this.#bindings;
556
+ const prefix = `${this.#name}: ${model.name}`;
557
+ const commonChain = { WithLabel, WithAnnotation, WithDeletionTimestamp, Mutate, Validate, Watch, Reconcile, Alias };
558
+ const isNotEmpty = (value) => Object.keys(value).length > 0;
559
+ const log = (message, cbString) => {
560
+ const filteredObj = (0, import_ramda2.pickBy)(isNotEmpty, binding.filters);
561
+ logger_default.info(`${message} configured for ${binding.event}`, prefix);
562
+ logger_default.info(filteredObj, prefix);
563
+ logger_default.debug(cbString, prefix);
564
+ };
565
+ function Validate(validateCallback) {
566
+ if (registerAdmission) {
567
+ log("Validate Action", validateCallback.toString());
568
+ const aliasLogger = logger_default.child({ alias: binding.alias || "no alias provided" });
569
+ bindings.push({
570
+ ...binding,
571
+ isValidate: true,
572
+ validateCallback: async (req, logger = aliasLogger) => {
573
+ logger_default.info(`Executing validate action with alias: ${binding.alias || "no alias provided"}`);
574
+ return await validateCallback(req, logger);
575
+ }
576
+ });
683
577
  }
578
+ return { Watch, Reconcile };
684
579
  }
685
- }
686
- logger_default.debug(`Non-ascii data detected in keys: ${skip}, skipping automatic base64 decoding`);
687
- return skip;
688
- }
689
- function base64Decode(data) {
690
- return Buffer.from(data, "base64").toString("utf-8");
691
- }
692
- function base64Encode(data) {
693
- return Buffer.from(data).toString("base64");
694
- }
695
-
696
- // src/cli/init/enums.ts
697
- var OnError = /* @__PURE__ */ ((OnError2) => {
698
- OnError2["AUDIT"] = "audit";
699
- OnError2["IGNORE"] = "ignore";
700
- OnError2["REJECT"] = "reject";
701
- return OnError2;
702
- })(OnError || {});
703
-
704
- // src/lib/assets/webhooks.ts
705
- var import_ramda3 = require("ramda");
706
- function resolveIgnoreNamespaces(ignoredNSConfig = []) {
707
- const ignoredNSEnv = process.env.PEPR_ADDITIONAL_IGNORED_NAMESPACES;
708
- if (!ignoredNSEnv) {
709
- return ignoredNSConfig;
710
- }
711
- const namespaces = ignoredNSEnv.split(",").map((ns) => ns.trim());
712
- if (ignoredNSConfig) {
713
- namespaces.push(...ignoredNSConfig);
714
- }
715
- return namespaces.filter((ns) => ns.length > 0);
716
- }
717
-
718
- // src/lib/processors/mutate-processor.ts
719
- function updateStatus(config, name2, wrapped, status) {
720
- if (wrapped.Request.operation === "DELETE") {
721
- return wrapped;
722
- }
723
- wrapped.SetAnnotation(`${config.uuid}.pepr.dev/${name2}`, status);
724
- return wrapped;
725
- }
726
- function logMutateErrorMessage(e) {
727
- try {
728
- if (e.message && e.message !== "[object Object]") {
729
- return e.message;
730
- } else {
731
- throw new Error("An error occurred in the mutate action.");
580
+ function Mutate(mutateCallback) {
581
+ if (registerAdmission) {
582
+ log("Mutate Action", mutateCallback.toString());
583
+ const aliasLogger = logger_default.child({ alias: binding.alias || "no alias provided" });
584
+ bindings.push({
585
+ ...binding,
586
+ isMutate: true,
587
+ mutateCallback: async (req, logger = aliasLogger) => {
588
+ logger_default.info(`Executing mutation action with alias: ${binding.alias || "no alias provided"}`);
589
+ await mutateCallback(req, logger);
590
+ }
591
+ });
592
+ }
593
+ return { Watch, Validate, Reconcile };
732
594
  }
733
- } catch {
734
- return "An error occurred with the mutate action.";
735
- }
736
- }
737
- function decodeData(wrapped) {
738
- let skipped = [];
739
- const isSecret = wrapped.Request.kind.version === "v1" && wrapped.Request.kind.kind === "Secret";
740
- if (isSecret) {
741
- skipped = convertFromBase64Map(wrapped.Raw);
742
- }
743
- return { skipped, wrapped };
744
- }
745
- function reencodeData(wrapped, skipped) {
746
- const transformed = (0, import_ramda4.clone)(wrapped.Raw);
747
- const isSecret = wrapped.Request.kind.version === "v1" && wrapped.Request.kind.kind === "Secret";
748
- if (isSecret) {
749
- convertToBase64Map(transformed, skipped);
750
- }
751
- return transformed;
752
- }
753
- async function processRequest(bindable, wrapped, response) {
754
- const { binding, actMeta, name: name2, config } = bindable;
755
- const label = binding.mutateCallback.name;
756
- logger_default.info(actMeta, `Processing mutation action (${label})`);
757
- wrapped = updateStatus(config, name2, wrapped, "started");
758
- try {
759
- await binding.mutateCallback(wrapped);
760
- logger_default.info(actMeta, `Mutation action succeeded (${label})`);
761
- wrapped = updateStatus(config, name2, wrapped, "succeeded");
762
- } catch (e) {
763
- wrapped = updateStatus(config, name2, wrapped, "warning");
764
- response.warnings = response.warnings || [];
765
- const errorMessage = logMutateErrorMessage(e);
766
- logger_default.error(actMeta, `Action failed: ${errorMessage}`);
767
- response.warnings.push(`Action failed: ${errorMessage}`);
768
- switch (config.onError) {
769
- case "reject" /* REJECT */:
770
- response.result = "Pepr module configured to reject on error";
771
- break;
772
- case "audit" /* AUDIT */:
773
- response.auditAnnotations = response.auditAnnotations || {};
774
- response.auditAnnotations[Date.now()] = `Action failed: ${errorMessage}`;
775
- break;
595
+ function Watch(watchCallback) {
596
+ if (registerWatch) {
597
+ log("Watch Action", watchCallback.toString());
598
+ const aliasLogger = logger_default.child({ alias: binding.alias || "no alias provided" });
599
+ bindings.push({
600
+ ...binding,
601
+ isWatch: true,
602
+ watchCallback: async (update, phase, logger = aliasLogger) => {
603
+ logger_default.info(`Executing watch action with alias: ${binding.alias || "no alias provided"}`);
604
+ await watchCallback(update, phase, logger);
605
+ }
606
+ });
607
+ }
608
+ return { Finalize };
776
609
  }
777
- }
778
- return { wrapped, response };
779
- }
780
- async function mutateProcessor(config, capabilities, req, reqMetadata) {
781
- const webhookTimer = new MeasureWebhookTimeout("mutate" /* MUTATE */);
782
- webhookTimer.start(config.webhookTimeout);
783
- let response = {
784
- uid: req.uid,
785
- warnings: [],
786
- allowed: false
787
- };
788
- const decoded = decodeData(new PeprMutateRequest(req));
789
- let wrapped = decoded.wrapped;
790
- logger_default.info(reqMetadata, `Processing request`);
791
- let bindables = capabilities.flatMap(
792
- (capa) => capa.bindings.map((bind) => ({
793
- req,
794
- config,
795
- name: capa.name,
796
- namespaces: capa.namespaces,
797
- binding: bind,
798
- actMeta: { ...reqMetadata, name: capa.name }
799
- }))
800
- );
801
- bindables = bindables.filter((bind) => {
802
- if (!bind.binding.mutateCallback) {
803
- return false;
610
+ function Reconcile(reconcileCallback) {
611
+ if (registerWatch) {
612
+ log("Reconcile Action", reconcileCallback.toString());
613
+ const aliasLogger = logger_default.child({ alias: binding.alias || "no alias provided" });
614
+ bindings.push({
615
+ ...binding,
616
+ isWatch: true,
617
+ isQueue: true,
618
+ watchCallback: async (update, phase, logger = aliasLogger) => {
619
+ logger_default.info(`Executing reconcile action with alias: ${binding.alias || "no alias provided"}`);
620
+ await reconcileCallback(update, phase, logger);
621
+ }
622
+ });
623
+ }
624
+ return { Finalize };
804
625
  }
805
- const shouldSkip = shouldSkipRequest(
806
- bind.binding,
807
- bind.req,
808
- bind.namespaces,
809
- resolveIgnoreNamespaces(bind.config?.alwaysIgnore?.namespaces)
810
- );
811
- if (shouldSkip !== "") {
812
- logger_default.debug(shouldSkip);
813
- return false;
626
+ function Finalize(finalizeCallback) {
627
+ log("Finalize Action", finalizeCallback.toString());
628
+ const aliasLogger = logger_default.child({ alias: binding.alias || "no alias provided" });
629
+ if (registerAdmission) {
630
+ const mutateBinding = {
631
+ ...binding,
632
+ isMutate: true,
633
+ isFinalize: true,
634
+ event: "*" /* ANY */,
635
+ mutateCallback: addFinalizer
636
+ };
637
+ bindings.push(mutateBinding);
638
+ }
639
+ if (registerWatch) {
640
+ const watchBinding = {
641
+ ...binding,
642
+ isWatch: true,
643
+ isFinalize: true,
644
+ event: "UPDATE" /* UPDATE */,
645
+ finalizeCallback: async (update, logger = aliasLogger) => {
646
+ logger_default.info(`Executing finalize action with alias: ${binding.alias || "no alias provided"}`);
647
+ return await finalizeCallback(update, logger);
648
+ }
649
+ };
650
+ bindings.push(watchBinding);
651
+ }
814
652
  }
815
- return true;
816
- });
817
- for (const bindable of bindables) {
818
- ({ wrapped, response } = await processRequest(bindable, wrapped, response));
819
- if (config.onError === "reject" /* REJECT */ && response?.warnings.length > 0) {
820
- return response;
653
+ function InNamespace(...namespaces) {
654
+ logger_default.debug(`Add namespaces filter ${namespaces}`, prefix);
655
+ binding.filters.namespaces.push(...namespaces);
656
+ return { ...commonChain, WithName, WithNameRegex };
821
657
  }
822
- }
823
- response.allowed = true;
824
- if (bindables.length === 0) {
825
- logger_default.info(reqMetadata, `No matching actions found`);
826
- return response;
827
- }
828
- if (req.operation === "DELETE") {
829
- return response;
830
- }
831
- const transformed = reencodeData(wrapped, decoded.skipped);
832
- const patches = import_fast_json_patch.default.compare(req.object, transformed);
833
- updateResponsePatchAndWarnings(patches, response);
834
- logger_default.debug({ ...reqMetadata, patches }, `Patches generated`);
835
- webhookTimer.stop();
836
- return response;
837
- }
838
- function updateResponsePatchAndWarnings(patches, response) {
839
- if (patches.length > 0) {
840
- response.patchType = "JSONPatch";
841
- response.patch = base64Encode(JSON.stringify(patches));
842
- }
843
- if (response.warnings && response.warnings.length < 1) {
844
- delete response.warnings;
845
- }
846
- }
658
+ function InNamespaceRegex(...namespaces) {
659
+ logger_default.debug(`Add regex namespaces filter ${namespaces}`, prefix);
660
+ binding.filters.regexNamespaces.push(...namespaces.map((regex) => regex.source));
661
+ return { ...commonChain, WithName, WithNameRegex };
662
+ }
663
+ function WithDeletionTimestamp() {
664
+ logger_default.debug("Add deletionTimestamp filter");
665
+ binding.filters.deletionTimestamp = true;
666
+ return commonChain;
667
+ }
668
+ function WithNameRegex(regexName) {
669
+ logger_default.debug(`Add regex name filter ${regexName}`, prefix);
670
+ binding.filters.regexName = regexName.source;
671
+ return commonChain;
672
+ }
673
+ function WithName(name2) {
674
+ logger_default.debug(`Add name filter ${name2}`, prefix);
675
+ binding.filters.name = name2;
676
+ return commonChain;
677
+ }
678
+ function WithLabel(key, value = "") {
679
+ logger_default.debug(`Add label filter ${key}=${value}`, prefix);
680
+ binding.filters.labels[key] = value;
681
+ return commonChain;
682
+ }
683
+ function WithAnnotation(key, value = "") {
684
+ logger_default.debug(`Add annotation filter ${key}=${value}`, prefix);
685
+ binding.filters.annotations[key] = value;
686
+ return commonChain;
687
+ }
688
+ function Alias(alias) {
689
+ logger_default.debug(`Adding prefix alias ${alias}`, prefix);
690
+ binding.alias = alias;
691
+ return commonChain;
692
+ }
693
+ function bindEvent(event) {
694
+ binding.event = event;
695
+ return {
696
+ ...commonChain,
697
+ InNamespace,
698
+ InNamespaceRegex,
699
+ WithName,
700
+ WithNameRegex,
701
+ WithDeletionTimestamp,
702
+ Alias
703
+ };
704
+ }
705
+ return {
706
+ IsCreatedOrUpdated: () => bindEvent("CREATEORUPDATE" /* CREATE_OR_UPDATE */),
707
+ IsCreated: () => bindEvent("CREATE" /* CREATE */),
708
+ IsUpdated: () => bindEvent("UPDATE" /* UPDATE */),
709
+ IsDeleted: () => bindEvent("DELETE" /* DELETE */)
710
+ };
711
+ };
712
+ };
713
+
714
+ // src/lib/core/module.ts
715
+ var import_ramda9 = require("ramda");
716
+
717
+ // src/lib/controller/index.ts
718
+ var import_express = __toESM(require("express"));
719
+ var import_fs = __toESM(require("fs"));
720
+ var import_https = __toESM(require("https"));
847
721
 
848
- // src/lib/validate-request.ts
849
- var import_ramda5 = require("ramda");
850
- var PeprValidateRequest = class {
851
- Raw;
852
- #input;
722
+ // src/lib/telemetry/metrics.ts
723
+ var import_perf_hooks = require("perf_hooks");
724
+ var import_prom_client = __toESM(require("prom-client"));
725
+ var loggingPrefix = "MetricsCollector";
726
+ var MetricsCollector = class {
727
+ #registry;
728
+ #counters = /* @__PURE__ */ new Map();
729
+ #gauges = /* @__PURE__ */ new Map();
730
+ #summaries = /* @__PURE__ */ new Map();
731
+ #prefix;
732
+ #cacheMissWindows = /* @__PURE__ */ new Map();
733
+ #metricNames = {
734
+ errors: "errors",
735
+ alerts: "alerts",
736
+ mutate: "mutate",
737
+ validate: "validate",
738
+ cacheMiss: "cache_miss",
739
+ resyncFailureCount: "resync_failure_count"
740
+ };
853
741
  /**
854
- * Provides access to the old resource in the request if available.
855
- * @returns The old Kubernetes resource object or null if not available.
742
+ * Creates a MetricsCollector instance with prefixed metrics.
743
+ * @param [prefix='pepr'] - The prefix for the metric names.
856
744
  */
857
- get OldResource() {
858
- return this.#input.oldObject;
745
+ constructor(prefix = "pepr") {
746
+ this.#registry = new import_prom_client.Registry();
747
+ this.#prefix = prefix;
748
+ this.addCounter(this.#metricNames.errors, "Mutation/Validate errors encountered");
749
+ this.addCounter(this.#metricNames.alerts, "Mutation/Validate bad api path received");
750
+ this.addSummary(this.#metricNames.mutate, "Mutation operation summary");
751
+ this.addSummary(this.#metricNames.validate, "Validation operation summary");
752
+ this.addGauge(this.#metricNames.cacheMiss, "Number of cache misses per window", ["window"]);
753
+ this.addGauge(this.#metricNames.resyncFailureCount, "Number of failures per resync operation", ["count"]);
859
754
  }
755
+ #getMetricName = (name2) => `${this.#prefix}_${name2}`;
756
+ #addMetric = (collection, MetricType, { name: name2, help, labelNames }) => {
757
+ if (collection.has(this.#getMetricName(name2))) {
758
+ logger_default.debug(`Metric for ${name2} already exists`, loggingPrefix);
759
+ return;
760
+ }
761
+ const metric = new MetricType({
762
+ name: this.#getMetricName(name2),
763
+ help,
764
+ registers: [this.#registry],
765
+ labelNames
766
+ });
767
+ collection.set(this.#getMetricName(name2), metric);
768
+ };
769
+ addCounter = (name2, help) => {
770
+ this.#addMetric(this.#counters, import_prom_client.default.Counter, { name: name2, help, labelNames: [] });
771
+ };
772
+ addSummary = (name2, help) => {
773
+ this.#addMetric(this.#summaries, import_prom_client.default.Summary, { name: name2, help, labelNames: [] });
774
+ };
775
+ addGauge = (name2, help, labelNames) => {
776
+ this.#addMetric(this.#gauges, import_prom_client.default.Gauge, { name: name2, help, labelNames });
777
+ };
778
+ incCounter = (name2) => {
779
+ this.#counters.get(this.#getMetricName(name2))?.inc();
780
+ };
781
+ incGauge = (name2, labels, value = 1) => {
782
+ this.#gauges.get(this.#getMetricName(name2))?.inc(labels || {}, value);
783
+ };
860
784
  /**
861
- * Provides access to the request object.
862
- * @returns The request object containing the Kubernetes resource.
785
+ * Increments the error counter.
863
786
  */
864
- get Request() {
865
- return this.#input;
866
- }
787
+ error = () => this.incCounter(this.#metricNames.errors);
867
788
  /**
868
- * Creates a new instance of the Action class.
869
- * @param input - The request object containing the Kubernetes resource to modify.
789
+ * Increments the alerts counter.
870
790
  */
871
- constructor(input) {
872
- this.#input = input;
873
- if (input.operation.toUpperCase() === "DELETE" /* DELETE */) {
874
- this.Raw = (0, import_ramda5.clone)(input.oldObject);
875
- } else {
876
- this.Raw = (0, import_ramda5.clone)(input.object);
877
- }
878
- if (!this.Raw) {
879
- throw new Error("unable to load the request object into PeprRequest.Raw");
880
- }
881
- }
791
+ alert = () => this.incCounter(this.#metricNames.alerts);
882
792
  /**
883
- * Check if a label exists on the Kubernetes resource.
884
- *
885
- * @param key the label key to check
886
- * @returns
793
+ * Observes the duration since the provided start time and updates the summary.
794
+ * @param startTime - The start time.
795
+ * @param name - The metrics summary to increment.
887
796
  */
888
- HasLabel = (key) => {
889
- return this.Raw.metadata?.labels?.[key] !== void 0;
797
+ observeEnd = (startTime, name2 = this.#metricNames.mutate) => {
798
+ this.#summaries.get(this.#getMetricName(name2))?.observe(import_perf_hooks.performance.now() - startTime);
890
799
  };
891
800
  /**
892
- * Check if an annotation exists on the Kubernetes resource.
893
- *
894
- * @param key the annotation key to check
895
- * @returns
801
+ * Fetches the current metrics from the registry.
802
+ * @returns The metrics.
896
803
  */
897
- HasAnnotation = (key) => {
898
- return this.Raw.metadata?.annotations?.[key] !== void 0;
899
- };
804
+ getMetrics = () => this.#registry.metrics();
900
805
  /**
901
- * Create a validation response that allows the request.
902
- *
903
- * @returns The validation response.
806
+ * Returns the current timestamp from performance.now() method. Useful for start timing an operation.
807
+ * @returns The timestamp.
904
808
  */
905
- Approve = () => {
906
- return {
907
- allowed: true
908
- };
809
+ static observeStart() {
810
+ return import_perf_hooks.performance.now();
811
+ }
812
+ /**
813
+ * Increments the cache miss gauge for a given label.
814
+ * @param label - The label for the cache miss.
815
+ */
816
+ incCacheMiss = (window) => {
817
+ this.incGauge(this.#metricNames.cacheMiss, { window });
909
818
  };
910
819
  /**
911
- * Create a validation response that denies the request.
912
- *
913
- * @param statusMessage Optional status message to return to the user.
914
- * @param statusCode Optional status code to return to the user.
915
- * @returns The validation response.
820
+ * Increments the retry count gauge.
821
+ * @param count - The count to increment by.
916
822
  */
917
- Deny = (statusMessage, statusCode) => {
918
- return {
919
- allowed: false,
920
- statusCode,
921
- statusMessage
922
- };
823
+ incRetryCount = (count) => {
824
+ this.incGauge(this.#metricNames.resyncFailureCount, { count: count.toString() });
923
825
  };
924
- };
925
-
926
- // src/lib/processors/validate-processor.ts
927
- async function processRequest2(binding, actionMetadata, peprValidateRequest) {
928
- const label = binding.validateCallback.name;
929
- logger_default.info(actionMetadata, `Processing validation action (${label})`);
930
- const valResp = {
931
- uid: peprValidateRequest.Request.uid,
932
- allowed: true
933
- // Assume it's allowed until a validation check fails
826
+ /**
827
+ * Initializes the cache miss gauge for a given label.
828
+ * @param label - The label for the cache miss.
829
+ */
830
+ initCacheMissWindow = (window) => {
831
+ this.#rollCacheMissWindows();
832
+ this.#gauges.get(this.#getMetricName(this.#metricNames.cacheMiss))?.set({ window }, 0);
833
+ this.#cacheMissWindows.set(window, 0);
934
834
  };
935
- try {
936
- const callbackResp = await binding.validateCallback(peprValidateRequest);
937
- valResp.allowed = callbackResp.allowed;
938
- if (callbackResp.statusCode || callbackResp.statusMessage) {
939
- valResp.status = {
940
- code: callbackResp.statusCode || 400,
941
- message: callbackResp.statusMessage || `Validation failed for ${name}`
942
- };
943
- }
944
- logger_default.info(actionMetadata, `Validation action complete (${label}): ${callbackResp.allowed ? "allowed" : "denied"}`);
945
- return valResp;
946
- } catch (e) {
947
- logger_default.error(actionMetadata, `Action failed: ${JSON.stringify(e)}`);
948
- valResp.allowed = false;
949
- valResp.status = {
950
- code: 500,
951
- message: `Action failed with error: ${JSON.stringify(e)}`
952
- };
953
- return valResp;
954
- }
955
- }
956
- async function validateProcessor(config, capabilities, req, reqMetadata) {
957
- const webhookTimer = new MeasureWebhookTimeout("validate" /* VALIDATE */);
958
- webhookTimer.start(config.webhookTimeout);
959
- const wrapped = new PeprValidateRequest(req);
960
- const response = [];
961
- if (req.kind.version === "v1" && req.kind.kind === "Secret") {
962
- convertFromBase64Map(wrapped.Raw);
963
- }
964
- logger_default.info(reqMetadata, `Processing validation request`);
965
- for (const { name: name2, bindings, namespaces } of capabilities) {
966
- const actionMetadata = { ...reqMetadata, name: name2 };
967
- for (const binding of bindings) {
968
- if (!binding.validateCallback) {
969
- continue;
970
- }
971
- const shouldSkip = shouldSkipRequest(
972
- binding,
973
- req,
974
- namespaces,
975
- resolveIgnoreNamespaces(config?.alwaysIgnore?.namespaces)
976
- );
977
- if (shouldSkip !== "") {
978
- logger_default.debug(shouldSkip);
979
- continue;
835
+ /**
836
+ * Manages the size of the cache miss gauge map.
837
+ */
838
+ #rollCacheMissWindows = () => {
839
+ const maxCacheMissWindows = process.env.PEPR_MAX_CACHE_MISS_WINDOWS ? parseInt(process.env.PEPR_MAX_CACHE_MISS_WINDOWS, 10) : void 0;
840
+ if (maxCacheMissWindows !== void 0 && this.#cacheMissWindows.size >= maxCacheMissWindows) {
841
+ const firstKey = this.#cacheMissWindows.keys().next().value;
842
+ if (firstKey !== void 0) {
843
+ this.#cacheMissWindows.delete(firstKey);
980
844
  }
981
- const resp = await processRequest2(binding, actionMetadata, wrapped);
982
- response.push(resp);
845
+ this.#gauges.get(this.#getMetricName(this.#metricNames.cacheMiss))?.remove({ window: firstKey });
983
846
  }
984
- }
985
- webhookTimer.stop();
986
- return response;
987
- }
988
-
989
- // src/lib/controller/store.ts
990
- var import_kubernetes_fluent_client3 = require("kubernetes-fluent-client");
991
- var import_ramda6 = require("ramda");
992
-
993
- // src/lib/k8s.ts
994
- var import_kubernetes_fluent_client = require("kubernetes-fluent-client");
995
- var Store = class extends import_kubernetes_fluent_client.GenericKind {
996
- };
997
- var peprStoreGVK = {
998
- kind: "PeprStore",
999
- version: "v1",
1000
- group: "pepr.dev"
847
+ };
1001
848
  };
1002
- (0, import_kubernetes_fluent_client.RegisterKind)(Store, peprStoreGVK);
849
+ var metricsCollector = new MetricsCollector("pepr");
1003
850
 
1004
- // src/lib/controller/storeCache.ts
1005
- var import_kubernetes_fluent_client2 = require("kubernetes-fluent-client");
1006
- var import_http_status_codes = require("http-status-codes");
1007
- var sendUpdatesAndFlushCache = async (cache, namespace2, name2) => {
1008
- const indexes = Object.keys(cache);
1009
- const payload = Object.values(cache);
1010
- try {
1011
- if (payload.length > 0) {
1012
- await (0, import_kubernetes_fluent_client2.K8s)(Store, { namespace: namespace2, name: name2 }).Patch(updateCacheID(payload));
1013
- Object.keys(cache).forEach((key) => delete cache[key]);
1014
- }
1015
- } catch (err) {
1016
- logger_default.error(err, "Pepr store update failure");
1017
- if (err.status === import_http_status_codes.StatusCodes.UNPROCESSABLE_ENTITY) {
1018
- Object.keys(cache).forEach((key) => delete cache[key]);
1019
- } else {
1020
- indexes.forEach((index) => {
1021
- cache[index] = payload[Number(index)];
1022
- });
1023
- }
851
+ // src/lib/processors/mutate-processor.ts
852
+ var import_fast_json_patch = __toESM(require("fast-json-patch"));
853
+ var import_ramda6 = require("ramda");
854
+
855
+ // src/lib/telemetry/timeUtils.ts
856
+ var getNow = () => performance.now();
857
+
858
+ // src/lib/telemetry/webhookTimeouts.ts
859
+ var MeasureWebhookTimeout = class {
860
+ #startTime = null;
861
+ #webhookType;
862
+ timeout = 0;
863
+ constructor(webhookType) {
864
+ this.#webhookType = webhookType;
865
+ metricsCollector.addCounter(`${webhookType}_timeouts`, `Number of ${webhookType} webhook timeouts`);
1024
866
  }
1025
- return cache;
1026
- };
1027
- var fillStoreCache = (cache, capabilityName, op, cacheItem) => {
1028
- const path = [`/data/${capabilityName}`, cacheItem.version, cacheItem.key].filter((str) => str !== "" && str !== void 0).join("-");
1029
- if (op === "add") {
1030
- const value = cacheItem.value || "";
1031
- const cacheIdx = [op, path, value].join(":");
1032
- cache[cacheIdx] = { op, path, value };
1033
- } else if (op === "remove") {
1034
- if (cacheItem.key.length < 1) {
1035
- throw new Error(`Key is required for REMOVE operation`);
867
+ start(timeout = 10) {
868
+ this.#startTime = getNow();
869
+ this.timeout = timeout;
870
+ logger_default.info(`Starting timer at ${this.#startTime}`);
871
+ }
872
+ stop() {
873
+ if (this.#startTime === null) {
874
+ throw new Error("Timer was not started before calling stop.");
875
+ }
876
+ const elapsedTime = getNow() - this.#startTime;
877
+ logger_default.info(`Webhook ${this.#startTime} took ${elapsedTime}ms`);
878
+ this.#startTime = null;
879
+ if (elapsedTime > this.timeout) {
880
+ metricsCollector.incCounter(`${this.#webhookType}_timeouts`);
1036
881
  }
1037
- const cacheIndex = [op, path].join(":");
1038
- cache[cacheIndex] = { op, path };
1039
- } else {
1040
- throw new Error(`Unsupported operation: ${op}`);
1041
882
  }
1042
- return cache;
1043
883
  };
1044
- function updateCacheID(payload) {
1045
- payload.push({
1046
- op: "replace",
1047
- path: "/metadata/labels/pepr.dev-cacheID",
1048
- value: `${Date.now()}`
1049
- });
1050
- return payload;
1051
- }
1052
884
 
1053
- // src/lib/controller/store.ts
1054
- var namespace = "pepr-system";
1055
- var debounceBackoffReceive = 1e3;
1056
- var debounceBackoffSend = 4e3;
1057
- var StoreController = class {
1058
- #name;
1059
- #stores = {};
1060
- #sendDebounce;
1061
- #onReady;
1062
- constructor(capabilities, name2, onReady) {
1063
- this.#onReady = onReady;
1064
- this.#name = name2;
1065
- const setStorageInstance = (registrationFunction, name3) => {
1066
- const scheduleStore = registrationFunction();
1067
- scheduleStore.registerSender(this.#send(name3));
1068
- this.#stores[name3] = scheduleStore;
1069
- };
1070
- if (name2.includes("schedule")) {
1071
- for (const { name: name3, registerScheduleStore, hasSchedule } of capabilities) {
1072
- if (hasSchedule === true) {
1073
- setStorageInstance(registerScheduleStore, name3);
1074
- }
1075
- }
1076
- } else {
1077
- for (const { name: name3, registerStore } of capabilities) {
1078
- setStorageInstance(registerStore, name3);
1079
- }
885
+ // src/lib/filter/adjudicators/adjudicators.ts
886
+ var import_ramda3 = require("ramda");
887
+ var declaredOperation = (0, import_ramda3.pipe)(
888
+ (request) => request?.operation,
889
+ (0, import_ramda3.defaultTo)("")
890
+ );
891
+ var declaredGroup = (0, import_ramda3.pipe)(
892
+ (request) => request?.kind?.group,
893
+ (0, import_ramda3.defaultTo)("")
894
+ );
895
+ var declaredVersion = (0, import_ramda3.pipe)(
896
+ (request) => request?.kind?.version,
897
+ (0, import_ramda3.defaultTo)("")
898
+ );
899
+ var declaredKind = (0, import_ramda3.pipe)(
900
+ (request) => request?.kind?.kind,
901
+ (0, import_ramda3.defaultTo)("")
902
+ );
903
+ var declaredUid = (0, import_ramda3.pipe)((request) => request?.uid, (0, import_ramda3.defaultTo)(""));
904
+ var carriesDeletionTimestamp = (0, import_ramda3.pipe)(
905
+ (kubernetesObject) => !!kubernetesObject.metadata?.deletionTimestamp,
906
+ (0, import_ramda3.defaultTo)(false)
907
+ );
908
+ var missingDeletionTimestamp = (0, import_ramda3.complement)(carriesDeletionTimestamp);
909
+ var carriedKind = (0, import_ramda3.pipe)(
910
+ (kubernetesObject) => kubernetesObject?.kind,
911
+ (0, import_ramda3.defaultTo)("not set")
912
+ );
913
+ var carriedVersion = (0, import_ramda3.pipe)(
914
+ (kubernetesObject) => kubernetesObject?.metadata?.resourceVersion,
915
+ (0, import_ramda3.defaultTo)("not set")
916
+ );
917
+ var carriedName = (0, import_ramda3.pipe)(
918
+ (kubernetesObject) => kubernetesObject?.metadata?.name,
919
+ (0, import_ramda3.defaultTo)("")
920
+ );
921
+ var carriesName = (0, import_ramda3.pipe)(carriedName, (0, import_ramda3.equals)(""), import_ramda3.not);
922
+ var missingName = (0, import_ramda3.complement)(carriesName);
923
+ var carriedNamespace = (0, import_ramda3.pipe)(
924
+ (kubernetesObject) => kubernetesObject?.metadata?.namespace,
925
+ (0, import_ramda3.defaultTo)("")
926
+ );
927
+ var carriesNamespace = (0, import_ramda3.pipe)(carriedNamespace, (0, import_ramda3.equals)(""), import_ramda3.not);
928
+ var carriedAnnotations = (0, import_ramda3.pipe)(
929
+ (kubernetesObject) => kubernetesObject?.metadata?.annotations,
930
+ (0, import_ramda3.defaultTo)({})
931
+ );
932
+ var carriesAnnotations = (0, import_ramda3.pipe)(carriedAnnotations, (0, import_ramda3.equals)({}), import_ramda3.not);
933
+ var carriedLabels = (0, import_ramda3.pipe)(
934
+ (kubernetesObject) => kubernetesObject?.metadata?.labels,
935
+ (0, import_ramda3.defaultTo)({})
936
+ );
937
+ var carriesLabels = (0, import_ramda3.pipe)(carriedLabels, (0, import_ramda3.equals)({}), import_ramda3.not);
938
+ var definesDeletionTimestamp = (0, import_ramda3.pipe)(
939
+ (binding) => binding?.filters?.deletionTimestamp ?? false,
940
+ (0, import_ramda3.defaultTo)(false)
941
+ );
942
+ var ignoresDeletionTimestamp = (0, import_ramda3.complement)(definesDeletionTimestamp);
943
+ var definedName = (0, import_ramda3.pipe)((binding) => {
944
+ return binding.filters.name;
945
+ }, (0, import_ramda3.defaultTo)(""));
946
+ var definesName = (0, import_ramda3.pipe)(definedName, (0, import_ramda3.equals)(""), import_ramda3.not);
947
+ var ignoresName = (0, import_ramda3.complement)(definesName);
948
+ var definedNameRegex = (0, import_ramda3.pipe)(
949
+ (binding) => binding.filters?.regexName,
950
+ (0, import_ramda3.defaultTo)("")
951
+ );
952
+ var definesNameRegex = (0, import_ramda3.pipe)(definedNameRegex, (0, import_ramda3.equals)(""), import_ramda3.not);
953
+ var definedNamespaces = (0, import_ramda3.pipe)((binding) => binding?.filters?.namespaces, (0, import_ramda3.defaultTo)([]));
954
+ var definesNamespaces = (0, import_ramda3.pipe)(definedNamespaces, (0, import_ramda3.equals)([]), import_ramda3.not);
955
+ var definedNamespaceRegexes = (0, import_ramda3.pipe)((binding) => binding?.filters?.regexNamespaces, (0, import_ramda3.defaultTo)([]));
956
+ var definesNamespaceRegexes = (0, import_ramda3.pipe)(definedNamespaceRegexes, (0, import_ramda3.equals)([]), import_ramda3.not);
957
+ var definedAnnotations = (0, import_ramda3.pipe)((binding) => binding?.filters?.annotations, (0, import_ramda3.defaultTo)({}));
958
+ var definesAnnotations = (0, import_ramda3.pipe)(definedAnnotations, (0, import_ramda3.equals)({}), import_ramda3.not);
959
+ var definedLabels = (0, import_ramda3.pipe)((binding) => binding?.filters?.labels, (0, import_ramda3.defaultTo)({}));
960
+ var definesLabels = (0, import_ramda3.pipe)(definedLabels, (0, import_ramda3.equals)({}), import_ramda3.not);
961
+ var definedEvent = (binding) => {
962
+ return binding.event;
963
+ };
964
+ var definesDelete = (0, import_ramda3.pipe)(definedEvent, (0, import_ramda3.equals)("DELETE" /* DELETE */));
965
+ var definedGroup = (0, import_ramda3.pipe)((binding) => binding?.kind?.group, (0, import_ramda3.defaultTo)(""));
966
+ var definesGroup = (0, import_ramda3.pipe)(definedGroup, (0, import_ramda3.equals)(""), import_ramda3.not);
967
+ var definedVersion = (0, import_ramda3.pipe)(
968
+ (binding) => binding?.kind?.version,
969
+ (0, import_ramda3.defaultTo)("")
970
+ );
971
+ var definesVersion = (0, import_ramda3.pipe)(definedVersion, (0, import_ramda3.equals)(""), import_ramda3.not);
972
+ var definedKind = (0, import_ramda3.pipe)((binding) => binding?.kind?.kind, (0, import_ramda3.defaultTo)(""));
973
+ var definesKind = (0, import_ramda3.pipe)(definedKind, (0, import_ramda3.equals)(""), import_ramda3.not);
974
+ var definedCallback = (binding) => {
975
+ return binding.isFinalize ? binding.finalizeCallback : binding.isWatch ? binding.watchCallback : binding.isMutate ? binding.mutateCallback : binding.isValidate ? binding.validateCallback : null;
976
+ };
977
+ var definedCallbackName = (0, import_ramda3.pipe)(definedCallback, (0, import_ramda3.defaultTo)({ name: "" }), (callback) => callback.name);
978
+ var mismatchedDeletionTimestamp = (0, import_ramda3.allPass)([
979
+ (0, import_ramda3.pipe)((0, import_ramda3.nthArg)(0), definesDeletionTimestamp),
980
+ (0, import_ramda3.pipe)((0, import_ramda3.nthArg)(1), missingDeletionTimestamp)
981
+ ]);
982
+ var mismatchedName = (0, import_ramda3.allPass)([
983
+ (0, import_ramda3.pipe)((0, import_ramda3.nthArg)(0), definesName),
984
+ (0, import_ramda3.pipe)((binding, kubernetesObject) => definedName(binding) !== carriedName(kubernetesObject))
985
+ ]);
986
+ var mismatchedNameRegex = (0, import_ramda3.allPass)([
987
+ (0, import_ramda3.pipe)((0, import_ramda3.nthArg)(0), definesNameRegex),
988
+ (0, import_ramda3.pipe)((binding, kubernetesObject) => new RegExp(definedNameRegex(binding)).test(carriedName(kubernetesObject)), import_ramda3.not)
989
+ ]);
990
+ var bindsToKind = (0, import_ramda3.curry)(
991
+ (0, import_ramda3.allPass)([(0, import_ramda3.pipe)((0, import_ramda3.nthArg)(0), definedKind, (0, import_ramda3.equals)(""), import_ramda3.not), (0, import_ramda3.pipe)((binding, kind3) => definedKind(binding) === kind3)])
992
+ );
993
+ var bindsToNamespace = (0, import_ramda3.curry)((0, import_ramda3.pipe)(bindsToKind(import_ramda3.__, "Namespace")));
994
+ var misboundNamespace = (0, import_ramda3.allPass)([bindsToNamespace, definesNamespaces]);
995
+ var mismatchedNamespace = (0, import_ramda3.allPass)([
996
+ (0, import_ramda3.pipe)((0, import_ramda3.nthArg)(0), definesNamespaces),
997
+ (0, import_ramda3.pipe)((binding, kubernetesObject) => definedNamespaces(binding).includes(carriedNamespace(kubernetesObject)), import_ramda3.not)
998
+ ]);
999
+ var mismatchedNamespaceRegex = (0, import_ramda3.allPass)([
1000
+ (0, import_ramda3.pipe)((0, import_ramda3.nthArg)(0), definesNamespaceRegexes),
1001
+ (0, import_ramda3.pipe)(
1002
+ (binding, kubernetesObject) => (0, import_ramda3.pipe)(
1003
+ (0, import_ramda3.any)((regEx) => new RegExp(regEx).test(carriedNamespace(kubernetesObject))),
1004
+ import_ramda3.not
1005
+ )(definedNamespaceRegexes(binding))
1006
+ )
1007
+ ]);
1008
+ var metasMismatch = (0, import_ramda3.pipe)(
1009
+ (defined, carried) => {
1010
+ const result = { defined, carried, unalike: {} };
1011
+ result.unalike = Object.entries(result.defined).map(([key, value]) => {
1012
+ const keyMissing = !Object.hasOwn(result.carried, key);
1013
+ const noValue = !value;
1014
+ const valMissing = !result.carried[key];
1015
+ const valDiffers = result.carried[key] !== result.defined[key];
1016
+ return keyMissing ? { [key]: value } : noValue ? {} : valMissing ? { [key]: value } : valDiffers ? { [key]: value } : {};
1017
+ }).reduce((acc, cur) => ({ ...acc, ...cur }), {});
1018
+ return result.unalike;
1019
+ },
1020
+ (unalike) => Object.keys(unalike).length > 0
1021
+ );
1022
+ var mismatchedAnnotations = (0, import_ramda3.allPass)([
1023
+ (0, import_ramda3.pipe)((0, import_ramda3.nthArg)(0), definesAnnotations),
1024
+ (0, import_ramda3.pipe)((binding, kubernetesObject) => metasMismatch(definedAnnotations(binding), carriedAnnotations(kubernetesObject)))
1025
+ ]);
1026
+ var mismatchedLabels = (0, import_ramda3.allPass)([
1027
+ (0, import_ramda3.pipe)((0, import_ramda3.nthArg)(0), definesLabels),
1028
+ (0, import_ramda3.pipe)((binding, kubernetesObject) => metasMismatch(definedLabels(binding), carriedLabels(kubernetesObject)))
1029
+ ]);
1030
+ var uncarryableNamespace = (0, import_ramda3.allPass)([
1031
+ (0, import_ramda3.pipe)((0, import_ramda3.nthArg)(0), import_ramda3.length, (0, import_ramda3.gt)(import_ramda3.__, 0)),
1032
+ (0, import_ramda3.pipe)((namespaceSelector, kubernetesObject) => {
1033
+ if (kubernetesObject?.kind === "Namespace") {
1034
+ return namespaceSelector.includes(kubernetesObject?.metadata?.name);
1080
1035
  }
1081
- setTimeout(
1082
- () => (0, import_kubernetes_fluent_client3.K8s)(Store).InNamespace(namespace).Get(this.#name).then(async (store) => await this.#migrateAndSetupWatch(store)).catch(this.#createStoreResource),
1083
- Math.random() * 3e3
1084
- // Add a jitter to the Store creation to avoid collisions
1085
- );
1086
- }
1087
- #setupWatch = () => {
1088
- const watcher = (0, import_kubernetes_fluent_client3.K8s)(Store, { name: this.#name, namespace }).Watch(this.#receive);
1089
- watcher.start().catch((e) => logger_default.error(e, "Error starting Pepr store watch"));
1090
- };
1091
- #migrateAndSetupWatch = async (store) => {
1092
- logger_default.debug(redactedStore(store), "Pepr Store migration");
1093
- await (0, import_kubernetes_fluent_client3.K8s)(Store, { namespace, name: this.#name }).Patch([
1094
- {
1095
- op: "add",
1096
- path: "/metadata/labels/pepr.dev-cacheID",
1097
- value: `${Date.now()}`
1098
- }
1099
- ]);
1100
- const data = store.data || {};
1101
- let storeCache = {};
1102
- for (const name2 of Object.keys(this.#stores)) {
1103
- const offset = `${name2}-`.length;
1104
- for (const key of Object.keys(data)) {
1105
- if ((0, import_ramda6.startsWith)(name2, key) && !(0, import_ramda6.startsWith)(`${name2}-v2`, key)) {
1106
- storeCache = fillStoreCache(storeCache, name2, "remove", {
1107
- key: [key.slice(offset)],
1108
- value: data[key]
1109
- });
1110
- storeCache = fillStoreCache(storeCache, name2, "add", {
1111
- key: [key.slice(offset)],
1112
- value: data[key],
1113
- version: "v2"
1114
- });
1115
- }
1116
- }
1036
+ if (carriesNamespace(kubernetesObject)) {
1037
+ return namespaceSelector.includes(carriedNamespace(kubernetesObject));
1117
1038
  }
1118
- storeCache = await sendUpdatesAndFlushCache(storeCache, namespace, this.#name);
1119
- this.#setupWatch();
1120
- };
1121
- #receive = (store) => {
1122
- logger_default.debug(redactedStore(store), "Pepr Store update");
1123
- const debounced = () => {
1124
- const data = store.data || {};
1125
- for (const name2 of Object.keys(this.#stores)) {
1126
- const offset = `${name2}-`.length;
1127
- const filtered = {};
1128
- for (const key of Object.keys(data)) {
1129
- if ((0, import_ramda6.startsWith)(name2, key)) {
1130
- filtered[key.slice(offset)] = data[key];
1131
- }
1132
- }
1133
- this.#stores[name2].receive(filtered);
1134
- }
1135
- if (this.#onReady) {
1136
- this.#onReady();
1137
- this.#onReady = void 0;
1138
- }
1139
- };
1140
- clearTimeout(this.#sendDebounce);
1141
- this.#sendDebounce = setTimeout(debounced, this.#onReady ? 0 : debounceBackoffReceive);
1142
- };
1143
- #send = (capabilityName) => {
1144
- let storeCache = {};
1145
- const sender = async (op, key, value) => {
1146
- storeCache = fillStoreCache(storeCache, capabilityName, op, { key, value });
1147
- };
1148
- setInterval(() => {
1149
- if (Object.keys(storeCache).length > 0) {
1150
- logger_default.debug(redactedPatch(storeCache), "Sending updates to Pepr store");
1151
- void sendUpdatesAndFlushCache(storeCache, namespace, this.#name);
1152
- }
1153
- }, debounceBackoffSend);
1154
- return sender;
1155
- };
1156
- #createStoreResource = async (e) => {
1157
- logger_default.info(`Pepr store not found, creating...`);
1158
- logger_default.debug(e);
1159
- try {
1160
- await (0, import_kubernetes_fluent_client3.K8s)(Store).Apply({
1161
- metadata: {
1162
- name: this.#name,
1163
- namespace,
1164
- labels: {
1165
- "pepr.dev-cacheID": `${Date.now()}`
1166
- }
1167
- },
1168
- data: {
1169
- // JSON Patch will die if the data is empty, so we need to add a placeholder
1170
- __pepr_do_not_delete__: "k-thx-bye"
1171
- }
1172
- });
1173
- this.#setupWatch();
1174
- } catch (err) {
1175
- logger_default.error(err, "Failed to create Pepr store");
1039
+ return true;
1040
+ }, import_ramda3.not)
1041
+ ]);
1042
+ var missingCarriableNamespace = (0, import_ramda3.allPass)([
1043
+ (0, import_ramda3.pipe)((0, import_ramda3.nthArg)(0), import_ramda3.length, (0, import_ramda3.gt)(import_ramda3.__, 0)),
1044
+ (0, import_ramda3.pipe)(
1045
+ (namespaceSelector, kubernetesObject) => kubernetesObject.kind === "Namespace" ? !namespaceSelector.includes(kubernetesObject.metadata.name) : !carriesNamespace(kubernetesObject)
1046
+ )
1047
+ ]);
1048
+ var carriesIgnoredNamespace = (0, import_ramda3.allPass)([
1049
+ (0, import_ramda3.pipe)((0, import_ramda3.nthArg)(0), import_ramda3.length, (0, import_ramda3.gt)(import_ramda3.__, 0)),
1050
+ (0, import_ramda3.pipe)((namespaceSelector, kubernetesObject) => {
1051
+ if (kubernetesObject?.kind === "Namespace") {
1052
+ return namespaceSelector.includes(kubernetesObject?.metadata?.name);
1053
+ }
1054
+ if (carriesNamespace(kubernetesObject)) {
1055
+ return namespaceSelector.includes(carriedNamespace(kubernetesObject));
1056
+ }
1057
+ return false;
1058
+ })
1059
+ ]);
1060
+ var unbindableNamespaces = (0, import_ramda3.allPass)([
1061
+ (0, import_ramda3.pipe)((0, import_ramda3.nthArg)(0), import_ramda3.length, (0, import_ramda3.gt)(import_ramda3.__, 0)),
1062
+ (0, import_ramda3.pipe)((0, import_ramda3.nthArg)(1), definesNamespaces),
1063
+ (0, import_ramda3.pipe)(
1064
+ (namespaceSelector, binding) => (0, import_ramda3.difference)(definedNamespaces(binding), namespaceSelector),
1065
+ import_ramda3.length,
1066
+ (0, import_ramda3.equals)(0),
1067
+ import_ramda3.not
1068
+ )
1069
+ ]);
1070
+ var misboundDeleteWithDeletionTimestamp = (0, import_ramda3.allPass)([definesDelete, definesDeletionTimestamp]);
1071
+ var operationMatchesEvent = (0, import_ramda3.anyPass)([
1072
+ (0, import_ramda3.pipe)((0, import_ramda3.nthArg)(1), (0, import_ramda3.equals)("*" /* ANY */)),
1073
+ (0, import_ramda3.pipe)((operation, event) => operation.valueOf() === event.valueOf()),
1074
+ (0, import_ramda3.pipe)((operation, event) => operation ? event.includes(operation) : false)
1075
+ ]);
1076
+ var mismatchedEvent = (0, import_ramda3.pipe)(
1077
+ (binding, request) => operationMatchesEvent(declaredOperation(request), definedEvent(binding)),
1078
+ import_ramda3.not
1079
+ );
1080
+ var mismatchedGroup = (0, import_ramda3.allPass)([
1081
+ (0, import_ramda3.pipe)((0, import_ramda3.nthArg)(0), definesGroup),
1082
+ (0, import_ramda3.pipe)((binding, request) => definedGroup(binding) !== declaredGroup(request))
1083
+ ]);
1084
+ var mismatchedVersion = (0, import_ramda3.allPass)([
1085
+ (0, import_ramda3.pipe)((0, import_ramda3.nthArg)(0), definesVersion),
1086
+ (0, import_ramda3.pipe)((binding, request) => definedVersion(binding) !== declaredVersion(request))
1087
+ ]);
1088
+ var mismatchedKind = (0, import_ramda3.allPass)([
1089
+ (0, import_ramda3.pipe)((0, import_ramda3.nthArg)(0), definesKind),
1090
+ (0, import_ramda3.pipe)((binding, request) => definedKind(binding) !== declaredKind(request))
1091
+ ]);
1092
+
1093
+ // src/lib/filter/filter.ts
1094
+ function shouldSkipRequest(binding, req, capabilityNamespaces, ignoredNamespaces) {
1095
+ const obj = req.operation === "DELETE" /* DELETE */ ? req.oldObject : req.object;
1096
+ const prefix = "Ignoring Admission Callback:";
1097
+ const adjudicators = [
1098
+ () => adjudicateMisboundDeleteWithDeletionTimestamp(binding),
1099
+ () => adjudicateMismatchedDeletionTimestamp(binding, obj),
1100
+ () => adjudicateMismatchedEvent(binding, req),
1101
+ () => adjudicateMismatchedName(binding, obj),
1102
+ () => adjudicateMismatchedGroup(binding, req),
1103
+ () => adjudicateMismatchedVersion(binding, req),
1104
+ () => adjudicateMismatchedKind(binding, req),
1105
+ () => adjudicateUnbindableNamespaces(capabilityNamespaces, binding),
1106
+ () => adjudicateUncarryableNamespace(capabilityNamespaces, obj),
1107
+ () => adjudicateMismatchedNamespace(binding, obj),
1108
+ () => adjudicateMismatchedLabels(binding, obj),
1109
+ () => adjudicateMismatchedAnnotations(binding, obj),
1110
+ () => adjudicateMismatchedNamespaceRegex(binding, obj),
1111
+ () => adjudicateMismatchedNameRegex(binding, obj),
1112
+ () => adjudicateCarriesIgnoredNamespace(ignoredNamespaces, obj),
1113
+ () => adjudicateMissingCarriableNamespace(capabilityNamespaces, obj)
1114
+ ];
1115
+ for (const adjudicator of adjudicators) {
1116
+ const result = adjudicator();
1117
+ if (result) {
1118
+ return `${prefix} ${result}`;
1176
1119
  }
1177
- };
1178
- };
1179
-
1180
- // src/lib/controller/index.util.ts
1181
- function karForMutate(mr) {
1182
- return {
1183
- apiVersion: "admission.k8s.io/v1",
1184
- kind: "AdmissionReview",
1185
- response: mr
1186
- };
1120
+ }
1121
+ return "";
1187
1122
  }
1188
- function karForValidate(ar, vr) {
1189
- const isAllowed = vr.filter((r) => !r.allowed).length === 0;
1190
- const resp = vr.length === 0 ? {
1191
- uid: ar.uid,
1192
- allowed: true,
1193
- status: { code: 200, message: "no in-scope validations -- allowed!" }
1194
- } : {
1195
- uid: vr[0].uid,
1196
- allowed: isAllowed,
1197
- status: {
1198
- code: isAllowed ? 200 : 422,
1199
- message: vr.filter((rl) => !rl.allowed).map((curr) => curr.status?.message).join("; ")
1123
+ function filterNoMatchReason(binding, obj, capabilityNamespaces, ignoredNamespaces) {
1124
+ const prefix = "Ignoring Watch Callback:";
1125
+ const adjudicators = [
1126
+ () => adjudicateMismatchedDeletionTimestamp(binding, obj),
1127
+ () => adjudicateMismatchedName(binding, obj),
1128
+ () => adjudicateMisboundNamespace(binding),
1129
+ () => adjudicateMismatchedLabels(binding, obj),
1130
+ () => adjudicateMismatchedAnnotations(binding, obj),
1131
+ () => adjudicateUncarryableNamespace(capabilityNamespaces, obj),
1132
+ () => adjudicateUnbindableNamespaces(capabilityNamespaces, binding),
1133
+ () => adjudicateMismatchedNamespace(binding, obj),
1134
+ () => adjudicateMismatchedNamespaceRegex(binding, obj),
1135
+ () => adjudicateMismatchedNameRegex(binding, obj),
1136
+ () => adjudicateCarriesIgnoredNamespace(ignoredNamespaces, obj),
1137
+ () => adjudicateMissingCarriableNamespace(capabilityNamespaces, obj)
1138
+ ];
1139
+ for (const adjudicator of adjudicators) {
1140
+ const result = adjudicator();
1141
+ if (result) {
1142
+ return `${prefix} ${result}`;
1200
1143
  }
1201
- };
1202
- return {
1203
- apiVersion: "admission.k8s.io/v1",
1204
- kind: "AdmissionReview",
1205
- response: resp
1206
- };
1144
+ }
1145
+ return "";
1207
1146
  }
1208
-
1209
- // src/lib/controller/index.ts
1210
- if (!process.env.PEPR_NODE_WARNINGS) {
1211
- process.removeAllListeners("warning");
1147
+ function adjudicateMisboundNamespace(binding) {
1148
+ return misboundNamespace(binding) ? "Cannot use namespace filter on a namespace object." : null;
1212
1149
  }
1213
- var Controller = class _Controller {
1214
- // Track whether the server is running
1215
- #running = false;
1216
- // Metrics collector
1217
- #metricsCollector = metricsCollector;
1218
- // The token used to authenticate requests
1219
- #token = "";
1220
- // The express app instance
1221
- #app = (0, import_express.default)();
1222
- // Initialized with the constructor
1223
- #config;
1224
- #capabilities;
1225
- #beforeHook;
1226
- #afterHook;
1227
- constructor(config, capabilities, hooks = {}) {
1228
- const { beforeHook, afterHook, onReady } = hooks;
1229
- this.#config = config;
1230
- this.#capabilities = capabilities;
1231
- new StoreController(capabilities, `pepr-${config.uuid}-store`, () => {
1232
- this.#bindEndpoints();
1233
- if (typeof onReady === "function") {
1234
- onReady();
1235
- }
1236
- logger_default.info("\u2705 Controller startup complete");
1237
- new StoreController(capabilities, `pepr-${config.uuid}-schedule`, () => {
1238
- logger_default.info("\u2705 Scheduling processed");
1239
- });
1240
- });
1241
- this.#app.use(_Controller.#logger);
1242
- this.#app.use(import_express.default.json({ limit: "2mb" }));
1243
- if (beforeHook) {
1244
- logger_default.info(`Using beforeHook: ${beforeHook}`);
1245
- this.#beforeHook = beforeHook;
1246
- }
1247
- if (afterHook) {
1248
- logger_default.info(`Using afterHook: ${afterHook}`);
1249
- this.#afterHook = afterHook;
1250
- }
1150
+ function adjudicateMisboundDeleteWithDeletionTimestamp(binding) {
1151
+ return misboundDeleteWithDeletionTimestamp(binding) ? "Cannot use deletionTimestamp filter on a DELETE operation." : null;
1152
+ }
1153
+ function adjudicateMismatchedDeletionTimestamp(binding, obj) {
1154
+ return mismatchedDeletionTimestamp(binding, obj) ? "Binding defines deletionTimestamp but Object does not carry it." : null;
1155
+ }
1156
+ function adjudicateMismatchedEvent(binding, req) {
1157
+ return mismatchedEvent(binding, req) ? `Binding defines event '${definedEvent(binding)}' but Request declares '${declaredOperation(req)}'.` : null;
1158
+ }
1159
+ function adjudicateMismatchedName(binding, obj) {
1160
+ return mismatchedName(binding, obj) ? `Binding defines name '${definedName(binding)}' but Object carries '${carriedName(obj)}'.` : null;
1161
+ }
1162
+ function adjudicateMismatchedGroup(binding, req) {
1163
+ return mismatchedGroup(binding, req) ? `Binding defines group '${definedGroup(binding)}' but Request declares '${declaredGroup(req)}'.` : null;
1164
+ }
1165
+ function adjudicateMismatchedVersion(binding, req) {
1166
+ return mismatchedVersion(binding, req) ? `Binding defines version '${definedVersion(binding)}' but Request declares '${declaredVersion(req)}'.` : null;
1167
+ }
1168
+ function adjudicateMismatchedKind(binding, req) {
1169
+ return mismatchedKind(binding, req) ? `Binding defines kind '${definedKind(binding)}' but Request declares '${declaredKind(req)}'.` : null;
1170
+ }
1171
+ function adjudicateUnbindableNamespaces(capabilityNamespaces, binding) {
1172
+ return unbindableNamespaces(capabilityNamespaces, binding) ? `Binding defines namespaces ${JSON.stringify(definedNamespaces(binding))} but namespaces allowed by Capability are '${JSON.stringify(capabilityNamespaces)}'.` : null;
1173
+ }
1174
+ function adjudicateUncarryableNamespace(capabilityNamespaces, obj) {
1175
+ return uncarryableNamespace(capabilityNamespaces, obj) ? `Object carries namespace '${obj.kind && obj.kind === "Namespace" ? obj.metadata?.name : carriedNamespace(obj)}' but namespaces allowed by Capability are '${JSON.stringify(capabilityNamespaces)}'.` : null;
1176
+ }
1177
+ function adjudicateMismatchedNamespace(binding, obj) {
1178
+ return mismatchedNamespace(binding, obj) ? `Binding defines namespaces '${JSON.stringify(definedNamespaces(binding))}' but Object carries '${carriedNamespace(obj)}'.` : null;
1179
+ }
1180
+ function adjudicateMismatchedLabels(binding, obj) {
1181
+ return mismatchedLabels(binding, obj) ? `Binding defines labels '${JSON.stringify(definedLabels(binding))}' but Object carries '${JSON.stringify(carriedLabels(obj))}'.` : null;
1182
+ }
1183
+ function adjudicateMismatchedAnnotations(binding, obj) {
1184
+ return mismatchedAnnotations(binding, obj) ? `Binding defines annotations '${JSON.stringify(definedAnnotations(binding))}' but Object carries '${JSON.stringify(carriedAnnotations(obj))}'.` : null;
1185
+ }
1186
+ function adjudicateMismatchedNamespaceRegex(binding, obj) {
1187
+ return mismatchedNamespaceRegex(binding, obj) ? `Binding defines namespace regexes '${JSON.stringify(definedNamespaceRegexes(binding))}' but Object carries '${carriedNamespace(obj)}'.` : null;
1188
+ }
1189
+ function adjudicateMismatchedNameRegex(binding, obj) {
1190
+ return mismatchedNameRegex(binding, obj) ? `Binding defines name regex '${definedNameRegex(binding)}' but Object carries '${carriedName(obj)}'.` : null;
1191
+ }
1192
+ function adjudicateCarriesIgnoredNamespace(ignoredNamespaces, obj) {
1193
+ return carriesIgnoredNamespace(ignoredNamespaces, obj) ? `Object carries namespace '${obj.kind && obj.kind === "Namespace" ? obj.metadata?.name : carriedNamespace(obj)}' but ignored namespaces include '${JSON.stringify(ignoredNamespaces)}'.` : null;
1194
+ }
1195
+ function adjudicateMissingCarriableNamespace(capabilityNamespaces, obj) {
1196
+ return missingCarriableNamespace(capabilityNamespaces, obj) ? `Object does not carry a namespace but namespaces allowed by Capability are '${JSON.stringify(capabilityNamespaces)}'.` : null;
1197
+ }
1198
+
1199
+ // src/lib/mutate-request.ts
1200
+ var import_ramda4 = require("ramda");
1201
+ var PeprMutateRequest = class {
1202
+ Raw;
1203
+ #input;
1204
+ get PermitSideEffects() {
1205
+ return !this.#input.dryRun;
1251
1206
  }
1252
- /** Start the webhook server */
1253
- startServer = (port) => {
1254
- if (this.#running) {
1255
- throw new Error("Cannot start Pepr module: Pepr module was not instantiated with deferStart=true");
1207
+ get IsDryRun() {
1208
+ return this.#input.dryRun;
1209
+ }
1210
+ get OldResource() {
1211
+ return this.#input.oldObject;
1212
+ }
1213
+ get Request() {
1214
+ return this.#input;
1215
+ }
1216
+ constructor(input) {
1217
+ this.#input = input;
1218
+ if (input.operation.toUpperCase() === "DELETE" /* DELETE */) {
1219
+ this.Raw = (0, import_ramda4.clone)(input.oldObject);
1220
+ } else {
1221
+ this.Raw = (0, import_ramda4.clone)(input.object);
1256
1222
  }
1257
- const options = {
1258
- key: import_fs.default.readFileSync(process.env.SSL_KEY_PATH || "/etc/certs/tls.key"),
1259
- cert: import_fs.default.readFileSync(process.env.SSL_CERT_PATH || "/etc/certs/tls.crt")
1260
- };
1261
- if (!isWatchMode()) {
1262
- this.#token = process.env.PEPR_API_TOKEN || import_fs.default.readFileSync("/app/api-token/value").toString().trim();
1263
- logger_default.info(`Using API token: ${this.#token}`);
1264
- if (!this.#token) {
1265
- throw new Error("API token not found");
1266
- }
1223
+ if (!this.Raw) {
1224
+ throw new Error("Unable to load the request object into PeprRequest.Raw");
1267
1225
  }
1268
- const server = import_https.default.createServer(options, this.#app).listen(port);
1269
- server.on("listening", () => {
1270
- logger_default.info(`Server listening on port ${port}`);
1271
- this.#running = true;
1272
- });
1273
- server.on("error", (e) => {
1274
- if (e.code === "EADDRINUSE") {
1275
- logger_default.info(
1276
- `Address in use, retrying in 2 seconds. If this persists, ensure ${port} is not in use, e.g. "lsof -i :${port}"`
1277
- );
1278
- setTimeout(() => {
1279
- server.close();
1280
- server.listen(port);
1281
- }, 2e3);
1282
- }
1283
- });
1284
- process.on("SIGTERM", () => {
1285
- logger_default.info("Received SIGTERM, closing server");
1286
- server.close(() => {
1287
- logger_default.info("Server closed");
1288
- process.exit(0);
1289
- });
1290
- });
1226
+ }
1227
+ Merge = (obj) => {
1228
+ this.Raw = (0, import_ramda4.mergeDeepRight)(this.Raw, obj);
1291
1229
  };
1292
- #bindEndpoints = () => {
1293
- this.#app.get("/healthz", _Controller.#healthz);
1294
- this.#app.get("/metrics", this.#metrics);
1295
- if (isWatchMode()) {
1296
- return;
1297
- }
1298
- this.#app.use(["/mutate/:token", "/validate/:token"], this.#validateToken);
1299
- this.#app.post("/mutate/:token", this.#admissionReq("Mutate"));
1300
- this.#app.post("/validate/:token", this.#admissionReq("Validate"));
1230
+ SetLabel = (key, value) => {
1231
+ const ref = this.Raw;
1232
+ ref.metadata = ref.metadata ?? {};
1233
+ ref.metadata.labels = ref.metadata.labels ?? {};
1234
+ ref.metadata.labels[key] = value;
1235
+ return this;
1301
1236
  };
1302
- /**
1303
- * Validate the token in the request path
1304
- *
1305
- * @param req The incoming request
1306
- * @param res The outgoing response
1307
- * @param next The next middleware function
1308
- * @returns
1309
- */
1310
- #validateToken = (req, res, next) => {
1311
- const { token } = req.params;
1312
- if (token !== this.#token) {
1313
- const err = `Unauthorized: invalid token '${token.replace(/[^\w]/g, "_")}'`;
1314
- logger_default.info(err);
1315
- res.status(401).send(err);
1316
- this.#metricsCollector.alert();
1317
- return;
1237
+ SetAnnotation = (key, value) => {
1238
+ const ref = this.Raw;
1239
+ ref.metadata = ref.metadata ?? {};
1240
+ ref.metadata.annotations = ref.metadata.annotations ?? {};
1241
+ ref.metadata.annotations[key] = value;
1242
+ return this;
1243
+ };
1244
+ RemoveLabel = (key) => {
1245
+ if (this.Raw.metadata?.labels?.[key]) {
1246
+ delete this.Raw.metadata.labels[key];
1318
1247
  }
1319
- next();
1248
+ return this;
1320
1249
  };
1321
- /**
1322
- * Metrics endpoint handler
1323
- *
1324
- * @param req the incoming request
1325
- * @param res the outgoing response
1326
- */
1327
- #metrics = async (req, res) => {
1328
- try {
1329
- res.set("Content-Type", "text/plain; version=0.0.4");
1330
- res.send(await this.#metricsCollector.getMetrics());
1331
- } catch (err) {
1332
- logger_default.error(err, `Error getting metrics`);
1333
- res.status(500).send("Internal Server Error");
1250
+ RemoveAnnotation = (key) => {
1251
+ if (this.Raw.metadata?.annotations?.[key]) {
1252
+ delete this.Raw.metadata.annotations[key];
1334
1253
  }
1254
+ return this;
1335
1255
  };
1336
- /**
1337
- * Admission request handler for both mutate and validate requests
1338
- *
1339
- * @param admissionKind the type of admission request
1340
- * @returns the request handler
1341
- */
1342
- #admissionReq = (admissionKind) => {
1343
- return async (req, res) => {
1344
- const startTime = MetricsCollector.observeStart();
1345
- try {
1346
- const request = req.body?.request || {};
1347
- const { name: name2, namespace: namespace2, gvk } = {
1348
- name: request?.name ? `/${request.name}` : "",
1349
- namespace: request?.namespace || "",
1350
- gvk: request?.kind || { group: "", version: "", kind: "" }
1351
- };
1352
- const reqMetadata = { uid: request.uid, namespace: namespace2, name: name2 };
1353
- logger_default.info({ ...reqMetadata, gvk, operation: request.operation, admissionKind }, "Incoming request");
1354
- logger_default.debug({ ...reqMetadata, request }, "Incoming request body");
1355
- if (typeof this.#beforeHook === "function") {
1356
- this.#beforeHook(request || {});
1357
- }
1358
- const response = admissionKind === "Mutate" ? await mutateProcessor(this.#config, this.#capabilities, request, reqMetadata) : await validateProcessor(this.#config, this.#capabilities, request, reqMetadata);
1359
- [response].flat().map((res2) => {
1360
- if (typeof this.#afterHook === "function") {
1361
- this.#afterHook(res2);
1362
- }
1363
- logger_default.info({ ...reqMetadata, res: res2 }, "Check response");
1364
- });
1365
- const kar = admissionKind === "Mutate" ? karForMutate(response) : karForValidate(request, response);
1366
- logger_default.debug({ ...reqMetadata, kubeAdmissionResponse: kar.response }, "Outgoing response");
1367
- res.send(kar);
1368
- this.#metricsCollector.observeEnd(startTime, admissionKind);
1369
- } catch (err) {
1370
- logger_default.error(err, `Error processing ${admissionKind} request`);
1371
- res.status(500).send("Internal Server Error");
1372
- this.#metricsCollector.error();
1373
- }
1374
- };
1256
+ HasLabel = (key) => {
1257
+ return this.Raw.metadata?.labels?.[key] !== void 0;
1375
1258
  };
1376
- /**
1377
- * Middleware for logging requests
1378
- *
1379
- * @param req the incoming request
1380
- * @param res the outgoing response
1381
- * @param next the next middleware function
1382
- */
1383
- static #logger(req, res, next) {
1384
- const startTime = Date.now();
1385
- res.on("finish", () => {
1386
- const excludedRoutes = ["/healthz", "/metrics"];
1387
- if (excludedRoutes.includes(req.originalUrl)) {
1388
- return;
1259
+ HasAnnotation = (key) => {
1260
+ return this.Raw.metadata?.annotations?.[key] !== void 0;
1261
+ };
1262
+ };
1263
+
1264
+ // src/lib/utils.ts
1265
+ var utils_exports = {};
1266
+ __export(utils_exports, {
1267
+ base64Decode: () => base64Decode,
1268
+ base64Encode: () => base64Encode,
1269
+ convertFromBase64Map: () => convertFromBase64Map,
1270
+ convertToBase64Map: () => convertToBase64Map,
1271
+ isAscii: () => isAscii
1272
+ });
1273
+ var isAscii = /^[\s\x20-\x7E]*$/;
1274
+ function convertToBase64Map(obj, skip) {
1275
+ obj.data = obj.data ?? {};
1276
+ for (const key in obj.data) {
1277
+ const value = obj.data[key];
1278
+ obj.data[key] = skip.includes(key) ? value : base64Encode(value);
1279
+ }
1280
+ }
1281
+ function convertFromBase64Map(obj) {
1282
+ const skip = [];
1283
+ obj.data = obj.data ?? {};
1284
+ for (const key in obj.data) {
1285
+ if (obj.data[key] === void 0) {
1286
+ obj.data[key] = "";
1287
+ } else {
1288
+ const decoded = base64Decode(obj.data[key]);
1289
+ if (isAscii.test(decoded)) {
1290
+ obj.data[key] = decoded;
1291
+ } else {
1292
+ skip.push(key);
1389
1293
  }
1390
- const elapsedTime = Date.now() - startTime;
1391
- const message = {
1392
- uid: req.body?.request?.uid,
1393
- method: req.method,
1394
- url: req.originalUrl,
1395
- status: res.statusCode,
1396
- duration: `${elapsedTime} ms`
1397
- };
1398
- logger_default.info(message);
1399
- });
1400
- next();
1294
+ }
1401
1295
  }
1402
- /**
1403
- * Health check endpoint handler
1404
- *
1405
- * @param req the incoming request
1406
- * @param res the outgoing response
1407
- */
1408
- static #healthz(req, res) {
1409
- try {
1410
- res.send("OK");
1411
- } catch (err) {
1412
- logger_default.error(err, `Error processing health check`);
1413
- res.status(500).send("Internal Server Error");
1296
+ logger_default.debug(`Non-ascii data detected in keys: ${skip}, skipping automatic base64 decoding`);
1297
+ return skip;
1298
+ }
1299
+ function base64Decode(data) {
1300
+ return Buffer.from(data, "base64").toString("utf-8");
1301
+ }
1302
+ function base64Encode(data) {
1303
+ return Buffer.from(data).toString("base64");
1304
+ }
1305
+
1306
+ // src/cli/init/enums.ts
1307
+ var OnError = /* @__PURE__ */ ((OnError2) => {
1308
+ OnError2["AUDIT"] = "audit";
1309
+ OnError2["IGNORE"] = "ignore";
1310
+ OnError2["REJECT"] = "reject";
1311
+ return OnError2;
1312
+ })(OnError || {});
1313
+
1314
+ // src/lib/assets/webhooks.ts
1315
+ var import_ramda5 = require("ramda");
1316
+ function resolveIgnoreNamespaces(ignoredNSConfig = []) {
1317
+ const ignoredNSEnv = process.env.PEPR_ADDITIONAL_IGNORED_NAMESPACES;
1318
+ if (!ignoredNSEnv) {
1319
+ return ignoredNSConfig;
1320
+ }
1321
+ const namespaces = ignoredNSEnv.split(",").map((ns) => ns.trim());
1322
+ if (ignoredNSConfig) {
1323
+ namespaces.push(...ignoredNSConfig);
1324
+ }
1325
+ return namespaces.filter((ns) => ns.length > 0);
1326
+ }
1327
+
1328
+ // src/lib/processors/mutate-processor.ts
1329
+ function updateStatus(config, name2, wrapped, status) {
1330
+ if (wrapped.Request.operation === "DELETE") {
1331
+ return wrapped;
1332
+ }
1333
+ wrapped.SetAnnotation(`${config.uuid}.pepr.dev/${name2}`, status);
1334
+ return wrapped;
1335
+ }
1336
+ function logMutateErrorMessage(e) {
1337
+ try {
1338
+ if (e.message && e.message !== "[object Object]") {
1339
+ return e.message;
1340
+ } else {
1341
+ throw new Error("An error occurred in the mutate action.");
1342
+ }
1343
+ } catch {
1344
+ return "An error occurred with the mutate action.";
1345
+ }
1346
+ }
1347
+ function decodeData(wrapped) {
1348
+ let skipped = [];
1349
+ const isSecret = wrapped.Request.kind.version === "v1" && wrapped.Request.kind.kind === "Secret";
1350
+ if (isSecret) {
1351
+ skipped = convertFromBase64Map(wrapped.Raw);
1352
+ }
1353
+ return { skipped, wrapped };
1354
+ }
1355
+ function reencodeData(wrapped, skipped) {
1356
+ const transformed = (0, import_ramda6.clone)(wrapped.Raw);
1357
+ const isSecret = wrapped.Request.kind.version === "v1" && wrapped.Request.kind.kind === "Secret";
1358
+ if (isSecret) {
1359
+ convertToBase64Map(transformed, skipped);
1360
+ }
1361
+ return transformed;
1362
+ }
1363
+ async function processRequest(bindable, wrapped, response) {
1364
+ const { binding, actMeta, name: name2, config } = bindable;
1365
+ const label = binding.mutateCallback.name;
1366
+ logger_default.info(actMeta, `Processing mutation action (${label})`);
1367
+ wrapped = updateStatus(config, name2, wrapped, "started");
1368
+ try {
1369
+ await binding.mutateCallback(wrapped);
1370
+ logger_default.info(actMeta, `Mutation action succeeded (${label})`);
1371
+ wrapped = updateStatus(config, name2, wrapped, "succeeded");
1372
+ } catch (e) {
1373
+ wrapped = updateStatus(config, name2, wrapped, "warning");
1374
+ response.warnings = response.warnings || [];
1375
+ const errorMessage = logMutateErrorMessage(e);
1376
+ logger_default.error(actMeta, `Action failed: ${errorMessage}`);
1377
+ response.warnings.push(`Action failed: ${errorMessage}`);
1378
+ switch (config.onError) {
1379
+ case "reject" /* REJECT */:
1380
+ response.result = "Pepr module configured to reject on error";
1381
+ break;
1382
+ case "audit" /* AUDIT */:
1383
+ response.auditAnnotations = response.auditAnnotations || {};
1384
+ response.auditAnnotations[Date.now()] = `Action failed: ${errorMessage}`;
1385
+ break;
1386
+ }
1387
+ }
1388
+ return { wrapped, response };
1389
+ }
1390
+ async function mutateProcessor(config, capabilities, req, reqMetadata) {
1391
+ const webhookTimer = new MeasureWebhookTimeout("mutate" /* MUTATE */);
1392
+ webhookTimer.start(config.webhookTimeout);
1393
+ let response = {
1394
+ uid: req.uid,
1395
+ warnings: [],
1396
+ allowed: false
1397
+ };
1398
+ const decoded = decodeData(new PeprMutateRequest(req));
1399
+ let wrapped = decoded.wrapped;
1400
+ logger_default.info(reqMetadata, `Processing request`);
1401
+ let bindables = capabilities.flatMap(
1402
+ (capa) => capa.bindings.map((bind) => ({
1403
+ req,
1404
+ config,
1405
+ name: capa.name,
1406
+ namespaces: capa.namespaces,
1407
+ binding: bind,
1408
+ actMeta: { ...reqMetadata, name: capa.name }
1409
+ }))
1410
+ );
1411
+ bindables = bindables.filter((bind) => {
1412
+ if (!bind.binding.mutateCallback) {
1413
+ return false;
1414
+ }
1415
+ const shouldSkip = shouldSkipRequest(
1416
+ bind.binding,
1417
+ bind.req,
1418
+ bind.namespaces,
1419
+ resolveIgnoreNamespaces(bind.config?.alwaysIgnore?.namespaces)
1420
+ );
1421
+ if (shouldSkip !== "") {
1422
+ logger_default.debug(shouldSkip);
1423
+ return false;
1424
+ }
1425
+ return true;
1426
+ });
1427
+ for (const bindable of bindables) {
1428
+ ({ wrapped, response } = await processRequest(bindable, wrapped, response));
1429
+ if (config.onError === "reject" /* REJECT */ && response?.warnings.length > 0) {
1430
+ return response;
1414
1431
  }
1415
1432
  }
1416
- };
1417
-
1418
- // src/lib/errors.ts
1419
- var ErrorList = Object.values(OnError);
1420
- function ValidateError(error = "") {
1421
- if (!ErrorList.includes(error)) {
1422
- throw new Error(`Invalid error: ${error}. Must be one of: ${ErrorList.join(", ")}`);
1433
+ response.allowed = true;
1434
+ if (bindables.length === 0) {
1435
+ logger_default.info(reqMetadata, `No matching actions found`);
1436
+ return response;
1437
+ }
1438
+ if (req.operation === "DELETE") {
1439
+ return response;
1423
1440
  }
1441
+ const transformed = reencodeData(wrapped, decoded.skipped);
1442
+ const patches = import_fast_json_patch.default.compare(req.object, transformed);
1443
+ updateResponsePatchAndWarnings(patches, response);
1444
+ logger_default.debug({ ...reqMetadata, patches }, `Patches generated`);
1445
+ webhookTimer.stop();
1446
+ return response;
1424
1447
  }
1425
-
1426
- // src/lib/processors/watch-processor.ts
1427
- var import_kubernetes_fluent_client5 = require("kubernetes-fluent-client");
1428
-
1429
- // src/lib/core/queue.ts
1430
- var import_node_crypto = require("node:crypto");
1431
- var Queue = class {
1432
- #name;
1433
- #uid;
1434
- #queue = [];
1435
- #pendingPromise = false;
1436
- constructor(name2) {
1437
- this.#name = name2;
1438
- this.#uid = `${Date.now()}-${(0, import_node_crypto.randomBytes)(2).toString("hex")}`;
1448
+ function updateResponsePatchAndWarnings(patches, response) {
1449
+ if (patches.length > 0) {
1450
+ response.patchType = "JSONPatch";
1451
+ response.patch = base64Encode(JSON.stringify(patches));
1439
1452
  }
1440
- label() {
1441
- return { name: this.#name, uid: this.#uid };
1453
+ if (response.warnings && response.warnings.length < 1) {
1454
+ delete response.warnings;
1442
1455
  }
1443
- stats() {
1444
- return {
1445
- queue: this.label(),
1446
- stats: {
1447
- length: this.#queue.length
1448
- }
1449
- };
1456
+ }
1457
+
1458
+ // src/lib/validate-request.ts
1459
+ var import_ramda7 = require("ramda");
1460
+ var PeprValidateRequest = class {
1461
+ Raw;
1462
+ #input;
1463
+ /**
1464
+ * Provides access to the old resource in the request if available.
1465
+ * @returns The old Kubernetes resource object or null if not available.
1466
+ */
1467
+ get OldResource() {
1468
+ return this.#input.oldObject;
1450
1469
  }
1451
1470
  /**
1452
- * Enqueue adds an item to the queue and returns a promise that resolves when the item is
1453
- * reconciled.
1454
- *
1455
- * @param item The object to reconcile
1456
- * @param type The watch phase requested for reconcile
1457
- * @param reconcile The callback to enqueue for reconcile
1458
- * @returns A promise that resolves when the object is reconciled
1471
+ * Provides access to the request object.
1472
+ * @returns The request object containing the Kubernetes resource.
1459
1473
  */
1460
- enqueue(item, phase, reconcile) {
1461
- const note = {
1462
- queue: this.label(),
1463
- item: {
1464
- name: item.metadata?.name,
1465
- namespace: item.metadata?.namespace,
1466
- resourceVersion: item.metadata?.resourceVersion
1467
- }
1468
- };
1469
- logger_default.debug(note, "Enqueueing");
1470
- return new Promise((resolve, reject) => {
1471
- this.#queue.push({ item, phase, callback: reconcile, resolve, reject });
1472
- logger_default.debug(this.stats(), "Queue stats - push");
1473
- return this.#dequeue();
1474
- });
1474
+ get Request() {
1475
+ return this.#input;
1475
1476
  }
1476
1477
  /**
1477
- * Dequeue reconciles the next item in the queue
1478
- *
1479
- * @returns A promise that resolves when the webapp is reconciled
1478
+ * Creates a new instance of the Action class.
1479
+ * @param input - The request object containing the Kubernetes resource to modify.
1480
1480
  */
1481
- async #dequeue() {
1482
- if (this.#pendingPromise) {
1483
- logger_default.debug("Pending promise, not dequeuing");
1484
- return false;
1485
- }
1486
- const element = this.#queue.shift();
1487
- if (!element) {
1488
- logger_default.debug("No element, not dequeuing");
1489
- return false;
1481
+ constructor(input) {
1482
+ this.#input = input;
1483
+ if (input.operation.toUpperCase() === "DELETE" /* DELETE */) {
1484
+ this.Raw = (0, import_ramda7.clone)(input.oldObject);
1485
+ } else {
1486
+ this.Raw = (0, import_ramda7.clone)(input.object);
1490
1487
  }
1491
- try {
1492
- this.#pendingPromise = true;
1493
- const note = {
1494
- queue: this.label(),
1495
- item: {
1496
- name: element.item.metadata?.name,
1497
- namespace: element.item.metadata?.namespace,
1498
- resourceVersion: element.item.metadata?.resourceVersion
1499
- }
1500
- };
1501
- logger_default.debug(note, "Reconciling");
1502
- await element.callback(element.item, element.phase);
1503
- logger_default.debug(note, "Reconciled");
1504
- element.resolve();
1505
- } catch (e) {
1506
- logger_default.debug(`Error reconciling ${element.item.metadata.name}`, { error: e });
1507
- element.reject(e);
1508
- } finally {
1509
- logger_default.debug(this.stats(), "Queue stats - shift");
1510
- logger_default.debug("Resetting pending promise and dequeuing");
1511
- this.#pendingPromise = false;
1512
- await this.#dequeue();
1488
+ if (!this.Raw) {
1489
+ throw new Error("unable to load the request object into PeprRequest.Raw");
1513
1490
  }
1514
1491
  }
1492
+ /**
1493
+ * Check if a label exists on the Kubernetes resource.
1494
+ *
1495
+ * @param key the label key to check
1496
+ * @returns
1497
+ */
1498
+ HasLabel = (key) => {
1499
+ return this.Raw.metadata?.labels?.[key] !== void 0;
1500
+ };
1501
+ /**
1502
+ * Check if an annotation exists on the Kubernetes resource.
1503
+ *
1504
+ * @param key the annotation key to check
1505
+ * @returns
1506
+ */
1507
+ HasAnnotation = (key) => {
1508
+ return this.Raw.metadata?.annotations?.[key] !== void 0;
1509
+ };
1510
+ /**
1511
+ * Create a validation response that allows the request.
1512
+ *
1513
+ * @returns The validation response.
1514
+ */
1515
+ Approve = () => {
1516
+ return {
1517
+ allowed: true
1518
+ };
1519
+ };
1520
+ /**
1521
+ * Create a validation response that denies the request.
1522
+ *
1523
+ * @param statusMessage Optional status message to return to the user.
1524
+ * @param statusCode Optional status code to return to the user.
1525
+ * @returns The validation response.
1526
+ */
1527
+ Deny = (statusMessage, statusCode) => {
1528
+ return {
1529
+ allowed: false,
1530
+ statusCode,
1531
+ statusMessage
1532
+ };
1533
+ };
1515
1534
  };
1516
1535
 
1517
- // src/lib/processors/watch-processor.ts
1518
- var import_types = require("kubernetes-fluent-client/dist/fluent/types");
1519
-
1520
- // src/lib/finalizer.ts
1521
- var import_kubernetes_fluent_client4 = require("kubernetes-fluent-client");
1522
- function addFinalizer(request) {
1523
- if (request.Request.operation === "DELETE" /* DELETE */) {
1524
- return;
1525
- }
1526
- if (request.Request.operation === "UPDATE" /* UPDATE */ && request.Raw.metadata?.deletionTimestamp) {
1527
- return;
1528
- }
1529
- const peprFinal = "pepr.dev/finalizer";
1530
- const finalizers = request.Raw.metadata?.finalizers || [];
1531
- if (!finalizers.includes(peprFinal)) {
1532
- finalizers.push(peprFinal);
1533
- }
1534
- request.Merge({ metadata: { finalizers } });
1535
- }
1536
- async function removeFinalizer(binding, obj) {
1537
- const peprFinal = "pepr.dev/finalizer";
1538
- const meta = obj.metadata;
1539
- const resource = `${meta.namespace || "ClusterScoped"}/${meta.name}`;
1540
- logger_default.debug({ obj }, `Removing finalizer '${peprFinal}' from '${resource}'`);
1541
- const { model, kind: kind3 } = binding;
1536
+ // src/lib/processors/validate-processor.ts
1537
+ async function processRequest2(binding, actionMetadata, peprValidateRequest) {
1538
+ const label = binding.validateCallback.name;
1539
+ logger_default.info(actionMetadata, `Processing validation action (${label})`);
1540
+ const valResp = {
1541
+ uid: peprValidateRequest.Request.uid,
1542
+ allowed: true
1543
+ // Assume it's allowed until a validation check fails
1544
+ };
1542
1545
  try {
1543
- (0, import_kubernetes_fluent_client4.RegisterKind)(model, kind3);
1546
+ const callbackResp = await binding.validateCallback(peprValidateRequest);
1547
+ valResp.allowed = callbackResp.allowed;
1548
+ if (callbackResp.statusCode || callbackResp.statusMessage) {
1549
+ valResp.status = {
1550
+ code: callbackResp.statusCode || 400,
1551
+ message: callbackResp.statusMessage || `Validation failed for ${name}`
1552
+ };
1553
+ }
1554
+ logger_default.info(actionMetadata, `Validation action complete (${label}): ${callbackResp.allowed ? "allowed" : "denied"}`);
1555
+ return valResp;
1544
1556
  } catch (e) {
1545
- const expected = e.message === `GVK ${model.name} already registered`;
1546
- if (!expected) {
1547
- logger_default.error({ model, kind: kind3, error: e }, `Error registering "${kind3}" during finalization.`);
1548
- return;
1557
+ logger_default.error(actionMetadata, `Action failed: ${JSON.stringify(e)}`);
1558
+ valResp.allowed = false;
1559
+ valResp.status = {
1560
+ code: 500,
1561
+ message: `Action failed with error: ${JSON.stringify(e)}`
1562
+ };
1563
+ return valResp;
1564
+ }
1565
+ }
1566
+ async function validateProcessor(config, capabilities, req, reqMetadata) {
1567
+ const webhookTimer = new MeasureWebhookTimeout("validate" /* VALIDATE */);
1568
+ webhookTimer.start(config.webhookTimeout);
1569
+ const wrapped = new PeprValidateRequest(req);
1570
+ const response = [];
1571
+ if (req.kind.version === "v1" && req.kind.kind === "Secret") {
1572
+ convertFromBase64Map(wrapped.Raw);
1573
+ }
1574
+ logger_default.info(reqMetadata, `Processing validation request`);
1575
+ for (const { name: name2, bindings, namespaces } of capabilities) {
1576
+ const actionMetadata = { ...reqMetadata, name: name2 };
1577
+ for (const binding of bindings) {
1578
+ if (!binding.validateCallback) {
1579
+ continue;
1580
+ }
1581
+ const shouldSkip = shouldSkipRequest(
1582
+ binding,
1583
+ req,
1584
+ namespaces,
1585
+ resolveIgnoreNamespaces(config?.alwaysIgnore?.namespaces)
1586
+ );
1587
+ if (shouldSkip !== "") {
1588
+ logger_default.debug(shouldSkip);
1589
+ continue;
1590
+ }
1591
+ const resp = await processRequest2(binding, actionMetadata, wrapped);
1592
+ response.push(resp);
1549
1593
  }
1550
1594
  }
1551
- const finalizers = meta.finalizers?.filter((f) => f !== peprFinal) || [];
1552
- obj = await (0, import_kubernetes_fluent_client4.K8s)(model, meta).Patch([
1553
- {
1554
- op: "replace",
1555
- path: `/metadata/finalizers`,
1556
- value: finalizers
1557
- }
1558
- ]);
1559
- logger_default.debug({ obj }, `Removed finalizer '${peprFinal}' from '${resource}'`);
1595
+ webhookTimer.stop();
1596
+ return response;
1560
1597
  }
1561
1598
 
1562
- // src/lib/processors/watch-processor.ts
1563
- var queues = {};
1564
- function queueKey(obj) {
1565
- const options = ["kind", "kindNs", "kindNsName", "global"];
1566
- const d3fault = "kind";
1567
- let strat = process.env.PEPR_RECONCILE_STRATEGY || d3fault;
1568
- strat = options.includes(strat) ? strat : d3fault;
1569
- const ns = obj.metadata?.namespace ?? "cluster-scoped";
1570
- const kind3 = obj.kind ?? "UnknownKind";
1571
- const name2 = obj.metadata?.name ?? "Unnamed";
1572
- const lookup = {
1573
- kind: `${kind3}`,
1574
- kindNs: `${kind3}/${ns}`,
1575
- kindNsName: `${kind3}/${ns}/${name2}`,
1576
- global: "global"
1577
- };
1578
- return lookup[strat];
1579
- }
1580
- function getOrCreateQueue(obj) {
1581
- const key = queueKey(obj);
1582
- if (!queues[key]) {
1583
- queues[key] = new Queue(key);
1584
- }
1585
- return queues[key];
1586
- }
1587
- var watchCfg = {
1588
- resyncFailureMax: process.env.PEPR_RESYNC_FAILURE_MAX ? parseInt(process.env.PEPR_RESYNC_FAILURE_MAX, 10) : 5,
1589
- resyncDelaySec: process.env.PEPR_RESYNC_DELAY_SECONDS ? parseInt(process.env.PEPR_RESYNC_DELAY_SECONDS, 10) : 5,
1590
- lastSeenLimitSeconds: process.env.PEPR_LAST_SEEN_LIMIT_SECONDS ? parseInt(process.env.PEPR_LAST_SEEN_LIMIT_SECONDS, 10) : 300,
1591
- relistIntervalSec: process.env.PEPR_RELIST_INTERVAL_SECONDS ? parseInt(process.env.PEPR_RELIST_INTERVAL_SECONDS, 10) : 600
1599
+ // src/lib/controller/store.ts
1600
+ var import_kubernetes_fluent_client5 = require("kubernetes-fluent-client");
1601
+ var import_ramda8 = require("ramda");
1602
+
1603
+ // src/lib/k8s.ts
1604
+ var import_kubernetes_fluent_client3 = require("kubernetes-fluent-client");
1605
+ var Store = class extends import_kubernetes_fluent_client3.GenericKind {
1592
1606
  };
1593
- var eventToPhaseMap = {
1594
- ["CREATE" /* CREATE */]: [import_types.WatchPhase.Added],
1595
- ["UPDATE" /* UPDATE */]: [import_types.WatchPhase.Modified],
1596
- ["CREATEORUPDATE" /* CREATE_OR_UPDATE */]: [import_types.WatchPhase.Added, import_types.WatchPhase.Modified],
1597
- ["DELETE" /* DELETE */]: [import_types.WatchPhase.Deleted],
1598
- ["*" /* ANY */]: [import_types.WatchPhase.Added, import_types.WatchPhase.Modified, import_types.WatchPhase.Deleted]
1607
+ var peprStoreGVK = {
1608
+ kind: "PeprStore",
1609
+ version: "v1",
1610
+ group: "pepr.dev"
1599
1611
  };
1600
- function setupWatch(capabilities, ignoredNamespaces) {
1601
- capabilities.map(
1602
- (capability) => capability.bindings.filter((binding) => binding.isWatch).forEach((bindingElement) => runBinding(bindingElement, capability.namespaces, ignoredNamespaces))
1603
- );
1604
- }
1605
- async function runBinding(binding, capabilityNamespaces, ignoredNamespaces) {
1606
- const phaseMatch = eventToPhaseMap[binding.event] || eventToPhaseMap["*" /* ANY */];
1607
- logger_default.debug({ watchCfg }, "Effective WatchConfig");
1608
- const watchCallback = async (kubernetesObject, phase) => {
1609
- if (phaseMatch.includes(phase)) {
1610
- try {
1611
- const filterMatch = filterNoMatchReason(binding, kubernetesObject, capabilityNamespaces, ignoredNamespaces);
1612
- if (filterMatch !== "") {
1613
- logger_default.debug(filterMatch);
1614
- return;
1615
- }
1616
- if (binding.isFinalize) {
1617
- await handleFinalizerRemoval(kubernetesObject);
1618
- } else {
1619
- await binding.watchCallback?.(kubernetesObject, phase);
1620
- }
1621
- } catch (e) {
1622
- logger_default.error(e, "Error executing watch callback");
1623
- }
1624
- }
1625
- };
1626
- const handleFinalizerRemoval = async (kubernetesObject) => {
1627
- if (!kubernetesObject.metadata?.deletionTimestamp) {
1628
- return;
1629
- }
1630
- let shouldRemoveFinalizer = true;
1631
- try {
1632
- shouldRemoveFinalizer = await binding.finalizeCallback?.(kubernetesObject);
1633
- } finally {
1634
- const peprFinal = "pepr.dev/finalizer";
1635
- const meta = kubernetesObject.metadata;
1636
- const resource = `${meta.namespace || "ClusterScoped"}/${meta.name}`;
1637
- if (shouldRemoveFinalizer === false) {
1638
- logger_default.debug({ obj: kubernetesObject }, `Skipping removal of finalizer '${peprFinal}' from '${resource}'`);
1639
- } else {
1640
- await removeFinalizer(binding, kubernetesObject);
1641
- }
1612
+ (0, import_kubernetes_fluent_client3.RegisterKind)(Store, peprStoreGVK);
1613
+
1614
+ // src/lib/controller/storeCache.ts
1615
+ var import_kubernetes_fluent_client4 = require("kubernetes-fluent-client");
1616
+ var import_http_status_codes = require("http-status-codes");
1617
+ var sendUpdatesAndFlushCache = async (cache, namespace2, name2) => {
1618
+ const indexes = Object.keys(cache);
1619
+ const payload = Object.values(cache);
1620
+ try {
1621
+ if (payload.length > 0) {
1622
+ await (0, import_kubernetes_fluent_client4.K8s)(Store, { namespace: namespace2, name: name2 }).Patch(updateCacheID(payload));
1623
+ Object.keys(cache).forEach((key) => delete cache[key]);
1642
1624
  }
1643
- };
1644
- const watcher = (0, import_kubernetes_fluent_client5.K8s)(binding.model, binding.filters).Watch(async (obj, phase) => {
1645
- logger_default.debug(obj, `Watch event ${phase} received`);
1646
- if (binding.isQueue) {
1647
- const queue = getOrCreateQueue(obj);
1648
- await queue.enqueue(obj, phase, watchCallback);
1625
+ } catch (err) {
1626
+ logger_default.error(err, "Pepr store update failure");
1627
+ if (err.status === import_http_status_codes.StatusCodes.UNPROCESSABLE_ENTITY) {
1628
+ Object.keys(cache).forEach((key) => delete cache[key]);
1649
1629
  } else {
1650
- await watchCallback(obj, phase);
1630
+ indexes.forEach((index) => {
1631
+ cache[index] = payload[Number(index)];
1632
+ });
1651
1633
  }
1652
- }, watchCfg);
1653
- watcher.events.on(import_kubernetes_fluent_client5.WatchEvent.GIVE_UP, (err) => {
1654
- logger_default.error(err, "Watch failed after 5 attempts, giving up");
1655
- process.exit(1);
1656
- });
1657
- watcher.events.on(import_kubernetes_fluent_client5.WatchEvent.CONNECT, (url) => logEvent(import_kubernetes_fluent_client5.WatchEvent.CONNECT, url));
1658
- watcher.events.on(import_kubernetes_fluent_client5.WatchEvent.DATA_ERROR, (err) => logEvent(import_kubernetes_fluent_client5.WatchEvent.DATA_ERROR, err.message));
1659
- watcher.events.on(
1660
- import_kubernetes_fluent_client5.WatchEvent.RECONNECT,
1661
- (retryCount) => logEvent(import_kubernetes_fluent_client5.WatchEvent.RECONNECT, `Reconnecting after ${retryCount} attempt${retryCount === 1 ? "" : "s"}`)
1662
- );
1663
- watcher.events.on(import_kubernetes_fluent_client5.WatchEvent.RECONNECT_PENDING, () => logEvent(import_kubernetes_fluent_client5.WatchEvent.RECONNECT_PENDING));
1664
- watcher.events.on(import_kubernetes_fluent_client5.WatchEvent.GIVE_UP, (err) => logEvent(import_kubernetes_fluent_client5.WatchEvent.GIVE_UP, err.message));
1665
- watcher.events.on(import_kubernetes_fluent_client5.WatchEvent.ABORT, (err) => logEvent(import_kubernetes_fluent_client5.WatchEvent.ABORT, err.message));
1666
- watcher.events.on(import_kubernetes_fluent_client5.WatchEvent.OLD_RESOURCE_VERSION, (err) => logEvent(import_kubernetes_fluent_client5.WatchEvent.OLD_RESOURCE_VERSION, err));
1667
- watcher.events.on(import_kubernetes_fluent_client5.WatchEvent.NETWORK_ERROR, (err) => logEvent(import_kubernetes_fluent_client5.WatchEvent.NETWORK_ERROR, err.message));
1668
- watcher.events.on(import_kubernetes_fluent_client5.WatchEvent.LIST_ERROR, (err) => logEvent(import_kubernetes_fluent_client5.WatchEvent.LIST_ERROR, err.message));
1669
- watcher.events.on(import_kubernetes_fluent_client5.WatchEvent.LIST, (list) => logEvent(import_kubernetes_fluent_client5.WatchEvent.LIST, JSON.stringify(list, void 0, 2)));
1670
- watcher.events.on(import_kubernetes_fluent_client5.WatchEvent.CACHE_MISS, (windowName) => {
1671
- metricsCollector.incCacheMiss(windowName);
1672
- });
1673
- watcher.events.on(import_kubernetes_fluent_client5.WatchEvent.INIT_CACHE_MISS, (windowName) => {
1674
- metricsCollector.initCacheMissWindow(windowName);
1675
- });
1676
- watcher.events.on(import_kubernetes_fluent_client5.WatchEvent.INC_RESYNC_FAILURE_COUNT, (retryCount) => {
1677
- metricsCollector.incRetryCount(retryCount);
1678
- });
1679
- try {
1680
- await watcher.start();
1681
- } catch (err) {
1682
- logger_default.error(err, "Error starting watch");
1683
- process.exit(1);
1684
1634
  }
1685
- }
1686
- function logEvent(event, message = "", obj) {
1687
- const logMessage = `Watch event ${event} received${message ? `. ${message}.` : "."}`;
1688
- if (obj) {
1689
- logger_default.debug(obj, logMessage);
1635
+ return cache;
1636
+ };
1637
+ var fillStoreCache = (cache, capabilityName, op, cacheItem) => {
1638
+ const path = [`/data/${capabilityName}`, cacheItem.version, cacheItem.key].filter((str) => str !== "" && str !== void 0).join("-");
1639
+ if (op === "add") {
1640
+ const value = cacheItem.value || "";
1641
+ const cacheIdx = [op, path, value].join(":");
1642
+ cache[cacheIdx] = { op, path, value };
1643
+ } else if (op === "remove") {
1644
+ if (cacheItem.key.length < 1) {
1645
+ throw new Error(`Key is required for REMOVE operation`);
1646
+ }
1647
+ const cacheIndex = [op, path].join(":");
1648
+ cache[cacheIndex] = { op, path };
1690
1649
  } else {
1691
- logger_default.debug(logMessage);
1650
+ throw new Error(`Unsupported operation: ${op}`);
1692
1651
  }
1652
+ return cache;
1653
+ };
1654
+ function updateCacheID(payload) {
1655
+ payload.push({
1656
+ op: "replace",
1657
+ path: "/metadata/labels/pepr.dev-cacheID",
1658
+ value: `${Date.now()}`
1659
+ });
1660
+ return payload;
1693
1661
  }
1694
1662
 
1695
- // src/lib/core/module.ts
1696
- var isWatchMode = () => process.env.PEPR_WATCH_MODE === "true";
1697
- var isBuildMode = () => process.env.PEPR_MODE === "build";
1698
- var isDevMode = () => process.env.PEPR_MODE === "dev";
1699
- var PeprModule = class {
1700
- #controller;
1701
- /**
1702
- * Create a new Pepr runtime
1703
- *
1704
- * @param config The configuration for the Pepr runtime
1705
- * @param capabilities The capabilities to be loaded into the Pepr runtime
1706
- * @param opts Options for the Pepr runtime
1707
- */
1708
- constructor({ description, pepr }, capabilities = [], opts = {}) {
1709
- const config = (0, import_ramda7.clone)(pepr);
1710
- config.description = description;
1711
- ValidateError(config.onError);
1712
- if (isBuildMode()) {
1713
- if (!process.send) {
1714
- throw new Error("process.send is not defined");
1663
+ // src/lib/controller/store.ts
1664
+ var namespace = "pepr-system";
1665
+ var debounceBackoffReceive = 1e3;
1666
+ var debounceBackoffSend = 4e3;
1667
+ var StoreController = class {
1668
+ #name;
1669
+ #stores = {};
1670
+ #sendDebounce;
1671
+ #onReady;
1672
+ constructor(capabilities, name2, onReady) {
1673
+ this.#onReady = onReady;
1674
+ this.#name = name2;
1675
+ const setStorageInstance = (registrationFunction, name3) => {
1676
+ const scheduleStore = registrationFunction();
1677
+ scheduleStore.registerSender(this.#send(name3));
1678
+ this.#stores[name3] = scheduleStore;
1679
+ };
1680
+ if (name2.includes("schedule")) {
1681
+ for (const { name: name3, registerScheduleStore, hasSchedule } of capabilities) {
1682
+ if (hasSchedule === true) {
1683
+ setStorageInstance(registerScheduleStore, name3);
1684
+ }
1715
1685
  }
1716
- const exportedCapabilities = [];
1717
- for (const capability of capabilities) {
1718
- exportedCapabilities.push({
1719
- name: capability.name,
1720
- description: capability.description,
1721
- namespaces: capability.namespaces,
1722
- bindings: capability.bindings,
1723
- hasSchedule: capability.hasSchedule
1724
- });
1686
+ } else {
1687
+ for (const { name: name3, registerStore } of capabilities) {
1688
+ setStorageInstance(registerStore, name3);
1689
+ }
1690
+ }
1691
+ setTimeout(
1692
+ () => (0, import_kubernetes_fluent_client5.K8s)(Store).InNamespace(namespace).Get(this.#name).then(async (store) => await this.#migrateAndSetupWatch(store)).catch(this.#createStoreResource),
1693
+ Math.random() * 3e3
1694
+ // Add a jitter to the Store creation to avoid collisions
1695
+ );
1696
+ }
1697
+ #setupWatch = () => {
1698
+ const watcher = (0, import_kubernetes_fluent_client5.K8s)(Store, { name: this.#name, namespace }).Watch(this.#receive);
1699
+ watcher.start().catch((e) => logger_default.error(e, "Error starting Pepr store watch"));
1700
+ };
1701
+ #migrateAndSetupWatch = async (store) => {
1702
+ logger_default.debug(redactedStore(store), "Pepr Store migration");
1703
+ await (0, import_kubernetes_fluent_client5.K8s)(Store, { namespace, name: this.#name }).Patch([
1704
+ {
1705
+ op: "add",
1706
+ path: "/metadata/labels/pepr.dev-cacheID",
1707
+ value: `${Date.now()}`
1708
+ }
1709
+ ]);
1710
+ const data = store.data || {};
1711
+ let storeCache = {};
1712
+ for (const name2 of Object.keys(this.#stores)) {
1713
+ const offset = `${name2}-`.length;
1714
+ for (const key of Object.keys(data)) {
1715
+ if ((0, import_ramda8.startsWith)(name2, key) && !(0, import_ramda8.startsWith)(`${name2}-v2`, key)) {
1716
+ storeCache = fillStoreCache(storeCache, name2, "remove", {
1717
+ key: [key.slice(offset)],
1718
+ value: data[key]
1719
+ });
1720
+ storeCache = fillStoreCache(storeCache, name2, "add", {
1721
+ key: [key.slice(offset)],
1722
+ value: data[key],
1723
+ version: "v2"
1724
+ });
1725
+ }
1725
1726
  }
1726
- process.send(exportedCapabilities);
1727
- return;
1728
1727
  }
1729
- const controllerHooks = {
1730
- beforeHook: opts.beforeHook,
1731
- afterHook: opts.afterHook,
1732
- onReady: () => {
1733
- if (isWatchMode() || isDevMode()) {
1734
- try {
1735
- setupWatch(capabilities, resolveIgnoreNamespaces(pepr?.alwaysIgnore?.namespaces));
1736
- } catch (e) {
1737
- logger_default.error(e, "Error setting up watch");
1738
- process.exit(1);
1728
+ storeCache = await sendUpdatesAndFlushCache(storeCache, namespace, this.#name);
1729
+ this.#setupWatch();
1730
+ };
1731
+ #receive = (store) => {
1732
+ logger_default.debug(redactedStore(store), "Pepr Store update");
1733
+ const debounced = () => {
1734
+ const data = store.data || {};
1735
+ for (const name2 of Object.keys(this.#stores)) {
1736
+ const offset = `${name2}-`.length;
1737
+ const filtered = {};
1738
+ for (const key of Object.keys(data)) {
1739
+ if ((0, import_ramda8.startsWith)(name2, key)) {
1740
+ filtered[key.slice(offset)] = data[key];
1739
1741
  }
1740
1742
  }
1743
+ this.#stores[name2].receive(filtered);
1744
+ }
1745
+ if (this.#onReady) {
1746
+ this.#onReady();
1747
+ this.#onReady = void 0;
1741
1748
  }
1742
1749
  };
1743
- this.#controller = new Controller(config, capabilities, controllerHooks);
1744
- if (opts.deferStart) {
1745
- return;
1746
- }
1747
- this.start();
1748
- }
1749
- /**
1750
- * Start the Pepr runtime manually.
1751
- * 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.
1752
- *
1753
- * @param port
1754
- */
1755
- start = (port = 3e3) => {
1756
- this.#controller.startServer(port);
1750
+ clearTimeout(this.#sendDebounce);
1751
+ this.#sendDebounce = setTimeout(debounced, this.#onReady ? 0 : debounceBackoffReceive);
1757
1752
  };
1758
- };
1759
-
1760
- // src/lib/core/storage.ts
1761
- var import_ramda8 = require("ramda");
1762
- var import_json_pointer = __toESM(require("json-pointer"));
1763
- var MAX_WAIT_TIME = 15e3;
1764
- var STORE_VERSION_PREFIX = "v2";
1765
- function v2StoreKey(key) {
1766
- return `${STORE_VERSION_PREFIX}-${import_json_pointer.default.escape(key)}`;
1767
- }
1768
- function v2UnescapedStoreKey(key) {
1769
- return `${STORE_VERSION_PREFIX}-${key}`;
1770
- }
1771
- var Storage = class {
1772
- #store = {};
1773
- #send;
1774
- #subscribers = {};
1775
- #subscriberId = 0;
1776
- #readyHandlers = [];
1777
- registerSender = (send) => {
1778
- this.#send = send;
1753
+ #send = (capabilityName) => {
1754
+ let storeCache = {};
1755
+ const sender = async (op, key, value) => {
1756
+ storeCache = fillStoreCache(storeCache, capabilityName, op, { key, value });
1757
+ };
1758
+ setInterval(() => {
1759
+ if (Object.keys(storeCache).length > 0) {
1760
+ logger_default.debug(redactedPatch(storeCache), "Sending updates to Pepr store");
1761
+ void sendUpdatesAndFlushCache(storeCache, namespace, this.#name);
1762
+ }
1763
+ }, debounceBackoffSend);
1764
+ return sender;
1779
1765
  };
1780
- receive = (data) => {
1781
- this.#store = data || {};
1782
- this.#onReady();
1783
- for (const idx in this.#subscribers) {
1784
- this.#subscribers[idx]((0, import_ramda8.clone)(this.#store));
1766
+ #createStoreResource = async (e) => {
1767
+ logger_default.info(`Pepr store not found, creating...`);
1768
+ logger_default.debug(e);
1769
+ try {
1770
+ await (0, import_kubernetes_fluent_client5.K8s)(Store).Apply({
1771
+ metadata: {
1772
+ name: this.#name,
1773
+ namespace,
1774
+ labels: {
1775
+ "pepr.dev-cacheID": `${Date.now()}`
1776
+ }
1777
+ },
1778
+ data: {
1779
+ // JSON Patch will die if the data is empty, so we need to add a placeholder
1780
+ __pepr_do_not_delete__: "k-thx-bye"
1781
+ }
1782
+ });
1783
+ this.#setupWatch();
1784
+ } catch (err) {
1785
+ logger_default.error(err, "Failed to create Pepr store");
1785
1786
  }
1786
1787
  };
1787
- getItem = (key) => {
1788
- const result = this.#store[v2UnescapedStoreKey(key)] || null;
1789
- if (result !== null && typeof result !== "function" && typeof result !== "object") {
1790
- return result;
1791
- }
1792
- return null;
1788
+ };
1789
+
1790
+ // src/lib/controller/index.util.ts
1791
+ function karForMutate(mr) {
1792
+ return {
1793
+ apiVersion: "admission.k8s.io/v1",
1794
+ kind: "AdmissionReview",
1795
+ response: mr
1793
1796
  };
1794
- clear = () => {
1795
- if (Object.keys(this.#store).length > 0) {
1796
- this.#dispatchUpdate(
1797
- "remove",
1798
- Object.keys(this.#store).map((key) => import_json_pointer.default.escape(key))
1799
- );
1797
+ }
1798
+ function karForValidate(ar, vr) {
1799
+ const isAllowed = vr.filter((r) => !r.allowed).length === 0;
1800
+ const resp = vr.length === 0 ? {
1801
+ uid: ar.uid,
1802
+ allowed: true,
1803
+ status: { code: 200, message: "no in-scope validations -- allowed!" }
1804
+ } : {
1805
+ uid: vr[0].uid,
1806
+ allowed: isAllowed,
1807
+ status: {
1808
+ code: isAllowed ? 200 : 422,
1809
+ message: vr.filter((rl) => !rl.allowed).map((curr) => curr.status?.message).join("; ")
1800
1810
  }
1801
1811
  };
1802
- removeItem = (key) => {
1803
- this.#dispatchUpdate("remove", [v2StoreKey(key)]);
1804
- };
1805
- setItem = (key, value) => {
1806
- this.#dispatchUpdate("add", [v2StoreKey(key)], value);
1812
+ return {
1813
+ apiVersion: "admission.k8s.io/v1",
1814
+ kind: "AdmissionReview",
1815
+ response: resp
1807
1816
  };
1808
- /**
1809
- * Creates a promise and subscribes to the store, the promise resolves when
1810
- * the key and value are seen in the store.
1811
- *
1812
- * @param key - The key to add into the store
1813
- * @param value - The value of the key
1814
- * @returns
1815
- */
1816
- setItemAndWait = (key, value) => {
1817
- this.#dispatchUpdate("add", [v2StoreKey(key)], value);
1818
- const record = {};
1819
- return new Promise((resolve, reject) => {
1820
- record.timeout = setTimeout(() => {
1821
- record.unsubscribe();
1822
- return reject(`MAX_WAIT_TIME elapsed: Key ${key} not seen in ${MAX_WAIT_TIME / 1e3}s`);
1823
- }, MAX_WAIT_TIME);
1824
- record.unsubscribe = this.subscribe((data) => {
1825
- if (data[`${v2UnescapedStoreKey(key)}`] === value) {
1826
- record.unsubscribe();
1827
- clearTimeout(record.timeout);
1828
- resolve("ok");
1829
- }
1817
+ }
1818
+
1819
+ // src/lib/controller/index.ts
1820
+ if (!process.env.PEPR_NODE_WARNINGS) {
1821
+ process.removeAllListeners("warning");
1822
+ }
1823
+ var Controller = class _Controller {
1824
+ // Track whether the server is running
1825
+ #running = false;
1826
+ // Metrics collector
1827
+ #metricsCollector = metricsCollector;
1828
+ // The path used to authenticate requests
1829
+ #path = "";
1830
+ // The express app instance
1831
+ #app = (0, import_express.default)();
1832
+ // Initialized with the constructor
1833
+ #config;
1834
+ #capabilities;
1835
+ #beforeHook;
1836
+ #afterHook;
1837
+ constructor(config, capabilities, hooks = {}) {
1838
+ const { beforeHook, afterHook, onReady } = hooks;
1839
+ this.#config = config;
1840
+ this.#capabilities = capabilities;
1841
+ new StoreController(capabilities, `pepr-${config.uuid}-store`, () => {
1842
+ this.#bindEndpoints();
1843
+ if (typeof onReady === "function") {
1844
+ onReady();
1845
+ }
1846
+ logger_default.info("\u2705 Controller startup complete");
1847
+ new StoreController(capabilities, `pepr-${config.uuid}-schedule`, () => {
1848
+ logger_default.info("\u2705 Scheduling processed");
1830
1849
  });
1831
1850
  });
1832
- };
1833
- /**
1834
- * Creates a promise and subscribes to the store, the promise resolves when
1835
- * the key is removed from the store.
1836
- *
1837
- * @param key - The key to add into the store
1838
- * @returns
1839
- */
1840
- removeItemAndWait = (key) => {
1841
- this.#dispatchUpdate("remove", [v2StoreKey(key)]);
1842
- const record = {};
1843
- return new Promise((resolve, reject) => {
1844
- record.timeout = setTimeout(() => {
1845
- record.unsubscribe();
1846
- return reject(`MAX_WAIT_TIME elapsed: Key ${key} still seen after ${MAX_WAIT_TIME / 1e3}s`);
1847
- }, MAX_WAIT_TIME);
1848
- record.unsubscribe = this.subscribe((data) => {
1849
- if (!Object.hasOwn(data, `${v2UnescapedStoreKey(key)}`)) {
1850
- record.unsubscribe();
1851
- clearTimeout(record.timeout);
1852
- resolve("ok");
1853
- }
1851
+ this.#app.use(_Controller.#logger);
1852
+ this.#app.use(import_express.default.json({ limit: "2mb" }));
1853
+ if (beforeHook) {
1854
+ logger_default.info(`Using beforeHook: ${beforeHook}`);
1855
+ this.#beforeHook = beforeHook;
1856
+ }
1857
+ if (afterHook) {
1858
+ logger_default.info(`Using afterHook: ${afterHook}`);
1859
+ this.#afterHook = afterHook;
1860
+ }
1861
+ }
1862
+ /** Start the webhook server */
1863
+ startServer = (port) => {
1864
+ if (this.#running) {
1865
+ throw new Error("Cannot start Pepr module: Pepr module was not instantiated with deferStart=true");
1866
+ }
1867
+ const options = {
1868
+ key: import_fs.default.readFileSync(process.env.SSL_KEY_PATH || "/etc/certs/tls.key"),
1869
+ cert: import_fs.default.readFileSync(process.env.SSL_CERT_PATH || "/etc/certs/tls.crt")
1870
+ };
1871
+ if (!isWatchMode()) {
1872
+ this.#path = process.env.PEPR_API_PATH || import_fs.default.readFileSync("/app/api-path/value").toString().trim();
1873
+ logger_default.info(`Using API path: ${this.#path}`);
1874
+ if (!this.#path) {
1875
+ throw new Error("API path not found");
1876
+ }
1877
+ }
1878
+ const server = import_https.default.createServer(options, this.#app).listen(port);
1879
+ server.on("listening", () => {
1880
+ logger_default.info(`Server listening on port ${port}`);
1881
+ this.#running = true;
1882
+ });
1883
+ server.on("error", (e) => {
1884
+ if (e.code === "EADDRINUSE") {
1885
+ logger_default.info(
1886
+ `Address in use, retrying in 2 seconds. If this persists, ensure ${port} is not in use, e.g. "lsof -i :${port}"`
1887
+ );
1888
+ setTimeout(() => {
1889
+ server.close();
1890
+ server.listen(port);
1891
+ }, 2e3);
1892
+ }
1893
+ });
1894
+ process.on("SIGTERM", () => {
1895
+ logger_default.info("Received SIGTERM, closing server");
1896
+ server.close(() => {
1897
+ logger_default.info("Server closed");
1898
+ process.exit(0);
1854
1899
  });
1855
1900
  });
1856
1901
  };
1857
- subscribe = (subscriber) => {
1858
- const idx = this.#subscriberId++;
1859
- this.#subscribers[idx] = subscriber;
1860
- return () => this.unsubscribe(idx);
1861
- };
1862
- onReady = (callback) => {
1863
- this.#readyHandlers.push(callback);
1864
- };
1865
- /**
1866
- * Remove a subscriber from the list of subscribers.
1867
- * @param idx - The index of the subscriber to remove.
1868
- */
1869
- unsubscribe = (idx) => {
1870
- delete this.#subscribers[idx];
1871
- };
1872
- #onReady = () => {
1873
- for (const handler of this.#readyHandlers) {
1874
- handler((0, import_ramda8.clone)(this.#store));
1902
+ #bindEndpoints = () => {
1903
+ this.#app.get("/healthz", _Controller.#healthz);
1904
+ this.#app.get("/metrics", this.#metrics);
1905
+ if (isWatchMode()) {
1906
+ return;
1875
1907
  }
1876
- this.#onReady = () => {
1877
- };
1878
- };
1879
- /**
1880
- * Dispatch an update to the store and notify all subscribers.
1881
- * @param op - The type of operation to perform.
1882
- * @param keys - The keys to update.
1883
- * @param [value] - The new value.
1884
- */
1885
- #dispatchUpdate = (op, keys, value) => {
1886
- this.#send(op, keys, value);
1908
+ this.#app.use(["/mutate/:path", "/validate/:path"], this.#validatepath);
1909
+ this.#app.post("/mutate/:path", this.#admissionReq("Mutate"));
1910
+ this.#app.post("/validate/:path", this.#admissionReq("Validate"));
1887
1911
  };
1888
- };
1889
-
1890
- // src/lib/core/schedule.ts
1891
- var OnSchedule = class {
1892
- intervalId = null;
1893
- store;
1894
- name;
1895
- completions;
1896
- every;
1897
- unit;
1898
- run;
1899
- startTime;
1900
- duration;
1901
- lastTimestamp;
1902
- constructor(schedule) {
1903
- this.name = schedule.name;
1904
- this.run = schedule.run;
1905
- this.every = schedule.every;
1906
- this.unit = schedule.unit;
1907
- this.startTime = schedule?.startTime;
1908
- this.completions = schedule?.completions;
1909
- }
1910
- setStore(store) {
1911
- this.store = store;
1912
- this.startInterval();
1913
- }
1914
- startInterval() {
1915
- this.checkStore();
1916
- this.getDuration();
1917
- this.setupInterval();
1918
- }
1919
- /**
1920
- * Checks the store for this schedule and sets the values if it exists
1921
- * @returns
1922
- */
1923
- checkStore() {
1924
- const result = this.store && this.store.getItem(this.name);
1925
- if (result) {
1926
- const storedSchedule = JSON.parse(result);
1927
- this.completions = storedSchedule?.completions;
1928
- this.startTime = storedSchedule?.startTime;
1929
- this.lastTimestamp = storedSchedule?.lastTimestamp;
1930
- }
1931
- }
1932
1912
  /**
1933
- * Saves the schedule to the store
1913
+ * Validate the path in the request path
1914
+ *
1915
+ * @param req The incoming request
1916
+ * @param res The outgoing response
1917
+ * @param next The next middleware function
1934
1918
  * @returns
1935
1919
  */
1936
- saveToStore() {
1937
- const schedule = {
1938
- completions: this.completions,
1939
- startTime: this.startTime,
1940
- lastTimestamp: /* @__PURE__ */ new Date(),
1941
- name: this.name
1942
- };
1943
- if (this.store) this.store.setItem(this.name, JSON.stringify(schedule));
1944
- }
1945
- /**
1946
- * Gets the durations in milliseconds
1947
- */
1948
- getDuration() {
1949
- switch (this.unit) {
1950
- case "seconds":
1951
- if (this.every < 10) throw new Error("10 Seconds in the smallest interval allowed");
1952
- this.duration = 1e3 * this.every;
1953
- break;
1954
- case "minutes":
1955
- case "minute":
1956
- this.duration = 1e3 * 60 * this.every;
1957
- break;
1958
- case "hours":
1959
- case "hour":
1960
- this.duration = 1e3 * 60 * 60 * this.every;
1961
- break;
1962
- default:
1963
- throw new Error("Invalid time unit");
1920
+ #validatepath = (req, res, next) => {
1921
+ const { path } = req.params;
1922
+ if (path !== this.#path) {
1923
+ const err = `Unauthorized: invalid path '${path.replace(/[^\w]/g, "_")}'`;
1924
+ logger_default.info(err);
1925
+ res.status(401).send(err);
1926
+ this.#metricsCollector.alert();
1927
+ return;
1964
1928
  }
1965
- }
1929
+ next();
1930
+ };
1966
1931
  /**
1967
- * Sets up the interval
1932
+ * Metrics endpoint handler
1933
+ *
1934
+ * @param req the incoming request
1935
+ * @param res the outgoing response
1968
1936
  */
1969
- setupInterval() {
1970
- const now = /* @__PURE__ */ new Date();
1971
- let delay;
1972
- if (this.lastTimestamp && this.startTime) {
1973
- this.startTime = void 0;
1974
- }
1975
- if (this.startTime) {
1976
- delay = this.startTime.getTime() - now.getTime();
1977
- } else if (this.lastTimestamp && this.duration) {
1978
- const lastTimestamp = new Date(this.lastTimestamp);
1979
- delay = this.duration - (now.getTime() - lastTimestamp.getTime());
1980
- }
1981
- if (delay === void 0 || delay <= 0) {
1982
- this.start();
1983
- } else {
1984
- setTimeout(() => {
1985
- this.start();
1986
- }, delay);
1937
+ #metrics = async (req, res) => {
1938
+ try {
1939
+ res.set("Content-Type", "text/plain; version=0.0.4");
1940
+ res.send(await this.#metricsCollector.getMetrics());
1941
+ } catch (err) {
1942
+ logger_default.error(err, `Error getting metrics`);
1943
+ res.status(500).send("Internal Server Error");
1987
1944
  }
1988
- }
1945
+ };
1989
1946
  /**
1990
- * Starts the interval
1947
+ * Admission request handler for both mutate and validate requests
1948
+ *
1949
+ * @param admissionKind the type of admission request
1950
+ * @returns the request handler
1991
1951
  */
1992
- start() {
1993
- this.intervalId = setInterval(() => {
1994
- if (this.completions === 0) {
1995
- this.stop();
1996
- return;
1997
- } else {
1998
- this.run();
1999
- if (this.completions && this.completions !== 0) {
2000
- this.completions -= 1;
1952
+ #admissionReq = (admissionKind) => {
1953
+ return async (req, res) => {
1954
+ const startTime = MetricsCollector.observeStart();
1955
+ try {
1956
+ const request = req.body?.request || {};
1957
+ const { name: name2, namespace: namespace2, gvk } = {
1958
+ name: request?.name ? `/${request.name}` : "",
1959
+ namespace: request?.namespace || "",
1960
+ gvk: request?.kind || { group: "", version: "", kind: "" }
1961
+ };
1962
+ const reqMetadata = { uid: request.uid, namespace: namespace2, name: name2 };
1963
+ logger_default.info({ ...reqMetadata, gvk, operation: request.operation, admissionKind }, "Incoming request");
1964
+ logger_default.debug({ ...reqMetadata, request }, "Incoming request body");
1965
+ if (typeof this.#beforeHook === "function") {
1966
+ this.#beforeHook(request || {});
2001
1967
  }
2002
- this.saveToStore();
1968
+ const response = admissionKind === "Mutate" ? await mutateProcessor(this.#config, this.#capabilities, request, reqMetadata) : await validateProcessor(this.#config, this.#capabilities, request, reqMetadata);
1969
+ [response].flat().map((res2) => {
1970
+ if (typeof this.#afterHook === "function") {
1971
+ this.#afterHook(res2);
1972
+ }
1973
+ logger_default.info({ ...reqMetadata, res: res2 }, "Check response");
1974
+ });
1975
+ const kar = admissionKind === "Mutate" ? karForMutate(response) : karForValidate(request, response);
1976
+ logger_default.debug({ ...reqMetadata, kubeAdmissionResponse: kar.response }, "Outgoing response");
1977
+ res.send(kar);
1978
+ this.#metricsCollector.observeEnd(startTime, admissionKind);
1979
+ } catch (err) {
1980
+ logger_default.error(err, `Error processing ${admissionKind} request`);
1981
+ res.status(500).send("Internal Server Error");
1982
+ this.#metricsCollector.error();
2003
1983
  }
2004
- }, this.duration);
2005
- }
2006
- /**
2007
- * Stops the interval
2008
- */
2009
- stop() {
2010
- if (this.intervalId) {
2011
- clearInterval(this.intervalId);
2012
- this.intervalId = null;
2013
- }
2014
- if (this.store) this.store.removeItem(this.name);
2015
- }
2016
- };
2017
-
2018
- // src/lib/core/capability.ts
2019
- var registerAdmission = isBuildMode() || !isWatchMode();
2020
- var registerWatch = isBuildMode() || isWatchMode() || isDevMode();
2021
- var Capability = class {
2022
- #name;
2023
- #description;
2024
- #namespaces;
2025
- #bindings = [];
2026
- #store = new Storage();
2027
- #scheduleStore = new Storage();
2028
- #registered = false;
2029
- #scheduleRegistered = false;
2030
- hasSchedule;
2031
- /**
2032
- * Run code on a schedule with the capability.
2033
- *
2034
- * @param schedule The schedule to run the code on
2035
- * @returns
2036
- */
2037
- OnSchedule = (schedule) => {
2038
- const { name: name2, every, unit, run, startTime, completions } = schedule;
2039
- this.hasSchedule = true;
2040
- if (process.env.PEPR_WATCH_MODE === "true" || process.env.PEPR_MODE === "dev") {
2041
- const newSchedule = {
2042
- name: name2,
2043
- every,
2044
- unit,
2045
- run,
2046
- startTime,
2047
- completions
2048
- };
2049
- this.#scheduleStore.onReady(() => {
2050
- new OnSchedule(newSchedule).setStore(this.#scheduleStore);
2051
- });
2052
- }
1984
+ };
2053
1985
  };
2054
- getScheduleStore() {
2055
- return this.#scheduleStore;
2056
- }
2057
1986
  /**
2058
- * Store is a key-value data store that can be used to persist data that should be shared
2059
- * between requests. Each capability has its own store, and the data is persisted in Kubernetes
2060
- * in the `pepr-system` namespace.
1987
+ * Middleware for logging requests
2061
1988
  *
2062
- * Note: You should only access the store from within an action.
1989
+ * @param req the incoming request
1990
+ * @param res the outgoing response
1991
+ * @param next the next middleware function
2063
1992
  */
2064
- Store = {
2065
- clear: this.#store.clear,
2066
- getItem: this.#store.getItem,
2067
- removeItem: this.#store.removeItem,
2068
- removeItemAndWait: this.#store.removeItemAndWait,
2069
- setItem: this.#store.setItem,
2070
- subscribe: this.#store.subscribe,
2071
- onReady: this.#store.onReady,
2072
- setItemAndWait: this.#store.setItemAndWait
2073
- };
1993
+ static #logger(req, res, next) {
1994
+ const startTime = Date.now();
1995
+ res.on("finish", () => {
1996
+ const excludedRoutes = ["/healthz", "/metrics"];
1997
+ if (excludedRoutes.includes(req.originalUrl)) {
1998
+ return;
1999
+ }
2000
+ const elapsedTime = Date.now() - startTime;
2001
+ const message = {
2002
+ uid: req.body?.request?.uid,
2003
+ method: req.method,
2004
+ url: req.originalUrl,
2005
+ status: res.statusCode,
2006
+ duration: `${elapsedTime} ms`
2007
+ };
2008
+ logger_default.info(message);
2009
+ });
2010
+ next();
2011
+ }
2074
2012
  /**
2075
- * ScheduleStore is a key-value data store used to persist schedule data that should be shared
2076
- * between intervals. Each Schedule shares store, and the data is persisted in Kubernetes
2077
- * in the `pepr-system` namespace.
2013
+ * Health check endpoint handler
2078
2014
  *
2079
- * Note: There is no direct access to schedule store
2015
+ * @param req the incoming request
2016
+ * @param res the outgoing response
2080
2017
  */
2081
- ScheduleStore = {
2082
- clear: this.#scheduleStore.clear,
2083
- getItem: this.#scheduleStore.getItem,
2084
- removeItemAndWait: this.#scheduleStore.removeItemAndWait,
2085
- removeItem: this.#scheduleStore.removeItem,
2086
- setItemAndWait: this.#scheduleStore.setItemAndWait,
2087
- setItem: this.#scheduleStore.setItem,
2088
- subscribe: this.#scheduleStore.subscribe,
2089
- onReady: this.#scheduleStore.onReady
2090
- };
2091
- get bindings() {
2092
- return this.#bindings;
2018
+ static #healthz(req, res) {
2019
+ try {
2020
+ res.send("OK");
2021
+ } catch (err) {
2022
+ logger_default.error(err, `Error processing health check`);
2023
+ res.status(500).send("Internal Server Error");
2024
+ }
2093
2025
  }
2094
- get name() {
2095
- return this.#name;
2026
+ };
2027
+
2028
+ // src/lib/errors.ts
2029
+ var ErrorList = Object.values(OnError);
2030
+ function ValidateError(error = "") {
2031
+ if (!ErrorList.includes(error)) {
2032
+ throw new Error(`Invalid error: ${error}. Must be one of: ${ErrorList.join(", ")}`);
2096
2033
  }
2097
- get description() {
2098
- return this.#description;
2034
+ }
2035
+
2036
+ // src/lib/processors/watch-processor.ts
2037
+ var import_kubernetes_fluent_client6 = require("kubernetes-fluent-client");
2038
+
2039
+ // src/lib/core/queue.ts
2040
+ var import_node_crypto = require("node:crypto");
2041
+ var Queue = class {
2042
+ #name;
2043
+ #uid;
2044
+ #queue = [];
2045
+ #pendingPromise = false;
2046
+ constructor(name2) {
2047
+ this.#name = name2;
2048
+ this.#uid = `${Date.now()}-${(0, import_node_crypto.randomBytes)(2).toString("hex")}`;
2099
2049
  }
2100
- get namespaces() {
2101
- return this.#namespaces || [];
2050
+ label() {
2051
+ return { name: this.#name, uid: this.#uid };
2102
2052
  }
2103
- constructor(cfg) {
2104
- this.#name = cfg.name;
2105
- this.#description = cfg.description;
2106
- this.#namespaces = cfg.namespaces;
2107
- this.hasSchedule = false;
2108
- logger_default.info(`Capability ${this.#name} registered`);
2109
- logger_default.debug(cfg);
2053
+ stats() {
2054
+ return {
2055
+ queue: this.label(),
2056
+ stats: {
2057
+ length: this.#queue.length
2058
+ }
2059
+ };
2110
2060
  }
2111
2061
  /**
2112
- * Register the store with the capability. This is called automatically by the Pepr controller.
2062
+ * Enqueue adds an item to the queue and returns a promise that resolves when the item is
2063
+ * reconciled.
2064
+ *
2065
+ * @param item The object to reconcile
2066
+ * @param type The watch phase requested for reconcile
2067
+ * @param reconcile The callback to enqueue for reconcile
2068
+ * @returns A promise that resolves when the object is reconciled
2113
2069
  */
2114
- registerScheduleStore = () => {
2115
- logger_default.info(`Registering schedule store for ${this.#name}`);
2116
- if (this.#scheduleRegistered) {
2117
- throw new Error(`Schedule store already registered for ${this.#name}`);
2118
- }
2119
- this.#scheduleRegistered = true;
2120
- return this.#scheduleStore;
2121
- };
2070
+ enqueue(item, phase, reconcile) {
2071
+ const note = {
2072
+ queue: this.label(),
2073
+ item: {
2074
+ name: item.metadata?.name,
2075
+ namespace: item.metadata?.namespace,
2076
+ resourceVersion: item.metadata?.resourceVersion
2077
+ }
2078
+ };
2079
+ logger_default.debug(note, "Enqueueing");
2080
+ return new Promise((resolve, reject) => {
2081
+ this.#queue.push({ item, phase, callback: reconcile, resolve, reject });
2082
+ logger_default.debug(this.stats(), "Queue stats - push");
2083
+ return this.#dequeue();
2084
+ });
2085
+ }
2122
2086
  /**
2123
- * Register the store with the capability. This is called automatically by the Pepr controller.
2087
+ * Dequeue reconciles the next item in the queue
2124
2088
  *
2125
- * @param store
2089
+ * @returns A promise that resolves when the webapp is reconciled
2126
2090
  */
2127
- registerStore = () => {
2128
- logger_default.info(`Registering store for ${this.#name}`);
2129
- if (this.#registered) {
2130
- throw new Error(`Store already registered for ${this.#name}`);
2091
+ async #dequeue() {
2092
+ if (this.#pendingPromise) {
2093
+ logger_default.debug("Pending promise, not dequeuing");
2094
+ return false;
2095
+ }
2096
+ const element = this.#queue.shift();
2097
+ if (!element) {
2098
+ logger_default.debug("No element, not dequeuing");
2099
+ return false;
2100
+ }
2101
+ try {
2102
+ this.#pendingPromise = true;
2103
+ const note = {
2104
+ queue: this.label(),
2105
+ item: {
2106
+ name: element.item.metadata?.name,
2107
+ namespace: element.item.metadata?.namespace,
2108
+ resourceVersion: element.item.metadata?.resourceVersion
2109
+ }
2110
+ };
2111
+ logger_default.debug(note, "Reconciling");
2112
+ await element.callback(element.item, element.phase);
2113
+ logger_default.debug(note, "Reconciled");
2114
+ element.resolve();
2115
+ } catch (e) {
2116
+ logger_default.debug(`Error reconciling ${element.item.metadata.name}`, { error: e });
2117
+ element.reject(e);
2118
+ } finally {
2119
+ logger_default.debug(this.stats(), "Queue stats - shift");
2120
+ logger_default.debug("Resetting pending promise and dequeuing");
2121
+ this.#pendingPromise = false;
2122
+ await this.#dequeue();
2123
+ }
2124
+ }
2125
+ };
2126
+
2127
+ // src/lib/processors/watch-processor.ts
2128
+ var import_types = require("kubernetes-fluent-client/dist/fluent/types");
2129
+ var queues = {};
2130
+ function queueKey(obj) {
2131
+ const options = ["kind", "kindNs", "kindNsName", "global"];
2132
+ const d3fault = "kind";
2133
+ let strat = process.env.PEPR_RECONCILE_STRATEGY || d3fault;
2134
+ strat = options.includes(strat) ? strat : d3fault;
2135
+ const ns = obj.metadata?.namespace ?? "cluster-scoped";
2136
+ const kind3 = obj.kind ?? "UnknownKind";
2137
+ const name2 = obj.metadata?.name ?? "Unnamed";
2138
+ const lookup = {
2139
+ kind: `${kind3}`,
2140
+ kindNs: `${kind3}/${ns}`,
2141
+ kindNsName: `${kind3}/${ns}/${name2}`,
2142
+ global: "global"
2143
+ };
2144
+ return lookup[strat];
2145
+ }
2146
+ function getOrCreateQueue(obj) {
2147
+ const key = queueKey(obj);
2148
+ if (!queues[key]) {
2149
+ queues[key] = new Queue(key);
2150
+ }
2151
+ return queues[key];
2152
+ }
2153
+ var watchCfg = {
2154
+ resyncFailureMax: process.env.PEPR_RESYNC_FAILURE_MAX ? parseInt(process.env.PEPR_RESYNC_FAILURE_MAX, 10) : 5,
2155
+ resyncDelaySec: process.env.PEPR_RESYNC_DELAY_SECONDS ? parseInt(process.env.PEPR_RESYNC_DELAY_SECONDS, 10) : 5,
2156
+ lastSeenLimitSeconds: process.env.PEPR_LAST_SEEN_LIMIT_SECONDS ? parseInt(process.env.PEPR_LAST_SEEN_LIMIT_SECONDS, 10) : 300,
2157
+ relistIntervalSec: process.env.PEPR_RELIST_INTERVAL_SECONDS ? parseInt(process.env.PEPR_RELIST_INTERVAL_SECONDS, 10) : 600
2158
+ };
2159
+ var eventToPhaseMap = {
2160
+ ["CREATE" /* CREATE */]: [import_types.WatchPhase.Added],
2161
+ ["UPDATE" /* UPDATE */]: [import_types.WatchPhase.Modified],
2162
+ ["CREATEORUPDATE" /* CREATE_OR_UPDATE */]: [import_types.WatchPhase.Added, import_types.WatchPhase.Modified],
2163
+ ["DELETE" /* DELETE */]: [import_types.WatchPhase.Deleted],
2164
+ ["*" /* ANY */]: [import_types.WatchPhase.Added, import_types.WatchPhase.Modified, import_types.WatchPhase.Deleted]
2165
+ };
2166
+ function setupWatch(capabilities, ignoredNamespaces) {
2167
+ capabilities.map(
2168
+ (capability) => capability.bindings.filter((binding) => binding.isWatch).forEach((bindingElement) => runBinding(bindingElement, capability.namespaces, ignoredNamespaces))
2169
+ );
2170
+ }
2171
+ async function runBinding(binding, capabilityNamespaces, ignoredNamespaces) {
2172
+ const phaseMatch = eventToPhaseMap[binding.event] || eventToPhaseMap["*" /* ANY */];
2173
+ logger_default.debug({ watchCfg }, "Effective WatchConfig");
2174
+ const watchCallback = async (kubernetesObject, phase) => {
2175
+ if (phaseMatch.includes(phase)) {
2176
+ try {
2177
+ const filterMatch = filterNoMatchReason(binding, kubernetesObject, capabilityNamespaces, ignoredNamespaces);
2178
+ if (filterMatch !== "") {
2179
+ logger_default.debug(filterMatch);
2180
+ return;
2181
+ }
2182
+ if (binding.isFinalize) {
2183
+ await handleFinalizerRemoval(kubernetesObject);
2184
+ } else {
2185
+ await binding.watchCallback?.(kubernetesObject, phase);
2186
+ }
2187
+ } catch (e) {
2188
+ logger_default.error(e, "Error executing watch callback");
2189
+ }
2190
+ }
2191
+ };
2192
+ const handleFinalizerRemoval = async (kubernetesObject) => {
2193
+ if (!kubernetesObject.metadata?.deletionTimestamp) {
2194
+ return;
2195
+ }
2196
+ let shouldRemoveFinalizer = true;
2197
+ try {
2198
+ shouldRemoveFinalizer = await binding.finalizeCallback?.(kubernetesObject);
2199
+ } finally {
2200
+ const peprFinal = "pepr.dev/finalizer";
2201
+ const meta = kubernetesObject.metadata;
2202
+ const resource = `${meta.namespace || "ClusterScoped"}/${meta.name}`;
2203
+ if (shouldRemoveFinalizer === false) {
2204
+ logger_default.debug({ obj: kubernetesObject }, `Skipping removal of finalizer '${peprFinal}' from '${resource}'`);
2205
+ } else {
2206
+ await removeFinalizer(binding, kubernetesObject);
2207
+ }
2131
2208
  }
2132
- this.#registered = true;
2133
- return this.#store;
2134
2209
  };
2210
+ const watcher = (0, import_kubernetes_fluent_client6.K8s)(binding.model, binding.filters).Watch(async (obj, phase) => {
2211
+ logger_default.debug(obj, `Watch event ${phase} received`);
2212
+ if (binding.isQueue) {
2213
+ const queue = getOrCreateQueue(obj);
2214
+ await queue.enqueue(obj, phase, watchCallback);
2215
+ } else {
2216
+ await watchCallback(obj, phase);
2217
+ }
2218
+ }, watchCfg);
2219
+ registerWatchEventHandlers(watcher, logEvent, metricsCollector);
2220
+ try {
2221
+ await watcher.start();
2222
+ } catch (err) {
2223
+ logger_default.error(err, "Error starting watch");
2224
+ process.exit(1);
2225
+ }
2226
+ }
2227
+ function logEvent(event, message = "", obj) {
2228
+ const logMessage = `Watch event ${event} received${message ? `. ${message}.` : "."}`;
2229
+ if (obj) {
2230
+ logger_default.debug(obj, logMessage);
2231
+ } else {
2232
+ logger_default.debug(logMessage);
2233
+ }
2234
+ }
2235
+ function registerWatchEventHandlers(watcher, logEvent2, metricsCollector2) {
2236
+ const eventHandlers = {
2237
+ [import_kubernetes_fluent_client6.WatchEvent.DATA]: () => null,
2238
+ [import_kubernetes_fluent_client6.WatchEvent.GIVE_UP]: (err) => {
2239
+ logEvent2(import_kubernetes_fluent_client6.WatchEvent.GIVE_UP, err.message);
2240
+ process.exit(1);
2241
+ },
2242
+ [import_kubernetes_fluent_client6.WatchEvent.CONNECT]: (url) => logEvent2(import_kubernetes_fluent_client6.WatchEvent.CONNECT, url),
2243
+ [import_kubernetes_fluent_client6.WatchEvent.DATA_ERROR]: (err) => logEvent2(import_kubernetes_fluent_client6.WatchEvent.DATA_ERROR, err.message),
2244
+ [import_kubernetes_fluent_client6.WatchEvent.RECONNECT]: (retryCount) => logEvent2(import_kubernetes_fluent_client6.WatchEvent.RECONNECT, `Reconnecting after ${retryCount} attempt${retryCount === 1 ? "" : "s"}`),
2245
+ [import_kubernetes_fluent_client6.WatchEvent.RECONNECT_PENDING]: () => logEvent2(import_kubernetes_fluent_client6.WatchEvent.RECONNECT_PENDING),
2246
+ [import_kubernetes_fluent_client6.WatchEvent.ABORT]: (err) => logEvent2(import_kubernetes_fluent_client6.WatchEvent.ABORT, err.message),
2247
+ [import_kubernetes_fluent_client6.WatchEvent.OLD_RESOURCE_VERSION]: (errMessage) => logEvent2(import_kubernetes_fluent_client6.WatchEvent.OLD_RESOURCE_VERSION, errMessage),
2248
+ [import_kubernetes_fluent_client6.WatchEvent.NETWORK_ERROR]: (err) => logEvent2(import_kubernetes_fluent_client6.WatchEvent.NETWORK_ERROR, err.message),
2249
+ [import_kubernetes_fluent_client6.WatchEvent.LIST_ERROR]: (err) => logEvent2(import_kubernetes_fluent_client6.WatchEvent.LIST_ERROR, err.message),
2250
+ [import_kubernetes_fluent_client6.WatchEvent.LIST]: (list) => logEvent2(import_kubernetes_fluent_client6.WatchEvent.LIST, JSON.stringify(list, void 0, 2)),
2251
+ [import_kubernetes_fluent_client6.WatchEvent.CACHE_MISS]: (windowName) => metricsCollector2.incCacheMiss(windowName),
2252
+ [import_kubernetes_fluent_client6.WatchEvent.INIT_CACHE_MISS]: (windowName) => metricsCollector2.initCacheMissWindow(windowName),
2253
+ [import_kubernetes_fluent_client6.WatchEvent.INC_RESYNC_FAILURE_COUNT]: (retryCount) => metricsCollector2.incRetryCount(retryCount)
2254
+ };
2255
+ Object.entries(eventHandlers).forEach(([event, handler]) => {
2256
+ watcher.events.on(event, handler);
2257
+ });
2258
+ }
2259
+
2260
+ // src/lib/core/module.ts
2261
+ var PeprModule = class {
2262
+ #controller;
2135
2263
  /**
2136
- * The When method is used to register a action to be executed when a Kubernetes resource is
2137
- * processed by Pepr. The action will be executed if the resource matches the specified kind and any
2138
- * filters that are applied.
2264
+ * Create a new Pepr runtime
2139
2265
  *
2140
- * @param model the KubernetesObject model to match
2141
- * @param kind if using a custom KubernetesObject not available in `a.*`, specify the GroupVersionKind
2142
- * @returns
2266
+ * @param config The configuration for the Pepr runtime
2267
+ * @param capabilities The capabilities to be loaded into the Pepr runtime
2268
+ * @param opts Options for the Pepr runtime
2143
2269
  */
2144
- When = (model, kind3) => {
2145
- const matchedKind = (0, import_kubernetes_fluent_client6.modelToGroupVersionKind)(model.name);
2146
- if (!matchedKind && !kind3) {
2147
- throw new Error(`Kind not specified for ${model.name}`);
2148
- }
2149
- const binding = {
2150
- model,
2151
- // If the kind is not specified, use the matched kind from the model
2152
- kind: kind3 || matchedKind,
2153
- event: "*" /* ANY */,
2154
- filters: {
2155
- name: "",
2156
- namespaces: [],
2157
- regexNamespaces: [],
2158
- regexName: "",
2159
- labels: {},
2160
- annotations: {},
2161
- deletionTimestamp: false
2162
- }
2163
- };
2164
- const bindings = this.#bindings;
2165
- const prefix = `${this.#name}: ${model.name}`;
2166
- const commonChain = { WithLabel, WithAnnotation, WithDeletionTimestamp, Mutate, Validate, Watch, Reconcile, Alias };
2167
- const isNotEmpty = (value) => Object.keys(value).length > 0;
2168
- const log = (message, cbString) => {
2169
- const filteredObj = (0, import_ramda9.pickBy)(isNotEmpty, binding.filters);
2170
- logger_default.info(`${message} configured for ${binding.event}`, prefix);
2171
- logger_default.info(filteredObj, prefix);
2172
- logger_default.debug(cbString, prefix);
2173
- };
2174
- function Validate(validateCallback) {
2175
- if (registerAdmission) {
2176
- log("Validate Action", validateCallback.toString());
2177
- const aliasLogger = logger_default.child({ alias: binding.alias || "no alias provided" });
2178
- bindings.push({
2179
- ...binding,
2180
- isValidate: true,
2181
- validateCallback: async (req, logger = aliasLogger) => {
2182
- logger_default.info(`Executing validate action with alias: ${binding.alias || "no alias provided"}`);
2183
- return await validateCallback(req, logger);
2184
- }
2185
- });
2186
- }
2187
- return { Watch, Reconcile };
2188
- }
2189
- function Mutate(mutateCallback) {
2190
- if (registerAdmission) {
2191
- log("Mutate Action", mutateCallback.toString());
2192
- const aliasLogger = logger_default.child({ alias: binding.alias || "no alias provided" });
2193
- bindings.push({
2194
- ...binding,
2195
- isMutate: true,
2196
- mutateCallback: async (req, logger = aliasLogger) => {
2197
- logger_default.info(`Executing mutation action with alias: ${binding.alias || "no alias provided"}`);
2198
- await mutateCallback(req, logger);
2199
- }
2200
- });
2201
- }
2202
- return { Watch, Validate, Reconcile };
2203
- }
2204
- function Watch(watchCallback) {
2205
- if (registerWatch) {
2206
- log("Watch Action", watchCallback.toString());
2207
- const aliasLogger = logger_default.child({ alias: binding.alias || "no alias provided" });
2208
- bindings.push({
2209
- ...binding,
2210
- isWatch: true,
2211
- watchCallback: async (update, phase, logger = aliasLogger) => {
2212
- logger_default.info(`Executing watch action with alias: ${binding.alias || "no alias provided"}`);
2213
- await watchCallback(update, phase, logger);
2214
- }
2215
- });
2270
+ constructor({ description, pepr }, capabilities = [], opts = {}) {
2271
+ const config = (0, import_ramda9.clone)(pepr);
2272
+ config.description = description;
2273
+ ValidateError(config.onError);
2274
+ if (isBuildMode()) {
2275
+ if (!process.send) {
2276
+ throw new Error("process.send is not defined");
2216
2277
  }
2217
- return { Finalize };
2218
- }
2219
- function Reconcile(reconcileCallback) {
2220
- if (registerWatch) {
2221
- log("Reconcile Action", reconcileCallback.toString());
2222
- const aliasLogger = logger_default.child({ alias: binding.alias || "no alias provided" });
2223
- bindings.push({
2224
- ...binding,
2225
- isWatch: true,
2226
- isQueue: true,
2227
- watchCallback: async (update, phase, logger = aliasLogger) => {
2228
- logger_default.info(`Executing reconcile action with alias: ${binding.alias || "no alias provided"}`);
2229
- await reconcileCallback(update, phase, logger);
2230
- }
2278
+ const exportedCapabilities = [];
2279
+ for (const capability of capabilities) {
2280
+ exportedCapabilities.push({
2281
+ name: capability.name,
2282
+ description: capability.description,
2283
+ namespaces: capability.namespaces,
2284
+ bindings: capability.bindings,
2285
+ hasSchedule: capability.hasSchedule
2231
2286
  });
2232
2287
  }
2233
- return { Finalize };
2288
+ process.send(exportedCapabilities);
2289
+ return;
2234
2290
  }
2235
- function Finalize(finalizeCallback) {
2236
- log("Finalize Action", finalizeCallback.toString());
2237
- const aliasLogger = logger_default.child({ alias: binding.alias || "no alias provided" });
2238
- if (registerAdmission) {
2239
- const mutateBinding = {
2240
- ...binding,
2241
- isMutate: true,
2242
- isFinalize: true,
2243
- event: "*" /* ANY */,
2244
- mutateCallback: addFinalizer
2245
- };
2246
- bindings.push(mutateBinding);
2247
- }
2248
- if (registerWatch) {
2249
- const watchBinding = {
2250
- ...binding,
2251
- isWatch: true,
2252
- isFinalize: true,
2253
- event: "UPDATE" /* UPDATE */,
2254
- finalizeCallback: async (update, logger = aliasLogger) => {
2255
- logger_default.info(`Executing finalize action with alias: ${binding.alias || "no alias provided"}`);
2256
- return await finalizeCallback(update, logger);
2291
+ const controllerHooks = {
2292
+ beforeHook: opts.beforeHook,
2293
+ afterHook: opts.afterHook,
2294
+ onReady: () => {
2295
+ if (isWatchMode() || isDevMode()) {
2296
+ try {
2297
+ setupWatch(capabilities, resolveIgnoreNamespaces(pepr?.alwaysIgnore?.namespaces));
2298
+ } catch (e) {
2299
+ logger_default.error(e, "Error setting up watch");
2300
+ process.exit(1);
2257
2301
  }
2258
- };
2259
- bindings.push(watchBinding);
2302
+ }
2260
2303
  }
2261
- }
2262
- function InNamespace(...namespaces) {
2263
- logger_default.debug(`Add namespaces filter ${namespaces}`, prefix);
2264
- binding.filters.namespaces.push(...namespaces);
2265
- return { ...commonChain, WithName, WithNameRegex };
2266
- }
2267
- function InNamespaceRegex(...namespaces) {
2268
- logger_default.debug(`Add regex namespaces filter ${namespaces}`, prefix);
2269
- binding.filters.regexNamespaces.push(...namespaces.map((regex) => regex.source));
2270
- return { ...commonChain, WithName, WithNameRegex };
2271
- }
2272
- function WithDeletionTimestamp() {
2273
- logger_default.debug("Add deletionTimestamp filter");
2274
- binding.filters.deletionTimestamp = true;
2275
- return commonChain;
2276
- }
2277
- function WithNameRegex(regexName) {
2278
- logger_default.debug(`Add regex name filter ${regexName}`, prefix);
2279
- binding.filters.regexName = regexName.source;
2280
- return commonChain;
2281
- }
2282
- function WithName(name2) {
2283
- logger_default.debug(`Add name filter ${name2}`, prefix);
2284
- binding.filters.name = name2;
2285
- return commonChain;
2286
- }
2287
- function WithLabel(key, value = "") {
2288
- logger_default.debug(`Add label filter ${key}=${value}`, prefix);
2289
- binding.filters.labels[key] = value;
2290
- return commonChain;
2291
- }
2292
- function WithAnnotation(key, value = "") {
2293
- logger_default.debug(`Add annotation filter ${key}=${value}`, prefix);
2294
- binding.filters.annotations[key] = value;
2295
- return commonChain;
2296
- }
2297
- function Alias(alias) {
2298
- logger_default.debug(`Adding prefix alias ${alias}`, prefix);
2299
- binding.alias = alias;
2300
- return commonChain;
2301
- }
2302
- function bindEvent(event) {
2303
- binding.event = event;
2304
- return {
2305
- ...commonChain,
2306
- InNamespace,
2307
- InNamespaceRegex,
2308
- WithName,
2309
- WithNameRegex,
2310
- WithDeletionTimestamp,
2311
- Alias
2312
- };
2313
- }
2314
- return {
2315
- IsCreatedOrUpdated: () => bindEvent("CREATEORUPDATE" /* CREATE_OR_UPDATE */),
2316
- IsCreated: () => bindEvent("CREATE" /* CREATE */),
2317
- IsUpdated: () => bindEvent("UPDATE" /* UPDATE */),
2318
- IsDeleted: () => bindEvent("DELETE" /* DELETE */)
2319
2304
  };
2305
+ this.#controller = new Controller(config, capabilities, controllerHooks);
2306
+ if (opts.deferStart) {
2307
+ return;
2308
+ }
2309
+ this.start();
2310
+ }
2311
+ /**
2312
+ * Start the Pepr runtime manually.
2313
+ * 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.
2314
+ *
2315
+ * @param port
2316
+ */
2317
+ start = (port = 3e3) => {
2318
+ this.#controller.startServer(port);
2320
2319
  };
2321
2320
  };
2322
2321
 
@@ -2344,7 +2343,8 @@ function containers(request, containerType) {
2344
2343
  }
2345
2344
  return [...containers2, ...initContainers, ...ephemeralContainers];
2346
2345
  }
2347
- async function writeEvent(cr, event, eventType, eventReason, reportingComponent, reportingInstance) {
2346
+ async function writeEvent(cr, event, options) {
2347
+ const { eventType, eventReason, reportingComponent, reportingInstance } = options;
2348
2348
  await (0, import_kubernetes_fluent_client7.K8s)(import_kubernetes_fluent_client7.kind.CoreEvent).Create({
2349
2349
  type: eventType,
2350
2350
  reason: eventReason,