kubernetes-fluent-client 3.1.0 → 3.1.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/dist/cli.d.ts +3 -0
- package/dist/cli.d.ts.map +1 -0
- package/dist/cli.js +68 -0
- package/dist/fetch.d.ts +22 -0
- package/dist/fetch.d.ts.map +1 -0
- package/dist/fetch.js +82 -0
- package/dist/fetch.test.d.ts +2 -0
- package/dist/fetch.test.d.ts.map +1 -0
- package/dist/fetch.test.js +97 -0
- package/dist/fileSystem.d.ts +11 -0
- package/dist/fileSystem.d.ts.map +1 -0
- package/dist/fileSystem.js +42 -0
- package/dist/fileSystem.test.d.ts +2 -0
- package/dist/fileSystem.test.d.ts.map +1 -0
- package/dist/fileSystem.test.js +75 -0
- package/dist/fluent/http2-watch.spec.d.ts +2 -0
- package/dist/fluent/http2-watch.spec.d.ts.map +1 -0
- package/dist/fluent/http2-watch.spec.js +284 -0
- package/dist/fluent/index.d.ts +12 -0
- package/dist/fluent/index.d.ts.map +1 -0
- package/dist/fluent/index.js +228 -0
- package/dist/fluent/index.test.d.ts +2 -0
- package/dist/fluent/index.test.d.ts.map +1 -0
- package/dist/fluent/index.test.js +193 -0
- package/dist/fluent/types.d.ts +187 -0
- package/dist/fluent/types.d.ts.map +1 -0
- package/dist/fluent/types.js +16 -0
- package/dist/fluent/utils.d.ts +41 -0
- package/dist/fluent/utils.d.ts.map +1 -0
- package/dist/fluent/utils.js +153 -0
- package/dist/fluent/utils.test.d.ts +2 -0
- package/dist/fluent/utils.test.d.ts.map +1 -0
- package/dist/fluent/utils.test.js +215 -0
- package/dist/fluent/watch.d.ts +88 -0
- package/dist/fluent/watch.d.ts.map +1 -0
- package/dist/fluent/watch.js +546 -0
- 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 +261 -0
- package/dist/generate.d.ts +84 -0
- package/dist/generate.d.ts.map +1 -0
- package/dist/generate.js +208 -0
- package/dist/generate.test.d.ts +2 -0
- package/dist/generate.test.d.ts.map +1 -0
- package/dist/generate.test.js +320 -0
- package/dist/helpers.d.ts +33 -0
- package/dist/helpers.d.ts.map +1 -0
- package/dist/helpers.js +103 -0
- package/dist/helpers.test.d.ts +2 -0
- package/dist/helpers.test.d.ts.map +1 -0
- package/dist/helpers.test.js +37 -0
- package/dist/index.d.ts +14 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +60 -0
- package/dist/kinds.d.ts +16 -0
- package/dist/kinds.d.ts.map +1 -0
- package/dist/kinds.js +570 -0
- package/dist/kinds.test.d.ts +2 -0
- package/dist/kinds.test.d.ts.map +1 -0
- package/dist/kinds.test.js +155 -0
- package/dist/patch.d.ts +7 -0
- package/dist/patch.d.ts.map +1 -0
- package/dist/patch.js +2 -0
- package/dist/postProcessing.d.ts +246 -0
- package/dist/postProcessing.d.ts.map +1 -0
- package/dist/postProcessing.js +497 -0
- package/dist/postProcessing.test.d.ts +2 -0
- package/dist/postProcessing.test.d.ts.map +1 -0
- package/dist/postProcessing.test.js +550 -0
- package/dist/types.d.ts +32 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +16 -0
- package/dist/upstream.d.ts +4 -0
- package/dist/upstream.d.ts.map +1 -0
- package/dist/upstream.js +56 -0
- package/package.json +1 -1
|
@@ -0,0 +1,546 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
// SPDX-License-Identifier: Apache-2.0
|
|
3
|
+
// SPDX-FileCopyrightText: 2023-Present The Kubernetes Fluent Client Authors
|
|
4
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
5
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
6
|
+
};
|
|
7
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
8
|
+
exports.Watcher = exports.WatchEvent = void 0;
|
|
9
|
+
const byline_1 = __importDefault(require("byline"));
|
|
10
|
+
const crypto_1 = require("crypto");
|
|
11
|
+
const events_1 = require("events");
|
|
12
|
+
const https_1 = __importDefault(require("https"));
|
|
13
|
+
const http2_1 = __importDefault(require("http2"));
|
|
14
|
+
const node_fetch_1 = __importDefault(require("node-fetch"));
|
|
15
|
+
const fetch_1 = require("../fetch");
|
|
16
|
+
const types_1 = require("./types");
|
|
17
|
+
const utils_1 = require("./utils");
|
|
18
|
+
const fs_1 = __importDefault(require("fs"));
|
|
19
|
+
var WatchEvent;
|
|
20
|
+
(function (WatchEvent) {
|
|
21
|
+
/** Watch is connected successfully */
|
|
22
|
+
WatchEvent["CONNECT"] = "connect";
|
|
23
|
+
/** Network error occurs */
|
|
24
|
+
WatchEvent["NETWORK_ERROR"] = "network_error";
|
|
25
|
+
/** Error decoding data or running the callback */
|
|
26
|
+
WatchEvent["DATA_ERROR"] = "data_error";
|
|
27
|
+
/** Reconnect is called */
|
|
28
|
+
WatchEvent["RECONNECT"] = "reconnect";
|
|
29
|
+
/** Retry limit is exceeded */
|
|
30
|
+
WatchEvent["GIVE_UP"] = "give_up";
|
|
31
|
+
/** Abort is called */
|
|
32
|
+
WatchEvent["ABORT"] = "abort";
|
|
33
|
+
/** Data is received and decoded */
|
|
34
|
+
WatchEvent["DATA"] = "data";
|
|
35
|
+
/** 410 (old resource version) occurs */
|
|
36
|
+
WatchEvent["OLD_RESOURCE_VERSION"] = "old_resource_version";
|
|
37
|
+
/** A reconnect is already pending */
|
|
38
|
+
WatchEvent["RECONNECT_PENDING"] = "reconnect_pending";
|
|
39
|
+
/** Resource list operation run */
|
|
40
|
+
WatchEvent["LIST"] = "list";
|
|
41
|
+
/** List operation error */
|
|
42
|
+
WatchEvent["LIST_ERROR"] = "list_error";
|
|
43
|
+
/** Cache Misses */
|
|
44
|
+
WatchEvent["CACHE_MISS"] = "cache_miss";
|
|
45
|
+
/** Increment resync failure count */
|
|
46
|
+
WatchEvent["INC_RESYNC_FAILURE_COUNT"] = "inc_resync_failure_count";
|
|
47
|
+
/** Initialize a relist window */
|
|
48
|
+
WatchEvent["INIT_CACHE_MISS"] = "init_cache_miss";
|
|
49
|
+
})(WatchEvent || (exports.WatchEvent = WatchEvent = {}));
|
|
50
|
+
const NONE = 50;
|
|
51
|
+
const OVERRIDE = 100;
|
|
52
|
+
/** A wrapper around the Kubernetes watch API. */
|
|
53
|
+
class Watcher {
|
|
54
|
+
// User-provided properties
|
|
55
|
+
#model;
|
|
56
|
+
#filters;
|
|
57
|
+
#callback;
|
|
58
|
+
#watchCfg;
|
|
59
|
+
#latestRelistWindow = "";
|
|
60
|
+
#useHTTP2 = false;
|
|
61
|
+
// Track the last time data was received
|
|
62
|
+
#lastSeenTime = NONE;
|
|
63
|
+
#lastSeenLimit;
|
|
64
|
+
// Create a wrapped AbortController to allow the watch to be aborted externally
|
|
65
|
+
#abortController;
|
|
66
|
+
// Track the number of retries
|
|
67
|
+
#resyncFailureCount = 0;
|
|
68
|
+
// Create a stream to read the response body
|
|
69
|
+
#stream;
|
|
70
|
+
// Create an EventEmitter to emit events
|
|
71
|
+
#events = new events_1.EventEmitter();
|
|
72
|
+
// Create a timer to relist the watch
|
|
73
|
+
$relistTimer;
|
|
74
|
+
// Create a timer to resync the watch
|
|
75
|
+
#resyncTimer;
|
|
76
|
+
// Track if a reconnect is pending
|
|
77
|
+
#pendingReconnect = false;
|
|
78
|
+
// The resource version to start the watch at, this will be updated after the list operation.
|
|
79
|
+
#resourceVersion;
|
|
80
|
+
// Track the list of items in the cache
|
|
81
|
+
#cache = new Map();
|
|
82
|
+
// Token Path
|
|
83
|
+
#TOKEN_PATH = "/var/run/secrets/kubernetes.io/serviceaccount/token";
|
|
84
|
+
/**
|
|
85
|
+
* Setup a Kubernetes watcher for the specified model and filters. The callback function will be called for each event received.
|
|
86
|
+
* The watch can be aborted by calling {@link Watcher.close} or by calling abort() on the AbortController returned by {@link Watcher.start}.
|
|
87
|
+
*
|
|
88
|
+
*
|
|
89
|
+
* Kubernetes API docs: {@link https://kubernetes.io/docs/reference/using-api/api-concepts/#efficient-detection-of-changes}
|
|
90
|
+
*
|
|
91
|
+
* @param model - the model to use for the API
|
|
92
|
+
* @param filters - (optional) filter overrides, can also be chained
|
|
93
|
+
* @param callback - the callback function to call when an event is received
|
|
94
|
+
* @param watchCfg - (optional) watch configuration
|
|
95
|
+
*/
|
|
96
|
+
constructor(model, filters, callback, watchCfg = {}) {
|
|
97
|
+
// Set the retry delay to 5 seconds if not specified
|
|
98
|
+
watchCfg.resyncDelaySec ??= 5;
|
|
99
|
+
// Set the relist interval to 10 minutes if not specified
|
|
100
|
+
watchCfg.relistIntervalSec ??= 600;
|
|
101
|
+
// Set the resync interval to 10 minutes if not specified
|
|
102
|
+
watchCfg.lastSeenLimitSeconds ??= 600;
|
|
103
|
+
// Set the last seen limit to the resync interval
|
|
104
|
+
this.#lastSeenLimit = watchCfg.lastSeenLimitSeconds * 1000;
|
|
105
|
+
// Set the latest relist interval to now
|
|
106
|
+
this.#latestRelistWindow = new Date().toISOString();
|
|
107
|
+
// Set the latest relist interval to now
|
|
108
|
+
this.#useHTTP2 = watchCfg.useHTTP2 ?? false;
|
|
109
|
+
// Add random jitter to the relist/resync intervals (up to 1 second)
|
|
110
|
+
const jitter = Math.floor(Math.random() * 1000);
|
|
111
|
+
// Check every relist interval for cache staleness
|
|
112
|
+
this.$relistTimer = setInterval(() => {
|
|
113
|
+
this.#latestRelistWindow = new Date().toISOString();
|
|
114
|
+
this.#events.emit(WatchEvent.INIT_CACHE_MISS, this.#latestRelistWindow);
|
|
115
|
+
void this.#list();
|
|
116
|
+
}, watchCfg.relistIntervalSec * 1000 + jitter);
|
|
117
|
+
// Rebuild the watch every resync delay interval
|
|
118
|
+
this.#resyncTimer = setInterval(this.#checkResync, watchCfg.resyncDelaySec * 1000 + jitter);
|
|
119
|
+
// Bind class properties
|
|
120
|
+
this.#model = model;
|
|
121
|
+
this.#filters = filters;
|
|
122
|
+
this.#callback = callback;
|
|
123
|
+
this.#watchCfg = watchCfg;
|
|
124
|
+
// Create a new AbortController
|
|
125
|
+
this.#abortController = new AbortController();
|
|
126
|
+
}
|
|
127
|
+
/**
|
|
128
|
+
* Start the watch.
|
|
129
|
+
*
|
|
130
|
+
* @returns The AbortController for the watch.
|
|
131
|
+
*/
|
|
132
|
+
async start() {
|
|
133
|
+
this.#events.emit(WatchEvent.INIT_CACHE_MISS, this.#latestRelistWindow);
|
|
134
|
+
if (this.#useHTTP2) {
|
|
135
|
+
await this.#http2Watch();
|
|
136
|
+
}
|
|
137
|
+
else {
|
|
138
|
+
await this.#watch();
|
|
139
|
+
}
|
|
140
|
+
return this.#abortController;
|
|
141
|
+
}
|
|
142
|
+
/** Close the watch. Also available on the AbortController returned by {@link Watcher.start}. */
|
|
143
|
+
close() {
|
|
144
|
+
clearInterval(this.$relistTimer);
|
|
145
|
+
clearInterval(this.#resyncTimer);
|
|
146
|
+
this.#streamCleanup();
|
|
147
|
+
this.#abortController.abort();
|
|
148
|
+
}
|
|
149
|
+
/**
|
|
150
|
+
* Get a unique ID for the watch based on the model and filters.
|
|
151
|
+
* This is useful for caching the watch data or resource versions.
|
|
152
|
+
*
|
|
153
|
+
* @returns the watch CacheID
|
|
154
|
+
*/
|
|
155
|
+
getCacheID() {
|
|
156
|
+
// Build the URL, we don't care about the server URL or resourceVersion
|
|
157
|
+
const url = (0, utils_1.pathBuilder)("https://ignore", this.#model, this.#filters, false);
|
|
158
|
+
// Hash and truncate the ID to 10 characters, cache the result
|
|
159
|
+
return (0, crypto_1.createHash)("sha224")
|
|
160
|
+
.update(url.pathname + url.search)
|
|
161
|
+
.digest("hex")
|
|
162
|
+
.substring(0, 10);
|
|
163
|
+
}
|
|
164
|
+
/**
|
|
165
|
+
* Subscribe to watch events. This is an EventEmitter that emits the following events:
|
|
166
|
+
*
|
|
167
|
+
* Use {@link WatchEvent} for the event names.
|
|
168
|
+
*
|
|
169
|
+
* @returns an EventEmitter
|
|
170
|
+
*/
|
|
171
|
+
get events() {
|
|
172
|
+
return this.#events;
|
|
173
|
+
}
|
|
174
|
+
/**
|
|
175
|
+
* Read the serviceAccount Token
|
|
176
|
+
*
|
|
177
|
+
* @returns token or null
|
|
178
|
+
*/
|
|
179
|
+
async #getToken() {
|
|
180
|
+
try {
|
|
181
|
+
return (await fs_1.default.promises.readFile(this.#TOKEN_PATH, "utf8")).trim();
|
|
182
|
+
}
|
|
183
|
+
catch {
|
|
184
|
+
return null;
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
/**
|
|
188
|
+
* Build the URL and request options for the watch.
|
|
189
|
+
*
|
|
190
|
+
* @param isWatch - whether the request is for a watch operation
|
|
191
|
+
* @param resourceVersion - the resource version to use for the watch
|
|
192
|
+
* @param continueToken - the continue token for the watch
|
|
193
|
+
*
|
|
194
|
+
* @returns the URL and request options
|
|
195
|
+
*/
|
|
196
|
+
#buildURL = async (isWatch, resourceVersion, continueToken) => {
|
|
197
|
+
// Build the path and query params for the resource, excluding the name
|
|
198
|
+
const { opts, serverUrl } = await (0, utils_1.k8sCfg)("GET");
|
|
199
|
+
const url = (0, utils_1.pathBuilder)(serverUrl, this.#model, this.#filters, true);
|
|
200
|
+
// Enable the watch query param
|
|
201
|
+
if (isWatch) {
|
|
202
|
+
url.searchParams.set("watch", "true");
|
|
203
|
+
}
|
|
204
|
+
if (continueToken) {
|
|
205
|
+
url.searchParams.set("continue", continueToken);
|
|
206
|
+
}
|
|
207
|
+
// If a name is specified, add it to the query params
|
|
208
|
+
if (this.#filters.name) {
|
|
209
|
+
url.searchParams.set("fieldSelector", `metadata.name=${this.#filters.name}`);
|
|
210
|
+
}
|
|
211
|
+
// If a resource version is specified, add it to the query params
|
|
212
|
+
if (resourceVersion) {
|
|
213
|
+
url.searchParams.set("resourceVersion", resourceVersion);
|
|
214
|
+
}
|
|
215
|
+
// Add the abort signal to the request options
|
|
216
|
+
opts.signal = this.#abortController.signal;
|
|
217
|
+
return { opts, url };
|
|
218
|
+
};
|
|
219
|
+
/**
|
|
220
|
+
* Retrieve the list of resources and process the events.
|
|
221
|
+
*
|
|
222
|
+
* @param continueToken - the continue token for the list
|
|
223
|
+
* @param removedItems - the list of items that have been removed
|
|
224
|
+
*/
|
|
225
|
+
#list = async (continueToken, removedItems) => {
|
|
226
|
+
try {
|
|
227
|
+
const { opts, url } = await this.#buildURL(false, undefined, continueToken);
|
|
228
|
+
// Make the request to list the resources
|
|
229
|
+
const response = await (0, fetch_1.fetch)(url, opts);
|
|
230
|
+
const list = response.data;
|
|
231
|
+
// If the request fails, emit an error event and return
|
|
232
|
+
if (!response.ok) {
|
|
233
|
+
this.#events.emit(WatchEvent.LIST_ERROR, new Error(`list failed: ${response.status} ${response.statusText}`));
|
|
234
|
+
return;
|
|
235
|
+
}
|
|
236
|
+
// Gross hack, thanks upstream library :<
|
|
237
|
+
if (list.metadata.continue) {
|
|
238
|
+
continueToken = list.metadata.continue;
|
|
239
|
+
}
|
|
240
|
+
// Emit the list event
|
|
241
|
+
this.#events.emit(WatchEvent.LIST, list);
|
|
242
|
+
// Update the resource version from the list metadata
|
|
243
|
+
this.#resourceVersion = list.metadata?.resourceVersion;
|
|
244
|
+
// If removed items are not provided, clone the cache
|
|
245
|
+
removedItems = removedItems || new Map(this.#cache.entries());
|
|
246
|
+
// Process each item in the list
|
|
247
|
+
for (const item of list.items || []) {
|
|
248
|
+
const { uid } = item.metadata;
|
|
249
|
+
// Remove the item from the removed items list
|
|
250
|
+
const alreadyExists = removedItems.delete(uid);
|
|
251
|
+
// If the item does not exist, it is new and should be added
|
|
252
|
+
if (!alreadyExists) {
|
|
253
|
+
this.#events.emit(WatchEvent.CACHE_MISS, this.#latestRelistWindow);
|
|
254
|
+
// Send added event. Use void here because we don't care about the result (no consequences here if it fails)
|
|
255
|
+
void this.#process(item, types_1.WatchPhase.Added);
|
|
256
|
+
continue;
|
|
257
|
+
}
|
|
258
|
+
// Check if the resource version has changed for items that already exist
|
|
259
|
+
const cachedRV = parseInt(this.#cache.get(uid)?.metadata?.resourceVersion);
|
|
260
|
+
const itemRV = parseInt(item.metadata.resourceVersion);
|
|
261
|
+
// Check if the resource version is newer than the cached version
|
|
262
|
+
if (itemRV > cachedRV) {
|
|
263
|
+
this.#events.emit(WatchEvent.CACHE_MISS, this.#latestRelistWindow);
|
|
264
|
+
// Send a modified event if the resource version has changed
|
|
265
|
+
void this.#process(item, types_1.WatchPhase.Modified);
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
// If there is a continue token, call the list function again with the same removed items
|
|
269
|
+
if (continueToken) {
|
|
270
|
+
// If there is a continue token, call the list function again with the same removed items
|
|
271
|
+
// @todo: using all voids here is important for freshness, but is naive with regard to API load & pod resources
|
|
272
|
+
await this.#list(continueToken, removedItems);
|
|
273
|
+
}
|
|
274
|
+
else {
|
|
275
|
+
// Otherwise, process the removed items
|
|
276
|
+
for (const item of removedItems.values()) {
|
|
277
|
+
this.#events.emit(WatchEvent.CACHE_MISS, this.#latestRelistWindow);
|
|
278
|
+
void this.#process(item, types_1.WatchPhase.Deleted);
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
catch (err) {
|
|
283
|
+
this.#events.emit(WatchEvent.LIST_ERROR, err);
|
|
284
|
+
}
|
|
285
|
+
};
|
|
286
|
+
/**
|
|
287
|
+
* Process the event payload.
|
|
288
|
+
*
|
|
289
|
+
* @param payload - the event payload
|
|
290
|
+
* @param phase - the event phase
|
|
291
|
+
*/
|
|
292
|
+
#process = async (payload, phase) => {
|
|
293
|
+
try {
|
|
294
|
+
switch (phase) {
|
|
295
|
+
// If the event is added or modified, update the cache
|
|
296
|
+
case types_1.WatchPhase.Added:
|
|
297
|
+
case types_1.WatchPhase.Modified:
|
|
298
|
+
this.#cache.set(payload.metadata.uid, payload);
|
|
299
|
+
break;
|
|
300
|
+
// If the event is deleted, remove the item from the cache
|
|
301
|
+
case types_1.WatchPhase.Deleted:
|
|
302
|
+
this.#cache.delete(payload.metadata.uid);
|
|
303
|
+
break;
|
|
304
|
+
}
|
|
305
|
+
// Emit the data event
|
|
306
|
+
this.#events.emit(WatchEvent.DATA, payload, phase);
|
|
307
|
+
// Call the callback function with the parsed payload
|
|
308
|
+
await this.#callback(payload, phase);
|
|
309
|
+
}
|
|
310
|
+
catch (err) {
|
|
311
|
+
this.#events.emit(WatchEvent.DATA_ERROR, err);
|
|
312
|
+
}
|
|
313
|
+
};
|
|
314
|
+
// process a line from the chunk
|
|
315
|
+
#processLine = async (line, process) => {
|
|
316
|
+
try {
|
|
317
|
+
// Parse the event payload
|
|
318
|
+
const { object: payload, type: phase } = JSON.parse(line);
|
|
319
|
+
// Update the last seen time
|
|
320
|
+
this.#lastSeenTime = Date.now();
|
|
321
|
+
// If the watch is too old, remove the resourceVersion and reload the watch
|
|
322
|
+
if (phase === types_1.WatchPhase.Error && payload.code === 410) {
|
|
323
|
+
throw {
|
|
324
|
+
name: "TooOld",
|
|
325
|
+
message: this.#resourceVersion,
|
|
326
|
+
};
|
|
327
|
+
}
|
|
328
|
+
// Process the event payload, do not update the resource version as that is handled by the list operation
|
|
329
|
+
await process(payload, phase);
|
|
330
|
+
}
|
|
331
|
+
catch (err) {
|
|
332
|
+
if (err.name === "TooOld") {
|
|
333
|
+
// Reload the watch
|
|
334
|
+
void this.#errHandler(err);
|
|
335
|
+
return;
|
|
336
|
+
}
|
|
337
|
+
this.#events.emit(WatchEvent.DATA_ERROR, err);
|
|
338
|
+
}
|
|
339
|
+
};
|
|
340
|
+
/**
|
|
341
|
+
* Watch for changes to the resource.
|
|
342
|
+
*/
|
|
343
|
+
#watch = async () => {
|
|
344
|
+
try {
|
|
345
|
+
// Start with a list operation
|
|
346
|
+
await this.#list();
|
|
347
|
+
// Build the URL and request options
|
|
348
|
+
const { opts, url } = await this.#buildURL(true, this.#resourceVersion);
|
|
349
|
+
// Create a stream to read the response body
|
|
350
|
+
this.#stream = byline_1.default.createStream();
|
|
351
|
+
// Bind the stream events
|
|
352
|
+
this.#stream.on("error", this.#errHandler);
|
|
353
|
+
this.#stream.on("close", this.#streamCleanup);
|
|
354
|
+
this.#stream.on("finish", this.#streamCleanup);
|
|
355
|
+
// Make the actual request
|
|
356
|
+
const response = await (0, node_fetch_1.default)(url, { ...opts });
|
|
357
|
+
// Reset the pending reconnect flag
|
|
358
|
+
this.#pendingReconnect = false;
|
|
359
|
+
// If the request is successful, start listening for events
|
|
360
|
+
if (response.ok) {
|
|
361
|
+
this.#events.emit(WatchEvent.CONNECT, url.pathname);
|
|
362
|
+
const { body } = response;
|
|
363
|
+
// Reset the retry count
|
|
364
|
+
this.#resyncFailureCount = 0;
|
|
365
|
+
this.#events.emit(WatchEvent.INC_RESYNC_FAILURE_COUNT, this.#resyncFailureCount);
|
|
366
|
+
// Listen for events and call the callback function
|
|
367
|
+
this.#stream.on("data", async (line) => {
|
|
368
|
+
await this.#processLine(line, this.#process);
|
|
369
|
+
});
|
|
370
|
+
// Bind the body events
|
|
371
|
+
body.on("error", this.#errHandler);
|
|
372
|
+
body.on("close", this.#streamCleanup);
|
|
373
|
+
body.on("finish", this.#streamCleanup);
|
|
374
|
+
// Pipe the response body to the stream
|
|
375
|
+
body.pipe(this.#stream);
|
|
376
|
+
}
|
|
377
|
+
else {
|
|
378
|
+
throw new Error(`watch connect failed: ${response.status} ${response.statusText}`);
|
|
379
|
+
}
|
|
380
|
+
}
|
|
381
|
+
catch (e) {
|
|
382
|
+
void this.#errHandler(e);
|
|
383
|
+
}
|
|
384
|
+
};
|
|
385
|
+
/**
|
|
386
|
+
* Watch for changes to the resource.
|
|
387
|
+
*/
|
|
388
|
+
#http2Watch = async () => {
|
|
389
|
+
try {
|
|
390
|
+
// Start with a list operation
|
|
391
|
+
await this.#list();
|
|
392
|
+
// Build the URL and request options
|
|
393
|
+
const { opts, url } = await this.#buildURL(true, this.#resourceVersion);
|
|
394
|
+
let agentOptions;
|
|
395
|
+
if (opts.agent && opts.agent instanceof https_1.default.Agent) {
|
|
396
|
+
agentOptions = {
|
|
397
|
+
key: opts.agent.options.key,
|
|
398
|
+
cert: opts.agent.options.cert,
|
|
399
|
+
ca: opts.agent.options.ca,
|
|
400
|
+
rejectUnauthorized: false,
|
|
401
|
+
};
|
|
402
|
+
}
|
|
403
|
+
// HTTP/2 client connection setup
|
|
404
|
+
const client = http2_1.default.connect(url.origin, {
|
|
405
|
+
ca: agentOptions?.ca,
|
|
406
|
+
cert: agentOptions?.cert,
|
|
407
|
+
key: agentOptions?.key,
|
|
408
|
+
rejectUnauthorized: agentOptions?.rejectUnauthorized,
|
|
409
|
+
});
|
|
410
|
+
// Set up headers for the HTTP/2 request
|
|
411
|
+
const token = await this.#getToken();
|
|
412
|
+
const headers = {
|
|
413
|
+
":method": "GET",
|
|
414
|
+
":path": url.pathname + url.search,
|
|
415
|
+
"content-type": "application/json",
|
|
416
|
+
"user-agent": "kubernetes-fluent-client",
|
|
417
|
+
};
|
|
418
|
+
if (token) {
|
|
419
|
+
headers["Authorization"] = `Bearer ${token}`;
|
|
420
|
+
}
|
|
421
|
+
// Make the HTTP/2 request
|
|
422
|
+
const req = client.request(headers);
|
|
423
|
+
req.setEncoding("utf8");
|
|
424
|
+
let buffer = "";
|
|
425
|
+
// Handle response data
|
|
426
|
+
req.on("response", headers => {
|
|
427
|
+
const statusCode = headers[":status"];
|
|
428
|
+
if (statusCode && statusCode >= 200 && statusCode < 300) {
|
|
429
|
+
this.#pendingReconnect = false;
|
|
430
|
+
this.#events.emit(WatchEvent.CONNECT, url.pathname);
|
|
431
|
+
// Reset the retry count
|
|
432
|
+
this.#resyncFailureCount = 0;
|
|
433
|
+
this.#events.emit(WatchEvent.INC_RESYNC_FAILURE_COUNT, this.#resyncFailureCount);
|
|
434
|
+
req.on("data", async (chunk) => {
|
|
435
|
+
try {
|
|
436
|
+
buffer += chunk;
|
|
437
|
+
const lines = buffer.split("\n");
|
|
438
|
+
// Avoid Watch event data_error received. Unexpected end of JSON input.
|
|
439
|
+
buffer = lines.pop();
|
|
440
|
+
for (const line of lines) {
|
|
441
|
+
await this.#processLine(line, this.#process);
|
|
442
|
+
}
|
|
443
|
+
}
|
|
444
|
+
catch (err) {
|
|
445
|
+
void this.#errHandler(err);
|
|
446
|
+
}
|
|
447
|
+
});
|
|
448
|
+
req.on("end", () => {
|
|
449
|
+
client.close();
|
|
450
|
+
this.#streamCleanup();
|
|
451
|
+
});
|
|
452
|
+
req.on("close", () => {
|
|
453
|
+
client.close();
|
|
454
|
+
this.#streamCleanup();
|
|
455
|
+
});
|
|
456
|
+
req.on("error", err => {
|
|
457
|
+
void this.#errHandler(err);
|
|
458
|
+
});
|
|
459
|
+
}
|
|
460
|
+
else {
|
|
461
|
+
const statusMessage = headers[":status-text"] || "Unknown";
|
|
462
|
+
throw new Error(`watch connect failed: ${statusCode} ${statusMessage}`);
|
|
463
|
+
}
|
|
464
|
+
});
|
|
465
|
+
req.on("error", err => {
|
|
466
|
+
void this.#errHandler(err);
|
|
467
|
+
});
|
|
468
|
+
}
|
|
469
|
+
catch (e) {
|
|
470
|
+
void this.#errHandler(e);
|
|
471
|
+
}
|
|
472
|
+
};
|
|
473
|
+
/** Clear the resync timer and schedule a new one. */
|
|
474
|
+
#checkResync = () => {
|
|
475
|
+
// Ignore if the last seen time is not set
|
|
476
|
+
if (this.#lastSeenTime === NONE) {
|
|
477
|
+
return;
|
|
478
|
+
}
|
|
479
|
+
const now = Date.now();
|
|
480
|
+
// If the last seen time is greater than the limit, trigger a resync
|
|
481
|
+
if (this.#lastSeenTime == OVERRIDE || now - this.#lastSeenTime > this.#lastSeenLimit) {
|
|
482
|
+
// Reset the last seen time to now to allow the resync to be called again in case of failure
|
|
483
|
+
this.#lastSeenTime = now;
|
|
484
|
+
// If there are more attempts, retry the watch (undefined is unlimited retries)
|
|
485
|
+
if (this.#watchCfg.resyncFailureMax === undefined ||
|
|
486
|
+
this.#watchCfg.resyncFailureMax > this.#resyncFailureCount) {
|
|
487
|
+
// Increment the retry count
|
|
488
|
+
this.#resyncFailureCount++;
|
|
489
|
+
this.#events.emit(WatchEvent.INC_RESYNC_FAILURE_COUNT, this.#resyncFailureCount);
|
|
490
|
+
if (this.#pendingReconnect) {
|
|
491
|
+
// wait for the connection to be re-established
|
|
492
|
+
this.#events.emit(WatchEvent.RECONNECT_PENDING);
|
|
493
|
+
}
|
|
494
|
+
else {
|
|
495
|
+
this.#pendingReconnect = true;
|
|
496
|
+
this.#events.emit(WatchEvent.RECONNECT, this.#resyncFailureCount);
|
|
497
|
+
this.#streamCleanup();
|
|
498
|
+
if (!this.#useHTTP2) {
|
|
499
|
+
void this.#watch();
|
|
500
|
+
}
|
|
501
|
+
}
|
|
502
|
+
}
|
|
503
|
+
else {
|
|
504
|
+
// Otherwise, call the finally function if it exists
|
|
505
|
+
this.#events.emit(WatchEvent.GIVE_UP, new Error(`Retry limit (${this.#watchCfg.resyncFailureMax}) exceeded, giving up`));
|
|
506
|
+
this.close();
|
|
507
|
+
}
|
|
508
|
+
}
|
|
509
|
+
};
|
|
510
|
+
/**
|
|
511
|
+
* Handle errors from the stream.
|
|
512
|
+
*
|
|
513
|
+
* @param err - the error that occurred
|
|
514
|
+
*/
|
|
515
|
+
#errHandler = async (err) => {
|
|
516
|
+
switch (err.name) {
|
|
517
|
+
case "AbortError":
|
|
518
|
+
clearInterval(this.$relistTimer);
|
|
519
|
+
clearInterval(this.#resyncTimer);
|
|
520
|
+
this.#streamCleanup();
|
|
521
|
+
this.#events.emit(WatchEvent.ABORT, err);
|
|
522
|
+
return;
|
|
523
|
+
case "TooOld":
|
|
524
|
+
// Purge the resource version if it is too old
|
|
525
|
+
this.#resourceVersion = undefined;
|
|
526
|
+
this.#events.emit(WatchEvent.OLD_RESOURCE_VERSION, err.message);
|
|
527
|
+
break;
|
|
528
|
+
default:
|
|
529
|
+
this.#events.emit(WatchEvent.NETWORK_ERROR, err);
|
|
530
|
+
break;
|
|
531
|
+
}
|
|
532
|
+
// Force a resync
|
|
533
|
+
this.#lastSeenTime = OVERRIDE;
|
|
534
|
+
};
|
|
535
|
+
/** Cleanup the stream and listeners. */
|
|
536
|
+
#streamCleanup = () => {
|
|
537
|
+
if (this.#stream) {
|
|
538
|
+
this.#stream.removeAllListeners();
|
|
539
|
+
this.#stream.destroy();
|
|
540
|
+
}
|
|
541
|
+
if (this.#useHTTP2) {
|
|
542
|
+
void this.#http2Watch();
|
|
543
|
+
}
|
|
544
|
+
};
|
|
545
|
+
}
|
|
546
|
+
exports.Watcher = Watcher;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"watch.spec.d.ts","sourceRoot":"","sources":["../../src/fluent/watch.spec.ts"],"names":[],"mappings":""}
|