kubernetes-fluent-client 2.0.0 → 2.1.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/README.md CHANGED
@@ -20,13 +20,15 @@ async function demo() {
20
20
  // Now, we can use the fluent API to query for the resources we just created
21
21
 
22
22
  // You can use watch to monitor resources in the cluster and react to changes
23
- // This will run until the process is terminated or the watch is aborted
24
- const ctrl = await K8s(kind.Pod).Watch((pod, phase) => {
23
+ const watcher = K8s(kind.Pod).Watch((pod, phase) => {
25
24
  console.log(`Pod ${pod.metadata?.name} is ${phase}`);
26
25
  });
27
26
 
27
+ // This will run until the process is terminated or the watch is aborted
28
+ await watcher.start();
29
+
28
30
  // Let's abort the watch after 5 seconds
29
- setTimeout(ctrl.abort, 5 * 1000);
31
+ setTimeout(watcher.close, 5 * 1000);
30
32
 
31
33
  // Passing the name to Get() will return a single resource
32
34
  const ns = await K8s(kind.Namespace).Get(namespace);
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/fluent/index.ts"],"names":[],"mappings":"AAGA,OAAO,EAAwB,gBAAgB,EAAE,MAAM,yBAAyB,CAAC;AAOjF,OAAO,EAAE,YAAY,EAAE,MAAM,UAAU,CAAC;AAExC,OAAO,EAAE,OAAO,EAAE,OAAO,EAAsB,MAAM,SAAS,CAAC;AAI/D;;;;;;GAMG;AACH,wBAAgB,GAAG,CAAC,CAAC,SAAS,YAAY,EAAE,CAAC,SAAS,gBAAgB,GAAG,YAAY,CAAC,CAAC,CAAC,EACtF,KAAK,EAAE,CAAC,EACR,OAAO,GAAE,OAAY,GACpB,OAAO,CAAC,CAAC,EAAE,CAAC,CAAC,CAmKf"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/fluent/index.ts"],"names":[],"mappings":"AAGA,OAAO,EAAwB,gBAAgB,EAAE,MAAM,yBAAyB,CAAC;AAOjF,OAAO,EAAE,YAAY,EAAE,MAAM,UAAU,CAAC;AACxC,OAAO,EAA0B,OAAO,EAAE,OAAO,EAAsB,MAAM,SAAS,CAAC;AAIvF;;;;;;GAMG;AACH,wBAAgB,GAAG,CAAC,CAAC,SAAS,YAAY,EAAE,CAAC,SAAS,gBAAgB,GAAG,YAAY,CAAC,CAAC,CAAC,EACtF,KAAK,EAAE,CAAC,EACR,OAAO,GAAE,OAAY,GACpB,OAAO,CAAC,CAAC,EAAE,CAAC,CAAC,CA4Kf"}
@@ -131,6 +131,14 @@ function K8s(model, filters = {}) {
131
131
  }
132
132
  return (0, utils_1.k8sExec)(model, filters, "PATCH", payload);
133
133
  }
134
+ /**
135
+ * @inheritdoc
136
+ * @see {@link K8sInit.PatchStatus}
137
+ */
138
+ async function PatchStatus(resource) {
139
+ syncFilters(resource);
140
+ return (0, utils_1.k8sExec)(model, filters, "PATCH_STATUS", resource);
141
+ }
134
142
  /**
135
143
  * @inheritdoc
136
144
  * @see {@link K8sInit.Watch}
@@ -142,8 +150,8 @@ function K8s(model, filters = {}) {
142
150
  * @inheritdoc
143
151
  * @see {@link K8sInit.Raw}
144
152
  */
145
- async function Raw(url) {
146
- const thing = await (0, utils_1.k8sCfg)("GET");
153
+ async function Raw(url, method = "GET") {
154
+ const thing = await (0, utils_1.k8sCfg)(method);
147
155
  const { opts, serverUrl } = thing;
148
156
  const resp = await (0, fetch_1.fetch)(`${serverUrl}${url}`, opts);
149
157
  if (resp.ok) {
@@ -151,6 +159,6 @@ function K8s(model, filters = {}) {
151
159
  }
152
160
  throw resp;
153
161
  }
154
- return { InNamespace, Apply, Create, Patch, Raw, ...withFilters };
162
+ return { InNamespace, Apply, Create, Patch, PatchStatus, Raw, ...withFilters };
155
163
  }
156
164
  exports.K8s = K8s;
@@ -81,6 +81,35 @@ const generateFakePodManagedFields = (manager) => {
81
81
  (0, globals_1.expect)(result).toEqual(fakeResource);
82
82
  (0, globals_1.expect)(mockedKubeExec).toHaveBeenCalledWith(upstream_1.Pod, {}, "PATCH", patchOperations);
83
83
  });
