kubernetes-fluent-client 3.2.2 → 3.3.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,4 +1,6 @@
1
1
  import { EventEmitter } from "events";
2
+ import { RequestInit } from "node-fetch";
3
+ import { Agent } from "undici";
2
4
  import { GenericClass } from "../types";
3
5
  import { Filters, WatchAction } from "./types";
4
6
  export declare enum WatchEvent {
@@ -41,8 +43,8 @@ export type WatchCfg = {
41
43
  relistIntervalSec?: number;
42
44
  /** Max amount of seconds to go without receiving an event before reconciliation starts. Defaults to 300 (5 minutes). */
43
45
  lastSeenLimitSeconds?: number;
44
- /** Use http2 for the Watch */
45
- useHTTP2?: boolean;
46
+ /** Watch Mechansism */
47
+ useLegacyWatch?: boolean;
46
48
  };
47
49
  /** A wrapper around the Kubernetes watch API. */
48
50
  export declare class Watcher<T extends GenericClass> {
@@ -84,5 +86,6 @@ export declare class Watcher<T extends GenericClass> {
84
86
  * @returns an EventEmitter
85
87
  */
86
88
  get events(): EventEmitter;
89
+ static getHTTPSAgent: (opts: RequestInit) => Agent;
87
90
  }
88
91
  //# sourceMappingURL=watch.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"watch.d.ts","sourceRoot":"","sources":["../../src/fluent/watch.ts"],"names":[],"mappings":"AAKA,OAAO,EAAE,YAAY,EAAE,MAAM,QAAQ,CAAC;AAKtC,OAAO,EAAE,YAAY,EAAwB,MAAM,UAAU,CAAC;AAC9D,OAAO,EAAE,OAAO,EAAE,WAAW,EAAqC,MAAM,SAAS,CAAC;AAIlF,oBAAY,UAAU;IACpB,sCAAsC;IACtC,OAAO,YAAY;IACnB,2BAA2B;IAC3B,aAAa,kBAAkB;IAC/B,kDAAkD;IAClD,UAAU,eAAe;IACzB,0BAA0B;IAC1B,SAAS,cAAc;IACvB,8BAA8B;IAC9B,OAAO,YAAY;IACnB,sBAAsB;IACtB,KAAK,UAAU;IACf,mCAAmC;IACnC,IAAI,SAAS;IACb,wCAAwC;IACxC,oBAAoB,yBAAyB;IAC7C,qCAAqC;IACrC,iBAAiB,sBAAsB;IACvC,kCAAkC;IAClC,IAAI,SAAS;IACb,2BAA2B;IAC3B,UAAU,eAAe;IACzB,mBAAmB;IACnB,UAAU,eAAe;IACzB,qCAAqC;IACrC,wBAAwB,6BAA6B;IACrD,iCAAiC;IACjC,eAAe,oBAAoB;CACpC;AAED,4CAA4C;AAC5C,MAAM,MAAM,QAAQ,GAAG;IACrB,+HAA+H;IAC/H,gBAAgB,CAAC,EAAE,MAAM,CAAC;IAC1B,wDAAwD;IACxD,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB,+FAA+F;IAC/F,iBAAiB,CAAC,EAAE,MAAM,CAAC;IAC3B,wHAAwH;IACxH,oBAAoB,CAAC,EAAE,MAAM,CAAC;IAC9B,8BAA8B;IAC9B,QAAQ,CAAC,EAAE,OAAO,CAAC;CACpB,CAAC;AAKF,iDAAiD;AACjD,qBAAa,OAAO,CAAC,CAAC,SAAS,YAAY;;IA0BzC,YAAY,CAAC,EAAE,MAAM,CAAC,OAAO,CAAC;IAgB9B;;;;;;;;;;;OAWG;gBACS,KAAK,EAAE,CAAC,EAAE,OAAO,EAAE,OAAO,EAAE,QAAQ,EAAE,WAAW,CAAC,CAAC,CAAC,EAAE,QAAQ,GAAE,QAAa;IA6CzF;;;;OAIG;IACU,KAAK,IAAI,OAAO,CAAC,eAAe,CAAC;IAU9C,gGAAgG;IACzF,KAAK;IAOZ;;;;;OAKG;IACI,UAAU;IAWjB;;;;;;OAMG;IACH,IAAW,MAAM,IAAI,YAAY,CAEhC;CAufF"}
1
+ {"version":3,"file":"watch.d.ts","sourceRoot":"","sources":["../../src/fluent/watch.ts"],"names":[],"mappings":"AAIA,OAAO,EAAE,YAAY,EAAE,MAAM,QAAQ,CAAC;AAGtC,OAAoB,EAAE,WAAW,EAAE,MAAM,YAAY,CAAC;AACtD,OAAO,EAAE,KAAK,EAAS,MAAM,QAAQ,CAAC;AAEtC,OAAO,EAAE,YAAY,EAAwB,MAAM,UAAU,CAAC;AAC9D,OAAO,EAAE,OAAO,EAAE,WAAW,EAAc,MAAM,SAAS,CAAC;AAK3D,oBAAY,UAAU;IACpB,sCAAsC;IACtC,OAAO,YAAY;IACnB,2BAA2B;IAC3B,aAAa,kBAAkB;IAC/B,kDAAkD;IAClD,UAAU,eAAe;IACzB,0BAA0B;IAC1B,SAAS,cAAc;IACvB,8BAA8B;IAC9B,OAAO,YAAY;IACnB,sBAAsB;IACtB,KAAK,UAAU;IACf,mCAAmC;IACnC,IAAI,SAAS;IACb,wCAAwC;IACxC,oBAAoB,yBAAyB;IAC7C,qCAAqC;IACrC,iBAAiB,sBAAsB;IACvC,kCAAkC;IAClC,IAAI,SAAS;IACb,2BAA2B;IAC3B,UAAU,eAAe;IACzB,mBAAmB;IACnB,UAAU,eAAe;IACzB,qCAAqC;IACrC,wBAAwB,6BAA6B;IACrD,iCAAiC;IACjC,eAAe,oBAAoB;CACpC;AAED,4CAA4C;AAC5C,MAAM,MAAM,QAAQ,GAAG;IACrB,+HAA+H;IAC/H,gBAAgB,CAAC,EAAE,MAAM,CAAC;IAC1B,wDAAwD;IACxD,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB,+FAA+F;IAC/F,iBAAiB,CAAC,EAAE,MAAM,CAAC;IAC3B,wHAAwH;IACxH,oBAAoB,CAAC,EAAE,MAAM,CAAC;IAC9B,uBAAuB;IACvB,cAAc,CAAC,EAAE,OAAO,CAAC;CAC1B,CAAC;AAKF,iDAAiD;AACjD,qBAAa,OAAO,CAAC,CAAC,SAAS,YAAY;;IA0BzC,YAAY,CAAC,EAAE,MAAM,CAAC,OAAO,CAAC;IAgB9B;;;;;;;;;;;OAWG;gBACS,KAAK,EAAE,CAAC,EAAE,OAAO,EAAE,OAAO,EAAE,QAAQ,EAAE,WAAW,CAAC,CAAC,CAAC,EAAE,QAAQ,GAAE,QAAa;IA6CzF;;;;OAIG;IACU,KAAK,IAAI,OAAO,CAAC,eAAe,CAAC;IAU9C,gGAAgG;IACzF,KAAK;IAWZ;;;;;OAKG;IACI,UAAU;IAWjB;;;;;;OAMG;IACH,IAAW,MAAM,IAAI,YAAY,CAEhC;IA+PD,MAAM,CAAC,aAAa,SAAU,WAAW,WAqBvC;CA+KH"}
@@ -7,15 +7,16 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
7
7
  var _a;
8
8
  Object.defineProperty(exports, "__esModule", { value: true });
9
9
  exports.Watcher = exports.WatchEvent = void 0;
10
- const byline_1 = __importDefault(require("byline"));
11
10
  const crypto_1 = require("crypto");
12
11
  const events_1 = require("events");
12
+ const byline_1 = __importDefault(require("byline"));
13
13
  const https_1 = __importDefault(require("https"));
14
- const http2_1 = __importDefault(require("http2"));
15
14
  const node_fetch_1 = __importDefault(require("node-fetch"));
15
+ const undici_1 = require("undici");
16
16
  const fetch_1 = require("../fetch");
17
17
  const types_1 = require("./types");
18
18
  const utils_1 = require("./utils");
19
+ const stream_1 = require("stream");
19
20
  const fs_1 = __importDefault(require("fs"));
20
21
  var WatchEvent;
21
22
  (function (WatchEvent) {
@@ -58,7 +59,7 @@ class Watcher {
58
59
  #callback;
59
60
  #watchCfg;
60
61
  #latestRelistWindow = "";
61
- #useHTTP2 = false;
62
+ #useLegacyWatch = false;
62
63
  // Track the last time data was received
63
64
  #lastSeenTime = NONE;
64
65
  #lastSeenLimit;
@@ -68,6 +69,7 @@ class Watcher {
68
69
  #resyncFailureCount = 0;
69
70
  // Create a stream to read the response body
70
71
  #stream;
72
+ #legacyStream;
71
73
  // Create an EventEmitter to emit events
72
74
  #events = new events_1.EventEmitter();
73
75
  // Create a timer to relist the watch
@@ -101,12 +103,12 @@ class Watcher {
101
103
  watchCfg.relistIntervalSec ??= 600;
102
104
  // Set the resync interval to 10 minutes if not specified
103
105
  watchCfg.lastSeenLimitSeconds ??= 600;
106
+ // Set the watch mechanism
107
+ this.#useLegacyWatch = watchCfg.useLegacyWatch || false;
104
108
  // Set the last seen limit to the resync interval
105
109
  this.#lastSeenLimit = watchCfg.lastSeenLimitSeconds * 1000;
106
110
  // Set the latest relist interval to now
107
111
  this.#latestRelistWindow = new Date().toISOString();
108
- // Set the latest relist interval to now
109
- this.#useHTTP2 = watchCfg.useHTTP2 ?? false;
110
112
  // Add random jitter to the relist/resync intervals (up to 1 second)
111
113
  const jitter = Math.floor(Math.random() * 1000);
112
114
  // Check every relist interval for cache staleness
@@ -132,8 +134,8 @@ class Watcher {
132
134
  */
133
135
  async start() {
134
136
  this.#events.emit(WatchEvent.INIT_CACHE_MISS, this.#latestRelistWindow);
135
- if (this.#useHTTP2) {
136
- await this.#http2Watch();
137
+ if (this.#useLegacyWatch) {
138
+ await this.#legacyWatch();
137
139
  }
138
140
  else {
139
141
  await this.#watch();
@@ -144,7 +146,12 @@ class Watcher {
144
146
  close() {
145
147
  clearInterval(this.$relistTimer);
146
148
  clearInterval(this.#resyncTimer);
147
- this.#streamCleanup();
149
+ if (this.#useLegacyWatch) {
150
+ this.#legacyStreamCleanup();
151
+ }
152
+ else {
153
+ this.#streamCleanup();
154
+ }
148
155
  this.#abortController.abort();
149
156
  }
150
157
  /**
@@ -269,7 +276,6 @@ class Watcher {
269
276
  // If there is a continue token, call the list function again with the same removed items
270
277
  if (continueToken) {
271
278
  // If there is a continue token, call the list function again with the same removed items
272
- // @todo: using all voids here is important for freshness, but is naive with regard to API load & pod resources
273
279
  await this.#list(continueToken, removedItems);
274
280
  }
275
281
  else {
@@ -338,42 +344,40 @@ class Watcher {
338
344
  this.#events.emit(WatchEvent.DATA_ERROR, err);
339
345
  }
340
346
  };
341
- /**
342
- * Watch for changes to the resource.
343
- */
344
- #watch = async () => {
347
+ /** node-fetch watch */
348
+ #legacyWatch = async () => {
345
349
  try {
346
350
  // Start with a list operation
347
351
  await this.#list();
348
352
  // Build the URL and request options
349
353
  const { opts, url } = await this.#buildURL(true, this.#resourceVersion);
350
354
  // Create a stream to read the response body
351
- this.#stream = byline_1.default.createStream();
355
+ this.#legacyStream = byline_1.default.createStream();
352
356
  // Bind the stream events
353
- this.#stream.on("error", this.#errHandler);
354
- this.#stream.on("close", this.#streamCleanup);
355
- this.#stream.on("finish", this.#streamCleanup);
357
+ this.#legacyStream.on("error", this.#errHandler);
358
+ this.#legacyStream.on("close", this.#legacyStreamCleanup);
359
+ this.#legacyStream.on("finish", this.#legacyStreamCleanup);
356
360
  // Make the actual request
357
361
  const response = await (0, node_fetch_1.default)(url, { ...opts });
358
- // Reset the pending reconnect flag
359
- this.#pendingReconnect = false;
360
362
  // If the request is successful, start listening for events
361
363
  if (response.ok) {
364
+ // Reset the pending reconnect flag
365
+ this.#pendingReconnect = false;
362
366
  this.#events.emit(WatchEvent.CONNECT, url.pathname);
363
367
  const { body } = response;
364
368
  // Reset the retry count
365
369
  this.#resyncFailureCount = 0;
366
370
  this.#events.emit(WatchEvent.INC_RESYNC_FAILURE_COUNT, this.#resyncFailureCount);
367
371
  // Listen for events and call the callback function
368
- this.#stream.on("data", async (line) => {
372
+ this.#legacyStream.on("data", async (line) => {
369
373
  await this.#processLine(line, this.#process);
370
374
  });
371
375
  // Bind the body events
372
376
  body.on("error", this.#errHandler);
373
- body.on("close", this.#streamCleanup);
374
- body.on("finish", this.#streamCleanup);
377
+ body.on("close", this.#legacyStreamCleanup);
378
+ body.on("finish", this.#legacyStreamCleanup);
375
379
  // Pipe the response body to the stream
376
- body.pipe(this.#stream);
380
+ body.pipe(this.#legacyStream);
377
381
  }
378
382
  else {
379
383
  throw new Error(`watch connect failed: ${response.status} ${response.statusText}`);
@@ -383,69 +387,84 @@ class Watcher {
383
387
  void this.#errHandler(e);
384
388
  }
385
389
  };
386
- // Configure the agent options for the HTTP/2 client
387
- static #getAgentOptions(opts) {
388
- if (opts.agent && opts.agent instanceof https_1.default.Agent) {
389
- return {
390
- key: opts.agent.options.key,
391
- cert: opts.agent.options.cert,
390
+ static getHTTPSAgent = (opts) => {
391
+ // In cluster there will be agent - testing or dev no
392
+ const agentOptions = opts.agent instanceof https_1.default.Agent
393
+ ? {
392
394
  ca: opts.agent.options.ca,
393
- rejectUnauthorized: false,
395
+ cert: opts.agent.options.cert,
396
+ key: opts.agent.options.key,
397
+ }
398
+ : {
399
+ ca: undefined,
400
+ cert: undefined,
401
+ key: undefined,
394
402
  };
395
- }
396
- return undefined;
397
- }
398
- // Create an HTTP/2 client
399
- static #createHttp2Client(origin, agentOptions) {
400
- return http2_1.default.connect(origin, {
401
- ca: agentOptions?.ca,
402
- cert: agentOptions?.cert,
403
- key: agentOptions?.key,
404
- rejectUnauthorized: agentOptions?.rejectUnauthorized,
403
+ return new undici_1.Agent({
404
+ keepAliveMaxTimeout: 600000,
405
+ keepAliveTimeout: 600000,
406
+ bodyTimeout: 0,
407
+ connect: agentOptions,
405
408
  });
406
- }
407
- // Generate the request headers for the HTTP/2 request
408
- #generateRequestHeaders = async (url) => {
409
- const token = await this.#getToken();
410
- const headers = {
411
- ":method": "GET",
412
- ":path": url.pathname + url.search,
413
- "content-type": "application/json",
414
- "user-agent": "kubernetes-fluent-client",
415
- };
416
- if (token) {
417
- headers["Authorization"] = `Bearer ${token}`;
418
- }
419
- return headers;
420
409
  };
421
410
  /**
422
411
  * Watch for changes to the resource.
423
412
  */
424
- #http2Watch = async () => {
413
+ #watch = async () => {
425
414
  try {
426
415
  // Start with a list operation
427
416
  await this.#list();
428
417
  // Build the URL and request options
429
418
  const { opts, url } = await this.#buildURL(true, this.#resourceVersion);
430
- const agentOptions = _a.#getAgentOptions(opts);
431
- // HTTP/2 client connection setup
432
- const client = _a.#createHttp2Client(url.origin, agentOptions);
433
- // Handle client connection errors
434
- client.on("error", err => {
435
- this.#events.emit(WatchEvent.NETWORK_ERROR, err);
436
- this.#cleanupAndReconnect(client, err);
437
- });
438
- // Set up headers for the HTTP/2 request
439
- const headers = await this.#generateRequestHeaders(url);
440
- // Make the HTTP/2 request
441
- const req = client.request(headers);
442
- req.setEncoding("utf8");
443
- // Handler events for the HTTP/2 request
444
- this.#handleHttp2Request(req, client);
445
- // Handle abort signal
446
- this.#abortController.signal.addEventListener("abort", () => {
447
- this.#streamCleanup(client);
419
+ const token = await this.#getToken();
420
+ const headers = {
421
+ "Content-Type": "application/json",
422
+ "User-Agent": "kubernetes-fluent-client",
423
+ };
424
+ if (token) {
425
+ headers["Authorization"] = `Bearer ${token}`;
426
+ }
427
+ const response = await (0, undici_1.fetch)(url, {
428
+ headers,
429
+ dispatcher: _a.getHTTPSAgent(opts),
448
430
  });
431
+ // If the request is successful, start listening for events
432
+ if (response.ok) {
433
+ // Reset the pending reconnect flag
434
+ this.#pendingReconnect = false;
435
+ this.#events.emit(WatchEvent.CONNECT, url.pathname);
436
+ const { body } = response;
437
+ if (!body) {
438
+ throw new Error("No response body found");
439
+ }
440
+ // Reset the retry count
441
+ this.#resyncFailureCount = 0;
442
+ this.#events.emit(WatchEvent.INC_RESYNC_FAILURE_COUNT, this.#resyncFailureCount);
443
+ this.#stream = stream_1.Readable.from(body);
444
+ const decoder = new TextDecoder();
445
+ let buffer = "";
446
+ // Listen for events and call the callback function
447
+ this.#stream.on("data", async (chunk) => {
448
+ try {
449
+ buffer += decoder.decode(chunk, { stream: true });
450
+ const lines = buffer.split("\n");
451
+ buffer = lines.pop();
452
+ for (const line of lines) {
453
+ await this.#processLine(line, this.#process);
454
+ }
455
+ }
456
+ catch (err) {
457
+ void this.#errHandler(err);
458
+ }
459
+ });
460
+ this.#stream.on("close", this.#cleanupAndReconnect);
461
+ this.#stream.on("end", this.#cleanupAndReconnect);
462
+ this.#stream.on("error", this.#errHandler);
463
+ this.#stream.on("finish", this.#cleanupAndReconnect);
464
+ }
465
+ else {
466
+ throw new Error(`watch connect failed: ${response.status} ${response.statusText}`);
467
+ }
449
468
  }
450
469
  catch (e) {
451
470
  void this.#errHandler(e);
@@ -475,12 +494,12 @@ class Watcher {
475
494
  else {
476
495
  this.#pendingReconnect = true;
477
496
  this.#events.emit(WatchEvent.RECONNECT, this.#resyncFailureCount);
478
- this.#streamCleanup();
479
- if (this.#useHTTP2) {
480
- this.#cleanupAndReconnect();
497
+ if (this.#useLegacyWatch) {
498
+ this.#legacyStreamCleanup();
499
+ void this.#legacyWatch();
481
500
  }
482
- if (!this.#useHTTP2) {
483
- void this.#watch();
501
+ else {
502
+ this.#cleanupAndReconnect();
484
503
  }
485
504
  }
486
505
  }
@@ -501,9 +520,11 @@ class Watcher {
501
520
  case "AbortError":
502
521
  clearInterval(this.$relistTimer);
503
522
  clearInterval(this.#resyncTimer);
504
- this.#streamCleanup();
505
- if (!this.#useHTTP2) {
506
- this.#scheduleReconnect();
523
+ if (this.#useLegacyWatch) {
524
+ this.#legacyStreamCleanup();
525
+ }
526
+ else {
527
+ this.#streamCleanup();
507
528
  }
508
529
  this.#events.emit(WatchEvent.ABORT, err);
509
530
  return;
@@ -519,80 +540,24 @@ class Watcher {
519
540
  // Force a resync
520
541
  this.#lastSeenTime = OVERRIDE;
521
542
  };
522
- /**
523
- *
524
- * @param req - the request stream
525
- * @param client - the client session
526
- */
527
- #handleHttp2Request(req, client) {
528
- let buffer = "";
529
- req.on("response", headers => {
530
- const statusCode = headers[":status"];
531
- if (statusCode && statusCode >= 200 && statusCode < 300) {
532
- this.#onWatchConnected();
533
- }
534
- else {
535
- this.#cleanupAndReconnect(client, new Error(`watch connect failed: ${statusCode}`));
536
- }
537
- });
538
- req.on("data", chunk => {
539
- buffer += chunk;
540
- const lines = buffer.split("\n");
541
- buffer = lines.pop() || ""; // Keep any incomplete line for the next chunk
542
- lines.forEach(line => {
543
- void this.#processLine(line, this.#process);
544
- });
545
- });
546
- req.on("end", () => this.#cleanupAndReconnect(client));
547
- req.on("close", () => this.#cleanupAndReconnect(client));
548
- req.on("error", error => this.#errHandler(error));
549
- }
550
- /** Schedules a reconnect with a delay to prevent rapid reconnections. */
551
- #scheduleReconnect() {
552
- const jitter = Math.floor(Math.random() * 1000);
553
- const delay = (this.#watchCfg.resyncDelaySec ?? 5) * 1000 + jitter;
554
- setTimeout(() => {
555
- this.#events.emit(WatchEvent.RECONNECT, this.#resyncFailureCount);
556
- void this.#http2Watch();
557
- }, delay);
558
- }
559
- /**
560
- * Handle a successful connection to the watch.
561
- */
562
- #onWatchConnected() {
563
- this.#pendingReconnect = false;
564
- this.#events.emit(WatchEvent.CONNECT);
565
- // Reset the retry count
566
- this.#resyncFailureCount = 0;
567
- this.#events.emit(WatchEvent.INC_RESYNC_FAILURE_COUNT, this.#resyncFailureCount);
568
- }
569
- /**
570
- * Cleanup the stream and listeners.
571
- *
572
- * @param client - the client session
573
- */
574
- #streamCleanup = (client) => {
543
+ /** Cleanup the stream and connect */
544
+ #cleanupAndReconnect = () => {
545
+ this.#streamCleanup();
546
+ void this.#watch();
547
+ };
548
+ /** Cleanup the stream and listeners. */
549
+ #streamCleanup = () => {
575
550
  if (this.#stream) {
576
551
  this.#stream.removeAllListeners();
577
552
  this.#stream.destroy();
578
553
  }
579
- if (client) {
580
- client.close();
581
- }
582
554
  };
583
- /**
584
- * Cleanup the stream and listeners and reconnect.
585
- *
586
- * @param client - the client session
587
- * @param error - the error that occurred
588
- */
589
- #cleanupAndReconnect(client, error) {
590
- this.#streamCleanup(client);
591
- if (error) {
592
- void this.#errHandler(error);
555
+ #legacyStreamCleanup = () => {
556
+ if (this.#legacyStream) {
557
+ this.#legacyStream.removeAllListeners();
558
+ this.#legacyStream.destroy();
593
559
  }
594
- this.#scheduleReconnect();
595
- }
560
+ };
596
561
  }
597
562
  exports.Watcher = Watcher;
598
563
  _a = Watcher;
@@ -0,0 +1,66 @@
1
+ import typescriptEslint from "@typescript-eslint/eslint-plugin";
2
+ import globals from "globals";
3
+ import tsParser from "@typescript-eslint/parser";
4
+ import path from "node:path";
5
+ import { fileURLToPath } from "node:url";
6
+ import js from "@eslint/js";
7
+ import { FlatCompat } from "@eslint/eslintrc";
8
+
9
+ const __filename = fileURLToPath(import.meta.url);
10
+ const __dirname = path.dirname(__filename);
11
+ const compat = new FlatCompat({
12
+ baseDirectory: __dirname,
13
+ recommendedConfig: js.configs.recommended,
14
+ allConfig: js.configs.all,
15
+ });
16
+
17
+ export default [
18
+ {
19
+ ignores: ["**/node_modules", "**/dist", "**/__mocks__", "e2e/crds", "e2e/crds"],
20
+ },
21
+ ...compat.extends(
22
+ "eslint:recommended",
23
+ "plugin:@typescript-eslint/recommended",
24
+ "plugin:jsdoc/recommended-typescript-error",
25
+ ),
26
+ {
27
+ plugins: {
28
+ "@typescript-eslint": typescriptEslint,
29
+ },
30
+
31
+ languageOptions: {
32
+ globals: {
33
+ ...Object.fromEntries(Object.entries(globals.browser).map(([key]) => [key, "off"])),
34
+ },
35
+
36
+ parser: tsParser,
37
+ ecmaVersion: 2022,
38
+ sourceType: "script",
39
+
40
+ parserOptions: {
41
+ project: ["tsconfig.json"],
42
+ },
43
+ },
44
+
45
+ rules: {
46
+ "class-methods-use-this": "warn",
47
+ "consistent-this": "warn",
48
+ "no-invalid-this": "warn",
49
+
50
+ "@typescript-eslint/no-floating-promises": [
51
+ "warn",
52
+ {
53
+ ignoreVoid: true,
54
+ },
55
+ ],
56
+
57
+ "jsdoc/tag-lines": [
58
+ "error",
59
+ "any",
60
+ {
61
+ startLines: 1,
62
+ },
63
+ ],
64
+ },
65
+ },
66
+ ];
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "kubernetes-fluent-client",
3
- "version": "3.2.2",
3
+ "version": "3.3.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",
@@ -44,19 +44,23 @@
44
44
  "node-fetch": "2.7.0",
45
45
  "quicktype-core": "23.0.170",
46
46
  "type-fest": "4.26.1",
47
+ "undici": "6.20.1",
47
48
  "yargs": "17.7.2"
48
49
  },
49
50
  "devDependencies": {
50
51
  "@commitlint/cli": "19.5.0",
51
52
  "@commitlint/config-conventional": "19.5.0",
53
+ "@eslint/eslintrc": "^3.1.0",
54
+ "@eslint/js": "^9.14.0",
52
55
  "@jest/globals": "29.7.0",
53
56
  "@types/byline": "4.2.36",
54
- "@types/readable-stream": "4.0.16",
57
+ "@types/readable-stream": "4.0.18",
55
58
  "@types/urijs": "^1.19.25",
56
59
  "@types/yargs": "17.0.33",
57
- "@typescript-eslint/eslint-plugin": "8.12.2",
58
- "@typescript-eslint/parser": "8.12.2",
60
+ "@typescript-eslint/eslint-plugin": "8.13.0",
61
+ "@typescript-eslint/parser": "8.13.0",
59
62
  "eslint-plugin-jsdoc": "50.4.3",
63
+ "globals": "^15.12.0",
60
64
  "husky": "^9.1.6",
61
65
  "jest": "29.7.0",
62
66
  "lint-staged": "^15.2.10",
@@ -1,16 +1,17 @@
1
1
  // SPDX-License-Identifier: Apache-2.0
2
2
  // SPDX-FileCopyrightText: 2023-Present The Kubernetes Fluent Client Authors
3
3
 
4
- import byline from "byline";
5
4
  import { createHash } from "crypto";
6
5
  import { EventEmitter } from "events";
6
+ import byline from "byline";
7
7
  import https from "https";
8
- import http2 from "http2";
9
- import fetch from "node-fetch";
8
+ import legacyFetch, { RequestInit } from "node-fetch";
9
+ import { Agent, fetch } from "undici";
10
10
  import { fetch as wrappedFetch } from "../fetch";
11
11
  import { GenericClass, KubernetesListObject } from "../types";
12
- import { Filters, WatchAction, WatchPhase, Options, AgentOptions } from "./types";
12
+ import { Filters, WatchAction, WatchPhase } from "./types";
13
13
  import { k8sCfg, pathBuilder } from "./utils";
14
+ import { Readable } from "stream";
14
15
  import fs from "fs";
15
16
 
16
17
  export enum WatchEvent {
@@ -54,8 +55,8 @@ export type WatchCfg = {
54
55
  relistIntervalSec?: number;
55
56
  /** Max amount of seconds to go without receiving an event before reconciliation starts. Defaults to 300 (5 minutes). */
56
57
  lastSeenLimitSeconds?: number;
57
- /** Use http2 for the Watch */
58
- useHTTP2?: boolean;
58
+ /** Watch Mechansism */
59
+ useLegacyWatch?: boolean;
59
60
  };
60
61
 
61
62
  const NONE = 50;
@@ -69,8 +70,7 @@ export class Watcher<T extends GenericClass> {
69
70
  #callback: WatchAction<T>;
70
71
  #watchCfg: WatchCfg;
71
72
  #latestRelistWindow: string = "";
72
- #useHTTP2: boolean = false;
73
-
73
+ #useLegacyWatch = false;
74
74
  // Track the last time data was received
75
75
  #lastSeenTime = NONE;
76
76
  #lastSeenLimit: number;
@@ -82,7 +82,8 @@ export class Watcher<T extends GenericClass> {
82
82
  #resyncFailureCount = 0;
83
83
 
84
84
  // Create a stream to read the response body
85
- #stream?: byline.LineStream;
85
+ #stream?: Readable;
86
+ #legacyStream?: byline.LineStream;
86
87
 
87
88
  // Create an EventEmitter to emit events
88
89
  #events = new EventEmitter();
@@ -126,15 +127,15 @@ export class Watcher<T extends GenericClass> {
126
127
  // Set the resync interval to 10 minutes if not specified
127
128
  watchCfg.lastSeenLimitSeconds ??= 600;
128
129
 
130
+ // Set the watch mechanism
131
+ this.#useLegacyWatch = watchCfg.useLegacyWatch || false;
132
+
129
133
  // Set the last seen limit to the resync interval
130
134
  this.#lastSeenLimit = watchCfg.lastSeenLimitSeconds * 1000;
131
135
 
132
136
  // Set the latest relist interval to now
133
137
  this.#latestRelistWindow = new Date().toISOString();
134
138
 
135
- // Set the latest relist interval to now
136
- this.#useHTTP2 = watchCfg.useHTTP2 ?? false;
137
-
138
139
  // Add random jitter to the relist/resync intervals (up to 1 second)
139
140
  const jitter = Math.floor(Math.random() * 1000);
140
141
 
@@ -168,8 +169,8 @@ export class Watcher<T extends GenericClass> {
168
169
  */
169
170
  public async start(): Promise<AbortController> {
170
171
  this.#events.emit(WatchEvent.INIT_CACHE_MISS, this.#latestRelistWindow);
171
- if (this.#useHTTP2) {
172
- await this.#http2Watch();
172
+ if (this.#useLegacyWatch) {
173
+ await this.#legacyWatch();
173
174
  } else {
174
175
  await this.#watch();
175
176
  }
@@ -180,7 +181,11 @@ export class Watcher<T extends GenericClass> {
180
181
  public close() {
181
182
  clearInterval(this.$relistTimer);
182
183
  clearInterval(this.#resyncTimer);
183
- this.#streamCleanup();
184
+ if (this.#useLegacyWatch) {
185
+ this.#legacyStreamCleanup();
186
+ } else {
187
+ this.#streamCleanup();
188
+ }
184
189
  this.#abortController.abort();
185
190
  }
186
191
 
@@ -333,7 +338,6 @@ export class Watcher<T extends GenericClass> {
333
338
  // If there is a continue token, call the list function again with the same removed items
334
339
  if (continueToken) {
335
340
  // If there is a continue token, call the list function again with the same removed items
336
- // @todo: using all voids here is important for freshness, but is naive with regard to API load & pod resources
337
341
  await this.#list(continueToken, removedItems);
338
342
  } else {
339
343
  // Otherwise, process the removed items
@@ -413,11 +417,8 @@ export class Watcher<T extends GenericClass> {
413
417
  this.#events.emit(WatchEvent.DATA_ERROR, err);
414
418
  }
415
419
  };
416
-
417
- /**
418
- * Watch for changes to the resource.
419
- */
420
- #watch = async () => {
420
+ /** node-fetch watch */
421
+ #legacyWatch = async () => {
421
422
  try {
422
423
  // Start with a list operation
423
424
  await this.#list();
@@ -426,21 +427,21 @@ export class Watcher<T extends GenericClass> {
426
427
  const { opts, url } = await this.#buildURL(true, this.#resourceVersion);
427
428
 
428
429
  // Create a stream to read the response body
429
- this.#stream = byline.createStream();
430
+ this.#legacyStream = byline.createStream();
430
431
 
431
432
  // Bind the stream events
432
- this.#stream.on("error", this.#errHandler);
433
- this.#stream.on("close", this.#streamCleanup);
434
- this.#stream.on("finish", this.#streamCleanup);
433
+ this.#legacyStream.on("error", this.#errHandler);
434
+ this.#legacyStream.on("close", this.#legacyStreamCleanup);
435
+ this.#legacyStream.on("finish", this.#legacyStreamCleanup);
435
436
 
436
437
  // Make the actual request
437
- const response = await fetch(url, { ...opts });
438
-
439
- // Reset the pending reconnect flag
440
- this.#pendingReconnect = false;
438
+ const response = await legacyFetch(url, { ...opts });
441
439
 
442
440
  // If the request is successful, start listening for events
443
441
  if (response.ok) {
442
+ // Reset the pending reconnect flag
443
+ this.#pendingReconnect = false;
444
+
444
445
  this.#events.emit(WatchEvent.CONNECT, url.pathname);
445
446
 
446
447
  const { body } = response;
@@ -450,17 +451,17 @@ export class Watcher<T extends GenericClass> {
450
451
  this.#events.emit(WatchEvent.INC_RESYNC_FAILURE_COUNT, this.#resyncFailureCount);
451
452
 
452
453
  // Listen for events and call the callback function
453
- this.#stream.on("data", async line => {
454
+ this.#legacyStream.on("data", async line => {
454
455
  await this.#processLine(line, this.#process);
455
456
  });
456
457
 
457
458
  // Bind the body events
458
459
  body.on("error", this.#errHandler);
459
- body.on("close", this.#streamCleanup);
460
- body.on("finish", this.#streamCleanup);
460
+ body.on("close", this.#legacyStreamCleanup);
461
+ body.on("finish", this.#legacyStreamCleanup);
461
462
 
462
463
  // Pipe the response body to the stream
463
- body.pipe(this.#stream);
464
+ body.pipe(this.#legacyStream);
464
465
  } else {
465
466
  throw new Error(`watch connect failed: ${response.status} ${response.statusText}`);
466
467
  }
@@ -469,79 +470,97 @@ export class Watcher<T extends GenericClass> {
469
470
  }
470
471
  };
471
472
 
472
- // Configure the agent options for the HTTP/2 client
473
- static #getAgentOptions(opts: Options) {
474
- if (opts.agent && opts.agent instanceof https.Agent) {
475
- return {
476
- key: opts.agent.options.key,
477
- cert: opts.agent.options.cert,
478
- ca: opts.agent.options.ca,
479
- rejectUnauthorized: false,
480
- };
481
- }
482
- return undefined;
483
- }
484
-
485
- // Create an HTTP/2 client
486
- static #createHttp2Client(origin: string, agentOptions?: AgentOptions) {
487
- return http2.connect(origin, {
488
- ca: agentOptions?.ca,
489
- cert: agentOptions?.cert,
490
- key: agentOptions?.key,
491
- rejectUnauthorized: agentOptions?.rejectUnauthorized,
473
+ static getHTTPSAgent = (opts: RequestInit) => {
474
+ // In cluster there will be agent - testing or dev no
475
+ const agentOptions =
476
+ opts.agent instanceof https.Agent
477
+ ? {
478
+ ca: opts.agent.options.ca,
479
+ cert: opts.agent.options.cert,
480
+ key: opts.agent.options.key,
481
+ }
482
+ : {
483
+ ca: undefined,
484
+ cert: undefined,
485
+ key: undefined,
486
+ };
487
+
488
+ return new Agent({
489
+ keepAliveMaxTimeout: 600000,
490
+ keepAliveTimeout: 600000,
491
+ bodyTimeout: 0,
492
+ connect: agentOptions,
492
493
  });
493
- }
494
-
495
- // Generate the request headers for the HTTP/2 request
496
- #generateRequestHeaders = async (url: URL) => {
497
- const token = await this.#getToken();
498
- const headers: Record<string, string> = {
499
- ":method": "GET",
500
- ":path": url.pathname + url.search,
501
- "content-type": "application/json",
502
- "user-agent": "kubernetes-fluent-client",
503
- };
504
- if (token) {
505
- headers["Authorization"] = `Bearer ${token}`;
506
- }
507
- return headers;
508
494
  };
509
-
510
495
  /**
511
496
  * Watch for changes to the resource.
512
497
  */
513
- #http2Watch = async () => {
498
+ #watch = async () => {
514
499
  try {
515
500
  // Start with a list operation
516
501
  await this.#list();
517
502
 
518
503
  // Build the URL and request options
519
504
  const { opts, url } = await this.#buildURL(true, this.#resourceVersion);
520
- const agentOptions = Watcher.#getAgentOptions(opts as Options);
521
505
 
522
- // HTTP/2 client connection setup
523
- const client = Watcher.#createHttp2Client(url.origin, agentOptions);
506
+ const token = await this.#getToken();
507
+ const headers: Record<string, string> = {
508
+ "Content-Type": "application/json",
509
+ "User-Agent": "kubernetes-fluent-client",
510
+ };
524
511
 
525
- // Handle client connection errors
526
- client.on("error", err => {
527
- this.#events.emit(WatchEvent.NETWORK_ERROR, err);
528
- this.#cleanupAndReconnect(client, err);
512
+ if (token) {
513
+ headers["Authorization"] = `Bearer ${token}`;
514
+ }
515
+
516
+ const response = await fetch(url, {
517
+ headers,
518
+ dispatcher: Watcher.getHTTPSAgent(opts),
529
519
  });
530
520
 
531
- // Set up headers for the HTTP/2 request
532
- const headers = await this.#generateRequestHeaders(url);
521
+ // If the request is successful, start listening for events
522
+ if (response.ok) {
523
+ // Reset the pending reconnect flag
524
+ this.#pendingReconnect = false;
525
+
526
+ this.#events.emit(WatchEvent.CONNECT, url.pathname);
527
+
528
+ const { body } = response;
533
529
 
534
- // Make the HTTP/2 request
535
- const req = client.request(headers);
536
- req.setEncoding("utf8");
530
+ if (!body) {
531
+ throw new Error("No response body found");
532
+ }
537
533
 
538
- // Handler events for the HTTP/2 request
539
- this.#handleHttp2Request(req, client);
534
+ // Reset the retry count
535
+ this.#resyncFailureCount = 0;
536
+ this.#events.emit(WatchEvent.INC_RESYNC_FAILURE_COUNT, this.#resyncFailureCount);
540
537
 
541
- // Handle abort signal
542
- this.#abortController.signal.addEventListener("abort", () => {
543
- this.#streamCleanup(client);
544
- });
538
+ this.#stream = Readable.from(body);
539
+ const decoder = new TextDecoder();
540
+ let buffer = "";
541
+
542
+ // Listen for events and call the callback function
543
+ this.#stream.on("data", async chunk => {
544
+ try {
545
+ buffer += decoder.decode(chunk, { stream: true });
546
+ const lines = buffer.split("\n");
547
+ buffer = lines.pop()!;
548
+
549
+ for (const line of lines) {
550
+ await this.#processLine(line, this.#process);
551
+ }
552
+ } catch (err) {
553
+ void this.#errHandler(err);
554
+ }
555
+ });
556
+
557
+ this.#stream.on("close", this.#cleanupAndReconnect);
558
+ this.#stream.on("end", this.#cleanupAndReconnect);
559
+ this.#stream.on("error", this.#errHandler);
560
+ this.#stream.on("finish", this.#cleanupAndReconnect);
561
+ } else {
562
+ throw new Error(`watch connect failed: ${response.status} ${response.statusText}`);
563
+ }
545
564
  } catch (e) {
546
565
  void this.#errHandler(e);
547
566
  }
@@ -576,15 +595,12 @@ export class Watcher<T extends GenericClass> {
576
595
  } else {
577
596
  this.#pendingReconnect = true;
578
597
  this.#events.emit(WatchEvent.RECONNECT, this.#resyncFailureCount);
579
- this.#streamCleanup();
580
-
581
- if (this.#useHTTP2) {
598
+ if (this.#useLegacyWatch) {
599
+ this.#legacyStreamCleanup();
600
+ void this.#legacyWatch();
601
+ } else {
582
602
  this.#cleanupAndReconnect();
583
603
  }
584
-
585
- if (!this.#useHTTP2) {
586
- void this.#watch();
587
- }
588
604
  }
589
605
  } else {
590
606
  // Otherwise, call the finally function if it exists
@@ -607,9 +623,10 @@ export class Watcher<T extends GenericClass> {
607
623
  case "AbortError":
608
624
  clearInterval(this.$relistTimer);
609
625
  clearInterval(this.#resyncTimer);
610
- this.#streamCleanup();
611
- if (!this.#useHTTP2) {
612
- this.#scheduleReconnect();
626
+ if (this.#useLegacyWatch) {
627
+ this.#legacyStreamCleanup();
628
+ } else {
629
+ this.#streamCleanup();
613
630
  }
614
631
  this.#events.emit(WatchEvent.ABORT, err);
615
632
  return;
@@ -628,89 +645,25 @@ export class Watcher<T extends GenericClass> {
628
645
  // Force a resync
629
646
  this.#lastSeenTime = OVERRIDE;
630
647
  };
631
- /**
632
- *
633
- * @param req - the request stream
634
- * @param client - the client session
635
- */
636
- #handleHttp2Request(req: http2.ClientHttp2Stream, client: http2.ClientHttp2Session) {
637
- let buffer = "";
638
-
639
- req.on("response", headers => {
640
- const statusCode = headers[":status"];
641
- if (statusCode && statusCode >= 200 && statusCode < 300) {
642
- this.#onWatchConnected();
643
- } else {
644
- this.#cleanupAndReconnect(client, new Error(`watch connect failed: ${statusCode}`));
645
- }
646
- });
647
-
648
- req.on("data", chunk => {
649
- buffer += chunk;
650
- const lines = buffer.split("\n");
651
- buffer = lines.pop() || ""; // Keep any incomplete line for the next chunk
652
-
653
- lines.forEach(line => {
654
- void this.#processLine(line, this.#process);
655
- });
656
- });
657
-
658
- req.on("end", () => this.#cleanupAndReconnect(client));
659
- req.on("close", () => this.#cleanupAndReconnect(client));
660
- req.on("error", error => this.#errHandler(error));
661
- }
662
-
663
- /** Schedules a reconnect with a delay to prevent rapid reconnections. */
664
- #scheduleReconnect() {
665
- const jitter = Math.floor(Math.random() * 1000);
666
- const delay = (this.#watchCfg.resyncDelaySec ?? 5) * 1000 + jitter;
667
648
 
668
- setTimeout(() => {
669
- this.#events.emit(WatchEvent.RECONNECT, this.#resyncFailureCount);
670
- void this.#http2Watch();
671
- }, delay);
672
- }
673
-
674
- /**
675
- * Handle a successful connection to the watch.
676
- */
677
- #onWatchConnected() {
678
- this.#pendingReconnect = false;
679
- this.#events.emit(WatchEvent.CONNECT);
680
-
681
- // Reset the retry count
682
- this.#resyncFailureCount = 0;
683
- this.#events.emit(WatchEvent.INC_RESYNC_FAILURE_COUNT, this.#resyncFailureCount);
684
- }
649
+ /** Cleanup the stream and connect */
650
+ #cleanupAndReconnect = () => {
651
+ this.#streamCleanup();
652
+ void this.#watch();
653
+ };
685
654
 
686
- /**
687
- * Cleanup the stream and listeners.
688
- *
689
- * @param client - the client session
690
- */
691
- #streamCleanup = (client?: http2.ClientHttp2Session) => {
655
+ /** Cleanup the stream and listeners. */
656
+ #streamCleanup = () => {
692
657
  if (this.#stream) {
693
658
  this.#stream.removeAllListeners();
694
659
  this.#stream.destroy();
695
660
  }
696
- if (client) {
697
- client.close();
698
- }
699
661
  };
700
662
 
701
- /**
702
- * Cleanup the stream and listeners and reconnect.
703
- *
704
- * @param client - the client session
705
- * @param error - the error that occurred
706
- */
707
- #cleanupAndReconnect(client?: http2.ClientHttp2Session, error?: Error) {
708
- this.#streamCleanup(client);
709
-
710
- if (error) {
711
- void this.#errHandler(error);
663
+ #legacyStreamCleanup = () => {
664
+ if (this.#legacyStream) {
665
+ this.#legacyStream.removeAllListeners();
666
+ this.#legacyStream.destroy();
712
667
  }
713
-
714
- this.#scheduleReconnect();
715
- }
668
+ };
716
669
  }