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 +9 -2
- package/dist/generate.d.ts +5 -0
- package/dist/generate.d.ts.map +1 -1
- package/dist/generate.js +33 -11
- package/dist/generate.test.js +126 -15
- package/package.json +1 -1
- package/src/cli.ts +9 -2
- package/src/generate.test.ts +132 -15
- package/src/generate.ts +50 -11
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
|
-
|
|
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();
|
package/dist/generate.d.ts
CHANGED
|
@@ -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
|
package/dist/generate.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"generate.d.ts","sourceRoot":"","sources":["../src/generate.ts"],"names":[],"mappings":"AAMA,OAAO,EAIL,cAAc,EAEf,MAAM,gBAAgB,CAAC;
|
|
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
|
|
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
|
|
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 "
|
|
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: "${
|
|
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()}-${
|
|
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
|
|
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(
|
|
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
|
-
//
|
|
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.
|
|
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 (
|
|
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;
|
package/dist/generate.test.js
CHANGED
|
@@ -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
|
|
115
|
+
(0, globals_1.describe)("CRD Generate", () => {
|
|
116
116
|
const originalReadFileSync = fs_1.default.readFileSync;
|
|
117
|
-
globals_1.jest.
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
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" };
|
|
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 };
|
|
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" };
|
|
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
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
|
-
|
|
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();
|
package/src/generate.test.ts
CHANGED
|
@@ -111,28 +111,26 @@ jest.mock("./fluent", () => ({
|
|
|
111
111
|
K8s: jest.fn(),
|
|
112
112
|
}));
|
|
113
113
|
|
|
114
|
-
describe("CRD
|
|
114
|
+
describe("CRD Generate", () => {
|
|
115
115
|
const originalReadFileSync = fs.readFileSync;
|
|
116
116
|
|
|
117
|
-
jest.
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
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" };
|
|
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 };
|
|
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" };
|
|
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
|
|
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
|
|
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 "
|
|
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: "${
|
|
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()}-${
|
|
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
|
|
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(
|
|
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
|
-
//
|
|
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(
|
|
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.
|
|
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 (
|
|
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
|
}
|