kubernetes-fluent-client 1.3.1 → 1.4.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/fluent/index.d.ts.map +1 -1
- package/dist/fluent/index.js +2 -2
- package/dist/fluent/index.test.js +10 -0
- package/dist/fluent/types.d.ts +7 -5
- package/dist/fluent/types.d.ts.map +1 -1
- package/dist/fluent/utils.d.ts.map +1 -1
- package/dist/fluent/utils.js +2 -0
- package/dist/fluent/watch.d.ts +39 -2
- package/dist/fluent/watch.d.ts.map +1 -1
- package/dist/fluent/watch.js +103 -47
- package/dist/index.d.ts +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2 -1
- package/dist/kinds.d.ts.map +1 -1
- package/dist/kinds.js +13 -0
- package/dist/kinds.test.js +4 -0
- package/dist/types.d.ts +5 -0
- package/dist/types.d.ts.map +1 -1
- package/package.json +8 -5
- package/src/fluent/index.test.ts +14 -0
- package/src/fluent/index.ts +6 -5
- package/src/fluent/types.ts +10 -5
- package/src/fluent/utils.ts +3 -0
- package/src/fluent/watch.ts +172 -57
- package/src/index.ts +2 -0
- package/src/kinds.test.ts +4 -0
- package/src/kinds.ts +14 -0
- package/src/types.ts +7 -0
- package/__mocks__/@kubernetes/client-node.ts +0 -22
- package/commitlint.config.js +0 -1
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/fluent/index.ts"],"names":[],"mappings":"AAGA,OAAO,EAAwB,gBAAgB,EAAE,MAAM,yBAAyB,CAAC;
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/fluent/index.ts"],"names":[],"mappings":"AAGA,OAAO,EAAwB,gBAAgB,EAAE,MAAM,yBAAyB,CAAC;AAMjF,OAAO,EAAE,YAAY,EAAE,MAAM,UAAU,CAAC;AACxC,OAAO,EAAE,OAAO,EAAE,OAAO,EAAsB,MAAM,SAAS,CAAC;AAI/D;;;;;GAKG;AACH,wBAAgB,GAAG,CAAC,CAAC,SAAS,YAAY,EAAE,CAAC,SAAS,gBAAgB,GAAG,YAAY,CAAC,CAAC,CAAC,EACtF,KAAK,EAAE,CAAC,EACR,OAAO,GAAE,OAAY,GACpB,OAAO,CAAC,CAAC,CAAC,CAuGZ"}
|
package/dist/fluent/index.js
CHANGED
|
@@ -92,8 +92,8 @@ function K8s(model, filters = {}) {
|
|
|
92
92
|
}
|
|
93
93
|
return (0, utils_1.k8sExec)(model, filters, "PATCH", payload);
|
|
94
94
|
}
|
|
95
|
-
async function Watch(callback) {
|
|
96
|
-
|
|
95
|
+
async function Watch(callback, watchCfg) {
|
|
96
|
+
return (0, watch_1.ExecWatch)(model, filters, callback, watchCfg);
|
|
97
97
|
}
|
|
98
98
|
return { InNamespace, Apply, Create, Patch, ...withFilters };
|
|
99
99
|
}
|
|
@@ -89,4 +89,14 @@ globals_1.jest.mock("./utils");
|
|
|
89
89
|
const kube = (0, _1.K8s)(upstream_1.Pod);
|
|
90
90
|
await (0, globals_1.expect)(kube.Patch([])).rejects.toThrow("No operations specified");
|
|
91
91
|
});
|
|
92
|
+
(0, globals_1.it)("should allow Apply of deep partials", async () => {
|
|
93
|
+
const kube = (0, _1.K8s)(upstream_1.Pod);
|
|
94
|
+
const result = await kube.Apply({ metadata: { name: "fake" }, spec: { priority: 3 } });
|
|
95
|
+
(0, globals_1.expect)(result).toEqual(fakeResource);
|
|
96
|
+
});
|
|
97
|
+
(0, globals_1.it)("should throw an error if a Delete failed for a reason other than Not Found", async () => {
|
|
98
|
+
mockedKubeExec.mockRejectedValueOnce({ status: 500 }); // Internal Server Error on first call
|
|
99
|
+
const kube = (0, _1.K8s)(upstream_1.Pod);
|
|
100
|
+
await (0, globals_1.expect)(kube.Delete("fakeResource")).rejects.toEqual(globals_1.expect.objectContaining({ status: 500 }));
|
|
101
|
+
});
|
|
92
102
|
});
|
package/dist/fluent/types.d.ts
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import { KubernetesListObject, KubernetesObject } from "@kubernetes/client-node";
|
|
2
2
|
import { Operation } from "fast-json-patch";
|
|
3
|
+
import type { PartialDeep } from "type-fest";
|
|
3
4
|
import { GenericClass, GroupVersionKind } from "../types";
|
|
5
|
+
import { WatchCfg, WatchController } from "./watch";
|
|
4
6
|
/**
|
|
5
7
|
* The Phase matched when using the K8s Watch API.
|
|
6
8
|
*/
|
|
@@ -39,7 +41,7 @@ export type K8sFilteredActions<K extends KubernetesObject> = {
|
|
|
39
41
|
* @param callback
|
|
40
42
|
* @returns
|
|
41
43
|
*/
|
|
42
|
-
Watch: (callback: (payload: K, phase: WatchPhase) => void) => Promise<
|
|
44
|
+
Watch: (callback: (payload: K, phase: WatchPhase) => void, watchCfg?: WatchCfg) => Promise<WatchController>;
|
|
43
45
|
};
|
|
44
46
|
export type K8sUnfilteredActions<K extends KubernetesObject> = {
|
|
45
47
|
/**
|
|
@@ -48,7 +50,7 @@ export type K8sUnfilteredActions<K extends KubernetesObject> = {
|
|
|
48
50
|
* @param resource
|
|
49
51
|
* @returns
|
|
50
52
|
*/
|
|
51
|
-
Apply: (resource: K) => Promise<K>;
|
|
53
|
+
Apply: (resource: PartialDeep<K>) => Promise<K>;
|
|
52
54
|
/**
|
|
53
55
|
* Create the provided K8s resource or throw an error if it already exists.
|
|
54
56
|
*
|
|
@@ -57,7 +59,7 @@ export type K8sUnfilteredActions<K extends KubernetesObject> = {
|
|
|
57
59
|
*/
|
|
58
60
|
Create: (resource: K) => Promise<K>;
|
|
59
61
|
/**
|
|
60
|
-
* Advanced JSON Patch operations for when Server Side Apply,
|
|
62
|
+
* Advanced JSON Patch operations for when Server Side Apply, K8s().Apply(), is insufficient.
|
|
61
63
|
*
|
|
62
64
|
* Note: Throws an error on an empty list of patch operations.
|
|
63
65
|
*
|
|
@@ -72,7 +74,7 @@ export type K8sWithFilters<K extends KubernetesObject> = K8sFilteredActions<K> &
|
|
|
72
74
|
* Note multiple calls to this method will result in an AND condition. e.g.
|
|
73
75
|
*
|
|
74
76
|
* ```ts
|
|
75
|
-
*
|
|
77
|
+
* K8s(kind.Deployment)
|
|
76
78
|
* .WithField("metadata.name", "bar")
|
|
77
79
|
* .WithField("metadata.namespace", "qux")
|
|
78
80
|
* .Delete(...)
|
|
@@ -90,7 +92,7 @@ export type K8sWithFilters<K extends KubernetesObject> = K8sFilteredActions<K> &
|
|
|
90
92
|
* Note multiple calls to this method will result in an AND condition. e.g.
|
|
91
93
|
*
|
|
92
94
|
* ```ts
|
|
93
|
-
*
|
|
95
|
+
* K8s(kind.Deployment)
|
|
94
96
|
* .WithLabel("foo", "bar")
|
|
95
97
|
* .WithLabel("baz", "qux")
|
|
96
98
|
* .Delete(...)
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../../src/fluent/types.ts"],"names":[],"mappings":"AAGA,OAAO,EAAE,oBAAoB,EAAE,gBAAgB,EAAE,MAAM,yBAAyB,CAAC;AACjF,OAAO,EAAE,SAAS,EAAE,MAAM,iBAAiB,CAAC;
|
|
1
|
+
{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../../src/fluent/types.ts"],"names":[],"mappings":"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"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"utils.d.ts","sourceRoot":"","sources":["../../src/fluent/utils.ts"],"names":[],"mappings":";AAMA,OAAO,EAAE,GAAG,EAAE,MAAM,KAAK,CAAC;AAG1B,OAAO,EAAE,YAAY,EAAE,MAAM,UAAU,CAAC;AACxC,OAAO,EAAE,YAAY,EAAE,OAAO,EAAE,MAAM,SAAS,CAAC;AAIhD;;;;;;;;GAQG;AACH,wBAAgB,WAAW,CAAC,CAAC,SAAS,YAAY,EAChD,SAAS,EAAE,MAAM,EACjB,KAAK,EAAE,CAAC,EACR,OAAO,EAAE,OAAO,EAChB,WAAW,UAAQ,OAsDpB;AAED;;;;;;;;;;GAUG;AACH,wBAAsB,MAAM,CAAC,MAAM,EAAE,YAAY;;;
|
|
1
|
+
{"version":3,"file":"utils.d.ts","sourceRoot":"","sources":["../../src/fluent/utils.ts"],"names":[],"mappings":";AAMA,OAAO,EAAE,GAAG,EAAE,MAAM,KAAK,CAAC;AAG1B,OAAO,EAAE,YAAY,EAAE,MAAM,UAAU,CAAC;AACxC,OAAO,EAAE,YAAY,EAAE,OAAO,EAAE,MAAM,SAAS,CAAC;AAIhD;;;;;;;;GAQG;AACH,wBAAgB,WAAW,CAAC,CAAC,SAAS,YAAY,EAChD,SAAS,EAAE,MAAM,EACjB,KAAK,EAAE,CAAC,EACR,OAAO,EAAE,OAAO,EAChB,WAAW,UAAQ,OAsDpB;AAED;;;;;;;;;;GAUG;AACH,wBAAsB,MAAM,CAAC,MAAM,EAAE,YAAY;;;GAwBhD;AAED,wBAAsB,OAAO,CAAC,CAAC,SAAS,YAAY,EAAE,CAAC,EACrD,KAAK,EAAE,CAAC,EACR,OAAO,EAAE,OAAO,EAChB,MAAM,EAAE,YAAY,EACpB,OAAO,CAAC,EAAE,CAAC,GAAG,OAAO,cA8BtB"}
|
package/dist/fluent/utils.js
CHANGED
package/dist/fluent/watch.d.ts
CHANGED
|
@@ -1,8 +1,45 @@
|
|
|
1
1
|
/// <reference types="node" />
|
|
2
|
-
import { GenericClass } from "../types";
|
|
2
|
+
import { GenericClass, LogFn } from "../types";
|
|
3
3
|
import { Filters, WatchAction } from "./types";
|
|
4
|
+
/**
|
|
5
|
+
* Wrapper for the AbortController to allow the watch to be aborted externally.
|
|
6
|
+
*/
|
|
7
|
+
export type WatchController = {
|
|
8
|
+
/**
|
|
9
|
+
* Abort the watch.
|
|
10
|
+
* @param reason optional reason for aborting the watch
|
|
11
|
+
* @returns
|
|
12
|
+
*/
|
|
13
|
+
abort: (reason?: string) => void;
|
|
14
|
+
/**
|
|
15
|
+
* Get the AbortSignal for the watch.
|
|
16
|
+
* @returns
|
|
17
|
+
*/
|
|
18
|
+
signal: () => AbortSignal;
|
|
19
|
+
};
|
|
20
|
+
/**
|
|
21
|
+
* Configuration for the watch function.
|
|
22
|
+
*/
|
|
23
|
+
export type WatchCfg = {
|
|
24
|
+
/**
|
|
25
|
+
* The maximum number of times to retry the watch, the retry count is reset on success.
|
|
26
|
+
*/
|
|
27
|
+
retryMax?: number;
|
|
28
|
+
/**
|
|
29
|
+
* The delay between retries in seconds.
|
|
30
|
+
*/
|
|
31
|
+
retryDelaySec?: number;
|
|
32
|
+
/**
|
|
33
|
+
* A function to log errors.
|
|
34
|
+
*/
|
|
35
|
+
logFn?: LogFn;
|
|
36
|
+
/**
|
|
37
|
+
* A function to call when the watch fails after the maximum number of retries.
|
|
38
|
+
*/
|
|
39
|
+
retryFail?: (e: Error) => void;
|
|
40
|
+
};
|
|
4
41
|
/**
|
|
5
42
|
* Execute a watch on the specified resource.
|
|
6
43
|
*/
|
|
7
|
-
export declare function ExecWatch<T extends GenericClass>(model: T, filters: Filters, callback: WatchAction<T
|
|
44
|
+
export declare function ExecWatch<T extends GenericClass>(model: T, filters: Filters, callback: WatchAction<T>, watchCfg?: WatchCfg): Promise<WatchController>;
|
|
8
45
|
//# sourceMappingURL=watch.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"watch.d.ts","sourceRoot":"","sources":["../../src/fluent/watch.ts"],"names":[],"mappings":";AAMA,OAAO,EAAE,YAAY,EAAE,MAAM,UAAU,CAAC;
|
|
1
|
+
{"version":3,"file":"watch.d.ts","sourceRoot":"","sources":["../../src/fluent/watch.ts"],"names":[],"mappings":";AAMA,OAAO,EAAE,YAAY,EAAE,KAAK,EAAE,MAAM,UAAU,CAAC;AAC/C,OAAO,EAAE,OAAO,EAAE,WAAW,EAAc,MAAM,SAAS,CAAC;AAG3D;;GAEG;AACH,MAAM,MAAM,eAAe,GAAG;IAC5B;;;;OAIG;IACH,KAAK,EAAE,CAAC,MAAM,CAAC,EAAE,MAAM,KAAK,IAAI,CAAC;IACjC;;;OAGG;IACH,MAAM,EAAE,MAAM,WAAW,CAAC;CAC3B,CAAC;AAEF;;GAEG;AACH,MAAM,MAAM,QAAQ,GAAG;IACrB;;OAEG;IACH,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB;;OAEG;IACH,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB;;OAEG;IACH,KAAK,CAAC,EAAE,KAAK,CAAC;IACd;;OAEG;IACH,SAAS,CAAC,EAAE,CAAC,CAAC,EAAE,KAAK,KAAK,IAAI,CAAC;CAChC,CAAC;AAEF;;GAEG;AACH,wBAAsB,SAAS,CAAC,CAAC,SAAS,YAAY,EACpD,KAAK,EAAE,CAAC,EACR,OAAO,EAAE,OAAO,EAChB,QAAQ,EAAE,WAAW,CAAC,CAAC,CAAC,EACxB,QAAQ,GAAE,QAAa,4BAwJxB"}
|
package/dist/fluent/watch.js
CHANGED
|
@@ -6,13 +6,14 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
|
6
6
|
};
|
|
7
7
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
8
8
|
exports.ExecWatch = void 0;
|
|
9
|
-
const
|
|
9
|
+
const byline_1 = __importDefault(require("byline"));
|
|
10
10
|
const node_fetch_1 = __importDefault(require("node-fetch"));
|
|
11
11
|
const utils_1 = require("./utils");
|
|
12
12
|
/**
|
|
13
13
|
* Execute a watch on the specified resource.
|
|
14
14
|
*/
|
|
15
|
-
async function ExecWatch(model, filters, callback) {
|
|
15
|
+
async function ExecWatch(model, filters, callback, watchCfg = {}) {
|
|
16
|
+
watchCfg.logFn?.({ model, filters, watchCfg }, "ExecWatch");
|
|
16
17
|
// Build the path and query params for the resource, excluding the name
|
|
17
18
|
const { opts, serverUrl } = await (0, utils_1.k8sCfg)("GET");
|
|
18
19
|
const url = (0, utils_1.pathBuilder)(serverUrl, model, filters, true);
|
|
@@ -24,55 +25,110 @@ async function ExecWatch(model, filters, callback) {
|
|
|
24
25
|
if (filters.name) {
|
|
25
26
|
url.searchParams.set("fieldSelector", `metadata.name=${filters.name}`);
|
|
26
27
|
}
|
|
27
|
-
//
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
28
|
+
// Set the initial timeout to 15 seconds
|
|
29
|
+
opts.timeout = 15 * 1000;
|
|
30
|
+
// Enable keep alive
|
|
31
|
+
opts.agent.keepAlive = true;
|
|
32
|
+
// Track the number of retries
|
|
33
|
+
let retryCount = 0;
|
|
34
|
+
// Set the maximum number of retries to 5 if not specified
|
|
35
|
+
watchCfg.retryMax ??= 5;
|
|
36
|
+
// Set the retry delay to 5 seconds if not specified
|
|
37
|
+
watchCfg.retryDelaySec ??= 5;
|
|
38
|
+
// Create a throwaway AbortController to setup the wrapped AbortController
|
|
39
|
+
let abortController;
|
|
40
|
+
// Create a wrapped AbortController to allow the watch to be aborted externally
|
|
41
|
+
const abortWrapper = {};
|
|
42
|
+
function bindAbortController() {
|
|
43
|
+
// Create a new AbortController
|
|
44
|
+
abortController = new AbortController();
|
|
45
|
+
// Update the abort wrapper
|
|
46
|
+
abortWrapper.abort = reason => abortController.abort(reason);
|
|
47
|
+
abortWrapper.signal = () => abortController.signal;
|
|
48
|
+
// Add the abort signal to the request options
|
|
49
|
+
opts.signal = abortController.signal;
|
|
50
|
+
}
|
|
51
|
+
async function runner() {
|
|
52
|
+
let doneCalled = false;
|
|
53
|
+
bindAbortController();
|
|
54
|
+
// Create a stream to read the response body
|
|
55
|
+
const stream = byline_1.default.createStream();
|
|
56
|
+
const onError = (err) => {
|
|
57
|
+
stream.removeAllListeners();
|
|
58
|
+
if (!doneCalled) {
|
|
59
|
+
doneCalled = true;
|
|
60
|
+
// If the error is not an AbortError, reload the watch
|
|
61
|
+
if (err.name !== "AbortError") {
|
|
62
|
+
watchCfg.logFn?.(err, "stream error");
|
|
63
|
+
void reload(err);
|
|
60
64
|
}
|
|
61
|
-
|
|
62
|
-
|
|
65
|
+
else {
|
|
66
|
+
watchCfg.logFn?.("watch aborted via WatchController.abort()");
|
|
63
67
|
}
|
|
64
|
-
}
|
|
68
|
+
}
|
|
69
|
+
};
|
|
70
|
+
const cleanup = () => {
|
|
71
|
+
if (!doneCalled) {
|
|
72
|
+
doneCalled = true;
|
|
73
|
+
stream.removeAllListeners();
|
|
74
|
+
}
|
|
75
|
+
};
|
|
76
|
+
try {
|
|
77
|
+
// Make the actual request
|
|
78
|
+
const response = await (0, node_fetch_1.default)(url, { ...opts });
|
|
79
|
+
// If the request is successful, start listening for events
|
|
80
|
+
if (response.ok) {
|
|
81
|
+
const { body } = response;
|
|
82
|
+
// Reset the retry count
|
|
83
|
+
retryCount = 0;
|
|
84
|
+
stream.on("error", onError);
|
|
85
|
+
stream.on("close", cleanup);
|
|
86
|
+
stream.on("finish", cleanup);
|
|
87
|
+
// Listen for events and call the callback function
|
|
88
|
+
stream.on("data", line => {
|
|
89
|
+
try {
|
|
90
|
+
// Parse the event payload
|
|
91
|
+
const { object: payload, type: phase } = JSON.parse(line);
|
|
92
|
+
// Call the callback function with the parsed payload
|
|
93
|
+
void callback(payload, phase);
|
|
94
|
+
}
|
|
95
|
+
catch (err) {
|
|
96
|
+
watchCfg.logFn?.(err, "watch callback error");
|
|
97
|
+
}
|
|
98
|
+
});
|
|
99
|
+
body.on("error", onError);
|
|
100
|
+
body.on("close", cleanup);
|
|
101
|
+
body.on("finish", cleanup);
|
|
102
|
+
// Pipe the response body to the stream
|
|
103
|
+
body.pipe(stream);
|
|
104
|
+
}
|
|
105
|
+
else {
|
|
106
|
+
throw new Error(`watch failed: ${response.status} ${response.statusText}`);
|
|
107
|
+
}
|
|
65
108
|
}
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
109
|
+
catch (e) {
|
|
110
|
+
onError(e);
|
|
111
|
+
}
|
|
112
|
+
// On unhandled errors, retry the watch
|
|
113
|
+
async function reload(e) {
|
|
114
|
+
// If there are more attempts, retry the watch
|
|
115
|
+
if (watchCfg.retryMax > retryCount) {
|
|
116
|
+
retryCount++;
|
|
117
|
+
watchCfg.logFn?.(`retrying watch ${retryCount}/${watchCfg.retryMax}`);
|
|
118
|
+
// Sleep for the specified delay or 5 seconds
|
|
119
|
+
await new Promise(r => setTimeout(r, watchCfg.retryDelaySec * 1000));
|
|
120
|
+
// Retry the watch after the delay
|
|
121
|
+
await runner();
|
|
122
|
+
}
|
|
123
|
+
else {
|
|
124
|
+
// Otherwise, call the finally function if it exists
|
|
125
|
+
if (watchCfg.retryFail) {
|
|
126
|
+
watchCfg.retryFail(e);
|
|
127
|
+
}
|
|
128
|
+
}
|
|
71
129
|
}
|
|
72
130
|
}
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
}
|
|
76
|
-
return controller;
|
|
131
|
+
await runner();
|
|
132
|
+
return abortWrapper;
|
|
77
133
|
}
|
|
78
134
|
exports.ExecWatch = ExecWatch;
|
package/dist/index.d.ts
CHANGED
|
@@ -6,4 +6,5 @@ export { StatusCodes as fetchStatus } from "http-status-codes";
|
|
|
6
6
|
export { K8s } from "./fluent";
|
|
7
7
|
export { RegisterKind, modelToGroupVersionKind } from "./kinds";
|
|
8
8
|
export * from "./types";
|
|
9
|
+
export * as K8sClientNode from "@kubernetes/client-node";
|
|
9
10
|
//# sourceMappingURL=index.d.ts.map
|
package/dist/index.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAIA,OAAO,KAAK,IAAI,MAAM,YAAY,CAAC;AAEnC,oGAAoG;AACpG,OAAO,EAAE,IAAI,EAAE,CAAC;AAGhB,OAAO,EAAE,KAAK,EAAE,MAAM,SAAS,CAAC;AAGhC,OAAO,EAAE,WAAW,IAAI,WAAW,EAAE,MAAM,mBAAmB,CAAC;AAG/D,OAAO,EAAE,GAAG,EAAE,MAAM,UAAU,CAAC;AAG/B,OAAO,EAAE,YAAY,EAAE,uBAAuB,EAAE,MAAM,SAAS,CAAC;AAEhE,cAAc,SAAS,CAAC"}
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAIA,OAAO,KAAK,IAAI,MAAM,YAAY,CAAC;AAEnC,oGAAoG;AACpG,OAAO,EAAE,IAAI,EAAE,CAAC;AAGhB,OAAO,EAAE,KAAK,EAAE,MAAM,SAAS,CAAC;AAGhC,OAAO,EAAE,WAAW,IAAI,WAAW,EAAE,MAAM,mBAAmB,CAAC;AAG/D,OAAO,EAAE,GAAG,EAAE,MAAM,UAAU,CAAC;AAG/B,OAAO,EAAE,YAAY,EAAE,uBAAuB,EAAE,MAAM,SAAS,CAAC;AAEhE,cAAc,SAAS,CAAC;AAExB,OAAO,KAAK,aAAa,MAAM,yBAAyB,CAAC"}
|
package/dist/index.js
CHANGED
|
@@ -28,7 +28,7 @@ var __exportStar = (this && this.__exportStar) || function(m, exports) {
|
|
|
28
28
|
for (var p in m) if (p !== "default" && !Object.prototype.hasOwnProperty.call(exports, p)) __createBinding(exports, m, p);
|
|
29
29
|
};
|
|
30
30
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
31
|
-
exports.modelToGroupVersionKind = exports.RegisterKind = exports.K8s = exports.fetchStatus = exports.fetch = exports.kind = void 0;
|
|
31
|
+
exports.K8sClientNode = exports.modelToGroupVersionKind = exports.RegisterKind = exports.K8s = exports.fetchStatus = exports.fetch = exports.kind = void 0;
|
|
32
32
|
// Export kinds as a single object
|
|
33
33
|
const kind = __importStar(require("./upstream"));
|
|
34
34
|
exports.kind = kind;
|
|
@@ -46,3 +46,4 @@ var kinds_1 = require("./kinds");
|
|
|
46
46
|
Object.defineProperty(exports, "RegisterKind", { enumerable: true, get: function () { return kinds_1.RegisterKind; } });
|
|
47
47
|
Object.defineProperty(exports, "modelToGroupVersionKind", { enumerable: true, get: function () { return kinds_1.modelToGroupVersionKind; } });
|
|
48
48
|
__exportStar(require("./types"), exports);
|
|
49
|
+
exports.K8sClientNode = __importStar(require("@kubernetes/client-node"));
|
package/dist/kinds.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"kinds.d.ts","sourceRoot":"","sources":["../src/kinds.ts"],"names":[],"mappings":"AAGA,OAAO,EAAE,YAAY,EAAE,gBAAgB,EAAE,MAAM,SAAS,CAAC;
|
|
1
|
+
{"version":3,"file":"kinds.d.ts","sourceRoot":"","sources":["../src/kinds.ts"],"names":[],"mappings":"AAGA,OAAO,EAAE,YAAY,EAAE,gBAAgB,EAAE,MAAM,SAAS,CAAC;AAwgBzD,wBAAgB,uBAAuB,CAAC,GAAG,EAAE,MAAM,GAAG,gBAAgB,CAErE;AAED;;;;;GAKG;AACH,eAAO,MAAM,YAAY,UAAW,YAAY,oBAAoB,gBAAgB,SAUnF,CAAC"}
|
package/dist/kinds.js
CHANGED
|
@@ -4,6 +4,19 @@
|
|
|
4
4
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
5
5
|
exports.RegisterKind = exports.modelToGroupVersionKind = void 0;
|
|
6
6
|
const gvkMap = {
|
|
7
|
+
/**
|
|
8
|
+
* Represents a K8s Event resource.
|
|
9
|
+
* Event is a report of an event somewhere in the cluster. It generally denotes some state change in the system.
|
|
10
|
+
* Events have a limited retention time and triggers and messages may evolve with time. Event consumers should not
|
|
11
|
+
* rely on the timing of an event with a given Reason reflecting a consistent underlying trigger, or the continued
|
|
12
|
+
* existence of events with that Reason. Events should be treated as informative, best-effort, supplemental data.
|
|
13
|
+
* @see {@link https://kubernetes.io/docs/reference/kubernetes-api/cluster-resources/event-v1/}
|
|
14
|
+
*/
|
|
15
|
+
CoreV1Event: {
|
|
16
|
+
kind: "Event",
|
|
17
|
+
version: "v1",
|
|
18
|
+
group: "events.k8s.io",
|
|
19
|
+
},
|
|
7
20
|
/**
|
|
8
21
|
* Represents a K8s ClusterRole resource.
|
|
9
22
|
* ClusterRole is a set of permissions that can be bound to a user or group in a cluster-wide scope.
|
package/dist/kinds.test.js
CHANGED
|
@@ -6,6 +6,10 @@ const globals_1 = require("@jest/globals");
|
|
|
6
6
|
const index_1 = require("./index");
|
|
7
7
|
const kinds_1 = require("./kinds");
|
|
8
8
|
const testCases = [
|
|
9
|
+
{
|
|
10
|
+
name: index_1.kind.Event,
|
|
11
|
+
expected: { group: "events.k8s.io", version: "v1", kind: "Event" },
|
|
12
|
+
},
|
|
9
13
|
{
|
|
10
14
|
name: index_1.kind.ClusterRole,
|
|
11
15
|
expected: { group: "rbac.authorization.k8s.io", version: "v1", kind: "ClusterRole" },
|
package/dist/types.d.ts
CHANGED
|
@@ -24,4 +24,9 @@ export interface GroupVersionKind {
|
|
|
24
24
|
/** Optional, override the plural name for use in Webhook rules generation */
|
|
25
25
|
readonly plural?: string;
|
|
26
26
|
}
|
|
27
|
+
export interface LogFn {
|
|
28
|
+
<T extends object>(obj: T, msg?: string, ...args: never[]): void;
|
|
29
|
+
(obj: unknown, msg?: string, ...args: never[]): void;
|
|
30
|
+
(msg: string, ...args: never[]): void;
|
|
31
|
+
}
|
|
27
32
|
//# sourceMappingURL=types.d.ts.map
|
package/dist/types.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"AAGA,OAAO,EAAE,gBAAgB,EAAE,YAAY,EAAE,MAAM,yBAAyB,CAAC;AAEzE,OAAO,EAAE,gBAAgB,EAAE,oBAAoB,EAAE,MAAM,yBAAyB,CAAC;AAGjF,MAAM,MAAM,YAAY,GAAG,QAAQ,WAAW,GAAG,CAAC;AAElD;;;;GAIG;AACH,qBAAa,WAAY,YAAW,gBAAgB;IAClD,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,QAAQ,CAAC,EAAE,YAAY,CAAC;IAExB,CAAC,GAAG,EAAE,MAAM,GAAG,GAAG,CAAC;CACpB;AAED;;;IAGI;AACJ,MAAM,WAAW,gBAAgB;IAC/B,yCAAyC;IACzC,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;IACvB,QAAQ,CAAC,OAAO,CAAC,EAAE,MAAM,CAAC;IAC1B,6EAA6E;IAC7E,QAAQ,CAAC,MAAM,CAAC,EAAE,MAAM,CAAC;CAC1B"}
|
|
1
|
+
{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"AAGA,OAAO,EAAE,gBAAgB,EAAE,YAAY,EAAE,MAAM,yBAAyB,CAAC;AAEzE,OAAO,EAAE,gBAAgB,EAAE,oBAAoB,EAAE,MAAM,yBAAyB,CAAC;AAGjF,MAAM,MAAM,YAAY,GAAG,QAAQ,WAAW,GAAG,CAAC;AAElD;;;;GAIG;AACH,qBAAa,WAAY,YAAW,gBAAgB;IAClD,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,QAAQ,CAAC,EAAE,YAAY,CAAC;IAExB,CAAC,GAAG,EAAE,MAAM,GAAG,GAAG,CAAC;CACpB;AAED;;;IAGI;AACJ,MAAM,WAAW,gBAAgB;IAC/B,yCAAyC;IACzC,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;IACvB,QAAQ,CAAC,OAAO,CAAC,EAAE,MAAM,CAAC;IAC1B,6EAA6E;IAC7E,QAAQ,CAAC,MAAM,CAAC,EAAE,MAAM,CAAC;CAC1B;AAED,MAAM,WAAW,KAAK;IAEpB,CAAC,CAAC,SAAS,MAAM,EAAE,GAAG,EAAE,CAAC,EAAE,GAAG,CAAC,EAAE,MAAM,EAAE,GAAG,IAAI,EAAE,KAAK,EAAE,GAAG,IAAI,CAAC;IACjE,CAAC,GAAG,EAAE,OAAO,EAAE,GAAG,CAAC,EAAE,MAAM,EAAE,GAAG,IAAI,EAAE,KAAK,EAAE,GAAG,IAAI,CAAC;IACrD,CAAC,GAAG,EAAE,MAAM,EAAE,GAAG,IAAI,EAAE,KAAK,EAAE,GAAG,IAAI,CAAC;CACvC"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "kubernetes-fluent-client",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.4.0",
|
|
4
4
|
"description": "A @kubernetes/client-node fluent API wrapper that leverages K8s Server Side Apply",
|
|
5
5
|
"main": "dist/index.js",
|
|
6
6
|
"types": "dist/index.d.ts",
|
|
@@ -35,16 +35,19 @@
|
|
|
35
35
|
"homepage": "https://github.com/defenseunicorns/kubernetes-fluent-client#readme",
|
|
36
36
|
"dependencies": {
|
|
37
37
|
"@kubernetes/client-node": "1.0.0-rc3",
|
|
38
|
+
"byline": "5.0.0",
|
|
38
39
|
"fast-json-patch": "3.1.1",
|
|
39
40
|
"http-status-codes": "2.3.0",
|
|
40
|
-
"node-fetch": "2.7.0"
|
|
41
|
+
"node-fetch": "2.7.0",
|
|
42
|
+
"type-fest": "4.3.2"
|
|
41
43
|
},
|
|
42
44
|
"devDependencies": {
|
|
43
|
-
"@commitlint/cli": "17.7.
|
|
45
|
+
"@commitlint/cli": "17.7.2",
|
|
44
46
|
"@commitlint/config-conventional": "17.7.0",
|
|
45
47
|
"@jest/globals": "29.7.0",
|
|
46
|
-
"@
|
|
47
|
-
"@typescript-eslint/
|
|
48
|
+
"@types/byline": "4.2.34",
|
|
49
|
+
"@typescript-eslint/eslint-plugin": "6.7.3",
|
|
50
|
+
"@typescript-eslint/parser": "6.7.3",
|
|
48
51
|
"jest": "29.7.0",
|
|
49
52
|
"nock": "13.3.3",
|
|
50
53
|
"prettier": "3.0.3",
|
package/src/fluent/index.test.ts
CHANGED
|
@@ -133,4 +133,18 @@ describe("Kube", () => {
|
|
|
133
133
|
const kube = K8s(Pod);
|
|
134
134
|
await expect(kube.Patch([])).rejects.toThrow("No operations specified");
|
|
135
135
|
});
|
|
136
|
+
|
|
137
|
+
it("should allow Apply of deep partials", async () => {
|
|
138
|
+
const kube = K8s(Pod);
|
|
139
|
+
const result = await kube.Apply({ metadata: { name: "fake" }, spec: { priority: 3 } });
|
|
140
|
+
expect(result).toEqual(fakeResource);
|
|
141
|
+
});
|
|
142
|
+
|
|
143
|
+
it("should throw an error if a Delete failed for a reason other than Not Found", async () => {
|
|
144
|
+
mockedKubeExec.mockRejectedValueOnce({ status: 500 }); // Internal Server Error on first call
|
|
145
|
+
const kube = K8s(Pod);
|
|
146
|
+
await expect(kube.Delete("fakeResource")).rejects.toEqual(
|
|
147
|
+
expect.objectContaining({ status: 500 }),
|
|
148
|
+
);
|
|
149
|
+
});
|
|
136
150
|
});
|
package/src/fluent/index.ts
CHANGED
|
@@ -4,12 +4,13 @@
|
|
|
4
4
|
import { KubernetesListObject, KubernetesObject } from "@kubernetes/client-node";
|
|
5
5
|
import { Operation } from "fast-json-patch";
|
|
6
6
|
import { StatusCodes } from "http-status-codes";
|
|
7
|
+
import type { PartialDeep } from "type-fest";
|
|
7
8
|
|
|
8
9
|
import { modelToGroupVersionKind } from "../kinds";
|
|
9
10
|
import { GenericClass } from "../types";
|
|
10
11
|
import { Filters, K8sInit, Paths, WatchAction } from "./types";
|
|
11
12
|
import { k8sExec } from "./utils";
|
|
12
|
-
import { ExecWatch } from "./watch";
|
|
13
|
+
import { ExecWatch, WatchCfg } from "./watch";
|
|
13
14
|
|
|
14
15
|
/**
|
|
15
16
|
* Kubernetes fluent API inspired by Kubectl. Pass in a model, then call filters and actions on it.
|
|
@@ -99,8 +100,8 @@ export function K8s<T extends GenericClass, K extends KubernetesObject = Instanc
|
|
|
99
100
|
}
|
|
100
101
|
}
|
|
101
102
|
|
|
102
|
-
async function Apply(resource: K): Promise<K> {
|
|
103
|
-
syncFilters(resource);
|
|
103
|
+
async function Apply(resource: PartialDeep<K>): Promise<K> {
|
|
104
|
+
syncFilters(resource as K);
|
|
104
105
|
return k8sExec(model, filters, "APPLY", resource);
|
|
105
106
|
}
|
|
106
107
|
|
|
@@ -118,8 +119,8 @@ export function K8s<T extends GenericClass, K extends KubernetesObject = Instanc
|
|
|
118
119
|
return k8sExec<T, K>(model, filters, "PATCH", payload);
|
|
119
120
|
}
|
|
120
121
|
|
|
121
|
-
async function Watch(callback: WatchAction<T
|
|
122
|
-
|
|
122
|
+
async function Watch(callback: WatchAction<T>, watchCfg?: WatchCfg) {
|
|
123
|
+
return ExecWatch(model, filters, callback, watchCfg);
|
|
123
124
|
}
|
|
124
125
|
|
|
125
126
|
return { InNamespace, Apply, Create, Patch, ...withFilters };
|
package/src/fluent/types.ts
CHANGED
|
@@ -3,8 +3,10 @@
|
|
|
3
3
|
|
|
4
4
|
import { KubernetesListObject, KubernetesObject } from "@kubernetes/client-node";
|
|
5
5
|
import { Operation } from "fast-json-patch";
|
|
6
|
+
import type { PartialDeep } from "type-fest";
|
|
6
7
|
|
|
7
8
|
import { GenericClass, GroupVersionKind } from "../types";
|
|
9
|
+
import { WatchCfg, WatchController } from "./watch";
|
|
8
10
|
|
|
9
11
|
/**
|
|
10
12
|
* The Phase matched when using the K8s Watch API.
|
|
@@ -50,7 +52,10 @@ export type K8sFilteredActions<K extends KubernetesObject> = {
|
|
|
50
52
|
* @param callback
|
|
51
53
|
* @returns
|
|
52
54
|
*/
|
|
53
|
-
Watch: (
|
|
55
|
+
Watch: (
|
|
56
|
+
callback: (payload: K, phase: WatchPhase) => void,
|
|
57
|
+
watchCfg?: WatchCfg,
|
|
58
|
+
) => Promise<WatchController>;
|
|
54
59
|
};
|
|
55
60
|
|
|
56
61
|
export type K8sUnfilteredActions<K extends KubernetesObject> = {
|
|
@@ -60,7 +65,7 @@ export type K8sUnfilteredActions<K extends KubernetesObject> = {
|
|
|
60
65
|
* @param resource
|
|
61
66
|
* @returns
|
|
62
67
|
*/
|
|
63
|
-
Apply: (resource: K) => Promise<K>;
|
|
68
|
+
Apply: (resource: PartialDeep<K>) => Promise<K>;
|
|
64
69
|
|
|
65
70
|
/**
|
|
66
71
|
* Create the provided K8s resource or throw an error if it already exists.
|
|
@@ -71,7 +76,7 @@ export type K8sUnfilteredActions<K extends KubernetesObject> = {
|
|
|
71
76
|
Create: (resource: K) => Promise<K>;
|
|
72
77
|
|
|
73
78
|
/**
|
|
74
|
-
* Advanced JSON Patch operations for when Server Side Apply,
|
|
79
|
+
* Advanced JSON Patch operations for when Server Side Apply, K8s().Apply(), is insufficient.
|
|
75
80
|
*
|
|
76
81
|
* Note: Throws an error on an empty list of patch operations.
|
|
77
82
|
*
|
|
@@ -87,7 +92,7 @@ export type K8sWithFilters<K extends KubernetesObject> = K8sFilteredActions<K> &
|
|
|
87
92
|
* Note multiple calls to this method will result in an AND condition. e.g.
|
|
88
93
|
*
|
|
89
94
|
* ```ts
|
|
90
|
-
*
|
|
95
|
+
* K8s(kind.Deployment)
|
|
91
96
|
* .WithField("metadata.name", "bar")
|
|
92
97
|
* .WithField("metadata.namespace", "qux")
|
|
93
98
|
* .Delete(...)
|
|
@@ -106,7 +111,7 @@ export type K8sWithFilters<K extends KubernetesObject> = K8sFilteredActions<K> &
|
|
|
106
111
|
* Note multiple calls to this method will result in an AND condition. e.g.
|
|
107
112
|
*
|
|
108
113
|
* ```ts
|
|
109
|
-
*
|
|
114
|
+
* K8s(kind.Deployment)
|
|
110
115
|
* .WithLabel("foo", "bar")
|
|
111
116
|
* .WithLabel("baz", "qux")
|
|
112
117
|
* .Delete(...)
|
package/src/fluent/utils.ts
CHANGED
package/src/fluent/watch.ts
CHANGED
|
@@ -1,13 +1,52 @@
|
|
|
1
1
|
// SPDX-License-Identifier: Apache-2.0
|
|
2
2
|
// SPDX-FileCopyrightText: 2023-Present The Pepr Authors
|
|
3
3
|
|
|
4
|
-
import
|
|
5
|
-
|
|
4
|
+
import byline from "byline";
|
|
6
5
|
import fetch from "node-fetch";
|
|
7
|
-
|
|
6
|
+
|
|
7
|
+
import { GenericClass, LogFn } from "../types";
|
|
8
8
|
import { Filters, WatchAction, WatchPhase } from "./types";
|
|
9
9
|
import { k8sCfg, pathBuilder } from "./utils";
|
|
10
10
|
|
|
11
|
+
/**
|
|
12
|
+
* Wrapper for the AbortController to allow the watch to be aborted externally.
|
|
13
|
+
*/
|
|
14
|
+
export type WatchController = {
|
|
15
|
+
/**
|
|
16
|
+
* Abort the watch.
|
|
17
|
+
* @param reason optional reason for aborting the watch
|
|
18
|
+
* @returns
|
|
19
|
+
*/
|
|
20
|
+
abort: (reason?: string) => void;
|
|
21
|
+
/**
|
|
22
|
+
* Get the AbortSignal for the watch.
|
|
23
|
+
* @returns
|
|
24
|
+
*/
|
|
25
|
+
signal: () => AbortSignal;
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Configuration for the watch function.
|
|
30
|
+
*/
|
|
31
|
+
export type WatchCfg = {
|
|
32
|
+
/**
|
|
33
|
+
* The maximum number of times to retry the watch, the retry count is reset on success.
|
|
34
|
+
*/
|
|
35
|
+
retryMax?: number;
|
|
36
|
+
/**
|
|
37
|
+
* The delay between retries in seconds.
|
|
38
|
+
*/
|
|
39
|
+
retryDelaySec?: number;
|
|
40
|
+
/**
|
|
41
|
+
* A function to log errors.
|
|
42
|
+
*/
|
|
43
|
+
logFn?: LogFn;
|
|
44
|
+
/**
|
|
45
|
+
* A function to call when the watch fails after the maximum number of retries.
|
|
46
|
+
*/
|
|
47
|
+
retryFail?: (e: Error) => void;
|
|
48
|
+
};
|
|
49
|
+
|
|
11
50
|
/**
|
|
12
51
|
* Execute a watch on the specified resource.
|
|
13
52
|
*/
|
|
@@ -15,7 +54,10 @@ export async function ExecWatch<T extends GenericClass>(
|
|
|
15
54
|
model: T,
|
|
16
55
|
filters: Filters,
|
|
17
56
|
callback: WatchAction<T>,
|
|
57
|
+
watchCfg: WatchCfg = {},
|
|
18
58
|
) {
|
|
59
|
+
watchCfg.logFn?.({ model, filters, watchCfg }, "ExecWatch");
|
|
60
|
+
|
|
19
61
|
// Build the path and query params for the resource, excluding the name
|
|
20
62
|
const { opts, serverUrl } = await k8sCfg("GET");
|
|
21
63
|
const url = pathBuilder(serverUrl, model, filters, true);
|
|
@@ -31,64 +73,137 @@ export async function ExecWatch<T extends GenericClass>(
|
|
|
31
73
|
url.searchParams.set("fieldSelector", `metadata.name=${filters.name}`);
|
|
32
74
|
}
|
|
33
75
|
|
|
34
|
-
//
|
|
35
|
-
|
|
36
|
-
|
|
76
|
+
// Set the initial timeout to 15 seconds
|
|
77
|
+
opts.timeout = 15 * 1000;
|
|
78
|
+
|
|
79
|
+
// Enable keep alive
|
|
80
|
+
(opts.agent as unknown as { keepAlive: boolean }).keepAlive = true;
|
|
81
|
+
|
|
82
|
+
// Track the number of retries
|
|
83
|
+
let retryCount = 0;
|
|
84
|
+
|
|
85
|
+
// Set the maximum number of retries to 5 if not specified
|
|
86
|
+
watchCfg.retryMax ??= 5;
|
|
87
|
+
|
|
88
|
+
// Set the retry delay to 5 seconds if not specified
|
|
89
|
+
watchCfg.retryDelaySec ??= 5;
|
|
90
|
+
|
|
91
|
+
// Create a throwaway AbortController to setup the wrapped AbortController
|
|
92
|
+
let abortController: AbortController;
|
|
93
|
+
|
|
94
|
+
// Create a wrapped AbortController to allow the watch to be aborted externally
|
|
95
|
+
const abortWrapper = {} as WatchController;
|
|
96
|
+
|
|
97
|
+
function bindAbortController() {
|
|
98
|
+
// Create a new AbortController
|
|
99
|
+
abortController = new AbortController();
|
|
100
|
+
|
|
101
|
+
// Update the abort wrapper
|
|
102
|
+
abortWrapper.abort = reason => abortController.abort(reason);
|
|
103
|
+
abortWrapper.signal = () => abortController.signal;
|
|
104
|
+
|
|
105
|
+
// Add the abort signal to the request options
|
|
106
|
+
opts.signal = abortController.signal;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
async function runner() {
|
|
110
|
+
let doneCalled = false;
|
|
37
111
|
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
112
|
+
bindAbortController();
|
|
113
|
+
|
|
114
|
+
// Create a stream to read the response body
|
|
115
|
+
const stream = byline.createStream();
|
|
116
|
+
|
|
117
|
+
const onError = (err: Error) => {
|
|
118
|
+
stream.removeAllListeners();
|
|
119
|
+
|
|
120
|
+
if (!doneCalled) {
|
|
121
|
+
doneCalled = true;
|
|
122
|
+
|
|
123
|
+
// If the error is not an AbortError, reload the watch
|
|
124
|
+
if (err.name !== "AbortError") {
|
|
125
|
+
watchCfg.logFn?.(err, "stream error");
|
|
126
|
+
void reload(err);
|
|
127
|
+
} else {
|
|
128
|
+
watchCfg.logFn?.("watch aborted via WatchController.abort()");
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
};
|
|
132
|
+
|
|
133
|
+
const cleanup = () => {
|
|
134
|
+
if (!doneCalled) {
|
|
135
|
+
doneCalled = true;
|
|
136
|
+
stream.removeAllListeners();
|
|
137
|
+
}
|
|
138
|
+
};
|
|
139
|
+
|
|
140
|
+
try {
|
|
141
|
+
// Make the actual request
|
|
142
|
+
const response = await fetch(url, { ...opts });
|
|
143
|
+
|
|
144
|
+
// If the request is successful, start listening for events
|
|
145
|
+
if (response.ok) {
|
|
146
|
+
const { body } = response;
|
|
147
|
+
|
|
148
|
+
// Reset the retry count
|
|
149
|
+
retryCount = 0;
|
|
150
|
+
|
|
151
|
+
stream.on("error", onError);
|
|
152
|
+
stream.on("close", cleanup);
|
|
153
|
+
stream.on("finish", cleanup);
|
|
154
|
+
|
|
155
|
+
// Listen for events and call the callback function
|
|
156
|
+
stream.on("data", line => {
|
|
157
|
+
try {
|
|
158
|
+
// Parse the event payload
|
|
159
|
+
const { object: payload, type: phase } = JSON.parse(line) as {
|
|
160
|
+
type: WatchPhase;
|
|
161
|
+
object: InstanceType<T>;
|
|
162
|
+
};
|
|
163
|
+
|
|
164
|
+
// Call the callback function with the parsed payload
|
|
165
|
+
void callback(payload, phase as WatchPhase);
|
|
166
|
+
} catch (err) {
|
|
167
|
+
watchCfg.logFn?.(err, "watch callback error");
|
|
168
|
+
}
|
|
169
|
+
});
|
|
170
|
+
|
|
171
|
+
body.on("error", onError);
|
|
172
|
+
body.on("close", cleanup);
|
|
173
|
+
body.on("finish", cleanup);
|
|
174
|
+
|
|
175
|
+
// Pipe the response body to the stream
|
|
176
|
+
body.pipe(stream);
|
|
177
|
+
} else {
|
|
178
|
+
throw new Error(`watch failed: ${response.status} ${response.statusText}`);
|
|
179
|
+
}
|
|
180
|
+
} catch (e) {
|
|
181
|
+
onError(e);
|
|
44
182
|
}
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
terminal: false,
|
|
64
|
-
});
|
|
65
|
-
|
|
66
|
-
// Listen for events and call the callback function
|
|
67
|
-
rl.on("line", line => {
|
|
68
|
-
try {
|
|
69
|
-
// Parse the event payload
|
|
70
|
-
const { object: payload, type: phase } = JSON.parse(line) as {
|
|
71
|
-
type: WatchPhase;
|
|
72
|
-
object: InstanceType<T>;
|
|
73
|
-
};
|
|
74
|
-
|
|
75
|
-
// Call the callback function with the parsed payload
|
|
76
|
-
void callback(payload, phase as WatchPhase);
|
|
77
|
-
} catch (ignore) {
|
|
78
|
-
// ignore parse errors
|
|
183
|
+
|
|
184
|
+
// On unhandled errors, retry the watch
|
|
185
|
+
async function reload(e: Error) {
|
|
186
|
+
// If there are more attempts, retry the watch
|
|
187
|
+
if (watchCfg.retryMax! > retryCount) {
|
|
188
|
+
retryCount++;
|
|
189
|
+
|
|
190
|
+
watchCfg.logFn?.(`retrying watch ${retryCount}/${watchCfg.retryMax}`);
|
|
191
|
+
|
|
192
|
+
// Sleep for the specified delay or 5 seconds
|
|
193
|
+
await new Promise(r => setTimeout(r, watchCfg.retryDelaySec! * 1000));
|
|
194
|
+
|
|
195
|
+
// Retry the watch after the delay
|
|
196
|
+
await runner();
|
|
197
|
+
} else {
|
|
198
|
+
// Otherwise, call the finally function if it exists
|
|
199
|
+
if (watchCfg.retryFail) {
|
|
200
|
+
watchCfg.retryFail(e);
|
|
79
201
|
}
|
|
80
|
-
}
|
|
81
|
-
} else {
|
|
82
|
-
// If the request fails, throw an error
|
|
83
|
-
const error = new Error(response.statusText) as Error & {
|
|
84
|
-
statusCode: number | undefined;
|
|
85
|
-
};
|
|
86
|
-
error.statusCode = response.status;
|
|
87
|
-
throw error;
|
|
202
|
+
}
|
|
88
203
|
}
|
|
89
|
-
} catch (e) {
|
|
90
|
-
close(e);
|
|
91
204
|
}
|
|
92
205
|
|
|
93
|
-
|
|
206
|
+
await runner();
|
|
207
|
+
|
|
208
|
+
return abortWrapper;
|
|
94
209
|
}
|
package/src/index.ts
CHANGED
package/src/kinds.test.ts
CHANGED
|
@@ -8,6 +8,10 @@ import { RegisterKind } from "./kinds";
|
|
|
8
8
|
import { GroupVersionKind } from "./types";
|
|
9
9
|
|
|
10
10
|
const testCases = [
|
|
11
|
+
{
|
|
12
|
+
name: kind.Event,
|
|
13
|
+
expected: { group: "events.k8s.io", version: "v1", kind: "Event" },
|
|
14
|
+
},
|
|
11
15
|
{
|
|
12
16
|
name: kind.ClusterRole,
|
|
13
17
|
expected: { group: "rbac.authorization.k8s.io", version: "v1", kind: "ClusterRole" },
|
package/src/kinds.ts
CHANGED
|
@@ -4,6 +4,20 @@
|
|
|
4
4
|
import { GenericClass, GroupVersionKind } from "./types";
|
|
5
5
|
|
|
6
6
|
const gvkMap: Record<string, GroupVersionKind> = {
|
|
7
|
+
/**
|
|
8
|
+
* Represents a K8s Event resource.
|
|
9
|
+
* Event is a report of an event somewhere in the cluster. It generally denotes some state change in the system.
|
|
10
|
+
* Events have a limited retention time and triggers and messages may evolve with time. Event consumers should not
|
|
11
|
+
* rely on the timing of an event with a given Reason reflecting a consistent underlying trigger, or the continued
|
|
12
|
+
* existence of events with that Reason. Events should be treated as informative, best-effort, supplemental data.
|
|
13
|
+
* @see {@link https://kubernetes.io/docs/reference/kubernetes-api/cluster-resources/event-v1/}
|
|
14
|
+
*/
|
|
15
|
+
CoreV1Event: {
|
|
16
|
+
kind: "Event",
|
|
17
|
+
version: "v1",
|
|
18
|
+
group: "events.k8s.io",
|
|
19
|
+
},
|
|
20
|
+
|
|
7
21
|
/**
|
|
8
22
|
* Represents a K8s ClusterRole resource.
|
|
9
23
|
* ClusterRole is a set of permissions that can be bound to a user or group in a cluster-wide scope.
|
package/src/types.ts
CHANGED
|
@@ -33,3 +33,10 @@ export interface GroupVersionKind {
|
|
|
33
33
|
/** Optional, override the plural name for use in Webhook rules generation */
|
|
34
34
|
readonly plural?: string;
|
|
35
35
|
}
|
|
36
|
+
|
|
37
|
+
export interface LogFn {
|
|
38
|
+
/* tslint:disable:no-unnecessary-generics */
|
|
39
|
+
<T extends object>(obj: T, msg?: string, ...args: never[]): void;
|
|
40
|
+
(obj: unknown, msg?: string, ...args: never[]): void;
|
|
41
|
+
(msg: string, ...args: never[]): void;
|
|
42
|
+
}
|
|
@@ -1,22 +0,0 @@
|
|
|
1
|
-
// __mocks__/@kubernetes/client-node.ts
|
|
2
|
-
|
|
3
|
-
import { jest } from "@jest/globals";
|
|
4
|
-
|
|
5
|
-
const actual = jest.requireActual("@kubernetes/client-node") as any;
|
|
6
|
-
|
|
7
|
-
const cloned = { ...actual };
|
|
8
|
-
|
|
9
|
-
cloned.KubeConfig = class MockedKubeConfig {
|
|
10
|
-
loadFromDefault = jest.fn();
|
|
11
|
-
|
|
12
|
-
applyToFetchOptions = jest.fn(data => data);
|
|
13
|
-
|
|
14
|
-
getCurrentCluster() {
|
|
15
|
-
return {
|
|
16
|
-
server: "http://jest-test:8080",
|
|
17
|
-
};
|
|
18
|
-
}
|
|
19
|
-
};
|
|
20
|
-
|
|
21
|
-
// export all elements of the mocked module
|
|
22
|
-
module.exports = cloned;
|
package/commitlint.config.js
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
module.exports = { extends: ["@commitlint/config-conventional"] };
|