84
+ (0, globals_1.it)("should patch the status of a resource", async () => {
85
+ await (0, _1.K8s)(upstream_1.Pod).PatchStatus({
86
+ metadata: {
87
+ name: "fake",
88
+ namespace: "default",
89
+ managedFields: generateFakePodManagedFields("pepr"),
90
+ },
91
+ spec: { priority: 3 },
92
+ status: {
93
+ phase: "Ready",
94
+ },
95
+ });
96
+ (0, globals_1.expect)(utils_1.k8sExec).toBeCalledWith(upstream_1.Pod, globals_1.expect.objectContaining({
97
+ name: "fake",
98
+ namespace: "default",
99
+ }), "PATCH_STATUS", {
100
+ apiVersion: "v1",
101
+ kind: "Pod",
102
+ metadata: {
103
+ name: "fake",
104
+ namespace: "default",
105
+ managedFields: generateFakePodManagedFields("pepr"),
106
+ },
107
+ spec: { priority: 3 },
108
+ status: {
109
+ phase: "Ready",
110
+ },
111
+ });
112
+ });
84
113
  (0, globals_1.it)("should filter with WithField", async () => {
85
114
  await (0, _1.K8s)(upstream_1.Pod).WithField("metadata.name", "fake").Get();
86
115
  (0, globals_1.expect)(mockedKubeExec).toHaveBeenCalledWith(upstream_1.Pod, globals_1.expect.objectContaining({
@@ -2,7 +2,6 @@ import { KubernetesListObject, KubernetesObject } from "@kubernetes/client-node"
2
2
  import { Operation } from "fast-json-patch";
3
3
  import type { PartialDeep } from "type-fest";
4
4
  import { GenericClass, GroupVersionKind } from "../types";
5
- import { ApplyCfg } from "./apply";
6
5
  import { WatchCfg, Watcher } from "./watch";
7
6
  /**
8
7
  * The Phase matched when using the K8s Watch API.
@@ -14,7 +13,7 @@ export declare enum WatchPhase {
14
13
  Bookmark = "BOOKMARK",
15
14
  Error = "ERROR"
16
15
  }
17
- export type FetchMethods = "GET" | "APPLY" | "POST" | "PUT" | "DELETE" | "PATCH" | "WATCH";
16
+ export type FetchMethods = "GET" | "APPLY" | "POST" | "PUT" | "DELETE" | "PATCH" | "WATCH" | "PATCH_STATUS";
18
17
  export interface Filters {
19
18
  kindOverride?: GroupVersionKind;
20
19
  fields?: Record<string, string>;
@@ -81,6 +80,19 @@ export type K8sUnfilteredActions<K extends KubernetesObject> = {
81
80
  * @returns The patched resource
82
81
  */
83
82
  Patch: (payload: Operation[]) => Promise<K>;
83
+ /**
84
+ * Patch the status of the provided K8s resource. Note this is a special case of the Patch method that
85
+ * only allows patching the status subresource. This can be used in Operator reconciliation loops to
86
+ * update the status of a resource without triggering a new Generation of the resource.
87
+ *
88
+ * See https://stackoverflow.com/q/47100389/467373 for more details.
89
+ *
90
+ * IMPORTANT: This method will throw a 404 error if the resource does not have a status subresource defined.
91
+ *
92
+ * @param resource - the resource to patch
93
+ * @returns the patched resource
94
+ */
95
+ PatchStatus: (resource: PartialDeep<K>) => Promise<K>;
84
96
  /**
85
97
  * Perform a raw GET request to the Kubernetes API. This is useful for calling endpoints that are not supported by the fluent API.
86
98
  * This command mirrors the `kubectl get --raw` command.
@@ -98,7 +110,7 @@ export type K8sUnfilteredActions<K extends KubernetesObject> = {
98
110
  * @param url the URL to call (e.g. /api)
99
111
  * @returns
100
112
  */
101
- Raw: (url: string) => Promise<K>;
113
+ Raw: (url: string, method?: FetchMethods) => Promise<K>;
102
114
  };
103
115
  export type K8sWithFilters<T extends GenericClass, K extends KubernetesObject> = K8sFilteredActions<T, K> & {
104
116
  /**
@@ -141,6 +153,15 @@ export type K8sWithFilters<T extends GenericClass, K extends KubernetesObject> =
141
153
  */
142
154
  WithLabel: (key: string, value?: string) => K8sWithFilters<T, K>;
143
155
  };
156
+ /**
157
+ * Configuration for the apply function.
158
+ */
159
+ export type ApplyCfg = {
160
+ /**
161
+ * Force the apply to be a create.
162
+ */
163
+ force?: boolean;
164
+ };
144
165
  export type K8sInit<T extends GenericClass, K extends KubernetesObject> = K8sWithFilters<T, K> & K8sUnfilteredActions<K> & {
145
166
  /**
146
167
  * Set the namespace filter.
@@ -1 +1 @@
1
- {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../../src/fluent/types.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../../src/fluent/types.ts"],"names":[],"mappings":"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"}
@@ -1,8 +1,7 @@
1
1
  /// <reference types="node" />
2
2
  import { URL } from "url";
3
3
  import { GenericClass } from "../types";
4
- import { FetchMethods, Filters } from "./types";
5
- import { ApplyCfg } from "./apply";
4
+ import { ApplyCfg, FetchMethods, Filters } from "./types";
6
5
  /**
7
6
  * Generate a path to a Kubernetes resource
8
7
  *
@@ -1 +1 @@
1
- {"version":3,"file":"utils.d.ts","sourceRoot":"","sources":["../../src/fluent/utils.ts"],"names":[],"mappings":";AAMA,OAAO,EAAE,GAAG,EAAE,MAAM,KAAK,CAAC;AAG1B,OAAO,EAAE,YAAY,EAAE,MAAM,UAAU,CAAC;AACxC,OAAO,EAAE,YAAY,EAAE,OAAO,EAAE,MAAM,SAAS,CAAC;AAChD,OAAO,EAAE,QAAQ,EAAE,MAAM,SAAS,CAAC;AAInC;;;;;;;;GAQG;AACH,wBAAgB,WAAW,CAAC,CAAC,SAAS,YAAY,EAChD,SAAS,EAAE,MAAM,EACjB,KAAK,EAAE,CAAC,EACR,OAAO,EAAE,OAAO,EAChB,WAAW,UAAQ,OAwDpB;AAED;;;;;;;;;;GAUG;AACH,wBAAsB,MAAM,CAAC,MAAM,EAAE,YAAY;;;GAwBhD;AAED;;;;;;;;;;GAUG;AACH,wBAAsB,OAAO,CAAC,CAAC,SAAS,YAAY,EAAE,CAAC,EACrD,KAAK,EAAE,CAAC,EACR,OAAO,EAAE,OAAO,EAChB,MAAM,EAAE,YAAY,EACpB,OAAO,CAAC,EAAE,CAAC,GAAG,OAAO,EACrB,QAAQ,GAAE,QAA2B,cA8BtC"}
1
+ {"version":3,"file":"utils.d.ts","sourceRoot":"","sources":["../../src/fluent/utils.ts"],"names":[],"mappings":";AAKA,OAAO,EAAE,GAAG,EAAE,MAAM,KAAK,CAAC;AAI1B,OAAO,EAAE,YAAY,EAAE,MAAM,UAAU,CAAC;AACxC,OAAO,EAAE,QAAQ,EAAE,YAAY,EAAE,OAAO,EAAE,MAAM,SAAS,CAAC;AAI1D;;;;;;;;GAQG;AACH,wBAAgB,WAAW,CAAC,CAAC,SAAS,YAAY,EAChD,SAAS,EAAE,MAAM,EACjB,KAAK,EAAE,CAAC,EACR,OAAO,EAAE,OAAO,EAChB,WAAW,UAAQ,OAwDpB;AAED;;;;;;;;;;GAUG;AACH,wBAAsB,MAAM,CAAC,MAAM,EAAE,YAAY;;;GAwBhD;AAED;;;;;;;;;;GAUG;AACH,wBAAsB,OAAO,CAAC,CAAC,SAAS,YAAY,EAAE,CAAC,EACrD,KAAK,EAAE,CAAC,EACR,OAAO,EAAE,OAAO,EAChB,MAAM,EAAE,YAAY,EACpB,OAAO,CAAC,EAAE,CAAC,GAAG,OAAO,EACrB,QAAQ,GAAE,QAA2B,cA2CtC"}
@@ -108,6 +108,13 @@ async function k8sExec(model, filters, method, payload, applyCfg = { force: fals
108
108
  const { opts, serverUrl } = await k8sCfg(method);
109
109
  const url = pathBuilder(serverUrl, model, filters, method === "POST");
110
110
  switch (opts.method) {
111
+ // PATCH_STATUS is a special case that uses the PATCH method on status subresources
112
+ case "PATCH_STATUS":
113
+ opts.method = "PATCH";
114
+ url.pathname = `${url.pathname}/status`;
115
+ opts.headers.set("Content-Type", client_node_1.PatchStrategy.MergePatch);
116
+ payload = { status: payload.status };
117
+ break;
111
118
  case "PATCH":
112
119
  opts.headers.set("Content-Type", client_node_1.PatchStrategy.JsonPatch);
113
120
  break;
@@ -126,6 +133,10 @@ async function k8sExec(model, filters, method, payload, applyCfg = { force: fals
126
133
  if (resp.ok) {
127
134
  return resp.data;
128
135
  }
136
+ if (resp.status === 404 && method === "PATCH_STATUS") {
137
+ resp.statusText =
138
+ "Not Found" + " (NOTE: This error is expected if the resource has no status subresource)";
139
+ }
129
140
  throw resp;
130
141
  }
131
142
  exports.k8sExec = k8sExec;
@@ -3,10 +3,11 @@
3
3
  // SPDX-FileCopyrightText: 2023-Present The Kubernetes Fluent Client Authors
4
4
  Object.defineProperty(exports, "__esModule", { value: true });
5
5
  const globals_1 = require("@jest/globals");
6
+ const node_fetch_1 = require("node-fetch");
6
7
  const fetch_1 = require("../fetch");
8
+ const kinds_1 = require("../kinds");
7
9
  const upstream_1 = require("../upstream");
8
10
  const utils_1 = require("./utils");
9
- const kinds_1 = require("../kinds");
10
11
  globals_1.jest.mock("https");
11
12
  globals_1.jest.mock("../fetch");
12
13
  (0, globals_1.describe)("pathBuilder Function", () => {
@@ -92,14 +93,18 @@ globals_1.jest.mock("../fetch");
92
93
  const mockedFetch = globals_1.jest.mocked(fetch_1.fetch);
93
94
  const fakeFilters = { name: "fake", namespace: "default" };
94
95
  const fakeMethod = "GET";
95
- const fakePayload = { metadata: { name: "fake", namespace: "default" } };
96
+ const fakePayload = {
97
+ metadata: { name: "fake", namespace: "default" },
98
+ status: { phase: "Ready" },
99
+ };
96
100
  const fakeUrl = new URL("http://jest-test:8080/api/v1/namespaces/default/pods/fake");
97
101
  const fakeOpts = {
98
102
  body: JSON.stringify(fakePayload),
99
- headers: {
103
+ compress: true,
104
+ headers: new node_fetch_1.Headers({
100
105
  "Content-Type": "application/json",
101
106
  "User-Agent": `kubernetes-fluent-client`,
102
- },
107
+ }),
103
108
  method: fakeMethod,
104
109
  };
105
110
  (0, globals_1.beforeEach)(() => {
@@ -116,6 +121,83 @@ globals_1.jest.mock("../fetch");
116
121
  (0, globals_1.expect)(result).toEqual(fakePayload);
117
122
  (0, globals_1.expect)(mockedFetch).toHaveBeenCalledWith(fakeUrl, globals_1.expect.objectContaining(fakeOpts));
118
123
  });
124
+ (0, globals_1.it)("should handle PATCH_STATUS", async () => {
125
+ mockedFetch.mockResolvedValueOnce({
126
+ ok: true,
127
+ data: fakePayload,
128
+ status: 200,
129
+ statusText: "OK",
130
+ });
131
+ const result = await (0, utils_1.k8sExec)(upstream_1.Pod, fakeFilters, "PATCH_STATUS", fakePayload);
132
+ (0, globals_1.expect)(result).toEqual(fakePayload);
133
+ (0, globals_1.expect)(mockedFetch).toHaveBeenCalledWith(new URL("http://jest-test:8080/api/v1/namespaces/default/pods/fake/status"), globals_1.expect.objectContaining({
134
+ method: "PATCH",
135
+ compress: true,
136
+ headers: new node_fetch_1.Headers({
137
+ "Content-Type": "application/merge-patch+json",
138
+ "User-Agent": `kubernetes-fluent-client`,
139
+ }),
140
+ body: JSON.stringify({ status: fakePayload.status }),
141
+ }));
142
+ });
143
+ (0, globals_1.it)("should handle PATCH", async () => {
144
+ mockedFetch.mockResolvedValueOnce({
145
+ ok: true,
146
+ data: fakePayload,
147
+ status: 200,
148
+ statusText: "OK",
149
+ });
150
+ const patchPayload = [{ op: "replace", path: "/status/phase", value: "Ready" }];
151
+ const result = await (0, utils_1.k8sExec)(upstream_1.Pod, fakeFilters, "PATCH", patchPayload);
152
+ (0, globals_1.expect)(result).toEqual(fakePayload);
153
+ (0, globals_1.expect)(mockedFetch).toHaveBeenCalledWith(new URL("http://jest-test:8080/api/v1/namespaces/default/pods/fake"), globals_1.expect.objectContaining({
154
+ method: "PATCH",
155
+ compress: true,
156
+ headers: new node_fetch_1.Headers({
157
+ "Content-Type": "application/json-patch+json",
158
+ "User-Agent": `kubernetes-fluent-client`,
159
+ }),
160
+ body: JSON.stringify(patchPayload),
161
+ }));
162
+ });
163
+ (0, globals_1.it)("should handle APPLY", async () => {
164
+ mockedFetch.mockResolvedValueOnce({
165
+ ok: true,
166
+ data: fakePayload,
167
+ status: 200,
168
+ statusText: "OK",
169
+ });
170
+ const result = await (0, utils_1.k8sExec)(upstream_1.Pod, fakeFilters, "APPLY", fakePayload);
171
+ (0, globals_1.expect)(result).toEqual(fakePayload);
172
+ (0, globals_1.expect)(mockedFetch).toHaveBeenCalledWith(new URL("http://jest-test:8080/api/v1/namespaces/default/pods/fake?fieldManager=pepr&fieldValidation=Strict&force=false"), globals_1.expect.objectContaining({
173
+ method: "PATCH",
174
+ compress: true,
175
+ headers: new node_fetch_1.Headers({
176
+ "Content-Type": "application/apply-patch+yaml",
177
+ "User-Agent": `kubernetes-fluent-client`,
178
+ }),
179
+ body: JSON.stringify(fakePayload),
180
+ }));
181
+ });
182
+ (0, globals_1.it)("should handle APPLY with force", async () => {
183
+ mockedFetch.mockResolvedValueOnce({
184
+ ok: true,
185
+ data: fakePayload,
186
+ status: 200,
187
+ statusText: "OK",
188
+ });
189
+ const result = await (0, utils_1.k8sExec)(upstream_1.Pod, fakeFilters, "APPLY", fakePayload, { force: true });
190
+ (0, globals_1.expect)(result).toEqual(fakePayload);
191
+ (0, globals_1.expect)(mockedFetch).toHaveBeenCalledWith(new URL("http://jest-test:8080/api/v1/namespaces/default/pods/fake?fieldManager=pepr&fieldValidation=Strict&force=true"), globals_1.expect.objectContaining({
192
+ method: "PATCH",
193
+ compress: true,
194
+ headers: new node_fetch_1.Headers({
195
+ "Content-Type": "application/apply-patch+yaml",
196
+ "User-Agent": `kubernetes-fluent-client`,
197
+ }),
198
+ body: JSON.stringify(fakePayload),
199
+ }));
200
+ });
119
201
  (0, globals_1.it)("should handle fetch call failure", async () => {
120
202
  const fakeStatus = 404;
121
203
  const fakeStatusText = "Not Found";
@@ -68,9 +68,21 @@ export declare class Watcher<T extends GenericClass> {
68
68
  * Get a unique ID for the watch based on the model and filters.
69
69
  * This is useful for caching the watch data or resource versions.
70
70
  *
71
- * @returns the watch ID
71
+ * @returns the watch CacheID
72
72
  */
73
- get id(): string;
73
+ getCacheID(): string;
74
+ /**
75
+ * Get the current resource version.
76
+ *
77
+ * @returns the current resource version
78
+ */
79
+ get resourceVersion(): string | undefined;
80
+ /**
81
+ * Set the current resource version.
82
+ *
83
+ * @param resourceVersion - the new resource version
84
+ */
85
+ set resourceVersion(resourceVersion: string | undefined);
74
86
  /**
75
87
  * Subscribe to watch events. This is an EventEmitter that emits the following events:
76
88
  *
@@ -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;AAGtC,OAAO,EAAE,YAAY,EAAE,MAAM,UAAU,CAAC;AACxC,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,uBAAuB;IACvB,MAAM,WAAW;IACjB,mCAAmC;IACnC,IAAI,SAAS;IACb,2BAA2B;IAC3B,QAAQ,aAAa;IACrB,iCAAiC;IACjC,gBAAgB,qBAAqB;IACrC,wCAAwC;IACxC,oBAAoB,yBAAyB;IAC7C,qCAAqC;IACrC,iBAAiB,sBAAsB;CACxC;AAED,4CAA4C;AAC5C,MAAM,MAAM,QAAQ,GAAG;IACrB,sFAAsF;IACtF,eAAe,CAAC,EAAE,MAAM,CAAC;IACzB,+HAA+H;IAC/H,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,oEAAoE;IACpE,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB,0GAA0G;IAC1G,iBAAiB,CAAC,EAAE,MAAM,CAAC;CAC5B,CAAC;AAEF,iDAAiD;AACjD,qBAAa,OAAO,CAAC,CAAC,SAAS,YAAY;;IA6BzC;;;;;;;;;;;OAWG;gBACS,KAAK,EAAE,CAAC,EAAE,OAAO,EAAE,OAAO,EAAE,QAAQ,EAAE,WAAW,CAAC,CAAC,CAAC,EAAE,QAAQ,GAAE,QAAa;IAiBzF;;;;OAIG;IACU,KAAK,IAAI,OAAO,CAAC,eAAe,CAAC;IAK9C,gGAAgG;IACzF,KAAK;IAMZ;;;;;OAKG;IACH,IAAW,EAAE,WAYZ;IAED;;;;;;OAMG;IACH,IAAW,MAAM,IAAI,YAAY,CAEhC;CAyOF"}
1
+ {"version":3,"file":"watch.d.ts","sourceRoot":"","sources":["../../src/fluent/watch.ts"],"names":[],"mappings":";;AAKA,OAAO,EAAE,YAAY,EAAE,MAAM,QAAQ,CAAC;AAGtC,OAAO,EAAE,YAAY,EAAE,MAAM,UAAU,CAAC;AACxC,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,uBAAuB;IACvB,MAAM,WAAW;IACjB,mCAAmC;IACnC,IAAI,SAAS;IACb,2BAA2B;IAC3B,QAAQ,aAAa;IACrB,iCAAiC;IACjC,gBAAgB,qBAAqB;IACrC,wCAAwC;IACxC,oBAAoB,yBAAyB;IAC7C,qCAAqC;IACrC,iBAAiB,sBAAsB;CACxC;AAED,4CAA4C;AAC5C,MAAM,MAAM,QAAQ,GAAG;IACrB,sFAAsF;IACtF,eAAe,CAAC,EAAE,MAAM,CAAC;IACzB,+HAA+H;IAC/H,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,oEAAoE;IACpE,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB,0GAA0G;IAC1G,iBAAiB,CAAC,EAAE,MAAM,CAAC;CAC5B,CAAC;AAEF,iDAAiD;AACjD,qBAAa,OAAO,CAAC,CAAC,SAAS,YAAY;;IAyBzC;;;;;;;;;;;OAWG;gBACS,KAAK,EAAE,CAAC,EAAE,OAAO,EAAE,OAAO,EAAE,QAAQ,EAAE,WAAW,CAAC,CAAC,CAAC,EAAE,QAAQ,GAAE,QAAa;IAiBzF;;;;OAIG;IACU,KAAK,IAAI,OAAO,CAAC,eAAe,CAAC;IAK9C,gGAAgG;IACzF,KAAK;IAMZ;;;;;OAKG;IACI,UAAU;IAWjB;;;;OAIG;IACH,IAAW,eAAe,IASkB,MAAM,GAAG,SAAS,CAP7D;IAED;;;;OAIG;IACH,IAAW,eAAe,CAAC,eAAe,EAAE,MAAM,GAAG,SAAS,EAE7D;IAED;;;;;;OAMG;IACH,IAAW,MAAM,IAAI,YAAY,CAEhC;CAoOF"}
@@ -56,9 +56,6 @@ class Watcher {
56
56
  #events = new events_1.EventEmitter();
57
57
  // Create a timer to resync the watch
58
58
  #resyncTimer;
59
- // Unique ID for the watch
60
- #id;
61
- #hashedID;
62
59
  // Track if a reconnect is pending
63
60
  #pendingReconnect = false;
64
61
  /**
@@ -105,18 +102,32 @@ class Watcher {
105
102
  * Get a unique ID for the watch based on the model and filters.
106
103
  * This is useful for caching the watch data or resource versions.
107
104
  *
108
- * @returns the watch ID
105
+ * @returns the watch CacheID
109
106
  */
110
- get id() {
111
- // The ID must exist at this point
112
- if (!this.#id) {
113
- throw new Error("watch not started");
114
- }
107
+ getCacheID() {
108
+ // Build the URL, we don't care about the server URL or resourceVersion
109
+ const url = (0, utils_1.pathBuilder)("https://ignore", this.#model, this.#filters, false);
115
110
  // 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;
111
+ return (0, crypto_1.createHash)("sha224")
112
+ .update(url.pathname + url.search)
113
+ .digest("hex")
114
+ .substring(0, 10);
115
+ }
116
+ /**
117
+ * Get the current resource version.
118
+ *
119
+ * @returns the current resource version
120
+ */
121
+ get resourceVersion() {
122
+ return this.#watchCfg.resourceVersion;
123
+ }
124
+ /**
125
+ * Set the current resource version.
126
+ *
127
+ * @param resourceVersion - the new resource version
128
+ */
129
+ set resourceVersion(resourceVersion) {
130
+ this.#watchCfg.resourceVersion = resourceVersion;
120
131
  }
121
132
  /**
122
133
  * Subscribe to watch events. This is an EventEmitter that emits the following events:
@@ -137,10 +148,6 @@ class Watcher {
137
148
  // Build the path and query params for the resource, excluding the name
138
149
  const { opts, serverUrl } = await (0, utils_1.k8sCfg)("GET");
139
150
  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
151
  // Enable the watch query param
145
152
  url.searchParams.set("watch", "true");
146
153
  // If a name is specified, add it to the query params
@@ -109,17 +109,24 @@ const types_1 = require("./types");
109
109
  });
110
110
  watcher.start().catch(errMock);
111
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);
112
+ (0, globals_1.it)("should return the cache id", () => {
113
+ (0, globals_1.expect)(watcher.getCacheID()).toEqual("d69b75a611");
120
114
  });
121
- (0, globals_1.it)("should handle calling .id() before .start()", () => {
122
- (0, globals_1.expect)(() => watcher.id).toThrowError("watch not started");
115
+ (0, globals_1.it)("should use an updated resourceVersion", done => {
116
+ nock_1.default.cleanAll();
117
+ (0, nock_1.default)("http://jest-test:8080")
118
+ .get("/api/v1/pods")
119
+ .query({
120
+ watch: "true",
121
+ allowWatchBookmarks: "true",
122
+ resourceVersion: "35",
123
+ })
124
+ .reply(200);
125
+ // Update the resource version, could be combined with getCacheID to store the value
126
+ watcher.resourceVersion = "35";
127
+ setupAndStartWatcher(__1.WatchEvent.CONNECT, () => {
128
+ done();
129
+ });
123
130
  });
124
131
  (0, globals_1.it)("should handle the CONNECT event", done => {
125
132
  setupAndStartWatcher(__1.WatchEvent.CONNECT, () => {
@@ -150,6 +157,13 @@ const types_1 = require("./types");
150
157
  done();
151
158
  });
152
159
  });
160
+ (0, globals_1.it)("should handle the RESOURCE_VERSION event", done => {
161
+ setupAndStartWatcher(__1.WatchEvent.RESOURCE_VERSION, resourceVersion => {
162
+ (0, globals_1.expect)(watcher.resourceVersion).toEqual("2");
163
+ (0, globals_1.expect)(resourceVersion).toEqual("2");
164
+ done();
165
+ });
166
+ });
153
167
  (0, globals_1.it)("should handle the RECONNECT event", done => {
154
168
  nock_1.default.cleanAll();
155
169
  (0, nock_1.default)("http://jest-test:8080")
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "kubernetes-fluent-client",
3
- "version": "2.0.0",
3
+ "version": "2.1.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",
@@ -51,8 +51,8 @@
51
51
  "@types/byline": "4.2.36",
52
52
  "@types/readable-stream": "4.0.10",
53
53
  "@types/yargs": "17.0.32",
54
- "@typescript-eslint/eslint-plugin": "6.15.0",
55
- "@typescript-eslint/parser": "6.15.0",
54
+ "@typescript-eslint/eslint-plugin": "6.16.0",
55
+ "@typescript-eslint/parser": "6.16.0",
56
56
  "eslint-plugin-jsdoc": "46.9.1",
57
57
  "jest": "29.7.0",
58
58
  "nock": "13.4.0",
@@ -102,6 +102,42 @@ describe("Kube", () => {
102
102
  expect(mockedKubeExec).toHaveBeenCalledWith(Pod, {}, "PATCH", patchOperations);
103
103
  });
104
104
 
105
+ it("should patch the status of a resource", async () => {
106
+ await K8s(Pod).PatchStatus({
107
+ metadata: {
108
+ name: "fake",
109
+ namespace: "default",
110
+ managedFields: generateFakePodManagedFields("pepr"),
111
+ },
112
+ spec: { priority: 3 },
113
+ status: {
114
+ phase: "Ready",
115
+ },
116
+ });
117
+
118
+ expect(k8sExec).toBeCalledWith(
119
+ Pod,
120
+ expect.objectContaining({
121
+ name: "fake",
122
+ namespace: "default",
123
+ }),
124
+ "PATCH_STATUS",
125
+ {
126
+ apiVersion: "v1",
127
+ kind: "Pod",
128
+ metadata: {
129
+ name: "fake",
130
+ namespace: "default",
131
+ managedFields: generateFakePodManagedFields("pepr"),
132
+ },
133
+ spec: { priority: 3 },
134
+ status: {
135
+ phase: "Ready",
136
+ },
137
+ },
138
+ );
139
+ });
140
+
105
141
  it("should filter with WithField", async () => {
106
142
  await K8s(Pod).WithField("metadata.name", "fake").Get();
107
143
 
@@ -9,8 +9,7 @@ import type { PartialDeep } from "type-fest";
9
9
  import { fetch } from "../fetch";
10
10
  import { modelToGroupVersionKind } from "../kinds";
11
11
  import { GenericClass } from "../types";
12
- import { ApplyCfg } from "./apply";
13
- import { Filters, K8sInit, Paths, WatchAction } from "./types";
12
+ import { ApplyCfg, FetchMethods, Filters, K8sInit, Paths, WatchAction } from "./types";
14
13
  import { k8sCfg, k8sExec } from "./utils";
15
14
  import { WatchCfg, Watcher } from "./watch";
16
15
 
@@ -162,6 +161,15 @@ export function K8s<T extends GenericClass, K extends KubernetesObject = Instanc
162
161
  return k8sExec(model, filters, "PATCH", payload);
163
162
  }
164
163
 
164
+ /**
165
+ * @inheritdoc
166
+ * @see {@link K8sInit.PatchStatus}
167
+ */
168
+ async function PatchStatus(resource: PartialDeep<K>): Promise<K> {
169
+ syncFilters(resource as K);
170
+ return k8sExec(model, filters, "PATCH_STATUS", resource);
171
+ }
172
+
165
173
  /**
166
174
  * @inheritdoc
167
175
  * @see {@link K8sInit.Watch}
@@ -174,8 +182,8 @@ export function K8s<T extends GenericClass, K extends KubernetesObject = Instanc
174
182
  * @inheritdoc
175
183
  * @see {@link K8sInit.Raw}
176
184
  */
177
- async function Raw(url: string) {
178
- const thing = await k8sCfg("GET");
185
+ async function Raw(url: string, method: FetchMethods = "GET") {
186
+ const thing = await k8sCfg(method);
179
187
  const { opts, serverUrl } = thing;
180
188
  const resp = await fetch<K>(`${serverUrl}${url}`, opts);
181
189
 
@@ -186,5 +194,5 @@ export function K8s<T extends GenericClass, K extends KubernetesObject = Instanc
186
194
  throw resp;
187
195
  }
188
196
 
189
- return { InNamespace, Apply, Create, Patch, Raw, ...withFilters };
197
+ return { InNamespace, Apply, Create, Patch, PatchStatus, Raw, ...withFilters };
190
198
  }
@@ -6,7 +6,6 @@ import { Operation } from "fast-json-patch";
6
6
  import type { PartialDeep } from "type-fest";
7
7
 
8
8
  import { GenericClass, GroupVersionKind } from "../types";
9
- import { ApplyCfg } from "./apply";
10
9
  import { WatchCfg, Watcher } from "./watch";
11
10
 
12
11
  /**
@@ -20,7 +19,15 @@ export enum WatchPhase {
20
19
  Error = "ERROR",
21
20
  }
22
21
 
23
- export type FetchMethods = "GET" | "APPLY" | "POST" | "PUT" | "DELETE" | "PATCH" | "WATCH";
22
+ export type FetchMethods =
23
+ | "GET"
24
+ | "APPLY"
25
+ | "POST"
26
+ | "PUT"
27
+ | "DELETE"
28
+ | "PATCH"
29
+ | "WATCH"
30
+ | "PATCH_STATUS";
24
31
 
25
32
  export interface Filters {
26
33
  kindOverride?: GroupVersionKind;
@@ -96,6 +103,20 @@ export type K8sUnfilteredActions<K extends KubernetesObject> = {
96
103
  */
97
104
  Patch: (payload: Operation[]) => Promise<K>;
98
105
 
106
+ /**
107
+ * Patch the status of the provided K8s resource. Note this is a special case of the Patch method that
108
+ * only allows patching the status subresource. This can be used in Operator reconciliation loops to
109
+ * update the status of a resource without triggering a new Generation of the resource.
110
+ *
111
+ * See https://stackoverflow.com/q/47100389/467373 for more details.
112
+ *
113
+ * IMPORTANT: This method will throw a 404 error if the resource does not have a status subresource defined.
114
+ *
115
+ * @param resource - the resource to patch
116
+ * @returns the patched resource
117
+ */
118
+ PatchStatus: (resource: PartialDeep<K>) => Promise<K>;
119
+
99
120
  /**
100
121
  * Perform a raw GET request to the Kubernetes API. This is useful for calling endpoints that are not supported by the fluent API.
101
122
  * This command mirrors the `kubectl get --raw` command.
@@ -113,7 +134,7 @@ export type K8sUnfilteredActions<K extends KubernetesObject> = {
113
134
  * @param url the URL to call (e.g. /api)
114
135
  * @returns
115
136
  */
116
- Raw: (url: string) => Promise<K>;
137
+ Raw: (url: string, method?: FetchMethods) => Promise<K>;
117
138
  };
118
139
 
119
140
  export type K8sWithFilters<T extends GenericClass, K extends KubernetesObject> = K8sFilteredActions<
@@ -162,6 +183,16 @@ export type K8sWithFilters<T extends GenericClass, K extends KubernetesObject> =
162
183
  WithLabel: (key: string, value?: string) => K8sWithFilters<T, K>;
163
184
  };
164
185
 
186
+ /**
187
+ * Configuration for the apply function.
188
+ */
189
+ export type ApplyCfg = {
190
+ /**
191
+ * Force the apply to be a create.
192
+ */
193
+ force?: boolean;
194
+ };
195
+
165
196
  export type K8sInit<T extends GenericClass, K extends KubernetesObject> = K8sWithFilters<T, K> &
166
197
  K8sUnfilteredActions<K> & {
167
198
  /**
@@ -2,13 +2,14 @@
2
2
  // SPDX-FileCopyrightText: 2023-Present The Kubernetes Fluent Client Authors
3
3
 
4
4
  import { beforeEach, describe, expect, it, jest } from "@jest/globals";
5
+ import { Headers } from "node-fetch";
5
6
 
6
7
  import { fetch } from "../fetch";
8
+ import { RegisterKind } from "../kinds";
7
9
  import { GenericClass } from "../types";
8
10
  import { ClusterRole, Ingress, Pod } from "../upstream";
9
11
  import { Filters } from "./types";
10
12
  import { k8sExec, pathBuilder } from "./utils";
11
- import { RegisterKind } from "../kinds";
12
13
 
13
14
  jest.mock("https");
14
15
  jest.mock("../fetch");
@@ -115,14 +116,18 @@ describe("kubeExec Function", () => {
115
116
 
116
117
  const fakeFilters: Filters = { name: "fake", namespace: "default" };
117
118
  const fakeMethod = "GET";
118
- const fakePayload = { metadata: { name: "fake", namespace: "default" } };
119
+ const fakePayload = {
120
+ metadata: { name: "fake", namespace: "default" },
121
+ status: { phase: "Ready" },
122
+ };
119
123
  const fakeUrl = new URL("http://jest-test:8080/api/v1/namespaces/default/pods/fake");
120
124
  const fakeOpts = {
121
125
  body: JSON.stringify(fakePayload),
122
- headers: {
126
+ compress: true,
127
+ headers: new Headers({
123
128
  "Content-Type": "application/json",
124
129
  "User-Agent": `kubernetes-fluent-client`,
125
- },
130
+ }),
126
131
  method: fakeMethod,
127
132
  };
128
133
 
@@ -144,6 +149,112 @@ describe("kubeExec Function", () => {
144
149
  expect(mockedFetch).toHaveBeenCalledWith(fakeUrl, expect.objectContaining(fakeOpts));
145
150
  });
146
151
 
152
+ it("should handle PATCH_STATUS", async () => {
153
+ mockedFetch.mockResolvedValueOnce({
154
+ ok: true,
155
+ data: fakePayload,
156
+ status: 200,
157
+ statusText: "OK",
158
+ });
159
+
160
+ const result = await k8sExec(Pod, fakeFilters, "PATCH_STATUS", fakePayload);
161
+
162
+ expect(result).toEqual(fakePayload);
163
+ expect(mockedFetch).toHaveBeenCalledWith(
164
+ new URL("http://jest-test:8080/api/v1/namespaces/default/pods/fake/status"),
165
+ expect.objectContaining({
166
+ method: "PATCH",
167
+ compress: true,
168
+ headers: new Headers({
169
+ "Content-Type": "application/merge-patch+json",
170
+ "User-Agent": `kubernetes-fluent-client`,
171
+ }),
172
+ body: JSON.stringify({ status: fakePayload.status }),
173
+ }),
174
+ );
175
+ });
176
+
177
+ it("should handle PATCH", async () => {
178
+ mockedFetch.mockResolvedValueOnce({
179
+ ok: true,
180
+ data: fakePayload,
181
+ status: 200,
182
+ statusText: "OK",
183
+ });
184
+
185
+ const patchPayload = [{ op: "replace", path: "/status/phase", value: "Ready" }];
186
+
187
+ const result = await k8sExec(Pod, fakeFilters, "PATCH", patchPayload);
188
+
189
+ expect(result).toEqual(fakePayload);
190
+ expect(mockedFetch).toHaveBeenCalledWith(
191
+ new URL("http://jest-test:8080/api/v1/namespaces/default/pods/fake"),
192
+ expect.objectContaining({
193
+ method: "PATCH",
194
+ compress: true,
195
+ headers: new Headers({
196
+ "Content-Type": "application/json-patch+json",
197
+ "User-Agent": `kubernetes-fluent-client`,
198
+ }),
199
+ body: JSON.stringify(patchPayload),
200
+ }),
201
+ );
202
+ });
203
+
204
+ it("should handle APPLY", async () => {
205
+ mockedFetch.mockResolvedValueOnce({
206
+ ok: true,
207
+ data: fakePayload,
208
+ status: 200,
209
+ statusText: "OK",
210
+ });
211
+
212
+ const result = await k8sExec(Pod, fakeFilters, "APPLY", fakePayload);
213
+
214
+ expect(result).toEqual(fakePayload);
215
+ expect(mockedFetch).toHaveBeenCalledWith(
216
+ new URL(
217
+ "http://jest-test:8080/api/v1/namespaces/default/pods/fake?fieldManager=pepr&fieldValidation=Strict&force=false",
218
+ ),
219
+ expect.objectContaining({
220
+ method: "PATCH",
221
+ compress: true,
222
+ headers: new Headers({
223
+ "Content-Type": "application/apply-patch+yaml",
224
+ "User-Agent": `kubernetes-fluent-client`,
225
+ }),
226
+ body: JSON.stringify(fakePayload),
227
+ }),
228
+ );
229
+ });
230
+
231
+ it("should handle APPLY with force", async () => {
232
+ mockedFetch.mockResolvedValueOnce({
233
+ ok: true,
234
+ data: fakePayload,
235
+ status: 200,
236
+ statusText: "OK",
237
+ });
238
+
239
+ const result = await k8sExec(Pod, fakeFilters, "APPLY", fakePayload, { force: true });
240
+
241
+ expect(result).toEqual(fakePayload);
242
+ expect(mockedFetch).toHaveBeenCalledWith(
243
+ new URL(
244
+ "http://jest-test:8080/api/v1/namespaces/default/pods/fake?fieldManager=pepr&fieldValidation=Strict&force=true",
245
+ ),
246
+ expect.objectContaining({
247
+ method: "PATCH",
248
+ compress: true,
249
+ headers: new Headers({
250
+ "Content-Type": "application/apply-patch+yaml",
251
+ "User-Agent": `kubernetes-fluent-client`,
252
+ }),
253
+ body: JSON.stringify(fakePayload),
254
+ }),
255
+ );
256
+ });
257
+
147
258
  it("should handle fetch call failure", async () => {
148
259
  const fakeStatus = 404;
149
260
  const fakeStatusText = "Not Found";
@@ -2,14 +2,13 @@
2
2
  // SPDX-FileCopyrightText: 2023-Present The Kubernetes Fluent Client Authors
3
3
 
4
4
  import { KubeConfig, PatchStrategy } from "@kubernetes/client-node";
5
-
6
5
  import { Headers } from "node-fetch";
7
6
  import { URL } from "url";
7
+
8
8
  import { fetch } from "../fetch";
9
9
  import { modelToGroupVersionKind } from "../kinds";
10
10
  import { GenericClass } from "../types";
11
- import { FetchMethods, Filters } from "./types";
12
- import { ApplyCfg } from "./apply";
11
+ import { ApplyCfg, FetchMethods, Filters } from "./types";
13
12
 
14
13
  const SSA_CONTENT_TYPE = "application/apply-patch+yaml";
15
14
 
@@ -143,6 +142,14 @@ export async function k8sExec<T extends GenericClass, K>(
143
142
  const url = pathBuilder(serverUrl, model, filters, method === "POST");
144
143
 
145
144
  switch (opts.method) {
145
+ // PATCH_STATUS is a special case that uses the PATCH method on status subresources
146
+ case "PATCH_STATUS":
147
+ opts.method = "PATCH";
148
+ url.pathname = `${url.pathname}/status`;
149
+ (opts.headers as Headers).set("Content-Type", PatchStrategy.MergePatch);
150
+ payload = { status: (payload as { status: unknown }).status };
151
+ break;
152
+
146
153
  case "PATCH":
147
154
  (opts.headers as Headers).set("Content-Type", PatchStrategy.JsonPatch);
148
155
  break;
@@ -166,5 +173,10 @@ export async function k8sExec<T extends GenericClass, K>(
166
173
  return resp.data;
167
174
  }
168
175
 
176
+ if (resp.status === 404 && method === "PATCH_STATUS") {
177
+ resp.statusText =
178
+ "Not Found" + " (NOTE: This error is expected if the resource has no status subresource)";
179
+ }
180
+
169
181
  throw resp;
170
182
  }
@@ -132,18 +132,27 @@ describe("Watcher", () => {
132
132
  watcher.start().catch(errMock);
133
133
  });
134
134
 
135
- it("should return the cache id", done => {
136
- watcher
137
- .start()
138
- .then(() => {
139
- expect(watcher.id).toEqual("d69b75a611");
140
- done();
141
- })
142
- .catch(errMock);
135
+ it("should return the cache id", () => {
136
+ expect(watcher.getCacheID()).toEqual("d69b75a611");
143
137
  });
144
138
 
145
- it("should handle calling .id() before .start()", () => {
146
- expect(() => watcher.id).toThrowError("watch not started");
139
+ it("should use an updated resourceVersion", done => {
140
+ nock.cleanAll();
141
+ nock("http://jest-test:8080")
142
+ .get("/api/v1/pods")
143
+ .query({
144
+ watch: "true",
145
+ allowWatchBookmarks: "true",
146
+ resourceVersion: "35",
147
+ })
148
+ .reply(200);
149
+
150
+ // Update the resource version, could be combined with getCacheID to store the value
151
+ watcher.resourceVersion = "35";
152
+
153
+ setupAndStartWatcher(WatchEvent.CONNECT, () => {
154
+ done();
155
+ });
147
156
  });
148
157
 
149
158
  it("should handle the CONNECT event", done => {
@@ -182,6 +191,14 @@ describe("Watcher", () => {
182
191
  });
183
192
  });
184
193
 
194
+ it("should handle the RESOURCE_VERSION event", done => {
195
+ setupAndStartWatcher(WatchEvent.RESOURCE_VERSION, resourceVersion => {
196
+ expect(watcher.resourceVersion).toEqual("2");
197
+ expect(resourceVersion).toEqual("2");
198
+ done();
199
+ });
200
+ });
201
+
185
202
  it("should handle the RECONNECT event", done => {
186
203
  nock.cleanAll();
187
204
  nock("http://jest-test:8080")
@@ -72,10 +72,6 @@ export class Watcher<T extends GenericClass> {
72
72
  // Create a timer to resync the watch
73
73
  #resyncTimer?: NodeJS.Timeout;
74
74
 
75
- // Unique ID for the watch
76
- #id?: string;
77
- #hashedID?: string;
78
-
79
75
  // Track if a reconnect is pending
80
76
  #pendingReconnect = false;
81
77
 
@@ -129,20 +125,35 @@ export class Watcher<T extends GenericClass> {
129
125
  * Get a unique ID for the watch based on the model and filters.
130
126
  * This is useful for caching the watch data or resource versions.
131
127
  *
132
- * @returns the watch ID
128
+ * @returns the watch CacheID
133
129
  */
134
- public get id() {
135
- // The ID must exist at this point
136
- if (!this.#id) {
137
- throw new Error("watch not started");
138
- }
130
+ public getCacheID() {
131
+ // Build the URL, we don't care about the server URL or resourceVersion
132
+ const url = pathBuilder("https://ignore", this.#model, this.#filters, false);
139
133
 
140
134
  // Hash and truncate the ID to 10 characters, cache the result
141
- if (!this.#hashedID) {
142
- this.#hashedID = createHash("sha224").update(this.#id).digest("hex").substring(0, 10);
143
- }
135
+ return createHash("sha224")
136
+ .update(url.pathname + url.search)
137
+ .digest("hex")
138
+ .substring(0, 10);
139
+ }
140
+
141
+ /**
142
+ * Get the current resource version.
143
+ *
144
+ * @returns the current resource version
145
+ */
146
+ public get resourceVersion() {
147
+ return this.#watchCfg.resourceVersion;
148
+ }
144
149
 
145
- return this.#hashedID;
150
+ /**
151
+ * Set the current resource version.
152
+ *
153
+ * @param resourceVersion - the new resource version
154
+ */
155
+ public set resourceVersion(resourceVersion: string | undefined) {
156
+ this.#watchCfg.resourceVersion = resourceVersion;
146
157
  }
147
158
 
148
159
  /**
@@ -166,11 +177,6 @@ export class Watcher<T extends GenericClass> {
166
177
  const { opts, serverUrl } = await k8sCfg("GET");
167
178
  const url = pathBuilder(serverUrl, this.#model, this.#filters, true);
168
179
 
169
- // Set the watch ID if it does not exist (this does not change on reconnect)
170
- if (!this.#id) {
171
- this.#id = url.pathname + url.search;
172
- }
173
-
174
180
  // Enable the watch query param
175
181
  url.searchParams.set("watch", "true");
176
182
 
@@ -1,10 +0,0 @@
1
- /**
2
- * Configuration for the apply function.
3
- */
4
- export type ApplyCfg = {
5
- /**
6
- * Force the apply to be a create.
7
- */
8
- force?: boolean;
9
- };
10
- //# sourceMappingURL=apply.d.ts.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"apply.d.ts","sourceRoot":"","sources":["../../src/fluent/apply.ts"],"names":[],"mappings":"AAGA;;GAEG;AACH,MAAM,MAAM,QAAQ,GAAG;IACrB;;OAEG;IACH,KAAK,CAAC,EAAE,OAAO,CAAC;CACjB,CAAC"}
@@ -1,4 +0,0 @@
1
- "use strict";
2
- // SPDX-License-Identifier: Apache-2.0
3
- // SPDX-FileCopyrightText: 2023-Present The Kubernetes Fluent Client Authors
4
- Object.defineProperty(exports, "__esModule", { value: true });
@@ -1,12 +0,0 @@
1
- // SPDX-License-Identifier: Apache-2.0
2
- // SPDX-FileCopyrightText: 2023-Present The Kubernetes Fluent Client Authors
3
-
4
- /**
5
- * Configuration for the apply function.
6
- */
7
- export type ApplyCfg = {
8
- /**
9
- * Force the apply to be a create.
10
- */
11
- force?: boolean;
12
- };