Files
grafana/scripts/process-specs.ts
Ashley Harrison c94b8b78ae API client generation: Add verify-api-clients step to drone (#99987)
* add verify-api-clients step to drone

* change to check the verify step fails

* ignore conf dir

* rerun make drone

* undo change to make step fail
2025-02-04 09:13:44 +00:00

179 lines
5.7 KiB
TypeScript

import fs from 'fs';
import { OpenAPIV3 } from 'openapi-types';
import path from 'path';
/**
* Process an OpenAPI spec to remove k8s metadata from names and paths:
* - Remove paths containing "/watch/" as they're deprecated.
* - Remove 'ForAllNamespaces' endpoints
* - Remove the prefix: "/apis/<group>/<version>/namespaces/{namespace}" from paths.
* - Filter out `namespace` from path parameters.
* - Update all $ref fields to remove k8s metadata from schema names.
* - Simplify schema names in "components.schemas".
*/
function processOpenAPISpec(spec: OpenAPIV3.Document) {
// Create a deep copy of the spec to avoid mutating the original
const newSpec = JSON.parse(JSON.stringify(spec));
// Process 'paths' property
const newPaths: Record<string, unknown> = {};
for (const [path, pathItem] of Object.entries<OpenAPIV3.PathItemObject>(newSpec.paths)) {
// Remove empty path items
if (!pathItem) {
continue;
}
// Remove the specified part from the path key
const newPathKey = path.replace(/^\/apis\/[^\/]+\/[^\/]+\/namespaces\/\{namespace}/, '');
// Process each method in the path (e.g., get, post)
const newPathItem: Record<string, unknown> = {};
// Filter out namespace parameter at path level
if (Array.isArray(pathItem.parameters)) {
pathItem.parameters = filterNamespaceParameters(pathItem.parameters);
}
for (const method of Object.keys(pathItem)) {
// eslint-disable-next-line @typescript-eslint/consistent-type-assertions
const operation = pathItem[method as keyof OpenAPIV3.PathItemObject];
if (
typeof operation === 'object' &&
operation !== null &&
'operationId' in operation &&
operation.operationId?.includes('ForAllNamespaces')
) {
continue;
}
// Filter out namespace parameter at operation level
if (
operation &&
typeof operation === 'object' &&
'parameters' in operation &&
Array.isArray(operation.parameters)
) {
operation.parameters = filterNamespaceParameters(operation.parameters);
}
updateRefs(operation);
newPathItem[method] = operation;
}
newPaths[newPathKey] = newPathItem;
}
newSpec.paths = newPaths;
// Process 'components.schemas', i.e., type definitions
const newSchemas: Record<string, unknown> = {};
for (const schemaKey of Object.keys(newSpec.components.schemas)) {
const newKey = simplifySchemaName(schemaKey);
const schemaObject = newSpec.components.schemas[schemaKey];
updateRefs(schemaObject);
newSchemas[newKey] = schemaObject;
}
newSpec.components.schemas = newSchemas;
return newSpec;
}
/**
* Filter out namespace parameters from an array of parameters
*/
function filterNamespaceParameters(parameters: Array<OpenAPIV3.ReferenceObject | OpenAPIV3.ParameterObject>) {
return parameters.filter((param) => 'name' in param && param.name !== 'namespace');
}
/**
* Recursively update all $ref fields to remove k8s metadata from names
*/
function updateRefs(obj: unknown) {
if (Array.isArray(obj)) {
for (const item of obj) {
updateRefs(item);
}
} else if (typeof obj === 'object' && obj !== null) {
if ('$ref' in obj && typeof obj.$ref === 'string') {
const refParts = obj.$ref.split('/');
const lastRefPart = refParts[refParts.length - 1];
const newRefName = simplifySchemaName(lastRefPart);
obj.$ref = `#/components/schemas/${newRefName}`;
}
for (const key in obj) {
if (key !== '$ref') {
// eslint-disable-next-line @typescript-eslint/consistent-type-assertions
updateRefs(obj[key as keyof typeof obj]);
}
}
}
}
/**
* Simplify a schema name by removing the version prefix if present.
* For example, 'io.k8s.apimachinery.pkg.apis.meta.v1.Time' becomes 'Time'.
*/
function simplifySchemaName(schemaName: string) {
const parts = schemaName.split('.');
// Regex to match version segments like 'v1', 'v1beta1', 'v0alpha1', etc.
const versionRegex = /^v\d+[a-zA-Z0-9]*$/;
const versionIndex = parts.findIndex((part) => versionRegex.test(part));
if (versionIndex !== -1 && versionIndex + 1 < parts.length) {
return parts.slice(versionIndex + 1).join('.');
} else {
return schemaName;
}
}
/**
* Process all files in a source directory and write results to output directory
*/
function processDirectory(sourceDir: string, outputDir: string) {
// Skip if source directory doesn't exist
if (!fs.existsSync(sourceDir)) {
return;
}
// Create the output directory if it doesn't exist
if (!fs.existsSync(outputDir)) {
fs.mkdirSync(outputDir, { recursive: true });
}
const files = fs.readdirSync(sourceDir).filter((file: string) => file.endsWith('.json'));
for (const file of files) {
const inputPath = path.join(sourceDir, file);
const outputPath = path.join(outputDir, file);
console.log(`Processing file "${file}"...`);
const fileContent = fs.readFileSync(inputPath, 'utf-8');
let inputSpec;
try {
inputSpec = JSON.parse(fileContent);
} catch (err) {
console.error(`Invalid JSON file "${file}". Skipping this file.`);
continue;
}
const outputSpec = processOpenAPISpec(inputSpec);
fs.writeFileSync(outputPath, JSON.stringify(outputSpec, null, 2), 'utf-8');
console.log(`Processing completed for file "${file}".`);
}
}
const sourceDirs = [
path.resolve(import.meta.dirname, '../pkg/tests/apis/openapi_snapshots'),
path.resolve(import.meta.dirname, '../pkg/extensions/apiserver/tests/openapi_snapshots'),
];
const outputDir = path.resolve(import.meta.dirname, '../data/openapi');
for (const sourceDir of sourceDirs) {
processDirectory(sourceDir, outputDir);
}