kubernetes-fluent-client 1.10.0 → 2.0.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/index.ts"],"names":[],"mappings":"AAGA,OAAO,SAAS,CAAC;AAGjB,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;AAGhE,OAAO,EAAE,WAAW,EAAE,MAAM,SAAS,CAAC;AAEtC,cAAc,SAAS,CAAC;AAGxB,OAAO,KAAK,MAAM,MAAM,6CAA6C,CAAC;AAEtE,OAAO,EAAE,OAAO,EAAE,cAAc,EAAE,MAAM,WAAW,CAAC"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAGA,OAAO,SAAS,CAAC;AAGjB,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,QAAQ,EAAE,UAAU,EAAE,MAAM,gBAAgB,CAAC;AAGtD,OAAO,EAAE,GAAG,EAAE,MAAM,UAAU,CAAC;AAG/B,OAAO,EAAE,YAAY,EAAE,uBAAuB,EAAE,MAAM,SAAS,CAAC;AAGhE,OAAO,EAAE,WAAW,EAAE,MAAM,SAAS,CAAC;AAEtC,cAAc,SAAS,CAAC;AAGxB,OAAO,KAAK,MAAM,MAAM,6CAA6C,CAAC;AAEtE,OAAO,EAAE,OAAO,EAAE,cAAc,EAAE,MAAM,WAAW,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.waitForCluster = exports.fromEnv = exports.models = exports.GenericKind = exports.modelToGroupVersionKind = exports.RegisterKind = exports.K8s = exports.fetchStatus = exports.fetch = exports.kind = void 0;
31
+ exports.waitForCluster = exports.fromEnv = exports.models = exports.GenericKind = exports.modelToGroupVersionKind = exports.RegisterKind = exports.K8s = exports.WatchEvent = exports.fetchStatus = exports.fetch = exports.kind = void 0;
32
32
  require("./patch");
33
33
  // Export kinds as a single object
34
34
  const kind = __importStar(require("./upstream"));
