kubernetes-fluent-client 1.3.2 → 1.4.1

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/README.md CHANGED
@@ -5,11 +5,90 @@
5
5
  [![Npm package version](https://badgen.net/npm/v/kubernetes-fluent-client)](https://npmjs.com/package/kubernetes-fluent-client)
6
6
  [![Npm package total downloads](https://badgen.net/npm/dt/kubernetes-fluent-client)](https://npmjs.com/package/kubernetes-fluent-client)
7
7
 
8
+ The Kubernetes Fluent Client for Node is a fluent API for the [Kubernetes JavaScript Client](https://github.com/kubernetes-client/javascript) with some additional logic for [Server Side Apply](https://kubernetes.io/docs/reference/using-api/server-side-apply/), [Watch](https://kubernetes.io/docs/reference/using-api/api-concepts/#efficient-detection-of-changes) with retry/signal control, and [Field Selectors](https://kubernetes.io/docs/concepts/overview/working-with-objects/field-selectors/). In addition to providing a human-friendly API, it also provides a simple way to create and manage resources in the cluster and integrate with K8s in a type-safe way.
9
+
10
+ See below for some example uses of the library.
11
+
8
12
  ```typescript
9
13
  import { K8s, kind } from "kubernetes-fluent-client";
10
14
 
11
- async function main() {
12
- const pods = await K8s(kind.Pod).Get();
15
+ // Let's create a random namespace to work in
16
+ const namespace = "my-namespace" + Math.floor(Math.random() * 1000);
17
+
18
+ // This will be called after the resources are created in the cluster
19
+ async function demo() {
20
+ // Now, we can use the fluent API to query for the resources we just created
21
+
22
+ // You can use watch to monitor resources in the cluster and react to changes
23
+ // This will run until the process is terminated or the watch is aborted
24
+ const ctrl = await K8s(kind.Pod).Watch((pod, phase) => {
25
+ console.log(`Pod ${pod.metadata?.name} is ${phase}`);
26
+ });
27
+
28
+ // Let's abort the watch after 5 seconds
29
+ setTimeout(ctrl.abort, 5 * 1000);
30
+
31
+ // Passing the name to Get() will return a single resource
32
+ const ns = await K8s(kind.Namespace).Get(namespace);
33
+ console.log(ns);
34
+
35
+ // This time we'll use the InNamespace() method to filter the results by namespace and name
36
+ const cm = await K8s(kind.ConfigMap).InNamespace(namespace).Get("my-configmap");
37
+ console.log(cm);
38
+
39
+ // If we don't pass a name to Get(), we'll get a list of resources as KubernetesListObject
40
+ // The matching resources will be in the items property
41
+ const pods = await K8s(kind.Pod).InNamespace(namespace).Get();
13
42
  console.log(pods);
43
+
44
+ // Now let's delete the resources we created, you can pass the name to Delete() or the resource itself
45
+ await K8s(kind.Namespace).Delete(namespace);
46
+
47
+ // Let's use the field selector to find all the running pods in the cluster
48
+ const runningPods = await K8s(kind.Pod).WithField("status.phase", "Running").Get();
49
+ runningPods.items.forEach(pod => {
50
+ console.log(`${pod.metadata?.namespace}/${pod.metadata?.name} is running`);
51
+ });
14
52
  }
53
+
54
+ // Create a few resources to work with: Namespace, ConfigMap, and Pod
55
+ Promise.all([
56
+ // Create the namespace
57
+ K8s(kind.Namespace).Apply({
58
+ metadata: {
59
+ name: namespace,
60
+ },
61
+ }),
62
+
63
+ // Create the ConfigMap in the namespace
64
+ K8s(kind.ConfigMap).Apply({
65
+ metadata: {
66
+ name: "my-configmap",
67
+ namespace,
68
+ },
69
+ data: {
70
+ "my-key": "my-value",
71
+ },
72
+ }),
73
+
74
+ // Create the Pod in the namespace
75
+ K8s(kind.Pod).Apply({
76
+ metadata: {
77
+ name: "my-pod",
78
+ namespace,
79
+ },
80
+ spec: {
81
+ containers: [
82
+ {
83
+ name: "my-container",
84
+ image: "nginx",
85
+ },
86
+ ],
87
+ },
88
+ }),
89
+ ])
90
+ .then(demo)
91
+ .catch(err => {
92
+ console.error(err);
93
+ });
15
94
  ```
@@ -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
- return (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
  }
@@ -1,8 +1,8 @@
1
- /// <reference types="node" />
2
1
  import { KubernetesListObject, KubernetesObject } from "@kubernetes/client-node";
3
2
  import { Operation } from "fast-json-patch";
4
3
  import type { PartialDeep } from "type-fest";
5
4
  import { GenericClass, GroupVersionKind } from "../types";
5
+ import { WatchCfg, WatchController } from "./watch";
6
6
  /**
7
7
  * The Phase matched when using the K8s Watch API.
8
8
  */
@@ -41,7 +41,7 @@ export type K8sFilteredActions<K extends KubernetesObject> = {
41
41
  * @param callback
42
42
  * @returns
43
43
  */
44
- Watch: (callback: (payload: K, phase: WatchPhase) => void) => Promise<AbortController>;
44
+ Watch: (callback: (payload: K, phase: WatchPhase) => void, watchCfg?: WatchCfg) => Promise<WatchController>;
45
45
  };
46
46
  export type K8sUnfilteredActions<K extends KubernetesObject> = {
47
47
  /**
@@ -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;
@@ -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.2",
3
+ "version": "1.4.1",
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,15 +35,17 @@
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
41
  "node-fetch": "2.7.0",
41
42
  "type-fest": "4.3.2"
42
43
  },
43
44
  "devDependencies": {
44
- "@commitlint/cli": "17.7.1",
45
+ "@commitlint/cli": "17.7.2",
45
46
  "@commitlint/config-conventional": "17.7.0",
46
47
  "@jest/globals": "29.7.0",
48
+ "@types/byline": "4.2.34",
47
49
  "@typescript-eslint/eslint-plugin": "6.7.3",
48
50
  "@typescript-eslint/parser": "6.7.3",
49
51
  "jest": "29.7.0",
@@ -10,7 +10,7 @@ import { modelToGroupVersionKind } from "../kinds";
10
10
  import { GenericClass } from "../types";
11
11
  import { Filters, K8sInit, Paths, WatchAction } from "./types";
12
12
  import { k8sExec } from "./utils";
13
- import { ExecWatch } from "./watch";
13
+ import { ExecWatch, WatchCfg } from "./watch";
14
14
 
15
15
  /**
16
16
  * Kubernetes fluent API inspired by Kubectl. Pass in a model, then call filters and actions on it.
@@ -119,8 +119,8 @@ export function K8s<T extends GenericClass, K extends KubernetesObject = Instanc
119
119
  return k8sExec<T, K>(model, filters, "PATCH", payload);
120
120
  }
121
121
 
122
- async function Watch(callback: WatchAction<T>): Promise<AbortController> {
123
- return ExecWatch(model, filters, callback);
122
+ async function Watch(callback: WatchAction<T>, watchCfg?: WatchCfg) {
123
+ return ExecWatch(model, filters, callback, watchCfg);
124
124
  }
125
125
 
126
126
  return { InNamespace, Apply, Create, Patch, ...withFilters };
@@ -6,6 +6,7 @@ import { Operation } from "fast-json-patch";
6
6
  import type { PartialDeep } from "type-fest";
7
7
 
8
8
  import { GenericClass, GroupVersionKind } from "../types";
9
+ import { WatchCfg, WatchController } from "./watch";
9
10
 
10
11
  /**
11
12
  * The Phase matched when using the K8s Watch API.
@@ -51,7 +52,10 @@ export type K8sFilteredActions<K extends KubernetesObject> = {
51
52
  * @param callback
52
53
  * @returns
53
54
  */
54
- Watch: (callback: (payload: K, phase: WatchPhase) => void) => Promise<AbortController>;
55
+ Watch: (
56
+ callback: (payload: K, phase: WatchPhase) => void,
57
+ watchCfg?: WatchCfg,
58
+ ) => Promise<WatchController>;
55
59
  };
56
60
 
57
61
  export type K8sUnfilteredActions<K extends KubernetesObject> = {
@@ -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/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"] };