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