kubernetes-fluent-client 3.0.4 → 4.0.0-rc-http2-watch

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.
@@ -41,6 +41,8 @@ export type WatchCfg = {
41
41
  relistIntervalSec?: number;
42
42
  /** Max amount of seconds to go without receiving an event before reconciliation starts. Defaults to 300 (5 minutes). */
43
43
  lastSeenLimitSeconds?: number;
44
+ /** Use http2 for the Watch */
45
+ useHTTP2?: boolean;
44
46
  };
45
47
  /** A wrapper around the Kubernetes watch API. */
46
48
  export declare class Watcher<T extends GenericClass> {
@@ -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;AAItC,OAAO,EAAE,YAAY,EAAwB,MAAM,UAAU,CAAC;AAC9D,OAAO,EAAE,OAAO,EAAE,WAAW,EAAc,MAAM,SAAS,CAAC;AAG3D,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;CAC/B,CAAC;AAKF,iDAAiD;AACjD,qBAAa,OAAO,CAAC,CAAC,SAAS,YAAY;;IAyBzC,YAAY,CAAC,EAAE,MAAM,CAAC,OAAO,CAAC;IAc9B;;;;;;;;;;;OAWG;gBACS,KAAK,EAAE,CAAC,EAAE,OAAO,EAAE,OAAO,EAAE,QAAQ,EAAE,WAAW,CAAC,CAAC,CAAC,EAAE,QAAQ,GAAE,QAAa;IA0CzF;;;;OAIG;IACU,KAAK,IAAI,OAAO,CAAC,eAAe,CAAC;IAM9C,gGAAgG;IACzF,KAAK;IAOZ;;;;;OAKG;IACI,UAAU;IAWjB;;;;;;OAMG;IACH,IAAW,MAAM,IAAI,YAAY,CAEhC;CAiUF"}
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,EAAc,MAAM,SAAS,CAAC;AAI3D,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;CA8bF"}
@@ -9,10 +9,13 @@ exports.Watcher = exports.WatchEvent = void 0;
9
9
  const byline_1 = __importDefault(require("byline"));
10
10
  const crypto_1 = require("crypto");
11
11
  const events_1 = require("events");
12
+ const https_1 = __importDefault(require("https"));
13
+ const http2_1 = __importDefault(require("http2"));
12
14
  const node_fetch_1 = __importDefault(require("node-fetch"));
13
15
  const fetch_1 = require("../fetch");
14
16
  const types_1 = require("./types");
15
17
  const utils_1 = require("./utils");
18
+ const fs_1 = __importDefault(require("fs"));
16
19
  var WatchEvent;
17
20
  (function (WatchEvent) {
18
21
  /** Watch is connected successfully */
@@ -54,6 +57,7 @@ class Watcher {
54
57
  #callback;
55
58
  #watchCfg;
56
59
  #latestRelistWindow = "";
60
+ #useHTTP2 = false;
57
61
  // Track the last time data was received
58
62
  #lastSeenTime = NONE;
59
63
  #lastSeenLimit;
@@ -75,6 +79,8 @@ class Watcher {
75
79
  #resourceVersion;
76
80
  // Track the list of items in the cache
77
81
  #cache = new Map();
82
+ // Token Path
83
+ #TOKEN_PATH = "/var/run/secrets/kubernetes.io/serviceaccount/token";
78
84
  /**
79
85
  * Setup a Kubernetes watcher for the specified model and filters. The callback function will be called for each event received.
80
86
  * The watch can be aborted by calling {@link Watcher.close} or by calling abort() on the AbortController returned by {@link Watcher.start}.
@@ -98,6 +104,8 @@ class Watcher {
98
104
  this.#lastSeenLimit = watchCfg.lastSeenLimitSeconds * 1000;
99
105
  // Set the latest relist interval to now
100
106
  this.#latestRelistWindow = new Date().toISOString();
107
+ // Set the latest relist interval to now
108
+ this.#useHTTP2 = watchCfg.useHTTP2 ?? false;
101
109
  // Add random jitter to the relist/resync intervals (up to 1 second)
102
110
  const jitter = Math.floor(Math.random() * 1000);
103
111
  // Check every relist interval for cache staleness
@@ -123,7 +131,12 @@ class Watcher {
123
131
  */
124
132
  async start() {
125
133
  this.#events.emit(WatchEvent.INIT_CACHE_MISS, this.#latestRelistWindow);
126
- await this.#watch();
134
+ if (this.#useHTTP2) {
135
+ await this.#http2Watch();
136
+ }
137
+ else {
138
+ await this.#watch();
139
+ }
127
140
  return this.#abortController;
128
141
  }
129
142
  /** Close the watch. Also available on the AbortController returned by {@link Watcher.start}. */
@@ -158,6 +171,19 @@ class Watcher {
158
171
  get events() {
159
172
  return this.#events;
160
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
+ }
161
187
  /**
162
188
  * Build the URL and request options for the watch.
163
189
  *
@@ -285,6 +311,32 @@ class Watcher {
285
311
  this.#events.emit(WatchEvent.DATA_ERROR, err);
286
312
  }
287
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
+ };
288
340
  /**
289
341
  * Watch for changes to the resource.
290
342
  */
@@ -313,31 +365,7 @@ class Watcher {
313
365
  this.#events.emit(WatchEvent.INC_RESYNC_FAILURE_COUNT, this.#resyncFailureCount);
314
366
  // Listen for events and call the callback function
315
367
  this.#stream.on("data", async (line) => {
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 this.#process(payload, phase);
330
- }
331
- catch (err) {
332
- if (err.name === "TooOld") {
333
- // Prevent any body events from firing
334
- body.removeAllListeners();
335
- // Reload the watch
336
- void this.#errHandler(err);
337
- return;
338
- }
339
- this.#events.emit(WatchEvent.DATA_ERROR, err);
340
- }
368
+ await this.#processLine(line, this.#process);
341
369
  });
342
370
  // Bind the body events
343
371
  body.on("error", this.#errHandler);
@@ -354,6 +382,93 @@ class Watcher {
354
382
  void this.#errHandler(e);
355
383
  }
356
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
+ this.#streamCleanup();
450
+ client.close();
451
+ });
452
+ req.on("close", () => {
453
+ this.#streamCleanup();
454
+ client.close();
455
+ });
456
+ req.on("error", err => {
457
+ void this.#errHandler(err);
458
+ client.close();
459
+ });
460
+ }
461
+ else {
462
+ const statusMessage = headers[":status-text"] || "Unknown";
463
+ throw new Error(`watch connect failed: ${statusCode} ${statusMessage}`);
464
+ }
465
+ });
466
+ req.end();
467
+ }
468
+ catch (e) {
469
+ void this.#errHandler(e);
470
+ }
471
+ };
357
472
  /** Clear the resync timer and schedule a new one. */