@@ -39,6 +39,9 @@ Object.defineProperty(exports, "fetch", { enumerable: true, get: function () { r
39
39
  // Export the HTTP status codes
40
40
  var http_status_codes_1 = require("http-status-codes");
41
41
  Object.defineProperty(exports, "fetchStatus", { enumerable: true, get: function () { return http_status_codes_1.StatusCodes; } });
42
+ // Export the Watch Config and Event types
43
+ var watch_1 = require("./fluent/watch");
44
+ Object.defineProperty(exports, "WatchEvent", { enumerable: true, get: function () { return watch_1.WatchEvent; } });
42
45
  // Export the fluent API entrypoint
43
46
  var fluent_1 = require("./fluent");
44
47
  Object.defineProperty(exports, "K8s", { enumerable: true, get: function () { return fluent_1.K8s; } });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "kubernetes-fluent-client",
3
- "version": "1.10.0",
3
+ "version": "2.0.0",
4
4
  "description": "A @kubernetes/client-node fluent API wrapper that leverages K8s Server Side Apply",
5
5
  "bin": "./dist/cli.js",
6
6
  "main": "dist/index.js",
@@ -35,13 +35,13 @@
35
35
  },
36
36
  "homepage": "https://github.com/defenseunicorns/kubernetes-fluent-client#readme",
37
37
  "dependencies": {
38
- "@kubernetes/client-node": "1.0.0-rc3",
38
+ "@kubernetes/client-node": "1.0.0-rc4",
39
39
  "byline": "5.0.0",
40
40
  "fast-json-patch": "3.1.1",
41
41
  "http-status-codes": "2.3.0",
42
42
  "node-fetch": "2.7.0",
43
43
  "quicktype-core": "23.0.80",
44
- "type-fest": "4.8.3",
44
+ "type-fest": "4.9.0",
45
45
  "yargs": "17.7.2"
46
46
  },
47
47
  "devDependencies": {
@@ -12,7 +12,7 @@ import { GenericClass } from "../types";
12
12
  import { ApplyCfg } from "./apply";
13
13
  import { Filters, K8sInit, Paths, WatchAction } from "./types";
14
14
  import { k8sCfg, k8sExec } from "./utils";
15
- import { ExecWatch, WatchCfg } from "./watch";
15
+ import { WatchCfg, Watcher } from "./watch";
16
16
 
17
17
  /**
18
18
  * Kubernetes fluent API inspired by Kubectl. Pass in a model, then call filters and actions on it.
@@ -24,7 +24,7 @@ import { ExecWatch, WatchCfg } from "./watch";
24
24
  export function K8s<T extends GenericClass, K extends KubernetesObject = InstanceType<T>>(
25
25
  model: T,
26
26
  filters: Filters = {},
27
- ): K8sInit<K> {
27
+ ): K8sInit<T, K> {
28
28
  const withFilters = { WithField, WithLabel, Get, Delete, Watch };
29
29
  const matchedKind = filters.kindOverride || modelToGroupVersionKind(model.name);
30
30
 
@@ -166,8 +166,8 @@ export function K8s<T extends GenericClass, K extends KubernetesObject = Instanc
166
166
  * @inheritdoc
167
167
  * @see {@link K8sInit.Watch}
168
168
  */
169
- async function Watch(callback: WatchAction<T>, watchCfg?: WatchCfg) {
170
- return ExecWatch(model, filters, callback, watchCfg);
169
+ function Watch(callback: WatchAction<T>, watchCfg?: WatchCfg) {
170
+ return new Watcher(model, filters, callback, watchCfg);
171
171
  }
172
172
 
173
173
  /**
@@ -6,8 +6,8 @@ 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";
10
9
  import { ApplyCfg } from "./apply";
10
+ import { WatchCfg, Watcher } from "./watch";
11
11
 
12
12
  /**
13
13
  * The Phase matched when using the K8s Watch API.
@@ -16,6 +16,8 @@ export enum WatchPhase {
16
16
  Added = "ADDED",
17
17
  Modified = "MODIFIED",
18
18
  Deleted = "DELETED",
19
+ Bookmark = "BOOKMARK",
20
+ Error = "ERROR",
19
21
  }
20
22
 
21
23
  export type FetchMethods = "GET" | "APPLY" | "POST" | "PUT" | "DELETE" | "PATCH" | "WATCH";
@@ -41,7 +43,7 @@ export type GetFunction<K extends KubernetesObject> = {
41
43
  (name: string): Promise<K>;
42
44
  };
43
45
 
44
- export type K8sFilteredActions<K extends KubernetesObject> = {
46
+ export type K8sFilteredActions<T extends GenericClass, K extends KubernetesObject> = {
45
47
  /**
46
48
  * Get the resource or resources matching the filters.
47
49
  * If no filters are specified, all resources will be returned.
@@ -63,10 +65,7 @@ export type K8sFilteredActions<K extends KubernetesObject> = {
63
65
  * @param watchCfg - (optional) watch configuration
64
66
  * @returns a watch controller
65
67
  */
66
- Watch: (
67
- callback: (payload: K, phase: WatchPhase) => void,
68
- watchCfg?: WatchCfg,
69
- ) => Promise<WatchController>;
68
+ Watch: (callback: WatchAction<T>, watchCfg?: WatchCfg) => Watcher<T>;
70
69
  };
71
70
 
72
71
  export type K8sUnfilteredActions<K extends KubernetesObject> = {
@@ -117,7 +116,10 @@ export type K8sUnfilteredActions<K extends KubernetesObject> = {
117
116
  Raw: (url: string) => Promise<K>;
118
117
  };
119
118
 
120
- export type K8sWithFilters<K extends KubernetesObject> = K8sFilteredActions<K> & {
119
+ export type K8sWithFilters<T extends GenericClass, K extends KubernetesObject> = K8sFilteredActions<
120
+ T,
121
+ K
122
+ > & {
121
123
  /**
122
124
  * Filter the query by the given field.
123
125
  * Note multiple calls to this method will result in an AND condition. e.g.
@@ -137,7 +139,7 @@ export type K8sWithFilters<K extends KubernetesObject> = K8sFilteredActions<K> &
137
139
  * @param value - the field value
138
140
  * @returns the fluent API
139
141
  */
140
- WithField: <P extends Paths<K>>(key: P, value: string) => K8sWithFilters<K>;
142
+ WithField: <P extends Paths<K>>(key: P, value: string) => K8sWithFilters<T, K>;
141
143
 
142
144
  /**
143
145
  * Filter the query by the given label. If no value is specified, the label simply must exist.
@@ -157,10 +159,10 @@ export type K8sWithFilters<K extends KubernetesObject> = K8sFilteredActions<K> &
157
159
  * @param value - the label value
158
160
  * @returns the fluent API
159
161
  */
160
- WithLabel: (key: string, value?: string) => K8sWithFilters<K>;
162
+ WithLabel: (key: string, value?: string) => K8sWithFilters<T, K>;
161
163
  };
162
164
 
163
- export type K8sInit<K extends KubernetesObject> = K8sWithFilters<K> &
165
+ export type K8sInit<T extends GenericClass, K extends KubernetesObject> = K8sWithFilters<T, K> &
164
166
  K8sUnfilteredActions<K> & {
165
167
  /**
166
168
  * Set the namespace filter.
@@ -168,7 +170,7 @@ export type K8sInit<K extends KubernetesObject> = K8sWithFilters<K> &
168
170
  * @param namespace - the namespace to filter on
169
171
  * @returns the fluent API
170
172
  */
171
- InNamespace: (namespace: string) => K8sWithFilters<K>;
173
+ InNamespace: (namespace: string) => K8sWithFilters<T, K>;
172
174
  };
173
175
 
174
176
  export type WatchAction<T extends GenericClass, K extends KubernetesObject = InstanceType<T>> = (
@@ -0,0 +1,264 @@
1
+ /* eslint-disable @typescript-eslint/no-explicit-any */
2
+
3
+ import { afterEach, beforeEach, describe, expect, it, jest } from "@jest/globals";
4
+ import nock from "nock";
5
+ import { PassThrough } from "readable-stream";
6
+
7
+ import { K8s } from ".";
8
+ import { WatchEvent, kind } from "..";
9
+ import { WatchPhase } from "./types";
10
+ import { Watcher } from "./watch";
11
+
12
+ describe("Watcher", () => {
13
+ const evtMock = jest.fn<(update: kind.Pod, phase: WatchPhase) => void>();
14
+ const errMock = jest.fn<(err: Error) => void>();
15
+
16
+ const setupAndStartWatcher = (eventType: WatchEvent, handler: (...args: any[]) => void) => {
17
+ watcher.events.on(eventType, handler);
18
+ watcher.start().catch(errMock);
19
+ };
20
+
21
+ let watcher: Watcher<typeof kind.Pod>;
22
+
23
+ beforeEach(() => {
24
+ jest.resetAllMocks();
25
+ watcher = K8s(kind.Pod).Watch(evtMock, {
26
+ retryDelaySec: 1,
27
+ });
28
+
29
+ nock("http://jest-test:8080")
30
+ .get("/api/v1/pods")
31
+ .query({ watch: "true", allowWatchBookmarks: "true" })
32
+ .reply(200, () => {
33
+ const stream = new PassThrough();
34
+
35
+ const resources = [
36
+ { type: "ADDED", object: createMockPod(`pod-0`, `1`) },
37
+ { type: "BOOKMARK", object: { metadata: { resourceVersion: "1" } } },
38
+ { type: "MODIFIED", object: createMockPod(`pod-0`, `2`) },
39
+ ];
40
+
41
+ resources.forEach(resource => {
42
+ stream.write(JSON.stringify(resource) + "\n");
43
+ });
44
+
45
+ stream.end();
46
+
47
+ return stream;
48
+ });
49
+ });
50
+
51
+ afterEach(() => {
52
+ watcher.close();
53
+ });
54
+
55
+ it("should watch named resources", done => {
56
+ nock.cleanAll();
57
+ nock("http://jest-test:8080")
58
+ .get("/api/v1/namespaces/tester/pods")
59
+ .query({ watch: "true", allowWatchBookmarks: "true", fieldSelector: "metadata.name=demo" })
60
+ .reply(200);
61
+
62
+ watcher = K8s(kind.Pod, { name: "demo" }).InNamespace("tester").Watch(evtMock);
63
+
64
+ setupAndStartWatcher(WatchEvent.CONNECT, () => {
65
+ done();
66
+ });
67
+ });
68
+
69
+ it("should start the watch at the specified resource version", done => {
70
+ nock.cleanAll();
71
+ nock("http://jest-test:8080")
72
+ .get("/api/v1/pods")
73
+ .query({
74
+ watch: "true",
75
+ allowWatchBookmarks: "true",
76
+ resourceVersion: "25",
77
+ })
78
+ .reply(200);
79
+
80
+ watcher = K8s(kind.Pod).Watch(evtMock, {
81
+ resourceVersion: "25",
82
+ });
83
+
84
+ setupAndStartWatcher(WatchEvent.CONNECT, () => {
85
+ done();
86
+ });
87
+ });
88
+
89
+ it("should handle resource version is too old", done => {
90
+ nock.cleanAll();
91
+ nock("http://jest-test:8080")
92
+ .get("/api/v1/pods")
93
+ .query({ watch: "true", allowWatchBookmarks: "true", resourceVersion: "45" })
94
+ .reply(200, () => {
95
+ const stream = new PassThrough();
96
+ stream.write(
97
+ JSON.stringify({
98
+ type: "ERROR",
99
+ object: {
100
+ kind: "Status",
101
+ apiVersion: "v1",
102
+ metadata: {},
103
+ status: "Failure",
104
+ message: "too old resource version: 123 (391079)",
105
+ reason: "Gone",
106
+ code: 410,
107
+ },
108
+ }) + "\n",
109
+ );
110
+
111
+ stream.end();
112
+ return stream;
113
+ });
114
+
115
+ watcher = K8s(kind.Pod).Watch(evtMock, {
116
+ resourceVersion: "45",
117
+ });
118
+
119
+ setupAndStartWatcher(WatchEvent.OLD_RESOURCE_VERSION, res => {
120
+ expect(res).toEqual("45");
121
+ done();
122
+ });
123
+ });
124
+
125
+ it("should call the event handler for each event", done => {
126
+ watcher = K8s(kind.Pod).Watch((evt, phase) => {
127
+ expect(evt.metadata?.name).toEqual(`pod-0`);
128
+ expect(phase).toEqual(WatchPhase.Added);
129
+ done();
130
+ });
131
+
132
+ watcher.start().catch(errMock);
133
+ });
134
+
135
+ it("should return the cache id", done => {
136
+ watcher
137
+ .start()
138
+ .then(() => {
139
+ expect(watcher.id).toEqual("d69b75a611");
140
+ done();
141
+ })
142
+ .catch(errMock);
143
+ });
144
+
145
+ it("should handle calling .id() before .start()", () => {
146
+ expect(() => watcher.id).toThrowError("watch not started");
147
+ });
148
+
149
+ it("should handle the CONNECT event", done => {
150
+ setupAndStartWatcher(WatchEvent.CONNECT, () => {
151
+ done();
152
+ });
153
+ });
154
+
155
+ it("should handle the DATA event", done => {
156
+ setupAndStartWatcher(WatchEvent.DATA, (pod, phase) => {
157
+ expect(pod.metadata?.name).toEqual(`pod-0`);
158
+ expect(phase).toEqual(WatchPhase.Added);
159
+ done();
160
+ });
161
+ });
162
+
163
+ it("should handle the BOOKMARK event", done => {
164
+ setupAndStartWatcher(WatchEvent.BOOKMARK, bookmark => {
165
+ expect(bookmark.metadata?.resourceVersion).toEqual("1");
166
+ done();
167
+ });
168
+ });
169
+
170
+ it("should handle the NETWORK_ERROR event", done => {
171
+ nock.cleanAll();
172
+ nock("http://jest-test:8080")
173
+ .get("/api/v1/pods")
174
+ .query({ watch: "true", allowWatchBookmarks: "true" })
175
+ .replyWithError("Something bad happened");
176
+
177
+ setupAndStartWatcher(WatchEvent.NETWORK_ERROR, error => {
178
+ expect(error.message).toEqual(
179
+ "request to http://jest-test:8080/api/v1/pods?watch=true&allowWatchBookmarks=true failed, reason: Something bad happened",
180
+ );
181
+ done();
182
+ });
183
+ });
184
+
185
+ it("should handle the RECONNECT event", done => {
186
+ nock.cleanAll();
187
+ nock("http://jest-test:8080")
188
+ .get("/api/v1/pods")
189
+ .query({ watch: "true", allowWatchBookmarks: "true" })
190
+ .replyWithError("Something bad happened");
191
+
192
+ setupAndStartWatcher(WatchEvent.RECONNECT, error => {
193
+ expect(error.message).toEqual(
194
+ "request to http://jest-test:8080/api/v1/pods?watch=true&allowWatchBookmarks=true failed, reason: Something bad happened",
195
+ );
196
+ done();
197
+ });
198
+ });
199
+
200
+ it("should perform a resync after the resync interval", done => {
201
+ watcher = K8s(kind.Pod).Watch(evtMock, {
202
+ resyncIntervalSec: 1,
203
+ });
204
+
205
+ setupAndStartWatcher(WatchEvent.RESYNC, err => {
206
+ expect(err.name).toEqual("Resync");
207
+ expect(err.message).toEqual("Resync triggered by resyncIntervalSec");
208
+ done();
209
+ });
210
+ });
211
+
212
+ it("should handle the GIVE_UP event", done => {
213
+ nock.cleanAll();
214
+ nock("http://jest-test:8080");
215
+
216
+ watcher = K8s(kind.Pod).Watch(evtMock, {
217
+ retryMax: 1,
218
+ retryDelaySec: 1,
219
+ });
220
+
221
+ setupAndStartWatcher(WatchEvent.GIVE_UP, error => {
222
+ expect(error.message).toContain(
223
+ "request to http://jest-test:8080/api/v1/pods?watch=true&allowWatchBookmarks=true failed",
224
+ );
225
+ done();
226
+ });
227
+ });
228
+ });
229
+
230
+ /**
231
+ * Creates a mock pod object
232
+ *
233
+ * @param name The name of the pod
234
+ * @param resourceVersion The resource version of the pod
235
+ * @returns A mock pod object
236
+ */
237
+ function createMockPod(name: string, resourceVersion: string): kind.Pod {
238
+ return {
239
+ kind: "Pod",
240
+ apiVersion: "v1",
241
+ metadata: {
242
+ name: name,
243
+ resourceVersion: resourceVersion,
244
+ // ... other metadata fields
245
+ },
246
+ spec: {
247
+ containers: [
248
+ {
249
+ name: "nginx",
250
+ image: "nginx:1.14.2",
251
+ ports: [
252
+ {
253
+ containerPort: 80,
254
+ protocol: "TCP",
255
+ },
256
+ ],
257
+ },
258
+ ],
259
+ },
260
+ status: {
261
+ // ... pod status
262
+ },
263
+ };
264
+ }