fix: adding a fix to the openapi2kong circular deps issue (#4872)

* commit progress

* save changes

* add components and $schema

* add components and $schema

* adding a doc

* add typings

* fix unit tests

* fix unit tests

* add fix and verified with kong cli

* fix unit tests

* Improve types and parameter/body resolution

Co-authored-by: Mark Kim <marckong@users.noreply.github.com>

* fix circular "components" issue

Co-authored-by: gatzjames <jamesgatzos@gmail.com>
Co-authored-by: Mark Kim <marckong@users.noreply.github.com>
This commit is contained in:
Mark Kim 2022-06-24 15:48:45 -04:00 committed by GitHub
parent 8300652981
commit 4f5ef73f7e
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
19 changed files with 1251 additions and 1173 deletions

File diff suppressed because it is too large Load Diff

View File

@ -1,4 +1,5 @@
import { describe, expect, it } from '@jest/globals';
import { OpenAPIV3 } from 'openapi-types';
import {
distinctByProperty,
@ -120,12 +121,12 @@ describe('common', () => {
type: 'http',
scheme: 'basic',
name: 'name',
},
} as OpenAPIV3.HttpSecurityScheme,
anotherAuth: {
type: 'http',
scheme: 'basic',
name: 'another-name',
},
} as OpenAPIV3.HttpSecurityScheme,
},
},
});
@ -155,6 +156,7 @@ describe('common', () => {
const spec = getSpec({
info: {
version: '1.0.0',
title: '',
},
});
const result = getName(spec);

View File

@ -41,7 +41,7 @@
"name": "request-validator",
"config": {
"version": "draft4",
"body_schema": "{\"type\":\"object\",\"title\":\"request-body-nullable\",\"description\":\"This object is sent in post application request.\",\"required\":[\"redirectUri\"],\"properties\":{\"redirectUri\":{\"type\":[\"string\",\"null\"],\"nullable\":true}}}",
"body_schema": "{\"type\":\"object\",\"title\":\"request-body-nullable\",\"description\":\"This object is sent in post application request.\",\"required\":[\"redirectUri\"],\"properties\":{\"redirectUri\":{\"type\":[\"string\",\"null\"],\"nullable\":true}},\"components\":{\"schemas\":{\"request-body-nullable\":{\"type\":\"object\",\"title\":\"request-body-nullable\",\"description\":\"This object is sent in post application request.\",\"required\":[\"redirectUri\"],\"properties\":{\"redirectUri\":{\"type\":[\"string\",\"null\"],\"nullable\":true}}}}},\"$schema\":\"http://json-schema.org/schema\"}",
"allowed_content_types": [
"application/json"
],

View File

@ -29,7 +29,7 @@
"application/json",
"application/xml"
],
"body_schema": "{\"type\":\"object\",\"properties\":{\"id\":{\"type\":\"integer\"},\"name\":{\"type\":\"string\"}}}",
"body_schema": "{\"type\":\"object\",\"properties\":{\"id\":{\"type\":\"integer\"},\"name\":{\"type\":\"string\"}},\"components\":{\"schemas\":{\"jsonSchema\":{\"type\":\"object\",\"properties\":{\"id\":{\"type\":\"integer\"},\"name\":{\"type\":\"string\"}}},\"xmlSchema\":{\"type\":\"object\",\"properties\":{\"prop\":{\"type\":\"integer\"}}}}},\"$schema\":\"http://json-schema.org/schema\"}",
"version": "draft4"
},
"tags": ["OAS3_import", "OAS3file_request-validator-plugin.yaml"],
@ -47,7 +47,7 @@
{
"config": {
"allowed_content_types": ["application/json"],
"body_schema": "{\"type\":\"object\",\"properties\":{\"id\":{\"type\":\"integer\"},\"name\":{\"type\":\"string\"}}}",
"body_schema": "{\"type\":\"object\",\"properties\":{\"id\":{\"type\":\"integer\"},\"name\":{\"type\":\"string\"}},\"components\":{\"schemas\":{\"jsonSchema\":{\"type\":\"object\",\"properties\":{\"id\":{\"type\":\"integer\"},\"name\":{\"type\":\"string\"}}},\"xmlSchema\":{\"type\":\"object\",\"properties\":{\"prop\":{\"type\":\"integer\"}}}}},\"$schema\":\"http://json-schema.org/schema\"}",
"version": "draft4"
},
"tags": ["OAS3_import", "OAS3file_request-validator-plugin.yaml"],

View File

@ -5,14 +5,14 @@ import { DeclarativeConfigResult } from '../types/outputs';
import { generateServices } from './services';
import { generateUpstreams } from './upstreams';
export function generateDeclarativeConfigFromSpec(
export async function generateDeclarativeConfigFromSpec(
api: OpenApi3Spec,
tags: string[],
) {
try {
const document: DeclarativeConfig = {
_format_version: '1.1',
services: generateServices(api, tags),
services: await generateServices(api, tags),
};
if (hasUpstreams(api)) {
@ -26,9 +26,16 @@ export function generateDeclarativeConfigFromSpec(
warnings: [],
};
// This removes any circular references or weirdness that might result from the JS objects used.
// see: https://github.com/Kong/studio/issues/93
const result: DeclarativeConfigResult = JSON.parse(JSON.stringify(declarativeConfigResult));
/**
* There was an [issue](https://github.com/Kong/studio/issues/93) that required us to stringify and parse the declarative config object containing circular dependencies.
* However, that fix didn't seem to clear the issue of the circular dependencies completely.
*
* It is attempted to resolve the circular issue by bundling the openapi spec using SwaggerParser.bundle() method, which resolves all the schemas into $ref instead of dereferencing them.
* Then, we would just dump the components part of it with $schema property, so any JSON parsing logic can refer to the components object.
*
* Therefore, JSON.parse(JSON.stringify(result)) doesn't seem to be needed any more.
*/
const result: DeclarativeConfigResult = declarativeConfigResult;
return result;
} catch (err) {
throw new Error('Failed to generate spec: ' + err.message);

View File

@ -58,7 +58,7 @@ export type UserXKongPlugin = XKongPlugin<Plugin> | XKongPlugin<DummyPlugin>;
export const getSpec = (overrides: Partial<OpenApi3Spec> = {}): OpenApi3Spec =>
JSON.parse(
JSON.stringify({
openapi: '3.0',
openapi: '3.0.0',
info: {
version: '1.0',
title: 'My API',

View File

@ -1,4 +1,5 @@
import { describe, expect, it } from '@jest/globals';
import { OpenAPIV3 } from 'openapi-types';
import { ParameterSchema, RequestTerminationPlugin, RequestValidatorPlugin, xKongPluginKeyAuth, xKongPluginRequestTermination, xKongPluginRequestValidator } from '../types/kong';
import { OA3Operation, OA3Parameter } from '../types/openapi3';
@ -25,7 +26,7 @@ describe('plugins', () => {
},
});
const result = generateGlobalPlugins(api, tags);
const result = await generateGlobalPlugins(api, tags);
expect(result.plugins).toEqual([
{
@ -63,7 +64,7 @@ describe('plugins', () => {
});
});
it('does not add extra things to the plugin', () => {
it('does not add extra things to the plugin', async () => {
const spec = getSpec({
[xKongPluginRequestTermination]: {
name: 'request-termination',
@ -77,7 +78,7 @@ describe('plugins', () => {
},
});
const result = generateGlobalPlugins(spec, tags);
const result = await generateGlobalPlugins(spec, tags);
expect(result.plugins as RequestTerminationPlugin[]).toEqual([
{
@ -95,6 +96,24 @@ describe('plugins', () => {
});
describe('generateRequestValidatorPlugin()', () => {
const api = getSpec({
[xKongPluginRequestValidator]: {
name: 'request-validator',
enabled: false,
config: {
body_schema: ALLOW_ALL_SCHEMA,
verbose_response: true,
},
},
...pluginDummy,
[xKongPluginKeyAuth]: {
name: 'key-auth',
config: {
key_names: ['x-api-key'],
},
},
});
const parameterSchema: ParameterSchema = {
explode: false,
in: 'path',
@ -104,7 +123,7 @@ describe('plugins', () => {
style: 'form',
};
it('should retain config properties', () => {
it('should retain config properties', async () => {
const plugin: RequestValidatorPlugin = {
name: 'request-validator',
enabled: true,
@ -115,7 +134,7 @@ describe('plugins', () => {
allowed_content_types: ['application/json'],
},
};
const generated = generateRequestValidatorPlugin({ plugin, tags });
const generated = await generateRequestValidatorPlugin({ plugin, tags, api });
expect(generated).toStrictEqual({
name: 'request-validator',
enabled: plugin.enabled,
@ -128,7 +147,7 @@ describe('plugins', () => {
});
});
it('should not add config properties if they are not defined', () => {
it('should not add config properties if they are not defined', async () => {
const plugin: RequestValidatorPlugin = {
name: 'request-validator',
enabled: true,
@ -140,7 +159,7 @@ describe('plugins', () => {
// allowed_content_types: ['application/json'],
},
};
const generated = generateRequestValidatorPlugin({ plugin, tags });
const generated = await generateRequestValidatorPlugin({ plugin, tags, api });
expect(generated).toStrictEqual({
name: 'request-validator',
enabled: plugin.enabled,
@ -154,7 +173,7 @@ describe('plugins', () => {
});
describe('parameter_schema', () => {
it('should not add parameter_schema if no parameters present', () => {
it('should not add parameter_schema if no parameters present', async () => {
const plugin: RequestValidatorPlugin = {
name: 'request-validator',
config: {
@ -162,18 +181,19 @@ describe('plugins', () => {
},
};
const generated1 = generateRequestValidatorPlugin({ plugin, tags });
const generated1 = await generateRequestValidatorPlugin({ plugin, tags, api });
expect(generated1.config).toStrictEqual({
version: 'draft4',
body_schema: ALLOW_ALL_SCHEMA,
});
const generated2 = generateRequestValidatorPlugin({
const generated2 = await generateRequestValidatorPlugin({
plugin,
operation: {
parameters: [],
},
tags,
api,
});
expect(generated2.config).toStrictEqual({
version: 'draft4',
@ -181,7 +201,7 @@ describe('plugins', () => {
});
});
it('should convert operation parameters to parameter_schema', () => {
it('should convert operation parameters to parameter_schema', async () => {
const param: OA3Parameter = {
in: 'query',
explode: true,
@ -199,7 +219,7 @@ describe('plugins', () => {
const operation: OA3Operation = {
parameters: [param],
};
const generated = generateRequestValidatorPlugin({ tags, operation });
const generated = await generateRequestValidatorPlugin({ tags, operation, api });
expect(generated.config).toStrictEqual({
version: 'draft4',
parameter_schema: [
@ -215,7 +235,7 @@ describe('plugins', () => {
});
});
it('should return default if operation parameter schema not defined on any parameters', () => {
it('should return default if operation parameter schema not defined on any parameters', async () => {
const operation: OA3Operation = {
parameters: [
{
@ -224,7 +244,7 @@ describe('plugins', () => {
},
],
};
const generated = generateRequestValidatorPlugin({ tags, operation });
const generated = await generateRequestValidatorPlugin({ tags, operation, api });
expect(generated.config).toStrictEqual({
version: 'draft4',
parameter_schema: [
@ -240,7 +260,7 @@ describe('plugins', () => {
});
});
it('should ignore parameters without schema', () => {
it('should ignore parameters without schema', async () => {
const paramWithSchema: OA3Parameter = {
in: 'query',
explode: true,
@ -265,7 +285,7 @@ describe('plugins', () => {
paramWithoutSchema,
],
};
const generated = generateRequestValidatorPlugin({ tags, operation });
const generated = await generateRequestValidatorPlugin({ tags, operation, api });
expect(generated.config).toStrictEqual({
version: 'draft4',
parameter_schema: [
@ -291,45 +311,44 @@ describe('plugins', () => {
});
describe('body_schema and allowed_content_types', () => {
it('should not add body_schema or allowed_content_types if no body present', () => {
it('should not add body_schema or allowed_content_types if no body present', async () => {
const plugin: RequestValidatorPlugin = {
name: 'request-validator',
config: {
body_schema: ALLOW_ALL_SCHEMA,
},
};
const generated = generateRequestValidatorPlugin({ plugin, tags });
const generated = await generateRequestValidatorPlugin({ plugin, tags, api });
expect(generated.config).toStrictEqual({
version: 'draft4',
body_schema: ALLOW_ALL_SCHEMA,
});
});
it('should return default if no operation request body content defined', () => {
it('should return default if no operation request body content defined', async () => {
const defaultReqVal: RequestValidatorPlugin['config'] = {
version: 'draft4',
body_schema: ALLOW_ALL_SCHEMA,
};
const op1: OA3Operation = {
requestBody: {},
};
const op2: OA3Operation = {
const op1: OpenAPIV3.OperationObject = {};
const op2: OpenAPIV3.OperationObject = {
requestBody: {
$ref: 'non-existent',
$ref: '#/components/non-existent',
},
};
expect(generateRequestValidatorPlugin({ operation: op1, tags }).config).toStrictEqual(
const apiWithNoComponents = { ...api, components: {} };
expect((await generateRequestValidatorPlugin({ operation: op1, tags, api: apiWithNoComponents })).config).toStrictEqual(
defaultReqVal,
);
expect(generateRequestValidatorPlugin({ operation: op2, tags }).config).toStrictEqual(
expect((await generateRequestValidatorPlugin({ operation: op2, tags, api: apiWithNoComponents })).config).toStrictEqual(
defaultReqVal,
);
expect(generateRequestValidatorPlugin({ tags }).config).toStrictEqual(
expect((await generateRequestValidatorPlugin({ tags, api: apiWithNoComponents })).config).toStrictEqual(
defaultReqVal,
);
});
it('should add non-json media types to allowed content types and not add body schema', () => {
it('should add non-json media types to allowed content types and not add body schema', async () => {
const operation: OA3Operation = {
requestBody: {
content: {
@ -338,7 +357,7 @@ describe('plugins', () => {
},
},
};
const generated = generateRequestValidatorPlugin({ tags, operation });
const generated = await generateRequestValidatorPlugin({ tags, operation, api });
expect(generated.config).toStrictEqual({
version: 'draft4',
body_schema: ALLOW_ALL_SCHEMA,
@ -346,7 +365,7 @@ describe('plugins', () => {
});
});
it('should add body_schema and allowed content types', () => {
it('should add body_schema and allowed content types', async () => {
const schemaXml = {
type: 'Object',
properties: {
@ -365,19 +384,19 @@ describe('plugins', () => {
},
},
};
const operation: OA3Operation = {
const operation: OpenAPIV3.OperationObject = {
requestBody: {
content: {
'application/xml': {
schema: schemaXml,
},
} as OpenAPIV3.SchemaObject,
'application/json': {
schema: schemaJson,
},
} as OpenAPIV3.SchemaObject,
},
},
};
const generated = generateRequestValidatorPlugin({ tags, operation });
const generated = await generateRequestValidatorPlugin({ tags, operation, api });
expect(generated.config).toStrictEqual({
version: 'draft4',
body_schema: JSON.stringify(schemaJson),
@ -385,8 +404,8 @@ describe('plugins', () => {
});
});
it('should default body_schema if no schema is defined or generated', () => {
const generated = generateRequestValidatorPlugin({ tags, operation: {} });
it('should default body_schema if no schema is defined or generated', async () => {
const generated = await generateRequestValidatorPlugin({ tags, operation: {}, api });
expect(generated.config).toStrictEqual({
version: 'draft4',
body_schema: ALLOW_ALL_SCHEMA,
@ -394,8 +413,8 @@ describe('plugins', () => {
});
});
describe('body_schema generateBodyOptions FTI-3278', () => {
it('should keep to properties[].type unchanged nullable is not set', () => {
const generated = generateBodyOptions({
it('should keep to properties[].type unchanged nullable is not set', async () => {
const generated = await generateBodyOptions(api, {
requestBody: {
content: {
'application/json': {
@ -412,8 +431,8 @@ describe('plugins', () => {
});
expect(generated.bodySchema).toStrictEqual('{\"properties\":{\"redirectUri\":{\"type\":\"string\"}}}');
});
it('should keep to properties[].type unchanged nullable is false', () => {
const generated = generateBodyOptions({
it('should keep to properties[].type unchanged nullable is false', async () => {
const generated = await generateBodyOptions(api, {
requestBody: {
content: {
'application/json': {
@ -431,8 +450,8 @@ describe('plugins', () => {
});
expect(generated.bodySchema).toStrictEqual('{\"properties\":{\"redirectUri\":{\"type\":\"string\",\"nullable\":false}}}');
});
it('should append null to properties[].type if nullable is true', () => {
const generated = generateBodyOptions({
it('should append null to properties[].type if nullable is true', async () => {
const generated = await generateBodyOptions(api, {
requestBody: {
content: {
'application/json': {

View File

@ -1,9 +1,11 @@
import SwaggerParser from '@apidevtools/swagger-parser';
import { OpenAPIV3 } from 'openapi-types';
import { Entry } from 'type-fest';
import { distinctByProperty, getPluginNameFromKey, isPluginKey } from '../common';
import { DCPlugin } from '../types/declarative-config';
import { isBodySchema, isParameterSchema, ParameterSchema, RequestValidatorPlugin, XKongPluginRequestValidator, xKongPluginRequestValidator } from '../types/kong';
import { OA3Operation, OA3Parameter, OA3RequestBody, OpenApi3Spec } from '../types/openapi3';
import type { OA3Operation, OpenApi3Spec } from '../types/openapi3';
export const isRequestValidatorPluginKey = (property: string): property is typeof xKongPluginRequestValidator => (
property.match(/-request-validator$/) != null
@ -36,7 +38,7 @@ const generatePlugin = (tags: string[]) => ([key, value]: Entry<PluginItem>): DC
* See: https://github.com/Kong/kong-plugin-enterprise-request-validator/pull/34/files#diff-1a1d2d5ce801cc1cfb2aa91ae15686d81ef900af1dbef00f004677bc727bfd3cR284
*/
export const ALLOW_ALL_SCHEMA = '{}';
const $schema = 'http://json-schema.org/schema#';
const DEFAULT_PARAM_STYLE = {
header: 'simple',
cookie: 'form',
@ -44,19 +46,79 @@ const DEFAULT_PARAM_STYLE = {
path: 'simple',
};
const generateParameterSchema = (operation?: OA3Operation) => {
if (!operation?.parameters?.length) {
return undefined;
interface ResolvedParameter {
resolvedParam: OpenAPIV3.ParameterObject;
components: OpenAPIV3.ComponentsObject | undefined;
}
const resolveParameter = ($refs: SwaggerParser.$Refs, parameter: OpenAPIV3.ParameterObject | OpenAPIV3.ReferenceObject): ResolvedParameter => {
if ('$ref' in parameter) {
const components = getOperationRef<OpenAPIV3.ComponentsObject>($refs, '#/components');
const dereferenced = getOperationRef<OpenAPIV3.ParameterObject>($refs, parameter.$ref);
const { $ref, ...param } = parameter;
let schema: OpenAPIV3.ParameterObject['schema'] = dereferenced?.schema;
if (schema && '$ref' in schema) {
schema = getOperationRef<OpenAPIV3.ParameterObject['schema']>($refs, schema.$ref);
}
const resolvedParam: OpenAPIV3.ParameterObject = {
...param,
...dereferenced,
name: dereferenced?.name || '',
in: dereferenced?.in || '',
schema,
};
return {
resolvedParam,
components,
};
}
if (parameter.schema && '$ref' in parameter.schema) {
const components = getOperationRef<OpenAPIV3.ComponentsObject>($refs, '#/components');
const schema = getOperationRef<OpenAPIV3.ParameterObject['schema']>($refs, parameter.schema.$ref);
return {
resolvedParam: {
...parameter,
schema,
},
components,
};
}
return { resolvedParam: parameter, components: undefined };
};
type KongSchema = (OpenAPIV3.ReferenceObject | OpenAPIV3.SchemaObject) & {
components?: OpenAPIV3.ComponentsObject;
$schema?: string;
};
const generateParameterSchema = async (api: OpenApi3Spec, operation?: OA3Operation) => {
if (!operation?.parameters?.length) {
return;
}
const refs: SwaggerParser.$Refs = await SwaggerParser.resolve(api);
const parameterSchemas: ParameterSchema[] = [];
for (const parameter of operation.parameters as OA3Parameter[]) {
for (const parameter of operation.parameters) {
// The following is valid config to allow all content to pass, in the case where schema is not defined
let schema = '';
if (parameter.schema) {
schema = JSON.stringify(parameter.schema);
} else if (parameter.content) {
const { resolvedParam, components } = resolveParameter(refs, parameter);
if (resolvedParam.schema) {
const kongSchema: KongSchema = { ...resolvedParam.schema };
// The $schema property should only exist if components exist with a $ref path
if (components) {
kongSchema.components = components;
kongSchema.$schema = $schema;
}
schema = JSON.stringify(kongSchema);
} else if ('content' in parameter) {
// only parameters defined with a schema (not content) are supported
schema = ALLOW_ALL_SCHEMA;
} else {
@ -64,18 +126,19 @@ const generateParameterSchema = (operation?: OA3Operation) => {
schema = ALLOW_ALL_SCHEMA;
}
const paramStyle = parameter.style ?? DEFAULT_PARAM_STYLE[parameter.in];
// @ts-expect-error fix this
const paramStyle = (parameter as OpenAPIV3.ParameterObject).style ?? DEFAULT_PARAM_STYLE[resolvedParam.in];
if (typeof paramStyle === 'undefined') {
const name = parameter.name;
const name = resolvedParam.name;
throw new Error(`invalid 'in' property (parameter '${name}')`);
}
const parameterSchema: ParameterSchema = {
in: parameter.in,
explode: !!parameter.explode,
required: !!parameter.required,
name: parameter.name,
in: resolvedParam.in,
explode: !!resolvedParam.explode,
required: !!resolvedParam.required,
name: resolvedParam.name,
schema,
style: paramStyle,
};
@ -85,25 +148,73 @@ const generateParameterSchema = (operation?: OA3Operation) => {
return parameterSchemas;
};
export function generateBodyOptions(operation?: OA3Operation) {
function resolveRequestBodyContent($refs: SwaggerParser.$Refs, operation?: OA3Operation): OpenAPIV3.RequestBodyObject | undefined {
if (!operation || !operation?.requestBody) {
return;
}
if ('$ref' in operation.requestBody) {
return getOperationRef($refs, operation.requestBody.$ref);
}
return operation.requestBody;
}
function getOperationRef<RefType = OpenAPIV3.RequestBodyObject>($refs: SwaggerParser.$Refs, refPath: OpenAPIV3.ReferenceObject['$ref']): RefType | undefined {
if ($refs.exists(refPath)) {
return $refs.get(refPath);
}
return;
}
function resolveItemSchema($refs: SwaggerParser.$Refs, item: OpenAPIV3.MediaTypeObject): OpenAPIV3.SchemaObject {
if (item.schema && '$ref' in item.schema) {
const resolved: OpenAPIV3.NonArraySchemaObject & { $schema: string; components: Record<string, unknown> } = { ...$refs.get(item.schema.$ref) };
resolved.components = $refs.get('#/components');
resolved.$schema = 'http://json-schema.org/schema';
return resolved;
}
if (!item.schema) {
return {};
}
return item.schema;
}
function serializeSchema(schema: OpenAPIV3.SchemaObject): string {
for (const key in schema.properties) {
// Append 'null' to property type if nullable true, see FTI-3278
// TODO: this does not conform to the OpenAPI 3 spec typings. We may need to investifate further why this was needed
// @ts-expect-error this needs a casting perhaps. schema can be either ArraySchemaObject or NonArraySchemaObject. Only the later has 'properties'
if (schema.properties[key].nullable === true) {
// @ts-expect-error this needs some further investigation. 'type' is merely an string enum, not an array according to the OpenAPI 3 typings.
schema.properties[key].type = [schema.properties[key].type, 'null'];
}
}
return JSON.stringify(schema);
}
export async function generateBodyOptions(api: OpenApi3Spec, operation?: OA3Operation) {
const $refs: SwaggerParser.$Refs = await SwaggerParser.resolve(api);
let bodySchema;
let allowedContentTypes;
const bodyContent = (operation?.requestBody as OA3RequestBody)?.content;
const requestBody = resolveRequestBodyContent($refs, operation);
const bodyContent = requestBody?.content;
if (bodyContent) {
const jsonContentType = 'application/json';
allowedContentTypes = Object.keys(bodyContent);
if (allowedContentTypes.includes(jsonContentType)) {
const item = bodyContent[jsonContentType];
const schema = item.schema;
for (const key in schema.properties) {
// Append 'null' to property type if nullable true, see FTI-3278
if (schema.properties[key].nullable === true) {
schema.properties[key].type = [schema.properties[key].type, 'null'];
}
}
bodySchema = JSON.stringify(item.schema);
const item: OpenAPIV3.MediaTypeObject = bodyContent[jsonContentType];
const schema = resolveItemSchema($refs, item);
bodySchema = serializeSchema(schema);
}
}
@ -113,13 +224,15 @@ export function generateBodyOptions(operation?: OA3Operation) {
};
}
export function generateRequestValidatorPlugin({
plugin = { name: 'request-validator' },
export async function generateRequestValidatorPlugin({
tags,
api,
plugin = { name: 'request-validator' },
operation,
}: {
plugin?: Partial<RequestValidatorPlugin>;
tags: string[];
api: OpenApi3Spec;
plugin?: Partial<RequestValidatorPlugin>;
operation?: OA3Operation;
}) {
const config: Partial<RequestValidatorPlugin['config']> = {
@ -127,9 +240,9 @@ export function generateRequestValidatorPlugin({
};
// // Use original or generated parameter_schema
const parameterSchema = isParameterSchema(plugin.config) ? plugin.config.parameter_schema : generateParameterSchema(operation);
const parameterSchema = isParameterSchema(plugin.config) ? plugin.config.parameter_schema : await generateParameterSchema(api, operation);
const generated = generateBodyOptions(operation);
const generated = await generateBodyOptions(api, operation);
// Use original or generated body_schema
let bodySchema = isBodySchema(plugin.config) ? plugin.config.body_schema : generated.bodySchema;
@ -174,12 +287,12 @@ export function generateRequestValidatorPlugin({
return requestValidatorPlugin;
}
export function generateGlobalPlugins(api: OpenApi3Spec, tags: string[]) {
export async function generateGlobalPlugins(api: OpenApi3Spec, tags: string[]) {
const globalPlugins = generatePlugins(api, tags);
const plugin = getRequestValidatorPluginDirective(api);
if (plugin) {
globalPlugins.push(generateRequestValidatorPlugin({ plugin, tags }));
globalPlugins.push(await generateRequestValidatorPlugin({ plugin, tags, api }));
}
return {
@ -189,14 +302,14 @@ export function generateGlobalPlugins(api: OpenApi3Spec, tags: string[]) {
};
}
export const generateOperationPlugins = ({ operation, pathPlugins, parentValidatorPlugin, tags }: {
export const generateOperationPlugins = async ({ operation, pathPlugins, parentValidatorPlugin, tags, api }: {
operation: OA3Operation;
pathPlugins: DCPlugin[];
parentValidatorPlugin?: RequestValidatorPlugin | null;
tags: string[];
api: OpenApi3Spec;
}) => {
const operationPlugins = generatePlugins(operation, tags);
// Check if validator plugin exists on the operation, even if the value of the plugin is undefined
const operationValidatorPlugin = getRequestValidatorPluginDirective(operation);
@ -204,7 +317,7 @@ export const generateOperationPlugins = ({ operation, pathPlugins, parentValidat
const plugin = operationValidatorPlugin || parentValidatorPlugin;
if (plugin) {
operationPlugins.push(generateRequestValidatorPlugin({ plugin, tags, operation }));
operationPlugins.push(await generateRequestValidatorPlugin({ plugin, tags, operation, api }));
}
// Operation plugins take precedence over path plugins

View File

@ -1,6 +1,7 @@
import { describe, expect, it } from '@jest/globals';
import { OpenAPIV3 } from 'openapi-types';
import { OA3SecurityScheme, OA3SecuritySchemeOpenIdConnect } from '../types/openapi3';
import { OA3SecurityScheme } from '../types/openapi3';
import { tags } from './jest/test-helpers';
import { generateSecurityPlugin } from './security-plugins';
@ -35,7 +36,7 @@ describe('security-plugins', () => {
enabled: true,
protocols: ['http', 'https'],
},
} as OA3SecuritySchemeOpenIdConnect;
} as OpenAPIV3.OpenIdSecurityScheme;
const scopesRequired = ['required_scope', 'ziltoid_omniscient_power'];
const result = generateSecurityPlugin(scheme, scopesRequired, tags);

View File

@ -1,7 +1,9 @@
import { OpenAPIV3 } from 'openapi-types';
import { getSecurity } from '../common';
import { DCPlugin } from '../types/declarative-config';
import { BasicAuthPlugin, KeyAuthPlugin, OpenIDConnectPlugin } from '../types/kong';
import { OA3Operation, OA3SecurityScheme, OA3SecuritySchemeApiKey, OA3SecuritySchemeHttp, OA3SecuritySchemeOpenIdConnect, OpenApi3Spec } from '../types/openapi3';
import { OA3Operation, OA3SecurityScheme, OpenApi3Spec } from '../types/openapi3';
export function generateSecurityPlugins(
op: OA3Operation | null,
@ -28,7 +30,7 @@ export function generateSecurityPlugins(
return plugins;
}
export const generateApiKeySecurityPlugin = (scheme: OA3SecuritySchemeApiKey) => {
export const generateApiKeySecurityPlugin = (scheme: OpenAPIV3.ApiKeySecurityScheme) => {
if (!['query', 'header', 'cookie'].includes(scheme.in)) {
throw new Error(`a ${scheme.type} object expects valid "in" property. Got ${scheme.in}`);
}
@ -45,7 +47,7 @@ export const generateApiKeySecurityPlugin = (scheme: OA3SecuritySchemeApiKey) =>
return keyAuthPlugin;
};
export const generateBasicAuthPlugin = (scheme: OA3SecuritySchemeHttp) => {
export const generateBasicAuthPlugin = (scheme: OpenAPIV3.HttpSecurityScheme) => {
if ((scheme.scheme || '').toLowerCase() !== 'basic') {
throw new Error(`Only "basic" http scheme supported. got ${scheme.scheme}`);
}
@ -55,7 +57,7 @@ export const generateBasicAuthPlugin = (scheme: OA3SecuritySchemeHttp) => {
return basicAuthPlugin;
};
export const generateOpenIdConnectSecurityPlugin = (scheme: OA3SecuritySchemeOpenIdConnect, args: string[]) => {
export const generateOpenIdConnectSecurityPlugin = (scheme: OpenAPIV3.OpenIdSecurityScheme, args: string[]) => {
if (!scheme.openIdConnectUrl) {
throw new Error(`invalid "openIdConnectUrl" property. Got ${scheme.openIdConnectUrl}`);
}
@ -86,15 +88,15 @@ export function generateSecurityPlugin(
// Generate base plugin
switch (scheme?.type.toLowerCase()) {
case 'apikey':
plugin = generateApiKeySecurityPlugin(scheme as OA3SecuritySchemeApiKey);
plugin = generateApiKeySecurityPlugin(scheme as OpenAPIV3.ApiKeySecurityScheme);
break;
case 'http':
plugin = generateBasicAuthPlugin(scheme as OA3SecuritySchemeHttp);
plugin = generateBasicAuthPlugin(scheme as OpenAPIV3.HttpSecurityScheme);
break;
case 'openidconnect':
plugin = generateOpenIdConnectSecurityPlugin(scheme as OA3SecuritySchemeOpenIdConnect, args);
plugin = generateOpenIdConnectSecurityPlugin(scheme as OpenAPIV3.OpenIdSecurityScheme, args);
break;
case 'oauth2':

View File

@ -58,7 +58,7 @@ describe('services', () => {
const fn = () => generateServices(spec, tags);
expect(fn).toThrowError('no servers defined in spec');
expect(fn).rejects.toThrowError('no servers defined in spec');
});
it('throws for a root level x-kong-route-default', () => {
@ -69,16 +69,16 @@ describe('services', () => {
const fn = () => generateServices(spec, tags);
expect(fn).toThrowError('expected root-level \'x-kong-route-defaults\' to be an object');
expect(fn).rejects.toThrowError('expected root-level \'x-kong-route-defaults\' to be an object');
});
it('ignores null for a root level x-kong-route-default', () => {
it('ignores null for a root level x-kong-route-default', async () => {
const spec = getSpec({
// @ts-expect-error intentionally invalid
[xKongRouteDefaults]: null,
});
const specResult = getSpecResult();
expect(generateServices(spec, tags)).toEqual([specResult]);
expect(await generateServices(spec, tags)).toEqual([specResult]);
});
it('throws for a paths level x-kong-route-default', () => {
@ -88,15 +88,15 @@ describe('services', () => {
const fn = () => generateServices(spec, tags);
expect(fn).toThrowError('expected \'x-kong-route-defaults\' to be an object (at path \'/cats\')');
expect(fn).rejects.toThrowError('expected \'x-kong-route-defaults\' to be an object (at path \'/cats\')');
});
it('ignores null for a paths level x-kong-route-default', () => {
it('ignores null for a paths level x-kong-route-default', async () => {
const spec = getSpec();
// @ts-expect-error intentionally invalid
spec.paths['/cats'][xKongRouteDefaults] = null;
const specResult = getSpecResult();
expect(generateServices(spec, tags)).toEqual([specResult]);
expect(await generateServices(spec, tags)).toEqual([specResult]);
});
it('throws for an operation level x-kong-route-default', () => {
@ -106,27 +106,27 @@ describe('services', () => {
const fn = () => generateServices(spec, tags);
expect(fn).toThrowError(
expect(fn).rejects.toThrowError(
'expected \'x-kong-route-defaults\' to be an object (at operation \'post\' of path \'/cats\')',
);
});
it('ignores null for an operation level x-kong-route-default', () => {
it('ignores null for an operation level x-kong-route-default', async () => {
const spec = getSpec();
// @ts-expect-error intentionally invalid
spec.paths['/cats'].post[xKongRouteDefaults] = null;
const specResult = getSpecResult();
expect(generateServices(spec, tags)).toEqual([specResult]);
expect(await generateServices(spec, tags)).toEqual([specResult]);
});
});
describe('generateServices()', () => {
it('generates generic service with paths', () => {
it('generates generic service with paths', async () => {
const spec = getSpec();
const specResult = getSpecResult();
expect(generateServices(spec, tags)).toEqual([specResult]);
expect(await generateServices(spec, tags)).toEqual([specResult]);
});
it('generates routes with request validator plugin from operation over path over global', () => {
it('generates routes with request validator plugin from operation over path over global', async () => {
const spec = getSpec({
// global req validator plugin
[xKongPluginRequestValidator]: {
@ -271,10 +271,10 @@ describe('services', () => {
],
},
];
expect(generateServices(spec, tags)).toEqual([specResult]);
expect(await generateServices(spec, tags)).toEqual([specResult]);
});
it('generates routes with plugins from operation over path', () => {
it('generates routes with plugins from operation over path', async () => {
const spec = getSpec();
spec.paths = {
'/dogs': {
@ -333,10 +333,10 @@ describe('services', () => {
],
},
];
expect(generateServices(spec, tags)).toEqual([specResult]);
expect(await generateServices(spec, tags)).toEqual([specResult]);
});
it('replaces variables', () => {
it('replaces variables', async () => {
const spec = getSpec();
spec.servers = [
{
@ -356,7 +356,7 @@ describe('services', () => {
specResult.port = 8443;
specResult.host = 'demo.saas-app.com';
specResult.path = '/v2';
expect(generateServices(spec, tags)).toEqual([specResult]);
expect(await generateServices(spec, tags)).toEqual([specResult]);
});
describe('x-kong-route-defaults and strip_path', () => {
@ -374,33 +374,33 @@ describe('services', () => {
operationLevel: true,
} as unknown as DCRoute;
it('root level', () => {
it('root level', async () => {
const spec = getSpec({
[xKongRouteDefaults]: rootLevel,
});
const specResult = getSpecResult();
specResult.routes = specResult.routes.map(route => ({ ...route, ...rootLevel }));
expect(generateServices(spec, tags)).toEqual([specResult]);
expect(await generateServices(spec, tags)).toEqual([specResult]);
});
it('path level', () => {
it('path level', async () => {
const spec = getSpec();
spec.paths['/dogs'][xKongRouteDefaults] = pathLevel;
const specResult = getSpecResult();
specResult.routes[1] = { ...specResult.routes[1], ...pathLevel };
specResult.routes[2] = { ...specResult.routes[2], ...pathLevel };
expect(generateServices(spec, tags)).toEqual([specResult]);
expect(await generateServices(spec, tags)).toEqual([specResult]);
});
it('operation level', () => {
it('operation level', async () => {
const spec = getSpec();
(spec.paths['/dogs'].get as OA3Operation)[xKongRouteDefaults] = operationLevel;
const specResult = getSpecResult();
specResult.routes[1] = { ...specResult.routes[1], ...operationLevel };
expect(generateServices(spec, tags)).toEqual([specResult]);
expect(await generateServices(spec, tags)).toEqual([specResult]);
});
it('will select (but not merge) the operation level over the root level', () => {
it('will select (but not merge) the operation level over the root level', async () => {
const spec = getSpec({
[xKongRouteDefaults]: rootLevel,
});
@ -410,20 +410,20 @@ describe('services', () => {
...route,
...(route.paths[0] === '/cats$' ? operationLevel : rootLevel),
}));
expect(generateServices(spec, tags)).toEqual([specResult]);
expect(await generateServices(spec, tags)).toEqual([specResult]);
});
it('will select (but not merge) the operation level over the path level', () => {
it('will select (but not merge) the operation level over the path level', async () => {
const spec = getSpec();
spec.paths['/dogs'][xKongRouteDefaults] = pathLevel;
(spec.paths['/dogs'].post as OA3Operation)[xKongRouteDefaults] = operationLevel;
const specResult = getSpecResult();
specResult.routes[1] = { ...specResult.routes[1], ...pathLevel };
specResult.routes[2] = { ...specResult.routes[2], ...operationLevel };
expect(generateServices(spec, tags)).toEqual([specResult]);
expect(await generateServices(spec, tags)).toEqual([specResult]);
});
it('will select (but not merge) the path level over the root level', () => {
it('will select (but not merge) the path level over the root level', async () => {
const spec = getSpec({
[xKongRouteDefaults]: rootLevel,
});
@ -433,19 +433,19 @@ describe('services', () => {
...route,
...(route.paths[0] === '/cats$' ? pathLevel : rootLevel),
}));
expect(generateServices(spec, tags)).toEqual([specResult]);
expect(await generateServices(spec, tags)).toEqual([specResult]);
});
it('allows overriding strip_path at the path level', () => {
it('allows overriding strip_path at the path level', async () => {
const spec = getSpec();
spec.paths['/cats'][xKongRouteDefaults] = { strip_path: true };
const specResult = getSpecResult();
const cats = specResult.routes.find(route => route.paths[0] === '/cats$') as DCRoute;
cats.strip_path = true;
expect(generateServices(spec, tags)).toEqual([specResult]);
expect(await generateServices(spec, tags)).toEqual([specResult]);
});
it('allows overriding `strip_path` from `x-kong-route-defaults` at the root', () => {
it('allows overriding `strip_path` from `x-kong-route-defaults` at the root', async () => {
const spec = getSpec({
[xKongRouteDefaults]: {
strip_path: true,
@ -453,7 +453,7 @@ describe('services', () => {
});
const specResult = getSpecResult();
specResult.routes = specResult.routes.map(route => ({ ...route, strip_path: true }));
expect(generateServices(spec, tags)).toEqual([specResult]);
expect(await generateServices(spec, tags)).toEqual([specResult]);
});
});
});

View File

@ -4,13 +4,12 @@ import {
getAllServers,
getName,
hasUpstreams,
HttpMethod,
parseUrl,
pathVariablesToRegex,
} from '../common';
import { DCRoute, DCService } from '../types/declarative-config';
import { xKongName, xKongServiceDefaults } from '../types/kong';
import { OA3PathItem, OA3Server, OpenApi3Spec } from '../types/openapi3';
import { HttpMethods, OA3PathItem, OA3Server, OpenApi3Spec } from '../types/openapi3';
import {
generateGlobalPlugins,
generateOperationPlugins,
@ -20,7 +19,7 @@ import {
import { generateSecurityPlugins } from './security-plugins';
import { appendUpstreamToName } from './upstreams';
export function generateServices(api: OpenApi3Spec, tags: string[]) {
export async function generateServices(api: OpenApi3Spec, tags: string[]) {
const servers = getAllServers(api);
if (servers.length === 0) {
@ -28,22 +27,22 @@ export function generateServices(api: OpenApi3Spec, tags: string[]) {
}
// only support one service for now
const service = generateService(servers[0], api, tags);
const service = await generateService(servers[0], api, tags);
return [service];
}
export function generateService(server: OA3Server, api: OpenApi3Spec, tags: string[]) {
export async function generateService(server: OA3Server, api: OpenApi3Spec, tags: string[]) {
const serverUrl = fillServerVariables(server);
const name = getName(api);
const parsedUrl = parseUrl(serverUrl);
let host = parsedUrl.hostname;
if (hasUpstreams(api)) {
host = appendUpstreamToName(name);
host = appendUpstreamToName(name);
}
// Service plugins
const globalPlugins = generateGlobalPlugins(api, tags);
const globalPlugins = await generateGlobalPlugins(api, tags);
const serviceDefaults = api[xKongServiceDefaults] || {};
if (typeof serviceDefaults !== 'object') {
@ -128,11 +127,12 @@ export function generateService(server: OA3Server, api: OpenApi3Spec, tags: stri
// Path plugin takes precedence over global
const parentValidatorPlugin = pathValidatorPlugin || globalPlugins.requestValidatorPlugin;
const regularPlugins = generateOperationPlugins({
const regularPlugins = await generateOperationPlugins({
operation,
pathPlugins,
parentValidatorPlugin,
tags,
api,
});
const plugins = [...regularPlugins, ...securityPlugins];
@ -151,7 +151,7 @@ export function generateService(server: OA3Server, api: OpenApi3Spec, tags: stri
export function generateRouteName(
api: OpenApi3Spec,
routePath: string,
method: keyof typeof HttpMethod,
method: HttpMethods
) {
const name = getName(api);
const pathItem = api.paths[routePath];

View File

@ -7,17 +7,17 @@ import { generateServices } from './services';
import { generateUpstreams } from './upstreams';
describe('tags', () => {
it('test that tags are appended to Service entities', () => {
it('test that tags are appended to Service entities', async () => {
const spec = getSpec();
const services = generateServices(spec, tags);
const services = await generateServices(spec, tags);
services.forEach(service => {
expect(service.tags).toEqual(tags);
});
});
it('test that tags are appended to Route entities', () => {
it('test that tags are appended to Route entities', async () => {
const spec = getSpec();
const services = generateServices(spec, tags);
const services = await generateServices(spec, tags);
services.forEach(service => {
service.routes.forEach(route => {
expect(route.tags).toEqual(tags);

View File

@ -1,5 +1,6 @@
import { describe, expect, it } from '@jest/globals';
import fs from 'fs';
import { OpenAPIV3 } from 'openapi-types';
import path from 'path';
import YAML from 'yaml';
@ -15,6 +16,7 @@ const firstK8sDocument = {
},
route: {
methods: [
// There was some discrepency how we "generate" mock data and how we implemented it
'get',
],
},
@ -91,7 +93,7 @@ describe('top-level API exports', () => {
const parsedSpec = YAML.parse(dcFixtureFileString);
const {
documents: [dc],
} = generateFromSpec(parsedSpec, 'kong-declarative-config') as DeclarativeConfigResult;
} = await generateFromSpec(parsedSpec, 'kong-declarative-config') as DeclarativeConfigResult;
expect(dc._format_version).toBe('1.1');
expect(dc.services.length).toBe(1);
expect(dc).not.toHaveProperty('upstreams');
@ -104,7 +106,7 @@ describe('top-level API exports', () => {
label,
documents,
warnings,
} = generateFromSpec(parsedSpec, 'kong-for-kubernetes') as KongForKubernetesResult;
} = await generateFromSpec(parsedSpec, 'kong-for-kubernetes') as KongForKubernetesResult;
expect(type).toBe('kong-for-kubernetes');
expect(label).toBe('Kong for Kubernetes');
expect(documents).toHaveLength(9);
@ -133,24 +135,25 @@ describe('top-level API exports', () => {
name: {
type: 'string',
},
},
} as OpenAPIV3.SchemaObject,
},
},
};
const specResolved: OpenApi3Spec = {
openapi: '3.0.0',
components: partialSpec.components,
info: {},
info: {
title: '',
version: '',
},
paths: {
'/': {
post: {
responses: {
200: {
name: {
type: 'string',
},
},
},
'$ref': '#/components/schemas/dog',
} as OpenAPIV3.SchemaObject,
} as OpenAPIV3.ResponsesObject,
},
},
},

View File

@ -1,6 +1,6 @@
import SwaggerParser from '@apidevtools/swagger-parser';
import fs from 'fs';
import path from 'path';
import SwaggerParser from 'swagger-parser';
import YAML from 'yaml';
import { generateDeclarativeConfigFromSpec } from './declarative-config/generate';
@ -30,18 +30,20 @@ export const parseSpec = (spec: string | Record<string, any>) => {
// Ensure it has some required properties to make parsing a bit less strict
if (!api.info) {
api.info = {};
api.info = {
title: '',
version: '',
};
}
if (api.openapi === '3.0') {
api.openapi = '3.0.0';
}
// @ts-expect-error until we make our OpenAPI type extend from the canonical one (i.e. from `openapi-types`, we'll need to shim this here)
return SwaggerParser.dereference(api) as Promise<OpenApi3Spec>;
return SwaggerParser.bundle(api) as Promise<OpenApi3Spec>;
};
export const generateFromSpec = (
export const generateFromSpec = async (
api: OpenApi3Spec,
type: ConversionResultType,
tags: string[] = [],
@ -50,7 +52,7 @@ export const generateFromSpec = (
switch (type) {
case 'kong-declarative-config':
return generateDeclarativeConfigFromSpec(api, allTags);
return await generateDeclarativeConfigFromSpec(api, allTags);
case 'kong-for-kubernetes':
return generateKongForKubernetesConfigFromSpec(api);

View File

@ -47,6 +47,8 @@ describe('index', () => {
'x-kubernetes-ingress-metadata': {
name: 'K8s name',
},
title: '',
version: '',
},
});
expect(getSpecName(spec)).toBe('k8s-name');
@ -63,6 +65,8 @@ describe('index', () => {
'nginx.ingress.kubernetes.io/rewrite-target': '/',
},
},
title: '',
version: '',
},
});
const result = generateMetadataAnnotations(spec, {
@ -117,6 +121,8 @@ describe('index', () => {
name: 'info-name',
annotations,
},
title: '',
version: '',
},
});
const result = generateMetadataAnnotations(spec, {
@ -610,9 +616,9 @@ describe('index', () => {
...pluginKeyAuth,
paths: {
'/path': {
GET: {},
PUT: { ...pluginKeyAuth },
POST: { ...pluginKeyAuth, ...pluginDummy },
get: {},
put: { ...pluginKeyAuth },
post: { ...pluginKeyAuth, ...pluginDummy },
},
},
});

View File

@ -40,10 +40,10 @@ export const methodDoc = (method: HttpMethodType | Lowercase<HttpMethodType>): K
apiVersion: 'configuration.konghq.com/v1',
kind: 'KongIngress',
metadata: {
name: `${method}-method`,
name: `${method}-method`.toLowerCase(),
},
route: {
methods: [method.toUpperCase() as HttpMethodType],
methods: [method],
},
});

View File

@ -1,4 +1,5 @@
import { beforeEach, describe, expect, it, jest } from '@jest/globals';
import { OpenAPIV3 } from 'openapi-types';
import { HttpMethod } from '../common';
import { dummyPluginDoc, pluginDummy } from '../declarative-config/jest/test-helpers';
@ -61,7 +62,6 @@ describe('plugins', () => {
const components: OA3Components = {
securitySchemes: {
// @ts-expect-error -- TSCONVERSION
really_basic: {
type: 'http',
scheme: 'basic',
@ -78,7 +78,6 @@ describe('plugins', () => {
},
petstore_oauth2: {
type: 'oauth2',
// @ts-expect-error -- TSCONVERSION
flows: {
clientCredentials: {
tokenUrl: 'http://example.org/api/oauth/dialog',
@ -89,7 +88,6 @@ describe('plugins', () => {
},
},
},
// @ts-expect-error -- TSCONVERSION
petstore_openid: {
type: 'openIdConnect',
openIdConnectUrl: 'http://example.org/oid-discovery',
@ -362,24 +360,24 @@ describe('plugins', () => {
it('should return plugins for all operations on path', () => {
const pathItem: OA3PathItem = {
[HttpMethod.get]: {},
[HttpMethod.put]: { ...pluginKeyAuth, ...pluginDummy },
[HttpMethod.post]: pluginDummy as OA3Operation,
get: {},
put: { ...pluginKeyAuth, ...pluginDummy },
post: pluginDummy as OA3Operation,
};
const result = getOperationPlugins(pathItem, increment, spec);
expect(result).toHaveLength(3);
const get = result[0];
expect(get.method).toBe(HttpMethod.get);
expect(get.method).toBe('get');
expect(get.plugins).toHaveLength(0);
const put = result[1];
expect(put.method).toBe(HttpMethod.put);
expect(put.method).toBe('put');
expect(put.plugins).toEqual([keyAuthPluginDoc('m0'), dummyPluginDoc('m1')]);
const post = result[2];
expect(post.method).toBe(HttpMethod.post);
expect(post.method).toBe('post');
expect(post.plugins).toEqual([dummyPluginDoc('m2')]);
});
it.each(Object.values(HttpMethod))(
it.each(Object.values(OpenAPIV3.HttpMethods))(
'should extract method plugins for %o from path item',
methodName => {
const pathItem = {
@ -396,7 +394,7 @@ describe('plugins', () => {
it('should return security plugin from operation', () => {
const api: OpenApi3Spec = { ...spec, components };
const pathItem: OA3PathItem = {
[HttpMethod.get]: {
'get': {
security: [
{
really_basic: [],

View File

@ -1,4 +1,5 @@
import { HttpMethodType } from '../common';
import type { OpenAPIV3 } from 'openapi-types';
import {
XKongName,
XKongPluginKeyAuth,
@ -9,68 +10,31 @@ import {
XKongUpstreamDefaults,
} from './kong';
import { K8sIngressTLS } from './kubernetes-config';
import { Taggable } from './outputs';
export interface StripPath {
// eslint-disable-next-line camelcase -- this is defined by a spec that is out of our control
strip_path?: boolean;
}
// eslint-disable-next-line camelcase -- this is defined by a spec that is out of our control
strip_path?: boolean;
}
export interface OA3Info {
title?: string;
version?: string;
description?: string;
termsOfService?: string;
contact?: {
name?: string;
url?: string;
email?: string;
};
license?: {
name: string;
url?: string;
};
'x-kubernetes-ingress-metadata'?: {
name?: string;
annotations?: Record<string, any>;
};
}
export interface OA3InfoObjectKubernetesIngressMetadata {
'x-kubernetes-ingress-metadata'?: {
name?: string;
annotations?: Record<string, any>;
};
}
export interface OA3ExternalDocs {
url: string;
description?: string;
}
export type OA3Info = OpenAPIV3.InfoObject & OA3InfoObjectKubernetesIngressMetadata;
export interface OA3Parameter {
name: string;
in: 'query' | 'header' | 'path' | 'cookie';
description?: string;
required?: boolean;
deprecated?: boolean;
allowEmptyValue?: boolean;
style?: 'form' | 'spaceDelimited' | 'pipeDelimited' | 'deepObject' | string;
schema?: Record<string, any> | string;
content?: Record<string, any>;
explode?: boolean;
}
export type OA3ExternalDocs = OpenAPIV3.ExternalDocumentationObject;
export type OA3Parameter = OpenAPIV3.ParameterObject;
/** see: https://swagger.io/specification/#request-body-object */
export interface OA3RequestBody {
content?: Record<string, any>; // TODO
description?: string;
required?: boolean;
}
export type OA3SecurityRequirement = Record<string, any>;
/** see: https://swagger.io/specification/#reference-object */
export interface OA3Reference {
$ref: string;
}
export type OA3RequestBody = OpenAPIV3.RequestBodyObject | OpenAPIV3.ReferenceObject;
export type OA3SecurityRequirement = OpenAPIV3.SecurityRequirementObject;
export interface OA3ServerKubernetesTLS {
'x-kubernetes-tls'?: K8sIngressTLS[];
}
'x-kubernetes-tls'?: K8sIngressTLS[];
}
export interface OA3ServerKubernetesBackend {
'x-kubernetes-backend'?: {
@ -93,57 +57,26 @@ export interface OA3ServerKubernetesService {
}
/** see: https://swagger.io/specification/#server-variable-object */
export interface OA3ServerVariable {
default: string;
enum?: string[];
description?: string;
}
export type OA3ServerVariable = OpenAPIV3.ServerVariableObject;
/** see: https://swagger.io/specification/#server-object */
export type OA3Server = {
url: string;
description?: string;
variables?: Record<string, OA3ServerVariable>;
} & OA3ServerKubernetesTLS
export type OA3Server = OpenAPIV3.ServerObject
& OA3ServerKubernetesTLS
& OA3ServerKubernetesBackend
& OA3ServerKubernetesService;
export interface OA3ResponsesObject {
$ref?: string;
}
export type OA3ResponsesObject = OpenAPIV3.ResponsesObject;
/** see: https://swagger.io/specification/#operation-object */
export type OA3Operation = {
description?: string;
summary?: string;
externalDocs?: OA3ExternalDocs;
responses?: OA3ResponsesObject;
operationId?: string;
parameters?: (OA3Parameter | OA3Reference)[];
requestBody?: OA3RequestBody | OA3Reference;
deprecated?: boolean;
security?: OA3SecurityRequirement[];
servers?: OA3Server[];
} & Taggable
& XKongName
& XKongRouteDefaults
& XKongPluginKeyAuth
& XKongPluginRequestValidator
;
export type OA3Operation = OpenAPIV3.OperationObject<XKongName & XKongRouteDefaults & XKongPluginKeyAuth & XKongPluginRequestValidator>;
type HTTPMethodPaths = Partial<Record<
HttpMethodType | Lowercase<HttpMethodType>,
OA3Operation
>>;
export type HttpMethods = OpenAPIV3.HttpMethods | Lowercase<OpenAPIV3.HttpMethods>;
/** see: https://swagger.io/specification/#path-item-object */
export type OA3PathItem = {
$ref?: string;
summary?: string;
description?: string;
servers?: OA3Server[];
parameters?: OA3Reference | OA3Parameter;
} & HTTPMethodPaths
export type OA3PathItem = OpenAPIV3.PathItemObject
& {
[method in HttpMethods]?: OA3Operation;
}
& XKongName
& XKongRouteDefaults
& XKongPluginRequestValidator
@ -151,111 +84,44 @@ export type OA3PathItem = {
;
/** see: https://swagger.io/specification/#paths-object */
export type OA3Paths = Record<string, OA3PathItem>
export type OA3Paths = {
[pattern: string]: OA3PathItem;
}
& StripPath
& XKongRouteDefaults
;
/** see: https://swagger.io/specification/#security-scheme-object */
export interface OA3SecuritySchemeApiKey {
type: 'apiKey';
name: string;
in: 'query' | 'header' | 'cookie';
description?: string;
}
/** see: https://swagger.io/specification/#security-scheme-object */
export interface OA3SecuritySchemeHttp {
type: 'http';
name: string;
scheme: string;
bearerFormat?: string;
description?: string;
}
/** see: https://swagger.io/specification/#security-scheme-object */
export interface OA3SecuritySchemeOpenIdConnect {
type: 'openIdConnect';
name: string;
openIdConnectUrl: string;
description?: string;
}
/** see: https://swagger.io/specification/#security-scheme-object */
export interface OA3SecuritySchemeOAuth2Flow {
authorizationUrl?: string;
tokenUrl?: string;
refreshUrl?: string;
scopes: Record<string, string>;
}
/** see: https://swagger.io/specification/#security-scheme-object */
export interface OA3SecuritySchemeOAuth2 {
type: 'oauth2';
name: string;
flows: {
implicit: OA3SecuritySchemeOAuth2Flow;
password: OA3SecuritySchemeOAuth2Flow;
clientCredentials: OA3SecuritySchemeOAuth2Flow;
authorizationCode: OA3SecuritySchemeOAuth2Flow;
};
description?: string;
}
/** see: https://swagger.io/specification/#security-scheme-object */
export type OA3SecurityScheme =
| OA3SecuritySchemeApiKey
| OA3SecuritySchemeHttp
| OA3SecuritySchemeOpenIdConnect
| OA3SecuritySchemeOAuth2;
export type OA3SecurityScheme = OpenAPIV3.SecuritySchemeObject;
/** see: https://swagger.io/specification/#example-object */
export interface OA3Example {
summary?: string;
description?: string;
value?: any;
externalValue?: string;
}
export type OA3Example = OpenAPIV3.ExampleObject;
/** see: https://swagger.io/specification/#schema-object */
export interface OA3Schema {}
export type OA3Schema = OpenAPIV3.SchemaObject;
/** see: https://swagger.io/specification/#header-object */
export interface OA3Header {
description?: string;
required?: boolean;
deprecated?: boolean;
allowEmptyValue?: boolean;
}
export type OA3Header = OpenAPIV3.HeaderObject;
/** see: https://swagger.io/specification/#components-object */
export interface OA3Components {
schemas?: Record<string, OA3Schema | OA3Reference>;
parameters?: Record<string, OA3Parameter | OA3Reference>;
headers?: Record<string, OA3Header | OA3Reference>;
requestBodies?: Record<string, OA3RequestBody | OA3Reference>;
examples?: Record<string, OA3Example | OA3Reference>;
securitySchemes?: Record<string, OA3SecurityScheme | OA3Reference>;
}
export type OA3Components = OpenAPIV3.ComponentsObject;
/** see: https://swagger.io/specification/#tag-object */
export interface TagObject {
name: string;
description?: string;
externalDocs?: Record<string, any>;
}
export type TagObject = OpenAPIV3.TagObject;
/** see: https://swagger.io/specification/#openapi-object */
export type OpenApi3Spec = {
openapi: string;
info: OA3Info;
paths: OA3Paths;
servers?: OA3Server[];
components?: OA3Components;
security?: OA3SecurityRequirement[];
externalDocs?: OA3ExternalDocs;
tags?: TagObject[];
}
export type OpenApi3Spec =
{
openapi: string;
info: OA3Info;
servers?: OA3Server[];
paths: OA3Paths;
components?: OA3Components;
security?: OA3SecurityRequirement[];
tags?: TagObject[];
externalDocs?: OA3ExternalDocs;
'x-express-openapi-additional-middleware'?: (((request: any, response: any, next: any) => Promise<void>) | ((request: any, response: any, next: any) => void))[];
'x-express-openapi-validation-strict'?: boolean;
}
& XKongName
& XKongPluginKeyAuth
& XKongPluginRequestTermination