kubernetes-fluent-client 1.5.0 → 1.6.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.
Files changed (49) hide show
  1. package/dist/cli.d.ts +3 -0
  2. package/dist/cli.d.ts.map +1 -0
  3. package/dist/cli.js +39 -0
  4. package/dist/fetch.d.ts +1 -1
  5. package/dist/fetch.js +2 -2
  6. package/dist/fetch.test.js +1 -1
  7. package/dist/fluent/apply.js +1 -1
  8. package/dist/fluent/index.d.ts +1 -0
  9. package/dist/fluent/index.d.ts.map +1 -1
  10. package/dist/fluent/index.js +45 -3
  11. package/dist/fluent/types.d.ts +33 -21
  12. package/dist/fluent/types.d.ts.map +1 -1
  13. package/dist/fluent/types.js +1 -1
  14. package/dist/fluent/utils.d.ts +18 -7
  15. package/dist/fluent/utils.d.ts.map +1 -1
  16. package/dist/fluent/utils.js +19 -8
  17. package/dist/fluent/utils.test.js +1 -1
  18. package/dist/fluent/watch.d.ts +9 -2
  19. package/dist/fluent/watch.d.ts.map +1 -1
  20. package/dist/fluent/watch.js +19 -2
  21. package/dist/generate.d.ts +19 -0
  22. package/dist/generate.d.ts.map +1 -0
  23. package/dist/generate.js +165 -0
  24. package/dist/generate.test.d.ts +2 -0
  25. package/dist/generate.test.d.ts.map +1 -0
  26. package/dist/generate.test.js +256 -0
  27. package/dist/index.d.ts +1 -0
  28. package/dist/index.d.ts.map +1 -1
  29. package/dist/index.js +4 -1
  30. package/dist/kinds.d.ts +6 -0
  31. package/dist/kinds.d.ts.map +1 -1
  32. package/dist/kinds.js +53 -0
  33. package/dist/types.d.ts +1 -1
  34. package/dist/types.d.ts.map +1 -1
  35. package/package.json +13 -7
  36. package/src/cli.ts +44 -0
  37. package/src/fetch.test.ts +1 -1
  38. package/src/fetch.ts +2 -2
  39. package/src/fluent/apply.ts +1 -1
  40. package/src/fluent/index.ts +46 -4
  41. package/src/fluent/types.ts +34 -22
  42. package/src/fluent/utils.test.ts +1 -1
  43. package/src/fluent/utils.ts +19 -8
  44. package/src/fluent/watch.ts +22 -4
  45. package/src/generate.test.ts +266 -0
  46. package/src/generate.ts +189 -0
  47. package/src/index.ts +3 -0
  48. package/src/kinds.ts +53 -0
  49. package/src/types.ts +1 -1
@@ -1,5 +1,5 @@
1
1
  // SPDX-License-Identifier: Apache-2.0
2
- // SPDX-FileCopyrightText: 2023-Present The Pepr Authors
2
+ // SPDX-FileCopyrightText: 2023-Present The Kubernetes Fluent Client Authors
3
3
 
4
4
  import { KubernetesListObject, KubernetesObject } from "@kubernetes/client-node";
5
5
  import { Operation } from "fast-json-patch";
@@ -28,6 +28,14 @@ export interface Filters {
28
28
  namespace?: string;
29
29
  }
30
30
 