358
473
  #checkResync = () => {
359
474
  // Ignore if the last seen time is not set
@@ -379,7 +494,9 @@ class Watcher {
379
494
  this.#pendingReconnect = true;
380
495
  this.#events.emit(WatchEvent.RECONNECT, this.#resyncFailureCount);
381
496
  this.#streamCleanup();
382
- void this.#watch();
497
+ if (!this.#useHTTP2) {
498
+ void this.#watch();
499
+ }
383
500
  }
384
501
  }
385
502
  else {
@@ -420,6 +537,9 @@ class Watcher {
420
537
  this.#stream.removeAllListeners();
421
538
  this.#stream.destroy();
422
539
  }
540
+ if (this.#useHTTP2) {
541
+ void this.#http2Watch();
542
+ }
423
543
  };
424
544
  }
425
545
  exports.Watcher = Watcher;
@@ -36,16 +36,20 @@ describe("End-to-End CLI tests with multiple test files", () => {
36
36
  const expectedDir = path.join(__dirname, `crds/${name}.default.expected`); // Expected default directory
37
37
  const expectedPostDir = path.join(__dirname, `crds/${name}.no.post.expected`); // Expected post-processing directory
38
38
 
39
- console.log(`Running tests for ${name}`);
40
- console.log(`Test file: ${mockYamlPath}`);
41
- console.log(`Output directory: ${mockDir}`);
42
- console.log(`Expected directory: ${expectedDir}`);
43
- console.log(`Expected post-processing directory: ${expectedPostDir}`);
39
+ const testInfoMessage = `
40
+ Running tests for ${name}
41
+ Test file: ${mockYamlPath}
42
+ Output directory: ${mockDir}
43
+ Expected directory: ${expectedDir}
44
+ Expected post-processing directory: ${expectedPostDir}
45
+ `;
46
+
47
+ console.log(testInfoMessage);
44
48
 
45
49
  beforeEach(() => {
46
50
  // Ensure the output directory is clean
47
51
  if (fs.existsSync(mockDir)) {
48
- fs.rmdirSync(mockDir, { recursive: true });
52
+ fs.rmSync(mockDir, { recursive: true });
49
53
  }
50
54
 
51
55
  // Recreate the output directory
@@ -55,7 +59,7 @@ describe("End-to-End CLI tests with multiple test files", () => {
55
59
  afterEach(() => {
56
60
  // Cleanup the output directory after each test
57
61
  if (fs.existsSync(mockDir)) {
58
- fs.rmdirSync(mockDir, { recursive: true });
62
+ fs.rmSync(mockDir, { recursive: true });
59
63
  }
60
64
  });
61
65
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "kubernetes-fluent-client",
3
- "version": "3.0.4",
3
+ "version": "4.0.0-rc-http2-watch",
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",
@@ -53,9 +53,9 @@
53
53
  "@types/readable-stream": "4.0.15",
54
54
  "@types/urijs": "^1.19.25",
55
55
  "@types/yargs": "17.0.33",
56
- "@typescript-eslint/eslint-plugin": "8.7.0",
57
- "@typescript-eslint/parser": "8.7.0",
58
- "eslint-plugin-jsdoc": "50.3.0",
56
+ "@typescript-eslint/eslint-plugin": "8.8.1",
57
+ "@typescript-eslint/parser": "8.8.1",
58
+ "eslint-plugin-jsdoc": "50.3.1",
59
59
  "jest": "29.7.0",
60
60
  "nock": "13.5.5",
61
61
  "prettier": "3.3.3",
@@ -4,12 +4,14 @@
4
4
  import byline from "byline";
5
5
  import { createHash } from "crypto";
6
6
  import { EventEmitter } from "events";
7
+ import https from "https";
8
+ import http2 from "http2";
7
9
  import fetch from "node-fetch";
8
-
9
10
  import { fetch as wrappedFetch } from "../fetch";
10
11
  import { GenericClass, KubernetesListObject } from "../types";
11
12
  import { Filters, WatchAction, WatchPhase } from "./types";
12
13
  import { k8sCfg, pathBuilder } from "./utils";
14
+ import fs from "fs";
13
15
 
14
16
  export enum WatchEvent {
15
17
  /** Watch is connected successfully */
@@ -52,6 +54,8 @@ export type WatchCfg = {
52
54
  relistIntervalSec?: number;
53
55
  /** Max amount of seconds to go without receiving an event before reconciliation starts. Defaults to 300 (5 minutes). */
54
56
  lastSeenLimitSeconds?: number;
57
+ /** Use http2 for the Watch */
58
+ useHTTP2?: boolean;
55
59
  };
56
60
 
57
61
  const NONE = 50;
@@ -65,6 +69,7 @@ export class Watcher<T extends GenericClass> {
65
69
  #callback: WatchAction<T>;
66
70
  #watchCfg: WatchCfg;
67
71
  #latestRelistWindow: string = "";
72
+ #useHTTP2: boolean = false;
68
73
 
69
74
  // Track the last time data was received
70
75
  #lastSeenTime = NONE;
@@ -97,6 +102,8 @@ export class Watcher<T extends GenericClass> {
97
102
  // Track the list of items in the cache
98
103
  #cache = new Map<string, InstanceType<T>>();
99
104
 
105
+ // Token Path
106
+ #TOKEN_PATH = "/var/run/secrets/kubernetes.io/serviceaccount/token";
100
107
  /**
101
108
  * Setup a Kubernetes watcher for the specified model and filters. The callback function will be called for each event received.
102
109
  * The watch can be aborted by calling {@link Watcher.close} or by calling abort() on the AbortController returned by {@link Watcher.start}.
@@ -125,6 +132,9 @@ export class Watcher<T extends GenericClass> {
125
132
  // Set the latest relist interval to now
126
133
  this.#latestRelistWindow = new Date().toISOString();
127
134
 
135
+ // Set the latest relist interval to now
136
+ this.#useHTTP2 = watchCfg.useHTTP2 ?? false;
137
+
128
138
  // Add random jitter to the relist/resync intervals (up to 1 second)
129
139
  const jitter = Math.floor(Math.random() * 1000);
130
140
 
@@ -158,7 +168,11 @@ export class Watcher<T extends GenericClass> {
158
168
  */
159
169
  public async start(): Promise<AbortController> {
160
170
  this.#events.emit(WatchEvent.INIT_CACHE_MISS, this.#latestRelistWindow);
161
- await this.#watch();
171
+ if (this.#useHTTP2) {
172
+ await this.#http2Watch();
173
+ } else {
174
+ await this.#watch();
175
+ }
162
176
  return this.#abortController;
163
177
  }
164
178
 
@@ -198,6 +212,19 @@ export class Watcher<T extends GenericClass> {
198
212
  return this.#events;
199
213
  }
200
214
 
215
+ /**
216
+ * Read the serviceAccount Token
217
+ *
218
+ * @returns token or null
219
+ */
220
+ async #getToken() {
221
+ try {
222
+ return (await fs.promises.readFile(this.#TOKEN_PATH, "utf8")).trim();
223
+ } catch {
224
+ return null;
225
+ }
226
+ }
227
+
201
228
  /**
202
229
  * Build the URL and request options for the watch.
203
230
  *
@@ -351,6 +378,42 @@ export class Watcher<T extends GenericClass> {
351
378
  }
352
379
  };
353
380
 
381
+ // process a line from the chunk
382
+ #processLine = async (
383
+ line: string,
384
+ process: (payload: InstanceType<T>, phase: WatchPhase) => Promise<void>,
385
+ ) => {
386
+ try {
387
+ // Parse the event payload
388
+ const { object: payload, type: phase } = JSON.parse(line) as {
389
+ type: WatchPhase;
390
+ object: InstanceType<T>;
391
+ };
392
+
393
+ // Update the last seen time
394
+ this.#lastSeenTime = Date.now();
395
+
396
+ // If the watch is too old, remove the resourceVersion and reload the watch
397
+ if (phase === WatchPhase.Error && payload.code === 410) {
398
+ throw {
399
+ name: "TooOld",
400
+ message: this.#resourceVersion!,
401
+ };
402
+ }
403
+
404
+ // Process the event payload, do not update the resource version as that is handled by the list operation
405
+ await process(payload, phase);
406
+ } catch (err) {
407
+ if (err.name === "TooOld") {
408
+ // Reload the watch
409
+ void this.#errHandler(err);
410
+ return;
411
+ }
412
+
413
+ this.#events.emit(WatchEvent.DATA_ERROR, err);
414
+ }
415
+ };
416
+
354
417
  /**
355
418
  * Watch for changes to the resource.
356
419
  */
@@ -388,38 +451,7 @@ export class Watcher<T extends GenericClass> {
388
451
 
389
452
  // Listen for events and call the callback function
390
453
  this.#stream.on("data", async line => {
391
- try {
392
- // Parse the event payload
393
- const { object: payload, type: phase } = JSON.parse(line) as {
394
- type: WatchPhase;
395
- object: InstanceType<T>;
396
- };
397
-
398
- // Update the last seen time
399
- this.#lastSeenTime = Date.now();
400
-
401
- // If the watch is too old, remove the resourceVersion and reload the watch
402
- if (phase === WatchPhase.Error && payload.code === 410) {
403
- throw {
404
- name: "TooOld",
405
- message: this.#resourceVersion!,
406
- };
407
- }
408
-
409
- // Process the event payload, do not update the resource version as that is handled by the list operation
410
- await this.#process(payload, phase);
411
- } catch (err) {
412
- if (err.name === "TooOld") {
413
- // Prevent any body events from firing
414
- body.removeAllListeners();
415
-
416
- // Reload the watch
417
- void this.#errHandler(err);
418
- return;
419
- }
420
-
421
- this.#events.emit(WatchEvent.DATA_ERROR, err);
422
- }
454
+ await this.#processLine(line, this.#process);
423
455
  });
424
456
 
425
457
  // Bind the body events
@@ -437,6 +469,108 @@ export class Watcher<T extends GenericClass> {
437
469
  }
438
470
  };
439
471
 
472
+ /**
473
+ * Watch for changes to the resource.
474
+ */
475
+ #http2Watch = async () => {
476
+ try {
477
+ // Start with a list operation
478
+ await this.#list();
479
+
480
+ // Build the URL and request options
481
+ const { opts, url } = await this.#buildURL(true, this.#resourceVersion);
482
+ let agentOptions;
483
+
484
+ if (opts.agent && opts.agent instanceof https.Agent) {
485
+ agentOptions = {
486
+ key: opts.agent.options.key,
487
+ cert: opts.agent.options.cert,
488
+ ca: opts.agent.options.ca,
489
+ rejectUnauthorized: false,
490
+ };
491
+ }
492
+
493
+ // HTTP/2 client connection setup
494
+ const client = http2.connect(url.origin, {
495
+ ca: agentOptions?.ca,
496
+ cert: agentOptions?.cert,
497
+ key: agentOptions?.key,
498
+ rejectUnauthorized: agentOptions?.rejectUnauthorized,
499
+ });
500
+
501
+ // Set up headers for the HTTP/2 request
502
+ const token = await this.#getToken();
503
+ const headers: Record<string, string> = {
504
+ ":method": "GET",
505
+ ":path": url.pathname + url.search,
506
+ "content-type": "application/json",
507
+ "user-agent": "kubernetes-fluent-client",
508
+ };
509
+
510
+ if (token) {
511
+ headers["Authorization"] = `Bearer ${token}`;
512
+ }
513
+
514
+ // Make the HTTP/2 request
515
+ const req = client.request(headers);
516
+
517
+ req.setEncoding("utf8");
518
+
519
+ let buffer = "";
520
+
521
+ // Handle response data
522
+ req.on("response", headers => {
523
+ const statusCode = headers[":status"];
524
+
525
+ if (statusCode && statusCode >= 200 && statusCode < 300) {
526
+ this.#pendingReconnect = false;
527
+ this.#events.emit(WatchEvent.CONNECT, url.pathname);
528
+
529
+ // Reset the retry count
530
+ this.#resyncFailureCount = 0;
531
+ this.#events.emit(WatchEvent.INC_RESYNC_FAILURE_COUNT, this.#resyncFailureCount);
532
+
533
+ req.on("data", async chunk => {
534
+ try {
535
+ buffer += chunk;
536
+ const lines = buffer.split("\n");
537
+ // Avoid Watch event data_error received. Unexpected end of JSON input.
538
+ buffer = lines.pop()!;
539
+
540
+ for (const line of lines) {
541
+ await this.#processLine(line, this.#process);
542
+ }
543
+ } catch (err) {
544
+ void this.#errHandler(err);
545
+ }
546
+ });
547
+
548
+ req.on("end", () => {
549
+ this.#streamCleanup();
550
+ client.close();
551
+ });
552
+
553
+ req.on("close", () => {
554
+ this.#streamCleanup();
555
+ client.close();
556
+ });
557
+
558
+ req.on("error", err => {
559
+ void this.#errHandler(err);
560
+ client.close();
561
+ });
562
+ } else {
563
+ const statusMessage = headers[":status-text"] || "Unknown";
564
+ throw new Error(`watch connect failed: ${statusCode} ${statusMessage}`);
565
+ }
566
+ });
567
+
568
+ req.end();
569
+ } catch (e) {
570
+ void this.#errHandler(e);
571
+ }
572
+ };
573
+
440
574
  /** Clear the resync timer and schedule a new one. */
441
575
  #checkResync = () => {
442
576
  // Ignore if the last seen time is not set
@@ -468,7 +602,9 @@ export class Watcher<T extends GenericClass> {
468
602
  this.#events.emit(WatchEvent.RECONNECT, this.#resyncFailureCount);
469
603
  this.#streamCleanup();
470
604
 
471
- void this.#watch();
605
+ if (!this.#useHTTP2) {
606
+ void this.#watch();
607
+ }
472
608
  }
473
609
  } else {
474
610
  // Otherwise, call the finally function if it exists
@@ -516,5 +652,8 @@ export class Watcher<T extends GenericClass> {
516
652
  this.#stream.removeAllListeners();
517
653
  this.#stream.destroy();
518
654
  }
655
+ if (this.#useHTTP2) {
656
+ void this.#http2Watch();
657
+ }
519
658
  };
520
659
  }