pepr 0.0.8 → 0.1.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.
@@ -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
+ }
@@ -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";