31
+ /**
32
+ * Get the resource or resources matching the filters.
33
+ * If no filters are specified, all resources will be returned.
34
+ * If a name is specified, only a single resource will be returned.
35
+ *
36
+ * @param name - (optional) the name of the resource to get
37
+ * @returns the resource or list of resources
38
+ */
31
39
  export type GetFunction<K extends KubernetesObject> = {
32
40
  (): Promise<KubernetesListObject<K>>;
33
41
  (name: string): Promise<K>;
@@ -42,16 +50,18 @@ export type K8sFilteredActions<K extends KubernetesObject> = {
42
50
  Get: GetFunction<K>;
43
51
 
44
52
  /**
45
- * Delete the resource if it exists.
53
+ * Delete the resource matching the filters.
46
54
  *
47
55
  * @param filter - the resource or resource name to delete
48
56
  */
49
57
  Delete: (filter?: K | string) => Promise<void>;
50
58
 
51
59
  /**
60
+ * Watch the resource matching the filters.
52
61
  *
53
- * @param callback
54
- * @returns
62
+ * @param callback - the callback function to call when an event occurs
63
+ * @param watchCfg - (optional) watch configuration
64
+ * @returns a watch controller
55
65
  */
56
66
  Watch: (
57
67
  callback: (payload: K, phase: WatchPhase) => void,
@@ -63,16 +73,17 @@ export type K8sUnfilteredActions<K extends KubernetesObject> = {
63
73
  /**
64
74
  * Perform a server-side apply of the provided K8s resource.
65
75
  *
66
- * @param resource
67
- * @returns
76
+ * @param resource - the resource to apply
77
+ * @param applyCfg - (optional) apply configuration
78
+ * @returns the applied resource
68
79
  */
69
80
  Apply: (resource: PartialDeep<K>, applyCfg?: ApplyCfg) => Promise<K>;
70
81
 
71
82
  /**
72
83
  * Create the provided K8s resource or throw an error if it already exists.
73
84
  *
74
- * @param resource
75
- * @returns
85
+ * @param resource - the resource to create
86
+ * @returns the created resource
76
87
  */
77
88
  Create: (resource: K) => Promise<K>;
78
89
 
@@ -94,18 +105,18 @@ export type K8sWithFilters<K extends KubernetesObject> = K8sFilteredActions<K> &
94
105
  *
95
106
  * ```ts
96
107
  * K8s(kind.Deployment)
97
- * .WithField("metadata.name", "bar")
98
- * .WithField("metadata.namespace", "qux")
99
- * .Delete(...)
108
+ * .WithField("metadata.name", "bar")
109
+ * .WithField("metadata.namespace", "qux")
110
+ * .Delete(...)
100
111
  * ```
101
112
  *
102
113
  * Will only delete the Deployment if it has the `metadata.name=bar` and `metadata.namespace=qux` fields.
103
114
  * Not all fields are supported, see https://kubernetes.io/docs/concepts/overview/working-with-objects/field-selectors/#supported-fields,
104
115
  * but Typescript will limit to only fields that exist on the resource.
105
116
  *
106
- * @param key The field key
107
- * @param value The field value
108
- * @returns
117
+ * @param key - the field key
118
+ * @param value - the field value
119
+ * @returns the fluent API
109
120
  */
110
121
  WithField: <P extends Paths<K>>(key: P, value: string) => K8sWithFilters<K>;
111
122
 
@@ -115,15 +126,16 @@ export type K8sWithFilters<K extends KubernetesObject> = K8sFilteredActions<K> &
115
126
  *
116
127
  * ```ts
117
128
  * K8s(kind.Deployment)
118
- * .WithLabel("foo", "bar")
119
- * .WithLabel("baz", "qux")
120
- * .Delete(...)
129
+ * .WithLabel("foo", "bar")
130
+ * .WithLabel("baz", "qux")
131
+ * .Delete(...)
121
132
  * ```
122
133
  *
123
134
  * Will only delete the Deployment if it has the`foo=bar` and `baz=qux` labels.
124
135
  *
125
- * @param key The label key
126
- * @param value (optional) The label value
136
+ * @param key - the label key
137
+ * @param value - the label value
138
+ * @returns the fluent API
127
139
  */
128
140
  WithLabel: (key: string, value: string) => K8sWithFilters<K>;
129
141
  };
@@ -131,10 +143,10 @@ export type K8sWithFilters<K extends KubernetesObject> = K8sFilteredActions<K> &
131
143
  export type K8sInit<K extends KubernetesObject> = K8sWithFilters<K> &
132
144
  K8sUnfilteredActions<K> & {
133
145
  /**
134
- * Filter the query by the given namespace.
146
+ * Set the namespace filter.
135
147
  *
136
- * @param namespace
137
- * @returns
148
+ * @param namespace - the namespace to filter on
149
+ * @returns the fluent API
138
150
  */
139
151
  InNamespace: (namespace: string) => K8sWithFilters<K>;
140
152
  };
@@ -1,5 +1,5 @@
1
1
  // SPDX-License-Identifier: Apache-2.0
2
- // SPDX-FileCopyrightText: 2023-Present The Pepr Authors
2
+ // SPDX-FileCopyrightText: 2023-Present The Kubernetes Fluent Client Authors
3
3
 
4
4
  import { beforeEach, describe, expect, it, jest } from "@jest/globals";
5
5
 
@@ -1,5 +1,5 @@
1
1
  // SPDX-License-Identifier: Apache-2.0
2
- // SPDX-FileCopyrightText: 2023-Present The Pepr Authors
2
+ // SPDX-FileCopyrightText: 2023-Present The Kubernetes Fluent Client Authors
3
3
 
4
4
  import { KubeConfig, PatchStrategy } from "@kubernetes/client-node";
5
5
 
@@ -16,11 +16,11 @@ const SSA_CONTENT_TYPE = "application/apply-patch+yaml";
16
16
  /**
17
17
  * Generate a path to a Kubernetes resource
18
18
  *
19
- * @param serverUrl
20
- * @param model
21
- * @param filters
22
- * @param excludeName
23
- * @returns
19
+ * @param serverUrl - the URL of the Kubernetes API server
20
+ * @param model - the model to use for the API
21
+ * @param filters - (optional) filter overrides, can also be chained
22
+ * @param excludeName - (optional) exclude the name from the path
23
+ * @returns the path to the resource
24
24
  */
25
25
  export function pathBuilder<T extends GenericClass>(
26
26
  serverUrl: string,
@@ -90,8 +90,8 @@ export function pathBuilder<T extends GenericClass>(
90
90
  * - We have to create an agent to handle the TLS connection (for the custom CA + mTLS in some cases)
91
91
  * - The K8s lib uses request instead of node-fetch today so the object is slightly different
92
92
  *
93
- * @param method
94
- * @returns
93
+ * @param method - the HTTP method to use
94
+ * @returns the fetch options and server URL
95
95
  */
96
96
  export async function k8sCfg(method: FetchMethods) {
97
97
  const kubeConfig = new KubeConfig();
@@ -119,6 +119,17 @@ export async function k8sCfg(method: FetchMethods) {
119
119
  return { opts, serverUrl: cluster.server };
120
120
  }
121
121
 
122
+ /**
123
+ * Execute a request against the Kubernetes API server.
124
+ *
125
+ * @param model - the model to use for the API
126
+ * @param filters - (optional) filter overrides, can also be chained
127
+ * @param method - the HTTP method to use
128
+ * @param payload - (optional) the payload to send
129
+ * @param applyCfg - (optional) configuration for the apply method
130
+ *
131
+ * @returns the parsed JSON response
132
+ */
122
133
  export async function k8sExec<T extends GenericClass, K>(
123
134
  model: T,
124
135
  filters: Filters,
@@ -1,5 +1,5 @@
1
1
  // SPDX-License-Identifier: Apache-2.0
2
- // SPDX-FileCopyrightText: 2023-Present The Pepr Authors
2
+ // SPDX-FileCopyrightText: 2023-Present The Kubernetes Fluent Client Authors
3
3
 
4
4
  import byline from "byline";
5
5
  import fetch from "node-fetch";
@@ -14,13 +14,14 @@ import { k8sCfg, pathBuilder } from "./utils";
14
14
  export type WatchController = {
15
15
  /**
16
16
  * Abort the watch.
17
+ *
17
18
  * @param reason optional reason for aborting the watch
18
- * @returns
19
19
  */
20
20
  abort: (reason?: string) => void;
21
21
  /**
22
22
  * Get the AbortSignal for the watch.
23
- * @returns
23
+ *
24
+ * @returns the AbortSignal
24
25
  */
25
26
  signal: () => AbortSignal;
26
27
  };
@@ -49,6 +50,12 @@ export type WatchCfg = {
49
50
 
50
51
  /**
51
52
  * Execute a watch on the specified resource.
53
+ *
54
+ * @param model - the model to use for the API
55
+ * @param filters - (optional) filter overrides, can also be chained
56
+ * @param callback - the callback function to call when an event is received
57
+ * @param watchCfg - (optional) watch configuration
58
+ * @returns a WatchController to allow the watch to be aborted externally
52
59
  */
53
60
  export async function ExecWatch<T extends GenericClass>(
54
61
  model: T,
@@ -94,6 +101,9 @@ export async function ExecWatch<T extends GenericClass>(
94
101
  // Create a wrapped AbortController to allow the watch to be aborted externally
95
102
  const abortWrapper = {} as WatchController;
96
103
 
104
+ /**
105
+ * Bind the abort controller to the wrapper.
106
+ */
97
107
  function bindAbortController() {
98
108
  // Create a new AbortController
99
109
  abortController = new AbortController();
@@ -106,6 +116,9 @@ export async function ExecWatch<T extends GenericClass>(
106
116
  opts.signal = abortController.signal;
107
117
  }
108
118
 
119
+ /**
120
+ * The main watch runner. This will run until the process is terminated or the watch is aborted.
121
+ */
109
122
  async function runner() {
110
123
  let doneCalled = false;
111
124
 
@@ -130,6 +143,7 @@ export async function ExecWatch<T extends GenericClass>(
130
143
  }
131
144
  };
132
145
 
146
+ // Cleanup the stream listeners
133
147
  const cleanup = () => {
134
148
  if (!doneCalled) {
135
149
  doneCalled = true;
@@ -181,7 +195,11 @@ export async function ExecWatch<T extends GenericClass>(
181
195
  onError(e);
182
196
  }
183
197
 
184
- // On unhandled errors, retry the watch
198
+ /**
199
+ * Reload the watch.
200
+ *
201
+ * @param e - the error that caused the reload
202
+ */
185
203
  async function reload(e: Error) {
186
204
  // If there are more attempts, retry the watch
187
205
  if (watchCfg.retryMax! > retryCount) {
@@ -0,0 +1,266 @@
1
+ import { beforeEach, describe, expect, jest, test } from "@jest/globals";
2
+ import { generate } from "./generate";
3
+ import fs from "fs";
4
+
5
+ const sampleYaml = `
6
+ # non-crd should be ignored
7
+ apiVersion: v1
8
+ kind: ConfigMap
9
+ metadata:
10
+ name: test
11
+ namespace: default
12
+ data:
13
+ any: bleh
14
+ ---
15
+ apiVersion: apiextensions.k8s.io/v1
16
+ kind: CustomResourceDefinition
17
+ metadata:
18
+ name: movies.example.com
19
+ spec:
20
+ group: example.com
21
+ names:
22
+ kind: Movie
23
+ plural: movies
24
+ scope: Namespaced
25
+ versions:
26
+ - name: v1
27
+ schema:
28
+ openAPIV3Schema:
29
+ type: object
30
+ description: Movie nerd
31
+ properties:
32
+ spec:
33
+ properties:
34
+ title:
35
+ type: string
36
+ author:
37
+ type: string
38
+ type: object
39
+ ---
40
+ # duplicate entries should not break things
41
+ apiVersion: apiextensions.k8s.io/v1
42
+ kind: CustomResourceDefinition
43
+ metadata:
44
+ name: movies.example.com
45
+ spec:
46
+ group: example.com
47
+ names:
48
+ kind: Movie
49
+ plural: movies
50
+ scope: Namespaced
51
+ versions:
52
+ - name: v1
53
+ schema:
54
+ openAPIV3Schema:
55
+ type: object
56
+ description: Movie nerd
57
+ properties:
58
+ spec:
59
+ properties:
60
+ title:
61
+ type: string
62
+ author:
63
+ type: string
64
+ type: object
65
+ ---
66
+ # should support multiple versions
67
+ apiVersion: apiextensions.k8s.io/v1
68
+ kind: CustomResourceDefinition
69
+ metadata:
70
+ name: books.example.com
71
+ spec:
72
+ group: example.com
73
+ names:
74
+ kind: Book
75
+ plural: books
76
+ scope: Namespaced
77
+ versions:
78
+ - name: v1
79
+ schema:
80
+ openAPIV3Schema:
81
+ type: object
82
+ description: Book nerd
83
+ properties:
84
+ spec:
85
+ properties:
86
+ title:
87
+ type: string
88
+ author:
89
+ type: string
90
+ type: object
91
+ - name: v2
92
+ schema:
93
+ openAPIV3Schema:
94
+ type: object
95
+ description: Book nerd
96
+ properties:
97
+ spec:
98
+ properties:
99
+ author:
100
+ type: string
101
+ type: object
102
+ served: true
103
+ storage: true
104
+ `;
105
+
106
+ jest.mock("./fetch", () => ({
107
+ fetch: jest.fn(),
108
+ }));
109
+
110
+ jest.mock("./fluent", () => ({
111
+ K8s: jest.fn(),
112
+ }));
113
+
114
+ describe("CRD to TypeScript Conversion", () => {
115
+ const originalReadFileSync = fs.readFileSync;
116
+
117
+ jest.isolateModules(() => {
118
+ jest.spyOn(fs, "existsSync").mockReturnValue(true);
119
+ jest.spyOn(fs, "readFileSync").mockImplementation((...args) => {
120
+ // Super janky hack due ot source-map-support calling readFileSync internally
121
+ if (args[0].toString().includes("test-crd.yaml")) {
122
+ return sampleYaml;
123
+ }
124
+ return originalReadFileSync(...args);
125
+ });
126
+ jest.spyOn(fs, "mkdirSync").mockReturnValue(undefined);
127
+ jest.spyOn(fs, "writeFileSync").mockReturnValue(undefined);
128
+ });
129
+
130
+ beforeEach(() => {
131
+ jest.clearAllMocks();
132
+ });
133
+
134
+ test("converts CRD to TypeScript", async () => {
135
+ const options = { source: "test-crd.yaml", language: "ts" }; // specify your options
136
+
137
+ const actual = await generate(options);
138
+ const expectedMovie = [
139
+ "// This file is auto-generated by kubernetes-fluent-client, do not edit manually\n",
140
+ 'import { GenericKind, RegisterKind } from "kubernetes-fluent-client";\n',
141
+ "/**",
142
+ " * Movie nerd",
143
+ " */",
144
+ "export class Movie extends GenericKind {",
145
+ " spec?: Spec;",
146
+ "}",
147
+ "",
148
+ "export interface Spec {",
149
+ " author?: string;",
150
+ " title?: string;",
151
+ "}",
152
+ "",
153
+ "RegisterKind(Movie, {",
154
+ ' group: "example.com",',
155
+ ' version: "v1",',
156
+ ' kind: "Movie",',
157
+ "});",
158
+ ];
159
+ const expectedBookV1 = [
160
+ "// This file is auto-generated by kubernetes-fluent-client, do not edit manually\n",
161
+ 'import { GenericKind, RegisterKind } from "kubernetes-fluent-client";\n',
162
+ "/**",
163
+ " * Book nerd",
164
+ " */",
165
+ "export class Book extends GenericKind {",
166
+ " spec?: Spec;",
167
+ "}",
168
+ "",
169
+ "export interface Spec {",
170
+ " author?: string;",
171
+ " title?: string;",
172
+ "}",
173
+ "",
174
+ "RegisterKind(Book, {",
175
+ ' group: "example.com",',
176
+ ' version: "v1",',
177
+ ' kind: "Book",',
178
+ "});",
179
+ ];
180
+ const expectedBookV2 = expectedBookV1
181
+ .filter(line => !line.includes("title?"))
182
+ .map(line => line.replace("v1", "v2"));
183
+
184
+ expect(actual["movie-v1"]).toEqual(expectedMovie);
185
+ expect(actual["book-v1"]).toEqual(expectedBookV1);
186
+ expect(actual["book-v2"]).toEqual(expectedBookV2);
187
+ });
188
+
189
+ test("converts CRD to TypeScript with plain option", async () => {
190
+ const options = { source: "test-crd.yaml", language: "ts", plain: true }; // specify your options
191
+
192
+ const actual = await generate(options);
193
+ const expectedMovie = [
194
+ "/**",
195
+ " * Movie nerd",
196
+ " */",
197
+ "export interface Movie {",
198
+ " spec?: Spec;",
199
+ "}",
200
+ "",
201
+ "export interface Spec {",
202
+ " author?: string;",
203
+ " title?: string;",
204
+ "}",
205
+ "",
206
+ ];
207
+ const expectedBookV1 = [
208
+ "/**",
209
+ " * Book nerd",
210
+ " */",
211
+ "export interface Book {",
212
+ " spec?: Spec;",
213
+ "}",
214
+ "",
215
+ "export interface Spec {",
216
+ " author?: string;",
217
+ " title?: string;",
218
+ "}",
219
+ "",
220
+ ];
221
+ const expectedBookV2 = expectedBookV1
222
+ .filter(line => !line.includes("title?"))
223
+ .map(line => line.replace("v1", "v2"));
224
+
225
+ expect(actual["movie-v1"]).toEqual(expectedMovie);
226
+ expect(actual["book-v1"]).toEqual(expectedBookV1);
227
+ expect(actual["book-v2"]).toEqual(expectedBookV2);
228
+ });
229
+
230
+ test("converts CRD to Go", async () => {
231
+ const options = { source: "test-crd.yaml", language: "go" }; // specify your options
232
+
233
+ const actual = await generate(options);
234
+ const expectedMovie = [
235
+ "// Movie nerd",
236
+ "type Movie struct {",
237
+ '\tSpec *Spec `json:"spec,omitempty"`',
238
+ "}",
239
+ "",
240
+ "type Spec struct {",
241
+ '\tAuthor *string `json:"author,omitempty"`',
242
+ '\tTitle *string `json:"title,omitempty"`',
243
+ "}",
244
+ "",
245
+ ];
246
+ const expectedBookV1 = [
247
+ "// Book nerd",
248
+ "type Book struct {",
249
+ '\tSpec *Spec `json:"spec,omitempty"`',
250
+ "}",
251
+ "",
252
+ "type Spec struct {",
253
+ '\tAuthor *string `json:"author,omitempty"`',
254
+ '\tTitle *string `json:"title,omitempty"`',
255
+ "}",
256
+ "",
257
+ ];
258
+ const expectedBookV2 = expectedBookV1
259
+ .filter(line => !line.includes("Title"))
260
+ .map(line => line.replace("v1", "v2"));
261
+
262
+ expect(actual["movie-v1"]).toEqual(expectedMovie);
263
+ expect(actual["book-v1"]).toEqual(expectedBookV1);
264
+ expect(actual["book-v2"]).toEqual(expectedBookV2);
265
+ });
266
+ });