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.
@@ -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;AAKjF,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"}
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"}
@@ -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
- await (0, watch_1.ExecWatch)(model, filters, callback);
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
  });
@@ -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<void>;
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, Kube().Apply(), is insufficient.
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
- * Kube(given.Deployment)
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
- * Kube(given.Deployment)
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":"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"}
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;;;GAqBhD;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"}
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"}
@@ -86,6 +86,8 @@ async function k8sCfg(method) {
86
86
  "User-Agent": `kubernetes-fluent-client`,
87
87
  },
88
88
  });
89
+ // Enable compression
90
+ opts.compress = true;
89
91
  return { opts, serverUrl: cluster.server };
90
92
  }
91
93
  exports.k8sCfg = k8sCfg;
@@ -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>): Promise<AbortController>;
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;AACxC,OAAO,EAAE,OAAO,EAAE,WAAW,EAAc,MAAM,SAAS,CAAC;AAG3D;;GAEG;AACH,wBAAsB,SAAS,CAAC,CAAC,SAAS,YAAY,EACpD,KAAK,EAAE,CAAC,EACR,OAAO,EAAE,OAAO,EAChB,QAAQ,EAAE,WAAW,CAAC,CAAC,CAAC,4BA6EzB"}
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"}
@@ -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 readline_1 = __importDefault(require("readline"));
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
- // Add abort controller to the long-running request
28
- const controller = new AbortController();
29
- opts.signal = controller.signal;
30
- // Close the connection and make the callback function no-op
31
- let close = (err) => {
32
- controller.abort();
33
- close = () => { };
34
- if (err) {
35
- throw err;
36
- }
37
- };
38
- try {
39
- // Make the actual request
40
- const response = await (0, node_fetch_1.default)(url, opts);
41
- // If the request is successful, start listening for events
42
- if (response.ok) {
43
- const { body } = response;
44
- // Bind connection events to the close function
45
- body.on("error", close);
46
- body.on("close", close);
47
- body.on("finish", close);
48
- // Create a readline interface to parse the stream
49
- const rl = readline_1.default.createInterface({
50
- input: response.body,
51
- terminal: false,
52
- });
53
- // Listen for events and call the callback function
54
- rl.on("line", line => {
55
- try {
56
- // Parse the event payload
57
- const { object: payload, type: phase } = JSON.parse(line);
58
- // Call the callback function with the parsed payload
59
- void callback(payload, phase);
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
- catch (ignore) {
62
- // ignore parse errors
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
- else {
67
- // If the request fails, throw an error
68
- const error = new Error(response.statusText);
69
- error.statusCode = response.status;
70
- throw error;
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
- catch (e) {
74
- close(e);
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
@@ -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"));
@@ -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;AA0fzD,wBAAgB,uBAAuB,CAAC,GAAG,EAAE,MAAM,GAAG,gBAAgB,CAErE;AAED;;;;;GAKG;AACH,eAAO,MAAM,YAAY,UAAW,YAAY,oBAAoB,gBAAgB,SAUnF,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.
@@ -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
@@ -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.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.1",
45
+ "@commitlint/cli": "17.7.2",
44
46
  "@commitlint/config-conventional": "17.7.0",
45
47
  "@jest/globals": "29.7.0",
46
- "@typescript-eslint/eslint-plugin": "6.7.2",
47
- "@typescript-eslint/parser": "6.7.2",
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",
@@ -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
  });
@@ -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>): Promise<void> {
122
- await ExecWatch(model, filters, callback);
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 };
@@ -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: (callback: (payload: K, phase: WatchPhase) => void) => Promise<void>;
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, Kube().Apply(), is insufficient.
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
- * Kube(given.Deployment)
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
- * Kube(given.Deployment)
114
+ * K8s(kind.Deployment)
110
115
  * .WithLabel("foo", "bar")
111
116
  * .WithLabel("baz", "qux")
112
117
  * .Delete(...)
@@ -112,6 +112,9 @@ export async function k8sCfg(method: FetchMethods) {
112
112
  },
113
113
  });
114
114
 
115
+ // Enable compression
116
+ opts.compress = true;
117
+
115
118
  return { opts, serverUrl: cluster.server };
116
119
  }
117
120
 
@@ -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 readline from "readline";
5
-
4
+ import byline from "byline";
6
5
  import fetch from "node-fetch";
7
- import { GenericClass } from "../types";
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
- // Add abort controller to the long-running request
35
- const controller = new AbortController();
36
- opts.signal = controller.signal;
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
- // Close the connection and make the callback function no-op
39
- let close = (err?: Error) => {
40
- controller.abort();
41
- close = () => {};
42
- if (err) {
43
- throw err;
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
- try {
48
- // Make the actual request
49
- const response = await fetch(url, opts);
50
-
51
- // If the request is successful, start listening for events
52
- if (response.ok) {
53
- const { body } = response;
54
-
55
- // Bind connection events to the close function
56
- body.on("error", close);
57
- body.on("close", close);
58
- body.on("finish", close);
59
-
60
- // Create a readline interface to parse the stream
61
- const rl = readline.createInterface({
62
- input: response.body!,
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
- return controller;
206
+ await runner();
207
+
208
+ return abortWrapper;
94
209
  }
package/src/index.ts CHANGED
@@ -20,3 +20,5 @@ export { K8s } from "./fluent";
20
20
  export { RegisterKind, modelToGroupVersionKind } from "./kinds";
21
21
 
22
22
  export * from "./types";
23
+
24
+ export * as K8sClientNode from "@kubernetes/client-node";
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;
@@ -1 +0,0 @@
1
- module.exports = { extends: ["@commitlint/config-conventional"] };