pepr 0.37.1 → 0.37.3

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