kubernetes-fluent-client 1.5.1 → 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.
- package/dist/cli.d.ts +3 -0
- package/dist/cli.d.ts.map +1 -0
- package/dist/cli.js +39 -0
- package/dist/fetch.js +1 -1
- package/dist/fetch.test.js +1 -1
- package/dist/fluent/apply.js +1 -1
- package/dist/fluent/index.js +1 -1
- package/dist/fluent/types.js +1 -1
- package/dist/fluent/utils.js +1 -1
- package/dist/fluent/utils.test.js +1 -1
- package/dist/fluent/watch.js +1 -1
- package/dist/generate.d.ts +19 -0
- package/dist/generate.d.ts.map +1 -0
- package/dist/generate.js +165 -0
- package/dist/generate.test.d.ts +2 -0
- package/dist/generate.test.d.ts.map +1 -0
- package/dist/generate.test.js +256 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +4 -1
- package/package.json +10 -5
- package/src/cli.ts +44 -0
- package/src/fetch.test.ts +1 -1
- package/src/fetch.ts +1 -1
- package/src/fluent/apply.ts +1 -1
- package/src/fluent/index.ts +1 -1
- package/src/fluent/types.ts +1 -1
- package/src/fluent/utils.test.ts +1 -1
- package/src/fluent/utils.ts +1 -1
- package/src/fluent/watch.ts +1 -1
- package/src/generate.test.ts +266 -0
- package/src/generate.ts +189 -0
- package/src/index.ts +3 -0
package/dist/cli.d.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"cli.d.ts","sourceRoot":"","sources":["../src/cli.ts"],"names":[],"mappings":""}
|
package/dist/cli.js
ADDED
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
"use strict";
|
|
3
|
+
// SPDX-License-Identifier: Apache-2.0
|
|
4
|
+
// SPDX-FileCopyrightText: 2023-Present The Kubernetes Fluent Client Authors
|
|
5
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
6
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
7
|
+
};
|
|
8
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
9
|
+
const helpers_1 = require("yargs/helpers");
|
|
10
|
+
const yargs_1 = __importDefault(require("yargs/yargs"));
|
|
11
|
+
const generate_1 = require("./generate");
|
|
12
|
+
void (0, yargs_1.default)((0, helpers_1.hideBin)(process.argv))
|
|
13
|
+
.version(false)
|
|
14
|
+
.command("crd [source] [directory]", "generate usable types from a K8s CRD", yargs => {
|
|
15
|
+
return yargs
|
|
16
|
+
.positional("source", {
|
|
17
|
+
describe: "the yaml file path, remote url, or K8s CRD name",
|
|
18
|
+
type: "string",
|
|
19
|
+
})
|
|
20
|
+
.positional("directory", {
|
|
21
|
+
describe: "the directory to output the generated types to",
|
|
22
|
+
type: "string",
|
|
23
|
+
})
|
|
24
|
+
.option("plain", {
|
|
25
|
+
alias: "p",
|
|
26
|
+
type: "boolean",
|
|
27
|
+
description: "generate plain types without binding to the fluent client, automatically enabled when an alternate language is specified",
|
|
28
|
+
})
|
|
29
|
+
.option("language", {
|
|
30
|
+
alias: "l",
|
|
31
|
+
type: "string",
|
|
32
|
+
default: "ts",
|
|
33
|
+
description: "the language to generate types in, see https://github.com/glideapps/quicktype#target-languages for a list of supported languages",
|
|
34
|
+
})
|
|
35
|
+
.demandOption(["source", "directory"]);
|
|
36
|
+
}, argv => {
|
|
37
|
+
void (0, generate_1.generate)(argv);
|
|
38
|
+
})
|
|
39
|
+
.parse();
|
package/dist/fetch.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
// SPDX-License-Identifier: Apache-2.0
|
|
3
|
-
// SPDX-FileCopyrightText: 2023-Present The
|
|
3
|
+
// SPDX-FileCopyrightText: 2023-Present The Kubernetes Fluent Client Authors
|
|
4
4
|
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
5
5
|
if (k2 === undefined) k2 = k;
|
|
6
6
|
var desc = Object.getOwnPropertyDescriptor(m, k);
|
package/dist/fetch.test.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
// SPDX-License-Identifier: Apache-2.0
|
|
3
|
-
// SPDX-FileCopyrightText: 2023-Present The
|
|
3
|
+
// SPDX-FileCopyrightText: 2023-Present The Kubernetes Fluent Client Authors
|
|
4
4
|
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
5
5
|
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
6
6
|
};
|
package/dist/fluent/apply.js
CHANGED
package/dist/fluent/index.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
// SPDX-License-Identifier: Apache-2.0
|
|
3
|
-
// SPDX-FileCopyrightText: 2023-Present The
|
|
3
|
+
// SPDX-FileCopyrightText: 2023-Present The Kubernetes Fluent Client Authors
|
|
4
4
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
5
5
|
exports.K8s = void 0;
|
|
6
6
|
const http_status_codes_1 = require("http-status-codes");
|
package/dist/fluent/types.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
// SPDX-License-Identifier: Apache-2.0
|
|
3
|
-
// SPDX-FileCopyrightText: 2023-Present The
|
|
3
|
+
// SPDX-FileCopyrightText: 2023-Present The Kubernetes Fluent Client Authors
|
|
4
4
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
5
5
|
exports.WatchPhase = void 0;
|
|
6
6
|
/**
|
package/dist/fluent/utils.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
// SPDX-License-Identifier: Apache-2.0
|
|
3
|
-
// SPDX-FileCopyrightText: 2023-Present The
|
|
3
|
+
// SPDX-FileCopyrightText: 2023-Present The Kubernetes Fluent Client Authors
|
|
4
4
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
5
5
|
exports.k8sExec = exports.k8sCfg = exports.pathBuilder = void 0;
|
|
6
6
|
const client_node_1 = require("@kubernetes/client-node");
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
// SPDX-License-Identifier: Apache-2.0
|
|
3
|
-
// SPDX-FileCopyrightText: 2023-Present The
|
|
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
6
|
const fetch_1 = require("../fetch");
|
package/dist/fluent/watch.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
// SPDX-License-Identifier: Apache-2.0
|
|
3
|
-
// SPDX-FileCopyrightText: 2023-Present The
|
|
3
|
+
// SPDX-FileCopyrightText: 2023-Present The Kubernetes Fluent Client Authors
|
|
4
4
|
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
5
5
|
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
6
6
|
};
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { TargetLanguage } from "quicktype-core";
|
|
2
|
+
export interface GenerateOptions {
|
|
3
|
+
/** The source URL, yaml file path or K8s CRD name */
|
|
4
|
+
source: string;
|
|
5
|
+
/** The output directory path */
|
|
6
|
+
directory?: string;
|
|
7
|
+
/** Disable kubernetes-fluent-client wrapping */
|
|
8
|
+
plain?: boolean;
|
|
9
|
+
/** The language to generate types in */
|
|
10
|
+
language?: string | TargetLanguage;
|
|
11
|
+
}
|
|
12
|
+
/**
|
|
13
|
+
* Generate TypeScript types from a K8s CRD
|
|
14
|
+
*
|
|
15
|
+
* @param opts The options to use when generating
|
|
16
|
+
* @returns A promise that resolves when the TypeScript types have been generated
|
|
17
|
+
*/
|
|
18
|
+
export declare function generate(opts: GenerateOptions): Promise<Record<string, string[]>>;
|
|
19
|
+
//# sourceMappingURL=generate.d.ts.map
|
|
@@ -0,0 +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"}
|
package/dist/generate.js
ADDED
|
@@ -0,0 +1,165 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
// SPDX-License-Identifier: Apache-2.0
|
|
3
|
+
// SPDX-FileCopyrightText: 2023-Present The Kubernetes Fluent Client Authors
|
|
4
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
5
|
+
if (k2 === undefined) k2 = k;
|
|
6
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
7
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
8
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
9
|
+
}
|
|
10
|
+
Object.defineProperty(o, k2, desc);
|
|
11
|
+
}) : (function(o, m, k, k2) {
|
|
12
|
+
if (k2 === undefined) k2 = k;
|
|
13
|
+
o[k2] = m[k];
|
|
14
|
+
}));
|
|
15
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
16
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
17
|
+
}) : function(o, v) {
|
|
18
|
+
o["default"] = v;
|
|
19
|
+
});
|
|
20
|
+
var __importStar = (this && this.__importStar) || function (mod) {
|
|
21
|
+
if (mod && mod.__esModule) return mod;
|
|
22
|
+
var result = {};
|
|
23
|
+
if (mod != null) for (var k in mod) if (k !== "default" && Object.prototype.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k);
|
|
24
|
+
__setModuleDefault(result, mod);
|
|
25
|
+
return result;
|
|
26
|
+
};
|
|
27
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
28
|
+
exports.generate = void 0;
|
|
29
|
+
const client_node_1 = require("@kubernetes/client-node");
|
|
30
|
+
const fs = __importStar(require("fs"));
|
|
31
|
+
const path = __importStar(require("path"));
|
|
32
|
+
const quicktype_core_1 = require("quicktype-core");
|
|
33
|
+
const fetch_1 = require("./fetch");
|
|
34
|
+
const fluent_1 = require("./fluent");
|
|
35
|
+
const upstream_1 = require("./upstream");
|
|
36
|
+
/**
|
|
37
|
+
* Converts a CustomResourceDefinition to TypeScript types
|
|
38
|
+
*
|
|
39
|
+
* @param crd The CustomResourceDefinition to convert
|
|
40
|
+
* @param opts The options to use when converting
|
|
41
|
+
* @returns A promise that resolves when the CustomResourceDefinition has been converted
|
|
42
|
+
*/
|
|
43
|
+
async function convertCRDtoTS(crd, opts) {
|
|
44
|
+
// Get the name of the kind
|
|
45
|
+
const name = crd.spec.names.kind;
|
|
46
|
+
const results = {};
|
|
47
|
+
for (const match of crd.spec.versions) {
|
|
48
|
+
// Get the schema from the matched version
|
|
49
|
+
const schema = JSON.stringify(match?.schema?.openAPIV3Schema);
|
|
50
|
+
// Create a new JSONSchemaInput
|
|
51
|
+
const schemaInput = new quicktype_core_1.JSONSchemaInput(new quicktype_core_1.FetchingJSONSchemaStore());
|
|
52
|
+
// Add the schema to the input
|
|
53
|
+
await schemaInput.addSource({ name, schema });
|
|
54
|
+
// Create a new InputData object
|
|
55
|
+
const inputData = new quicktype_core_1.InputData();
|
|
56
|
+
inputData.addInput(schemaInput);
|
|
57
|
+
// Generate the types
|
|
58
|
+
const out = await (0, quicktype_core_1.quicktype)({
|
|
59
|
+
inputData,
|
|
60
|
+
lang: opts.language || "ts",
|
|
61
|
+
rendererOptions: { "just-types": "true" },
|
|
62
|
+
});
|
|
63
|
+
let processedLines = out.lines;
|
|
64
|
+
// If using typescript, remove the line containing `[property: string]: any;`
|
|
65
|
+
if (opts.language === "ts" || opts.language === "typescript") {
|
|
66
|
+
processedLines = out.lines.filter(line => !line.includes("[property: string]: any;"));
|
|
67
|
+
}
|
|
68
|
+
// If the language is TypeScript and plain is not specified, wire up the fluent client
|
|
69
|
+
if (opts.language === "ts" && !opts.plain) {
|
|
70
|
+
processedLines.unshift(
|
|
71
|
+
// Add warning that the file is auto-generated
|
|
72
|
+
`// This file is auto-generated by kubernetes-fluent-client, do not edit manually\n`,
|
|
73
|
+
// Add the imports before any other lines
|
|
74
|
+
`import { GenericKind, RegisterKind } from "kubernetes-fluent-client";\n`);
|
|
75
|
+
// Replace the interface with a named class that extends GenericKind
|
|
76
|
+
const entryIdx = processedLines.findIndex(line => line.includes(`export interface ${name} {`));
|
|
77
|
+
// Replace the interface with a named class that extends GenericKind
|
|
78
|
+
processedLines[entryIdx] = `export class ${name} extends GenericKind {`;
|
|
79
|
+
// Add the RegisterKind call
|
|
80
|
+
processedLines.push(`RegisterKind(${name}, {`);
|
|
81
|
+
processedLines.push(` group: "${crd.spec.group}",`);
|
|
82
|
+
processedLines.push(` version: "${match.name}",`);
|
|
83
|
+
processedLines.push(` kind: "${name}",`);
|
|
84
|
+
processedLines.push(`});`);
|
|
85
|
+
}
|
|
86
|
+
const finalContents = processedLines.join("\n");
|
|
87
|
+
const fileName = `${name.toLowerCase()}-${match.name.toLowerCase()}`;
|
|
88
|
+
// If an output file is specified, write the output to the file
|
|
89
|
+
if (opts.directory) {
|
|
90
|
+
// Create the directory if it doesn't exist
|
|
91
|
+
fs.mkdirSync(opts.directory, { recursive: true });
|
|
92
|
+
// Write the file
|
|
93
|
+
const filePath = path.join(opts.directory, `${fileName}.${opts.language}`);
|
|
94
|
+
fs.writeFileSync(filePath, finalContents);
|
|
95
|
+
}
|
|
96
|
+
// Add the results to the array
|
|
97
|
+
results[fileName] = processedLines;
|
|
98
|
+
}
|
|
99
|
+
return results;
|
|
100
|
+
}
|
|
101
|
+
/**
|
|
102
|
+
* Reads a CustomResourceDefinition from a file, the cluster or the internet
|
|
103
|
+
*
|
|
104
|
+
* @param source The source to read from (file path, cluster or URL)
|
|
105
|
+
* @returns A promise that resolves when the CustomResourceDefinition has been read
|
|
106
|
+
*/
|
|
107
|
+
async function readOrFetchCrd(source) {
|
|
108
|
+
const filePath = path.join(process.cwd(), source);
|
|
109
|
+
// First try to read the source as a file
|
|
110
|
+
try {
|
|
111
|
+
if (fs.existsSync(filePath)) {
|
|
112
|
+
const payload = fs.readFileSync(filePath, "utf8");
|
|
113
|
+
return (0, client_node_1.loadAllYaml)(payload);
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
catch (e) {
|
|
117
|
+
// Ignore errors
|
|
118
|
+
}
|
|
119
|
+
// Next try to parse the source as a URL
|
|
120
|
+
try {
|
|
121
|
+
const url = new URL(source);
|
|
122
|
+
// If the source is a URL, fetch it
|
|
123
|
+
if (url.protocol === "http:" || url.protocol === "https:") {
|
|
124
|
+
const { ok, data } = await (0, fetch_1.fetch)(source);
|
|
125
|
+
// If the request failed, throw an error
|
|
126
|
+
if (!ok) {
|
|
127
|
+
throw new Error(`Failed to fetch ${source}: ${data}`);
|
|
128
|
+
}
|
|
129
|
+
return (0, client_node_1.loadAllYaml)(data);
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
catch (e) {
|
|
133
|
+
// Ignore errors
|
|
134
|
+
}
|
|
135
|
+
// Finally, if the source is not a file or URL, try to read it as a CustomResourceDefinition from the cluster
|
|
136
|
+
try {
|
|
137
|
+
return [await (0, fluent_1.K8s)(upstream_1.CustomResourceDefinition).Get(source)];
|
|
138
|
+
}
|
|
139
|
+
catch (e) {
|
|
140
|
+
throw new Error(`Failed to read ${source} as a file, url or K8s CRD: ${e}`);
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
/**
|
|
144
|
+
* Generate TypeScript types from a K8s CRD
|
|
145
|
+
*
|
|
146
|
+
* @param opts The options to use when generating
|
|
147
|
+
* @returns A promise that resolves when the TypeScript types have been generated
|
|
148
|
+
*/
|
|
149
|
+
async function generate(opts) {
|
|
150
|
+
const crds = await readOrFetchCrd(opts.source);
|
|
151
|
+
const results = {};
|
|
152
|
+
for (const crd of crds) {
|
|
153
|
+
if (!crd || crd.kind !== "CustomResourceDefinition" || !crd.spec?.versions?.length) {
|
|
154
|
+
// Ignore empty and non-CRD objects
|
|
155
|
+
continue;
|
|
156
|
+
}
|
|
157
|
+
// Add the results to the record
|
|
158
|
+
const out = await convertCRDtoTS(crd, opts);
|
|
159
|
+
for (const key of Object.keys(out)) {
|
|
160
|
+
results[key] = out[key];
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
return results;
|
|
164
|
+
}
|
|
165
|
+
exports.generate = generate;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"generate.test.d.ts","sourceRoot":"","sources":["../src/generate.test.ts"],"names":[],"mappings":""}
|
|
@@ -0,0 +1,256 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
const globals_1 = require("@jest/globals");
|
|
7
|
+
const generate_1 = require("./generate");
|
|
8
|
+
const fs_1 = __importDefault(require("fs"));
|
|
9
|
+
const sampleYaml = `
|
|
10
|
+
# non-crd should be ignored
|
|
11
|
+
apiVersion: v1
|
|
12
|
+
kind: ConfigMap
|
|
13
|
+
metadata:
|
|
14
|
+
name: test
|
|
15
|
+
namespace: default
|
|
16
|
+
data:
|
|
17
|
+
any: bleh
|
|
18
|
+
---
|
|
19
|
+
apiVersion: apiextensions.k8s.io/v1
|
|
20
|
+
kind: CustomResourceDefinition
|
|
21
|
+
metadata:
|
|
22
|
+
name: movies.example.com
|
|
23
|
+
spec:
|
|
24
|
+
group: example.com
|
|
25
|
+
names:
|
|
26
|
+
kind: Movie
|
|
27
|
+
plural: movies
|
|
28
|
+
scope: Namespaced
|
|
29
|
+
versions:
|
|
30
|
+
- name: v1
|
|
31
|
+
schema:
|
|
32
|
+
openAPIV3Schema:
|
|
33
|
+
type: object
|
|
34
|
+
description: Movie nerd
|
|
35
|
+
properties:
|
|
36
|
+
spec:
|
|
37
|
+
properties:
|
|
38
|
+
title:
|
|
39
|
+
type: string
|
|
40
|
+
author:
|
|
41
|
+
type: string
|
|
42
|
+
type: object
|
|
43
|
+
---
|
|
44
|
+
# duplicate entries should not break things
|
|
45
|
+
apiVersion: apiextensions.k8s.io/v1
|
|
46
|
+
kind: CustomResourceDefinition
|
|
47
|
+
metadata:
|
|
48
|
+
name: movies.example.com
|
|
49
|
+
spec:
|
|
50
|
+
group: example.com
|
|
51
|
+
names:
|
|
52
|
+
kind: Movie
|
|
53
|
+
plural: movies
|
|
54
|
+
scope: Namespaced
|
|
55
|
+
versions:
|
|
56
|
+
- name: v1
|
|
57
|
+
schema:
|
|
58
|
+
openAPIV3Schema:
|
|
59
|
+
type: object
|
|
60
|
+
description: Movie nerd
|
|
61
|
+
properties:
|
|
62
|
+
spec:
|
|
63
|
+
properties:
|
|
64
|
+
title:
|
|
65
|
+
type: string
|
|
66
|
+
author:
|
|
67
|
+
type: string
|
|
68
|
+
type: object
|
|
69
|
+
---
|
|
70
|
+
# should support multiple versions
|
|
71
|
+
apiVersion: apiextensions.k8s.io/v1
|
|
72
|
+
kind: CustomResourceDefinition
|
|
73
|
+
metadata:
|
|
74
|
+
name: books.example.com
|
|
75
|
+
spec:
|
|
76
|
+
group: example.com
|
|
77
|
+
names:
|
|
78
|
+
kind: Book
|
|
79
|
+
plural: books
|
|
80
|
+
scope: Namespaced
|
|
81
|
+
versions:
|
|
82
|
+
- name: v1
|
|
83
|
+
schema:
|
|
84
|
+
openAPIV3Schema:
|
|
85
|
+
type: object
|
|
86
|
+
description: Book nerd
|
|
87
|
+
properties:
|
|
88
|
+
spec:
|
|
89
|
+
properties:
|
|
90
|
+
title:
|
|
91
|
+
type: string
|
|
92
|
+
author:
|
|
93
|
+
type: string
|
|
94
|
+
type: object
|
|
95
|
+
- name: v2
|
|
96
|
+
schema:
|
|
97
|
+
openAPIV3Schema:
|
|
98
|
+
type: object
|
|
99
|
+
description: Book nerd
|
|
100
|
+
properties:
|
|
101
|
+
spec:
|
|
102
|
+
properties:
|
|
103
|
+
author:
|
|
104
|
+
type: string
|
|
105
|
+
type: object
|
|
106
|
+
served: true
|
|
107
|
+
storage: true
|
|
108
|
+
`;
|
|
109
|
+
globals_1.jest.mock("./fetch", () => ({
|
|
110
|
+
fetch: globals_1.jest.fn(),
|
|
111
|
+
}));
|
|
112
|
+
globals_1.jest.mock("./fluent", () => ({
|
|
113
|
+
K8s: globals_1.jest.fn(),
|
|
114
|
+
}));
|
|
115
|
+
(0, globals_1.describe)("CRD to TypeScript Conversion", () => {
|
|
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);
|
|
128
|
+
});
|
|
129
|
+
(0, globals_1.beforeEach)(() => {
|
|
130
|
+
globals_1.jest.clearAllMocks();
|
|
131
|
+
});
|
|
132
|
+
(0, globals_1.test)("converts CRD to TypeScript", async () => {
|
|
133
|
+
const options = { source: "test-crd.yaml", language: "ts" }; // specify your options
|
|
134
|
+
const actual = await (0, generate_1.generate)(options);
|
|
135
|
+
const expectedMovie = [
|
|
136
|
+
"// This file is auto-generated by kubernetes-fluent-client, do not edit manually\n",
|
|
137
|
+
'import { GenericKind, RegisterKind } from "kubernetes-fluent-client";\n',
|
|
138
|
+
"/**",
|
|
139
|
+
" * Movie nerd",
|
|
140
|
+
" */",
|
|
141
|
+
"export class Movie extends GenericKind {",
|
|
142
|
+
" spec?: Spec;",
|
|
143
|
+
"}",
|
|
144
|
+
"",
|
|
145
|
+
"export interface Spec {",
|
|
146
|
+
" author?: string;",
|
|
147
|
+
" title?: string;",
|
|
148
|
+
"}",
|
|
149
|
+
"",
|
|
150
|
+
"RegisterKind(Movie, {",
|
|
151
|
+
' group: "example.com",',
|
|
152
|
+
' version: "v1",',
|
|
153
|
+
' kind: "Movie",',
|
|
154
|
+
"});",
|
|
155
|
+
];
|
|
156
|
+
const expectedBookV1 = [
|
|
157
|
+
"// This file is auto-generated by kubernetes-fluent-client, do not edit manually\n",
|
|
158
|
+
'import { GenericKind, RegisterKind } from "kubernetes-fluent-client";\n',
|
|
159
|
+
"/**",
|
|
160
|
+
" * Book nerd",
|
|
161
|
+
" */",
|
|
162
|
+
"export class Book extends GenericKind {",
|
|
163
|
+
" spec?: Spec;",
|
|
164
|
+
"}",
|
|
165
|
+
"",
|
|
166
|
+
"export interface Spec {",
|
|
167
|
+
" author?: string;",
|
|
168
|
+
" title?: string;",
|
|
169
|
+
"}",
|
|
170
|
+
"",
|
|
171
|
+
"RegisterKind(Book, {",
|
|
172
|
+
' group: "example.com",',
|
|
173
|
+
' version: "v1",',
|
|
174
|
+
' kind: "Book",',
|
|
175
|
+
"});",
|
|
176
|
+
];
|
|
177
|
+
const expectedBookV2 = expectedBookV1
|
|
178
|
+
.filter(line => !line.includes("title?"))
|
|
179
|
+
.map(line => line.replace("v1", "v2"));
|
|
180
|
+
(0, globals_1.expect)(actual["movie-v1"]).toEqual(expectedMovie);
|
|
181
|
+
(0, globals_1.expect)(actual["book-v1"]).toEqual(expectedBookV1);
|
|
182
|
+
(0, globals_1.expect)(actual["book-v2"]).toEqual(expectedBookV2);
|
|
183
|
+
});
|
|
184
|
+
(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
|
|
186
|
+
const actual = await (0, generate_1.generate)(options);
|
|
187
|
+
const expectedMovie = [
|
|
188
|
+
"/**",
|
|
189
|
+
" * Movie nerd",
|
|
190
|
+
" */",
|
|
191
|
+
"export interface Movie {",
|
|
192
|
+
" spec?: Spec;",
|
|
193
|
+
"}",
|
|
194
|
+
"",
|
|
195
|
+
"export interface Spec {",
|
|
196
|
+
" author?: string;",
|
|
197
|
+
" title?: string;",
|
|
198
|
+
"}",
|
|
199
|
+
"",
|
|
200
|
+
];
|
|
201
|
+
const expectedBookV1 = [
|
|
202
|
+
"/**",
|
|
203
|
+
" * Book nerd",
|
|
204
|
+
" */",
|
|
205
|
+
"export interface Book {",
|
|
206
|
+
" spec?: Spec;",
|
|
207
|
+
"}",
|
|
208
|
+
"",
|
|
209
|
+
"export interface Spec {",
|
|
210
|
+
" author?: string;",
|
|
211
|
+
" title?: string;",
|
|
212
|
+
"}",
|
|
213
|
+
"",
|
|
214
|
+
];
|
|
215
|
+
const expectedBookV2 = expectedBookV1
|
|
216
|
+
.filter(line => !line.includes("title?"))
|
|
217
|
+
.map(line => line.replace("v1", "v2"));
|
|
218
|
+
(0, globals_1.expect)(actual["movie-v1"]).toEqual(expectedMovie);
|
|
219
|
+
(0, globals_1.expect)(actual["book-v1"]).toEqual(expectedBookV1);
|
|
220
|
+
(0, globals_1.expect)(actual["book-v2"]).toEqual(expectedBookV2);
|
|
221
|
+
});
|
|
222
|
+
(0, globals_1.test)("converts CRD to Go", async () => {
|
|
223
|
+
const options = { source: "test-crd.yaml", language: "go" }; // specify your options
|
|
224
|
+
const actual = await (0, generate_1.generate)(options);
|
|
225
|
+
const expectedMovie = [
|
|
226
|
+
"// Movie nerd",
|
|
227
|
+
"type Movie struct {",
|
|
228
|
+
'\tSpec *Spec `json:"spec,omitempty"`',
|
|
229
|
+
"}",
|
|
230
|
+
"",
|
|
231
|
+
"type Spec struct {",
|
|
232
|
+
'\tAuthor *string `json:"author,omitempty"`',
|
|
233
|
+
'\tTitle *string `json:"title,omitempty"`',
|
|
234
|
+
"}",
|
|
235
|
+
"",
|
|
236
|
+
];
|
|
237
|
+
const expectedBookV1 = [
|
|
238
|
+
"// Book nerd",
|
|
239
|
+
"type Book struct {",
|
|
240
|
+
'\tSpec *Spec `json:"spec,omitempty"`',
|
|
241
|
+
"}",
|
|
242
|
+
"",
|
|
243
|
+
"type Spec struct {",
|
|
244
|
+
'\tAuthor *string `json:"author,omitempty"`',
|
|
245
|
+
'\tTitle *string `json:"title,omitempty"`',
|
|
246
|
+
"}",
|
|
247
|
+
"",
|
|
248
|
+
];
|
|
249
|
+
const expectedBookV2 = expectedBookV1
|
|
250
|
+
.filter(line => !line.includes("Title"))
|
|
251
|
+
.map(line => line.replace("v1", "v2"));
|
|
252
|
+
(0, globals_1.expect)(actual["movie-v1"]).toEqual(expectedMovie);
|
|
253
|
+
(0, globals_1.expect)(actual["book-v1"]).toEqual(expectedBookV1);
|
|
254
|
+
(0, globals_1.expect)(actual["book-v2"]).toEqual(expectedBookV2);
|
|
255
|
+
});
|
|
256
|
+
});
|
package/dist/index.d.ts
CHANGED
|
@@ -5,6 +5,7 @@ export { fetch } from "./fetch";
|
|
|
5
5
|
export { StatusCodes as fetchStatus } from "http-status-codes";
|
|
6
6
|
export { K8s } from "./fluent";
|
|
7
7
|
export { RegisterKind, modelToGroupVersionKind } from "./kinds";
|
|
8
|
+
export { GenericKind } from "./types";
|
|
8
9
|
export * from "./types";
|
|
9
10
|
export * as K8sClientNode from "@kubernetes/client-node";
|
|
10
11
|
//# sourceMappingURL=index.d.ts.map
|
package/dist/index.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAIA,OAAO,KAAK,IAAI,MAAM,YAAY,CAAC;AAEnC,oGAAoG;AACpG,OAAO,EAAE,IAAI,EAAE,CAAC;AAGhB,OAAO,EAAE,KAAK,EAAE,MAAM,SAAS,CAAC;AAGhC,OAAO,EAAE,WAAW,IAAI,WAAW,EAAE,MAAM,mBAAmB,CAAC;AAG/D,OAAO,EAAE,GAAG,EAAE,MAAM,UAAU,CAAC;AAG/B,OAAO,EAAE,YAAY,EAAE,uBAAuB,EAAE,MAAM,SAAS,CAAC;
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAIA,OAAO,KAAK,IAAI,MAAM,YAAY,CAAC;AAEnC,oGAAoG;AACpG,OAAO,EAAE,IAAI,EAAE,CAAC;AAGhB,OAAO,EAAE,KAAK,EAAE,MAAM,SAAS,CAAC;AAGhC,OAAO,EAAE,WAAW,IAAI,WAAW,EAAE,MAAM,mBAAmB,CAAC;AAG/D,OAAO,EAAE,GAAG,EAAE,MAAM,UAAU,CAAC;AAG/B,OAAO,EAAE,YAAY,EAAE,uBAAuB,EAAE,MAAM,SAAS,CAAC;AAGhE,OAAO,EAAE,WAAW,EAAE,MAAM,SAAS,CAAC;AAEtC,cAAc,SAAS,CAAC;AAExB,OAAO,KAAK,aAAa,MAAM,yBAAyB,CAAC"}
|
package/dist/index.js
CHANGED
|
@@ -28,7 +28,7 @@ var __exportStar = (this && this.__exportStar) || function(m, exports) {
|
|
|
28
28
|
for (var p in m) if (p !== "default" && !Object.prototype.hasOwnProperty.call(exports, p)) __createBinding(exports, m, p);
|
|
29
29
|
};
|
|
30
30
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
31
|
-
exports.K8sClientNode = exports.modelToGroupVersionKind = exports.RegisterKind = exports.K8s = exports.fetchStatus = exports.fetch = exports.kind = void 0;
|
|
31
|
+
exports.K8sClientNode = exports.GenericKind = exports.modelToGroupVersionKind = exports.RegisterKind = exports.K8s = exports.fetchStatus = exports.fetch = exports.kind = void 0;
|
|
32
32
|
// Export kinds as a single object
|
|
33
33
|
const kind = __importStar(require("./upstream"));
|
|
34
34
|
exports.kind = kind;
|
|
@@ -45,5 +45,8 @@ Object.defineProperty(exports, "K8s", { enumerable: true, get: function () { ret
|
|
|
45
45
|
var kinds_1 = require("./kinds");
|
|
46
46
|
Object.defineProperty(exports, "RegisterKind", { enumerable: true, get: function () { return kinds_1.RegisterKind; } });
|
|
47
47
|
Object.defineProperty(exports, "modelToGroupVersionKind", { enumerable: true, get: function () { return kinds_1.modelToGroupVersionKind; } });
|
|
48
|
+
// Export the GenericKind interface for CRD registration
|
|
49
|
+
var types_1 = require("./types");
|
|
50
|
+
Object.defineProperty(exports, "GenericKind", { enumerable: true, get: function () { return types_1.GenericKind; } });
|
|
48
51
|
__exportStar(require("./types"), exports);
|
|
49
52
|
exports.K8sClientNode = __importStar(require("@kubernetes/client-node"));
|
package/package.json
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "kubernetes-fluent-client",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.6.0",
|
|
4
4
|
"description": "A @kubernetes/client-node fluent API wrapper that leverages K8s Server Side Apply",
|
|
5
|
+
"bin": "./dist/cli.js",
|
|
5
6
|
"main": "dist/index.js",
|
|
6
7
|
"types": "dist/index.d.ts",
|
|
7
8
|
"scripts": {
|
|
@@ -39,18 +40,22 @@
|
|
|
39
40
|
"fast-json-patch": "3.1.1",
|
|
40
41
|
"http-status-codes": "2.3.0",
|
|
41
42
|
"node-fetch": "2.7.0",
|
|
42
|
-
"
|
|
43
|
+
"quicktype-core": "23.0.76",
|
|
44
|
+
"type-fest": "4.4.0",
|
|
45
|
+
"yargs": "17.7.2"
|
|
43
46
|
},
|
|
44
47
|
"devDependencies": {
|
|
45
48
|
"@commitlint/cli": "17.7.2",
|
|
46
49
|
"@commitlint/config-conventional": "17.7.0",
|
|
47
50
|
"@jest/globals": "29.7.0",
|
|
48
51
|
"@types/byline": "4.2.34",
|
|
49
|
-
"@
|
|
50
|
-
"@
|
|
52
|
+
"@types/readable-stream": "4.0.3",
|
|
53
|
+
"@types/yargs": "17.0.28",
|
|
54
|
+
"@typescript-eslint/eslint-plugin": "6.7.5",
|
|
55
|
+
"@typescript-eslint/parser": "6.7.5",
|
|
51
56
|
"eslint-plugin-jsdoc": "46.8.2",
|
|
52
57
|
"jest": "29.7.0",
|
|
53
|
-
"nock": "13.3.
|
|
58
|
+
"nock": "13.3.4",
|
|
54
59
|
"prettier": "3.0.3",
|
|
55
60
|
"semantic-release": "22.0.5",
|
|
56
61
|
"ts-jest": "29.1.1",
|
package/src/cli.ts
ADDED
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
// SPDX-License-Identifier: Apache-2.0
|
|
4
|
+
// SPDX-FileCopyrightText: 2023-Present The Kubernetes Fluent Client Authors
|
|
5
|
+
|
|
6
|
+
import { hideBin } from "yargs/helpers";
|
|
7
|
+
import yargs from "yargs/yargs";
|
|
8
|
+
import { GenerateOptions, generate } from "./generate";
|
|
9
|
+
|
|
10
|
+
void yargs(hideBin(process.argv))
|
|
11
|
+
.version(false)
|
|
12
|
+
.command(
|
|
13
|
+
"crd [source] [directory]",
|
|
14
|
+
"generate usable types from a K8s CRD",
|
|
15
|
+
yargs => {
|
|
16
|
+
return yargs
|
|
17
|
+
.positional("source", {
|
|
18
|
+
describe: "the yaml file path, remote url, or K8s CRD name",
|
|
19
|
+
type: "string",
|
|
20
|
+
})
|
|
21
|
+
.positional("directory", {
|
|
22
|
+
describe: "the directory to output the generated types to",
|
|
23
|
+
type: "string",
|
|
24
|
+
})
|
|
25
|
+
.option("plain", {
|
|
26
|
+
alias: "p",
|
|
27
|
+
type: "boolean",
|
|
28
|
+
description:
|
|
29
|
+
"generate plain types without binding to the fluent client, automatically enabled when an alternate language is specified",
|
|
30
|
+
})
|
|
31
|
+
.option("language", {
|
|
32
|
+
alias: "l",
|
|
33
|
+
type: "string",
|
|
34
|
+
default: "ts",
|
|
35
|
+
description:
|
|
36
|
+
"the language to generate types in, see https://github.com/glideapps/quicktype#target-languages for a list of supported languages",
|
|
37
|
+
})
|
|
38
|
+
.demandOption(["source", "directory"]);
|
|
39
|
+
},
|
|
40
|
+
argv => {
|
|
41
|
+
void generate(argv as GenerateOptions);
|
|
42
|
+
},
|
|
43
|
+
)
|
|
44
|
+
.parse();
|
package/src/fetch.test.ts
CHANGED
package/src/fetch.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
// SPDX-License-Identifier: Apache-2.0
|
|
2
|
-
// SPDX-FileCopyrightText: 2023-Present The
|
|
2
|
+
// SPDX-FileCopyrightText: 2023-Present The Kubernetes Fluent Client Authors
|
|
3
3
|
|
|
4
4
|
import { StatusCodes } from "http-status-codes";
|
|
5
5
|
import fetchRaw, { FetchError, RequestInfo, RequestInit } from "node-fetch";
|
package/src/fluent/apply.ts
CHANGED
package/src/fluent/index.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
// SPDX-License-Identifier: Apache-2.0
|
|
2
|
-
// SPDX-FileCopyrightText: 2023-Present The
|
|
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";
|
package/src/fluent/types.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
// SPDX-License-Identifier: Apache-2.0
|
|
2
|
-
// SPDX-FileCopyrightText: 2023-Present The
|
|
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";
|
package/src/fluent/utils.test.ts
CHANGED
package/src/fluent/utils.ts
CHANGED
package/src/fluent/watch.ts
CHANGED
|
@@ -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
|
+
});
|
package/src/generate.ts
ADDED
|
@@ -0,0 +1,189 @@
|
|
|
1
|
+
// SPDX-License-Identifier: Apache-2.0
|
|
2
|
+
// SPDX-FileCopyrightText: 2023-Present The Kubernetes Fluent Client Authors
|
|
3
|
+
|
|
4
|
+
import { loadAllYaml } from "@kubernetes/client-node";
|
|
5
|
+
import * as fs from "fs";
|
|
6
|
+
import * as path from "path";
|
|
7
|
+
import {
|
|
8
|
+
FetchingJSONSchemaStore,
|
|
9
|
+
InputData,
|
|
10
|
+
JSONSchemaInput,
|
|
11
|
+
TargetLanguage,
|
|
12
|
+
quicktype,
|
|
13
|
+
} from "quicktype-core";
|
|
14
|
+
|
|
15
|
+
import { fetch } from "./fetch";
|
|
16
|
+
import { K8s } from "./fluent";
|
|
17
|
+
import { CustomResourceDefinition } from "./upstream";
|
|
18
|
+
|
|
19
|
+
export interface GenerateOptions {
|
|
20
|
+
/** The source URL, yaml file path or K8s CRD name */
|
|
21
|
+
source: string;
|
|
22
|
+
/** The output directory path */
|
|
23
|
+
directory?: string;
|
|
24
|
+
/** Disable kubernetes-fluent-client wrapping */
|
|
25
|
+
plain?: boolean;
|
|
26
|
+
/** The language to generate types in */
|
|
27
|
+
language?: string | TargetLanguage;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Converts a CustomResourceDefinition to TypeScript types
|
|
32
|
+
*
|
|
33
|
+
* @param crd The CustomResourceDefinition to convert
|
|
34
|
+
* @param opts The options to use when converting
|
|
35
|
+
* @returns A promise that resolves when the CustomResourceDefinition has been converted
|
|
36
|
+
*/
|
|
37
|
+
async function convertCRDtoTS(
|
|
38
|
+
crd: CustomResourceDefinition,
|
|
39
|
+
opts: GenerateOptions,
|
|
40
|
+
): Promise<Record<string, string[]>> {
|
|
41
|
+
// Get the name of the kind
|
|
42
|
+
const name = crd.spec.names.kind;
|
|
43
|
+
|
|
44
|
+
const results: Record<string, string[]> = {};
|
|
45
|
+
|
|
46
|
+
for (const match of crd.spec.versions) {
|
|
47
|
+
// Get the schema from the matched version
|
|
48
|
+
const schema = JSON.stringify(match?.schema?.openAPIV3Schema);
|
|
49
|
+
|
|
50
|
+
// Create a new JSONSchemaInput
|
|
51
|
+
const schemaInput = new JSONSchemaInput(new FetchingJSONSchemaStore());
|
|
52
|
+
|
|
53
|
+
// Add the schema to the input
|
|
54
|
+
await schemaInput.addSource({ name, schema });
|
|
55
|
+
|
|
56
|
+
// Create a new InputData object
|
|
57
|
+
const inputData = new InputData();
|
|
58
|
+
inputData.addInput(schemaInput);
|
|
59
|
+
|
|
60
|
+
// Generate the types
|
|
61
|
+
const out = await quicktype({
|
|
62
|
+
inputData,
|
|
63
|
+
lang: opts.language || "ts",
|
|
64
|
+
rendererOptions: { "just-types": "true" },
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
let processedLines = out.lines;
|
|
68
|
+
|
|
69
|
+
// If using typescript, remove the line containing `[property: string]: any;`
|
|
70
|
+
if (opts.language === "ts" || opts.language === "typescript") {
|
|
71
|
+
processedLines = out.lines.filter(line => !line.includes("[property: string]: any;"));
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
// If the language is TypeScript and plain is not specified, wire up the fluent client
|
|
75
|
+
if (opts.language === "ts" && !opts.plain) {
|
|
76
|
+
processedLines.unshift(
|
|
77
|
+
// Add warning that the file is auto-generated
|
|
78
|
+
`// This file is auto-generated by kubernetes-fluent-client, do not edit manually\n`,
|
|
79
|
+
// Add the imports before any other lines
|
|
80
|
+
`import { GenericKind, RegisterKind } from "kubernetes-fluent-client";\n`,
|
|
81
|
+
);
|
|
82
|
+
|
|
83
|
+
// Replace the interface with a named class that extends GenericKind
|
|
84
|
+
const entryIdx = processedLines.findIndex(line =>
|
|
85
|
+
line.includes(`export interface ${name} {`),
|
|
86
|
+
);
|
|
87
|
+
|
|
88
|
+
// Replace the interface with a named class that extends GenericKind
|
|
89
|
+
processedLines[entryIdx] = `export class ${name} extends GenericKind {`;
|
|
90
|
+
|
|
91
|
+
// Add the RegisterKind call
|
|
92
|
+
processedLines.push(`RegisterKind(${name}, {`);
|
|
93
|
+
processedLines.push(` group: "${crd.spec.group}",`);
|
|
94
|
+
processedLines.push(` version: "${match.name}",`);
|
|
95
|
+
processedLines.push(` kind: "${name}",`);
|
|
96
|
+
processedLines.push(`});`);
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
const finalContents = processedLines.join("\n");
|
|
100
|
+
const fileName = `${name.toLowerCase()}-${match.name.toLowerCase()}`;
|
|
101
|
+
|
|
102
|
+
// If an output file is specified, write the output to the file
|
|
103
|
+
if (opts.directory) {
|
|
104
|
+
// Create the directory if it doesn't exist
|
|
105
|
+
fs.mkdirSync(opts.directory, { recursive: true });
|
|
106
|
+
|
|
107
|
+
// Write the file
|
|
108
|
+
const filePath = path.join(opts.directory, `${fileName}.${opts.language}`);
|
|
109
|
+
fs.writeFileSync(filePath, finalContents);
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
// Add the results to the array
|
|
113
|
+
results[fileName] = processedLines;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
return results;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/**
|
|
120
|
+
* Reads a CustomResourceDefinition from a file, the cluster or the internet
|
|
121
|
+
*
|
|
122
|
+
* @param source The source to read from (file path, cluster or URL)
|
|
123
|
+
* @returns A promise that resolves when the CustomResourceDefinition has been read
|
|
124
|
+
*/
|
|
125
|
+
async function readOrFetchCrd(source: string): Promise<CustomResourceDefinition[]> {
|
|
126
|
+
const filePath = path.join(process.cwd(), source);
|
|
127
|
+
|
|
128
|
+
// First try to read the source as a file
|
|
129
|
+
try {
|
|
130
|
+
if (fs.existsSync(filePath)) {
|
|
131
|
+
const payload = fs.readFileSync(filePath, "utf8");
|
|
132
|
+
return loadAllYaml(payload) as CustomResourceDefinition[];
|
|
133
|
+
}
|
|
134
|
+
} catch (e) {
|
|
135
|
+
// Ignore errors
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
// Next try to parse the source as a URL
|
|
139
|
+
try {
|
|
140
|
+
const url = new URL(source);
|
|
141
|
+
|
|
142
|
+
// If the source is a URL, fetch it
|
|
143
|
+
if (url.protocol === "http:" || url.protocol === "https:") {
|
|
144
|
+
const { ok, data } = await fetch<string>(source);
|
|
145
|
+
|
|
146
|
+
// If the request failed, throw an error
|
|
147
|
+
if (!ok) {
|
|
148
|
+
throw new Error(`Failed to fetch ${source}: ${data}`);
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
return loadAllYaml(data) as CustomResourceDefinition[];
|
|
152
|
+
}
|
|
153
|
+
} catch (e) {
|
|
154
|
+
// Ignore errors
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
// Finally, if the source is not a file or URL, try to read it as a CustomResourceDefinition from the cluster
|
|
158
|
+
try {
|
|
159
|
+
return [await K8s(CustomResourceDefinition).Get(source)];
|
|
160
|
+
} catch (e) {
|
|
161
|
+
throw new Error(`Failed to read ${source} as a file, url or K8s CRD: ${e}`);
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
/**
|
|
166
|
+
* Generate TypeScript types from a K8s CRD
|
|
167
|
+
*
|
|
168
|
+
* @param opts The options to use when generating
|
|
169
|
+
* @returns A promise that resolves when the TypeScript types have been generated
|
|
170
|
+
*/
|
|
171
|
+
export async function generate(opts: GenerateOptions) {
|
|
172
|
+
const crds = await readOrFetchCrd(opts.source);
|
|
173
|
+
const results: Record<string, string[]> = {};
|
|
174
|
+
|
|
175
|
+
for (const crd of crds) {
|
|
176
|
+
if (!crd || crd.kind !== "CustomResourceDefinition" || !crd.spec?.versions?.length) {
|
|
177
|
+
// Ignore empty and non-CRD objects
|
|
178
|
+
continue;
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
// Add the results to the record
|
|
182
|
+
const out = await convertCRDtoTS(crd, opts);
|
|
183
|
+
for (const key of Object.keys(out)) {
|
|
184
|
+
results[key] = out[key];
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
return results;
|
|
189
|
+
}
|
package/src/index.ts
CHANGED
|
@@ -19,6 +19,9 @@ export { K8s } from "./fluent";
|
|
|
19
19
|
// Export helpers for working with K8s types
|
|
20
20
|
export { RegisterKind, modelToGroupVersionKind } from "./kinds";
|
|
21
21
|
|
|
22
|
+
// Export the GenericKind interface for CRD registration
|
|
23
|
+
export { GenericKind } from "./types";
|
|
24
|
+
|
|
22
25
|
export * from "./types";
|
|
23
26
|
|
|
24
27
|
export * as K8sClientNode from "@kubernetes/client-node";
|