[Automatic Import] bug fixes around openapi spec parsing for CEL generation (#212145)

## Summary

This PR fixes a couple of things with regards to the parsing of the
OpenAPI spec for use in CEL generation:
1) fixes and greatly simplifies the parsing of the OpenAPI spec so that
we collect all the $ref tags in the response object
2) only collects the top level schemas from the response object (since
that's all we really need for the CEL program)
3) fixes it so that users cannot select 'Save configuration' if there is
a generation error
4) better error messaging if/when a spec parsing error occurs

Note re fix # 3, the 'Save configuration' button will still initially be
available upon an error occurring. Then when if the user tries to click
save after an error, it will then disable the save button and show the
message indicating they need a successful generation to save. This is
consistent with the UX philosophy in the rest of the flyout that all
buttons are enabled by default, and if the user does something 'wrong',
we then provide guidance for how to proceed.

Relates: https://github.com/elastic/kibana/issues/210271

## Screenshots

<details>
  <summary>parsing fix</summary>
<img width="450" alt="Screenshot 2025-02-21 at 2 15 34 PM"
src="https://github.com/user-attachments/assets/80fe8e56-ffe3-4d5c-b6ac-5a57e025b70b"
/>

</details>

<details>
  <summary>save disabled fix</summary>
<img width="450" alt="Screenshot 2025-02-21 at 2 13 45 PM"
src="https://github.com/user-attachments/assets/5220bad7-70b1-4ade-83f7-ce1f97d115d1"
/>

<img width="450" alt="Screenshot 2025-02-21 at 2 13 55 PM"
src="https://github.com/user-attachments/assets/427bb52c-6fa9-457f-ab28-f490be981094"
/>

</details>
This commit is contained in:
Kylie Meli 2025-02-28 08:06:56 -05:00 committed by GitHub
parent af6968bcb7
commit 0da5a87207
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
4 changed files with 1159 additions and 1125 deletions

View file

