pepr 0.0.9 → 0.1.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/README.md +40 -32
- package/dist/pepr-cli.js +281 -29
- package/dist/pepr-cli.js.map +1 -1
- package/index.ts +2 -0
- package/package.json +14 -3
- package/src/lib/capability.ts +158 -0
- package/src/lib/filter.test.ts +211 -0
- package/src/lib/filter.ts +55 -0
- package/src/lib/index.ts +10 -0
- package/src/lib/k8s/index.ts +10 -0
- package/src/lib/k8s/kinds.test.ts +341 -0
- package/src/lib/k8s/kinds.ts +470 -0
- package/src/lib/k8s/stub-tls.ts +88 -0
- package/src/lib/k8s/types.ts +175 -0
- package/src/lib/k8s/upstream.ts +47 -0
- package/src/lib/k8s/webhook.ts +278 -0
- package/src/lib/logger.test.ts +80 -0
- package/src/lib/logger.ts +131 -0
- package/src/lib/module.ts +64 -0
- package/src/lib/processor.ts +83 -0
- package/src/lib/request.ts +140 -0
- package/src/lib/types.ts +212 -0
- package/tsconfig.json +1 -1
- package/.eslintrc.json +0 -16
- package/.prettierrc +0 -14
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
// SPDX-License-Identifier: Apache-2.0
|
|
2
|
+
// SPDX-FileCopyrightText: 2023-Present The Pepr Authors
|
|
3
|
+
|
|
4
|
+
import { modelToGroupVersionKind } from "./k8s";
|
|
5
|
+
import logger from "./logger";
|
|
6
|
+
import {
|
|
7
|
+
Binding,
|
|
8
|
+
BindingFilter,
|
|
9
|
+
BindingWithName,
|
|
10
|
+
BindToAction,
|
|
11
|
+
CapabilityAction,
|
|
12
|
+
CapabilityCfg,
|
|
13
|
+
DeepPartial,
|
|
14
|
+
Event,
|
|
15
|
+
GenericClass,
|
|
16
|
+
HookPhase,
|
|
17
|
+
WhenSelector,
|
|
18
|
+
} from "./types";
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* A capability is a unit of functionality that can be registered with the Pepr runtime.
|
|
22
|
+
*/
|
|
23
|
+
export class Capability implements CapabilityCfg {
|
|
24
|
+
private _name: string;
|
|
25
|
+
private _description: string;
|
|
26
|
+
private _namespaces?: string[] | undefined;
|
|
27
|
+
|
|
28
|
+
// Currently everything is considered a mutation
|
|
29
|
+
private _mutateOrValidate = HookPhase.mutate;
|
|
30
|
+
|
|
31
|
+
private _bindings: Binding[] = [];
|
|
32
|
+
|
|
33
|
+
get bindings(): Binding[] {
|
|
34
|
+
return this._bindings;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
get name() {
|
|
38
|
+
return this._name;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
get description() {
|
|
42
|
+
return this._description;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
get namespaces() {
|
|
46
|
+
return this._namespaces || [];
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
get mutateOrValidate() {
|
|
50
|
+
return this._mutateOrValidate;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
constructor(cfg: CapabilityCfg) {
|
|
54
|
+
this._name = cfg.name;
|
|
55
|
+
this._description = cfg.description;
|
|
56
|
+
this._namespaces = cfg.namespaces;
|
|
57
|
+
logger.info(`Capability ${this._name} registered`);
|
|
58
|
+
logger.debug(cfg);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* The Register method is used to register a capability with the Pepr runtime. This method is
|
|
63
|
+
* called in the order that the capabilities should be executed.
|
|
64
|
+
*
|
|
65
|
+
* @param callback the state register method to call, passing the capability as an argument
|
|
66
|
+
*/
|
|
67
|
+
Register = (register: (capability: Capability) => void) => register(this);
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* The When method is used to register a capability action to be executed when a Kubernetes resource is
|
|
71
|
+
* processed by Pepr. The action will be executed if the resource matches the specified kind and any
|
|
72
|
+
* filters that are applied.
|
|
73
|
+
*
|
|
74
|
+
* @param model if using a custom KubernetesObject not available in `a.*`, specify the GroupVersionKind
|
|
75
|
+
* @returns
|
|
76
|
+
*/
|
|
77
|
+
When = <T extends GenericClass>(model: T): WhenSelector<T> => {
|
|
78
|
+
const binding: Binding = {
|
|
79
|
+
// If the kind is not specified, use the default KubernetesObject
|
|
80
|
+
kind: modelToGroupVersionKind(model.name),
|
|
81
|
+
filters: {
|
|
82
|
+
name: "",
|
|
83
|
+
namespaces: [],
|
|
84
|
+
labels: {},
|
|
85
|
+
annotations: {},
|
|
86
|
+
},
|
|
87
|
+
callback: () => null,
|
|
88
|
+
};
|
|
89
|
+
|
|
90
|
+
const prefix = `${this._name}: ${model.name}`;
|
|
91
|
+
|
|
92
|
+
logger.info(`Binding created`, prefix);
|
|
93
|
+
|
|
94
|
+
const Then = (cb: CapabilityAction<T>): BindToAction<T> => {
|
|
95
|
+
logger.info(`Binding action created`, prefix);
|
|
96
|
+
logger.debug(cb.toString(), prefix);
|
|
97
|
+
// Push the binding to the list of bindings for this capability as a new BindingAction
|
|
98
|
+
// with the callback function to preserve
|
|
99
|
+
this._bindings.push({
|
|
100
|
+
...binding,
|
|
101
|
+
callback: cb,
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
// Now only allow adding actions to the same binding
|
|
105
|
+
return { Then };
|
|
106
|
+
};
|
|
107
|
+
|
|
108
|
+
const ThenSet = (merge: DeepPartial<InstanceType<T>>): BindToAction<T> => {
|
|
109
|
+
// Add the new action to the binding
|
|
110
|
+
Then(req => req.Merge(merge));
|
|
111
|
+
|
|
112
|
+
return { Then };
|
|
113
|
+
};
|
|
114
|
+
|
|
115
|
+
function InNamespace(...namespaces: string[]): BindingWithName<T> {
|
|
116
|
+
logger.debug(`Add namespaces filter ${namespaces}`, prefix);
|
|
117
|
+
binding.filters.namespaces.push(...namespaces);
|
|
118
|
+
return { WithLabel, WithAnnotation, WithName, Then, ThenSet };
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
function WithName(name: string): BindingFilter<T> {
|
|
122
|
+
logger.debug(`Add name filter ${name}`, prefix);
|
|
123
|
+
binding.filters.name = name;
|
|
124
|
+
return { WithLabel, WithAnnotation, Then, ThenSet };
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
function WithLabel(key: string, value = ""): BindingFilter<T> {
|
|
128
|
+
logger.debug(`Add label filter ${key}=${value}`, prefix);
|
|
129
|
+
binding.filters.labels[key] = value;
|
|
130
|
+
return { WithLabel, WithAnnotation, Then, ThenSet };
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
const WithAnnotation = (key: string, value = ""): BindingFilter<T> => {
|
|
134
|
+
logger.debug(`Add annotation filter ${key}=${value}`, prefix);
|
|
135
|
+
binding.filters.annotations[key] = value;
|
|
136
|
+
return { WithLabel, WithAnnotation, Then, ThenSet };
|
|
137
|
+
};
|
|
138
|
+
|
|
139
|
+
const bindEvent = (event: Event) => {
|
|
140
|
+
binding.event = event;
|
|
141
|
+
return {
|
|
142
|
+
InNamespace,
|
|
143
|
+
Then,
|
|
144
|
+
ThenSet,
|
|
145
|
+
WithAnnotation,
|
|
146
|
+
WithLabel,
|
|
147
|
+
WithName,
|
|
148
|
+
};
|
|
149
|
+
};
|
|
150
|
+
|
|
151
|
+
return {
|
|
152
|
+
IsCreatedOrUpdated: () => bindEvent(Event.CreateOrUpdate),
|
|
153
|
+
IsCreated: () => bindEvent(Event.Create),
|
|
154
|
+
IsUpdated: () => bindEvent(Event.Update),
|
|
155
|
+
IsDeleted: () => bindEvent(Event.Delete),
|
|
156
|
+
};
|
|
157
|
+
};
|
|
158
|
+
}
|
|
@@ -0,0 +1,211 @@
|
|
|
1
|
+
import { gvkMap } from "@k8s";
|
|
2
|
+
import test from "ava";
|
|
3
|
+
import { POD1 } from "@fixtures/loader";
|
|
4
|
+
import { shouldSkipRequest } from "./filter";
|
|
5
|
+
|
|
6
|
+
test("should reject when kind does not match", t => {
|
|
7
|
+
const binding = {
|
|
8
|
+
kind: gvkMap.V1ConfigMap,
|
|
9
|
+
filters: {
|
|
10
|
+
name: "",
|
|
11
|
+
namespaces: [],
|
|
12
|
+
labels: {},
|
|
13
|
+
annotations: {},
|
|
14
|
+
},
|
|
15
|
+
callback: () => null,
|
|
16
|
+
};
|
|
17
|
+
const pod = POD1();
|
|
18
|
+
|
|
19
|
+
t.true(shouldSkipRequest(binding, pod));
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
test("should reject when group does not match", t => {
|
|
23
|
+
const binding = {
|
|
24
|
+
kind: gvkMap.V1CronJob,
|
|
25
|
+
filters: {
|
|
26
|
+
name: "",
|
|
27
|
+
namespaces: [],
|
|
28
|
+
labels: {},
|
|
29
|
+
annotations: {},
|
|
30
|
+
},
|
|
31
|
+
callback: () => null,
|
|
32
|
+
};
|
|
33
|
+
const pod = POD1();
|
|
34
|
+
|
|
35
|
+
t.true(shouldSkipRequest(binding, pod));
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
test("should reject when version does not match", t => {
|
|
39
|
+
const binding = {
|
|
40
|
+
kind: {
|
|
41
|
+
group: "",
|
|
42
|
+
version: "v2",
|
|
43
|
+
kind: "Pod",
|
|
44
|
+
},
|
|
45
|
+
filters: {
|
|
46
|
+
name: "",
|
|
47
|
+
namespaces: [],
|
|
48
|
+
labels: {},
|
|
49
|
+
annotations: {},
|
|
50
|
+
},
|
|
51
|
+
callback: () => null,
|
|
52
|
+
};
|
|
53
|
+
const pod = POD1();
|
|
54
|
+
|
|
55
|
+
t.true(shouldSkipRequest(binding, pod));
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
test("should allow when group, version, and kind match", t => {
|
|
59
|
+
const binding = {
|
|
60
|
+
kind: gvkMap.V1Pod,
|
|
61
|
+
filters: {
|
|
62
|
+
name: "",
|
|
63
|
+
namespaces: [],
|
|
64
|
+
labels: {},
|
|
65
|
+
annotations: {},
|
|
66
|
+
},
|
|
67
|
+
callback: () => null,
|
|
68
|
+
};
|
|
69
|
+
const pod = POD1();
|
|
70
|
+
|
|
71
|
+
t.false(shouldSkipRequest(binding, pod));
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
test("should allow when kind match and others are empty", t => {
|
|
75
|
+
const binding = {
|
|
76
|
+
kind: {
|
|
77
|
+
group: "",
|
|
78
|
+
version: "",
|
|
79
|
+
kind: "Pod",
|
|
80
|
+
},
|
|
81
|
+
filters: {
|
|
82
|
+
name: "",
|
|
83
|
+
namespaces: [],
|
|
84
|
+
labels: {},
|
|
85
|
+
annotations: {},
|
|
86
|
+
},
|
|
87
|
+
callback: () => null,
|
|
88
|
+
};
|
|
89
|
+
const pod = POD1();
|
|
90
|
+
|
|
91
|
+
t.false(shouldSkipRequest(binding, pod));
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
test("should reject when namespace does not match", t => {
|
|
95
|
+
const binding = {
|
|
96
|
+
kind: gvkMap.V1Pod,
|
|
97
|
+
filters: {
|
|
98
|
+
name: "",
|
|
99
|
+
namespaces: ["bleh"],
|
|
100
|
+
labels: {},
|
|
101
|
+
annotations: {},
|
|
102
|
+
},
|
|
103
|
+
callback: () => null,
|
|
104
|
+
};
|
|
105
|
+
const pod = POD1();
|
|
106
|
+
|
|
107
|
+
t.true(shouldSkipRequest(binding, pod));
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
test("should allow when namespace is match", t => {
|
|
111
|
+
const binding = {
|
|
112
|
+
kind: gvkMap.V1Pod,
|
|
113
|
+
filters: {
|
|
114
|
+
name: "",
|
|
115
|
+
namespaces: ["default", "unicorn", "things"],
|
|
116
|
+
labels: {},
|
|
117
|
+
annotations: {},
|
|
118
|
+
},
|
|
119
|
+
callback: () => null,
|
|
120
|
+
};
|
|
121
|
+
const pod = POD1();
|
|
122
|
+
|
|
123
|
+
t.false(shouldSkipRequest(binding, pod));
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
test("should reject when label does not match", t => {
|
|
127
|
+
const binding = {
|
|
128
|
+
kind: gvkMap.V1Pod,
|
|
129
|
+
filters: {
|
|
130
|
+
name: "",
|
|
131
|
+
namespaces: [],
|
|
132
|
+
labels: {
|
|
133
|
+
foo: "bar",
|
|
134
|
+
},
|
|
135
|
+
annotations: {},
|
|
136
|
+
},
|
|
137
|
+
callback: () => null,
|
|
138
|
+
};
|
|
139
|
+
const pod = POD1();
|
|
140
|
+
|
|
141
|
+
t.true(shouldSkipRequest(binding, pod));
|
|
142
|
+
});
|
|
143
|
+
|
|
144
|
+
test("should allow when label is match", t => {
|
|
145
|
+
const binding = {
|
|
146
|
+
kind: gvkMap.V1Pod,
|
|
147
|
+
filters: {
|
|
148
|
+
name: "",
|
|
149
|
+
|
|
150
|
+
namespaces: [],
|
|
151
|
+
labels: {
|
|
152
|
+
foo: "bar",
|
|
153
|
+
test: "test1",
|
|
154
|
+
},
|
|
155
|
+
annotations: {},
|
|
156
|
+
},
|
|
157
|
+
callback: () => null,
|
|
158
|
+
};
|
|
159
|
+
|
|
160
|
+
const pod = POD1();
|
|
161
|
+
pod.object.metadata.labels = {
|
|
162
|
+
foo: "bar",
|
|
163
|
+
test: "test1",
|
|
164
|
+
test2: "test2",
|
|
165
|
+
};
|
|
166
|
+
|
|
167
|
+
t.false(shouldSkipRequest(binding, pod));
|
|
168
|
+
});
|
|
169
|
+
|
|
170
|
+
test("should reject when annotation does not match", t => {
|
|
171
|
+
const binding = {
|
|
172
|
+
kind: gvkMap.V1Pod,
|
|
173
|
+
filters: {
|
|
174
|
+
name: "",
|
|
175
|
+
namespaces: [],
|
|
176
|
+
labels: {},
|
|
177
|
+
annotations: {
|
|
178
|
+
foo: "bar",
|
|
179
|
+
},
|
|
180
|
+
},
|
|
181
|
+
callback: () => null,
|
|
182
|
+
};
|
|
183
|
+
const pod = POD1();
|
|
184
|
+
|
|
185
|
+
t.true(shouldSkipRequest(binding, pod));
|
|
186
|
+
});
|
|
187
|
+
|
|
188
|
+
test("should allow when annotation is match", t => {
|
|
189
|
+
const binding = {
|
|
190
|
+
kind: gvkMap.V1Pod,
|
|
191
|
+
filters: {
|
|
192
|
+
name: "",
|
|
193
|
+
namespaces: [],
|
|
194
|
+
labels: {},
|
|
195
|
+
annotations: {
|
|
196
|
+
foo: "bar",
|
|
197
|
+
test: "test1",
|
|
198
|
+
},
|
|
199
|
+
},
|
|
200
|
+
callback: () => null,
|
|
201
|
+
};
|
|
202
|
+
|
|
203
|
+
const pod = POD1();
|
|
204
|
+
pod.object.metadata.annotations = {
|
|
205
|
+
foo: "bar",
|
|
206
|
+
test: "test1",
|
|
207
|
+
test2: "test2",
|
|
208
|
+
};
|
|
209
|
+
|
|
210
|
+
t.false(shouldSkipRequest(binding, pod));
|
|
211
|
+
});
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
// SPDX-License-Identifier: Apache-2.0
|
|
2
|
+
// SPDX-FileCopyrightText: 2023-Present The Pepr Authors
|
|
3
|
+
|
|
4
|
+
import { Request } from "./k8s";
|
|
5
|
+
import logger from "./logger";
|
|
6
|
+
import { Binding } from "./types";
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* shouldSkipRequest determines if a request should be skipped based on the binding filters.
|
|
10
|
+
*
|
|
11
|
+
* @param binding the capability action binding
|
|
12
|
+
* @param req the incoming request
|
|
13
|
+
* @returns
|
|
14
|
+
*/
|
|
15
|
+
export function shouldSkipRequest(binding: Binding, req: Request) {
|
|
16
|
+
const { group, kind, version } = binding.kind;
|
|
17
|
+
const { namespaces, labels, annotations } = binding.filters;
|
|
18
|
+
const { metadata } = req.object;
|
|
19
|
+
|
|
20
|
+
if (kind !== req.kind.kind) {
|
|
21
|
+
logger.debug(`${req.kind.kind} does not match ${kind}`);
|
|
22
|
+
return true;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
if (group && group !== req.kind.group) {
|
|
26
|
+
logger.debug(`${req.kind.group} does not match ${group}`);
|
|
27
|
+
return true;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
if (version && version !== req.kind.version) {
|
|
31
|
+
logger.debug(`${req.kind.version} does not match ${version}`);
|
|
32
|
+
return true;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
if (namespaces.length && !namespaces.includes(req.namespace || "")) {
|
|
36
|
+
logger.debug(`${req.namespace} is not in ${namespaces}`);
|
|
37
|
+
return true;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
for (const [key, value] of Object.entries(labels)) {
|
|
41
|
+
if (metadata?.labels?.[key] !== value) {
|
|
42
|
+
logger.debug(`${metadata?.labels?.[key]} does not match ${value}`);
|
|
43
|
+
return true;
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
for (const [key, value] of Object.entries(annotations)) {
|
|
48
|
+
if (metadata?.annotations?.[key] !== value) {
|
|
49
|
+
logger.debug(`${metadata?.annotations?.[key]} does not match ${value}`);
|
|
50
|
+
return true;
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
return false;
|
|
55
|
+
}
|
package/src/lib/index.ts
ADDED
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
// SPDX-License-Identifier: Apache-2.0
|
|
2
|
+
// SPDX-FileCopyrightText: 2023-Present The Pepr Authors
|
|
3
|
+
|
|
4
|
+
import logger from "./logger";
|
|
5
|
+
|
|
6
|
+
export { logger as Log };
|
|
7
|
+
export * from "./capability";
|
|
8
|
+
export * from "./request";
|
|
9
|
+
export * from "./module";
|
|
10
|
+
export * from "./types";
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
// SPDX-License-Identifier: Apache-2.0
|
|
2
|
+
// SPDX-FileCopyrightText: 2023-Present The Pepr Authors
|
|
3
|
+
|
|
4
|
+
// Export kinds as a single object
|
|
5
|
+
import * as kind from "./upstream";
|
|
6
|
+
export { kind as a };
|
|
7
|
+
|
|
8
|
+
export { modelToGroupVersionKind, gvkMap } from "./kinds";
|
|
9
|
+
|
|
10
|
+
export * from "./types";
|