kubernetes-fluent-client
Version:
A @kubernetes/client-node fluent API wrapper that leverages K8s Server Side Apply.
174 lines (173 loc) • 6.69 kB
JavaScript
// SPDX-License-Identifier: Apache-2.0
// SPDX-FileCopyrightText: 2023-Present The Kubernetes Fluent Client Authors
import { loadAllYaml } from "@kubernetes/client-node";
import * as fs from "fs";
import * as path from "path";
import { FetchingJSONSchemaStore, InputData, JSONSchemaInput, quicktype, } from "quicktype-core";
import { fetch } from "./fetch.js";
import { K8s } from "./fluent/index.js";
import { CustomResourceDefinition } from "./upstream.js";
/**
* Converts a CustomResourceDefinition to TypeScript types
*
* @param crd - The CustomResourceDefinition object to convert.
* @param opts - The options for generating the TypeScript types.
* @returns A promise that resolves to a record of generated TypeScript types.
*/
export async function convertCRDtoTS(crd, opts) {
const name = crd.spec.names.kind;
const results = {};
const output = [];
// Check for missing versions or empty schema
if (!crd.spec.versions || crd.spec.versions.length === 0) {
opts.logFn(`Skipping ${crd.metadata?.name}, it does not appear to be a CRD`);
return [];
}
// Iterate through each version of the CRD
for (const match of crd.spec.versions) {
if (!match.schema?.openAPIV3Schema) {
opts.logFn(`Skipping ${crd.metadata?.name ?? "unknown"}, it does not appear to have a valid schema`);
continue;
}
const schema = JSON.stringify(match.schema.openAPIV3Schema);
opts.logFn(`- Generating ${crd.spec.group}/${match.name} types for ${name}`);
const inputData = await prepareInputData(name, schema);
const generatedTypes = await generateTypes(inputData, opts);
const fileName = `${name.toLowerCase()}-${match.name.toLowerCase()}`;
writeGeneratedFile(fileName, opts.directory || "", generatedTypes, opts.language || "ts");
results[fileName] = generatedTypes;
output.push({ results, name, crd, version: match.name });
}
return output;
}
/**
* Prepares the input data for quicktype from the provided schema.
*
* @param name - The name of the schema.
* @param schema - The JSON schema as a string.
* @returns A promise that resolves to the input data for quicktype.
*/
export async function prepareInputData(name, schema) {
// Create a new JSONSchemaInput
const schemaInput = new JSONSchemaInput(new FetchingJSONSchemaStore());
// Add the schema to the input
await schemaInput.addSource({ name, schema });
// Create a new InputData object
const inputData = new InputData();
inputData.addInput(schemaInput);
return inputData;
}
/**
* Generates TypeScript types using quicktype.
*
* @param inputData - The input data for quicktype.
* @param opts - The options for generating the TypeScript types.
* @returns A promise that resolves to an array of generated TypeScript type lines.
*/
export async function generateTypes(inputData, opts) {
// Generate the types
const out = await quicktype({
inputData,
lang: opts.language,
rendererOptions: { "just-types": "true" },
});
return out.lines;
}
/**
* Writes the processed lines to the output file.
*
* @param fileName - The name of the file to write.
* @param directory - The directory where the file will be written.
* @param content - The content to write to the file.
* @param language - The programming language of the file.
*/
export function writeGeneratedFile(fileName, directory, content, language) {
language = language || "ts";
if (!directory)
return;
const filePath = path.join(directory, `${fileName}.${language}`);
fs.mkdirSync(directory, { recursive: true });
fs.writeFileSync(filePath, content.join("\n"));
}
/**
* Reads or fetches a CustomResourceDefinition from a file, URL, or the cluster.
*
* @param opts - The options for generating the TypeScript types.
* @returns A promise that resolves to an array of CustomResourceDefinition objects.
*/
export async function readOrFetchCrd(opts) {
try {
const filePath = resolveFilePath(opts.source);
if (fs.existsSync(filePath)) {
opts.logFn(`Attempting to load ${opts.source} as a local file`);
const content = fs.readFileSync(filePath, "utf8");
return loadAllYaml(content);
}
const url = tryParseUrl(opts.source);
if (url) {
opts.logFn(`Attempting to load ${opts.source} as a URL`);
const { ok, data } = await fetch(url.href);
if (ok) {
return loadAllYaml(data);
}
}
// Fallback to Kubernetes cluster
opts.logFn(`Attempting to read ${opts.source} from the Kubernetes cluster`);
return [await K8s(CustomResourceDefinition).Get(opts.source)];
}
catch (error) {
opts.logFn(`Error loading CRD: ${error.message}`);
throw new Error(`Failed to read ${opts.source} as a file, URL, or Kubernetes CRD`);
}
}
/**
* Resolves the source file path, treating relative paths as local files.
*
* @param source - The source path to resolve.
* @returns The resolved file path.
*/
export function resolveFilePath(source) {
return source.startsWith("/") ? source : path.join(process.cwd(), source);
}
/**
* Tries to parse the source as a URL.
*
* @param source - The source string to parse as a URL.
* @returns The parsed URL object or null if parsing fails.
*/
export function tryParseUrl(source) {
try {
return new URL(source);
}
catch {
return null;
}
}
/**
* Main generate function to convert CRDs to TypeScript types.
*
* @param opts - The options for generating the TypeScript types.
* @returns A promise that resolves to a record of generated TypeScript types.
*/
export async function generate(opts) {
const crds = (await readOrFetchCrd(opts)).filter(crd => !!crd);
const allResults = [];
opts.logFn("");
for (const crd of crds) {
if (crd.kind !== "CustomResourceDefinition" || !crd.spec?.versions?.length) {
opts.logFn(`Skipping ${crd?.metadata?.name}, it does not appear to be a CRD`);
// Ignore empty and non-CRD objects
continue;
}
allResults.push(...(await convertCRDtoTS(crd, opts)));
}
if (opts.directory) {
// Notify the user that the files have been generated
opts.logFn(`\n✅ Generated ${allResults.length} files in the ${opts.directory} directory`);
}
else {
// Log a message about the number of generated files even when no directory is provided
opts.logFn(`\n✅ Generated ${allResults.length} files`);
}
return allResults;
}