kubernetes-fluent-client 1.6.0 → 1.6.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/cli.js CHANGED
@@ -33,7 +33,14 @@ void (0, yargs_1.default)((0, helpers_1.hideBin)(process.argv))
33
33
  description: "the language to generate types in, see https://github.com/glideapps/quicktype#target-languages for a list of supported languages",
34
34
  })
35
35
  .demandOption(["source", "directory"]);
36
- }, argv => {
37
- void (0, generate_1.generate)(argv);
36
+ }, async (argv) => {
37
+ const opts = argv;
38
+ opts.logFn = console.log;
39
+ try {
40
+ await (0, generate_1.generate)(opts);
41
+ }
42
+ catch (e) {
43
+ console.log(`\n❌ ${e.message}`);
44
+ }
38
45
  })
39
46
  .parse();
@@ -1,4 +1,5 @@
1
1
  import { TargetLanguage } from "quicktype-core";
2
+ import { LogFn } from "./types";
2
3
  export interface GenerateOptions {
3
4
  /** The source URL, yaml file path or K8s CRD name */
4
5
  source: string;
@@ -8,6 +9,10 @@ export interface GenerateOptions {
8
9
  plain?: boolean;
9
10
  /** The language to generate types in */
10
11
  language?: string | TargetLanguage;
12
+ /** Override the NPM package to import when generating formatted Typescript */
13
+ npmPackage?: string;
14
+ /** Log function callback */
15
+ logFn: LogFn;
11
16
  }
12
17
  /**
13
18
  * Generate TypeScript types from a K8s CRD
@@ -1 +1 @@
1
- {"version":3,"file":"generate.d.ts","sourceRoot":"","sources":["../src/generate.ts"],"names":[],"mappings":"AAMA,OAAO,EAIL,cAAc,EAEf,MAAM,gBAAgB,CAAC;AAMxB,MAAM,WAAW,eAAe;IAC9B,qDAAqD;IACrD,MAAM,EAAE,MAAM,CAAC;IACf,gCAAgC;IAChC,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,gDAAgD;IAChD,KAAK,CAAC,EAAE,OAAO,CAAC;IAChB,wCAAwC;IACxC,QAAQ,CAAC,EAAE,MAAM,GAAG,cAAc,CAAC;CACpC;AAyID;;;;;GAKG;AACH,wBAAsB,QAAQ,CAAC,IAAI,EAAE,eAAe,qCAkBnD"}
1
+ {"version":3,"file":"generate.d.ts","sourceRoot":"","sources":["../src/generate.ts"],"names":[],"mappings":"AAMA,OAAO,EAIL,cAAc,EAEf,MAAM,gBAAgB,CAAC;AAKxB,OAAO,EAAE,KAAK,EAAE,MAAM,SAAS,CAAC;AAEhC,MAAM,WAAW,eAAe;IAC9B,qDAAqD;IACrD,MAAM,EAAE,MAAM,CAAC;IACf,gCAAgC;IAChC,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,gDAAgD;IAChD,KAAK,CAAC,EAAE,OAAO,CAAC;IAChB,wCAAwC;IACxC,QAAQ,CAAC,EAAE,MAAM,GAAG,cAAc,CAAC;IACnC,8EAA8E;IAC9E,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,4BAA4B;IAC5B,KAAK,EAAE,KAAK,CAAC;CACd;AAiKD;;;;;GAKG;AACH,wBAAsB,QAAQ,CAAC,IAAI,EAAE,eAAe,qCA4BnD"}
package/dist/generate.js CHANGED
@@ -45,19 +45,25 @@ async function convertCRDtoTS(crd, opts) {
45
45
  const name = crd.spec.names.kind;
46
46
  const results = {};
47
47
  for (const match of crd.spec.versions) {
48
+ const version = match.name;
48
49
  // Get the schema from the matched version
49
50
  const schema = JSON.stringify(match?.schema?.openAPIV3Schema);
50
51
  // Create a new JSONSchemaInput
51
52
  const schemaInput = new quicktype_core_1.JSONSchemaInput(new quicktype_core_1.FetchingJSONSchemaStore());
53
+ opts.logFn(`- Generating ${crd.spec.group}/${version} types for ${name}`);
52
54
  // Add the schema to the input
53
55
  await schemaInput.addSource({ name, schema });
54
56
  // Create a new InputData object
55
57
  const inputData = new quicktype_core_1.InputData();
56
58
  inputData.addInput(schemaInput);
59
+ // If the language is not specified, default to TypeScript
60
+ if (!opts.language) {
61
+ opts.language = "ts";
62
+ }
57
63
  // Generate the types
58
64
  const out = await (0, quicktype_core_1.quicktype)({
59
65
  inputData,
60
- lang: opts.language || "ts",
66
+ lang: opts.language,
61
67
  rendererOptions: { "just-types": "true" },
62
68
  });
63
69
  let processedLines = out.lines;
@@ -67,11 +73,14 @@ async function convertCRDtoTS(crd, opts) {
67
73
  }
68
74
  // If the language is TypeScript and plain is not specified, wire up the fluent client
69
75
  if (opts.language === "ts" && !opts.plain) {
76
+ if (!opts.npmPackage) {
77
+ opts.npmPackage = "kubernetes-fluent-client";
78
+ }
70
79
  processedLines.unshift(
71
80
  // Add warning that the file is auto-generated
72
- `// This file is auto-generated by kubernetes-fluent-client, do not edit manually\n`,
81
+ `// This file is auto-generated by ${opts.npmPackage}, do not edit manually\n`,
73
82
  // Add the imports before any other lines
74
- `import { GenericKind, RegisterKind } from "kubernetes-fluent-client";\n`);
83
+ `import { GenericKind, RegisterKind } from "${opts.npmPackage}";\n`);
75
84
  // Replace the interface with a named class that extends GenericKind
76
85
  const entryIdx = processedLines.findIndex(line => line.includes(`export interface ${name} {`));
77
86
  // Replace the interface with a named class that extends GenericKind
@@ -79,12 +88,12 @@ async function convertCRDtoTS(crd, opts) {
79
88
  // Add the RegisterKind call
80
89
  processedLines.push(`RegisterKind(${name}, {`);
81
90
  processedLines.push(` group: "${crd.spec.group}",`);
82
- processedLines.push(` version: "${match.name}",`);
91
+ processedLines.push(` version: "${version}",`);
83
92
  processedLines.push(` kind: "${name}",`);
84
93
  processedLines.push(`});`);
85
94
  }
86
95
  const finalContents = processedLines.join("\n");
87
- const fileName = `${name.toLowerCase()}-${match.name.toLowerCase()}`;
96
+ const fileName = `${name.toLowerCase()}-${version.toLowerCase()}`;
88
97
  // If an output file is specified, write the output to the file
89
98
  if (opts.directory) {
90
99
  // Create the directory if it doesn't exist
@@ -101,14 +110,16 @@ async function convertCRDtoTS(crd, opts) {
101
110
  /**
102
111
  * Reads a CustomResourceDefinition from a file, the cluster or the internet
103
112
  *
104
- * @param source The source to read from (file path, cluster or URL)
113
+ * @param opts The options to use when reading
105
114
  * @returns A promise that resolves when the CustomResourceDefinition has been read
106
115
  */
107
- async function readOrFetchCrd(source) {
116
+ async function readOrFetchCrd(opts) {
117
+ const { source, logFn } = opts;
108
118
  const filePath = path.join(process.cwd(), source);
109
119
  // First try to read the source as a file
110
120
  try {
111
121
  if (fs.existsSync(filePath)) {
122
+ logFn(`Attempting to load ${source} as a local file`);
112
123
  const payload = fs.readFileSync(filePath, "utf8");
113
124
  return (0, client_node_1.loadAllYaml)(payload);
114
125
  }
@@ -121,6 +132,7 @@ async function readOrFetchCrd(source) {
121
132
  const url = new URL(source);
122
133
  // If the source is a URL, fetch it
123
134
  if (url.protocol === "http:" || url.protocol === "https:") {
135
+ logFn(`Attempting to load ${source} as a URL`);
124
136
  const { ok, data } = await (0, fetch_1.fetch)(source);
125
137
  // If the request failed, throw an error
126
138
  if (!ok) {
@@ -130,14 +142,18 @@ async function readOrFetchCrd(source) {
130
142
  }
131
143
  }
132
144
  catch (e) {
133
- // Ignore errors
145
+ // If invalid, ignore the error
146
+ if (e.code !== "ERR_INVALID_URL") {
147
+ throw new Error(e);
148
+ }
134
149
  }
135
150
  // Finally, if the source is not a file or URL, try to read it as a CustomResourceDefinition from the cluster
136
151
  try {
152
+ logFn(`Attempting to read ${source} from the current Kubernetes context`);
137
153
  return [await (0, fluent_1.K8s)(upstream_1.CustomResourceDefinition).Get(source)];
138
154
  }
139
155
  catch (e) {
140
- throw new Error(`Failed to read ${source} as a file, url or K8s CRD: ${e}`);
156
+ throw new Error(`Failed to read ${source} as a file, url or K8s CRD: ${e.data?.message || "Cluster not available"}`);
141
157
  }
142
158
  }
143
159
  /**
@@ -147,10 +163,12 @@ async function readOrFetchCrd(source) {
147
163
  * @returns A promise that resolves when the TypeScript types have been generated
148
164
  */
149
165
  async function generate(opts) {
150
- const crds = await readOrFetchCrd(opts.source);
166
+ const crds = (await readOrFetchCrd(opts)).filter(crd => !!crd);
151
167
  const results = {};
168
+ opts.logFn("");
152
169
  for (const crd of crds) {
153
- if (!crd || crd.kind !== "CustomResourceDefinition" || !crd.spec?.versions?.length) {
170
+ if (crd.kind !== "CustomResourceDefinition" || !crd.spec?.versions?.length) {
171
+ opts.logFn(`Skipping ${crd?.metadata?.name}, it does not appear to be a CRD`);
154
172
  // Ignore empty and non-CRD objects
155
173
  continue;
156
174
  }
@@ -160,6 +178,10 @@ async function generate(opts) {
160
178
  results[key] = out[key];
161
179
  }
162
180
  }
181
+ if (opts.directory) {
182
+ // Notify the user that the files have been generated
183
+ opts.logFn(`\n✅ Generated ${Object.keys(results).length} files in the ${opts.directory} directory`);
184
+ }
163
185
  return results;
164
186
  }
165
187
  exports.generate = generate;
@@ -112,25 +112,23 @@ globals_1.jest.mock("./fetch", () => ({
112
112
  globals_1.jest.mock("./fluent", () => ({
113
113
  K8s: globals_1.jest.fn(),
114
114
  }));
115
- (0, globals_1.describe)("CRD to TypeScript Conversion", () => {
115
+ (0, globals_1.describe)("CRD Generate", () => {
116
116
  const originalReadFileSync = fs_1.default.readFileSync;
117
- globals_1.jest.isolateModules(() => {
118
- globals_1.jest.spyOn(fs_1.default, "existsSync").mockReturnValue(true);
119
- globals_1.jest.spyOn(fs_1.default, "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
- globals_1.jest.spyOn(fs_1.default, "mkdirSync").mockReturnValue(undefined);
127
- globals_1.jest.spyOn(fs_1.default, "writeFileSync").mockReturnValue(undefined);
117
+ globals_1.jest.spyOn(fs_1.default, "existsSync").mockReturnValue(true);
118
+ globals_1.jest.spyOn(fs_1.default, "readFileSync").mockImplementation((...args) => {
119
+ // Super janky hack due ot source-map-support calling readFileSync internally
120
+ if (args[0].toString().includes("test-crd.yaml")) {
121
+ return sampleYaml;
122
+ }
123
+ return originalReadFileSync(...args);
128
124
  });
125
+ const mkdirSyncSpy = globals_1.jest.spyOn(fs_1.default, "mkdirSync").mockReturnValue(undefined);
126
+ const writeFileSyncSpy = globals_1.jest.spyOn(fs_1.default, "writeFileSync").mockReturnValue(undefined);
129
127
  (0, globals_1.beforeEach)(() => {
130
128
  globals_1.jest.clearAllMocks();
131
129
  });
132
130
  (0, globals_1.test)("converts CRD to TypeScript", async () => {
133
- const options = { source: "test-crd.yaml", language: "ts" }; // specify your options
131
+ const options = { source: "test-crd.yaml", language: "ts", logFn: globals_1.jest.fn() };
134
132
  const actual = await (0, generate_1.generate)(options);
135
133
  const expectedMovie = [
136
134
  "// This file is auto-generated by kubernetes-fluent-client, do not edit manually\n",
@@ -182,7 +180,7 @@ globals_1.jest.mock("./fluent", () => ({
182
180
  (0, globals_1.expect)(actual["book-v2"]).toEqual(expectedBookV2);
183
181
  });
184
182
  (0, globals_1.test)("converts CRD to TypeScript with plain option", async () => {
185
- const options = { source: "test-crd.yaml", language: "ts", plain: true }; // specify your options
183
+ const options = { source: "test-crd.yaml", language: "ts", plain: true, logFn: globals_1.jest.fn() };
186
184
  const actual = await (0, generate_1.generate)(options);
187
185
  const expectedMovie = [
188
186
  "/**",
@@ -219,8 +217,121 @@ globals_1.jest.mock("./fluent", () => ({
219
217
  (0, globals_1.expect)(actual["book-v1"]).toEqual(expectedBookV1);
220
218
  (0, globals_1.expect)(actual["book-v2"]).toEqual(expectedBookV2);
221
219
  });
220
+ (0, globals_1.test)("converts CRD to TypeScript with other options", async () => {
221
+ const options = {
222
+ source: "test-crd.yaml",
223
+ npmPackage: "test-package",
224
+ logFn: globals_1.jest.fn(),
225
+ };
226
+ const actual = await (0, generate_1.generate)(options);
227
+ const expectedMovie = [
228
+ "// This file is auto-generated by test-package, do not edit manually\n",
229
+ 'import { GenericKind, RegisterKind } from "test-package";\n',
230
+ "/**",
231
+ " * Movie nerd",
232
+ " */",
233
+ "export class Movie extends GenericKind {",
234
+ " spec?: Spec;",
235
+ "}",
236
+ "",
237
+ "export interface Spec {",
238
+ " author?: string;",
239
+ " title?: string;",
240
+ "}",
241
+ "",
242
+ "RegisterKind(Movie, {",
243
+ ' group: "example.com",',
244
+ ' version: "v1",',
245
+ ' kind: "Movie",',
246
+ "});",
247
+ ];
248
+ const expectedBookV1 = [
249
+ "// This file is auto-generated by test-package, do not edit manually\n",
250
+ 'import { GenericKind, RegisterKind } from "test-package";\n',
251
+ "/**",
252
+ " * Book nerd",
253
+ " */",
254
+ "export class Book extends GenericKind {",
255
+ " spec?: Spec;",
256
+ "}",
257
+ "",
258
+ "export interface Spec {",
259
+ " author?: string;",
260
+ " title?: string;",
261
+ "}",
262
+ "",
263
+ "RegisterKind(Book, {",
264
+ ' group: "example.com",',
265
+ ' version: "v1",',
266
+ ' kind: "Book",',
267
+ "});",
268
+ ];
269
+ const expectedBookV2 = expectedBookV1
270
+ .filter(line => !line.includes("title?"))
271
+ .map(line => line.replace("v1", "v2"));
272
+ (0, globals_1.expect)(actual["movie-v1"]).toEqual(expectedMovie);
273
+ (0, globals_1.expect)(actual["book-v1"]).toEqual(expectedBookV1);
274
+ (0, globals_1.expect)(actual["book-v2"]).toEqual(expectedBookV2);
275
+ });
276
+ (0, globals_1.test)("converts CRD to TypeScript and writes to the given directory", async () => {
277
+ const options = {
278
+ source: "test-crd.yaml",
279
+ directory: "test",
280
+ logFn: globals_1.jest.fn(),
281
+ };
282
+ await (0, generate_1.generate)(options);
283
+ const expectedMovie = [
284
+ "// This file is auto-generated by kubernetes-fluent-client, do not edit manually\n",
285
+ 'import { GenericKind, RegisterKind } from "kubernetes-fluent-client";\n',
286
+ "/**",
287
+ " * Movie nerd",
288
+ " */",
289
+ "export class Movie extends GenericKind {",
290
+ " spec?: Spec;",
291
+ "}",
292
+ "",
293
+ "export interface Spec {",
294
+ " author?: string;",
295
+ " title?: string;",
296
+ "}",
297
+ "",
298
+ "RegisterKind(Movie, {",
299
+ ' group: "example.com",',
300
+ ' version: "v1",',
301
+ ' kind: "Movie",',
302
+ "});",
303
+ ];
304
+ const expectedBookV1 = [
305
+ "// This file is auto-generated by kubernetes-fluent-client, do not edit manually\n",
306
+ 'import { GenericKind, RegisterKind } from "kubernetes-fluent-client";\n',
307
+ "/**",
308
+ " * Book nerd",
309
+ " */",
310
+ "export class Book extends GenericKind {",
311
+ " spec?: Spec;",
312
+ "}",
313
+ "",
314
+ "export interface Spec {",
315
+ " author?: string;",
316
+ " title?: string;",
317
+ "}",
318
+ "",
319
+ "RegisterKind(Book, {",
320
+ ' group: "example.com",',
321
+ ' version: "v1",',
322
+ ' kind: "Book",',
323
+ "});",
324
+ ];
325
+ const expectedBookV2 = expectedBookV1
326
+ .filter(line => !line.includes("title?"))
327
+ .map(line => line.replace("v1", "v2"));
328
+ (0, globals_1.expect)(mkdirSyncSpy).toHaveBeenCalledWith("test", { recursive: true });
329
+ (0, globals_1.expect)(writeFileSyncSpy).toHaveBeenCalledWith("test/movie-v1.ts", expectedMovie.join("\n"));
330
+ (0, globals_1.expect)(writeFileSyncSpy).toHaveBeenCalledWith("test/book-v1.ts", expectedBookV1.join("\n"));
331
+ (0, globals_1.expect)(writeFileSyncSpy).toHaveBeenCalledWith("test/book-v2.ts", expectedBookV2.join("\n"));
332
+ });
222
333
  (0, globals_1.test)("converts CRD to Go", async () => {
223
- const options = { source: "test-crd.yaml", language: "go" }; // specify your options
334
+ const options = { source: "test-crd.yaml", language: "go", logFn: globals_1.jest.fn() };
224
335
  const actual = await (0, generate_1.generate)(options);
225
336
  const expectedMovie = [
226
337
  "// Movie nerd",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "kubernetes-fluent-client",
3
- "version": "1.6.0",
3
+ "version": "1.6.1",
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",
package/src/cli.ts CHANGED
@@ -37,8 +37,15 @@ void yargs(hideBin(process.argv))
37
37
  })
38
38
  .demandOption(["source", "directory"]);
39
39
  },
40
- argv => {
41
- void generate(argv as GenerateOptions);
40
+ async argv => {
41
+ const opts = argv as unknown as GenerateOptions;
42
+ opts.logFn = console.log;
43
+
44
+ try {
45
+ await generate(opts);
46
+ } catch (e) {
47
+ console.log(`\n❌ ${e.message}`);
48
+ }
42
49
  },
43
50
  )
44
51
  .parse();
@@ -111,28 +111,26 @@ jest.mock("./fluent", () => ({
111
111
  K8s: jest.fn(),
112
112
  }));
113
113
 
114
- describe("CRD to TypeScript Conversion", () => {
114
+ describe("CRD Generate", () => {
115
115
  const originalReadFileSync = fs.readFileSync;
116
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);
117
+ jest.spyOn(fs, "existsSync").mockReturnValue(true);
118
+ jest.spyOn(fs, "readFileSync").mockImplementation((...args) => {
119
+ // Super janky hack due ot source-map-support calling readFileSync internally
120
+ if (args[0].toString().includes("test-crd.yaml")) {
121
+ return sampleYaml;
122
+ }
123
+ return originalReadFileSync(...args);
128
124
  });
125
+ const mkdirSyncSpy = jest.spyOn(fs, "mkdirSync").mockReturnValue(undefined);
126
+ const writeFileSyncSpy = jest.spyOn(fs, "writeFileSync").mockReturnValue(undefined);
129
127
 
130
128
  beforeEach(() => {
131
129
  jest.clearAllMocks();
132
130
  });
133
131
 
134
132
  test("converts CRD to TypeScript", async () => {
135
- const options = { source: "test-crd.yaml", language: "ts" }; // specify your options
133
+ const options = { source: "test-crd.yaml", language: "ts", logFn: jest.fn() };
136
134
 
137
135
  const actual = await generate(options);
138
136
  const expectedMovie = [
@@ -187,7 +185,7 @@ describe("CRD to TypeScript Conversion", () => {
187
185
  });
188
186
 
189
187
  test("converts CRD to TypeScript with plain option", async () => {
190
- const options = { source: "test-crd.yaml", language: "ts", plain: true }; // specify your options
188
+ const options = { source: "test-crd.yaml", language: "ts", plain: true, logFn: jest.fn() };
191
189
 
192
190
  const actual = await generate(options);
193
191
  const expectedMovie = [
@@ -227,8 +225,127 @@ describe("CRD to TypeScript Conversion", () => {
227
225
  expect(actual["book-v2"]).toEqual(expectedBookV2);
228
226
  });
229
227
 
228
+ test("converts CRD to TypeScript with other options", async () => {
229
+ const options = {
230
+ source: "test-crd.yaml",
231
+ npmPackage: "test-package",
232
+ logFn: jest.fn(),
233
+ };
234
+
235
+ const actual = await generate(options);
236
+ const expectedMovie = [
237
+ "// This file is auto-generated by test-package, do not edit manually\n",
238
+ 'import { GenericKind, RegisterKind } from "test-package";\n',
239
+ "/**",
240
+ " * Movie nerd",
241
+ " */",
242
+ "export class Movie extends GenericKind {",
243
+ " spec?: Spec;",
244
+ "}",
245
+ "",
246
+ "export interface Spec {",
247
+ " author?: string;",
248
+ " title?: string;",
249
+ "}",
250
+ "",
251
+ "RegisterKind(Movie, {",
252
+ ' group: "example.com",',
253
+ ' version: "v1",',
254
+ ' kind: "Movie",',
255
+ "});",
256
+ ];
257
+ const expectedBookV1 = [
258
+ "// This file is auto-generated by test-package, do not edit manually\n",
259
+ 'import { GenericKind, RegisterKind } from "test-package";\n',
260
+ "/**",
261
+ " * Book nerd",
262
+ " */",
263
+ "export class Book extends GenericKind {",
264
+ " spec?: Spec;",
265
+ "}",
266
+ "",
267
+ "export interface Spec {",
268
+ " author?: string;",
269
+ " title?: string;",
270
+ "}",
271
+ "",
272
+ "RegisterKind(Book, {",
273
+ ' group: "example.com",',
274
+ ' version: "v1",',
275
+ ' kind: "Book",',
276
+ "});",
277
+ ];
278
+ const expectedBookV2 = expectedBookV1
279
+ .filter(line => !line.includes("title?"))
280
+ .map(line => line.replace("v1", "v2"));
281
+
282
+ expect(actual["movie-v1"]).toEqual(expectedMovie);
283
+ expect(actual["book-v1"]).toEqual(expectedBookV1);
284
+ expect(actual["book-v2"]).toEqual(expectedBookV2);
285
+ });
286
+
287
+ test("converts CRD to TypeScript and writes to the given directory", async () => {
288
+ const options = {
289
+ source: "test-crd.yaml",
290
+ directory: "test",
291
+ logFn: jest.fn(),
292
+ };
293
+
294
+ await generate(options);
295
+ const expectedMovie = [
296
+ "// This file is auto-generated by kubernetes-fluent-client, do not edit manually\n",
297
+ 'import { GenericKind, RegisterKind } from "kubernetes-fluent-client";\n',
298
+ "/**",
299
+ " * Movie nerd",
300
+ " */",
301
+ "export class Movie extends GenericKind {",
302
+ " spec?: Spec;",
303
+ "}",
304
+ "",
305
+ "export interface Spec {",
306
+ " author?: string;",
307
+ " title?: string;",
308
+ "}",
309
+ "",
310
+ "RegisterKind(Movie, {",
311
+ ' group: "example.com",',
312
+ ' version: "v1",',
313
+ ' kind: "Movie",',
314
+ "});",
315
+ ];
316
+ const expectedBookV1 = [
317
+ "// This file is auto-generated by kubernetes-fluent-client, do not edit manually\n",
318
+ 'import { GenericKind, RegisterKind } from "kubernetes-fluent-client";\n',
319
+ "/**",
320
+ " * Book nerd",
321
+ " */",
322
+ "export class Book extends GenericKind {",
323
+ " spec?: Spec;",
324
+ "}",
325
+ "",
326
+ "export interface Spec {",
327
+ " author?: string;",
328
+ " title?: string;",
329
+ "}",
330
+ "",
331
+ "RegisterKind(Book, {",
332
+ ' group: "example.com",',
333
+ ' version: "v1",',
334
+ ' kind: "Book",',
335
+ "});",
336
+ ];
337
+ const expectedBookV2 = expectedBookV1
338
+ .filter(line => !line.includes("title?"))
339
+ .map(line => line.replace("v1", "v2"));
340
+
341
+ expect(mkdirSyncSpy).toHaveBeenCalledWith("test", { recursive: true });
342
+ expect(writeFileSyncSpy).toHaveBeenCalledWith("test/movie-v1.ts", expectedMovie.join("\n"));
343
+ expect(writeFileSyncSpy).toHaveBeenCalledWith("test/book-v1.ts", expectedBookV1.join("\n"));
344
+ expect(writeFileSyncSpy).toHaveBeenCalledWith("test/book-v2.ts", expectedBookV2.join("\n"));
345
+ });
346
+
230
347
  test("converts CRD to Go", async () => {
231
- const options = { source: "test-crd.yaml", language: "go" }; // specify your options
348
+ const options = { source: "test-crd.yaml", language: "go", logFn: jest.fn() };
232
349
 
233
350
  const actual = await generate(options);
234
351
  const expectedMovie = [
package/src/generate.ts CHANGED
@@ -15,6 +15,7 @@ import {
15
15
  import { fetch } from "./fetch";
16
16
  import { K8s } from "./fluent";
17
17
  import { CustomResourceDefinition } from "./upstream";
18
+ import { LogFn } from "./types";
18
19
 
19
20
  export interface GenerateOptions {
20
21
  /** The source URL, yaml file path or K8s CRD name */
@@ -25,6 +26,10 @@ export interface GenerateOptions {
25
26
  plain?: boolean;
26
27
  /** The language to generate types in */
27
28
  language?: string | TargetLanguage;
29
+ /** Override the NPM package to import when generating formatted Typescript */
30
+ npmPackage?: string;
31
+ /** Log function callback */
32
+ logFn: LogFn;
28
33
  }
29
34
 
30
35
  /**
@@ -44,12 +49,16 @@ async function convertCRDtoTS(
44
49
  const results: Record<string, string[]> = {};
45
50
 
46
51
  for (const match of crd.spec.versions) {
52
+ const version = match.name;
53
+
47
54
  // Get the schema from the matched version
48
55
  const schema = JSON.stringify(match?.schema?.openAPIV3Schema);
49
56
 
50
57
  // Create a new JSONSchemaInput
51
58
  const schemaInput = new JSONSchemaInput(new FetchingJSONSchemaStore());
52
59
 
60
+ opts.logFn(`- Generating ${crd.spec.group}/${version} types for ${name}`);
61
+
53
62
  // Add the schema to the input
54
63
  await schemaInput.addSource({ name, schema });
55
64
 
@@ -57,10 +66,15 @@ async function convertCRDtoTS(
57
66
  const inputData = new InputData();
58
67
  inputData.addInput(schemaInput);
59
68
 
69
+ // If the language is not specified, default to TypeScript
70
+ if (!opts.language) {
71
+ opts.language = "ts";
72
+ }
73
+
60
74
  // Generate the types
61
75
  const out = await quicktype({
62
76
  inputData,
63
- lang: opts.language || "ts",
77
+ lang: opts.language,
64
78
  rendererOptions: { "just-types": "true" },
65
79
  });
66
80
 
@@ -73,11 +87,15 @@ async function convertCRDtoTS(
73
87
 
74
88
  // If the language is TypeScript and plain is not specified, wire up the fluent client
75
89
  if (opts.language === "ts" && !opts.plain) {
90
+ if (!opts.npmPackage) {
91
+ opts.npmPackage = "kubernetes-fluent-client";
92
+ }
93
+
76
94
  processedLines.unshift(
77
95
  // Add warning that the file is auto-generated
78
- `// This file is auto-generated by kubernetes-fluent-client, do not edit manually\n`,
96
+ `// This file is auto-generated by ${opts.npmPackage}, do not edit manually\n`,
79
97
  // Add the imports before any other lines
80
- `import { GenericKind, RegisterKind } from "kubernetes-fluent-client";\n`,
98
+ `import { GenericKind, RegisterKind } from "${opts.npmPackage}";\n`,
81
99
  );
82
100
 
83
101
  // Replace the interface with a named class that extends GenericKind
@@ -91,13 +109,13 @@ async function convertCRDtoTS(
91
109
  // Add the RegisterKind call
92
110
  processedLines.push(`RegisterKind(${name}, {`);
93
111
  processedLines.push(` group: "${crd.spec.group}",`);
94
- processedLines.push(` version: "${match.name}",`);
112
+ processedLines.push(` version: "${version}",`);
95
113
  processedLines.push(` kind: "${name}",`);
96
114
  processedLines.push(`});`);
97
115
  }
98
116
 
99
117
  const finalContents = processedLines.join("\n");
100
- const fileName = `${name.toLowerCase()}-${match.name.toLowerCase()}`;
118
+ const fileName = `${name.toLowerCase()}-${version.toLowerCase()}`;
101
119
 
102
120
  // If an output file is specified, write the output to the file
103
121
  if (opts.directory) {
@@ -119,15 +137,17 @@ async function convertCRDtoTS(
119
137
  /**
120
138
  * Reads a CustomResourceDefinition from a file, the cluster or the internet
121
139
  *
122
- * @param source The source to read from (file path, cluster or URL)
140
+ * @param opts The options to use when reading
123
141
  * @returns A promise that resolves when the CustomResourceDefinition has been read
124
142
  */
125
- async function readOrFetchCrd(source: string): Promise<CustomResourceDefinition[]> {
143
+ async function readOrFetchCrd(opts: GenerateOptions): Promise<CustomResourceDefinition[]> {
144
+ const { source, logFn } = opts;
126
145
  const filePath = path.join(process.cwd(), source);
127
146
 
128
147
  // First try to read the source as a file
129
148
  try {
130
149
  if (fs.existsSync(filePath)) {
150
+ logFn(`Attempting to load ${source} as a local file`);
131
151
  const payload = fs.readFileSync(filePath, "utf8");
132
152
  return loadAllYaml(payload) as CustomResourceDefinition[];
133
153
  }
@@ -141,6 +161,7 @@ async function readOrFetchCrd(source: string): Promise<CustomResourceDefinition[
141
161
 
142
162
  // If the source is a URL, fetch it
143
163
  if (url.protocol === "http:" || url.protocol === "https:") {
164
+ logFn(`Attempting to load ${source} as a URL`);
144
165
  const { ok, data } = await fetch<string>(source);
145
166
 
146
167
  // If the request failed, throw an error
@@ -151,14 +172,22 @@ async function readOrFetchCrd(source: string): Promise<CustomResourceDefinition[
151
172
  return loadAllYaml(data) as CustomResourceDefinition[];
152
173
  }
153
174
  } catch (e) {
154
- // Ignore errors
175
+ // If invalid, ignore the error
176
+ if (e.code !== "ERR_INVALID_URL") {
177
+ throw new Error(e);
178
+ }
155
179
  }
156
180
 
157
181
  // Finally, if the source is not a file or URL, try to read it as a CustomResourceDefinition from the cluster
158
182
  try {
183
+ logFn(`Attempting to read ${source} from the current Kubernetes context`);
159
184
  return [await K8s(CustomResourceDefinition).Get(source)];
160
185
  } catch (e) {
161
- throw new Error(`Failed to read ${source} as a file, url or K8s CRD: ${e}`);
186
+ throw new Error(
187
+ `Failed to read ${source} as a file, url or K8s CRD: ${
188
+ e.data?.message || "Cluster not available"
189
+ }`,
190
+ );
162
191
  }
163
192
  }
164
193
 
@@ -169,11 +198,14 @@ async function readOrFetchCrd(source: string): Promise<CustomResourceDefinition[
169
198
  * @returns A promise that resolves when the TypeScript types have been generated
170
199
  */
171
200
  export async function generate(opts: GenerateOptions) {
172
- const crds = await readOrFetchCrd(opts.source);
201
+ const crds = (await readOrFetchCrd(opts)).filter(crd => !!crd);
173
202
  const results: Record<string, string[]> = {};
174
203
 
204
+ opts.logFn("");
205
+
175
206
  for (const crd of crds) {
176
- if (!crd || crd.kind !== "CustomResourceDefinition" || !crd.spec?.versions?.length) {
207
+ if (crd.kind !== "CustomResourceDefinition" || !crd.spec?.versions?.length) {
208
+ opts.logFn(`Skipping ${crd?.metadata?.name}, it does not appear to be a CRD`);
177
209
  // Ignore empty and non-CRD objects
178
210
  continue;
179
211
  }
@@ -185,5 +217,12 @@ export async function generate(opts: GenerateOptions) {
185
217
  }
186
218
  }
187
219
 
220
+ if (opts.directory) {
221
+ // Notify the user that the files have been generated
222
+ opts.logFn(
223
+ `\n✅ Generated ${Object.keys(results).length} files in the ${opts.directory} directory`,
224
+ );
225
+ }
226
+
188
227
  return results;
189
228
  }