pepr 0.26.2 → 0.28.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cli.js +572 -97
- package/dist/controller.js +1 -1
- 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 +4 -0
- package/dist/lib/assets/index.d.ts.map +1 -1
- package/dist/lib/assets/pods.d.ts +9 -2
- package/dist/lib/assets/pods.d.ts.map +1 -1
- package/dist/lib/assets/yaml.d.ts +1 -0
- package/dist/lib/assets/yaml.d.ts.map +1 -1
- package/dist/lib/filter.d.ts.map +1 -1
- package/dist/lib/helpers.d.ts +9 -0
- package/dist/lib/helpers.d.ts.map +1 -1
- package/dist/lib/k8s.d.ts +0 -11
- package/dist/lib/k8s.d.ts.map +1 -1
- package/dist/lib/module.d.ts +0 -2
- package/dist/lib/module.d.ts.map +1 -1
- package/dist/lib/watch-processor.d.ts.map +1 -1
- package/dist/lib.js +86 -27
- package/dist/lib.js.map +4 -4
- package/package.json +9 -9
- package/src/cli.ts +2 -0
- package/src/lib/assets/deploy.ts +2 -2
- package/src/lib/assets/helm.ts +199 -0
- package/src/lib/assets/index.ts +121 -4
- package/src/lib/assets/pods.ts +22 -12
- package/src/lib/assets/yaml.ts +116 -4
- package/src/lib/filter.ts +4 -1
- package/src/lib/helpers.ts +120 -5
- package/src/lib/k8s.ts +0 -11
- package/src/lib/module.ts +0 -2
- package/src/lib/watch-processor.ts +21 -10
- package/src/templates/package.json +4 -3
package/src/lib/helpers.ts
CHANGED
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
// SPDX-License-Identifier: Apache-2.0
|
|
2
2
|
// SPDX-FileCopyrightText: 2023-Present The Pepr Authors
|
|
3
3
|
|
|
4
|
-
import { K8s, kind } from "kubernetes-fluent-client";
|
|
4
|
+
import { K8s, KubernetesObject, kind } from "kubernetes-fluent-client";
|
|
5
5
|
import Log from "./logger";
|
|
6
6
|
import { CapabilityExport } from "./types";
|
|
7
7
|
import { promises as fs } from "fs";
|
|
8
|
-
import
|
|
8
|
+
import { Binding } from "./types";
|
|
9
9
|
|
|
10
10
|
type RBACMap = {
|
|
11
11
|
[key: string]: {
|
|
@@ -14,6 +14,96 @@ type RBACMap = {
|
|
|
14
14
|
};
|
|
15
15
|
};
|
|
16
16
|
|
|
17
|
+
// check for overlap with labels and annotations between bindings and kubernetes objects
|
|
18
|
+
export function checkOverlap(record1: Record<string, string>, record2: Record<string, string>) {
|
|
19
|
+
if (Object.keys(record1).length === 0) {
|
|
20
|
+
return true;
|
|
21
|
+
}
|
|
22
|
+
for (const key in record1) {
|
|
23
|
+
if (
|
|
24
|
+
Object.prototype.hasOwnProperty.call(record1, key) &&
|
|
25
|
+
Object.prototype.hasOwnProperty.call(record2, key) &&
|
|
26
|
+
record1[key] === record2[key]
|
|
27
|
+
) {
|
|
28
|
+
return true;
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
return false;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Decide to run callback after the event comes back from API Server
|
|
36
|
+
**/
|
|
37
|
+
export const filterMatcher = (
|
|
38
|
+
binding: Partial<Binding>,
|
|
39
|
+
obj: Partial<KubernetesObject>,
|
|
40
|
+
capabilityNamespaces: string[],
|
|
41
|
+
): string => {
|
|
42
|
+
// binding kind is namespace with a InNamespace filter
|
|
43
|
+
if (binding.kind && binding.kind.kind === "Namespace" && binding.filters && binding.filters.namespaces.length !== 0) {
|
|
44
|
+
return `Ignoring Watch Callback: Cannot use a namespace filter in a namespace object.`;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
if (typeof obj === "object" && obj !== null && "metadata" in obj && obj.metadata !== undefined && binding.filters) {
|
|
48
|
+
// binding labels and object labels dont match
|
|
49
|
+
if (obj.metadata.labels && !checkOverlap(binding.filters.labels, obj.metadata.labels)) {
|
|
50
|
+
return `Ignoring Watch Callback: No overlap between binding and object labels. Binding labels ${JSON.stringify(
|
|
51
|
+
binding.filters.labels,
|
|
52
|
+
)}, Object Labels ${JSON.stringify(obj.metadata.labels)}.`;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
// binding annotations and object annotations dont match
|
|
56
|
+
if (obj.metadata.annotations && !checkOverlap(binding.filters.annotations, obj.metadata.annotations)) {
|
|
57
|
+
return `Ignoring Watch Callback: No overlap between binding and object annotations. Binding annotations ${JSON.stringify(
|
|
58
|
+
binding.filters.annotations,
|
|
59
|
+
)}, Object annotations ${JSON.stringify(obj.metadata.annotations)}.`;
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
// Check object is in the capability namespace
|
|
64
|
+
if (
|
|
65
|
+
Array.isArray(capabilityNamespaces) &&
|
|
66
|
+
capabilityNamespaces.length > 0 &&
|
|
67
|
+
obj.metadata &&
|
|
68
|
+
obj.metadata.namespace &&
|
|
69
|
+
!capabilityNamespaces.includes(obj.metadata.namespace)
|
|
70
|
+
) {
|
|
71
|
+
return `Ignoring Watch Callback: Object is not in the capability namespace. Capability namespaces: ${capabilityNamespaces.join(
|
|
72
|
+
", ",
|
|
73
|
+
)}, Object namespace: ${obj.metadata.namespace}.`;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
// chceck every filter namespace is a capability namespace
|
|
77
|
+
if (
|
|
78
|
+
Array.isArray(capabilityNamespaces) &&
|
|
79
|
+
capabilityNamespaces.length > 0 &&
|
|
80
|
+
binding.filters &&
|
|
81
|
+
Array.isArray(binding.filters.namespaces) &&
|
|
82
|
+
binding.filters.namespaces.length > 0 &&
|
|
83
|
+
!binding.filters.namespaces.every(ns => capabilityNamespaces.includes(ns))
|
|
84
|
+
) {
|
|
85
|
+
return `Ignoring Watch Callback: Binding namespace is not part of capability namespaces. Capability namespaces: ${capabilityNamespaces.join(
|
|
86
|
+
", ",
|
|
87
|
+
)}, Binding namespaces: ${binding.filters.namespaces.join(", ")}.`;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
// filter namespace is not the same of object namespace
|
|
91
|
+
if (
|
|
92
|
+
binding.filters &&
|
|
93
|
+
Array.isArray(binding.filters.namespaces) &&
|
|
94
|
+
binding.filters.namespaces.length > 0 &&
|
|
95
|
+
obj.metadata &&
|
|
96
|
+
obj.metadata.namespace &&
|
|
97
|
+
!binding.filters.namespaces.includes(obj.metadata.namespace)
|
|
98
|
+
) {
|
|
99
|
+
return `Ignoring Watch Callback: Binding namespace and object namespace are not the same. Binding namespaces: ${binding.filters.namespaces.join(
|
|
100
|
+
", ",
|
|
101
|
+
)}, Object namespace: ${obj.metadata.namespace}.`;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
// no problems
|
|
105
|
+
return "";
|
|
106
|
+
};
|
|
17
107
|
export const addVerbIfNotExists = (verbs: string[], verb: string) => {
|
|
18
108
|
if (!verbs.includes(verb)) {
|
|
19
109
|
verbs.push(verb);
|
|
@@ -177,11 +267,36 @@ export const parseTimeout = (value: string, previous: unknown): number => {
|
|
|
177
267
|
const parsedValue = parseInt(value, 10);
|
|
178
268
|
const floatValue = parseFloat(value);
|
|
179
269
|
if (isNaN(parsedValue)) {
|
|
180
|
-
throw new
|
|
270
|
+
throw new Error("Not a number.");
|
|
181
271
|
} else if (parsedValue !== floatValue) {
|
|
182
|
-
throw new
|
|
272
|
+
throw new Error("Value must be an integer.");
|
|
183
273
|
} else if (parsedValue < 1 || parsedValue > 30) {
|
|
184
|
-
throw new
|
|
274
|
+
throw new Error("Number must be between 1 and 30.");
|
|
185
275
|
}
|
|
186
276
|
return parsedValue;
|
|
187
277
|
};
|
|
278
|
+
|
|
279
|
+
// Remove leading whitespace while keeping format of file
|
|
280
|
+
export function dedent(file: string) {
|
|
281
|
+
// Check if the first line is empty and remove it
|
|
282
|
+
const lines = file.split("\n");
|
|
283
|
+
if (lines[0].trim() === "") {
|
|
284
|
+
lines.shift(); // Remove the first line if it's empty
|
|
285
|
+
file = lines.join("\n"); // Rejoin the remaining lines back into a single string
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
const match = file.match(/^[ \t]*(?=\S)/gm);
|
|
289
|
+
const indent = match && Math.min(...match.map(el => el.length));
|
|
290
|
+
if (indent && indent > 0) {
|
|
291
|
+
const re = new RegExp(`^[ \\t]{${indent}}`, "gm");
|
|
292
|
+
return file.replace(re, "");
|
|
293
|
+
}
|
|
294
|
+
return file;
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
export function replaceString(str: string, stringA: string, stringB: string) {
|
|
298
|
+
//eslint-disable-next-line
|
|
299
|
+
const escapedStringA = stringA.replace(/[-\/\\^$*+?.()|[\]{}]/g, "\\$&");
|
|
300
|
+
const regExp = new RegExp(escapedStringA, "g");
|
|
301
|
+
return str.replace(regExp, stringB);
|
|
302
|
+
}
|
package/src/lib/k8s.ts
CHANGED
|
@@ -166,15 +166,4 @@ export type WebhookIgnore = {
|
|
|
166
166
|
* Note: `kube-system` and `pepr-system` are always ignored.
|
|
167
167
|
*/
|
|
168
168
|
namespaces?: string[];
|
|
169
|
-
/**
|
|
170
|
-
* List of Kubernetes labels to always ignore.
|
|
171
|
-
* Any resources with these labels will be ignored by Pepr.
|
|
172
|
-
*
|
|
173
|
-
* The example below will ignore any resources with the label `my-label=ulta-secret`:
|
|
174
|
-
* ```
|
|
175
|
-
* alwaysIgnore:
|
|
176
|
-
* labels: [{ "my-label": "ultra-secret" }]
|
|
177
|
-
* ```
|
|
178
|
-
*/
|
|
179
|
-
labels?: Record<string, string>[];
|
|
180
169
|
};
|
package/src/lib/module.ts
CHANGED
|
@@ -15,8 +15,6 @@ export interface CustomLabels {
|
|
|
15
15
|
}
|
|
16
16
|
/** Global configuration for the Pepr runtime. */
|
|
17
17
|
export type ModuleConfig = {
|
|
18
|
-
/** The user-defined name for the module */
|
|
19
|
-
name: string;
|
|
20
18
|
/** The Pepr version this module uses */
|
|
21
19
|
peprVersion?: string;
|
|
22
20
|
/** The user-defined version of the module */
|
|
@@ -11,6 +11,7 @@ import Log from "./logger";
|
|
|
11
11
|
import { Binding, Event } from "./types";
|
|
12
12
|
import { Watcher } from "kubernetes-fluent-client/dist/fluent/watch";
|
|
13
13
|
import { GenericClass } from "kubernetes-fluent-client";
|
|
14
|
+
import { filterMatcher } from "./helpers";
|
|
14
15
|
|
|
15
16
|
// Track if the store has been updated
|
|
16
17
|
let storeUpdates = false;
|
|
@@ -58,13 +59,14 @@ export async function setupStore(uuid: string) {
|
|
|
58
59
|
export async function setupWatch(uuid: string, capabilities: Capability[]) {
|
|
59
60
|
await setupStore(uuid);
|
|
60
61
|
|
|
61
|
-
capabilities
|
|
62
|
-
.
|
|
63
|
-
|
|
64
|
-
|
|
62
|
+
capabilities.map(capability =>
|
|
63
|
+
capability.bindings
|
|
64
|
+
.filter(binding => binding.isWatch)
|
|
65
|
+
.forEach(bindingElement => runBinding(bindingElement, capability.namespaces)),
|
|
66
|
+
);
|
|
65
67
|
}
|
|
66
68
|
|
|
67
|
-
async function runBinding(binding: Binding) {
|
|
69
|
+
async function runBinding(binding: Binding, capabilityNamespaces: string[]) {
|
|
68
70
|
// Map the event to the watch phase
|
|
69
71
|
const eventToPhaseMap = {
|
|
70
72
|
[Event.Create]: [WatchPhase.Added],
|
|
@@ -92,9 +94,14 @@ async function runBinding(binding: Binding) {
|
|
|
92
94
|
// If the type matches the phase, call the watch callback
|
|
93
95
|
if (phaseMatch.includes(type)) {
|
|
94
96
|
try {
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
97
|
+
const filterMatch = filterMatcher(binding, obj, capabilityNamespaces);
|
|
98
|
+
if (filterMatch === "") {
|
|
99
|
+
queue.setReconcile(async () => await binding.watchCallback?.(obj, type));
|
|
100
|
+
// Enqueue the object for reconciliation through callback
|
|
101
|
+
await queue.enqueue(obj);
|
|
102
|
+
} else {
|
|
103
|
+
Log.debug(filterMatch);
|
|
104
|
+
}
|
|
98
105
|
} catch (e) {
|
|
99
106
|
// Errors in the watch callback should not crash the controller
|
|
100
107
|
Log.error(e, "Error executing watch callback");
|
|
@@ -109,8 +116,12 @@ async function runBinding(binding: Binding) {
|
|
|
109
116
|
// If the type matches the phase, call the watch callback
|
|
110
117
|
if (phaseMatch.includes(type)) {
|
|
111
118
|
try {
|
|
112
|
-
|
|
113
|
-
|
|
119
|
+
const filterMatch = filterMatcher(binding, obj, capabilityNamespaces);
|
|
120
|
+
if (filterMatch === "") {
|
|
121
|
+
await binding.watchCallback?.(obj, type);
|
|
122
|
+
} else {
|
|
123
|
+
Log.debug(filterMatch);
|
|
124
|
+
}
|
|
114
125
|
} catch (e) {
|
|
115
126
|
// Errors in the watch callback should not crash the controller
|
|
116
127
|
Log.error(e, "Error executing watch callback");
|