The open and composable observability and data visualization platform. Visualize metrics, logs, and traces from multiple sources like Prometheus, Loki, Elasticsearch, InfluxDB, Postgres and many more.
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 
 
 
grafana/scripts/process-specs.ts

178 lines
5.7 KiB

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);
}