pepr 0.34.0 → 0.35.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/templates.d.ts +1 -0
- package/dist/cli/init/templates.d.ts.map +1 -1
- package/dist/cli.js +48 -23
- package/dist/controller.js +1 -1
- package/dist/lib/assets/helm.d.ts.map +1 -1
- package/dist/lib/assets/pods.d.ts.map +1 -1
- package/dist/lib/assets/yaml.d.ts.map +1 -1
- package/dist/lib/controller/store.d.ts.map +1 -1
- package/dist/lib/storage.d.ts +1 -0
- package/dist/lib/storage.d.ts.map +1 -1
- package/dist/lib/watch-processor.d.ts +7 -0
- package/dist/lib/watch-processor.d.ts.map +1 -1
- package/dist/lib.js +30 -7
- package/dist/lib.js.map +3 -3
- package/package.json +8 -8
- package/src/cli/init/templates.ts +1 -0
- package/src/lib/assets/helm.ts +4 -16
- package/src/lib/assets/pods.ts +4 -0
- package/src/lib/assets/yaml.ts +32 -0
- package/src/lib/controller/store.ts +0 -2
- package/src/lib/storage.test.ts +24 -11
- package/src/lib/storage.ts +12 -4
- package/src/lib/watch-processor.test.ts +134 -3
- package/src/lib/watch-processor.ts +33 -3
- package/src/templates/capabilities/hello-pepr.ts +35 -11
package/package.json
CHANGED
|
@@ -13,7 +13,7 @@
|
|
|
13
13
|
"/dist",
|
|
14
14
|
"/src"
|
|
15
15
|
],
|
|
16
|
-
"version": "0.
|
|
16
|
+
"version": "0.35.0",
|
|
17
17
|
"main": "dist/lib.js",
|
|
18
18
|
"types": "dist/lib.d.ts",
|
|
19
19
|
"scripts": {
|
|
@@ -38,22 +38,22 @@
|
|
|
38
38
|
"format:fix": "eslint src --fix && prettier src --write"
|
|
39
39
|
},
|
|
40
40
|
"dependencies": {
|
|
41
|
-
"@types/ramda": "0.30.
|
|
41
|
+
"@types/ramda": "0.30.2",
|
|
42
42
|
"express": "4.19.2",
|
|
43
43
|
"fast-json-patch": "3.1.1",
|
|
44
44
|
"json-pointer": "^0.6.2",
|
|
45
|
-
"kubernetes-fluent-client": "3.0.
|
|
45
|
+
"kubernetes-fluent-client": "3.0.2",
|
|
46
46
|
"pino": "9.3.2",
|
|
47
47
|
"pino-pretty": "11.2.2",
|
|
48
48
|
"prom-client": "15.1.3",
|
|
49
49
|
"ramda": "0.30.1"
|
|
50
50
|
},
|
|
51
51
|
"devDependencies": {
|
|
52
|
-
"@commitlint/cli": "19.
|
|
53
|
-
"@commitlint/config-conventional": "19.
|
|
52
|
+
"@commitlint/cli": "19.4.1",
|
|
53
|
+
"@commitlint/config-conventional": "19.4.1",
|
|
54
54
|
"@fast-check/jest": "^2.0.1",
|
|
55
55
|
"@jest/globals": "29.7.0",
|
|
56
|
-
"@types/eslint": "9.6.
|
|
56
|
+
"@types/eslint": "9.6.1",
|
|
57
57
|
"@types/express": "4.17.21",
|
|
58
58
|
"@types/json-pointer": "^1.0.34",
|
|
59
59
|
"@types/node": "22.x.x",
|
|
@@ -63,8 +63,8 @@
|
|
|
63
63
|
"fast-check": "^3.19.0",
|
|
64
64
|
"jest": "29.7.0",
|
|
65
65
|
"js-yaml": "^4.1.0",
|
|
66
|
-
"nock": "13.5.4",
|
|
67
|
-
"ts-jest": "29.2.
|
|
66
|
+
"nock": "^13.5.4",
|
|
67
|
+
"ts-jest": "29.2.5"
|
|
68
68
|
},
|
|
69
69
|
"peerDependencies": {
|
|
70
70
|
"@typescript-eslint/eslint-plugin": "7.18.0",
|
package/src/lib/assets/helm.ts
CHANGED
|
@@ -90,15 +90,9 @@ export function watcherDeployTemplate(buildTimestamp: string) {
|
|
|
90
90
|
- /app/node_modules/pepr/dist/controller.js
|
|
91
91
|
- {{ .Values.hash }}
|
|
92
92
|
readinessProbe:
|
|
93
|
-
|
|
94
|
-
path: /healthz
|
|
95
|
-
port: 3000
|
|
96
|
-
scheme: HTTPS
|
|
93
|
+
{{- toYaml .Values.watcher.readinessProbe | nindent 12 }}
|
|
97
94
|
livenessProbe:
|
|
98
|
-
|
|
99
|
-
path: /healthz
|
|
100
|
-
port: 3000
|
|
101
|
-
scheme: HTTPS
|
|
95
|
+
{{- toYaml .Values.watcher.livenessProbe | nindent 12 }}
|
|
102
96
|
ports:
|
|
103
97
|
- containerPort: 3000
|
|
104
98
|
resources:
|
|
@@ -176,15 +170,9 @@ export function admissionDeployTemplate(buildTimestamp: string) {
|
|
|
176
170
|
- /app/node_modules/pepr/dist/controller.js
|
|
177
171
|
- {{ .Values.hash }}
|
|
178
172
|
readinessProbe:
|
|
179
|
-
|
|
180
|
-
path: /healthz
|
|
181
|
-
port: 3000
|
|
182
|
-
scheme: HTTPS
|
|
173
|
+
{{- toYaml .Values.admission.readinessProbe | nindent 12 }}
|
|
183
174
|
livenessProbe:
|
|
184
|
-
|
|
185
|
-
path: /healthz
|
|
186
|
-
port: 3000
|
|
187
|
-
scheme: HTTPS
|
|
175
|
+
{{- toYaml .Values.admission.livenessProbe | nindent 12 }}
|
|
188
176
|
ports:
|
|
189
177
|
- containerPort: 3000
|
|
190
178
|
resources:
|
package/src/lib/assets/pods.ts
CHANGED
|
@@ -111,6 +111,7 @@ export function watcher(assets: Assets, hash: string, buildTimestamp: string, im
|
|
|
111
111
|
port: 3000,
|
|
112
112
|
scheme: "HTTPS",
|
|
113
113
|
},
|
|
114
|
+
initialDelaySeconds: 10,
|
|
114
115
|
},
|
|
115
116
|
livenessProbe: {
|
|
116
117
|
httpGet: {
|
|
@@ -118,6 +119,7 @@ export function watcher(assets: Assets, hash: string, buildTimestamp: string, im
|
|
|
118
119
|
port: 3000,
|
|
119
120
|
scheme: "HTTPS",
|
|
120
121
|
},
|
|
122
|
+
initialDelaySeconds: 10,
|
|
121
123
|
},
|
|
122
124
|
ports: [
|
|
123
125
|
{
|
|
@@ -248,6 +250,7 @@ export function deployment(
|
|
|
248
250
|
port: 3000,
|
|
249
251
|
scheme: "HTTPS",
|
|
250
252
|
},
|
|
253
|
+
initialDelaySeconds: 10,
|
|
251
254
|
},
|
|
252
255
|
livenessProbe: {
|
|
253
256
|
httpGet: {
|
|
@@ -255,6 +258,7 @@ export function deployment(
|
|
|
255
258
|
port: 3000,
|
|
256
259
|
scheme: "HTTPS",
|
|
257
260
|
},
|
|
261
|
+
initialDelaySeconds: 10,
|
|
258
262
|
},
|
|
259
263
|
ports: [
|
|
260
264
|
{
|
package/src/lib/assets/yaml.ts
CHANGED
|
@@ -45,6 +45,22 @@ export async function overridesFile({ hash, name, image, config, apiToken }: Ass
|
|
|
45
45
|
runAsNonRoot: true,
|
|
46
46
|
fsGroup: 65532,
|
|
47
47
|
},
|
|
48
|
+
readinessProbe: {
|
|
49
|
+
httpGet: {
|
|
50
|
+
path: "/healthz",
|
|
51
|
+
port: 3000,
|
|
52
|
+
scheme: "HTTPS",
|
|
53
|
+
},
|
|
54
|
+
initialDelaySeconds: 10,
|
|
55
|
+
},
|
|
56
|
+
livenessProbe: {
|
|
57
|
+
httpGet: {
|
|
58
|
+
path: "/healthz",
|
|
59
|
+
port: 3000,
|
|
60
|
+
scheme: "HTTPS",
|
|
61
|
+
},
|
|
62
|
+
initialDelaySeconds: 10,
|
|
63
|
+
},
|
|
48
64
|
resources: {
|
|
49
65
|
requests: {
|
|
50
66
|
memory: "64Mi",
|
|
@@ -95,6 +111,22 @@ export async function overridesFile({ hash, name, image, config, apiToken }: Ass
|
|
|
95
111
|
runAsNonRoot: true,
|
|
96
112
|
fsGroup: 65532,
|
|
97
113
|
},
|
|
114
|
+
readinessProbe: {
|
|
115
|
+
httpGet: {
|
|
116
|
+
path: "/healthz",
|
|
117
|
+
port: 3000,
|
|
118
|
+
scheme: "HTTPS",
|
|
119
|
+
},
|
|
120
|
+
initialDelaySeconds: 10,
|
|
121
|
+
},
|
|
122
|
+
livenessProbe: {
|
|
123
|
+
httpGet: {
|
|
124
|
+
path: "/healthz",
|
|
125
|
+
port: 3000,
|
|
126
|
+
scheme: "HTTPS",
|
|
127
|
+
},
|
|
128
|
+
initialDelaySeconds: 10,
|
|
129
|
+
},
|
|
98
130
|
resources: {
|
|
99
131
|
requests: {
|
|
100
132
|
memory: "64Mi",
|
package/src/lib/storage.test.ts
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
// SPDX-FileCopyrightText: 2023-Present The Pepr Authors
|
|
3
3
|
|
|
4
4
|
import { beforeEach, describe, expect, it, jest } from "@jest/globals";
|
|
5
|
-
import { DataStore, Storage, v2StoreKey, stripV2Prefix } from "./storage";
|
|
5
|
+
import { DataStore, Storage, v2StoreKey, v2UnescapedStoreKey, stripV2Prefix } from "./storage";
|
|
6
6
|
import fc from "fast-check";
|
|
7
7
|
|
|
8
8
|
describe("stripV2Prefix", () => {
|
|
@@ -27,6 +27,18 @@ describe("v2StoreKey", () => {
|
|
|
27
27
|
}
|
|
28
28
|
});
|
|
29
29
|
});
|
|
30
|
+
|
|
31
|
+
describe("v2UnescapedStoreKey", () => {
|
|
32
|
+
it("should prefix the key with v2", () => {
|
|
33
|
+
const keys = ["https://google.com", "sso-client-http://bin", "key3", "key4", "key5"];
|
|
34
|
+
const results = ["v2-https://google.com", "v2-sso-client-http://bin", "v2-key3", "v2-key4", "v2-key5"];
|
|
35
|
+
|
|
36
|
+
for (let i = 0; i < keys.length; i++) {
|
|
37
|
+
const result = v2UnescapedStoreKey(keys[i]);
|
|
38
|
+
expect(result).toEqual(results[i]);
|
|
39
|
+
}
|
|
40
|
+
});
|
|
41
|
+
});
|
|
30
42
|
describe("Storage with fuzzing and property-based tests", () => {
|
|
31
43
|
let storage: Storage;
|
|
32
44
|
|
|
@@ -39,7 +51,7 @@ describe("Storage with fuzzing and property-based tests", () => {
|
|
|
39
51
|
fc.assert(
|
|
40
52
|
fc.property(fc.string(), fc.string(), (key, value) => {
|
|
41
53
|
storage.setItem(key, value);
|
|
42
|
-
const mockData: DataStore = { [
|
|
54
|
+
const mockData: DataStore = { [v2UnescapedStoreKey(key)]: value };
|
|
43
55
|
storage.receive(mockData);
|
|
44
56
|
if (value === "") {
|
|
45
57
|
expect(storage.getItem(key)).toBeNull();
|
|
@@ -75,7 +87,7 @@ describe("Storage with fuzzing and property-based tests", () => {
|
|
|
75
87
|
fc.assert(
|
|
76
88
|
fc.property(fc.string(), fc.string(), (key, value) => {
|
|
77
89
|
storage.setItem(key, value);
|
|
78
|
-
const mockData: DataStore = { [
|
|
90
|
+
const mockData: DataStore = { [v2UnescapedStoreKey(key)]: value };
|
|
79
91
|
storage.receive(mockData);
|
|
80
92
|
if (value === "") {
|
|
81
93
|
expect(storage.getItem(key)).toBeNull();
|
|
@@ -100,19 +112,20 @@ describe("Storage", () => {
|
|
|
100
112
|
const key = "key1";
|
|
101
113
|
storage.setItem(key, "value1");
|
|
102
114
|
|
|
103
|
-
expect(mockSender).toHaveBeenCalledWith("add", [
|
|
115
|
+
expect(mockSender).toHaveBeenCalledWith("add", [v2UnescapedStoreKey(key)], "value1");
|
|
104
116
|
});
|
|
105
117
|
|
|
106
118
|
it("should set an item and wait", () => {
|
|
107
119
|
const mockSender = jest.fn();
|
|
108
120
|
storage.registerSender(mockSender);
|
|
109
121
|
jest.useFakeTimers();
|
|
110
|
-
const
|
|
122
|
+
const keys = ["key1", "https://google.com", "sso-client-http://bin"];
|
|
111
123
|
|
|
112
|
-
|
|
113
|
-
|
|
124
|
+
keys.map(key => {
|
|
125
|
+
void storage.setItemAndWait(key, "value1");
|
|
126
|
+
expect(mockSender).toHaveBeenCalledWith("add", [v2StoreKey(key)], "value1");
|
|
127
|
+
});
|
|
114
128
|
|
|
115
|
-
expect(mockSender).toHaveBeenCalledWith("add", [v2StoreKey(key)], "value1");
|
|
116
129
|
jest.useRealTimers();
|
|
117
130
|
});
|
|
118
131
|
|
|
@@ -142,10 +155,10 @@ describe("Storage", () => {
|
|
|
142
155
|
const mockSender = jest.fn();
|
|
143
156
|
storage.registerSender(mockSender);
|
|
144
157
|
|
|
145
|
-
storage.receive({ key1: "value1", key2: "value2" });
|
|
158
|
+
storage.receive({ key1: "value1", key2: "value2", "sso-client-http://bin": "value3" });
|
|
146
159
|
storage.clear();
|
|
147
160
|
|
|
148
|
-
expect(mockSender).toHaveBeenCalledWith("remove", ["key1", "key2"], undefined);
|
|
161
|
+
expect(mockSender).toHaveBeenCalledWith("remove", ["key1", "key2", "sso-client-http:~1~1bin"], undefined);
|
|
149
162
|
});
|
|
150
163
|
|
|
151
164
|
it("should get an item", () => {
|
|
@@ -153,7 +166,7 @@ describe("Storage", () => {
|
|
|
153
166
|
const results = ["value1", null, "!", "was-here", "3f7dd007-568f-4f4a-bbac-2e6bfff93860", "your-machine", " "];
|
|
154
167
|
|
|
155
168
|
keys.map((key, i) => {
|
|
156
|
-
const mockData: DataStore = { [
|
|
169
|
+
const mockData: DataStore = { [v2UnescapedStoreKey(key)]: results[i]! };
|
|
157
170
|
|
|
158
171
|
storage.receive(mockData);
|
|
159
172
|
const value = storage.getItem(keys[i]);
|
package/src/lib/storage.ts
CHANGED
|
@@ -17,6 +17,10 @@ export function v2StoreKey(key: string) {
|
|
|
17
17
|
return `${STORE_VERSION_PREFIX}-${pointer.escape(key)}`;
|
|
18
18
|
}
|
|
19
19
|
|
|
20
|
+
export function v2UnescapedStoreKey(key: string) {
|
|
21
|
+
return `${STORE_VERSION_PREFIX}-${key}`;
|
|
22
|
+
}
|
|
23
|
+
|
|
20
24
|
export function stripV2Prefix(key: string) {
|
|
21
25
|
return key.replace(/^v2-/, "");
|
|
22
26
|
}
|
|
@@ -95,7 +99,7 @@ export class Storage implements PeprStore {
|
|
|
95
99
|
};
|
|
96
100
|
|
|
97
101
|
getItem = (key: string) => {
|
|
98
|
-
const result = this.#store[
|
|
102
|
+
const result = this.#store[v2UnescapedStoreKey(key)] || null;
|
|
99
103
|
if (result !== null && typeof result !== "function" && typeof result !== "object") {
|
|
100
104
|
return result;
|
|
101
105
|
}
|
|
@@ -103,7 +107,10 @@ export class Storage implements PeprStore {
|
|
|
103
107
|
};
|
|
104
108
|
|
|
105
109
|
clear = () => {
|
|
106
|
-
this.#dispatchUpdate(
|
|
110
|
+
this.#dispatchUpdate(
|
|
111
|
+
"remove",
|
|
112
|
+
Object.keys(this.#store).map(key => pointer.escape(key)),
|
|
113
|
+
);
|
|
107
114
|
};
|
|
108
115
|
|
|
109
116
|
removeItem = (key: string) => {
|
|
@@ -124,9 +131,10 @@ export class Storage implements PeprStore {
|
|
|
124
131
|
*/
|
|
125
132
|
setItemAndWait = (key: string, value: string) => {
|
|
126
133
|
this.#dispatchUpdate("add", [v2StoreKey(key)], value);
|
|
134
|
+
|
|
127
135
|
return new Promise<void>((resolve, reject) => {
|
|
128
136
|
const unsubscribe = this.subscribe(data => {
|
|
129
|
-
if (data[`${
|
|
137
|
+
if (data[`${v2UnescapedStoreKey(key)}`] === value) {
|
|
130
138
|
unsubscribe();
|
|
131
139
|
resolve();
|
|
132
140
|
}
|
|
@@ -151,7 +159,7 @@ export class Storage implements PeprStore {
|
|
|
151
159
|
this.#dispatchUpdate("remove", [v2StoreKey(key)]);
|
|
152
160
|
return new Promise<void>((resolve, reject) => {
|
|
153
161
|
const unsubscribe = this.subscribe(data => {
|
|
154
|
-
if (!Object.hasOwn(data, `${
|
|
162
|
+
if (!Object.hasOwn(data, `${v2UnescapedStoreKey(key)}`)) {
|
|
155
163
|
unsubscribe();
|
|
156
164
|
resolve();
|
|
157
165
|
}
|
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
// SPDX-License-Identifier: Apache-2.0
|
|
2
2
|
// SPDX-FileCopyrightText: 2023-Present The Pepr Authors
|
|
3
|
-
import { beforeEach, describe, expect, it, jest } from "@jest/globals";
|
|
3
|
+
import { afterAll, beforeEach, beforeAll, describe, expect, it, jest } from "@jest/globals";
|
|
4
4
|
import { GenericClass, K8s, KubernetesObject, kind } from "kubernetes-fluent-client";
|
|
5
5
|
import { K8sInit, WatchPhase } from "kubernetes-fluent-client/dist/fluent/types";
|
|
6
6
|
import { WatchCfg, WatchEvent, Watcher } from "kubernetes-fluent-client/dist/fluent/watch";
|
|
7
7
|
import { Capability } from "./capability";
|
|
8
|
-
import { setupWatch, logEvent } from "./watch-processor";
|
|
8
|
+
import { setupWatch, logEvent, queueRecordKey } from "./watch-processor";
|
|
9
9
|
import Log from "./logger";
|
|
10
10
|
import { metricsCollector } from "./metrics";
|
|
11
11
|
|
|
@@ -88,7 +88,7 @@ describe("WatchProcessor", () => {
|
|
|
88
88
|
resyncFailureMax: 5,
|
|
89
89
|
resyncDelaySec: 5,
|
|
90
90
|
lastSeenLimitSeconds: 300,
|
|
91
|
-
relistIntervalSec:
|
|
91
|
+
relistIntervalSec: 600,
|
|
92
92
|
};
|
|
93
93
|
|
|
94
94
|
capabilities.push({
|
|
@@ -320,3 +320,134 @@ describe("logEvent function", () => {
|
|
|
320
320
|
expect(Log.debug).toHaveBeenCalledWith(`Watch event ${WatchEvent.DATA_ERROR} received. ${message}.`);
|
|
321
321
|
});
|
|
322
322
|
});
|
|
323
|
+
|
|
324
|
+
describe("queueRecordKey", () => {
|
|
325
|
+
describe("PEPR_RECONCILE_STRATEGY=sharded", () => {
|
|
326
|
+
const original = process.env.PEPR_RECONCILE_STRATEGY;
|
|
327
|
+
|
|
328
|
+
beforeAll(() => {
|
|
329
|
+
process.env.PEPR_RECONCILE_STRATEGY = "sharded";
|
|
330
|
+
});
|
|
331
|
+
afterAll(() => {
|
|
332
|
+
process.env.PEPR_RECONCILE_STRATEGY = original;
|
|
333
|
+
});
|
|
334
|
+
|
|
335
|
+
it("should return correct key for an object with 'kind/name/namespace'", () => {
|
|
336
|
+
const obj: KubernetesObject = {
|
|
337
|
+
kind: "Pod",
|
|
338
|
+
metadata: {
|
|
339
|
+
name: "my-pod",
|
|
340
|
+
namespace: "my-namespace",
|
|
341
|
+
},
|
|
342
|
+
};
|
|
343
|
+
|
|
344
|
+
expect(queueRecordKey(obj)).toBe("Pod/my-pod/my-namespace");
|
|
345
|
+
});
|
|
346
|
+
|
|
347
|
+
it("should handle objects with missing namespace", () => {
|
|
348
|
+
const obj: KubernetesObject = {
|
|
349
|
+
kind: "Pod",
|
|
350
|
+
metadata: {
|
|
351
|
+
name: "my-pod",
|
|
352
|
+
},
|
|
353
|
+
};
|
|
354
|
+
|
|
355
|
+
expect(queueRecordKey(obj)).toBe("Pod/my-pod/cluster-scoped");
|
|
356
|
+
});
|
|
357
|
+
|
|
358
|
+
it("should handle objects with missing name", () => {
|
|
359
|
+
const obj: KubernetesObject = {
|
|
360
|
+
kind: "Pod",
|
|
361
|
+
metadata: {
|
|
362
|
+
namespace: "my-namespace",
|
|
363
|
+
},
|
|
364
|
+
};
|
|
365
|
+
|
|
366
|
+
expect(queueRecordKey(obj)).toBe("Pod/Unnamed/my-namespace");
|
|
367
|
+
});
|
|
368
|
+
|
|
369
|
+
it("should handle objects with missing metadata", () => {
|
|
370
|
+
const obj: KubernetesObject = {
|
|
371
|
+
kind: "Pod",
|
|
372
|
+
};
|
|
373
|
+
|
|
374
|
+
expect(queueRecordKey(obj)).toBe("Pod/Unnamed/cluster-scoped");
|
|
375
|
+
});
|
|
376
|
+
|
|
377
|
+
it("should handle objects with missing kind", () => {
|
|
378
|
+
const obj: KubernetesObject = {
|
|
379
|
+
metadata: {
|
|
380
|
+
name: "my-pod",
|
|
381
|
+
namespace: "my-namespace",
|
|
382
|
+
},
|
|
383
|
+
};
|
|
384
|
+
|
|
385
|
+
expect(queueRecordKey(obj)).toBe("UnknownKind/my-pod/my-namespace");
|
|
386
|
+
});
|
|
387
|
+
|
|
388
|
+
it("should handle completely empty objects", () => {
|
|
389
|
+
const obj: KubernetesObject = {};
|
|
390
|
+
|
|
391
|
+
expect(queueRecordKey(obj)).toBe("UnknownKind/Unnamed/cluster-scoped");
|
|
392
|
+
});
|
|
393
|
+
});
|
|
394
|
+
|
|
395
|
+
describe("PEPR_RECONCILE_STRATEGY=singular", () => {
|
|
396
|
+
const original = process.env.PEPR_RECONCILE_STRATEGY;
|
|
397
|
+
|
|
398
|
+
beforeAll(() => {
|
|
399
|
+
process.env.PEPR_RECONCILE_STRATEGY = "singular";
|
|
400
|
+
});
|
|
401
|
+
afterAll(() => {
|
|
402
|
+
process.env.PEPR_RECONCILE_STRATEGY = original;
|
|
403
|
+
});
|
|
404
|
+
|
|
405
|
+
it("should return correct key for an object with 'kind/namespace'", () => {
|
|
406
|
+
const obj: KubernetesObject = {
|
|
407
|
+
kind: "Pod",
|
|
408
|
+
metadata: {
|
|
409
|
+
name: "my-pod",
|
|
410
|
+
namespace: "my-namespace",
|
|
411
|
+
},
|
|
412
|
+
};
|
|
413
|
+
|
|
414
|
+
expect(queueRecordKey(obj)).toBe("Pod/my-namespace");
|
|
415
|
+
});
|
|
416
|
+
|
|
417
|
+
it("should handle objects with missing namespace", () => {
|
|
418
|
+
const obj: KubernetesObject = {
|
|
419
|
+
kind: "Pod",
|
|
420
|
+
metadata: {
|
|
421
|
+
name: "my-pod",
|
|
422
|
+
},
|
|
423
|
+
};
|
|
424
|
+
|
|
425
|
+
expect(queueRecordKey(obj)).toBe("Pod/cluster-scoped");
|
|
426
|
+
});
|
|
427
|
+
|
|
428
|
+
it("should handle objects with missing metadata", () => {
|
|
429
|
+
const obj: KubernetesObject = {
|
|
430
|
+
kind: "Pod",
|
|
431
|
+
};
|
|
432
|
+
|
|
433
|
+
expect(queueRecordKey(obj)).toBe("Pod/cluster-scoped");
|
|
434
|
+
});
|
|
435
|
+
|
|
436
|
+
it("should handle objects with missing kind", () => {
|
|
437
|
+
const obj: KubernetesObject = {
|
|
438
|
+
metadata: {
|
|
439
|
+
name: "my-pod",
|
|
440
|
+
namespace: "my-namespace",
|
|
441
|
+
},
|
|
442
|
+
};
|
|
443
|
+
|
|
444
|
+
expect(queueRecordKey(obj)).toBe("UnknownKind/my-namespace");
|
|
445
|
+
});
|
|
446
|
+
|
|
447
|
+
it("should handle completely empty objects", () => {
|
|
448
|
+
const obj: KubernetesObject = {};
|
|
449
|
+
|
|
450
|
+
expect(queueRecordKey(obj)).toBe("UnknownKind/cluster-scoped");
|
|
451
|
+
});
|
|
452
|
+
});
|
|
453
|
+
});
|
|
@@ -9,6 +9,29 @@ import { Queue } from "./queue";
|
|
|
9
9
|
import { Binding, Event } from "./types";
|
|
10
10
|
import { metricsCollector } from "./metrics";
|
|
11
11
|
|
|
12
|
+
// init a queueRecord record to store Queue instances for a given Kubernetes Object
|
|
13
|
+
const queueRecord: Record<string, Queue<KubernetesObject>> = {};
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Get the key for a record in the queueRecord
|
|
17
|
+
*
|
|
18
|
+
* @param obj The object to get the key for
|
|
19
|
+
* @returns The key for the object
|
|
20
|
+
*/
|
|
21
|
+
export function queueRecordKey(obj: KubernetesObject) {
|
|
22
|
+
const options = ["singular", "sharded"]; // TODO : ts-type this fella
|
|
23
|
+
const d3fault = "singular";
|
|
24
|
+
|
|
25
|
+
let strat = process.env.PEPR_RECONCILE_STRATEGY || d3fault;
|
|
26
|
+
strat = options.includes(strat) ? strat : d3fault;
|
|
27
|
+
|
|
28
|
+
const ns = obj.metadata?.namespace ?? "cluster-scoped";
|
|
29
|
+
const kind = obj.kind ?? "UnknownKind";
|
|
30
|
+
const name = obj.metadata?.name ?? "Unnamed";
|
|
31
|
+
|
|
32
|
+
return strat === "singular" ? `${kind}/${ns}` : `${kind}/${name}/${ns}`;
|
|
33
|
+
}
|
|
34
|
+
|
|
12
35
|
// Watch configuration
|
|
13
36
|
const watchCfg: WatchCfg = {
|
|
14
37
|
resyncFailureMax: process.env.PEPR_RESYNC_FAILURE_MAX ? parseInt(process.env.PEPR_RESYNC_FAILURE_MAX, 10) : 5,
|
|
@@ -18,7 +41,7 @@ const watchCfg: WatchCfg = {
|
|
|
18
41
|
: 300,
|
|
19
42
|
relistIntervalSec: process.env.PEPR_RELIST_INTERVAL_SECONDS
|
|
20
43
|
? parseInt(process.env.PEPR_RELIST_INTERVAL_SECONDS, 10)
|
|
21
|
-
:
|
|
44
|
+
: 600,
|
|
22
45
|
};
|
|
23
46
|
|
|
24
47
|
// Map the event to the watch phase
|
|
@@ -74,13 +97,20 @@ async function runBinding(binding: Binding, capabilityNamespaces: string[]) {
|
|
|
74
97
|
}
|
|
75
98
|
};
|
|
76
99
|
|
|
77
|
-
|
|
78
|
-
|
|
100
|
+
function getOrCreateQueue(key: string): Queue<KubernetesObject> {
|
|
101
|
+
if (!queueRecord[key]) {
|
|
102
|
+
queueRecord[key] = new Queue<KubernetesObject>();
|
|
103
|
+
queueRecord[key].setReconcile(watchCallback);
|
|
104
|
+
}
|
|
105
|
+
return queueRecord[key];
|
|
106
|
+
}
|
|
79
107
|
|
|
80
108
|
// Setup the resource watch
|
|
81
109
|
const watcher = K8s(binding.model, binding.filters).Watch(async (obj, type) => {
|
|
82
110
|
Log.debug(obj, `Watch event ${type} received`);
|
|
83
111
|
|
|
112
|
+
const queue = getOrCreateQueue(queueRecordKey(obj));
|
|
113
|
+
|
|
84
114
|
// If the binding is a queue, enqueue the object
|
|
85
115
|
if (binding.isQueue) {
|
|
86
116
|
await queue.enqueue(obj, type);
|
|
@@ -9,6 +9,7 @@ import {
|
|
|
9
9
|
fetchStatus,
|
|
10
10
|
kind,
|
|
11
11
|
} from "pepr";
|
|
12
|
+
import nock from "nock";
|
|
12
13
|
|
|
13
14
|
/**
|
|
14
15
|
* The HelloPepr Capability is an example capability to demonstrate some general concepts of Pepr.
|
|
@@ -268,24 +269,47 @@ interface TheChuckNorrisJoke {
|
|
|
268
269
|
}
|
|
269
270
|
|
|
270
271
|
When(a.ConfigMap)
|
|
271
|
-
.
|
|
272
|
+
.IsCreatedOrUpdated()
|
|
272
273
|
.WithLabel("chuck-norris")
|
|
273
|
-
.Mutate(
|
|
274
|
+
.Mutate(cm => cm.SetLabel("got-jokes", "true"))
|
|
275
|
+
.Watch(async cm => {
|
|
276
|
+
const jokeURL = "https://icanhazdadjoke.com/";
|
|
277
|
+
|
|
278
|
+
// Set up Nock to mock the API calls globally with header matching
|
|
279
|
+
nock(jokeURL).get("/").reply(200, {
|
|
280
|
+
id: "R7UfaahVfFd",
|
|
281
|
+
joke: "Funny joke goes here.",
|
|
282
|
+
status: 200,
|
|
283
|
+
});
|
|
284
|
+
|
|
274
285
|
// Try/catch is not needed as a response object will always be returned
|
|
275
|
-
const response = await fetch<TheChuckNorrisJoke>(
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
headers: {
|
|
279
|
-
Accept: "application/json",
|
|
280
|
-
},
|
|
286
|
+
const response = await fetch<TheChuckNorrisJoke>(jokeURL, {
|
|
287
|
+
headers: {
|
|
288
|
+
Accept: "application/json",
|
|
281
289
|
},
|
|
282
|
-
);
|
|
290
|
+
});
|
|
283
291
|
|
|
284
292
|
// Instead, check the `response.ok` field
|
|
285
293
|
if (response.ok) {
|
|
294
|
+
const { joke } = response.data;
|
|
295
|
+
// Add Joke to the Store
|
|
296
|
+
await Store.setItemAndWait(jokeURL, joke);
|
|
286
297
|
// Add the Chuck Norris joke to the configmap
|
|
287
|
-
|
|
288
|
-
|
|
298
|
+
try {
|
|
299
|
+
await K8s(kind.ConfigMap).Apply({
|
|
300
|
+
metadata: {
|
|
301
|
+
name: cm.metadata.name,
|
|
302
|
+
namespace: cm.metadata.namespace,
|
|
303
|
+
},
|
|
304
|
+
data: {
|
|
305
|
+
"chuck-says": Store.getItem(jokeURL),
|
|
306
|
+
},
|
|
307
|
+
});
|
|
308
|
+
} catch (error) {
|
|
309
|
+
Log.error(error, "Failed to apply ConfigMap using server-side apply.", {
|
|
310
|
+
cm,
|
|
311
|
+
});
|
|
312
|
+
}
|
|
289
313
|
}
|
|
290
314
|
|
|
291
315
|
// You can also assert on different HTTP response codes
|