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.
@@ -5,147 +5,322 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
5
5
  return (mod && mod.__esModule) ? mod : { "default": mod };
6
6
  };
7
7
  Object.defineProperty(exports, "__esModule", { value: true });
8
- exports.ExecWatch = void 0;
8
+ exports.Watcher = exports.WatchEvent = void 0;
9
9
  const byline_1 = __importDefault(require("byline"));
10
+ const crypto_1 = require("crypto");
11
+ const events_1 = require("events");
10
12
  const node_fetch_1 = __importDefault(require("node-fetch"));
13
+ const types_1 = require("./types");
11
14
  const utils_1 = require("./utils");
12
- /**
13
- * Execute a watch on the specified resource.
14
- *
15
- * @param model - the model to use for the API
16
- * @param filters - (optional) filter overrides, can also be chained
17
- * @param callback - the callback function to call when an event is received
18
- * @param watchCfg - (optional) watch configuration
19
- * @returns a WatchController to allow the watch to be aborted externally
20
- */
21
- async function ExecWatch(model, filters, callback, watchCfg = {}) {
22
- watchCfg.logFn?.({ model, filters, watchCfg }, "ExecWatch");
23
- // Build the path and query params for the resource, excluding the name
24
- const { opts, serverUrl } = await (0, utils_1.k8sCfg)("GET");
25
- const url = (0, utils_1.pathBuilder)(serverUrl, model, filters, true);
26
- // Enable the watch query param
27
- url.searchParams.set("watch", "true");
28
- // Allow bookmarks to be used for the watch
29
- url.searchParams.set("allowWatchBookmarks", "true");
30
- // If a name is specified, add it to the query params
31
- if (filters.name) {
32
- url.searchParams.set("fieldSelector", `metadata.name=${filters.name}`);
33
- }
34
- // Set the initial timeout to 15 seconds
35
- opts.timeout = 15 * 1000;
36
- // Enable keep alive
37
- opts.agent.keepAlive = true;
38
- // Track the number of retries
39
- let retryCount = 0;
40
- // Set the maximum number of retries to 5 if not specified
41
- watchCfg.retryMax ??= 5;
42
- // Set the retry delay to 5 seconds if not specified
43
- watchCfg.retryDelaySec ??= 5;
44
- // Create a throwaway AbortController to setup the wrapped AbortController
45
- let abortController;
15
+ var WatchEvent;
16
+ (function (WatchEvent) {
17
+ /** Watch is connected successfully */
18
+ WatchEvent["CONNECT"] = "connect";
19
+ /** Network error occurs */
20
+ WatchEvent["NETWORK_ERROR"] = "network_error";
21
+ /** Error decoding data or running the callback */
22
+ WatchEvent["DATA_ERROR"] = "data_error";
23
+ /** Reconnect is called */
24
+ WatchEvent["RECONNECT"] = "reconnect";
25
+ /** Retry limit is exceeded */
26
+ WatchEvent["GIVE_UP"] = "give_up";
27
+ /** Abort is called */
28
+ WatchEvent["ABORT"] = "abort";
29
+ /** Resync is called */
30
+ WatchEvent["RESYNC"] = "resync";
31
+ /** Data is received and decoded */
32
+ WatchEvent["DATA"] = "data";
33
+ /** Bookmark is received */
34
+ WatchEvent["BOOKMARK"] = "bookmark";
35
+ /** ResourceVersion is updated */
36
+ WatchEvent["RESOURCE_VERSION"] = "resource_version";
37
+ /** 410 (old resource version) occurs */
38
+ WatchEvent["OLD_RESOURCE_VERSION"] = "old_resource_version";
39
+ /** A reconnect is already pending */
40
+ WatchEvent["RECONNECT_PENDING"] = "reconnect_pending";
41
+ })(WatchEvent || (exports.WatchEvent = WatchEvent = {}));
42
+ /** A wrapper around the Kubernetes watch API. */
43
+ class Watcher {
44
+ // User-provided properties
45
+ #model;
46
+ #filters;
47
+ #callback;
48
+ #watchCfg;
46
49
  // Create a wrapped AbortController to allow the watch to be aborted externally
47
- const abortWrapper = {};
50
+ #abortController;
51
+ // Track the number of retries
52
+ #retryCount = 0;
53
+ // Create a stream to read the response body
54
+ #stream;
55
+ // Create an EventEmitter to emit events
56
+ #events = new events_1.EventEmitter();
57
+ // Create a timer to resync the watch
58
+ #resyncTimer;
59
+ // Unique ID for the watch
60
+ #id;
61
+ #hashedID;
62
+ // Track if a reconnect is pending
63
+ #pendingReconnect = false;
48
64
  /**
49
- * Bind the abort controller to the wrapper.
65
+ * Setup a Kubernetes watcher for the specified model and filters. The callback function will be called for each event received.
66
+ * The watch can be aborted by calling {@link Watcher.close} or by calling abort() on the AbortController returned by {@link Watcher.start}.
67
+ *
68
+ *
69
+ * Kubernetes API docs: {@link https://kubernetes.io/docs/reference/using-api/api-concepts/#efficient-detection-of-changes}
70
+ *
71
+ * @param model - the model to use for the API
72
+ * @param filters - (optional) filter overrides, can also be chained
73
+ * @param callback - the callback function to call when an event is received
74
+ * @param watchCfg - (optional) watch configuration
50
75
  */
51
- function bindAbortController() {
76
+ constructor(model, filters, callback, watchCfg = {}) {
77
+ // Set the retry delay to 10 seconds if not specified
78
+ watchCfg.retryDelaySec ??= 10;
79
+ // Set the resync interval to 5 minutes if not specified
80
+ watchCfg.resyncIntervalSec ??= 300;
81
+ // Bind class properties
82
+ this.#model = model;
83
+ this.#filters = filters;
84
+ this.#callback = callback;
85
+ this.#watchCfg = watchCfg;
52
86
  // Create a new AbortController
53
- abortController = new AbortController();
54
- // Update the abort wrapper
55
- abortWrapper.abort = reason => abortController.abort(reason);
56
- abortWrapper.signal = () => abortController.signal;
57
- // Add the abort signal to the request options
58
- opts.signal = abortController.signal;
87
+ this.#abortController = new AbortController();
59
88
  }
60
89
  /**
61
- * The main watch runner. This will run until the process is terminated or the watch is aborted.
90
+ * Start the watch.
91
+ *
92
+ * @returns The AbortController for the watch.
62
93
  */
63
- async function runner() {
64
- let doneCalled = false;
65
- bindAbortController();
66
- // Create a stream to read the response body
67
- const stream = byline_1.default.createStream();
68
- const onError = (err) => {
69
- stream.removeAllListeners();
70
- if (!doneCalled) {
71
- doneCalled = true;
72
- // If the error is not an AbortError, reload the watch
73
- if (err.name !== "AbortError") {
74
- watchCfg.logFn?.(err, "stream error");
75
- void reload(err);
76
- }
77
- else {
78
- watchCfg.logFn?.("watch aborted via WatchController.abort()");
79
- }
80
- }
81
- };
82
- // Cleanup the stream listeners
83
- const cleanup = () => {
84
- if (!doneCalled) {
85
- doneCalled = true;
86
- stream.removeAllListeners();
87
- }
88
- };
94
+ async start() {
95
+ await this.#runner();
96
+ return this.#abortController;
97
+ }
98
+ /** Close the watch. Also available on the AbortController returned by {@link Watcher.start}. */
99
+ close() {
100
+ clearTimeout(this.#resyncTimer);
101
+ this.#cleanup();
102
+ this.#abortController.abort();
103
+ }
104
+ /**
105
+ * Get a unique ID for the watch based on the model and filters.
106
+ * This is useful for caching the watch data or resource versions.
107
+ *
108
+ * @returns the watch ID
109
+ */
110
+ get id() {
111
+ // The ID must exist at this point
112
+ if (!this.#id) {
113
+ throw new Error("watch not started");
114
+ }
115
+ // Hash and truncate the ID to 10 characters, cache the result
116
+ if (!this.#hashedID) {
117
+ this.#hashedID = (0, crypto_1.createHash)("sha224").update(this.#id).digest("hex").substring(0, 10);
118
+ }
119
+ return this.#hashedID;
120
+ }
121
+ /**
122
+ * Subscribe to watch events. This is an EventEmitter that emits the following events:
123
+ *
124
+ * Use {@link WatchEvent} for the event names.
125
+ *
126
+ * @returns an EventEmitter
127
+ */
128
+ get events() {
129
+ return this.#events;
130
+ }
131
+ /**
132
+ * Build the URL and request options for the watch.
133
+ *
134
+ * @returns the URL and request options
135
+ */
136
+ #buildURL = async () => {
137
+ // Build the path and query params for the resource, excluding the name
138
+ const { opts, serverUrl } = await (0, utils_1.k8sCfg)("GET");
139
+ const url = (0, utils_1.pathBuilder)(serverUrl, this.#model, this.#filters, true);
140
+ // Set the watch ID if it does not exist (this does not change on reconnect)
141
+ if (!this.#id) {
142
+ this.#id = url.pathname + url.search;
143
+ }
144
+ // Enable the watch query param
145
+ url.searchParams.set("watch", "true");
146
+ // If a name is specified, add it to the query params
147
+ if (this.#filters.name) {
148
+ url.searchParams.set("fieldSelector", `metadata.name=${this.#filters.name}`);
149
+ }
150
+ // If a resource version is specified, add it to the query params
151
+ if (this.#watchCfg.resourceVersion) {
152
+ url.searchParams.set("resourceVersion", this.#watchCfg.resourceVersion);
153
+ }
154
+ // Enable watch bookmarks
155
+ url.searchParams.set("allowWatchBookmarks", "true");
156
+ // Add the abort signal to the request options
157
+ opts.signal = this.#abortController.signal;
158
+ return { opts, url };
159
+ };
160
+ /** Run the watch. */
161
+ #runner = async () => {
89
162
  try {
163
+ // Build the URL and request options
164
+ const { opts, url } = await this.#buildURL();
165
+ // Create a stream to read the response body
166
+ this.#stream = byline_1.default.createStream();
167
+ // Bind the stream events
168
+ this.#stream.on("error", this.#errHandler);
169
+ this.#stream.on("close", this.#cleanup);
170
+ this.#stream.on("finish", this.#cleanup);
90
171
  // Make the actual request
91
172
  const response = await (0, node_fetch_1.default)(url, { ...opts });
173
+ // Reset the pending reconnect flag
174
+ this.#pendingReconnect = false;
175
+ // Reset the resync timer
176
+ void this.#scheduleResync();
92
177
  // If the request is successful, start listening for events
93
178
  if (response.ok) {
179
+ this.#events.emit(WatchEvent.CONNECT);
94
180
  const { body } = response;
95
181
  // Reset the retry count
96
- retryCount = 0;
97
- stream.on("error", onError);
98
- stream.on("close", cleanup);
99
- stream.on("finish", cleanup);
182
+ this.#retryCount = 0;
100
183
  // Listen for events and call the callback function
101
- stream.on("data", line => {
184
+ this.#stream.on("data", async (line) => {
102
185
  try {
103
186
  // Parse the event payload
104
187
  const { object: payload, type: phase } = JSON.parse(line);
105
- // Call the callback function with the parsed payload
106
- void callback(payload, phase);
188
+ void this.#scheduleResync();
189
+ // If the watch is too old, remove the resourceVersion and reload the watch
190
+ if (phase === types_1.WatchPhase.Error && payload.code === 410) {
191
+ throw {
192
+ name: "TooOld",
193
+ message: this.#watchCfg.resourceVersion,
194
+ };
195
+ }
196
+ // If the event is a bookmark, emit the event and skip the callback
197
+ if (phase === types_1.WatchPhase.Bookmark) {
198
+ this.#events.emit(WatchEvent.BOOKMARK, payload);
199
+ }
200
+ else {
201
+ this.#events.emit(WatchEvent.DATA, payload, phase);
202
+ // Call the callback function with the parsed payload
203
+ await this.#callback(payload, phase);
204
+ }
205
+ // Update the resource version if the callback was successful
206
+ this.#setResourceVersion(payload.metadata.resourceVersion);
107
207
  }
108
208
  catch (err) {
109
- watchCfg.logFn?.(err, "watch callback error");
209
+ if (err.name === "TooOld") {
210
+ // Prevent any body events from firing
211
+ body.removeAllListeners();
212
+ // Reload the watch
213
+ void this.#errHandler(err);
214
+ return;
215
+ }
216
+ this.#events.emit(WatchEvent.DATA_ERROR, err);
110
217
  }
111
218
  });
112
- body.on("error", onError);
113
- body.on("close", cleanup);
114
- body.on("finish", cleanup);
219
+ // Bind the body events
220
+ body.on("error", this.#errHandler);
221
+ body.on("close", this.#cleanup);
222
+ body.on("finish", this.#cleanup);
115
223
  // Pipe the response body to the stream
116
- body.pipe(stream);
224
+ body.pipe(this.#stream);
117
225
  }
118
226
  else {
119
- throw new Error(`watch failed: ${response.status} ${response.statusText}`);
227
+ throw new Error(`watch connect failed: ${response.status} ${response.statusText}`);
120
228
  }
121
229
  }
122
230
  catch (e) {
123
- onError(e);
231
+ void this.#errHandler(e);
124
232
  }
125
- /**
126
- * Reload the watch.
127
- *
128
- * @param e - the error that caused the reload
129
- */
130
- async function reload(e) {
131
- // If there are more attempts, retry the watch
132
- if (watchCfg.retryMax > retryCount) {
133
- retryCount++;
134
- watchCfg.logFn?.(`retrying watch ${retryCount}/${watchCfg.retryMax}`);
135
- // Sleep for the specified delay or 5 seconds
136
- await new Promise(r => setTimeout(r, watchCfg.retryDelaySec * 1000));
137
- // Retry the watch after the delay
138
- await runner();
233
+ };
234
+ /**
235
+ * Resync the watch.
236
+ *
237
+ * @returns the error handler
238
+ */
239
+ #resync = () => this.#errHandler({
240
+ name: "Resync",
241
+ message: "Resync triggered by resyncIntervalSec",
242
+ });
243
+ /** Clear the resync timer and schedule a new one. */
244
+ #scheduleResync = async () => {
245
+ clearTimeout(this.#resyncTimer);
246
+ this.#resyncTimer = setTimeout(this.#resync, this.#watchCfg.resyncIntervalSec * 1000);
247
+ };
248
+ /**
249
+ * Update the resource version.
250
+ *
251
+ * @param resourceVersion - the new resource version
252
+ */
253
+ #setResourceVersion = (resourceVersion) => {
254
+ this.#watchCfg.resourceVersion = resourceVersion;
255
+ this.#events.emit(WatchEvent.RESOURCE_VERSION, resourceVersion);
256
+ };
257
+ /**
258
+ * Reload the watch after an error.
259
+ *
260
+ * @param err - the error that occurred
261
+ */
262
+ #reconnect = async (err) => {
263
+ // If there are more attempts, retry the watch (undefined is unlimited retries)
264
+ if (this.#watchCfg.retryMax === undefined || this.#watchCfg.retryMax > this.#retryCount) {
265
+ // Sleep for the specified delay, but check every 500ms if the watch has been aborted
266
+ let delay = this.#watchCfg.retryDelaySec * 1000;
267
+ while (delay > 0) {
268
+ if (this.#abortController.signal.aborted) {
269
+ return;
270
+ }
271
+ delay -= 500;
272
+ await new Promise(resolve => setTimeout(resolve, 500));
273
+ }
274
+ this.#retryCount++;
275
+ this.#events.emit(WatchEvent.RECONNECT, err, this.#retryCount);
276
+ if (this.#pendingReconnect) {
277
+ this.#events.emit(WatchEvent.RECONNECT_PENDING);
139
278
  }
140
279
  else {
141
- // Otherwise, call the finally function if it exists
142
- if (watchCfg.retryFail) {
143
- watchCfg.retryFail(e);
144
- }
280
+ this.#pendingReconnect = true;
281
+ this.#cleanup();
282
+ // Retry the watch after the delay
283
+ await this.#runner();
145
284
  }
146
285
  }
147
- }
148
- await runner();
149
- return abortWrapper;
286
+ else {
287
+ // Otherwise, call the finally function if it exists
288
+ this.#events.emit(WatchEvent.GIVE_UP, err);
289
+ this.close();
290
+ }
291
+ };
292
+ /**
293
+ * Handle errors from the stream.
294
+ *
295
+ * @param err - the error that occurred
296
+ */
297
+ #errHandler = async (err) => {
298
+ switch (err.name) {
299
+ case "AbortError":
300
+ clearTimeout(this.#resyncTimer);
301
+ this.#cleanup();
302
+ this.#events.emit(WatchEvent.ABORT, err);
303
+ return;
304
+ case "TooOld":
305
+ // Purge the resource version if it is too old
306
+ this.#setResourceVersion(undefined);
307
+ this.#events.emit(WatchEvent.OLD_RESOURCE_VERSION, err.message);
308
+ break;
309
+ case "Resync":
310
+ this.#events.emit(WatchEvent.RESYNC, err);
311
+ break;
312
+ default:
313
+ this.#events.emit(WatchEvent.NETWORK_ERROR, err);
314
+ break;
315
+ }
316
+ await this.#reconnect(err);
317
+ };
318
+ /** Cleanup the stream and listeners. */
319
+ #cleanup = () => {
320
+ if (this.#stream) {
321
+ this.#stream.removeAllListeners();
322
+ this.#stream.destroy();
323
+ }
324
+ };
150
325
  }
151
- exports.ExecWatch = ExecWatch;
326
+ exports.Watcher = Watcher;
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=watch.spec.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"watch.spec.d.ts","sourceRoot":"","sources":["../../src/fluent/watch.spec.ts"],"names":[],"mappings":""}
@@ -0,0 +1,221 @@
1
+ "use strict";
2
+ /* eslint-disable @typescript-eslint/no-explicit-any */
3
+ var __importDefault = (this && this.__importDefault) || function (mod) {
4
+ return (mod && mod.__esModule) ? mod : { "default": mod };
5
+ };
6
+ Object.defineProperty(exports, "__esModule", { value: true });
7
+ const globals_1 = require("@jest/globals");
8
+ const nock_1 = __importDefault(require("nock"));
9
+ const readable_stream_1 = require("readable-stream");
10
+ const _1 = require(".");
11
+ const __1 = require("..");
12
+ const types_1 = require("./types");
13
+ (0, globals_1.describe)("Watcher", () => {
14
+ const evtMock = globals_1.jest.fn();
15
+ const errMock = globals_1.jest.fn();
16
+ const setupAndStartWatcher = (eventType, handler) => {
17
+ watcher.events.on(eventType, handler);
18
+ watcher.start().catch(errMock);
19
+ };
20
+ let watcher;
21
+ (0, globals_1.beforeEach)(() => {
22
+ globals_1.jest.resetAllMocks();
23
+ watcher = (0, _1.K8s)(__1.kind.Pod).Watch(evtMock, {
24
+ retryDelaySec: 1,
25
+ });
26
+ (0, nock_1.default)("http://jest-test:8080")
27
+ .get("/api/v1/pods")
28
+ .query({ watch: "true", allowWatchBookmarks: "true" })
29
+ .reply(200, () => {
30
+ const stream = new readable_stream_1.PassThrough();
31
+ const resources = [
32
+ { type: "ADDED", object: createMockPod(`pod-0`, `1`) },
33
+ { type: "BOOKMARK", object: { metadata: { resourceVersion: "1" } } },
34
+ { type: "MODIFIED", object: createMockPod(`pod-0`, `2`) },
35
+ ];
36
+ resources.forEach(resource => {
37
+ stream.write(JSON.stringify(resource) + "\n");
38
+ });
39
+ stream.end();
40
+ return stream;
41
+ });
42
+ });
43
+ (0, globals_1.afterEach)(() => {
44
+ watcher.close();
45
+ });
46
+ (0, globals_1.it)("should watch named resources", done => {
47
+ nock_1.default.cleanAll();
48
+ (0, nock_1.default)("http://jest-test:8080")
49
+ .get("/api/v1/namespaces/tester/pods")
50
+ .query({ watch: "true", allowWatchBookmarks: "true", fieldSelector: "metadata.name=demo" })
51
+ .reply(200);
52
+ watcher = (0, _1.K8s)(__1.kind.Pod, { name: "demo" }).InNamespace("tester").Watch(evtMock);
53
+ setupAndStartWatcher(__1.WatchEvent.CONNECT, () => {
54
+ done();
55
+ });
56
+ });
57
+ (0, globals_1.it)("should start the watch at the specified resource version", done => {
58
+ nock_1.default.cleanAll();
59
+ (0, nock_1.default)("http://jest-test:8080")
60
+ .get("/api/v1/pods")
61
+ .query({
62
+ watch: "true",
63
+ allowWatchBookmarks: "true",
64
+ resourceVersion: "25",
65
+ })
66
+ .reply(200);
67
+ watcher = (0, _1.K8s)(__1.kind.Pod).Watch(evtMock, {
68
+ resourceVersion: "25",
69
+ });
70
+ setupAndStartWatcher(__1.WatchEvent.CONNECT, () => {
71
+ done();
72
+ });
73
+ });
74
+ (0, globals_1.it)("should handle resource version is too old", done => {
75
+ nock_1.default.cleanAll();
76
+ (0, nock_1.default)("http://jest-test:8080")
77
+ .get("/api/v1/pods")
78
+ .query({ watch: "true", allowWatchBookmarks: "true", resourceVersion: "45" })
79
+ .reply(200, () => {
80
+ const stream = new readable_stream_1.PassThrough();
81
+ stream.write(JSON.stringify({
82
+ type: "ERROR",
83
+ object: {
84
+ kind: "Status",
85
+ apiVersion: "v1",
86
+ metadata: {},
87
+ status: "Failure",
88
+ message: "too old resource version: 123 (391079)",
89
+ reason: "Gone",
90
+ code: 410,
91
+ },
92
+ }) + "\n");
93
+ stream.end();
94
+ return stream;
95
+ });
96
+ watcher = (0, _1.K8s)(__1.kind.Pod).Watch(evtMock, {
97
+ resourceVersion: "45",
98
+ });
99
+ setupAndStartWatcher(__1.WatchEvent.OLD_RESOURCE_VERSION, res => {
100
+ (0, globals_1.expect)(res).toEqual("45");
101
+ done();
102
+ });
103
+ });
104
+ (0, globals_1.it)("should call the event handler for each event", done => {
105
+ watcher = (0, _1.K8s)(__1.kind.Pod).Watch((evt, phase) => {
106
+ (0, globals_1.expect)(evt.metadata?.name).toEqual(`pod-0`);
107
+ (0, globals_1.expect)(phase).toEqual(types_1.WatchPhase.Added);
108
+ done();
109
+ });
110
+ watcher.start().catch(errMock);
111
+ });
112
+ (0, globals_1.it)("should return the cache id", done => {
113
+ watcher
114
+ .start()
115
+ .then(() => {
116
+ (0, globals_1.expect)(watcher.id).toEqual("d69b75a611");
117
+ done();
118
+ })
119
+ .catch(errMock);
120
+ });
121
+ (0, globals_1.it)("should handle calling .id() before .start()", () => {
122
+ (0, globals_1.expect)(() => watcher.id).toThrowError("watch not started");
123
+ });
124
+ (0, globals_1.it)("should handle the CONNECT event", done => {
125
+ setupAndStartWatcher(__1.WatchEvent.CONNECT, () => {
126
+ done();
127
+ });
128
+ });
129
+ (0, globals_1.it)("should handle the DATA event", done => {
130
+ setupAndStartWatcher(__1.WatchEvent.DATA, (pod, phase) => {
131
+ (0, globals_1.expect)(pod.metadata?.name).toEqual(`pod-0`);
132
+ (0, globals_1.expect)(phase).toEqual(types_1.WatchPhase.Added);
133
+ done();
134
+ });
135
+ });
136
+ (0, globals_1.it)("should handle the BOOKMARK event", done => {
137
+ setupAndStartWatcher(__1.WatchEvent.BOOKMARK, bookmark => {
138
+ (0, globals_1.expect)(bookmark.metadata?.resourceVersion).toEqual("1");
139
+ done();
140
+ });
141
+ });
142
+ (0, globals_1.it)("should handle the NETWORK_ERROR event", done => {
143
+ nock_1.default.cleanAll();
144
+ (0, nock_1.default)("http://jest-test:8080")
145
+ .get("/api/v1/pods")
146
+ .query({ watch: "true", allowWatchBookmarks: "true" })
147
+ .replyWithError("Something bad happened");
148
+ setupAndStartWatcher(__1.WatchEvent.NETWORK_ERROR, error => {
149
+ (0, globals_1.expect)(error.message).toEqual("request to http://jest-test:8080/api/v1/pods?watch=true&allowWatchBookmarks=true failed, reason: Something bad happened");
150
+ done();
151
+ });
152
+ });
153
+ (0, globals_1.it)("should handle the RECONNECT event", done => {
154
+ nock_1.default.cleanAll();
155
+ (0, nock_1.default)("http://jest-test:8080")
156
+ .get("/api/v1/pods")
157
+ .query({ watch: "true", allowWatchBookmarks: "true" })
158
+ .replyWithError("Something bad happened");
159
+ setupAndStartWatcher(__1.WatchEvent.RECONNECT, error => {
160
+ (0, globals_1.expect)(error.message).toEqual("request to http://jest-test:8080/api/v1/pods?watch=true&allowWatchBookmarks=true failed, reason: Something bad happened");
161
+ done();
162
+ });
163
+ });
164
+ (0, globals_1.it)("should perform a resync after the resync interval", done => {
165
+ watcher = (0, _1.K8s)(__1.kind.Pod).Watch(evtMock, {
166
+ resyncIntervalSec: 1,
167
+ });
168
+ setupAndStartWatcher(__1.WatchEvent.RESYNC, err => {
169
+ (0, globals_1.expect)(err.name).toEqual("Resync");
170
+ (0, globals_1.expect)(err.message).toEqual("Resync triggered by resyncIntervalSec");
171
+ done();
172
+ });
173
+ });
174
+ (0, globals_1.it)("should handle the GIVE_UP event", done => {
175
+ nock_1.default.cleanAll();
176
+ (0, nock_1.default)("http://jest-test:8080");
177
+ watcher = (0, _1.K8s)(__1.kind.Pod).Watch(evtMock, {
178
+ retryMax: 1,
179
+ retryDelaySec: 1,
180
+ });
181
+ setupAndStartWatcher(__1.WatchEvent.GIVE_UP, error => {
182
+ (0, globals_1.expect)(error.message).toContain("request to http://jest-test:8080/api/v1/pods?watch=true&allowWatchBookmarks=true failed");
183
+ done();
184
+ });
185
+ });
186
+ });
187
+ /**
188
+ * Creates a mock pod object
189
+ *
190
+ * @param name The name of the pod
191
+ * @param resourceVersion The resource version of the pod
192
+ * @returns A mock pod object
193
+ */
194
+ function createMockPod(name, resourceVersion) {
195
+ return {
196
+ kind: "Pod",
197
+ apiVersion: "v1",
198
+ metadata: {
199
+ name: name,
200
+ resourceVersion: resourceVersion,
201
+ // ... other metadata fields
202
+ },
203
+ spec: {
204
+ containers: [
205
+ {
206
+ name: "nginx",
207
+ image: "nginx:1.14.2",
208
+ ports: [
209
+ {
210
+ containerPort: 80,
211
+ protocol: "TCP",
212
+ },
213
+ ],
214
+ },
215
+ ],
216
+ },
217
+ status: {
218
+ // ... pod status
219
+ },
220
+ };
221
+ }
package/dist/index.d.ts CHANGED
@@ -4,6 +4,7 @@ import * as kind from "./upstream";
4
4
  export { kind };
5
5
  export { fetch } from "./fetch";
6
6
  export { StatusCodes as fetchStatus } from "http-status-codes";
7
+ export { WatchCfg, WatchEvent } from "./fluent/watch";
7
8
  export { K8s } from "./fluent";
8
9
  export { RegisterKind, modelToGroupVersionKind } from "./kinds";
9
10
  export { GenericKind } from "./types";