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 +81 -2
- package/dist/fluent/index.js +2 -2
- package/dist/fluent/types.d.ts +2 -2
- 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/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 +4 -2
- package/src/fluent/index.ts +3 -3
- package/src/fluent/types.ts +5 -1
- package/src/fluent/utils.ts +3 -0
- package/src/fluent/watch.ts +172 -57
- 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
package/README.md
CHANGED
|
@@ -5,11 +5,90 @@
|
|
|
5
5
|
[](https://npmjs.com/package/kubernetes-fluent-client)
|
|
6
6
|
[](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
|
-
|
|
12
|
-
|
|
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
|
```
|
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
|
-
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
|
}
|
package/dist/fluent/types.d.ts
CHANGED
|
@@ -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<
|
|
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":"
|
|
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;AAC5C,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,WAAW,CAAC;AAE7C,OAAO,EAAE,YAAY,EAAE,gBAAgB,EAAE,MAAM,UAAU,CAAC;AAC1D,OAAO,EAAE,QAAQ,EAAE,eAAe,EAAE,MAAM,SAAS,CAAC;AAEpD;;GAEG;AACH,oBAAY,UAAU;IACpB,KAAK,UAAU;IACf,QAAQ,aAAa;IACrB,OAAO,YAAY;CACpB;AAED,MAAM,MAAM,YAAY,GAAG,KAAK,GAAG,OAAO,GAAG,MAAM,GAAG,KAAK,GAAG,QAAQ,GAAG,OAAO,GAAG,OAAO,CAAC;AAE3F,MAAM,WAAW,OAAO;IACtB,YAAY,CAAC,EAAE,gBAAgB,CAAC;IAChC,MAAM,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IAChC,MAAM,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IAChC,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,SAAS,CAAC,EAAE,MAAM,CAAC;CACpB;AAED,MAAM,MAAM,WAAW,CAAC,CAAC,SAAS,gBAAgB,IAAI;IACpD,IAAI,OAAO,CAAC,oBAAoB,CAAC,CAAC,CAAC,CAAC,CAAC;IACrC,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,CAAC,CAAC,CAAC;CAC5B,CAAC;AAEF,MAAM,MAAM,kBAAkB,CAAC,CAAC,SAAS,gBAAgB,IAAI;IAC3D;;;;OAIG;IACH,GAAG,EAAE,WAAW,CAAC,CAAC,CAAC,CAAC;IAEpB;;;;OAIG;IACH,MAAM,EAAE,CAAC,MAAM,CAAC,EAAE,CAAC,GAAG,MAAM,KAAK,OAAO,CAAC,IAAI,CAAC,CAAC;IAE/C;;;;OAIG;IACH,KAAK,EAAE,CACL,QAAQ,EAAE,CAAC,OAAO,EAAE,CAAC,EAAE,KAAK,EAAE,UAAU,KAAK,IAAI,EACjD,QAAQ,CAAC,EAAE,QAAQ,KAChB,OAAO,CAAC,eAAe,CAAC,CAAC;CAC/B,CAAC;AAEF,MAAM,MAAM,oBAAoB,CAAC,CAAC,SAAS,gBAAgB,IAAI;IAC7D;;;;;OAKG;IACH,KAAK,EAAE,CAAC,QAAQ,EAAE,WAAW,CAAC,CAAC,CAAC,KAAK,OAAO,CAAC,CAAC,CAAC,CAAC;IAEhD;;;;;OAKG;IACH,MAAM,EAAE,CAAC,QAAQ,EAAE,CAAC,KAAK,OAAO,CAAC,CAAC,CAAC,CAAC;IAEpC;;;;;;;OAOG;IACH,KAAK,EAAE,CAAC,OAAO,EAAE,SAAS,EAAE,KAAK,OAAO,CAAC,CAAC,CAAC,CAAC;CAC7C,CAAC;AAEF,MAAM,MAAM,cAAc,CAAC,CAAC,SAAS,gBAAgB,IAAI,kBAAkB,CAAC,CAAC,CAAC,GAAG;IAC/E;;;;;;;;;;;;;;;;OAgBG;IACH,SAAS,EAAE,CAAC,CAAC,SAAS,KAAK,CAAC,CAAC,CAAC,EAAE,GAAG,EAAE,CAAC,EAAE,KAAK,CAAC,EAAE,MAAM,KAAK,cAAc,CAAC,CAAC,CAAC,CAAC;IAE7E;;;;;;;;;;;;;;;OAeG;IACH,SAAS,EAAE,CAAC,GAAG,EAAE,MAAM,EAAE,KAAK,CAAC,EAAE,MAAM,KAAK,cAAc,CAAC,CAAC,CAAC,CAAC;CAC/D,CAAC;AAEF,MAAM,MAAM,OAAO,CAAC,CAAC,SAAS,gBAAgB,IAAI,cAAc,CAAC,CAAC,CAAC,GACjE,oBAAoB,CAAC,CAAC,CAAC,GAAG;IACxB;;;;;OAKG;IACH,WAAW,EAAE,CAAC,SAAS,EAAE,MAAM,KAAK,cAAc,CAAC,CAAC,CAAC,CAAC;CACvD,CAAC;AAEJ,MAAM,MAAM,WAAW,CAAC,CAAC,SAAS,YAAY,EAAE,CAAC,SAAS,gBAAgB,GAAG,YAAY,CAAC,CAAC,CAAC,IAAI,CAC9F,MAAM,EAAE,CAAC,EACT,KAAK,EAAE,UAAU,KACd,OAAO,CAAC,IAAI,CAAC,GAAG,IAAI,CAAC;AAG1B,KAAK,IAAI,CAAC,CAAC,EAAE,CAAC,IAAI,CAAC,SAAS,MAAM,GAAG,MAAM,GACvC,CAAC,SAAS,MAAM,GAAG,MAAM,GACvB,GAAG,CAAC,GAAG,EAAE,SAAS,CAAC,GAAG,EAAE,GAAG,GAAG,GAAG,CAAC,EAAE,GACpC,KAAK,GACP,KAAK,CAAC;AAEV,MAAM,MAAM,KAAK,CAAC,CAAC,EAAE,CAAC,SAAS,MAAM,GAAG,EAAE,IAAI,CAAC,CAAC,CAAC,SAAS,CAAC,KAAK,CAAC,GAC7D,KAAK,GACL,CAAC,SAAS,MAAM,GAChB;KAAG,CAAC,IAAI,MAAM,CAAC,CAAC,CAAC,GAAG,CAAC,SAAS,MAAM,GAAG,MAAM,GAAG,GAAG,CAAC,EAAE,GAAG,IAAI,CAAC,CAAC,EAAE,KAAK,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,GAAG,KAAK;CAAE,CAAC,MAAM,CAAC,CAAC,GAChG,EAAE,CAAC"}
|
|
@@ -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/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.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.
|
|
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",
|
package/src/fluent/index.ts
CHANGED
|
@@ -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
|
|
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 };
|
package/src/fluent/types.ts
CHANGED
|
@@ -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: (
|
|
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> = {
|
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/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"] };
|