From 2bf4586dc7f9b16b483ef7e971bc13843e2cb495 Mon Sep 17 00:00:00 2001 From: Dimitri Mitropoulos Date: Tue, 25 May 2021 16:05:52 -0400 Subject: [PATCH] [o2k] copy plugin objects as is (and respect enabled flag) (#3419) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * clears all errors in tests, enables overrides for spec generators * removes copy/pasta in tests, uses spec generator, removes useless async continuing from the last commit, but separated out for easier review * consolidates openapi x-kong extension to one place, uses consistently these were used for some things, not used for others, exported in some places, not exported in others. now it's uniform and we can look up references for each type. * adds failing test for ticket * only adds `enabled` where it was specified * allows adding passing top-level properties to config now, the line that was there specifically for `config` is redundant. * adds tags to test result per review feedback * removes unnecessary generic for some reason, this didn't error before, but now it is. * removes unnecessary argument this failed on the CI with: openapi-2-kong: FAIL src/generate.test.ts openapi-2-kong: ● Test suite failed to run openapi-2-kong: src/generate.test.ts:90:58 - error TS2769: No overload matches this call. openapi-2-kong: Overload 1 of 2, '(src: string, options?: (ParseOptions & DocumentOptions & SchemaOptions & ToJSOptions) | undefined): any', gave the following error. openapi-2-kong: Argument of type '"utf8"' is not assignable to parameter of type '(ParseOptions & DocumentOptions & SchemaOptions & ToJSOptions) | undefined'. openapi-2-kong: Overload 2 of 2, '(src: string, reviver: Reviver, options?: (ParseOptions & DocumentOptions & SchemaOptions & ToJSOptions) | undefined): any', gave the following error. openapi-2-kong: Argument of type 'string' is not assignable to parameter of type 'Reviver'. openapi-2-kong: 90 const parsedSpec = YAML.parse(dcFixtureFileString, 'utf8'); openapi-2-kong: ~~~~~~ openapi-2-kong: src/generate.test.ts:100:59 - error TS2769: No overload matches this call. openapi-2-kong: Overload 1 of 2, '(src: string, options?: (ParseOptions & DocumentOptions & SchemaOptions & ToJSOptions) | undefined): any', gave the following error. openapi-2-kong: Argument of type '"utf8"' is not assignable to parameter of type '(ParseOptions & DocumentOptions & SchemaOptions & ToJSOptions) | undefined'. openapi-2-kong: Overload 2 of 2, '(src: string, reviver: Reviver, options?: (ParseOptions & DocumentOptions & SchemaOptions & ToJSOptions) | undefined): any', gave the following error. openapi-2-kong: Argument of type 'string' is not assignable to parameter of type 'Reviver'. openapi-2-kong: 100 const parsedSpec = YAML.parse(k8sFixtureFileString, 'utf8'); --- packages/openapi-2-kong/jest.config.js | 5 + packages/openapi-2-kong/src/common.test.ts | 96 +++++---------- packages/openapi-2-kong/src/common.ts | 3 +- .../request-validator-plugin.expected.json | 3 - .../src/declarative-config/names.test.ts | 10 +- .../src/declarative-config/plugins.test.ts | 68 +++++++---- .../src/declarative-config/plugins.ts | 14 ++- .../security-plugins.test.ts | 2 +- .../src/declarative-config/services.test.ts | 113 ++++++++++-------- .../src/declarative-config/services.ts | 12 +- .../src/declarative-config/upstreams.test.ts | 19 +-- .../src/declarative-config/upstreams.ts | 6 +- .../src/declarative-config/utils.ts | 3 +- packages/openapi-2-kong/src/generate.test.ts | 4 +- packages/openapi-2-kong/src/generate.ts | 2 +- .../src/kubernetes/fixtures.test.ts | 2 +- .../src/kubernetes/generate.test.ts | 6 +- .../src/kubernetes/plugin-helpers.ts | 4 +- .../src/types/declarative-config.ts | 2 +- packages/openapi-2-kong/src/types/openapi3.ts | 60 +++++++--- 20 files changed, 236 insertions(+), 198 deletions(-) diff --git a/packages/openapi-2-kong/jest.config.js b/packages/openapi-2-kong/jest.config.js index 0f631282ec..82d81e5799 100644 --- a/packages/openapi-2-kong/jest.config.js +++ b/packages/openapi-2-kong/jest.config.js @@ -1,4 +1,9 @@ /** @type { import('@jest/types').Config.InitialOptions } */ module.exports = { preset: '../../jest-preset.js', + globals: { + 'ts-jest': { + isolatedModules: false, + }, + }, }; diff --git a/packages/openapi-2-kong/src/common.test.ts b/packages/openapi-2-kong/src/common.test.ts index 7378743b2f..0e58470cee 100644 --- a/packages/openapi-2-kong/src/common.test.ts +++ b/packages/openapi-2-kong/src/common.test.ts @@ -14,24 +14,10 @@ import { parseUrl, pathVariablesToRegex, } from './common'; -import { parseSpec } from './generate'; -import { OpenApi3Spec, OA3Operation, OA3Server } from './types/openapi3'; +import { getSpec } from './declarative-config/utils'; +import { OA3Operation, OA3Server, xKongName } from './types/openapi3'; describe('common', () => { - const spec: OpenApi3Spec = { - openapi: '3.0.0', - info: { - version: '1.0.0', - title: 'Swagger Petstore', - }, - servers: [ - { - url: 'https://server1.com/path', - }, - ], - paths: {}, - }; - describe('getPaths()', () => { it('should return api paths', () => { const paths = { @@ -39,19 +25,14 @@ describe('common', () => { description: 'test', }, }; - const api = { ...spec, paths }; + const api = getSpec({ paths }); const result = getPaths(api); - expect(result).toBe(paths); + expect(result).toStrictEqual(paths); }); }); describe('getServers()', () => { - const spec = { - openapi: '3.0.0', - info: { - version: '1.0.0', - title: 'Swagger Petstore', - }, + const spec = getSpec({ servers: [ { url: 'https://server1.com/path', @@ -66,11 +47,10 @@ describe('common', () => { ], }, }, - }; + }); - it('returns path item servers', async () => { - const s = await parseSpec(spec); - const result = getServers(s.paths['/']); + it('returns path item servers', () => { + const result = getServers(spec.paths['/']); expect(result).toEqual([ { url: 'https://server2.com/path', @@ -78,9 +58,8 @@ describe('common', () => { ]); }); - it('returns api servers', async () => { - const s = await parseSpec(spec); - const result = getServers(s); + it('returns api servers', () => { + const result = getServers(spec); expect(result).toEqual([ { url: 'https://server1.com/path', @@ -97,7 +76,6 @@ describe('common', () => { petstoreAuth: [], }, ], - // @ts-expect-error -- TSCONVERSION not sure, but this appears to be wrong maybe? responses: {}, }; const result = getSecurity(operation); @@ -109,12 +87,7 @@ describe('common', () => { }); it('returns security from api', () => { - const spec: OpenApi3Spec = { - openapi: '3.0.0', - info: { - version: '1.0.0', - title: 'Swagger Petstore', - }, + const spec = getSpec({ servers: [ { url: 'https://server1.com/path', @@ -151,7 +124,7 @@ describe('common', () => { }, }, }, - }; + }); const result = getSecurity(spec); expect(result).toEqual([ { @@ -162,48 +135,33 @@ describe('common', () => { }); describe('getName()', () => { - const spec = { - openapi: '3.0.0', - info: { - version: '1.0.0', - title: 'Swagger Petstore', - }, - servers: [ - { - url: 'https://server1.com/path', - }, - ], - paths: {}, - }; - - it('openapi object with x-kong-name', async () => { - const s = await parseSpec({ ...spec, 'x-kong-name': 'override' }); - const result = getName(s); + it('openapi object with x-kong-name', () => { + const spec = getSpec({ [xKongName]: 'override' }); + const result = getName(spec); expect(result).toBe('override'); }); - it('openapi object without x-kong-name', async () => { - const s = await parseSpec(spec); - const result = getName(s); - expect(result).toBe('Swagger_Petstore'); + it('openapi object without x-kong-name', () => { + const spec = getSpec(); + const result = getName(spec); + expect(result).toBe('My_API'); }); - it('openapi object without anything', async () => { - const s = await parseSpec({ - ...spec, + it('openapi object without anything', () => { + const spec = getSpec({ info: { version: '1.0.0', }, }); - const result = getName(s); + const result = getName(spec); expect(result).toBe('openapi'); - const result2 = getName(s, 'Another Default'); + const result2 = getName(spec, 'Another Default'); expect(result2).toBe('Another_Default'); }); it('works with slugify options', () => { - const p = { ...spec, 'x-kong-name': 'This Needs Slugify' }; - const result = getName(p, '', { + const spec = getSpec({ [xKongName]: 'This Needs Slugify' }); + const result = getName(spec, '', { replacement: '?', lower: true, }); @@ -254,7 +212,7 @@ describe('common', () => { const server: OA3Server = { url: 'https://{subdomain}.swagger.io/v1', variables: { - // @ts-expect-error intentionally missing a required value + // @ts-expect-error intentionally invalid - missing a required property, `default` subdomain: { enum: ['petstore'], }, @@ -291,7 +249,7 @@ describe('common', () => { }); it('should be false if key is not prefixed by x-kong-plugin-', () => { - expect(isPluginKey('x-kong-name')).toBe(false); + expect(isPluginKey(xKongName)).toBe(false); }); }); diff --git a/packages/openapi-2-kong/src/common.ts b/packages/openapi-2-kong/src/common.ts index 6bfd30d3eb..121c4ccef0 100644 --- a/packages/openapi-2-kong/src/common.ts +++ b/packages/openapi-2-kong/src/common.ts @@ -5,6 +5,7 @@ import { OA3PathItem, OA3Server, OA3Operation, + xKongName, } from './types/openapi3'; export function getServers(obj: OpenApi3Spec | OA3PathItem) { @@ -46,7 +47,7 @@ export function getName( rawName = isKubernetes ? api.info?.['x-kubernetes-ingress-metadata']?.name : ''; // Get $.x-kong-name - rawName = rawName || api['x-kong-name']; + rawName = rawName || api[xKongName]; // Get $.info.title rawName = rawName || api.info?.title; diff --git a/packages/openapi-2-kong/src/declarative-config/fixtures/request-validator-plugin.expected.json b/packages/openapi-2-kong/src/declarative-config/fixtures/request-validator-plugin.expected.json index a33832cdcf..82a2f73891 100644 --- a/packages/openapi-2-kong/src/declarative-config/fixtures/request-validator-plugin.expected.json +++ b/packages/openapi-2-kong/src/declarative-config/fixtures/request-validator-plugin.expected.json @@ -13,7 +13,6 @@ "body_schema": "{}", "version": "draft4" }, - "enabled": true, "tags": [ "OAS3_import", "OAS3file_request-validator-plugin.yaml" @@ -40,7 +39,6 @@ "body_schema": "{\"type\":\"object\",\"properties\":{\"id\":{\"type\":\"integer\"},\"name\":{\"type\":\"string\"}}}", "version": "draft4" }, - "enabled": true, "tags": [ "OAS3_import", "OAS3file_request-validator-plugin.yaml" @@ -71,7 +69,6 @@ "body_schema": "{\"type\":\"object\",\"properties\":{\"id\":{\"type\":\"integer\"},\"name\":{\"type\":\"string\"}}}", "version": "draft4" }, - "enabled": true, "tags": [ "OAS3_import", "OAS3file_request-validator-plugin.yaml" diff --git a/packages/openapi-2-kong/src/declarative-config/names.test.ts b/packages/openapi-2-kong/src/declarative-config/names.test.ts index c3a40e4d12..3f70897ee2 100644 --- a/packages/openapi-2-kong/src/declarative-config/names.test.ts +++ b/packages/openapi-2-kong/src/declarative-config/names.test.ts @@ -1,4 +1,4 @@ -import { OA3PathItem, OpenApi3Spec } from '../types/openapi3'; +import { OA3PathItem, OpenApi3Spec, xKongName } from '../types/openapi3'; import { generateRouteName } from './services'; const api: OpenApi3Spec = { @@ -35,9 +35,9 @@ const compare = (expected: string, pathItem: OA3PathItem) => { describe('names', () => { it("api.paths[path][method]['x-kong-name'] is highest priority", () => { compare('Nebulo_9-method_smash', { - 'x-kong-name': 'pathItem-smash', + [xKongName]: 'pathItem-smash', post: { - 'x-kong-name': 'method-smash', + [xKongName]: 'method-smash', operationId: 'operationId-smash', }, }); @@ -45,7 +45,7 @@ describe('names', () => { it('api.paths[path][method].operationId is second priority (and not slugified)', () => { compare('Nebulo_9-operationId_smash', { - 'x-kong-name': 'pathItem-smash', + [xKongName]: 'pathItem-smash', post: { operationId: 'operationId-smash', }, @@ -54,7 +54,7 @@ describe('names', () => { it("api.paths[path]['x-kong-name'] is third priority", () => { compare('Nebulo_9-pathItem_smash-post', { - 'x-kong-name': 'pathItem-smash', + [xKongName]: 'pathItem-smash', post: {}, }); }); diff --git a/packages/openapi-2-kong/src/declarative-config/plugins.test.ts b/packages/openapi-2-kong/src/declarative-config/plugins.test.ts index 8ab02d2eca..03981212e2 100644 --- a/packages/openapi-2-kong/src/declarative-config/plugins.test.ts +++ b/packages/openapi-2-kong/src/declarative-config/plugins.test.ts @@ -1,20 +1,15 @@ import { generateGlobalPlugins, generateRequestValidatorPlugin } from './plugins'; -import { OpenApi3Spec, OA3Operation, OA3Parameter } from '../types/openapi3'; +import { OA3Operation, OA3Parameter, xKongPluginKeyAuth, xKongPluginRequestTermination, xKongPluginRequestValidator } from '../types/openapi3'; import { DCPlugin, DCPluginConfig } from '../types/declarative-config'; +import { getSpec } from './utils'; const tags = ['Tag']; describe('plugins', () => { describe('generateGlobalPlugins()', () => { it('generates plugin given a spec with a plugin attached', async () => { - const api: OpenApi3Spec = { - openapi: '3.0.2', - info: { - title: 'something', - version: '12', - }, - paths: {}, - 'x-kong-plugin-request-validator': { + const api = getSpec({ + [xKongPluginRequestValidator]: { enabled: false, config: { verbose_response: true, @@ -26,26 +21,27 @@ describe('plugins', () => { some_config: ['something'], }, }, - 'x-kong-plugin-key-auth': { + [xKongPluginKeyAuth]: { name: 'key-auth', config: { key_names: ['x-api-key'], }, }, - }; - const result = generateGlobalPlugins(api, ['Tag']); + }); + + const result = generateGlobalPlugins(api, tags); + expect(result.plugins).toEqual([ { name: 'abcd', - // name from plugin tag - tags: ['Tag'], + tags, config: { some_config: ['something'], }, }, { name: 'key-auth', - tags: ['Tag'], + tags, config: { key_names: ['x-api-key'], }, @@ -56,7 +52,7 @@ describe('plugins', () => { verbose_response: true, version: 'draft4', }, - tags: ['Tag'], + tags, enabled: false, name: 'request-validator', }, @@ -68,7 +64,37 @@ describe('plugins', () => { enabled: false, }); }); + + it('does not add extra things to the plugin', () => { + const spec = getSpec({ + [xKongPluginRequestTermination]: { + name: 'request-termination', + mad: 'max', + config: { + max: 'is mad', + status_code: 403, + message: 'So long and thanks for all the fish!', + }, + }, + }); + + const result = generateGlobalPlugins(spec, tags); + + expect(result.plugins).toEqual([ + { + name: 'request-termination', + mad: 'max', + tags, + config: { + max: 'is mad', + status_code: 403, + message: 'So long and thanks for all the fish!', + }, + }, + ]); + }); }); + describe('generateRequestValidatorPlugin()', () => { const parameterSchema = [ { @@ -95,7 +121,7 @@ describe('plugins', () => { expect(generated).toStrictEqual({ name: 'request-validator', enabled: plugin.enabled, - tags: ['Tag'], + tags, config: { version: 'draft4', ...plugin.config, @@ -103,12 +129,13 @@ describe('plugins', () => { }); }); - it('should not add properties if they are not defined', () => { + it('should not add config properties if they are not defined', () => { const plugin = { enabled: true, config: { parameter_schema: [parameterSchema], - body_schema: '[{"name":{"type": "string", "required": true}}]', // The following properties are missing + body_schema: '[{"name":{"type": "string", "required": true}}]', + // The following properties are missing // verbose_response: true, // allowed_content_types: ['application/json'], }, @@ -117,7 +144,7 @@ describe('plugins', () => { expect(generated).toStrictEqual({ name: 'request-validator', enabled: plugin.enabled, - tags: ['Tag'], + tags, config: { version: 'draft4', ...plugin.config, @@ -261,6 +288,7 @@ describe('plugins', () => { }); }); }); + describe('body_schema and allowed_content_types', () => { it('should not add body_schema or allowed_content_types if no body present', () => { const plugin = { diff --git a/packages/openapi-2-kong/src/declarative-config/plugins.ts b/packages/openapi-2-kong/src/declarative-config/plugins.ts index 989b646308..bcbf6d648c 100644 --- a/packages/openapi-2-kong/src/declarative-config/plugins.ts +++ b/packages/openapi-2-kong/src/declarative-config/plugins.ts @@ -3,7 +3,7 @@ import { distinctByProperty, getPluginNameFromKey, isPluginKey } from '../common import { DCPlugin, DCPluginConfig } from '../types/declarative-config'; import { OA3Operation, OpenApi3Spec, OA3RequestBody, OA3Parameter } from '../types/openapi3'; -export function isRequestValidatorPluginKey(key: string): boolean { +export function isRequestValidatorPluginKey(key: string) { return key.match(/-request-validator$/) != null; } @@ -20,8 +20,8 @@ export function generatePlugins(item: PluginItem, tags: string[]) { } const generatePlugin = (tags: string[]) => ([key, value]: Entry): DCPlugin => ({ + ...(value ?? {}), name: value.name || getPluginNameFromKey(key), - ...(value.config ? { config: value.config } : {}), tags: [ // Add tags to plugins while appending defaults tags ...tags, @@ -147,10 +147,16 @@ export function generateRequestValidatorPlugin({ plugin, tags, operation }: { config.verbose_response = Boolean(pluginConfig.verbose_response); } + const isEnabledSpecified = Object.prototype.hasOwnProperty.call(plugin, 'enabled'); + const enabled = isEnabledSpecified ? { enabled: Boolean(plugin.enabled ?? true) } : {}; + const dcPlugin: DCPlugin = { config, - tags: [...(tags ?? []), ...(plugin.tags ?? [])], - enabled: Boolean(plugin.enabled ?? true), + tags: [ + ...(tags ?? []), + ...(plugin.tags ?? []), + ], + ...enabled, name: 'request-validator', }; return dcPlugin; diff --git a/packages/openapi-2-kong/src/declarative-config/security-plugins.test.ts b/packages/openapi-2-kong/src/declarative-config/security-plugins.test.ts index d4b111ebe3..7b7a897cbe 100644 --- a/packages/openapi-2-kong/src/declarative-config/security-plugins.test.ts +++ b/packages/openapi-2-kong/src/declarative-config/security-plugins.test.ts @@ -22,7 +22,7 @@ describe('security-plugins', () => { it('generates apikey plugin with funny casing', async () => { const scheme: OA3SecurityScheme = { - // @ts-expect-error -- this is intentionally using strange casing. Yes, technically speaking the type of OA3SecurityScheme['type'] should be `string`, but we really only support strange casing like this for backwards compat. We want the types to reflect the API we're aiming for, even though we're technically a bit more permissive. + // @ts-expect-error intentionally invalid -- using strange casing. Yes, technically speaking the type of OA3SecurityScheme['type'] should be `string`, but we really only support strange casing like this for backwards compat. We want the types to reflect the API we're aiming for, even though we're technically a bit more permissive. type: 'ApIKeY', in: 'header', name: 'x-api-key', diff --git a/packages/openapi-2-kong/src/declarative-config/services.test.ts b/packages/openapi-2-kong/src/declarative-config/services.test.ts index 48b54ae5b2..878979768a 100644 --- a/packages/openapi-2-kong/src/declarative-config/services.test.ts +++ b/packages/openapi-2-kong/src/declarative-config/services.test.ts @@ -2,9 +2,7 @@ import { generateServices } from './services'; import { parseSpec } from '../generate'; import { getSpec } from './utils'; import { DCRoute, DCService } from '../types/declarative-config'; - -const xKongPluginRequestValidator = 'x-kong-plugin-request-validator'; -const xKongRouteDefaults = 'x-kong-route-defaults'; +import { xKongPluginKeyAuth, xKongPluginRequestValidator, xKongRouteDefaults } from '../types'; /** This function is written in such a way as to allow mutations in tests but without affecting other tests. */ const getSpecResult = (): DCService => @@ -63,8 +61,10 @@ describe('services', () => { }); it('throws for a root level x-kong-route-default', async () => { - const spec = getSpec(); - spec[xKongRouteDefaults] = 'foo'; + const spec = getSpec({ + // @ts-expect-error intentionally invalid + [xKongRouteDefaults]: 'foo', + }); const api = await parseSpec(spec); const fn = () => generateServices(api, ['Tag']); @@ -73,15 +73,18 @@ describe('services', () => { }); it('ignores null for a root level x-kong-route-default', async () => { - const spec = getSpec(); - spec[xKongRouteDefaults] = null; + const spec = getSpec({ + // @ts-expect-error intentionally invalid + [xKongRouteDefaults]: null, + }); const specResult = getSpecResult(); const api = await parseSpec(spec); expect(generateServices(api, ['Tag'])).toEqual([specResult]); }); it('throws for a paths level x-kong-route-default', async () => { - const spec = getSpec(); + const spec = getSpec({}); + // @ts-expect-error intentionally invalid spec.paths['/cats'][xKongRouteDefaults] = 'foo'; const api = await parseSpec(spec); @@ -92,6 +95,7 @@ describe('services', () => { 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(); const api = await parseSpec(spec); @@ -100,6 +104,7 @@ describe('services', () => { it('throws for an operation level x-kong-route-default', async () => { const spec = getSpec(); + // @ts-expect-error intentionally invalid spec.paths['/cats'].post[xKongRouteDefaults] = 'foo'; const api = await parseSpec(spec); @@ -112,6 +117,7 @@ describe('services', () => { 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(); const api = await parseSpec(spec); @@ -127,13 +133,14 @@ describe('services', () => { }); it('generates routes with request validator plugin from operation over path over global', async () => { - const spec = getSpec(); - // global req validator plugin - spec[xKongPluginRequestValidator] = { - config: { - parameter_schema: 'global', + const spec = getSpec({ + // global req validator plugin + [xKongPluginRequestValidator]: { + config: { + parameter_schema: 'global', + }, }, - }; + }); // path req validator plugin spec.paths['/cats'][xKongPluginRequestValidator] = { config: { @@ -142,17 +149,21 @@ describe('services', () => { }; spec.paths['/cats'].get = {}; // operation req validator plugin - spec.paths['/cats'].post[xKongPluginRequestValidator] = { - config: { - parameter_schema: 'operation', - }, - }; + if (spec.paths['/cats'].post) { + spec.paths['/cats'].post[xKongPluginRequestValidator] = { + config: { + parameter_schema: 'operation', + }, + }; + } // operation req validator plugin - spec.paths['/dogs'].post[xKongPluginRequestValidator] = { - config: { - parameter_schema: 'operation', - }, - }; + if (spec.paths['/dogs'].post) { + spec.paths['/dogs'].post[xKongPluginRequestValidator] = { + config: { + parameter_schema: 'operation', + }, + }; + } const specResult = getSpecResult(); specResult.plugins = [ { @@ -160,7 +171,6 @@ describe('services', () => { version: 'draft4', parameter_schema: 'global', }, - enabled: true, tags: ['Tag'], name: 'request-validator', }, @@ -179,7 +189,6 @@ describe('services', () => { version: 'draft4', parameter_schema: 'operation', }, - enabled: true, tags: ['Tag'], name: 'request-validator', }, @@ -198,7 +207,6 @@ describe('services', () => { version: 'draft4', parameter_schema: 'path', }, - enabled: true, tags: ['Tag'], name: 'request-validator', }, @@ -217,7 +225,6 @@ describe('services', () => { version: 'draft4', parameter_schema: 'global', }, - enabled: true, tags: ['Tag'], name: 'request-validator', }, @@ -236,7 +243,6 @@ describe('services', () => { version: 'draft4', parameter_schema: 'operation', }, - enabled: true, tags: ['Tag'], name: 'request-validator', }, @@ -255,7 +261,6 @@ describe('services', () => { version: 'draft4', parameter_schema: 'global', }, - enabled: true, tags: ['Tag'], name: 'request-validator', }, @@ -271,14 +276,15 @@ describe('services', () => { spec.paths = { '/dogs': { summary: 'Dog stuff', - 'x-kong-plugin-key-auth': { + [xKongPluginKeyAuth]: { config: { key_names: ['path'], }, }, get: {}, post: { - 'x-kong-plugin-key-auth': { + [xKongPluginKeyAuth]: { + name: 'key-auth', config: { key_names: ['operation'], }, @@ -351,22 +357,24 @@ describe('services', () => { }); describe('x-kong-route-defaults and strip_path', () => { + // Note: although these are technically not valid DC Routes configs, we use them because they make it crystal-clear what the tests are checking for const rootLevel = { level: 'root', rootLevel: true, - }; + } as unknown as DCRoute; const pathLevel = { level: 'path', pathLevel: true, - }; + } as unknown as DCRoute; const operationLevel = { level: 'operation', operationLevel: true, - }; + } as unknown as DCRoute; it('root level', async () => { - const spec = getSpec(); - spec[xKongRouteDefaults] = rootLevel; + const spec = getSpec({ + [xKongRouteDefaults]: rootLevel, + }); const specResult = getSpecResult(); specResult.routes = specResult.routes.map(route => ({ ...route, ...rootLevel })); const api = await parseSpec(spec); @@ -385,7 +393,9 @@ describe('services', () => { it('operation level', async () => { const spec = getSpec(); - spec.paths['/dogs'].get[xKongRouteDefaults] = operationLevel; + if (spec.paths['/dogs'].get) { + spec.paths['/dogs'].get[xKongRouteDefaults] = operationLevel; + } const specResult = getSpecResult(); specResult.routes[1] = { ...specResult.routes[1], ...operationLevel }; const api = await parseSpec(spec); @@ -393,9 +403,12 @@ describe('services', () => { }); it('will select (but not merge) the operation level over the root level', async () => { - const spec = getSpec(); - spec[xKongRouteDefaults] = rootLevel; - spec.paths['/cats'].post[xKongRouteDefaults] = operationLevel; + const spec = getSpec({ + [xKongRouteDefaults]: rootLevel, + }); + if (spec.paths['/cats'].post) { + spec.paths['/cats'].post[xKongRouteDefaults] = operationLevel; + } const specResult = getSpecResult(); specResult.routes = specResult.routes.map(route => ({ ...route, @@ -408,7 +421,9 @@ describe('services', () => { 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[xKongRouteDefaults] = operationLevel; + if (spec.paths['/dogs'].post) { + spec.paths['/dogs'].post[xKongRouteDefaults] = operationLevel; + } const specResult = getSpecResult(); specResult.routes[1] = { ...specResult.routes[1], ...pathLevel }; specResult.routes[2] = { ...specResult.routes[2], ...operationLevel }; @@ -417,8 +432,9 @@ describe('services', () => { }); it('will select (but not merge) the path level over the root level', async () => { - const spec = getSpec(); - spec[xKongRouteDefaults] = rootLevel; + const spec = getSpec({ + [xKongRouteDefaults]: rootLevel, + }); spec.paths['/cats'][xKongRouteDefaults] = pathLevel; const specResult = getSpecResult(); specResult.routes = specResult.routes.map(route => ({ @@ -442,10 +458,11 @@ describe('services', () => { }); it('allows overriding `strip_path` from `x-kong-route-defaults` at the root', async () => { - const spec = getSpec(); - spec[xKongRouteDefaults] = { - strip_path: true, - }; + const spec = getSpec({ + [xKongRouteDefaults]: { + strip_path: true, + }, + }); const specResult = getSpecResult(); specResult.routes = specResult.routes.map(route => ({ ...route, strip_path: true })); const api = await parseSpec(spec); diff --git a/packages/openapi-2-kong/src/declarative-config/services.ts b/packages/openapi-2-kong/src/declarative-config/services.ts index 4b5d53a383..ca0f2876f6 100644 --- a/packages/openapi-2-kong/src/declarative-config/services.ts +++ b/packages/openapi-2-kong/src/declarative-config/services.ts @@ -15,7 +15,7 @@ import { generatePlugins, } from './plugins'; import { DCService, DCRoute } from '../types/declarative-config'; -import { OpenApi3Spec, OA3Server, OA3PathItem } from '../types/openapi3'; +import { OpenApi3Spec, OA3Server, OA3PathItem, xKongServiceDefaults, xKongName } from '../types/openapi3'; export function generateServices(api: OpenApi3Spec, tags: string[]) { const servers = getAllServers(api); @@ -35,10 +35,10 @@ export function generateService(server: OA3Server, api: OpenApi3Spec, tags: stri const parsedUrl = parseUrl(serverUrl); // Service plugins const globalPlugins = generateGlobalPlugins(api, tags); - const serviceDefaults = api['x-kong-service-defaults'] || {}; + const serviceDefaults = api[xKongServiceDefaults] || {}; if (typeof serviceDefaults !== 'object') { - throw new Error('expected \'x-kong-service-defaults\' to be an object'); + throw new Error(`expected '${xKongServiceDefaults}' to be an object`); } const service: DCService = { @@ -147,8 +147,8 @@ export function generateRouteName( const name = getName(api); const pathItem = api.paths[routePath]; - if (typeof pathItem?.[method]?.['x-kong-name'] === 'string') { - const opsName = generateSlug(pathItem?.[method]?.['x-kong-name'] as string); + if (typeof pathItem?.[method]?.[xKongName] === 'string') { + const opsName = generateSlug(pathItem?.[method]?.[xKongName] as string); return `${name}-${opsName}`; } @@ -159,6 +159,6 @@ export function generateRouteName( // replace all `/` with `-` except the ones at the beginning or end of a string const replacedRoute = routePath.replace(/(?!^)\/(?!$)/g, '-'); - const pathSlug = generateSlug(pathItem['x-kong-name'] || replacedRoute); + const pathSlug = generateSlug(pathItem[xKongName] || replacedRoute); return `${name}${pathSlug ? `-${pathSlug}` : ''}-${method}`; } diff --git a/packages/openapi-2-kong/src/declarative-config/upstreams.test.ts b/packages/openapi-2-kong/src/declarative-config/upstreams.test.ts index 4523c07574..2ec2eadfba 100644 --- a/packages/openapi-2-kong/src/declarative-config/upstreams.test.ts +++ b/packages/openapi-2-kong/src/declarative-config/upstreams.test.ts @@ -1,9 +1,8 @@ import { parseSpec } from '../generate'; +import { xKongUpstreamDefaults } from '../types'; import { generateUpstreams } from './upstreams'; import { getSpec } from './utils'; -const xKongUpstreamDefaults = 'x-kong-upstream-defaults'; - /** This function is written in such a way as to allow mutations in tests but without affecting other tests. */ const getSpecResult = () => JSON.parse( @@ -28,20 +27,22 @@ describe('upstreams', () => { }); it('throws for a root level x-kong-route-default', async () => { - const spec = getSpec(); - // @ts-expect-error intentionall invalid - spec[xKongUpstreamDefaults] = 'foo'; + const spec = getSpec({ + // @ts-expect-error intentionall invalid + [xKongUpstreamDefaults]: 'foo', + }); const api = await parseSpec(spec); const fn = () => generateUpstreams(api, ['Tag']); - expect(fn).toThrowError('expected \'x-kong-upstream-defaults\' to be an object'); + expect(fn).toThrowError(`expected '${xKongUpstreamDefaults}' to be an object`); }); it('ignores null for a root level x-kong-route-default', async () => { - const spec = getSpec(); - // @ts-expect-error intentionall invalid - spec[xKongUpstreamDefaults] = null; + const spec = getSpec({ + // @ts-expect-error intentionall invalid + [xKongUpstreamDefaults]: null, + }); const specResult = getSpecResult(); const api = await parseSpec(spec); expect(generateUpstreams(api, ['Tag'])).toEqual([specResult]); diff --git a/packages/openapi-2-kong/src/declarative-config/upstreams.ts b/packages/openapi-2-kong/src/declarative-config/upstreams.ts index ce8a7c22e6..bceb5384b8 100644 --- a/packages/openapi-2-kong/src/declarative-config/upstreams.ts +++ b/packages/openapi-2-kong/src/declarative-config/upstreams.ts @@ -1,6 +1,6 @@ import { getName, parseUrl, fillServerVariables } from '../common'; import { DCUpstream } from '../types/declarative-config'; -import { OpenApi3Spec } from '../types/openapi3'; +import { OpenApi3Spec, xKongUpstreamDefaults } from '../types/openapi3'; export function generateUpstreams(api: OpenApi3Spec, tags: string[]) { const servers = api.servers || []; @@ -11,10 +11,10 @@ export function generateUpstreams(api: OpenApi3Spec, tags: string[]) { // x-kong-upstream-defaults is free format so we do not want type checking. // If added, it would tightly couple these objects to Kong, and that would make future maintenance a lot harder. - const upstreamDefaults = api['x-kong-upstream-defaults'] || {}; + const upstreamDefaults = api[xKongUpstreamDefaults] || {}; if (typeof upstreamDefaults !== 'object') { - throw new Error("expected 'x-kong-upstream-defaults' to be an object"); + throw new Error(`expected '${xKongUpstreamDefaults}' to be an object`); } const upstream: DCUpstream = { diff --git a/packages/openapi-2-kong/src/declarative-config/utils.ts b/packages/openapi-2-kong/src/declarative-config/utils.ts index b83e927aef..67036839a0 100644 --- a/packages/openapi-2-kong/src/declarative-config/utils.ts +++ b/packages/openapi-2-kong/src/declarative-config/utils.ts @@ -1,7 +1,7 @@ import { OpenApi3Spec } from '../types/openapi3'; /** This function is written in such a way as to allow mutations in tests but without affecting other tests. */ -export const getSpec = (): OpenApi3Spec => +export const getSpec = (overrides: Partial = {}): OpenApi3Spec => JSON.parse( JSON.stringify({ openapi: '3.0', @@ -31,5 +31,6 @@ export const getSpec = (): OpenApi3Spec => get: {}, }, }, + ...overrides, }), ); diff --git a/packages/openapi-2-kong/src/generate.test.ts b/packages/openapi-2-kong/src/generate.test.ts index e09b770680..29475165b6 100644 --- a/packages/openapi-2-kong/src/generate.test.ts +++ b/packages/openapi-2-kong/src/generate.test.ts @@ -87,7 +87,7 @@ describe('top-level API exports', () => { describe('generateFromSpec()', () => { it('generates DC from spec', async () => { - const parsedSpec = YAML.parse(dcFixtureFileString, 'utf8'); + const parsedSpec = YAML.parse(dcFixtureFileString); const { documents: [dc], } = await generateFromSpec(parsedSpec, 'kong-declarative-config') as DeclarativeConfigResult; @@ -97,7 +97,7 @@ describe('top-level API exports', () => { }); it('generates kubernetes from spec', async () => { - const parsedSpec = YAML.parse(k8sFixtureFileString, 'utf8'); + const parsedSpec = YAML.parse(k8sFixtureFileString); const { type, label, diff --git a/packages/openapi-2-kong/src/generate.ts b/packages/openapi-2-kong/src/generate.ts index 30f5efe786..1d6cb357d7 100644 --- a/packages/openapi-2-kong/src/generate.ts +++ b/packages/openapi-2-kong/src/generate.ts @@ -81,6 +81,6 @@ export const parseSpec = (spec: string | Record) => { api.openapi = '3.0.0'; } - // @ts-expect-error -- TSCONVERSION + // @ts-expect-error until we make our OpenAPI type extend from the cannonical one (i.e. from `openapi-types`, we'll need to shim this here) return SwaggerParser.dereference(api) as Promise; }; diff --git a/packages/openapi-2-kong/src/kubernetes/fixtures.test.ts b/packages/openapi-2-kong/src/kubernetes/fixtures.test.ts index 9acf4a9792..57791d0592 100644 --- a/packages/openapi-2-kong/src/kubernetes/fixtures.test.ts +++ b/packages/openapi-2-kong/src/kubernetes/fixtures.test.ts @@ -19,7 +19,7 @@ describe('kubernetes fixtures', () => { const expectedBase = `${path.parse(fileBase).name}.expected.yaml`; const expectedPath = path.join(root, expectedBase); const expected = await readFile(expectedPath, 'utf8'); - const parsedExpected = YAML.parse(expected); + const parsedExpected = YAML.parse(expected); expect(result).toEqual(parsedExpected); }); diff --git a/packages/openapi-2-kong/src/kubernetes/generate.test.ts b/packages/openapi-2-kong/src/kubernetes/generate.test.ts index 0fb314ebba..edfe3fec20 100644 --- a/packages/openapi-2-kong/src/kubernetes/generate.test.ts +++ b/packages/openapi-2-kong/src/kubernetes/generate.test.ts @@ -1,6 +1,6 @@ import { parseSpec } from '../generate'; import { K8sAnnotations, K8sIngressRule, K8sIngressTLS, K8sManifest } from '../types/kubernetes-config'; -import { OA3Server, OpenApi3Spec } from '../types/openapi3'; +import { OA3Server, OpenApi3Spec, xKongName } from '../types/openapi3'; import { generateKongForKubernetesConfigFromSpec, generateMetadataAnnotations, @@ -50,14 +50,14 @@ describe('index', () => { }); it('with x-kong-name', () => { - const api: OpenApi3Spec = { ...spec, 'x-kong-name': 'kong-name' }; + const api: OpenApi3Spec = { ...spec, [xKongName]: 'kong-name' }; expect(getSpecName(api)).toBe('kong-name'); }); it('with x-kubernetes-ingress-metadata.name', async () => { const api = await parseSpec({ ...spec, - 'x-kong-name': 'Kong Name', + [xKongName]: 'Kong Name', info: { 'x-kubernetes-ingress-metadata': { name: 'K8s name', diff --git a/packages/openapi-2-kong/src/kubernetes/plugin-helpers.ts b/packages/openapi-2-kong/src/kubernetes/plugin-helpers.ts index e26b1f0cf6..13f83aa34a 100644 --- a/packages/openapi-2-kong/src/kubernetes/plugin-helpers.ts +++ b/packages/openapi-2-kong/src/kubernetes/plugin-helpers.ts @@ -1,9 +1,9 @@ import { HttpMethodType } from '../common'; import { K8sIngress, K8sKongIngress, K8sKongPlugin } from '../types/kubernetes-config'; -import { XKongPluginKeyAuth, XKongPluginUnknown } from '../types/openapi3'; +import { xKongPluginKeyAuth, XKongPluginKeyAuth, XKongPluginUnknown } from '../types/openapi3'; export const pluginKeyAuth: XKongPluginKeyAuth = { - 'x-kong-plugin-key-auth': { + [xKongPluginKeyAuth]: { name: 'key-auth', config: { key_names: ['api_key', 'apikey'], diff --git a/packages/openapi-2-kong/src/types/declarative-config.ts b/packages/openapi-2-kong/src/types/declarative-config.ts index 8e3aca2d42..7da022acff 100644 --- a/packages/openapi-2-kong/src/types/declarative-config.ts +++ b/packages/openapi-2-kong/src/types/declarative-config.ts @@ -6,7 +6,7 @@ export interface DCPluginConfig { body_schema?: string; issuer?: string; key_names?: string[]; - parameter_schema?: OA3Parameter[]; + parameter_schema?: OA3Parameter[] | 'global' | 'path' | 'operation'; scopes_required?: string[]; verbose_response?: boolean; version?: 'draft4'; diff --git a/packages/openapi-2-kong/src/types/openapi3.ts b/packages/openapi-2-kong/src/types/openapi3.ts index de492a8130..133c7f711a 100644 --- a/packages/openapi-2-kong/src/types/openapi3.ts +++ b/packages/openapi-2-kong/src/types/openapi3.ts @@ -1,39 +1,40 @@ import { DCRoute, DCUpstream } from './declarative-config'; import { K8sIngressTLS } from './kubernetes-config'; +export const xKongName = 'x-kong-name'; export interface XKongName { - 'x-kong-name'?: string; + [xKongName]?: string; } +export const xKongRouteDefaults = 'x-kong-route-defaults'; export interface XKongRouteDefaults { - 'x-kong-route-defaults'?: DCRoute; + [xKongRouteDefaults]?: Partial; } +export const xKongUpstreamDefaults = 'x-kong-upstream-defaults'; export interface XKongUpstreamDefaults { - 'x-kong-upstream-defaults'?: DCUpstream; + [xKongUpstreamDefaults]?: Partial; } +export const xKongServiceDefaults = 'x-kong-service-defaults'; export interface XKongServiceDefaults { - 'x-kong-service-defaults'?: Record; + [xKongServiceDefaults]?: Record; } +export const xKongPluginRequestValidator = 'x-kong-plugin-request-validator'; export interface XKongPluginRequestValidator { - 'x-kong-plugin-request-validator'?: { - enabled: boolean; + [xKongPluginRequestValidator]?: { + enabled?: boolean; config: { - verbose_response: boolean, - } - } + verbose_response?: boolean; + parameter_schema?: 'global' | 'path' | 'operation'; + }; + }; } -export type XKongPluginUnknown = Record<`x-kong-plugin-${string}`, { - enabled?: boolean; - name?: string; - config: Record; -}> - +export const xKongPluginKeyAuth = 'x-kong-plugin-key-auth'; export interface XKongPluginKeyAuth { - 'x-kong-plugin-key-auth'?: { + [xKongPluginKeyAuth]?: { name: 'key-auth'; config: { key_names: string[]; @@ -43,6 +44,25 @@ export interface XKongPluginKeyAuth { }; } +export const xKongPluginRequestTermination = 'x-kong-plugin-request-termination'; +export interface XKongPluginRequestTermination { + [xKongPluginRequestTermination]?: { + name: 'request-termination'; + config: { + status_code: number; + message: string; + [key: string]: string | number; + } + [key: string]: string | number | Record; + } +} + +export type XKongPluginUnknown = Record<`x-kong-plugin-${string}`, { + enabled?: boolean; + name?: string; + config: Record; +}> + export interface StripPath { // eslint-disable-next-line camelcase -- this is defined by a spec that is out of our control strip_path?: boolean; @@ -153,6 +173,7 @@ export type OA3Operation = { } & XKongName & XKongRouteDefaults & XKongPluginKeyAuth + & XKongPluginRequestValidator ; export type OA3PathItem = { @@ -171,6 +192,7 @@ export type OA3PathItem = { trace?: OA3Operation; } & XKongName & XKongRouteDefaults + & XKongPluginRequestValidator ; export type OA3Paths = Record @@ -254,11 +276,13 @@ export type OpenApi3Spec = { security?: OA3SecurityRequirement[]; tags?: string[]; externalDocs?: OA3ExternalDocs; -} & XKongName +} & XKongPluginKeyAuth + & XKongPluginRequestTermination & XKongPluginRequestValidator & XKongPluginUnknown + & XKongRouteDefaults & XKongServiceDefaults & XKongUpstreamDefaults - & XKongRouteDefaults + & XKongName ;