pepr 0.36.0 → 0.37.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/init/index.d.ts.map +1 -1
- package/dist/cli/init/templates.d.ts +4 -3
- package/dist/cli/init/templates.d.ts.map +1 -1
- package/dist/cli/init/utils.d.ts.map +1 -1
- package/dist/cli/init/walkthrough.d.ts +10 -3
- package/dist/cli/init/walkthrough.d.ts.map +1 -1
- package/dist/cli/types.d.ts +3 -0
- package/dist/cli/types.d.ts.map +1 -0
- package/dist/cli.js +253 -31
- package/dist/controller.js +138 -1
- package/dist/lib/adjudicators.d.ts +63 -0
- package/dist/lib/adjudicators.d.ts.map +1 -0
- package/dist/lib/adjudicators.test.d.ts +2 -0
- package/dist/lib/adjudicators.test.d.ts.map +1 -0
- package/dist/lib/assets/loader.d.ts.map +1 -1
- package/dist/lib/assets/pods.d.ts +1 -0
- package/dist/lib/assets/pods.d.ts.map +1 -1
- package/dist/lib/capability.d.ts +1 -0
- package/dist/lib/capability.d.ts.map +1 -1
- package/dist/lib/capability.test.d.ts +2 -0
- package/dist/lib/capability.test.d.ts.map +1 -0
- package/dist/lib/controller/index.d.ts.map +1 -1
- package/dist/lib/controller/store.d.ts +4 -0
- package/dist/lib/controller/store.d.ts.map +1 -1
- package/dist/lib/controller/store.test.d.ts +2 -0
- package/dist/lib/controller/store.test.d.ts.map +1 -0
- package/dist/lib/filter.d.ts +2 -3
- package/dist/lib/filter.d.ts.map +1 -1
- package/dist/lib/filter.test.d.ts +2 -1
- package/dist/lib/filter.test.d.ts.map +1 -1
- package/dist/lib/finalizer.d.ts +6 -0
- package/dist/lib/finalizer.d.ts.map +1 -0
- package/dist/lib/finalizer.test.d.ts +2 -0
- package/dist/lib/finalizer.test.d.ts.map +1 -0
- package/dist/lib/helpers.d.ts +2 -2
- package/dist/lib/helpers.d.ts.map +1 -1
- package/dist/lib/helpers.test.d.ts +1 -1
- package/dist/lib/helpers.test.d.ts.map +1 -1
- package/dist/lib/k8s.d.ts.map +1 -1
- package/dist/lib/module.d.ts +2 -1
- package/dist/lib/module.d.ts.map +1 -1
- package/dist/lib/mutate-processor.d.ts +2 -1
- package/dist/lib/mutate-processor.d.ts.map +1 -1
- package/dist/lib/mutate-request.d.ts +1 -2
- package/dist/lib/mutate-request.d.ts.map +1 -1
- package/dist/lib/schedule.d.ts +1 -2
- package/dist/lib/schedule.d.ts.map +1 -1
- package/dist/lib/storage.d.ts.map +1 -1
- package/dist/lib/types.d.ts +113 -6
- package/dist/lib/types.d.ts.map +1 -1
- package/dist/lib/validate-processor.d.ts +4 -2
- package/dist/lib/validate-processor.d.ts.map +1 -1
- package/dist/lib/validate-request.d.ts +1 -1
- package/dist/lib/validate-request.d.ts.map +1 -1
- package/dist/lib/watch-processor.d.ts +1 -1
- package/dist/lib/watch-processor.d.ts.map +1 -1
- package/dist/lib.js +383 -204
- package/dist/lib.js.map +4 -4
- package/package.json +13 -12
- package/src/cli/build.ts +3 -3
- package/src/cli/init/index.ts +20 -11
- package/src/cli/init/templates.ts +1 -1
- package/src/cli/init/utils.test.ts +11 -20
- package/src/cli/init/utils.ts +5 -0
- package/src/cli/init/walkthrough.test.ts +92 -11
- package/src/cli/init/walkthrough.ts +71 -16
- package/src/cli/monitor.ts +1 -1
- package/src/cli/types.ts +3 -0
- package/src/cli.ts +4 -2
- package/src/fixtures/data/create-pod.json +1 -1
- package/src/fixtures/data/delete-pod.json +1 -1
- package/src/lib/adjudicators.test.ts +1232 -0
- package/src/lib/adjudicators.ts +235 -0
- package/src/lib/assets/index.ts +1 -1
- package/src/lib/assets/loader.ts +1 -0
- package/src/lib/assets/webhooks.ts +1 -1
- package/src/lib/capability.test.ts +655 -0
- package/src/lib/capability.ts +104 -11
- package/src/lib/controller/index.ts +7 -4
- package/src/lib/controller/store.test.ts +131 -0
- package/src/lib/controller/store.ts +43 -5
- package/src/lib/filter.test.ts +194 -8
- package/src/lib/filter.ts +46 -107
- package/src/lib/finalizer.test.ts +236 -0
- package/src/lib/finalizer.ts +63 -0
- package/src/lib/helpers.test.ts +329 -69
- package/src/lib/helpers.ts +141 -100
- package/src/lib/k8s.ts +4 -0
- package/src/lib/module.ts +3 -3
- package/src/lib/mutate-processor.ts +5 -4
- package/src/lib/mutate-request.test.ts +1 -2
- package/src/lib/mutate-request.ts +1 -3
- package/src/lib/schedule.ts +1 -1
- package/src/lib/storage.ts +5 -6
- package/src/lib/types.ts +148 -5
- package/src/lib/validate-processor.ts +5 -2
- package/src/lib/validate-request.test.ts +1 -4
- package/src/lib/validate-request.ts +1 -1
- package/src/lib/watch-processor.ts +19 -5
package/src/lib/helpers.ts
CHANGED
|
@@ -6,6 +6,39 @@ import { K8s, KubernetesObject, kind } from "kubernetes-fluent-client";
|
|
|
6
6
|
import Log from "./logger";
|
|
7
7
|
import { Binding, CapabilityExport } from "./types";
|
|
8
8
|
import { sanitizeResourceName } from "../sdk/sdk";
|
|
9
|
+
import {
|
|
10
|
+
carriedAnnotations,
|
|
11
|
+
carriedLabels,
|
|
12
|
+
carriedName,
|
|
13
|
+
carriedNamespace,
|
|
14
|
+
carriesIgnoredNamespace,
|
|
15
|
+
definedAnnotations,
|
|
16
|
+
definedLabels,
|
|
17
|
+
definedName,
|
|
18
|
+
definedNameRegex,
|
|
19
|
+
definedNamespaces,
|
|
20
|
+
definedNamespaceRegexes,
|
|
21
|
+
misboundNamespace,
|
|
22
|
+
mismatchedAnnotations,
|
|
23
|
+
mismatchedDeletionTimestamp,
|
|
24
|
+
mismatchedLabels,
|
|
25
|
+
mismatchedName,
|
|
26
|
+
mismatchedNameRegex,
|
|
27
|
+
mismatchedNamespace,
|
|
28
|
+
mismatchedNamespaceRegex,
|
|
29
|
+
unbindableNamespaces,
|
|
30
|
+
uncarryableNamespace,
|
|
31
|
+
} from "./adjudicators";
|
|
32
|
+
|
|
33
|
+
export function matchesRegex(pattern: string, testString: string): boolean {
|
|
34
|
+
// edge-case
|
|
35
|
+
if (!pattern) {
|
|
36
|
+
return false;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
const regex = new RegExp(pattern);
|
|
40
|
+
return regex.test(testString);
|
|
41
|
+
}
|
|
9
42
|
|
|
10
43
|
export class ValidationError extends Error {}
|
|
11
44
|
|
|
@@ -35,36 +68,6 @@ type RBACMap = {
|
|
|
35
68
|
};
|
|
36
69
|
};
|
|
37
70
|
|
|
38
|
-
// check for overlap with labels and annotations between bindings and kubernetes objects
|
|
39
|
-
export function checkOverlap(bindingFilters: Record<string, string>, objectFilters: Record<string, string>): boolean {
|
|
40
|
-
// True if labels/annotations are empty
|
|
41
|
-
if (Object.keys(bindingFilters).length === 0) {
|
|
42
|
-
return true;
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
let matchCount = 0;
|
|
46
|
-
|
|
47
|
-
for (const key in bindingFilters) {
|
|
48
|
-
// object must have label/annotation
|
|
49
|
-
if (Object.prototype.hasOwnProperty.call(objectFilters, key)) {
|
|
50
|
-
const val1 = bindingFilters[key];
|
|
51
|
-
const val2 = objectFilters[key];
|
|
52
|
-
|
|
53
|
-
// If bindingFilter has empty value for this key, only need to ensure objectFilter has this key
|
|
54
|
-
if (val1 === "" && key in objectFilters) {
|
|
55
|
-
matchCount++;
|
|
56
|
-
}
|
|
57
|
-
// If bindingFilter has a value, it must match the value in objectFilter
|
|
58
|
-
else if (val1 !== "" && val1 === val2) {
|
|
59
|
-
matchCount++;
|
|
60
|
-
}
|
|
61
|
-
}
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
// For single-key objects in bindingFilter or matching all keys in multiple-keys scenario
|
|
65
|
-
return matchCount === Object.keys(bindingFilters).length;
|
|
66
|
-
}
|
|
67
|
-
|
|
68
71
|
/**
|
|
69
72
|
* Decide to run callback after the event comes back from API Server
|
|
70
73
|
**/
|
|
@@ -72,76 +75,72 @@ export function filterNoMatchReason(
|
|
|
72
75
|
binding: Partial<Binding>,
|
|
73
76
|
obj: Partial<KubernetesObject>,
|
|
74
77
|
capabilityNamespaces: string[],
|
|
78
|
+
ignoredNamespaces?: string[],
|
|
75
79
|
): string {
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
binding
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
binding
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
)}, Object namespace: ${obj.metadata.namespace}.`;
|
|
141
|
-
}
|
|
142
|
-
|
|
143
|
-
// no problems
|
|
144
|
-
return "";
|
|
80
|
+
const prefix = "Ignoring Watch Callback:";
|
|
81
|
+
|
|
82
|
+
// prettier-ignore
|
|
83
|
+
return (
|
|
84
|
+
mismatchedDeletionTimestamp(binding, obj) ?
|
|
85
|
+
`${prefix} Binding defines deletionTimestamp but Object does not carry it.` :
|
|
86
|
+
|
|
87
|
+
mismatchedName(binding, obj) ?
|
|
88
|
+
`${prefix} Binding defines name '${definedName(binding)}' but Object carries '${carriedName(obj)}'.` :
|
|
89
|
+
|
|
90
|
+
misboundNamespace(binding) ?
|
|
91
|
+
`${prefix} Cannot use namespace filter on a namespace object.` :
|
|
92
|
+
|
|
93
|
+
mismatchedLabels(binding, obj) ?
|
|
94
|
+
(
|
|
95
|
+
`${prefix} Binding defines labels '${JSON.stringify(definedLabels(binding))}' ` +
|
|
96
|
+
`but Object carries '${JSON.stringify(carriedLabels(obj))}'.`
|
|
97
|
+
) :
|
|
98
|
+
|
|
99
|
+
mismatchedAnnotations(binding, obj) ?
|
|
100
|
+
(
|
|
101
|
+
`${prefix} Binding defines annotations '${JSON.stringify(definedAnnotations(binding))}' ` +
|
|
102
|
+
`but Object carries '${JSON.stringify(carriedAnnotations(obj))}'.`
|
|
103
|
+
) :
|
|
104
|
+
|
|
105
|
+
uncarryableNamespace(capabilityNamespaces, obj) ?
|
|
106
|
+
(
|
|
107
|
+
`${prefix} Object carries namespace '${carriedNamespace(obj)}' ` +
|
|
108
|
+
`but namespaces allowed by Capability are '${JSON.stringify(capabilityNamespaces)}'.`
|
|
109
|
+
) :
|
|
110
|
+
|
|
111
|
+
unbindableNamespaces(capabilityNamespaces, binding) ?
|
|
112
|
+
(
|
|
113
|
+
`${prefix} Binding defines namespaces ${JSON.stringify(definedNamespaces(binding))} ` +
|
|
114
|
+
`but namespaces allowed by Capability are '${JSON.stringify(capabilityNamespaces)}'.`
|
|
115
|
+
) :
|
|
116
|
+
|
|
117
|
+
mismatchedNamespace(binding, obj) ?
|
|
118
|
+
(
|
|
119
|
+
`${prefix} Binding defines namespaces '${JSON.stringify(definedNamespaces(binding))}' ` +
|
|
120
|
+
`but Object carries '${carriedNamespace(obj)}'.`
|
|
121
|
+
) :
|
|
122
|
+
|
|
123
|
+
mismatchedNamespaceRegex(binding, obj) ?
|
|
124
|
+
(
|
|
125
|
+
`${prefix} Binding defines namespace regexes ` +
|
|
126
|
+
`'${JSON.stringify(definedNamespaceRegexes(binding))}' ` +
|
|
127
|
+
`but Object carries '${carriedNamespace(obj)}'.`
|
|
128
|
+
) :
|
|
129
|
+
|
|
130
|
+
mismatchedNameRegex(binding, obj) ?
|
|
131
|
+
(
|
|
132
|
+
`${prefix} Binding defines name regex '${definedNameRegex(binding)}' ` +
|
|
133
|
+
`but Object carries '${carriedName(obj)}'.`
|
|
134
|
+
) :
|
|
135
|
+
|
|
136
|
+
carriesIgnoredNamespace(ignoredNamespaces, obj) ?
|
|
137
|
+
(
|
|
138
|
+
`${prefix} Object carries namespace '${carriedNamespace(obj)}' ` +
|
|
139
|
+
`but ignored namespaces include '${JSON.stringify(ignoredNamespaces)}'.`
|
|
140
|
+
) :
|
|
141
|
+
|
|
142
|
+
""
|
|
143
|
+
);
|
|
145
144
|
}
|
|
146
145
|
|
|
147
146
|
export function addVerbIfNotExists(verbs: string[], verb: string) {
|
|
@@ -244,7 +243,8 @@ export function generateWatchNamespaceError(
|
|
|
244
243
|
// namespaceComplianceValidator ensures that capability bindinds respect ignored and capability namespaces
|
|
245
244
|
export function namespaceComplianceValidator(capability: CapabilityExport, ignoredNamespaces?: string[]) {
|
|
246
245
|
const { namespaces: capabilityNamespaces, bindings, name } = capability;
|
|
247
|
-
const bindingNamespaces = bindings.flatMap(binding => binding.filters.namespaces);
|
|
246
|
+
const bindingNamespaces = bindings.flatMap((binding: Binding) => binding.filters.namespaces);
|
|
247
|
+
const bindingRegexNamespaces = bindings.flatMap((binding: Binding) => binding.filters.regexNamespaces || []);
|
|
248
248
|
|
|
249
249
|
const namespaceError = generateWatchNamespaceError(
|
|
250
250
|
ignoredNamespaces ? ignoredNamespaces : [],
|
|
@@ -256,6 +256,47 @@ export function namespaceComplianceValidator(capability: CapabilityExport, ignor
|
|
|
256
256
|
`Error in ${name} capability. A binding violates namespace rules. Please check ignoredNamespaces and capability namespaces: ${namespaceError}`,
|
|
257
257
|
);
|
|
258
258
|
}
|
|
259
|
+
|
|
260
|
+
// Ensure that each regexNamespace matches a capabilityNamespace
|
|
261
|
+
|
|
262
|
+
if (
|
|
263
|
+
bindingRegexNamespaces &&
|
|
264
|
+
bindingRegexNamespaces.length > 0 &&
|
|
265
|
+
capabilityNamespaces &&
|
|
266
|
+
capabilityNamespaces.length > 0
|
|
267
|
+
) {
|
|
268
|
+
for (const regexNamespace of bindingRegexNamespaces) {
|
|
269
|
+
let matches = false;
|
|
270
|
+
for (const capabilityNamespace of capabilityNamespaces) {
|
|
271
|
+
if (regexNamespace !== "" && matchesRegex(regexNamespace, capabilityNamespace)) {
|
|
272
|
+
matches = true;
|
|
273
|
+
break;
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
if (!matches) {
|
|
277
|
+
throw new Error(
|
|
278
|
+
`Ignoring Watch Callback: Object namespace does not match any capability namespace with regex ${regexNamespace}.`,
|
|
279
|
+
);
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
// ensure regexNamespaces do not match ignored ns
|
|
284
|
+
if (
|
|
285
|
+
bindingRegexNamespaces &&
|
|
286
|
+
bindingRegexNamespaces.length > 0 &&
|
|
287
|
+
ignoredNamespaces &&
|
|
288
|
+
ignoredNamespaces.length > 0
|
|
289
|
+
) {
|
|
290
|
+
for (const regexNamespace of bindingRegexNamespaces) {
|
|
291
|
+
for (const ignoredNS of ignoredNamespaces) {
|
|
292
|
+
if (matchesRegex(regexNamespace, ignoredNS)) {
|
|
293
|
+
throw new Error(
|
|
294
|
+
`Ignoring Watch Callback: Regex namespace: ${regexNamespace}, is an ignored namespace: ${ignoredNS}.`,
|
|
295
|
+
);
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
}
|
|
259
300
|
}
|
|
260
301
|
|
|
261
302
|
// check to see if all replicas are ready for all deployments in the pepr-system namespace
|
package/src/lib/k8s.ts
CHANGED
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
|
|
4
4
|
import { GenericKind, GroupVersionKind, KubernetesObject, RegisterKind } from "kubernetes-fluent-client";
|
|
5
5
|
|
|
6
|
+
// DEPRECATED: Use Operation in types.ts instead
|
|
6
7
|
export enum Operation {
|
|
7
8
|
CREATE = "CREATE",
|
|
8
9
|
UPDATE = "UPDATE",
|
|
@@ -27,6 +28,7 @@ export const peprStoreGVK = {
|
|
|
27
28
|
|
|
28
29
|
RegisterKind(PeprStore, peprStoreGVK);
|
|
29
30
|
|
|
31
|
+
// DEPRECATED: Use Operation in types.ts instead
|
|
30
32
|
/**
|
|
31
33
|
* GroupVersionResource unambiguously identifies a resource. It doesn't anonymously include GroupVersion
|
|
32
34
|
* to avoid automatic coercion. It doesn't use a GroupVersion to avoid custom marshalling
|
|
@@ -37,6 +39,8 @@ export interface GroupVersionResource {
|
|
|
37
39
|
readonly resource: string;
|
|
38
40
|
}
|
|
39
41
|
|
|
42
|
+
// DEPRECATED: Use Operation in types.ts instead
|
|
43
|
+
|
|
40
44
|
/**
|
|
41
45
|
* A Kubernetes admission request to be processed by a capability.
|
|
42
46
|
*/
|
package/src/lib/module.ts
CHANGED
|
@@ -4,8 +4,8 @@ import { clone } from "ramda";
|
|
|
4
4
|
import { Capability } from "./capability";
|
|
5
5
|
import { Controller } from "./controller";
|
|
6
6
|
import { ValidateError } from "./errors";
|
|
7
|
-
import {
|
|
8
|
-
import { CapabilityExport } from "./types";
|
|
7
|
+
import { MutateResponse, ValidateResponse, WebhookIgnore } from "./k8s";
|
|
8
|
+
import { CapabilityExport, AdmissionRequest } from "./types";
|
|
9
9
|
import { setupWatch } from "./watch-processor";
|
|
10
10
|
import { Log } from "../lib";
|
|
11
11
|
|
|
@@ -108,7 +108,7 @@ export class PeprModule {
|
|
|
108
108
|
// Wait for the controller to be ready before setting up watches
|
|
109
109
|
if (isWatchMode() || isDevMode()) {
|
|
110
110
|
try {
|
|
111
|
-
setupWatch(capabilities);
|
|
111
|
+
setupWatch(capabilities, pepr?.alwaysIgnore?.namespaces);
|
|
112
112
|
} catch (e) {
|
|
113
113
|
Log.error(e, "Error setting up watch");
|
|
114
114
|
process.exit(1);
|
|
@@ -7,7 +7,8 @@ import { kind } from "kubernetes-fluent-client";
|
|
|
7
7
|
import { Capability } from "./capability";
|
|
8
8
|
import { Errors } from "./errors";
|
|
9
9
|
import { shouldSkipRequest } from "./filter";
|
|
10
|
-
import { MutateResponse
|
|
10
|
+
import { MutateResponse } from "./k8s";
|
|
11
|
+
import { AdmissionRequest } from "./types";
|
|
11
12
|
import Log from "./logger";
|
|
12
13
|
import { ModuleConfig } from "./module";
|
|
13
14
|
import { PeprMutateRequest } from "./mutate-request";
|
|
@@ -42,7 +43,6 @@ export async function mutateProcessor(
|
|
|
42
43
|
|
|
43
44
|
for (const { name, bindings, namespaces } of capabilities) {
|
|
44
45
|
const actionMetadata = { ...reqMetadata, name };
|
|
45
|
-
|
|
46
46
|
for (const action of bindings) {
|
|
47
47
|
// Skip this action if it's not a mutate action
|
|
48
48
|
if (!action.mutateCallback) {
|
|
@@ -50,13 +50,12 @@ export async function mutateProcessor(
|
|
|
50
50
|
}
|
|
51
51
|
|
|
52
52
|
// Continue to the next action without doing anything if this one should be skipped
|
|
53
|
-
if (shouldSkipRequest(action, req, namespaces)) {
|
|
53
|
+
if (shouldSkipRequest(action, req, namespaces, config?.alwaysIgnore?.namespaces)) {
|
|
54
54
|
continue;
|
|
55
55
|
}
|
|
56
56
|
|
|
57
57
|
const label = action.mutateCallback.name;
|
|
58
58
|
Log.info(actionMetadata, `Processing mutation action (${label})`);
|
|
59
|
-
|
|
60
59
|
matchedAction = true;
|
|
61
60
|
|
|
62
61
|
// Add annotations to the request to indicate that the capability started processing
|
|
@@ -79,6 +78,7 @@ export async function mutateProcessor(
|
|
|
79
78
|
// Run the action
|
|
80
79
|
await action.mutateCallback(wrapped);
|
|
81
80
|
|
|
81
|
+
// Log on success
|
|
82
82
|
Log.info(actionMetadata, `Mutation action succeeded (${label})`);
|
|
83
83
|
|
|
84
84
|
// Add annotations to the request to indicate that the capability succeeded
|
|
@@ -99,6 +99,7 @@ export async function mutateProcessor(
|
|
|
99
99
|
errorMessage = "An error occurred with the mutate action.";
|
|
100
100
|
}
|
|
101
101
|
|
|
102
|
+
// Log on failure
|
|
102
103
|
Log.error(actionMetadata, `Action failed: ${errorMessage}`);
|
|
103
104
|
response.warnings.push(`Action failed: ${errorMessage}`);
|
|
104
105
|
|
|
@@ -3,8 +3,7 @@
|
|
|
3
3
|
|
|
4
4
|
import { beforeEach, describe, expect, it } from "@jest/globals";
|
|
5
5
|
import { KubernetesObject } from "kubernetes-fluent-client";
|
|
6
|
-
|
|
7
|
-
import { Operation, AdmissionRequest } from "./k8s";
|
|
6
|
+
import { Operation, AdmissionRequest } from "./types";
|
|
8
7
|
import { PeprMutateRequest } from "./mutate-request";
|
|
9
8
|
|
|
10
9
|
describe("PeprMutateRequest", () => {
|
|
@@ -3,9 +3,7 @@
|
|
|
3
3
|
|
|
4
4
|
import { KubernetesObject } from "kubernetes-fluent-client";
|
|
5
5
|
import { clone, mergeDeepRight } from "ramda";
|
|
6
|
-
|
|
7
|
-
import { AdmissionRequest, Operation } from "./k8s";
|
|
8
|
-
import { DeepPartial } from "./types";
|
|
6
|
+
import { Operation, AdmissionRequest, DeepPartial } from "./types";
|
|
9
7
|
|
|
10
8
|
/**
|
|
11
9
|
* The RequestWrapper class provides methods to modify Kubernetes objects in the context
|
package/src/lib/schedule.ts
CHANGED
package/src/lib/storage.ts
CHANGED
|
@@ -2,7 +2,6 @@
|
|
|
2
2
|
// SPDX-FileCopyrightText: 2023-Present The Pepr Authors
|
|
3
3
|
|
|
4
4
|
import { clone } from "ramda";
|
|
5
|
-
import Log from "./logger";
|
|
6
5
|
import pointer from "json-pointer";
|
|
7
6
|
export type DataOp = "add" | "remove";
|
|
8
7
|
export type DataStore = Record<string, string>;
|
|
@@ -86,7 +85,6 @@ export class Storage implements PeprStore {
|
|
|
86
85
|
};
|
|
87
86
|
|
|
88
87
|
receive = (data: DataStore) => {
|
|
89
|
-
Log.debug(data, `Pepr store data received`);
|
|
90
88
|
this.#store = data || {};
|
|
91
89
|
|
|
92
90
|
this.#onReady();
|
|
@@ -107,10 +105,11 @@ export class Storage implements PeprStore {
|
|
|
107
105
|
};
|
|
108
106
|
|
|
109
107
|
clear = () => {
|
|
110
|
-
this.#
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
108
|
+
Object.keys(this.#store).length > 0 &&
|
|
109
|
+
this.#dispatchUpdate(
|
|
110
|
+
"remove",
|
|
111
|
+
Object.keys(this.#store).map(key => pointer.escape(key)),
|
|
112
|
+
);
|
|
114
113
|
};
|
|
115
114
|
|
|
116
115
|
removeItem = (key: string) => {
|
package/src/lib/types.ts
CHANGED
|
@@ -2,11 +2,20 @@
|
|
|
2
2
|
// SPDX-FileCopyrightText: 2023-Present The Pepr Authors
|
|
3
3
|
|
|
4
4
|
import { GenericClass, GroupVersionKind, KubernetesObject } from "kubernetes-fluent-client";
|
|
5
|
-
|
|
5
|
+
|
|
6
|
+
import { WatchPhase } from "kubernetes-fluent-client/dist/fluent/types";
|
|
6
7
|
|
|
7
8
|
import { PeprMutateRequest } from "./mutate-request";
|
|
8
9
|
import { PeprValidateRequest } from "./validate-request";
|
|
9
10
|
|
|
11
|
+
import { Logger } from "pino";
|
|
12
|
+
|
|
13
|
+
export enum Operation {
|
|
14
|
+
CREATE = "CREATE",
|
|
15
|
+
UPDATE = "UPDATE",
|
|
16
|
+
DELETE = "DELETE",
|
|
17
|
+
CONNECT = "CONNECT",
|
|
18
|
+
}
|
|
10
19
|
/**
|
|
11
20
|
* Specifically for deploying images with a private registry
|
|
12
21
|
*/
|
|
@@ -80,25 +89,33 @@ export type WhenSelector<T extends GenericClass> = {
|
|
|
80
89
|
/** Register an action to be executed when a Kubernetes resource is deleted. */
|
|
81
90
|
IsDeleted: () => BindingAll<T>;
|
|
82
91
|
};
|
|
83
|
-
|
|
92
|
+
export interface RegExpFilter {
|
|
93
|
+
obj: RegExp;
|
|
94
|
+
source: string;
|
|
95
|
+
}
|
|
84
96
|
export type Binding = {
|
|
85
97
|
event: Event;
|
|
86
98
|
isMutate?: boolean;
|
|
87
99
|
isValidate?: boolean;
|
|
88
100
|
isWatch?: boolean;
|
|
89
101
|
isQueue?: boolean;
|
|
102
|
+
isFinalize?: boolean;
|
|
90
103
|
readonly model: GenericClass;
|
|
91
104
|
readonly kind: GroupVersionKind;
|
|
92
105
|
readonly filters: {
|
|
93
106
|
name: string;
|
|
107
|
+
regexName: string;
|
|
94
108
|
namespaces: string[];
|
|
109
|
+
regexNamespaces: string[];
|
|
95
110
|
labels: Record<string, string>;
|
|
96
111
|
annotations: Record<string, string>;
|
|
97
112
|
deletionTimestamp: boolean;
|
|
98
113
|
};
|
|
114
|
+
alias?: string;
|
|
99
115
|
readonly mutateCallback?: MutateAction<GenericClass, InstanceType<GenericClass>>;
|
|
100
116
|
readonly validateCallback?: ValidateAction<GenericClass, InstanceType<GenericClass>>;
|
|
101
|
-
readonly watchCallback?:
|
|
117
|
+
readonly watchCallback?: WatchLogAction<GenericClass, InstanceType<GenericClass>>;
|
|
118
|
+
readonly finalizeCallback?: FinalizeAction<GenericClass, InstanceType<GenericClass>>;
|
|
102
119
|
};
|
|
103
120
|
|
|
104
121
|
export type BindingFilter<T extends GenericClass> = CommonActionChain<T> & {
|
|
@@ -145,11 +162,15 @@ export type BindingFilter<T extends GenericClass> = CommonActionChain<T> & {
|
|
|
145
162
|
export type BindingWithName<T extends GenericClass> = BindingFilter<T> & {
|
|
146
163
|
/** Only apply the action if the resource name matches the specified name. */
|
|
147
164
|
WithName: (name: string) => BindingFilter<T>;
|
|
165
|
+
/** Only apply the action if the resource name matches the specified regex name. */
|
|
166
|
+
WithNameRegex: (name: RegExp) => BindingFilter<T>;
|
|
148
167
|
};
|
|
149
168
|
|
|
150
169
|
export type BindingAll<T extends GenericClass> = BindingWithName<T> & {
|
|
151
170
|
/** Only apply the action if the resource is in one of the specified namespaces.*/
|
|
152
171
|
InNamespace: (...namespaces: string[]) => BindingWithName<T>;
|
|
172
|
+
/** Only apply the action if the resource is in one of the specified regex namespaces.*/
|
|
173
|
+
InNamespaceRegex: (...namespaces: RegExp[]) => BindingWithName<T>;
|
|
153
174
|
};
|
|
154
175
|
|
|
155
176
|
export type CommonActionChain<T extends GenericClass> = MutateActionChain<T> & {
|
|
@@ -162,6 +183,7 @@ export type CommonActionChain<T extends GenericClass> = MutateActionChain<T> & {
|
|
|
162
183
|
* @param action The action to be executed when the Kubernetes resource is processed by the AdmissionController.
|
|
163
184
|
*/
|
|
164
185
|
Mutate: (action: MutateAction<T, InstanceType<T>>) => MutateActionChain<T>;
|
|
186
|
+
Alias: (alias: string) => BindingFilter<T>;
|
|
165
187
|
};
|
|
166
188
|
|
|
167
189
|
export type ValidateActionChain<T extends GenericClass> = {
|
|
@@ -176,7 +198,8 @@ export type ValidateActionChain<T extends GenericClass> = {
|
|
|
176
198
|
* @param action
|
|
177
199
|
* @returns
|
|
178
200
|
*/
|
|
179
|
-
|
|
201
|
+
|
|
202
|
+
Watch: (action: WatchLogAction<T, InstanceType<T>>) => FinalizeActionChain<T>;
|
|
180
203
|
|
|
181
204
|
/**
|
|
182
205
|
* Establish a reconcile for the specified resource. The callback function will be executed after the admission controller has
|
|
@@ -189,7 +212,8 @@ export type ValidateActionChain<T extends GenericClass> = {
|
|
|
189
212
|
* @param action
|
|
190
213
|
* @returns
|
|
191
214
|
*/
|
|
192
|
-
|
|
215
|
+
|
|
216
|
+
Reconcile: (action: WatchLogAction<T, InstanceType<T>>) => FinalizeActionChain<T>;
|
|
193
217
|
};
|
|
194
218
|
|
|
195
219
|
export type MutateActionChain<T extends GenericClass> = ValidateActionChain<T> & {
|
|
@@ -219,14 +243,133 @@ export type MutateActionChain<T extends GenericClass> = ValidateActionChain<T> &
|
|
|
219
243
|
|
|
220
244
|
export type MutateAction<T extends GenericClass, K extends KubernetesObject = InstanceType<T>> = (
|
|
221
245
|
req: PeprMutateRequest<K>,
|
|
246
|
+
logger?: Logger,
|
|
222
247
|
) => Promise<void> | void | Promise<PeprMutateRequest<K>> | PeprMutateRequest<K>;
|
|
223
248
|
|
|
224
249
|
export type ValidateAction<T extends GenericClass, K extends KubernetesObject = InstanceType<T>> = (
|
|
225
250
|
req: PeprValidateRequest<K>,
|
|
251
|
+
logger?: Logger,
|
|
226
252
|
) => Promise<ValidateActionResponse> | ValidateActionResponse;
|
|
227
253
|
|
|
254
|
+
// Define WatchLogAction by adding an optional logger parameter to the WatchAction
|
|
255
|
+
export type WatchLogAction<T extends GenericClass, K extends KubernetesObject = InstanceType<T>> = (
|
|
256
|
+
update: K,
|
|
257
|
+
phase: WatchPhase,
|
|
258
|
+
logger?: Logger,
|
|
259
|
+
) => Promise<void> | void;
|
|
260
|
+
|
|
228
261
|
export type ValidateActionResponse = {
|
|
229
262
|
allowed: boolean;
|
|
230
263
|
statusCode?: number;
|
|
231
264
|
statusMessage?: string;
|
|
232
265
|
};
|
|
266
|
+
|
|
267
|
+
export type FinalizeAction<T extends GenericClass, K extends KubernetesObject = InstanceType<T>> = (
|
|
268
|
+
update: K,
|
|
269
|
+
logger?: Logger,
|
|
270
|
+
) => Promise<void> | void;
|
|
271
|
+
|
|
272
|
+
export type FinalizeActionChain<T extends GenericClass> = {
|
|
273
|
+
/**
|
|
274
|
+
* Establish a finalizer for the specified resource. The callback given will be executed by the watch
|
|
275
|
+
* controller after it has received notification of an update adding a deletionTimestamp.
|
|
276
|
+
*
|
|
277
|
+
* **Beta Function**: This method is still in early testing and edge cases may still exist.
|
|
278
|
+
*
|
|
279
|
+
* @since 0.35.0
|
|
280
|
+
*
|
|
281
|
+
* @param action
|
|
282
|
+
* @returns
|
|
283
|
+
*/
|
|
284
|
+
Finalize: (action: FinalizeAction<T, InstanceType<T>>) => void;
|
|
285
|
+
};
|
|
286
|
+
|
|
287
|
+
/**
|
|
288
|
+
* A Kubernetes admission request to be processed by a capability.
|
|
289
|
+
*/
|
|
290
|
+
export interface AdmissionRequest<T = KubernetesObject> {
|
|
291
|
+
/** UID is an identifier for the individual request/response. */
|
|
292
|
+
readonly uid: string;
|
|
293
|
+
|
|
294
|
+
/** Kind is the fully-qualified type of object being submitted (for example, v1.Pod or autoscaling.v1.Scale) */
|
|
295
|
+
readonly kind: GroupVersionKind;
|
|
296
|
+
|
|
297
|
+
/** Resource is the fully-qualified resource being requested (for example, v1.pods) */
|
|
298
|
+
readonly resource: GroupVersionResource;
|
|
299
|
+
|
|
300
|
+
/** SubResource is the sub-resource being requested, if any (for example, "status" or "scale") */
|
|
301
|
+
readonly subResource?: string;
|
|
302
|
+
|
|
303
|
+
/** RequestKind is the fully-qualified type of the original API request (for example, v1.Pod or autoscaling.v1.Scale). */
|
|
304
|
+
readonly requestKind?: GroupVersionKind;
|
|
305
|
+
|
|
306
|
+
/** RequestResource is the fully-qualified resource of the original API request (for example, v1.pods). */
|
|
307
|
+
readonly requestResource?: GroupVersionResource;
|
|
308
|
+
|
|
309
|
+
/** RequestSubResource is the sub-resource of the original API request, if any (for example, "status" or "scale"). */
|
|
310
|
+
readonly requestSubResource?: string;
|
|
311
|
+
|
|
312
|
+
/**
|
|
313
|
+
* Name is the name of the object as presented in the request. On a CREATE operation, the client may omit name and
|
|
314
|
+
* rely on the server to generate the name. If that is the case, this method will return the empty string.
|
|
315
|
+
*/
|
|
316
|
+
readonly name: string;
|
|
317
|
+
|
|
318
|
+
/** Namespace is the namespace associated with the request (if any). */
|
|
319
|
+
readonly namespace?: string;
|
|
320
|
+
|
|
321
|
+
/**
|
|
322
|
+
* Operation is the operation being performed. This may be different than the operation
|
|
323
|
+
* requested. e.g. a patch can result in either a CREATE or UPDATE Operation.
|
|
324
|
+
*/
|
|
325
|
+
readonly operation: Operation;
|
|
326
|
+
|
|
327
|
+
/** UserInfo is information about the requesting user */
|
|
328
|
+
readonly userInfo: {
|
|
329
|
+
/** The name that uniquely identifies this user among all active users. */
|
|
330
|
+
username?: string;
|
|
331
|
+
|
|
332
|
+
/**
|
|
333
|
+
* A unique value that identifies this user across time. If this user is deleted
|
|
334
|
+
* and another user by the same name is added, they will have different UIDs.
|
|
335
|
+
*/
|
|
336
|
+
uid?: string;
|
|
337
|
+
|
|
338
|
+
/** The names of groups this user is a part of. */
|
|
339
|
+
groups?: string[];
|
|
340
|
+
|
|
341
|
+
/** Any additional information provided by the authenticator. */
|
|
342
|
+
extra?: {
|
|
343
|
+
[key: string]: string[];
|
|
344
|
+
};
|
|
345
|
+
};
|
|
346
|
+
|
|
347
|
+
/** Object is the object from the incoming request prior to default values being applied */
|
|
348
|
+
readonly object: T;
|
|
349
|
+
|
|
350
|
+
/** OldObject is the existing object. Only populated for UPDATE or DELETE requests. */
|
|
351
|
+
readonly oldObject?: T;
|
|
352
|
+
|
|
353
|
+
/** DryRun indicates that modifications will definitely not be persisted for this request. Defaults to false. */
|
|
354
|
+
readonly dryRun?: boolean;
|
|
355
|
+
|
|
356
|
+
/**
|
|
357
|
+
* Options contains the options for the operation being performed.
|
|
358
|
+
* e.g. `meta.k8s.io/v1.DeleteOptions` or `meta.k8s.io/v1.CreateOptions`. This may be
|
|
359
|
+
* different than the options the caller provided. e.g. for a patch request the performed
|
|
360
|
+
* Operation might be a CREATE, in which case the Options will a
|
|
361
|
+
* `meta.k8s.io/v1.CreateOptions` even though the caller provided `meta.k8s.io/v1.PatchOptions`.
|
|
362
|
+
*/
|
|
363
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
364
|
+
readonly options?: any;
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
/**
|
|
368
|
+
* GroupVersionResource unambiguously identifies a resource. It doesn't anonymously include GroupVersion
|
|
369
|
+
* to avoid automatic coercion. It doesn't use a GroupVersion to avoid custom marshalling
|
|
370
|
+
*/
|
|
371
|
+
export interface GroupVersionResource {
|
|
372
|
+
readonly group: string;
|
|
373
|
+
readonly version: string;
|
|
374
|
+
readonly resource: string;
|
|
375
|
+
}
|