@ -216,7 +216,12 @@ export const ConfirmSettingsStep = React.memo<ConfirmSettingsStepProps>(
const authOptions = endpointOperation?.prepareSecurity();
const endpointAuth = getAuthDetails(auth, authOptions);
const schemas = reduceSpecComponents(oas, path);
let schemas;
try {
schemas = reduceSpecComponents(oas, path);
} catch (parsingError) {
throw new Error('Error parsing OpenAPI spec for required components');
}
const celRequest: CelInputRequestBody = {
dataStreamTitle: integrationSettings.dataStreamTitle ?? '',
@ -273,6 +278,8 @@ export const ConfirmSettingsStep = React.memo<ConfirmSettingsStepProps>(
});
setError(errorMessage);
onUpdateValidation(!!errorMessage);
onUpdateNeedsGeneration(true);
} finally {
setIsFlyoutGenerating(false);
}
@ -294,6 +301,7 @@ export const ConfirmSettingsStep = React.memo<ConfirmSettingsStepProps>(
setIsFlyoutGenerating,
reportCelGenerationComplete,
onCelInputGenerationComplete,
onUpdateValidation,
]);
const onCancel = useCallback(() => {

View file

@ -131,6 +131,8 @@ export const UploadSpecStep = React.memo<UploadSpecStepProps>(
e.body ? ` (${e.body.statusCode}): ${e.body.message}` : ''
}`;
setError(errorMessage);
onUpdateValidation(!!errorMessage);
onUpdateNeedsGeneration(true);
} finally {
setIsFlyoutGenerating(false);
}
@ -150,6 +152,7 @@ export const UploadSpecStep = React.memo<UploadSpecStepProps>(
setIsFlyoutGenerating,
dataStreamTitle,
onAnalyzeApiGenerationComplete,
onUpdateValidation,
]);
const onCancel = useCallback(() => {

View file

@ -8,144 +8,60 @@
/* eslint-disable @typescript-eslint/no-explicit-any */
import type Oas from 'oas';
import type {
ComponentsObject,
KeyedSecuritySchemeObject,
MediaTypeObject,
OASDocument,
SecurityType,
} from 'oas/dist/types.cjs';
import type { ComponentsObject, KeyedSecuritySchemeObject, SecurityType } from 'oas/dist/types.cjs';
import { CelAuthTypeEnum } from '../../common/api/model/cel_input_attributes.gen';
import type { CelAuthType } from '../../common';
/**
* Returns the inner-most schema object for the specified path array.
* Returns any $ref from the specified schema object.
*/
const getSchemaObject = (obj: object, paths: string[]): any => {
const attr = paths[0];
const innerObj = (obj as any)[attr];
if (paths.length > 1) {
const newPaths = paths.slice(1);
return getSchemaObject(innerObj, newPaths);
} else {
return innerObj;
}
};
export const getAllRefValues = (schemaObj: any): Set<string> => {
let allRefs = new Set<string>();
/**
* Returns any $ref from the specified schema object, or an empty string if there are none.
*/
const getRefValueOrEmpty = (schemaObj: any): string => {
if ('content' in schemaObj) {
const contentObj: MediaTypeObject = schemaObj.content;
for (const obj of Object.values(contentObj)) {
if ('schema' in obj) {
if ('items' in obj.schema) {
return obj.schema.items.$ref;
} else {
return obj.schema.$ref;
}
} else if ('$ref' in obj) {
return obj.$ref;
}
}
} else if ('properties' in schemaObj) {
for (const obj of Object.values(schemaObj.properties)) {
if ('items' in (obj as any)) {
if ('$ref' in (obj as any).items) {
return (obj as any).items.$ref;
}
} else {
if ('$ref' in (obj as any)) {
return (obj as any).$ref;
}
}
}
} else if ('items' in schemaObj) {
if ('$ref' in (schemaObj as any).items) {
return (schemaObj as any).items.$ref;
}
} else if ('$ref' in (schemaObj as any)) {
return (schemaObj as any).$ref;
}
return '';
};
/**
* Returns a list of utilized $refs from the specified layer of the schema.
*/
const getRefs = (refs: Set<string>, oas: OASDocument): Set<string> => {
const layerUsed = new Set<string>();
for (const ref of refs) {
const pathSplits = ref.split('/').filter((split: string) => split !== '#');
if (oas) {
const schemaObj = getSchemaObject(oas, pathSplits);
const refVal = getRefValueOrEmpty(schemaObj);
if (refVal) {
layerUsed.add(refVal);
}
}
}
return layerUsed;
};
/**
* Returns a list of all utilized $refs from the schema.
*/
const buildRefSet = (
allRefs: Set<string>,
refsToCheck: Set<string>,
oas: OASDocument
): Set<string> => {
if (refsToCheck.size > 0) {
const addtlRefs = getRefs(refsToCheck, oas);
const updated = new Set([...allRefs, ...addtlRefs]);
return buildRefSet(updated, addtlRefs, oas);
} else {
if (schemaObj === null || typeof schemaObj !== 'object') {
return allRefs;
}
if (Array.isArray(schemaObj)) {
for (const elem of schemaObj) {
if (typeof elem === 'object') {
const subRefs = getAllRefValues(elem);
if (subRefs.size > 0) {
allRefs = new Set([...allRefs, ...subRefs]);
}
}
}
return allRefs;
}
for (const [key, value] of Object.entries(schemaObj)) {
if (key === '$ref' && typeof value === 'string') {
allRefs.add(value);
} else if (typeof value === 'object' && value !== null) {
const subRefs = getAllRefValues(value);
if (subRefs.size > 0) {
allRefs = new Set([...allRefs, ...subRefs]);
}
}
}
return allRefs;
};
/**
* Retrieves the OAS spec components down to only those utilized by the specified path.
*/
export function reduceSpecComponents(oas: Oas, path: string): ComponentsObject | undefined {
const operation = oas?.operation(path, 'get');
const responses = operation?.schema.responses;
const usedSchemas = new Set<string>();
if (responses) {
for (const responseObj of Object.values(responses)) {
if ('$ref' in responseObj) {
usedSchemas.add(responseObj.$ref);
}
if ('content' in responseObj) {
const contentObj: MediaTypeObject = responseObj.content;
for (const obj of Object.values(contentObj)) {
if ('schema' in obj) {
if ('items' in obj.schema) {
usedSchemas.add(obj.schema.items.$ref);
} else {
usedSchemas.add(obj.schema.$ref);
}
} else if ('$ref' in obj) {
usedSchemas.add(obj.$ref);
}
}
}
}
}
const responses = oas?.operation(path, 'get')?.schema.responses;
const usedSchemas = getAllRefValues(responses);
if (oas?.api) {
const allUsedSchemas = buildRefSet(usedSchemas, usedSchemas, oas?.api);
// iterate the schemas and remove those not used
const reduced: ComponentsObject | undefined = JSON.parse(JSON.stringify(oas?.api.components));
if (reduced) {
for (const [componentType, items] of Object.entries(reduced)) {
for (const component of Object.keys(items)) {
if (!allUsedSchemas.has(`#/components/${componentType}/${component}`)) {
if (!usedSchemas.has(`#/components/${componentType}/${component}`)) {
delete reduced[componentType as keyof ComponentsObject]?.[component];
}
}