mirror of
https://github.com/elastic/kibana.git
synced 2025-04-23 17:28:26 -04:00
[Security Solution] Produce stable OAS bundle (#183053)
**Resolves:** https://github.com/elastic/kibana/issues/183051 ## Summary This PR add normalization to the OAS bundler produced by `@kbn/openapi-bundler` to get a stable OAS bundle. Normalization is achieved by sorting keys during serialization to YAML.
This commit is contained in:
parent
04417da086
commit
382f4ae808
6 changed files with 140 additions and 8 deletions
|
@ -267,7 +267,7 @@ describe('OpenAPI Bundler - bundle references', () => {
|
|||
},
|
||||
});
|
||||
|
||||
const { '2023-10-31.yaml': bundledSpec } = await bundleSpecs({ '1': spec1, '2': spec2 });
|
||||
const [bundledSpec] = Object.values(await bundleSpecs({ '1': spec1, '2': spec2 }));
|
||||
|
||||
expect(bundledSpec.paths['/api/some_api']).toEqual({
|
||||
get: spec1.paths['/api/some_api']!.get,
|
||||
|
|
|
@ -49,8 +49,7 @@ describe('OpenAPI Bundler - circular specs', () => {
|
|||
);
|
||||
|
||||
expect(dump(bundledSpec.paths['/api/some_api']!.get!.responses['200'])).toMatchInlineSnapshot(`
|
||||
"description: Successful response
|
||||
content:
|
||||
"content:
|
||||
application/json:
|
||||
schema: &ref_0
|
||||
type: object
|
||||
|
@ -58,6 +57,7 @@ content:
|
|||
fieldA:
|
||||
type: integer
|
||||
fieldB: *ref_0
|
||||
description: Successful response
|
||||
"
|
||||
`);
|
||||
});
|
||||
|
|
|
@ -77,10 +77,10 @@ describe('OpenAPI Bundler - different API versions', () => {
|
|||
});
|
||||
|
||||
expect(bundledSpecs).toEqual({
|
||||
'2023-10-31.yaml': expect.objectContaining({
|
||||
'2023_10_31.yaml': expect.objectContaining({
|
||||
paths: spec1.paths,
|
||||
}),
|
||||
'2023-11-11.yaml': expect.objectContaining({
|
||||
'2023_11_11.yaml': expect.objectContaining({
|
||||
paths: spec2.paths,
|
||||
}),
|
||||
});
|
||||
|
|
|
@ -0,0 +1,102 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0 and the Server Side Public License, v 1; you may not use this file except
|
||||
* in compliance with, at your election, the Elastic License 2.0 or the Server
|
||||
* Side Public License, v 1.
|
||||
*/
|
||||
|
||||
import { OpenAPIV3 } from 'openapi-types';
|
||||
import { bundleSpecs } from './bundle_specs';
|
||||
import { createOASDocument } from './create_oas_document';
|
||||
|
||||
describe('OpenAPI Bundler - produce stable bundle', () => {
|
||||
it('produces stable bundle (keys are sorted)', async () => {
|
||||
const response: OpenAPIV3.ResponseObject = {
|
||||
description: 'Successful response',
|
||||
content: {
|
||||
'application/json': {
|
||||
schema: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
fieldA: {
|
||||
$ref: './common.schema.yaml#/components/schemas/SchemaB',
|
||||
},
|
||||
fieldB: {
|
||||
$ref: './common.schema.yaml#/components/schemas/SchemaA',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
const spec1 = createOASDocument({
|
||||
paths: {
|
||||
'/api/some_api': {
|
||||
post: {
|
||||
responses: {
|
||||
'200': response,
|
||||
},
|
||||
},
|
||||
get: {
|
||||
responses: {
|
||||
'200': response,
|
||||
},
|
||||
},
|
||||
put: {
|
||||
responses: {
|
||||
'200': response,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
const spec2 = createOASDocument({
|
||||
paths: {
|
||||
'/api/another_api': {
|
||||
get: {
|
||||
responses: {
|
||||
'200': response,
|
||||
},
|
||||
},
|
||||
put: {
|
||||
responses: {
|
||||
'200': response,
|
||||
},
|
||||
},
|
||||
patch: {
|
||||
responses: {
|
||||
'200': response,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
const commonSpec = createOASDocument({
|
||||
components: {
|
||||
schemas: {
|
||||
SchemaB: {
|
||||
type: 'string',
|
||||
},
|
||||
SchemaA: {
|
||||
type: 'number',
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const [bundledSpec] = Object.values(
|
||||
await bundleSpecs({
|
||||
1: spec1,
|
||||
2: spec2,
|
||||
common: commonSpec,
|
||||
})
|
||||
);
|
||||
|
||||
expect(Object.keys(bundledSpec.paths)).toEqual(['/api/another_api', '/api/some_api']);
|
||||
expect(Object.keys(bundledSpec.paths['/api/another_api']!)).toEqual(['get', 'patch', 'put']);
|
||||
expect(Object.keys(bundledSpec.paths['/api/some_api']!)).toEqual(['get', 'post', 'put']);
|
||||
expect(Object.keys(bundledSpec.components!.schemas!)).toEqual(['SchemaA', 'SchemaB']);
|
||||
});
|
||||
});
|
|
@ -136,9 +136,10 @@ async function writeDocuments(
|
|||
|
||||
function getVersionedOutputFilePath(outputFilePath: string, version: string): string {
|
||||
const hasVersionPlaceholder = outputFilePath.indexOf('{version}') > -1;
|
||||
const snakeCasedVersion = version.replaceAll(/[^\w\d]+/g, '_');
|
||||
|
||||
if (hasVersionPlaceholder) {
|
||||
return outputFilePath.replace('{version}', version);
|
||||
return outputFilePath.replace('{version}', snakeCasedVersion);
|
||||
}
|
||||
|
||||
const filename = basename(outputFilePath);
|
||||
|
|
|
@ -25,16 +25,45 @@ function stringifyToYaml(document: unknown): string {
|
|||
try {
|
||||
// Disable YAML Anchors https://yaml.org/spec/1.2.2/#3222-anchors-and-aliases
|
||||
// It makes YAML much more human readable
|
||||
return dump(document, { noRefs: true });
|
||||
return dump(document, {
|
||||
noRefs: true,
|
||||
sortKeys: sortYamlKeys,
|
||||
});
|
||||
} catch (e) {
|
||||
// RangeError might happened because of stack overflow
|
||||
// due to circular references in the document
|
||||
// since YAML Anchors are disabled
|
||||
if (e instanceof RangeError) {
|
||||
// Try to stringify with YAML Anchors enabled
|
||||
return dump(document, { noRefs: false });
|
||||
return dump(document, { noRefs: false, sortKeys: sortYamlKeys });
|
||||
}
|
||||
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
|
||||
function sortYamlKeys(a: string, b: string): number {
|
||||
if (a in FIELDS_ORDER && b in FIELDS_ORDER) {
|
||||
return FIELDS_ORDER[a as CustomOrderedField] - FIELDS_ORDER[b as CustomOrderedField];
|
||||
}
|
||||
|
||||
return a.localeCompare(b);
|
||||
}
|
||||
|
||||
const FIELDS_ORDER = {
|
||||
// root level fields
|
||||
openapi: 1,
|
||||
info: 2,
|
||||
servers: 3,
|
||||
paths: 4,
|
||||
components: 5,
|
||||
security: 6,
|
||||
tags: 7,
|
||||
externalDocs: 8,
|
||||
// object schema fields
|
||||
type: 9,
|
||||
properties: 10,
|
||||
required: 11,
|
||||
} as const;
|
||||
|
||||
type CustomOrderedField = keyof typeof FIELDS_ORDER;
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue