pepr 0.36.0 → 0.37.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/init/index.d.ts.map +1 -1
- package/dist/cli/init/templates.d.ts +3 -1
- 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.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 +115 -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 +9 -7
- 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.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 +151 -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
|
@@ -0,0 +1,655 @@
|
|
|
1
|
+
import { Capability } from "./capability";
|
|
2
|
+
import Log from "./logger";
|
|
3
|
+
import { CapabilityCfg, FinalizeAction, MutateAction, ValidateAction, WatchLogAction } from "./types";
|
|
4
|
+
import { a } from "../lib";
|
|
5
|
+
import { V1Pod } from "@kubernetes/client-node";
|
|
6
|
+
import { expect, describe, jest, beforeEach, it } from "@jest/globals";
|
|
7
|
+
import { PeprMutateRequest } from "./mutate-request";
|
|
8
|
+
import { PeprValidateRequest } from "./validate-request";
|
|
9
|
+
import { Operation, AdmissionRequest } from "./k8s";
|
|
10
|
+
import { WatchPhase } from "kubernetes-fluent-client/dist/fluent/types";
|
|
11
|
+
import { Event } from "./types";
|
|
12
|
+
import { GenericClass } from "kubernetes-fluent-client";
|
|
13
|
+
import { Schedule } from "./schedule";
|
|
14
|
+
import { OnSchedule } from "./schedule";
|
|
15
|
+
|
|
16
|
+
// Mocking isBuildMode, isWatchMode, and isDevMode globally
|
|
17
|
+
jest.mock("./module", () => ({
|
|
18
|
+
isBuildMode: jest.fn(() => true),
|
|
19
|
+
isWatchMode: jest.fn(() => true),
|
|
20
|
+
isDevMode: jest.fn(() => true),
|
|
21
|
+
}));
|
|
22
|
+
|
|
23
|
+
// Mock logger globally
|
|
24
|
+
jest.mock("./logger", () => ({
|
|
25
|
+
__esModule: true,
|
|
26
|
+
default: {
|
|
27
|
+
info: jest.fn(),
|
|
28
|
+
debug: jest.fn(),
|
|
29
|
+
child: jest.fn().mockReturnThis(),
|
|
30
|
+
},
|
|
31
|
+
}));
|
|
32
|
+
|
|
33
|
+
// Mock Storage and OnSchedule
|
|
34
|
+
jest.mock("./storage", () => ({
|
|
35
|
+
Storage: jest.fn(() => ({
|
|
36
|
+
onReady: jest.fn(),
|
|
37
|
+
})),
|
|
38
|
+
}));
|
|
39
|
+
|
|
40
|
+
// Mock OnSchedule and ensure it has a mock setStore method
|
|
41
|
+
jest.mock("./schedule", () => ({
|
|
42
|
+
OnSchedule: jest.fn().mockImplementation(() => ({
|
|
43
|
+
setStore: jest.fn(), // Ensure setStore is a mocked function
|
|
44
|
+
})),
|
|
45
|
+
}));
|
|
46
|
+
|
|
47
|
+
const mockLog = Log as jest.Mocked<typeof Log>;
|
|
48
|
+
|
|
49
|
+
describe("Capability", () => {
|
|
50
|
+
let mockRequest: AdmissionRequest<V1Pod>;
|
|
51
|
+
|
|
52
|
+
beforeEach(() => {
|
|
53
|
+
jest.resetModules();
|
|
54
|
+
jest.clearAllMocks();
|
|
55
|
+
|
|
56
|
+
mockRequest = {
|
|
57
|
+
operation: Operation.CREATE,
|
|
58
|
+
object: {
|
|
59
|
+
apiVersion: "v1",
|
|
60
|
+
kind: "Pod",
|
|
61
|
+
metadata: {
|
|
62
|
+
name: "test-pod",
|
|
63
|
+
namespace: "default",
|
|
64
|
+
labels: {
|
|
65
|
+
"existing-label": "true",
|
|
66
|
+
},
|
|
67
|
+
annotations: {
|
|
68
|
+
"existing-annotation": "true",
|
|
69
|
+
},
|
|
70
|
+
},
|
|
71
|
+
spec: {
|
|
72
|
+
containers: [],
|
|
73
|
+
},
|
|
74
|
+
},
|
|
75
|
+
dryRun: false,
|
|
76
|
+
uid: "test-uid",
|
|
77
|
+
name: "test-pod",
|
|
78
|
+
kind: { group: "", version: "v1", kind: "Pod" },
|
|
79
|
+
resource: { group: "", version: "v1", resource: "pods" },
|
|
80
|
+
userInfo: { username: "test-user" },
|
|
81
|
+
oldObject: undefined,
|
|
82
|
+
};
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
const capabilityConfig: CapabilityCfg = {
|
|
86
|
+
name: "test-capability",
|
|
87
|
+
description: "Test capability description",
|
|
88
|
+
namespaces: ["default"],
|
|
89
|
+
};
|
|
90
|
+
|
|
91
|
+
it("should initialize with given configuration", () => {
|
|
92
|
+
const capability = new Capability(capabilityConfig);
|
|
93
|
+
expect(capability.name).toBe(capabilityConfig.name);
|
|
94
|
+
expect(capability.description).toBe(capabilityConfig.description);
|
|
95
|
+
expect(capability.namespaces).toEqual(capabilityConfig.namespaces);
|
|
96
|
+
expect(mockLog.info).toHaveBeenCalledWith(`Capability ${capabilityConfig.name} registered`);
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
it("should register store and schedule store", () => {
|
|
100
|
+
const capability = new Capability(capabilityConfig);
|
|
101
|
+
|
|
102
|
+
const storeResult = capability.registerStore();
|
|
103
|
+
expect(storeResult).toHaveProperty("store");
|
|
104
|
+
expect(mockLog.info).toHaveBeenCalledWith(`Registering store for ${capabilityConfig.name}`);
|
|
105
|
+
|
|
106
|
+
const scheduleStoreResult = capability.registerScheduleStore();
|
|
107
|
+
expect(scheduleStoreResult).toHaveProperty("scheduleStore");
|
|
108
|
+
expect(mockLog.info).toHaveBeenCalledWith(`Registering schedule store for ${capabilityConfig.name}`);
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
it("should throw an error if store is registered twice", () => {
|
|
112
|
+
const capability = new Capability(capabilityConfig);
|
|
113
|
+
|
|
114
|
+
capability.registerStore();
|
|
115
|
+
expect(() => capability.registerStore()).toThrowError("Store already registered for test-capability");
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
it("should throw an error if schedule store is registered twice", () => {
|
|
119
|
+
const capability = new Capability(capabilityConfig);
|
|
120
|
+
|
|
121
|
+
capability.registerScheduleStore();
|
|
122
|
+
expect(() => capability.registerScheduleStore()).toThrowError(
|
|
123
|
+
"Schedule store already registered for test-capability",
|
|
124
|
+
);
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
it("should correctly chain When, InNamespace, WithLabel, and Mutate methods", async () => {
|
|
128
|
+
const capability = new Capability(capabilityConfig);
|
|
129
|
+
|
|
130
|
+
const mockMutateCallback: MutateAction<typeof V1Pod, V1Pod> = jest.fn(
|
|
131
|
+
async (req: PeprMutateRequest<V1Pod>, logger: typeof Log = mockLog) => {
|
|
132
|
+
logger.info("Executing mutation action");
|
|
133
|
+
},
|
|
134
|
+
);
|
|
135
|
+
|
|
136
|
+
capability
|
|
137
|
+
.When(a.Pod)
|
|
138
|
+
.IsCreatedOrUpdated()
|
|
139
|
+
.InNamespace("default")
|
|
140
|
+
.WithLabel("test-label", "value")
|
|
141
|
+
.Alias("test-alias")
|
|
142
|
+
.Mutate(mockMutateCallback);
|
|
143
|
+
|
|
144
|
+
expect(capability.bindings).toHaveLength(1);
|
|
145
|
+
const binding = capability.bindings[0];
|
|
146
|
+
expect(binding.filters.namespaces).toContain("default");
|
|
147
|
+
expect(binding.filters.labels).toHaveProperty("test-label", "value");
|
|
148
|
+
expect(binding.alias).toBe("test-alias");
|
|
149
|
+
|
|
150
|
+
// Simulate the mutation action
|
|
151
|
+
const peprRequest = new PeprMutateRequest<V1Pod>(mockRequest);
|
|
152
|
+
|
|
153
|
+
if (binding.mutateCallback) {
|
|
154
|
+
await binding.mutateCallback(peprRequest);
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
expect(mockMutateCallback).toHaveBeenCalledWith(peprRequest, expect.anything());
|
|
158
|
+
expect(mockLog.child).toHaveBeenCalledWith({ alias: "test-alias" });
|
|
159
|
+
expect(mockLog.info).toHaveBeenCalledWith("Executing mutation action with alias: test-alias");
|
|
160
|
+
});
|
|
161
|
+
|
|
162
|
+
it("should use child logger for mutate callback", async () => {
|
|
163
|
+
const capability = new Capability(capabilityConfig);
|
|
164
|
+
|
|
165
|
+
const mockMutateCallback: MutateAction<typeof V1Pod, V1Pod> = jest.fn(
|
|
166
|
+
(req: PeprMutateRequest<V1Pod>, logger: typeof Log = mockLog) => {
|
|
167
|
+
logger.info("Mutate action log");
|
|
168
|
+
},
|
|
169
|
+
);
|
|
170
|
+
|
|
171
|
+
capability
|
|
172
|
+
.When(a.Pod)
|
|
173
|
+
.IsCreatedOrUpdated()
|
|
174
|
+
.InNamespace("default")
|
|
175
|
+
.WithLabel("test-label", "value")
|
|
176
|
+
.Alias("test-alias")
|
|
177
|
+
.Mutate(mockMutateCallback);
|
|
178
|
+
|
|
179
|
+
expect(capability.bindings).toHaveLength(1);
|
|
180
|
+
const binding = capability.bindings[0];
|
|
181
|
+
|
|
182
|
+
// Simulate the mutation action
|
|
183
|
+
const peprRequest = new PeprMutateRequest<V1Pod>(mockRequest);
|
|
184
|
+
|
|
185
|
+
if (binding.mutateCallback) {
|
|
186
|
+
await binding.mutateCallback(peprRequest);
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
expect(mockMutateCallback).toHaveBeenCalledWith(peprRequest, expect.anything());
|
|
190
|
+
expect(mockLog.child).toHaveBeenCalledWith({ alias: "test-alias" });
|
|
191
|
+
expect(mockLog.info).toHaveBeenCalledWith("Executing mutation action with alias: test-alias");
|
|
192
|
+
expect(mockLog.info).toHaveBeenCalledWith("Mutate action log");
|
|
193
|
+
});
|
|
194
|
+
|
|
195
|
+
it("should handle complex alias and logging correctly", async () => {
|
|
196
|
+
const complexCapabilityConfig: CapabilityCfg = {
|
|
197
|
+
name: "complex-capability",
|
|
198
|
+
description: "Test complex capability description",
|
|
199
|
+
namespaces: ["pepr-demo", "pepr-demo-2"],
|
|
200
|
+
};
|
|
201
|
+
|
|
202
|
+
const capability = new Capability(complexCapabilityConfig);
|
|
203
|
+
|
|
204
|
+
const mockMutateCallback: MutateAction<typeof V1Pod, V1Pod> = jest.fn(
|
|
205
|
+
async (po: PeprMutateRequest<V1Pod>, logger: typeof Log = mockLog) => {
|
|
206
|
+
logger.info(`SNAKES ON A PLANE! ${po.Raw.metadata?.name}`);
|
|
207
|
+
},
|
|
208
|
+
);
|
|
209
|
+
|
|
210
|
+
capability
|
|
211
|
+
.When(a.Pod)
|
|
212
|
+
.IsCreatedOrUpdated()
|
|
213
|
+
.InNamespace("pepr-demo")
|
|
214
|
+
.WithLabel("white")
|
|
215
|
+
.Alias("reject:pods:runAsRoot:privileged:runAsGroup<10:allowPrivilegeEscalation")
|
|
216
|
+
.Mutate(mockMutateCallback);
|
|
217
|
+
|
|
218
|
+
expect(capability.bindings).toHaveLength(1);
|
|
219
|
+
const binding = capability.bindings[0];
|
|
220
|
+
expect(binding.filters.namespaces).toContain("pepr-demo");
|
|
221
|
+
expect(binding.filters.labels).toHaveProperty("white", "");
|
|
222
|
+
expect(binding.alias).toBe("reject:pods:runAsRoot:privileged:runAsGroup<10:allowPrivilegeEscalation");
|
|
223
|
+
|
|
224
|
+
// Simulate the mutation action
|
|
225
|
+
const peprRequest = new PeprMutateRequest<V1Pod>(mockRequest);
|
|
226
|
+
|
|
227
|
+
if (binding.mutateCallback) {
|
|
228
|
+
await binding.mutateCallback(peprRequest);
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
expect(mockMutateCallback).toHaveBeenCalledWith(peprRequest, expect.anything());
|
|
232
|
+
expect(mockLog.child).toHaveBeenCalledWith({
|
|
233
|
+
alias: "reject:pods:runAsRoot:privileged:runAsGroup<10:allowPrivilegeEscalation",
|
|
234
|
+
});
|
|
235
|
+
expect(mockLog.info).toHaveBeenCalledWith(
|
|
236
|
+
"Executing mutation action with alias: reject:pods:runAsRoot:privileged:runAsGroup<10:allowPrivilegeEscalation",
|
|
237
|
+
);
|
|
238
|
+
expect(mockLog.info).toHaveBeenCalledWith(`SNAKES ON A PLANE! ${mockRequest.object.metadata?.name}`);
|
|
239
|
+
});
|
|
240
|
+
|
|
241
|
+
it("should reset the alias before each mutation", async () => {
|
|
242
|
+
const capability = new Capability(capabilityConfig);
|
|
243
|
+
|
|
244
|
+
const firstMutateCallback: MutateAction<typeof V1Pod, V1Pod> = jest.fn(
|
|
245
|
+
async (req: PeprMutateRequest<V1Pod>, logger: typeof Log = mockLog) => {
|
|
246
|
+
logger.info("First mutation action");
|
|
247
|
+
},
|
|
248
|
+
);
|
|
249
|
+
|
|
250
|
+
const secondMutateCallback: MutateAction<typeof V1Pod, V1Pod> = jest.fn(
|
|
251
|
+
async (req: PeprMutateRequest<V1Pod>, logger: typeof Log = mockLog) => {
|
|
252
|
+
logger.info("Second mutation action");
|
|
253
|
+
},
|
|
254
|
+
);
|
|
255
|
+
|
|
256
|
+
// First mutation with an alias
|
|
257
|
+
capability.When(a.Pod).IsCreatedOrUpdated().InNamespace("default").Alias("first-alias").Mutate(firstMutateCallback);
|
|
258
|
+
|
|
259
|
+
// Second mutation without an alias (should use "no alias provided")
|
|
260
|
+
capability.When(a.Pod).IsCreatedOrUpdated().InNamespace("default").Mutate(secondMutateCallback);
|
|
261
|
+
|
|
262
|
+
expect(capability.bindings).toHaveLength(2);
|
|
263
|
+
|
|
264
|
+
// Simulate the first mutation action
|
|
265
|
+
const peprRequest1 = new PeprMutateRequest<V1Pod>(mockRequest);
|
|
266
|
+
if (capability.bindings[0].mutateCallback) {
|
|
267
|
+
await capability.bindings[0].mutateCallback(peprRequest1);
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
expect(firstMutateCallback).toHaveBeenCalledWith(peprRequest1, expect.anything());
|
|
271
|
+
expect(mockLog.child).toHaveBeenCalledWith({ alias: "first-alias" });
|
|
272
|
+
expect(mockLog.info).toHaveBeenCalledWith("Executing mutation action with alias: first-alias");
|
|
273
|
+
|
|
274
|
+
// Simulate the second mutation action
|
|
275
|
+
const peprRequest2 = new PeprMutateRequest<V1Pod>(mockRequest);
|
|
276
|
+
if (capability.bindings[1].mutateCallback) {
|
|
277
|
+
await capability.bindings[1].mutateCallback(peprRequest2);
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
expect(secondMutateCallback).toHaveBeenCalledWith(peprRequest2, expect.anything());
|
|
281
|
+
expect(mockLog.child).toHaveBeenCalledWith({ alias: "no alias provided" });
|
|
282
|
+
expect(mockLog.info).toHaveBeenCalledWith("Executing mutation action with alias: no alias provided");
|
|
283
|
+
});
|
|
284
|
+
|
|
285
|
+
it("should use child logger for validate callback", async () => {
|
|
286
|
+
const capability = new Capability(capabilityConfig);
|
|
287
|
+
|
|
288
|
+
const mockValidateCallback: ValidateAction<typeof V1Pod, V1Pod> = jest.fn(
|
|
289
|
+
async (req: PeprValidateRequest<V1Pod>, logger: typeof Log = mockLog) => {
|
|
290
|
+
logger.info("Validate action log");
|
|
291
|
+
return { allowed: true };
|
|
292
|
+
},
|
|
293
|
+
);
|
|
294
|
+
|
|
295
|
+
capability
|
|
296
|
+
.When(a.Pod)
|
|
297
|
+
.IsCreatedOrUpdated()
|
|
298
|
+
.InNamespace("default")
|
|
299
|
+
.Alias("test-alias")
|
|
300
|
+
.Validate(mockValidateCallback);
|
|
301
|
+
|
|
302
|
+
expect(capability.bindings).toHaveLength(1);
|
|
303
|
+
const binding = capability.bindings[0];
|
|
304
|
+
|
|
305
|
+
// Simulate the validation action
|
|
306
|
+
const mockPeprRequest = new PeprValidateRequest<V1Pod>(mockRequest);
|
|
307
|
+
|
|
308
|
+
if (binding.validateCallback) {
|
|
309
|
+
await binding.validateCallback(mockPeprRequest);
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
expect(mockValidateCallback).toHaveBeenCalledWith(mockPeprRequest, expect.anything());
|
|
313
|
+
expect(mockLog.child).toHaveBeenCalledWith({ alias: "test-alias" });
|
|
314
|
+
expect(mockLog.info).toHaveBeenCalledWith("Executing validate action with alias: test-alias");
|
|
315
|
+
expect(mockLog.info).toHaveBeenCalledWith("Validate action log");
|
|
316
|
+
});
|
|
317
|
+
|
|
318
|
+
it("should log 'no alias provided' if alias is not set in validate callback", async () => {
|
|
319
|
+
const capability = new Capability(capabilityConfig);
|
|
320
|
+
|
|
321
|
+
// Mock the validate callback
|
|
322
|
+
const mockValidateCallback: ValidateAction<typeof V1Pod, V1Pod> = jest.fn(
|
|
323
|
+
async (req: PeprValidateRequest<V1Pod>, logger: typeof Log = mockLog) => {
|
|
324
|
+
logger.info("Validate action log");
|
|
325
|
+
return { allowed: true };
|
|
326
|
+
},
|
|
327
|
+
);
|
|
328
|
+
|
|
329
|
+
// Do not set alias, to trigger "no alias provided"
|
|
330
|
+
capability.When(a.Pod).IsCreatedOrUpdated().Validate(mockValidateCallback);
|
|
331
|
+
|
|
332
|
+
expect(capability.bindings).toHaveLength(1);
|
|
333
|
+
const binding = capability.bindings[0];
|
|
334
|
+
|
|
335
|
+
// Simulate the validation action
|
|
336
|
+
const mockPeprRequest = new PeprValidateRequest<V1Pod>(mockRequest);
|
|
337
|
+
|
|
338
|
+
if (binding.validateCallback) {
|
|
339
|
+
await binding.validateCallback(mockPeprRequest);
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
// Expect the log to contain "no alias provided"
|
|
343
|
+
expect(mockLog.info).toHaveBeenCalledWith("Executing validate action with alias: no alias provided");
|
|
344
|
+
expect(mockLog.info).toHaveBeenCalledWith("Validate action log");
|
|
345
|
+
});
|
|
346
|
+
|
|
347
|
+
it("should register a Watch action and execute it with the logger", async () => {
|
|
348
|
+
const capability = new Capability(capabilityConfig);
|
|
349
|
+
|
|
350
|
+
// Mock Watch callback function
|
|
351
|
+
const mockWatchCallback: WatchLogAction<typeof V1Pod> = jest.fn(
|
|
352
|
+
async (update, phase, logger: typeof Log = mockLog) => {
|
|
353
|
+
logger.info("Watch action executed");
|
|
354
|
+
},
|
|
355
|
+
);
|
|
356
|
+
|
|
357
|
+
// Chain the When and Watch methods
|
|
358
|
+
capability.When(a.Pod).IsCreated().Watch(mockWatchCallback);
|
|
359
|
+
|
|
360
|
+
// Log the bindings to ensure they are being added
|
|
361
|
+
console.log("Bindings after watch registration: ", capability.bindings);
|
|
362
|
+
|
|
363
|
+
// Retrieve the registered binding
|
|
364
|
+
const binding = capability.bindings.find(b => b.isWatch === true);
|
|
365
|
+
|
|
366
|
+
// Check that the watch callback was registered
|
|
367
|
+
expect(binding).toBeDefined();
|
|
368
|
+
expect(binding?.isWatch).toBe(true);
|
|
369
|
+
|
|
370
|
+
// Simulate calling the watch callback with test data
|
|
371
|
+
const testPod = new V1Pod();
|
|
372
|
+
await binding?.watchCallback?.(testPod, WatchPhase.Added, mockLog);
|
|
373
|
+
|
|
374
|
+
// Ensure that the logger's `info` method was called
|
|
375
|
+
expect(mockLog.info).toHaveBeenCalledWith("Watch action executed");
|
|
376
|
+
expect(mockWatchCallback).toHaveBeenCalledWith(testPod, WatchPhase.Added, mockLog);
|
|
377
|
+
});
|
|
378
|
+
|
|
379
|
+
it("should pass the correct parameters to the Watch action", async () => {
|
|
380
|
+
const capability = new Capability(capabilityConfig);
|
|
381
|
+
|
|
382
|
+
const mockWatchCallback: WatchLogAction<typeof V1Pod> = jest.fn(
|
|
383
|
+
async (update, phase, logger: typeof Log = mockLog) => {
|
|
384
|
+
logger.info("Watch action executed");
|
|
385
|
+
},
|
|
386
|
+
);
|
|
387
|
+
|
|
388
|
+
capability.When(a.Pod).IsCreated().Watch(mockWatchCallback);
|
|
389
|
+
|
|
390
|
+
const binding = capability.bindings.find(b => b.isWatch);
|
|
391
|
+
expect(binding).toBeDefined();
|
|
392
|
+
|
|
393
|
+
const testPod = new V1Pod();
|
|
394
|
+
const testPhase = WatchPhase.Modified;
|
|
395
|
+
|
|
396
|
+
// Call the watch callback with custom data
|
|
397
|
+
await binding?.watchCallback?.(testPod, testPhase, mockLog);
|
|
398
|
+
|
|
399
|
+
expect(mockWatchCallback).toHaveBeenCalledWith(testPod, testPhase, mockLog);
|
|
400
|
+
expect(mockLog.info).toHaveBeenCalledWith("Watch action executed");
|
|
401
|
+
});
|
|
402
|
+
|
|
403
|
+
it("should use child logger for reconcile callback", async () => {
|
|
404
|
+
const capability = new Capability(capabilityConfig);
|
|
405
|
+
|
|
406
|
+
const mockReconcileCallback: WatchLogAction<typeof V1Pod> = jest.fn(
|
|
407
|
+
async (update, phase, logger: typeof Log = mockLog) => {
|
|
408
|
+
logger.info("Reconcile action log");
|
|
409
|
+
},
|
|
410
|
+
);
|
|
411
|
+
|
|
412
|
+
capability.When(a.Pod).IsCreatedOrUpdated().Reconcile(mockReconcileCallback);
|
|
413
|
+
|
|
414
|
+
expect(capability.bindings).toHaveLength(1);
|
|
415
|
+
const binding = capability.bindings[0];
|
|
416
|
+
|
|
417
|
+
// Simulate calling the reconcile action
|
|
418
|
+
const testPod = new V1Pod();
|
|
419
|
+
const testPhase = WatchPhase.Modified;
|
|
420
|
+
|
|
421
|
+
if (binding.watchCallback) {
|
|
422
|
+
await binding.watchCallback(testPod, testPhase);
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
expect(mockReconcileCallback).toHaveBeenCalledWith(testPod, testPhase, expect.anything());
|
|
426
|
+
expect(mockLog.child).toHaveBeenCalledWith({ alias: "no alias provided" });
|
|
427
|
+
expect(mockLog.info).toHaveBeenCalledWith("Executing reconcile action with alias: no alias provided");
|
|
428
|
+
expect(mockLog.info).toHaveBeenCalledWith("Reconcile action log");
|
|
429
|
+
});
|
|
430
|
+
|
|
431
|
+
it("should use child logger for finalize callback", async () => {
|
|
432
|
+
const capability = new Capability(capabilityConfig);
|
|
433
|
+
|
|
434
|
+
const mockFinalizeCallback: FinalizeAction<typeof V1Pod> = jest.fn(async (update, logger: typeof Log = mockLog) => {
|
|
435
|
+
logger.info("Finalize action log");
|
|
436
|
+
});
|
|
437
|
+
|
|
438
|
+
// Create a mock WatchLogAction function that matches the expected signature
|
|
439
|
+
const mockWatchCallback: WatchLogAction<typeof V1Pod> = jest.fn(
|
|
440
|
+
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
|
441
|
+
async (update: V1Pod, phase: WatchPhase, logger?: typeof Log) => {},
|
|
442
|
+
);
|
|
443
|
+
|
|
444
|
+
// Chain .Watch() with the correct function signature before .Finalize()
|
|
445
|
+
capability.When(a.Pod).IsCreatedOrUpdated().Watch(mockWatchCallback).Finalize(mockFinalizeCallback);
|
|
446
|
+
|
|
447
|
+
// Find the finalize binding
|
|
448
|
+
const finalizeBinding = capability.bindings.find(binding => binding.finalizeCallback);
|
|
449
|
+
|
|
450
|
+
expect(finalizeBinding).toBeDefined(); // Ensure the finalize binding exists
|
|
451
|
+
|
|
452
|
+
// Simulate calling the finalize action
|
|
453
|
+
const testPod = new V1Pod();
|
|
454
|
+
|
|
455
|
+
if (finalizeBinding?.finalizeCallback) {
|
|
456
|
+
await finalizeBinding.finalizeCallback(testPod);
|
|
457
|
+
}
|
|
458
|
+
|
|
459
|
+
expect(mockFinalizeCallback).toHaveBeenCalledWith(testPod, expect.anything());
|
|
460
|
+
expect(mockLog.child).toHaveBeenCalledWith({ alias: "no alias provided" });
|
|
461
|
+
expect(mockLog.info).toHaveBeenCalledWith("Executing finalize action with alias: no alias provided");
|
|
462
|
+
expect(mockLog.info).toHaveBeenCalledWith("Finalize action log");
|
|
463
|
+
});
|
|
464
|
+
|
|
465
|
+
it("should add deletionTimestamp filter", () => {
|
|
466
|
+
const capability = new Capability(capabilityConfig);
|
|
467
|
+
|
|
468
|
+
const mockValidateCallback: ValidateAction<typeof V1Pod, V1Pod> = jest.fn(
|
|
469
|
+
async (req: PeprValidateRequest<V1Pod>, logger: typeof Log = mockLog) => {
|
|
470
|
+
logger.info("Validate action log");
|
|
471
|
+
return { allowed: true };
|
|
472
|
+
},
|
|
473
|
+
);
|
|
474
|
+
|
|
475
|
+
capability.When(a.Pod).IsCreatedOrUpdated().WithDeletionTimestamp().Validate(mockValidateCallback);
|
|
476
|
+
|
|
477
|
+
expect(capability.bindings).toHaveLength(1); // Ensure binding is created
|
|
478
|
+
expect(capability.bindings[0].filters.deletionTimestamp).toBe(true);
|
|
479
|
+
});
|
|
480
|
+
|
|
481
|
+
it("should add name filter", () => {
|
|
482
|
+
const capability = new Capability(capabilityConfig);
|
|
483
|
+
|
|
484
|
+
const mockValidateCallback: ValidateAction<typeof V1Pod, V1Pod> = jest.fn(
|
|
485
|
+
async (req: PeprValidateRequest<V1Pod>, logger: typeof Log = mockLog) => {
|
|
486
|
+
logger.info("Validate action log");
|
|
487
|
+
return { allowed: true };
|
|
488
|
+
},
|
|
489
|
+
);
|
|
490
|
+
|
|
491
|
+
capability.When(a.Pod).IsCreatedOrUpdated().WithName("test-name").Validate(mockValidateCallback);
|
|
492
|
+
|
|
493
|
+
expect(capability.bindings).toHaveLength(1); // Ensure binding is created
|
|
494
|
+
expect(capability.bindings[0].filters.name).toBe("test-name");
|
|
495
|
+
});
|
|
496
|
+
|
|
497
|
+
it("should add annotation filter", () => {
|
|
498
|
+
const capability = new Capability(capabilityConfig);
|
|
499
|
+
|
|
500
|
+
const mockValidateCallback: ValidateAction<typeof V1Pod, V1Pod> = jest.fn(
|
|
501
|
+
async (req: PeprValidateRequest<V1Pod>, logger: typeof Log = mockLog) => {
|
|
502
|
+
logger.info("Validate action log");
|
|
503
|
+
return { allowed: true };
|
|
504
|
+
},
|
|
505
|
+
);
|
|
506
|
+
|
|
507
|
+
capability.When(a.Pod).IsCreatedOrUpdated().WithAnnotation("test-key", "test-value").Validate(mockValidateCallback);
|
|
508
|
+
|
|
509
|
+
expect(capability.bindings).toHaveLength(1); // Ensure binding is created
|
|
510
|
+
expect(capability.bindings[0].filters.annotations["test-key"]).toBe("test-value");
|
|
511
|
+
});
|
|
512
|
+
|
|
513
|
+
it("should bind an update event", () => {
|
|
514
|
+
const capability = new Capability(capabilityConfig);
|
|
515
|
+
|
|
516
|
+
const mockValidateCallback: ValidateAction<typeof V1Pod, V1Pod> = jest.fn(
|
|
517
|
+
async (req: PeprValidateRequest<V1Pod>, logger: typeof Log = mockLog) => {
|
|
518
|
+
logger.info("Validate action log");
|
|
519
|
+
return { allowed: true };
|
|
520
|
+
},
|
|
521
|
+
);
|
|
522
|
+
|
|
523
|
+
capability.When(a.Pod).IsUpdated().InNamespace("default").Validate(mockValidateCallback);
|
|
524
|
+
|
|
525
|
+
expect(capability.bindings).toHaveLength(1); // Ensure binding is created
|
|
526
|
+
expect(capability.bindings[0].event).toBe(Event.Update);
|
|
527
|
+
});
|
|
528
|
+
|
|
529
|
+
it("should bind a delete event", async () => {
|
|
530
|
+
const capability = new Capability(capabilityConfig);
|
|
531
|
+
|
|
532
|
+
const mockValidateCallback: ValidateAction<typeof V1Pod, V1Pod> = jest.fn(
|
|
533
|
+
async (req: PeprValidateRequest<V1Pod>, logger: typeof Log = mockLog) => {
|
|
534
|
+
logger.info("Validate action log");
|
|
535
|
+
return { allowed: true };
|
|
536
|
+
},
|
|
537
|
+
);
|
|
538
|
+
|
|
539
|
+
capability.When(a.Pod).IsDeleted().InNamespace("default").Validate(mockValidateCallback);
|
|
540
|
+
|
|
541
|
+
expect(capability.bindings).toHaveLength(1);
|
|
542
|
+
|
|
543
|
+
expect(capability.bindings).toHaveLength(1); // Ensure binding is created
|
|
544
|
+
expect(capability.bindings[0].event).toBe(Event.Delete);
|
|
545
|
+
});
|
|
546
|
+
|
|
547
|
+
it("should throw an error if neither matchedKind nor kind is provided", () => {
|
|
548
|
+
const capability = new Capability(capabilityConfig);
|
|
549
|
+
|
|
550
|
+
// Mock a model with just a name, missing the kind
|
|
551
|
+
const mockModel: { name: string } = {
|
|
552
|
+
name: "InvalidModel",
|
|
553
|
+
};
|
|
554
|
+
|
|
555
|
+
// Expect an error when neither matchedKind nor kind is provided
|
|
556
|
+
expect(() => {
|
|
557
|
+
capability.When(mockModel as unknown as GenericClass); // Cast to the expected type without using 'any'
|
|
558
|
+
}).toThrowError(`Kind not specified for ${mockModel.name}`);
|
|
559
|
+
});
|
|
560
|
+
|
|
561
|
+
it("should create a new schedule and watch the schedule store when PEPR_WATCH_MODE is 'true'", () => {
|
|
562
|
+
// Set the environment variable
|
|
563
|
+
process.env.PEPR_WATCH_MODE = "true";
|
|
564
|
+
|
|
565
|
+
const capability = new Capability(capabilityConfig);
|
|
566
|
+
|
|
567
|
+
const mockSchedule: Schedule = {
|
|
568
|
+
name: "test-schedule",
|
|
569
|
+
every: 5,
|
|
570
|
+
unit: "minutes",
|
|
571
|
+
run: jest.fn(),
|
|
572
|
+
startTime: new Date(),
|
|
573
|
+
completions: 1,
|
|
574
|
+
};
|
|
575
|
+
|
|
576
|
+
// Call OnSchedule with a mock schedule
|
|
577
|
+
capability.OnSchedule(mockSchedule);
|
|
578
|
+
|
|
579
|
+
// Ensure that the schedule store's `onReady` method is called with the correct callback
|
|
580
|
+
const scheduleStoreInstance = capability.getScheduleStore();
|
|
581
|
+
expect(scheduleStoreInstance.onReady).toHaveBeenCalledWith(expect.any(Function));
|
|
582
|
+
|
|
583
|
+
// Simulate the `onReady` callback being invoked
|
|
584
|
+
const onReadyCallback = (scheduleStoreInstance.onReady as jest.Mock).mock.calls[0][0] as () => void;
|
|
585
|
+
onReadyCallback(); // The callback function is now invoked as a type of `() => void`
|
|
586
|
+
|
|
587
|
+
// Ensure the new OnSchedule instance is created with the correct schedule data
|
|
588
|
+
expect(OnSchedule).toHaveBeenCalledWith(mockSchedule);
|
|
589
|
+
|
|
590
|
+
// Clean up environment variables after the test
|
|
591
|
+
delete process.env.PEPR_WATCH_MODE;
|
|
592
|
+
});
|
|
593
|
+
|
|
594
|
+
it("should not create a new schedule or watch the schedule store when PEPR_WATCH_MODE is not set", () => {
|
|
595
|
+
// Ensure environment variables are not set
|
|
596
|
+
delete process.env.PEPR_WATCH_MODE;
|
|
597
|
+
delete process.env.PEPR_MODE;
|
|
598
|
+
|
|
599
|
+
const capability = new Capability(capabilityConfig);
|
|
600
|
+
|
|
601
|
+
const mockSchedule: Schedule = {
|
|
602
|
+
name: "test-schedule",
|
|
603
|
+
every: 5,
|
|
604
|
+
unit: "minutes",
|
|
605
|
+
run: jest.fn(),
|
|
606
|
+
startTime: new Date(),
|
|
607
|
+
completions: 1,
|
|
608
|
+
};
|
|
609
|
+
|
|
610
|
+
// Call OnSchedule with a mock schedule
|
|
611
|
+
capability.OnSchedule(mockSchedule);
|
|
612
|
+
|
|
613
|
+
// Ensure that the schedule store's `onReady` method is not called
|
|
614
|
+
const scheduleStoreInstance = capability.getScheduleStore();
|
|
615
|
+
expect(scheduleStoreInstance.onReady).not.toHaveBeenCalled();
|
|
616
|
+
|
|
617
|
+
// Ensure that OnSchedule was not called
|
|
618
|
+
expect(OnSchedule).not.toHaveBeenCalled();
|
|
619
|
+
});
|
|
620
|
+
|
|
621
|
+
it("should use aliasLogger if no logger is provided in watch callback", async () => {
|
|
622
|
+
const capability = new Capability(capabilityConfig);
|
|
623
|
+
|
|
624
|
+
// Mock the watch callback
|
|
625
|
+
const mockWatchCallback: WatchLogAction<typeof V1Pod> = jest.fn(
|
|
626
|
+
async (update: V1Pod, phase: WatchPhase, logger?: typeof Log) => {
|
|
627
|
+
logger?.info("Watch action log");
|
|
628
|
+
},
|
|
629
|
+
);
|
|
630
|
+
|
|
631
|
+
// Chain Watch without providing an explicit logger
|
|
632
|
+
capability.When(a.Pod).IsCreatedOrUpdated().Watch(mockWatchCallback);
|
|
633
|
+
|
|
634
|
+
expect(capability.bindings).toHaveLength(1);
|
|
635
|
+
const binding = capability.bindings[0];
|
|
636
|
+
|
|
637
|
+
// Simulate the watch action without passing a logger, so aliasLogger is used
|
|
638
|
+
const testPod = new V1Pod();
|
|
639
|
+
await binding.watchCallback?.(testPod, WatchPhase.Added); // No logger passed
|
|
640
|
+
|
|
641
|
+
// Assert that aliasLogger was used
|
|
642
|
+
expect(mockLog.child).toHaveBeenCalledWith({ alias: "no alias provided" });
|
|
643
|
+
expect(mockLog.info).toHaveBeenCalledWith("Executing watch action with alias: no alias provided");
|
|
644
|
+
expect(mockLog.info).toHaveBeenCalledWith("Watch action log");
|
|
645
|
+
});
|
|
646
|
+
|
|
647
|
+
it("should add annotation with an empty value when no value is provided in WithAnnotation", () => {
|
|
648
|
+
const capability = new Capability(capabilityConfig);
|
|
649
|
+
|
|
650
|
+
// Chain WithAnnotation without providing a value (default to empty string)
|
|
651
|
+
capability.When(a.Pod).IsCreatedOrUpdated().WithAnnotation("test-annotation");
|
|
652
|
+
|
|
653
|
+
expect(capability.bindings).toHaveLength(0);
|
|
654
|
+
});
|
|
655
|
+
});
|