pepr 0.45.0-nightly.0 → 0.45.0-nightly.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cli/build.d.ts +1 -1
- package/dist/cli/build.d.ts.map +1 -1
- package/dist/cli/init/templates.d.ts +1 -2
- package/dist/cli/init/templates.d.ts.map +1 -1
- package/dist/cli.js +2 -2
- package/dist/controller.js +1 -1
- package/dist/lib/assets/assets.d.ts +2 -2
- package/dist/lib/assets/assets.d.ts.map +1 -1
- package/dist/lib/assets/index.d.ts +1 -1
- package/dist/lib/assets/index.d.ts.map +1 -1
- package/dist/lib/assets/pods.d.ts +1 -1
- package/dist/lib/assets/pods.d.ts.map +1 -1
- package/dist/lib/assets/yaml/overridesFile.d.ts +1 -1
- package/dist/lib/assets/yaml/overridesFile.d.ts.map +1 -1
- package/dist/lib/controller/index.d.ts +1 -1
- package/dist/lib/controller/index.d.ts.map +1 -1
- package/dist/lib/core/envChecks.d.ts +4 -0
- package/dist/lib/core/envChecks.d.ts.map +1 -0
- package/dist/lib/core/module.d.ts +1 -51
- package/dist/lib/core/module.d.ts.map +1 -1
- package/dist/lib/processors/mutate-processor.d.ts +1 -1
- package/dist/lib/processors/mutate-processor.d.ts.map +1 -1
- package/dist/lib/processors/validate-processor.d.ts +1 -1
- package/dist/lib/processors/validate-processor.d.ts.map +1 -1
- package/dist/lib/types.d.ts +45 -0
- package/dist/lib/types.d.ts.map +1 -1
- package/dist/lib.js +2024 -2024
- package/dist/lib.js.map +4 -4
- package/package.json +2 -3
- package/src/cli/build.ts +3 -3
- package/src/cli/init/templates.ts +1 -1
- package/src/lib/assets/assets.ts +2 -2
- package/src/lib/assets/index.ts +1 -2
- package/src/lib/assets/pods.ts +1 -1
- package/src/lib/assets/yaml/overridesFile.ts +1 -1
- package/src/lib/controller/index.ts +2 -1
- package/src/lib/core/capability.ts +1 -1
- package/src/lib/core/envChecks.ts +6 -0
- package/src/lib/core/module.ts +3 -62
- package/src/lib/processors/mutate-processor.ts +1 -1
- package/src/lib/processors/validate-processor.ts +1 -1
- package/src/lib/types.ts +48 -0
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
|
|
54
|
-
var
|
|
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,2222 @@ function redactedPatch(patch = {}) {
|
|
|
101
101
|
}
|
|
102
102
|
var logger_default = Log;
|
|
103
103
|
|
|
104
|
-
// src/lib/core/
|
|
105
|
-
var
|
|
106
|
-
|
|
107
|
-
|
|
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/
|
|
113
|
-
var
|
|
114
|
-
var
|
|
115
|
-
var
|
|
116
|
-
var
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
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
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
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
|
-
};
|
|
159
|
-
addCounter = (name2, help) => {
|
|
160
|
-
this.#addMetric(this.#counters, import_prom_client.default.Counter, { name: name2, help, labelNames: [] });
|
|
161
135
|
};
|
|
162
|
-
|
|
163
|
-
|
|
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
|
-
|
|
166
|
-
|
|
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
|
-
|
|
169
|
-
this.#
|
|
151
|
+
removeItem = (key) => {
|
|
152
|
+
this.#dispatchUpdate("remove", [v2StoreKey(key)]);
|
|
170
153
|
};
|
|
171
|
-
|
|
172
|
-
this.#
|
|
154
|
+
setItem = (key, value) => {
|
|
155
|
+
this.#dispatchUpdate("add", [v2StoreKey(key)], value);
|
|
173
156
|
};
|
|
174
157
|
/**
|
|
175
|
-
*
|
|
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
|
-
|
|
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
|
-
*
|
|
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
|
-
|
|
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
|
-
*
|
|
184
|
-
* @param
|
|
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
|
-
|
|
188
|
-
this.#
|
|
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
|
-
*
|
|
192
|
-
* @
|
|
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
|
-
|
|
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
|
-
*
|
|
197
|
-
* @returns
|
|
269
|
+
* Checks the store for this schedule and sets the values if it exists
|
|
270
|
+
* @returns
|
|
198
271
|
*/
|
|
199
|
-
|
|
200
|
-
|
|
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
|
-
*
|
|
204
|
-
* @
|
|
282
|
+
* Saves the schedule to the store
|
|
283
|
+
* @returns
|
|
205
284
|
*/
|
|
206
|
-
|
|
207
|
-
|
|
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
|
-
*
|
|
211
|
-
* @param count - The count to increment by.
|
|
295
|
+
* Gets the durations in milliseconds
|
|
212
296
|
*/
|
|
213
|
-
|
|
214
|
-
|
|
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
|
-
*
|
|
218
|
-
* @param label - The label for the cache miss.
|
|
316
|
+
* Sets up the interval
|
|
219
317
|
*/
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
this
|
|
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
|
-
*
|
|
339
|
+
* Starts the interval
|
|
227
340
|
*/
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
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
|
-
|
|
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
|
|
264
|
-
|
|
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/
|
|
276
|
-
var
|
|
277
|
-
|
|
278
|
-
(request)
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
(
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
(
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
);
|
|
303
|
-
|
|
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
|
-
|
|
445
|
-
|
|
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
|
-
|
|
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/
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
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
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
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
|
-
|
|
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
|
|
598
|
-
return this.#
|
|
488
|
+
get description() {
|
|
489
|
+
return this.#description;
|
|
599
490
|
}
|
|
600
|
-
get
|
|
601
|
-
return this.#
|
|
491
|
+
get namespaces() {
|
|
492
|
+
return this.#namespaces || [];
|
|
602
493
|
}
|
|
603
|
-
|
|
604
|
-
|
|
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
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
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
|
-
|
|
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
|
-
|
|
635
|
-
|
|
636
|
-
|
|
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
|
-
|
|
523
|
+
this.#registered = true;
|
|
524
|
+
return this.#store;
|
|
639
525
|
};
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
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}`);
|
|
539
|
+
}
|
|
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
|
+
});
|
|
577
|
+
}
|
|
578
|
+
return { Watch, Reconcile };
|
|
579
|
+
}
|
|
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 };
|
|
594
|
+
}
|
|
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 };
|
|
609
|
+
}
|
|
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 };
|
|
625
|
+
}
|
|
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
|
+
}
|
|
652
|
+
}
|
|
653
|
+
function InNamespace(...namespaces) {
|
|
654
|
+
logger_default.debug(`Add namespaces filter ${namespaces}`, prefix);
|
|
655
|
+
binding.filters.namespaces.push(...namespaces);
|
|
656
|
+
return { ...commonChain, WithName, WithNameRegex };
|
|
657
|
+
}
|
|
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"));
|
|
721
|
+
|
|
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
|
+
};
|
|
741
|
+
/**
|
|
742
|
+
* Creates a MetricsCollector instance with prefixed metrics.
|
|
743
|
+
* @param [prefix='pepr'] - The prefix for the metric names.
|
|
744
|
+
*/
|
|
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 token 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"]);
|
|
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
|
+
};
|
|
784
|
+
/**
|
|
785
|
+
* Increments the error counter.
|
|
786
|
+
*/
|
|
787
|
+
error = () => this.incCounter(this.#metricNames.errors);
|
|
788
|
+
/**
|
|
789
|
+
* Increments the alerts counter.
|
|
790
|
+
*/
|
|
791
|
+
alert = () => this.incCounter(this.#metricNames.alerts);
|
|
792
|
+
/**
|
|
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.
|
|
796
|
+
*/
|
|
797
|
+
observeEnd = (startTime, name2 = this.#metricNames.mutate) => {
|
|
798
|
+
this.#summaries.get(this.#getMetricName(name2))?.observe(import_perf_hooks.performance.now() - startTime);
|
|
799
|
+
};
|
|
800
|
+
/**
|
|
801
|
+
* Fetches the current metrics from the registry.
|
|
802
|
+
* @returns The metrics.
|
|
803
|
+
*/
|
|
804
|
+
getMetrics = () => this.#registry.metrics();
|
|
805
|
+
/**
|
|
806
|
+
* Returns the current timestamp from performance.now() method. Useful for start timing an operation.
|
|
807
|
+
* @returns The timestamp.
|
|
808
|
+
*/
|
|
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 });
|
|
818
|
+
};
|
|
819
|
+
/**
|
|
820
|
+
* Increments the retry count gauge.
|
|
821
|
+
* @param count - The count to increment by.
|
|
822
|
+
*/
|
|
823
|
+
incRetryCount = (count) => {
|
|
824
|
+
this.incGauge(this.#metricNames.resyncFailureCount, { count });
|
|
825
|
+
};
|
|
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);
|
|
834
|
+
};
|
|
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);
|
|
844
|
+
}
|
|
845
|
+
this.#gauges.get(this.#getMetricName(this.#metricNames.cacheMiss))?.remove({ window: firstKey });
|
|
846
|
+
}
|
|
847
|
+
};
|
|
848
|
+
};
|
|
849
|
+
var metricsCollector = new MetricsCollector("pepr");
|
|
850
|
+
|
|
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`);
|
|
866
|
+
}
|
|
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`);
|
|
881
|
+
}
|
|
882
|
+
}
|
|
883
|
+
};
|
|
884
|
+
|
|
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);
|
|
1035
|
+
}
|
|
1036
|
+
if (carriesNamespace(kubernetesObject)) {
|
|
1037
|
+
return namespaceSelector.includes(carriedNamespace(kubernetesObject));
|
|
1038
|
+
}
|
|
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}`;
|
|
643
1119
|
}
|
|
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
1120
|
}
|
|
1121
|
+
return "";
|
|
670
1122
|
}
|
|
671
|
-
function
|
|
672
|
-
const
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
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}`;
|
|
684
1143
|
}
|
|
685
1144
|
}
|
|
686
|
-
|
|
687
|
-
return skip;
|
|
1145
|
+
return "";
|
|
688
1146
|
}
|
|
689
|
-
function
|
|
690
|
-
return
|
|
1147
|
+
function adjudicateMisboundNamespace(binding) {
|
|
1148
|
+
return misboundNamespace(binding) ? "Cannot use namespace filter on a namespace object." : null;
|
|
691
1149
|
}
|
|
692
|
-
function
|
|
693
|
-
return
|
|
1150
|
+
function adjudicateMisboundDeleteWithDeletionTimestamp(binding) {
|
|
1151
|
+
return misboundDeleteWithDeletionTimestamp(binding) ? "Cannot use deletionTimestamp filter on a DELETE operation." : null;
|
|
694
1152
|
}
|
|
695
|
-
|
|
696
|
-
|
|
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);
|
|
1153
|
+
function adjudicateMismatchedDeletionTimestamp(binding, obj) {
|
|
1154
|
+
return mismatchedDeletionTimestamp(binding, obj) ? "Binding defines deletionTimestamp but Object does not carry it." : null;
|
|
716
1155
|
}
|
|
717
|
-
|
|
718
|
-
|
|
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;
|
|
1156
|
+
function adjudicateMismatchedEvent(binding, req) {
|
|
1157
|
+
return mismatchedEvent(binding, req) ? `Binding defines event '${definedEvent(binding)}' but Request declares '${declaredOperation(req)}'.` : null;
|
|
725
1158
|
}
|
|
726
|
-
function
|
|
727
|
-
|
|
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.");
|
|
732
|
-
}
|
|
733
|
-
} catch {
|
|
734
|
-
return "An error occurred with the mutate action.";
|
|
735
|
-
}
|
|
1159
|
+
function adjudicateMismatchedName(binding, obj) {
|
|
1160
|
+
return mismatchedName(binding, obj) ? `Binding defines name '${definedName(binding)}' but Object carries '${carriedName(obj)}'.` : null;
|
|
736
1161
|
}
|
|
737
|
-
function
|
|
738
|
-
|
|
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 };
|
|
1162
|
+
function adjudicateMismatchedGroup(binding, req) {
|
|
1163
|
+
return mismatchedGroup(binding, req) ? `Binding defines group '${definedGroup(binding)}' but Request declares '${declaredGroup(req)}'.` : null;
|
|
744
1164
|
}
|
|
745
|
-
function
|
|
746
|
-
|
|
747
|
-
const isSecret = wrapped.Request.kind.version === "v1" && wrapped.Request.kind.kind === "Secret";
|
|
748
|
-
if (isSecret) {
|
|
749
|
-
convertToBase64Map(transformed, skipped);
|
|
750
|
-
}
|
|
751
|
-
return transformed;
|
|
1165
|
+
function adjudicateMismatchedVersion(binding, req) {
|
|
1166
|
+
return mismatchedVersion(binding, req) ? `Binding defines version '${definedVersion(binding)}' but Request declares '${declaredVersion(req)}'.` : null;
|
|
752
1167
|
}
|
|
753
|
-
|
|
754
|
-
|
|
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;
|
|
776
|
-
}
|
|
777
|
-
}
|
|
778
|
-
return { wrapped, response };
|
|
1168
|
+
function adjudicateMismatchedKind(binding, req) {
|
|
1169
|
+
return mismatchedKind(binding, req) ? `Binding defines kind '${definedKind(binding)}' but Request declares '${declaredKind(req)}'.` : null;
|
|
779
1170
|
}
|
|
780
|
-
|
|
781
|
-
|
|
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;
|
|
804
|
-
}
|
|
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;
|
|
814
|
-
}
|
|
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;
|
|
821
|
-
}
|
|
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;
|
|
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;
|
|
837
1173
|
}
|
|
838
|
-
function
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
|
|
842
|
-
}
|
|
843
|
-
|
|
844
|
-
|
|
845
|
-
}
|
|
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;
|
|
846
1197
|
}
|
|
847
1198
|
|
|
848
|
-
// src/lib/
|
|
849
|
-
var
|
|
850
|
-
var
|
|
1199
|
+
// src/lib/mutate-request.ts
|
|
1200
|
+
var import_ramda4 = require("ramda");
|
|
1201
|
+
var PeprMutateRequest = class {
|
|
851
1202
|
Raw;
|
|
852
1203
|
#input;
|
|
853
|
-
|
|
854
|
-
|
|
855
|
-
|
|
856
|
-
|
|
1204
|
+
get PermitSideEffects() {
|
|
1205
|
+
return !this.#input.dryRun;
|
|
1206
|
+
}
|
|
1207
|
+
get IsDryRun() {
|
|
1208
|
+
return this.#input.dryRun;
|
|
1209
|
+
}
|
|
857
1210
|
get OldResource() {
|
|
858
1211
|
return this.#input.oldObject;
|
|
859
1212
|
}
|
|
860
|
-
/**
|
|
861
|
-
* Provides access to the request object.
|
|
862
|
-
* @returns The request object containing the Kubernetes resource.
|
|
863
|
-
*/
|
|
864
1213
|
get Request() {
|
|
865
1214
|
return this.#input;
|
|
866
1215
|
}
|
|
867
|
-
/**
|
|
868
|
-
* Creates a new instance of the Action class.
|
|
869
|
-
* @param input - The request object containing the Kubernetes resource to modify.
|
|
870
|
-
*/
|
|
871
1216
|
constructor(input) {
|
|
872
|
-
this.#input = input;
|
|
873
|
-
if (input.operation.toUpperCase() === "DELETE" /* DELETE */) {
|
|
874
|
-
this.Raw = (0,
|
|
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
|
-
}
|
|
882
|
-
/**
|
|
883
|
-
* Check if a label exists on the Kubernetes resource.
|
|
884
|
-
*
|
|
885
|
-
* @param key the label key to check
|
|
886
|
-
* @returns
|
|
887
|
-
*/
|
|
888
|
-
HasLabel = (key) => {
|
|
889
|
-
return this.Raw.metadata?.labels?.[key] !== void 0;
|
|
890
|
-
};
|
|
891
|
-
/**
|
|
892
|
-
* Check if an annotation exists on the Kubernetes resource.
|
|
893
|
-
*
|
|
894
|
-
* @param key the annotation key to check
|
|
895
|
-
* @returns
|
|
896
|
-
*/
|
|
897
|
-
HasAnnotation = (key) => {
|
|
898
|
-
return this.Raw.metadata?.annotations?.[key] !== void 0;
|
|
899
|
-
};
|
|
900
|
-
/**
|
|
901
|
-
* Create a validation response that allows the request.
|
|
902
|
-
*
|
|
903
|
-
* @returns The validation response.
|
|
904
|
-
*/
|
|
905
|
-
Approve = () => {
|
|
906
|
-
return {
|
|
907
|
-
allowed: true
|
|
908
|
-
};
|
|
909
|
-
};
|
|
910
|
-
/**
|
|
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.
|
|
916
|
-
*/
|
|
917
|
-
Deny = (statusMessage, statusCode) => {
|
|
918
|
-
return {
|
|
919
|
-
allowed: false,
|
|
920
|
-
statusCode,
|
|
921
|
-
statusMessage
|
|
922
|
-
};
|
|
923
|
-
};
|
|
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
|
|
934
|
-
};
|
|
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;
|
|
980
|
-
}
|
|
981
|
-
const resp = await processRequest2(binding, actionMetadata, wrapped);
|
|
982
|
-
response.push(resp);
|
|
983
|
-
}
|
|
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"
|
|
1001
|
-
};
|
|
1002
|
-
(0, import_kubernetes_fluent_client.RegisterKind)(Store, peprStoreGVK);
|
|
1003
|
-
|
|
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
|
-
}
|
|
1024
|
-
}
|
|
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`);
|
|
1036
|
-
}
|
|
1037
|
-
const cacheIndex = [op, path].join(":");
|
|
1038
|
-
cache[cacheIndex] = { op, path };
|
|
1039
|
-
} else {
|
|
1040
|
-
throw new Error(`Unsupported operation: ${op}`);
|
|
1041
|
-
}
|
|
1042
|
-
return cache;
|
|
1043
|
-
};
|
|
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
|
-
|
|
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
|
-
}
|
|
1217
|
+
this.#input = input;
|
|
1218
|
+
if (input.operation.toUpperCase() === "DELETE" /* DELETE */) {
|
|
1219
|
+
this.Raw = (0, import_ramda4.clone)(input.oldObject);
|
|
1076
1220
|
} else {
|
|
1077
|
-
|
|
1078
|
-
|
|
1079
|
-
|
|
1221
|
+
this.Raw = (0, import_ramda4.clone)(input.object);
|
|
1222
|
+
}
|
|
1223
|
+
if (!this.Raw) {
|
|
1224
|
+
throw new Error("Unable to load the request object into PeprRequest.Raw");
|
|
1080
1225
|
}
|
|
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
1226
|
}
|
|
1087
|
-
|
|
1088
|
-
|
|
1089
|
-
watcher.start().catch((e) => logger_default.error(e, "Error starting Pepr store watch"));
|
|
1227
|
+
Merge = (obj) => {
|
|
1228
|
+
this.Raw = (0, import_ramda4.mergeDeepRight)(this.Raw, obj);
|
|
1090
1229
|
};
|
|
1091
|
-
|
|
1092
|
-
|
|
1093
|
-
|
|
1094
|
-
|
|
1095
|
-
|
|
1096
|
-
|
|
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
|
-
}
|
|
1117
|
-
}
|
|
1118
|
-
storeCache = await sendUpdatesAndFlushCache(storeCache, namespace, this.#name);
|
|
1119
|
-
this.#setupWatch();
|
|
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;
|
|
1120
1236
|
};
|
|
1121
|
-
|
|
1122
|
-
|
|
1123
|
-
|
|
1124
|
-
|
|
1125
|
-
|
|
1126
|
-
|
|
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);
|
|
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;
|
|
1142
1243
|
};
|
|
1143
|
-
|
|
1144
|
-
|
|
1145
|
-
|
|
1146
|
-
|
|
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;
|
|
1244
|
+
RemoveLabel = (key) => {
|
|
1245
|
+
if (this.Raw.metadata?.labels?.[key]) {
|
|
1246
|
+
delete this.Raw.metadata.labels[key];
|
|
1247
|
+
}
|
|
1248
|
+
return this;
|
|
1155
1249
|
};
|
|
1156
|
-
|
|
1157
|
-
|
|
1158
|
-
|
|
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");
|
|
1250
|
+
RemoveAnnotation = (key) => {
|
|
1251
|
+
if (this.Raw.metadata?.annotations?.[key]) {
|
|
1252
|
+
delete this.Raw.metadata.annotations[key];
|
|
1176
1253
|
}
|
|
1254
|
+
return this;
|
|
1255
|
+
};
|
|
1256
|
+
HasLabel = (key) => {
|
|
1257
|
+
return this.Raw.metadata?.labels?.[key] !== void 0;
|
|
1258
|
+
};
|
|
1259
|
+
HasAnnotation = (key) => {
|
|
1260
|
+
return this.Raw.metadata?.annotations?.[key] !== void 0;
|
|
1177
1261
|
};
|
|
1178
1262
|
};
|
|
1179
1263
|
|
|
1180
|
-
// src/lib/
|
|
1181
|
-
|
|
1182
|
-
|
|
1183
|
-
|
|
1184
|
-
|
|
1185
|
-
|
|
1186
|
-
|
|
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
|
+
}
|
|
1187
1280
|
}
|
|
1188
|
-
function
|
|
1189
|
-
const
|
|
1190
|
-
|
|
1191
|
-
|
|
1192
|
-
|
|
1193
|
-
|
|
1194
|
-
|
|
1195
|
-
|
|
1196
|
-
|
|
1197
|
-
|
|
1198
|
-
|
|
1199
|
-
|
|
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);
|
|
1293
|
+
}
|
|
1200
1294
|
}
|
|
1201
|
-
}
|
|
1202
|
-
|
|
1203
|
-
|
|
1204
|
-
|
|
1205
|
-
|
|
1206
|
-
|
|
1295
|
+
}
|
|
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");
|
|
1207
1304
|
}
|
|
1208
1305
|
|
|
1209
|
-
// src/
|
|
1210
|
-
|
|
1211
|
-
|
|
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);
|
|
1212
1326
|
}
|
|
1213
|
-
|
|
1214
|
-
|
|
1215
|
-
|
|
1216
|
-
|
|
1217
|
-
|
|
1218
|
-
|
|
1219
|
-
|
|
1220
|
-
|
|
1221
|
-
|
|
1222
|
-
|
|
1223
|
-
|
|
1224
|
-
|
|
1225
|
-
|
|
1226
|
-
|
|
1227
|
-
|
|
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;
|
|
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.");
|
|
1250
1342
|
}
|
|
1343
|
+
} catch {
|
|
1344
|
+
return "An error occurred with the mutate action.";
|
|
1251
1345
|
}
|
|
1252
|
-
|
|
1253
|
-
|
|
1254
|
-
|
|
1255
|
-
|
|
1256
|
-
|
|
1257
|
-
|
|
1258
|
-
|
|
1259
|
-
|
|
1260
|
-
|
|
1261
|
-
|
|
1262
|
-
|
|
1263
|
-
|
|
1264
|
-
|
|
1265
|
-
|
|
1266
|
-
|
|
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;
|
|
1267
1386
|
}
|
|
1268
|
-
|
|
1269
|
-
|
|
1270
|
-
|
|
1271
|
-
|
|
1272
|
-
|
|
1273
|
-
|
|
1274
|
-
|
|
1275
|
-
|
|
1276
|
-
|
|
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
|
-
});
|
|
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
|
|
1291
1397
|
};
|
|
1292
|
-
|
|
1293
|
-
|
|
1294
|
-
|
|
1295
|
-
|
|
1296
|
-
|
|
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;
|
|
1297
1414
|
}
|
|
1298
|
-
|
|
1299
|
-
|
|
1300
|
-
|
|
1301
|
-
|
|
1302
|
-
|
|
1303
|
-
|
|
1304
|
-
|
|
1305
|
-
|
|
1306
|
-
|
|
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;
|
|
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;
|
|
1318
1424
|
}
|
|
1319
|
-
|
|
1320
|
-
};
|
|
1321
|
-
|
|
1322
|
-
|
|
1323
|
-
|
|
1324
|
-
|
|
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");
|
|
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;
|
|
1334
1431
|
}
|
|
1335
|
-
};
|
|
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
|
-
};
|
|
1375
|
-
};
|
|
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;
|
|
1389
|
-
}
|
|
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();
|
|
1401
1432
|
}
|
|
1402
|
-
|
|
1403
|
-
|
|
1404
|
-
|
|
1405
|
-
|
|
1406
|
-
|
|
1407
|
-
|
|
1408
|
-
|
|
1409
|
-
|
|
1410
|
-
|
|
1411
|
-
|
|
1412
|
-
|
|
1413
|
-
|
|
1414
|
-
|
|
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;
|
|
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;
|
|
1447
|
+
}
|
|
1448
|
+
function updateResponsePatchAndWarnings(patches, response) {
|
|
1449
|
+
if (patches.length > 0) {
|
|
1450
|
+
response.patchType = "JSONPatch";
|
|
1451
|
+
response.patch = base64Encode(JSON.stringify(patches));
|
|
1415
1452
|
}
|
|
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(", ")}`);
|
|
1453
|
+
if (response.warnings && response.warnings.length < 1) {
|
|
1454
|
+
delete response.warnings;
|
|
1423
1455
|
}
|
|
1424
1456
|
}
|
|
1425
1457
|
|
|
1426
|
-
// src/lib/
|
|
1427
|
-
var
|
|
1428
|
-
|
|
1429
|
-
|
|
1430
|
-
|
|
1431
|
-
|
|
1432
|
-
|
|
1433
|
-
|
|
1434
|
-
|
|
1435
|
-
|
|
1436
|
-
|
|
1437
|
-
this.#name = name2;
|
|
1438
|
-
this.#uid = `${Date.now()}-${(0, import_node_crypto.randomBytes)(2).toString("hex")}`;
|
|
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;
|
|
1439
1469
|
}
|
|
1440
|
-
|
|
1441
|
-
|
|
1470
|
+
/**
|
|
1471
|
+
* Provides access to the request object.
|
|
1472
|
+
* @returns The request object containing the Kubernetes resource.
|
|
1473
|
+
*/
|
|
1474
|
+
get Request() {
|
|
1475
|
+
return this.#input;
|
|
1442
1476
|
}
|
|
1443
|
-
|
|
1444
|
-
|
|
1445
|
-
|
|
1446
|
-
|
|
1447
|
-
|
|
1448
|
-
|
|
1449
|
-
|
|
1477
|
+
/**
|
|
1478
|
+
* Creates a new instance of the Action class.
|
|
1479
|
+
* @param input - The request object containing the Kubernetes resource to modify.
|
|
1480
|
+
*/
|
|
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);
|
|
1487
|
+
}
|
|
1488
|
+
if (!this.Raw) {
|
|
1489
|
+
throw new Error("unable to load the request object into PeprRequest.Raw");
|
|
1490
|
+
}
|
|
1450
1491
|
}
|
|
1451
1492
|
/**
|
|
1452
|
-
*
|
|
1453
|
-
* reconciled.
|
|
1493
|
+
* Check if a label exists on the Kubernetes resource.
|
|
1454
1494
|
*
|
|
1455
|
-
* @param
|
|
1456
|
-
* @
|
|
1457
|
-
* @param reconcile The callback to enqueue for reconcile
|
|
1458
|
-
* @returns A promise that resolves when the object is reconciled
|
|
1495
|
+
* @param key the label key to check
|
|
1496
|
+
* @returns
|
|
1459
1497
|
*/
|
|
1460
|
-
|
|
1461
|
-
|
|
1462
|
-
|
|
1463
|
-
|
|
1464
|
-
|
|
1465
|
-
|
|
1466
|
-
|
|
1467
|
-
|
|
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
|
|
1468
1518
|
};
|
|
1469
|
-
|
|
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
|
-
});
|
|
1475
|
-
}
|
|
1519
|
+
};
|
|
1476
1520
|
/**
|
|
1477
|
-
*
|
|
1521
|
+
* Create a validation response that denies the request.
|
|
1478
1522
|
*
|
|
1479
|
-
* @
|
|
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.
|
|
1480
1526
|
*/
|
|
1481
|
-
|
|
1482
|
-
|
|
1483
|
-
|
|
1484
|
-
|
|
1485
|
-
|
|
1486
|
-
|
|
1487
|
-
|
|
1488
|
-
logger_default.debug("No element, not dequeuing");
|
|
1489
|
-
return false;
|
|
1490
|
-
}
|
|
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();
|
|
1513
|
-
}
|
|
1514
|
-
}
|
|
1527
|
+
Deny = (statusMessage, statusCode) => {
|
|
1528
|
+
return {
|
|
1529
|
+
allowed: false,
|
|
1530
|
+
statusCode,
|
|
1531
|
+
statusMessage
|
|
1532
|
+
};
|
|
1533
|
+
};
|
|
1515
1534
|
};
|
|
1516
1535
|
|
|
1517
|
-
// src/lib/processors/
|
|
1518
|
-
|
|
1519
|
-
|
|
1520
|
-
|
|
1521
|
-
|
|
1522
|
-
|
|
1523
|
-
|
|
1524
|
-
|
|
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
|
-
|
|
1544
|
-
|
|
1545
|
-
|
|
1546
|
-
|
|
1547
|
-
|
|
1548
|
-
|
|
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
|
+
};
|
|
1549
1553
|
}
|
|
1554
|
+
logger_default.info(actionMetadata, `Validation action complete (${label}): ${callbackResp.allowed ? "allowed" : "denied"}`);
|
|
1555
|
+
return valResp;
|
|
1556
|
+
} catch (e) {
|
|
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;
|
|
1550
1564
|
}
|
|
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}'`);
|
|
1560
|
-
}
|
|
1561
|
-
|
|
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
1565
|
}
|
|
1580
|
-
function
|
|
1581
|
-
const
|
|
1582
|
-
|
|
1583
|
-
|
|
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);
|
|
1593
|
+
}
|
|
1584
1594
|
}
|
|
1585
|
-
|
|
1595
|
+
webhookTimer.stop();
|
|
1596
|
+
return response;
|
|
1586
1597
|
}
|
|
1587
|
-
|
|
1588
|
-
|
|
1589
|
-
|
|
1590
|
-
|
|
1591
|
-
|
|
1598
|
+
|
|
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
|
|
1594
|
-
|
|
1595
|
-
|
|
1596
|
-
|
|
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
|
-
|
|
1601
|
-
|
|
1602
|
-
|
|
1603
|
-
|
|
1604
|
-
|
|
1605
|
-
async
|
|
1606
|
-
const
|
|
1607
|
-
|
|
1608
|
-
|
|
1609
|
-
if (
|
|
1610
|
-
|
|
1611
|
-
|
|
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
|
-
|
|
1645
|
-
|
|
1646
|
-
|
|
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
|
-
|
|
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
|
-
|
|
1687
|
-
|
|
1688
|
-
|
|
1689
|
-
|
|
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
|
-
|
|
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/
|
|
1696
|
-
var
|
|
1697
|
-
var
|
|
1698
|
-
var
|
|
1699
|
-
var
|
|
1700
|
-
#
|
|
1701
|
-
|
|
1702
|
-
|
|
1703
|
-
|
|
1704
|
-
|
|
1705
|
-
|
|
1706
|
-
|
|
1707
|
-
|
|
1708
|
-
|
|
1709
|
-
|
|
1710
|
-
|
|
1711
|
-
|
|
1712
|
-
if (
|
|
1713
|
-
|
|
1714
|
-
|
|
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
|
-
|
|
1717
|
-
for (const
|
|
1718
|
-
|
|
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);
|
|
1725
1689
|
}
|
|
1726
|
-
process.send(exportedCapabilities);
|
|
1727
|
-
return;
|
|
1728
1690
|
}
|
|
1729
|
-
|
|
1730
|
-
|
|
1731
|
-
|
|
1732
|
-
|
|
1733
|
-
|
|
1734
|
-
|
|
1735
|
-
|
|
1736
|
-
|
|
1737
|
-
|
|
1738
|
-
|
|
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
|
+
}
|
|
1726
|
+
}
|
|
1727
|
+
}
|
|
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.#
|
|
1744
|
-
|
|
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
|
-
|
|
1761
|
-
|
|
1762
|
-
|
|
1763
|
-
|
|
1764
|
-
|
|
1765
|
-
|
|
1766
|
-
|
|
1767
|
-
}
|
|
1768
|
-
|
|
1769
|
-
|
|
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
|
-
|
|
1781
|
-
|
|
1782
|
-
|
|
1783
|
-
|
|
1784
|
-
|
|
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
|
-
|
|
1788
|
-
|
|
1789
|
-
|
|
1790
|
-
|
|
1791
|
-
|
|
1792
|
-
|
|
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
|
-
|
|
1795
|
-
|
|
1796
|
-
|
|
1797
|
-
|
|
1798
|
-
|
|
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
|
-
|
|
1803
|
-
|
|
1804
|
-
|
|
1805
|
-
|
|
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
|
-
|
|
1810
|
-
|
|
1811
|
-
|
|
1812
|
-
|
|
1813
|
-
|
|
1814
|
-
|
|
1815
|
-
|
|
1816
|
-
|
|
1817
|
-
|
|
1818
|
-
|
|
1819
|
-
|
|
1820
|
-
|
|
1821
|
-
|
|
1822
|
-
|
|
1823
|
-
|
|
1824
|
-
|
|
1825
|
-
|
|
1826
|
-
|
|
1827
|
-
|
|
1828
|
-
|
|
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 token used to authenticate requests
|
|
1829
|
+
#token = "";
|
|
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
|
-
|
|
1835
|
-
|
|
1836
|
-
|
|
1837
|
-
|
|
1838
|
-
|
|
1839
|
-
|
|
1840
|
-
|
|
1841
|
-
|
|
1842
|
-
|
|
1843
|
-
|
|
1844
|
-
|
|
1845
|
-
|
|
1846
|
-
|
|
1847
|
-
|
|
1848
|
-
|
|
1849
|
-
|
|
1850
|
-
|
|
1851
|
-
|
|
1852
|
-
|
|
1853
|
-
|
|
1854
|
-
});
|
|
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.#token = process.env.PEPR_API_TOKEN || import_fs.default.readFileSync("/app/api-token/value").toString().trim();
|
|
1873
|
+
logger_default.info(`Using API token: ${this.#token}`);
|
|
1874
|
+
if (!this.#token) {
|
|
1875
|
+
throw new Error("API token 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);
|
|
1899
|
+
});
|
|
1855
1900
|
});
|
|
1856
1901
|
};
|
|
1857
|
-
|
|
1858
|
-
|
|
1859
|
-
this.#
|
|
1860
|
-
|
|
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.#
|
|
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/:token", "/validate/:token"], this.#validateToken);
|
|
1909
|
+
this.#app.post("/mutate/:token", this.#admissionReq("Mutate"));
|
|
1910
|
+
this.#app.post("/validate/:token", 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
|
-
*
|
|
1913
|
+
* Validate the token 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
|
-
|
|
1937
|
-
const
|
|
1938
|
-
|
|
1939
|
-
|
|
1940
|
-
|
|
1941
|
-
|
|
1942
|
-
|
|
1943
|
-
|
|
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
|
+
#validateToken = (req, res, next) => {
|
|
1921
|
+
const { token } = req.params;
|
|
1922
|
+
if (token !== this.#token) {
|
|
1923
|
+
const err = `Unauthorized: invalid token '${token.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
|
-
*
|
|
1932
|
+
* Metrics endpoint handler
|
|
1933
|
+
*
|
|
1934
|
+
* @param req the incoming request
|
|
1935
|
+
* @param res the outgoing response
|
|
1968
1936
|
*/
|
|
1969
|
-
|
|
1970
|
-
|
|
1971
|
-
|
|
1972
|
-
|
|
1973
|
-
|
|
1974
|
-
|
|
1975
|
-
|
|
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
|
-
*
|
|
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
|
-
|
|
1993
|
-
|
|
1994
|
-
|
|
1995
|
-
|
|
1996
|
-
|
|
1997
|
-
|
|
1998
|
-
|
|
1999
|
-
|
|
2000
|
-
|
|
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
|
|
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
|
-
}
|
|
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;
|
|
1984
|
+
};
|
|
1985
|
+
};
|
|
2031
1986
|
/**
|
|
2032
|
-
*
|
|
1987
|
+
* Middleware for logging requests
|
|
2033
1988
|
*
|
|
2034
|
-
* @param
|
|
2035
|
-
* @
|
|
1989
|
+
* @param req the incoming request
|
|
1990
|
+
* @param res the outgoing response
|
|
1991
|
+
* @param next the next middleware function
|
|
2036
1992
|
*/
|
|
2037
|
-
|
|
2038
|
-
const
|
|
2039
|
-
|
|
2040
|
-
|
|
2041
|
-
|
|
2042
|
-
|
|
2043
|
-
|
|
2044
|
-
|
|
2045
|
-
|
|
2046
|
-
|
|
2047
|
-
|
|
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`
|
|
2048
2007
|
};
|
|
2049
|
-
|
|
2050
|
-
|
|
2051
|
-
|
|
2052
|
-
}
|
|
2053
|
-
};
|
|
2054
|
-
getScheduleStore() {
|
|
2055
|
-
return this.#scheduleStore;
|
|
2008
|
+
logger_default.info(message);
|
|
2009
|
+
});
|
|
2010
|
+
next();
|
|
2056
2011
|
}
|
|
2057
2012
|
/**
|
|
2058
|
-
*
|
|
2059
|
-
* between requests. Each capability has its own store, and the data is persisted in Kubernetes
|
|
2060
|
-
* in the `pepr-system` namespace.
|
|
2061
|
-
*
|
|
2062
|
-
* Note: You should only access the store from within an action.
|
|
2063
|
-
*/
|
|
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
|
-
};
|
|
2074
|
-
/**
|
|
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
|
-
*
|
|
2015
|
+
* @param req the incoming request
|
|
2016
|
+
* @param res the outgoing response
|
|
2080
2017
|
*/
|
|
2081
|
-
|
|
2082
|
-
|
|
2083
|
-
|
|
2084
|
-
|
|
2085
|
-
|
|
2086
|
-
|
|
2087
|
-
|
|
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
|
-
|
|
2095
|
-
|
|
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
|
-
|
|
2098
|
-
|
|
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
|
-
|
|
2101
|
-
return this.#
|
|
2050
|
+
label() {
|
|
2051
|
+
return { name: this.#name, uid: this.#uid };
|
|
2102
2052
|
}
|
|
2103
|
-
|
|
2104
|
-
|
|
2105
|
-
|
|
2106
|
-
|
|
2107
|
-
|
|
2108
|
-
|
|
2109
|
-
|
|
2053
|
+
stats() {
|
|
2054
|
+
return {
|
|
2055
|
+
queue: this.label(),
|
|
2056
|
+
stats: {
|
|
2057
|
+
length: this.#queue.length
|
|
2058
|
+
}
|
|
2059
|
+
};
|
|
2110
2060
|
}
|
|
2111
2061
|
/**
|
|
2112
|
-
*
|
|
2113
|
-
|
|
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
|
-
};
|
|
2122
|
-
/**
|
|
2123
|
-
* 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.
|
|
2124
2064
|
*
|
|
2125
|
-
* @param
|
|
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
|
|
2126
2069
|
*/
|
|
2127
|
-
|
|
2128
|
-
|
|
2129
|
-
|
|
2130
|
-
|
|
2131
|
-
|
|
2132
|
-
|
|
2133
|
-
|
|
2134
|
-
|
|
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
|
+
}
|
|
2135
2086
|
/**
|
|
2136
|
-
*
|
|
2137
|
-
* processed by Pepr. The action will be executed if the resource matches the specified kind and any
|
|
2138
|
-
* filters that are applied.
|
|
2087
|
+
* Dequeue reconciles the next item in the queue
|
|
2139
2088
|
*
|
|
2140
|
-
* @
|
|
2141
|
-
* @param kind if using a custom KubernetesObject not available in `a.*`, specify the GroupVersionKind
|
|
2142
|
-
* @returns
|
|
2089
|
+
* @returns A promise that resolves when the webapp is reconciled
|
|
2143
2090
|
*/
|
|
2144
|
-
|
|
2145
|
-
|
|
2146
|
-
|
|
2147
|
-
|
|
2091
|
+
async #dequeue() {
|
|
2092
|
+
if (this.#pendingPromise) {
|
|
2093
|
+
logger_default.debug("Pending promise, not dequeuing");
|
|
2094
|
+
return false;
|
|
2148
2095
|
}
|
|
2149
|
-
const
|
|
2150
|
-
|
|
2151
|
-
|
|
2152
|
-
|
|
2153
|
-
|
|
2154
|
-
|
|
2155
|
-
|
|
2156
|
-
|
|
2157
|
-
|
|
2158
|
-
|
|
2159
|
-
|
|
2160
|
-
|
|
2161
|
-
|
|
2162
|
-
|
|
2163
|
-
|
|
2164
|
-
|
|
2165
|
-
|
|
2166
|
-
|
|
2167
|
-
|
|
2168
|
-
|
|
2169
|
-
|
|
2170
|
-
|
|
2171
|
-
|
|
2172
|
-
logger_default.debug(
|
|
2173
|
-
|
|
2174
|
-
|
|
2175
|
-
|
|
2176
|
-
|
|
2177
|
-
|
|
2178
|
-
|
|
2179
|
-
|
|
2180
|
-
|
|
2181
|
-
|
|
2182
|
-
|
|
2183
|
-
|
|
2184
|
-
|
|
2185
|
-
|
|
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");
|
|
2186
2189
|
}
|
|
2187
|
-
return { Watch, Reconcile };
|
|
2188
2190
|
}
|
|
2189
|
-
|
|
2190
|
-
|
|
2191
|
-
|
|
2192
|
-
|
|
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 };
|
|
2191
|
+
};
|
|
2192
|
+
const handleFinalizerRemoval = async (kubernetesObject) => {
|
|
2193
|
+
if (!kubernetesObject.metadata?.deletionTimestamp) {
|
|
2194
|
+
return;
|
|
2203
2195
|
}
|
|
2204
|
-
|
|
2205
|
-
|
|
2206
|
-
|
|
2207
|
-
|
|
2208
|
-
|
|
2209
|
-
|
|
2210
|
-
|
|
2211
|
-
|
|
2212
|
-
|
|
2213
|
-
|
|
2214
|
-
|
|
2215
|
-
});
|
|
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);
|
|
2216
2207
|
}
|
|
2217
|
-
return { Finalize };
|
|
2218
2208
|
}
|
|
2219
|
-
|
|
2220
|
-
|
|
2221
|
-
|
|
2222
|
-
|
|
2223
|
-
|
|
2224
|
-
|
|
2225
|
-
|
|
2226
|
-
|
|
2227
|
-
|
|
2228
|
-
|
|
2229
|
-
|
|
2230
|
-
|
|
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
|
+
watcher.events.on(import_kubernetes_fluent_client6.WatchEvent.GIVE_UP, (err) => {
|
|
2220
|
+
logger_default.error(err, "Watch failed after 5 attempts, giving up");
|
|
2221
|
+
process.exit(1);
|
|
2222
|
+
});
|
|
2223
|
+
watcher.events.on(import_kubernetes_fluent_client6.WatchEvent.CONNECT, (url) => logEvent(import_kubernetes_fluent_client6.WatchEvent.CONNECT, url));
|
|
2224
|
+
watcher.events.on(import_kubernetes_fluent_client6.WatchEvent.DATA_ERROR, (err) => logEvent(import_kubernetes_fluent_client6.WatchEvent.DATA_ERROR, err.message));
|
|
2225
|
+
watcher.events.on(
|
|
2226
|
+
import_kubernetes_fluent_client6.WatchEvent.RECONNECT,
|
|
2227
|
+
(retryCount) => logEvent(import_kubernetes_fluent_client6.WatchEvent.RECONNECT, `Reconnecting after ${retryCount} attempt${retryCount === 1 ? "" : "s"}`)
|
|
2228
|
+
);
|
|
2229
|
+
watcher.events.on(import_kubernetes_fluent_client6.WatchEvent.RECONNECT_PENDING, () => logEvent(import_kubernetes_fluent_client6.WatchEvent.RECONNECT_PENDING));
|
|
2230
|
+
watcher.events.on(import_kubernetes_fluent_client6.WatchEvent.GIVE_UP, (err) => logEvent(import_kubernetes_fluent_client6.WatchEvent.GIVE_UP, err.message));
|
|
2231
|
+
watcher.events.on(import_kubernetes_fluent_client6.WatchEvent.ABORT, (err) => logEvent(import_kubernetes_fluent_client6.WatchEvent.ABORT, err.message));
|
|
2232
|
+
watcher.events.on(import_kubernetes_fluent_client6.WatchEvent.OLD_RESOURCE_VERSION, (err) => logEvent(import_kubernetes_fluent_client6.WatchEvent.OLD_RESOURCE_VERSION, err));
|
|
2233
|
+
watcher.events.on(import_kubernetes_fluent_client6.WatchEvent.NETWORK_ERROR, (err) => logEvent(import_kubernetes_fluent_client6.WatchEvent.NETWORK_ERROR, err.message));
|
|
2234
|
+
watcher.events.on(import_kubernetes_fluent_client6.WatchEvent.LIST_ERROR, (err) => logEvent(import_kubernetes_fluent_client6.WatchEvent.LIST_ERROR, err.message));
|
|
2235
|
+
watcher.events.on(import_kubernetes_fluent_client6.WatchEvent.LIST, (list) => logEvent(import_kubernetes_fluent_client6.WatchEvent.LIST, JSON.stringify(list, void 0, 2)));
|
|
2236
|
+
watcher.events.on(import_kubernetes_fluent_client6.WatchEvent.CACHE_MISS, (windowName) => {
|
|
2237
|
+
metricsCollector.incCacheMiss(windowName);
|
|
2238
|
+
});
|
|
2239
|
+
watcher.events.on(import_kubernetes_fluent_client6.WatchEvent.INIT_CACHE_MISS, (windowName) => {
|
|
2240
|
+
metricsCollector.initCacheMissWindow(windowName);
|
|
2241
|
+
});
|
|
2242
|
+
watcher.events.on(import_kubernetes_fluent_client6.WatchEvent.INC_RESYNC_FAILURE_COUNT, (retryCount) => {
|
|
2243
|
+
metricsCollector.incRetryCount(retryCount);
|
|
2244
|
+
});
|
|
2245
|
+
try {
|
|
2246
|
+
await watcher.start();
|
|
2247
|
+
} catch (err) {
|
|
2248
|
+
logger_default.error(err, "Error starting watch");
|
|
2249
|
+
process.exit(1);
|
|
2250
|
+
}
|
|
2251
|
+
}
|
|
2252
|
+
function logEvent(event, message = "", obj) {
|
|
2253
|
+
const logMessage = `Watch event ${event} received${message ? `. ${message}.` : "."}`;
|
|
2254
|
+
if (obj) {
|
|
2255
|
+
logger_default.debug(obj, logMessage);
|
|
2256
|
+
} else {
|
|
2257
|
+
logger_default.debug(logMessage);
|
|
2258
|
+
}
|
|
2259
|
+
}
|
|
2260
|
+
|
|
2261
|
+
// src/lib/core/module.ts
|
|
2262
|
+
var PeprModule = class {
|
|
2263
|
+
#controller;
|
|
2264
|
+
/**
|
|
2265
|
+
* Create a new Pepr runtime
|
|
2266
|
+
*
|
|
2267
|
+
* @param config The configuration for the Pepr runtime
|
|
2268
|
+
* @param capabilities The capabilities to be loaded into the Pepr runtime
|
|
2269
|
+
* @param opts Options for the Pepr runtime
|
|
2270
|
+
*/
|
|
2271
|
+
constructor({ description, pepr }, capabilities = [], opts = {}) {
|
|
2272
|
+
const config = (0, import_ramda9.clone)(pepr);
|
|
2273
|
+
config.description = description;
|
|
2274
|
+
ValidateError(config.onError);
|
|
2275
|
+
if (isBuildMode()) {
|
|
2276
|
+
if (!process.send) {
|
|
2277
|
+
throw new Error("process.send is not defined");
|
|
2278
|
+
}
|
|
2279
|
+
const exportedCapabilities = [];
|
|
2280
|
+
for (const capability of capabilities) {
|
|
2281
|
+
exportedCapabilities.push({
|
|
2282
|
+
name: capability.name,
|
|
2283
|
+
description: capability.description,
|
|
2284
|
+
namespaces: capability.namespaces,
|
|
2285
|
+
bindings: capability.bindings,
|
|
2286
|
+
hasSchedule: capability.hasSchedule
|
|
2231
2287
|
});
|
|
2232
2288
|
}
|
|
2233
|
-
|
|
2289
|
+
process.send(exportedCapabilities);
|
|
2290
|
+
return;
|
|
2234
2291
|
}
|
|
2235
|
-
|
|
2236
|
-
|
|
2237
|
-
|
|
2238
|
-
|
|
2239
|
-
|
|
2240
|
-
|
|
2241
|
-
|
|
2242
|
-
|
|
2243
|
-
|
|
2244
|
-
|
|
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);
|
|
2292
|
+
const controllerHooks = {
|
|
2293
|
+
beforeHook: opts.beforeHook,
|
|
2294
|
+
afterHook: opts.afterHook,
|
|
2295
|
+
onReady: () => {
|
|
2296
|
+
if (isWatchMode() || isDevMode()) {
|
|
2297
|
+
try {
|
|
2298
|
+
setupWatch(capabilities, resolveIgnoreNamespaces(pepr?.alwaysIgnore?.namespaces));
|
|
2299
|
+
} catch (e) {
|
|
2300
|
+
logger_default.error(e, "Error setting up watch");
|
|
2301
|
+
process.exit(1);
|
|
2257
2302
|
}
|
|
2258
|
-
}
|
|
2259
|
-
bindings.push(watchBinding);
|
|
2303
|
+
}
|
|
2260
2304
|
}
|
|
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
2305
|
};
|
|
2306
|
+
this.#controller = new Controller(config, capabilities, controllerHooks);
|
|
2307
|
+
if (opts.deferStart) {
|
|
2308
|
+
return;
|
|
2309
|
+
}
|
|
2310
|
+
this.start();
|
|
2311
|
+
}
|
|
2312
|
+
/**
|
|
2313
|
+
* Start the Pepr runtime manually.
|
|
2314
|
+
* 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.
|
|
2315
|
+
*
|
|
2316
|
+
* @param port
|
|
2317
|
+
*/
|
|
2318
|
+
start = (port = 3e3) => {
|
|
2319
|
+
this.#controller.startServer(port);
|
|
2320
2320
|
};
|
|
2321
2321
|
};
|
|
2322
2322
|
|