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.
- package/dist/fluent/index.d.ts +1 -1
- package/dist/fluent/index.d.ts.map +1 -1
- package/dist/fluent/index.js +2 -2
- package/dist/fluent/types.d.ts +11 -9
- package/dist/fluent/types.d.ts.map +1 -1
- package/dist/fluent/types.js +2 -0
- package/dist/fluent/watch.d.ts +70 -39
- package/dist/fluent/watch.d.ts.map +1 -1
- package/dist/fluent/watch.js +282 -107
- package/dist/fluent/watch.spec.d.ts +2 -0
- package/dist/fluent/watch.spec.d.ts.map +1 -0
- package/dist/fluent/watch.spec.js +221 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +4 -1
- package/package.json +3 -3
- package/src/fluent/index.ts +4 -4
- package/src/fluent/types.ts +13 -11
- package/src/fluent/watch.spec.ts +264 -0
- package/src/fluent/watch.ts +313 -150
- package/src/index.ts +3 -0
package/dist/fluent/watch.js
CHANGED
|
@@ -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.
|
|
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
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
*/
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
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
|
-
|
|
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
|
-
*
|
|
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
|
-
|
|
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
|
-
*
|
|
90
|
+
* Start the watch.
|
|
91
|
+
*
|
|
92
|
+
* @returns The AbortController for the watch.
|
|
62
93
|
*/
|
|
63
|
-
async
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
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
|
-
|
|
106
|
-
|
|
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
|
-
|
|
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
|
|
113
|
-
body.on("
|
|
114
|
-
body.on("
|
|
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
|
-
|
|
231
|
+
void this.#errHandler(e);
|
|
124
232
|
}
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
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
|
-
|
|
142
|
-
|
|
143
|
-
|
|
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
|
-
|
|
149
|
-
|
|
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.
|
|
326
|
+
exports.Watcher = Watcher;
|
|
@@ -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";
|