kubernetes-fluent-client
Version:
A @kubernetes/client-node fluent API wrapper that leverages K8s Server Side Apply.
257 lines (256 loc) ⢠10.3 kB
JavaScript
// SPDX-License-Identifier: Apache-2.0
// SPDX-FileCopyrightText: 2023-Present The Kubernetes Fluent Client Authors
import * as fs from "fs";
import * as path from "path";
import { GenericKind } from "./types.js";
import { modifyAndNormalizeClassProperties, normalizeIndentationAndSpacing, } from "./normalization.js";
const genericKindProperties = getGenericKindProperties();
/**
* Performs post-processing on generated TypeScript files.
*
* @param allResults The array of CRD results.
* @param opts The options for post-processing.
*/
export async function postProcessing(allResults, opts) {
if (!opts.directory) {
opts.logFn("ā ļø Error: Directory is not defined.");
return;
}
const files = fs.readdirSync(opts.directory);
opts.logFn("\nš§ Post-processing started...");
const fileResultMap = mapFilesToCRD(allResults);
await processFiles(files, fileResultMap, opts);
opts.logFn("š§ Post-processing completed.\n");
}
/**
* Creates a map linking each file to its corresponding CRD result.
*
* @param allResults - The array of CRD results.
* @returns A map linking file names to their corresponding CRD results.
*/
export function mapFilesToCRD(allResults) {
const fileResultMap = {};
for (const { name, crd, version } of allResults) {
const expectedFileName = `${name.toLowerCase()}-${version.toLowerCase()}.ts`;
fileResultMap[expectedFileName] = { name, crd, version };
}
if (Object.keys(fileResultMap).length === 0) {
console.warn("ā ļø Warning: No CRD results were mapped to files.");
}
return fileResultMap;
}
/**
* Processes the list of files, applying CRD post-processing to each.
*
* @param files - The list of file names to process.
* @param fileResultMap - A map linking file names to their corresponding CRD results.
* @param opts - Options for the generation process.
*/
export async function processFiles(files, fileResultMap, opts) {
for (const file of files) {
if (!opts.directory) {
throw new Error("Directory is not defined.");
}
const filePath = path.join(opts.directory, file);
const fileResult = fileResultMap[file];
if (!fileResult) {
opts.logFn(`ā ļø Warning: No matching CRD result found for file: ${filePath}`);
continue;
}
try {
processAndModifySingleFile(filePath, fileResult, opts);
}
catch (error) {
logError(error, filePath, opts.logFn);
}
}
}
/**
* Handles the processing of a single file: reading, modifying, and writing back to disk.
*
* @param filePath - The path to the file to be processed.
* @param fileResult - The associated CRD result for this file.
* @param fileResult.name - The name of the schema.
* @param fileResult.crd - The CustomResourceDefinition object.
* @param fileResult.version - The version of the CRD.
* @param opts - Options for the generation process.
*/
export function processAndModifySingleFile(filePath, fileResult, opts) {
opts.logFn(`š Processing file: ${filePath}`);
const { name, crd, version } = fileResult;
let fileContent;
try {
fileContent = fs.readFileSync(filePath, "utf8");
}
catch (error) {
logError(error, filePath, opts.logFn);
return;
}
let modifiedContent;
try {
modifiedContent = applyCRDPostProcessing(fileContent, name, crd, version, opts);
}
catch (error) {
logError(error, filePath, opts.logFn);
return;
}
try {
fs.writeFileSync(filePath, modifiedContent);
opts.logFn(`ā
Successfully processed and wrote file: ${filePath}`);
}
catch (error) {
logError(error, filePath, opts.logFn);
}
}
/**
* Processes the TypeScript file content, applying wrapping and property modifications.
*
* @param content The content of the TypeScript file.
* @param name The name of the schema.
* @param crd The CustomResourceDefinition object.
* @param version The version of the CRD.
* @param opts The options for processing.
* @returns The processed TypeScript file content.
*/
export function applyCRDPostProcessing(content, name, crd, version, opts) {
try {
let lines = content.split("\n");
// Wraps with the fluent client if needed
if (opts.language === "ts" && !opts.plain) {
lines = wrapWithFluentClient(lines, name, crd, version, opts.npmPackage);
}
const foundInterfaces = collectInterfaceNames(lines);
// Process the lines, focusing on classes extending `GenericKind`
const processedLines = processLines(lines, genericKindProperties, foundInterfaces);
// Normalize the final output
const normalizedLines = normalizeIndentationAndSpacing(processedLines, opts);
return normalizedLines.join("\n");
}
catch (error) {
throw new Error(`Error while applying post-processing for ${name}: ${error.message}`);
}
}
/**
* Retrieves the properties of the `GenericKind` class, excluding dynamic properties like `[key: string]: any`.
*
* @returns An array of property names that belong to `GenericKind`.
*/
export function getGenericKindProperties() {
// Ensure we always include standard Kubernetes resource properties
const standardProperties = ["kind", "apiVersion", "metadata"];
// Get actual properties from GenericKind
const instanceProperties = Object.getOwnPropertyNames(new GenericKind()).filter(prop => prop !== "[key: string]");
// Combine both sets of properties, removing duplicates
return Array.from(new Set([...standardProperties, ...instanceProperties]));
}
/**
* Collects interface names from TypeScript file lines.
*
* @param lines The lines of the file content.
* @returns A set of found interface names.
*/
export function collectInterfaceNames(lines) {
// https://regex101.com/r/S6w8pW/1
const interfacePattern = /export interface (?<interfaceName>\w+)/;
const foundInterfaces = new Set();
for (const line of lines) {
const match = line.match(interfacePattern);
if (match?.groups?.interfaceName) {
foundInterfaces.add(match.groups.interfaceName);
}
}
return foundInterfaces;
}
/**
* Identifies whether a line declares a class that extends `GenericKind`.
*
* @param line The current line of code.
* @returns True if the line defines a class that extends `GenericKind`, false otherwise.
*/
export function isClassExtendingGenericKind(line) {
return line.includes("class") && line.includes("extends GenericKind");
}
/**
* Adjusts the brace balance to determine if the parser is within a class definition.
*
* @param line The current line of code.
* @param braceBalance The current balance of curly braces.
* @returns The updated brace balance.
*/
export function updateBraceBalance(line, braceBalance) {
return braceBalance + (line.includes("{") ? 1 : 0) - (line.includes("}") ? 1 : 0);
}
/**
* Wraps the generated TypeScript file with fluent client elements (`GenericKind` and `RegisterKind`).
*
* @param lines The generated TypeScript lines.
* @param name The name of the schema.
* @param crd The CustomResourceDefinition object.
* @param version The version of the CRD.
* @param npmPackage The NPM package name for the fluent client.
* @returns The processed TypeScript lines.
*/
export function wrapWithFluentClient(lines, name, crd, version, npmPackage = "kubernetes-fluent-client") {
const autoGenNotice = `// This file is auto-generated by ${npmPackage}, do not edit manually`;
const imports = `import { GenericKind, RegisterKind } from "${npmPackage}";`;
const classIndex = lines.findIndex(line => line.includes(`export interface ${name} {`));
if (classIndex !== -1) {
lines[classIndex] = `export class ${name} extends GenericKind {`;
}
lines.unshift(autoGenNotice, imports);
lines.push(`RegisterKind(${name}, {`, ` group: "${crd.spec.group}",`, ` version: "${version}",`, ` kind: "${name}",`, ` plural: "${crd.spec.names.plural}",`, `});`);
return lines;
}
/**
* Processes the lines of the TypeScript file, focusing on classes extending `GenericKind`.
*
* @param lines The lines of the file content.
* @param genericKindProperties The list of properties from `GenericKind`.
* @param foundInterfaces The set of found interfaces in the file.
* @returns The processed lines.
*/
export function processLines(lines, genericKindProperties, foundInterfaces) {
let insideClass = false;
let braceBalance = 0;
return lines.map(line => {
const result = processClassContext(line, insideClass, braceBalance, genericKindProperties, foundInterfaces);
insideClass = result.insideClass;
braceBalance = result.braceBalance;
return result.line;
});
}
/**
* Processes a single line inside a class extending `GenericKind`.
*
* @param line The current line of code.
* @param insideClass Whether we are inside a class context.
* @param braceBalance The current brace balance to detect when we exit the class.
* @param genericKindProperties The list of properties from `GenericKind`.
* @param foundInterfaces The set of found interfaces in the file.
* @returns An object containing the updated line, updated insideClass flag, and braceBalance.
*/
export function processClassContext(line, insideClass, braceBalance, genericKindProperties, foundInterfaces) {
if (isClassExtendingGenericKind(line)) {
insideClass = true;
braceBalance = 0;
}
if (!insideClass)
return { line, insideClass, braceBalance };
braceBalance = updateBraceBalance(line, braceBalance);
line = modifyAndNormalizeClassProperties(line, genericKindProperties, foundInterfaces);
if (braceBalance === 0) {
insideClass = false;
}
return { line, insideClass, braceBalance };
}
/**
* Handles logging for errors with stack trace.
*
* @param error The error object to log.
* @param filePath The path of the file being processed.
* @param logFn The logging function.
*/
export function logError(error, filePath, logFn) {
logFn(`ā Error processing file: ${filePath} - ${error.message}`);
logFn(`Stack trace: ${error.stack}`);
}