diff --git a/apps/cli/src/command/dump.command.ts b/apps/cli/src/command/dump.command.ts index 80926d1..a0c59ee 100644 --- a/apps/cli/src/command/dump.command.ts +++ b/apps/cli/src/command/dump.command.ts @@ -18,6 +18,7 @@ import { type DumpOptions = BackendOptions & { output: string; + withId: boolean; }; export interface LoadRemoteConfigurationTaskOptions { @@ -74,6 +75,7 @@ export const DumpCommand = new BackendCommand( 'path of the file to save the configuration', 'adc.yaml', ) + .option('--with-id', 'dump remote resources id') .addExamples([ { title: 'Save backend configuration to the default adc.yaml file', @@ -92,6 +94,10 @@ export const DumpCommand = new BackendCommand( title: 'Save only the resources with the specified labels', command: 'adc dump --label-selector app=catalog', }, + { + title: 'Save the remote resources id', + command: 'adc dump --with-id', + }, ]) .handle(async (opts) => { const backend = loadBackend(opts.backend, opts); @@ -105,6 +111,7 @@ export const DumpCommand = new BackendCommand( }), { // Remove output resource metadata fields + enabled: !opts.withId, task: (ctx) => recursiveRemoveMetadataField(ctx.remote), }, { diff --git a/apps/cli/src/command/utils.ts b/apps/cli/src/command/utils.ts index c1248df..a1ae0ef 100644 --- a/apps/cli/src/command/utils.ts +++ b/apps/cli/src/command/utils.ts @@ -250,6 +250,7 @@ export const filterResourceType = ( export const recursiveRemoveMetadataField = (c: ADCSDK.Configuration) => { const removeMetadata = (obj: object) => { + if ('id' in obj) delete obj.id; if ('metadata' in obj) delete obj.metadata; }; Object.entries(c).forEach(([key, value]) => { diff --git a/apps/cli/src/differ/differv3.ts b/apps/cli/src/differ/differv3.ts index c1d0994..f06ccbc 100644 --- a/apps/cli/src/differ/differv3.ts +++ b/apps/cli/src/differ/differv3.ts @@ -78,27 +78,19 @@ export class DifferV3 { ADCSDK.ResourceType.SERVICE, local?.services?.map((res) => [ res.name, - ADCSDK.utils.generateId(res.name), - res, - ]) ?? [], - remote?.services?.map((res) => [ - res.name, - ADCSDK.utils.generateId(res.name), + res.id ?? ADCSDK.utils.generateId(res.name), res, ]) ?? [], + remote?.services?.map((res) => [res.name, res.id, res]) ?? [], ), ...differ.diffResource( ADCSDK.ResourceType.SSL, local?.ssls?.map((res) => [ res.snis.join(','), - ADCSDK.utils.generateId(res.snis.join(',')), - res, - ]) ?? [], - remote?.ssls?.map((res) => [ - res.snis.join(','), - ADCSDK.utils.generateId(res.snis.join(',')), + res.id ?? ADCSDK.utils.generateId(res.snis.join(',')), res, ]) ?? [], + remote?.ssls?.map((res) => [res.snis.join(','), res.id, res]) ?? [], ), ...differ.diffResource( ADCSDK.ResourceType.CONSUMER, @@ -144,40 +136,29 @@ export class DifferV3 { ADCSDK.ResourceType.ROUTE, local?.routes?.map((res) => [ res.name, - ADCSDK.utils.generateId(generateResourceName(res.name)), - res, - ]) ?? [], - remote?.routes?.map((res) => [ - res.name, - ADCSDK.utils.generateId(generateResourceName(res.name)), + res.id ?? ADCSDK.utils.generateId(generateResourceName(res.name)), res, ]) ?? [], + remote?.routes?.map((res) => [res.name, res.id, res]) ?? [], ), ...differ.diffResource( ADCSDK.ResourceType.STREAM_ROUTE, local?.stream_routes?.map((res) => [ res.name, - ADCSDK.utils.generateId(generateResourceName(res.name)), - res, - ]) ?? [], - remote?.stream_routes?.map((res) => [ - res.name, - ADCSDK.utils.generateId(generateResourceName(res.name)), + res.id ?? ADCSDK.utils.generateId(generateResourceName(res.name)), res, ]) ?? [], + remote?.stream_routes?.map((res) => [res.name, res.id, res]) ?? [], ), ...differ.diffResource( ADCSDK.ResourceType.CONSUMER_CREDENTIAL, local?.consumer_credentials?.map((res) => [ res.name, - ADCSDK.utils.generateId(generateResourceName(res.name)), - res, - ]) ?? [], - remote?.consumer_credentials?.map((res) => [ - res.name, - ADCSDK.utils.generateId(generateResourceName(res.name)), + res.id ?? ADCSDK.utils.generateId(generateResourceName(res.name)), res, ]) ?? [], + remote?.consumer_credentials?.map((res) => [res.name, res.id, res]) ?? + [], ), /* ...differ.diffResource( ADCSDK.ResourceType.UPSTREAM, @@ -321,22 +302,20 @@ export class DifferV3 { const checkedRemoteId: Array = []; remote.forEach(([remoteName, remoteId, remoteItem]) => { - const remoteMetadata = cloneDeep( - (remoteItem as { metadata: ADCSDK.ResourceMetadata })?.metadata, - ); unset(remoteItem, 'metadata'); - const eventResourceId = remoteMetadata?.id ?? remoteId; + unset(remoteItem, 'id'); // Asserts that the remote resource should exist locally, and that // non-existence means that the user deleted that resource. - const localItem = localIdMap[eventResourceId]; + const localItem = localIdMap[remoteId]; + unset(localItem, 'id'); // Exists remotely but not locally: resource deleted by user if (!localItem) { return result.push({ resourceType, type: ADCSDK.EventType.DELETE, - resourceId: eventResourceId, + resourceId: remoteId, resourceName: remoteName, oldValue: remoteItem, @@ -359,13 +338,13 @@ export class DifferV3 { resourceType === ADCSDK.ResourceType.SERVICE ? remoteName : undefined, - ).map(this.postprocessSubEvent(remoteName, eventResourceId)), + ).map(this.postprocessSubEvent(remoteName, remoteId)), }); } // Record the remote IDs that have been checked. It will be used // to identify locally added resources. - checkedRemoteId.push(eventResourceId); + checkedRemoteId.push(remoteId); const originalLocalItem = cloneDeep(localItem); @@ -588,6 +567,10 @@ export class DifferV3 { // Exists locally but not remotely: resource created by user local.forEach(([localName, localId, localItem]) => { if (checkedRemoteId.includes(localId)) return; + + unset(localItem, 'metadata'); + unset(localItem, 'id'); + return result.push({ resourceType, type: ADCSDK.EventType.CREATE, diff --git a/apps/cli/src/differ/specs/basic.spec.ts b/apps/cli/src/differ/specs/basic.spec.ts index 4ec8dbb..37aa187 100644 --- a/apps/cli/src/differ/specs/basic.spec.ts +++ b/apps/cli/src/differ/specs/basic.spec.ts @@ -1,5 +1,4 @@ import * as ADCSDK from '@api7/adc-sdk'; -import { unset } from 'lodash'; import { DifferV3 } from '../differv3'; @@ -343,31 +342,46 @@ describe('Differ V3 - basic', () => { it('should update service nested route', () => { const serviceName = 'Test Service'; const routeName = 'Test Route'; - const oldService: ADCSDK.Service = { - name: serviceName, - routes: [ - { - name: routeName, - uris: ['/test'], - plugins: { - test: { - testKey: 'oldValue', - }, - }, - }, - ], - }; - - const newService = structuredClone(oldService); - newService.routes[0].plugins.test.testKey = 'newValue'; expect( DifferV3.diff( { - services: [structuredClone(newService)], + services: [ + { + name: serviceName, + routes: [ + { + name: routeName, + uris: ['/test'], + plugins: { + test: { + testKey: 'newValue', + }, + }, + }, + ], + }, + ], }, { - services: [structuredClone(oldService)], + services: [ + { + id: ADCSDK.utils.generateId(serviceName), + name: serviceName, + routes: [ + { + id: ADCSDK.utils.generateId(`${serviceName}.${routeName}`), + name: routeName, + uris: ['/test'], + plugins: { + test: { + testKey: 'oldValue', + }, + }, + }, + ], + }, + ], }, ), ).toEqual([ @@ -380,8 +394,24 @@ describe('Differ V3 - basic', () => { rhs: 'newValue', }, ], - newValue: newService.routes[0], - oldValue: oldService.routes[0], + newValue: { + name: routeName, + uris: ['/test'], + plugins: { + test: { + testKey: 'newValue', + }, + }, + }, + oldValue: { + name: routeName, + uris: ['/test'], + plugins: { + test: { + testKey: 'oldValue', + }, + }, + }, parentId: ADCSDK.utils.generateId(serviceName), resourceId: ADCSDK.utils.generateId(`${serviceName}.${routeName}`), resourceName: routeName, @@ -393,39 +423,60 @@ describe('Differ V3 - basic', () => { it('should update service and its nested route', () => { const serviceName = 'Test Service'; + const serviceId = ADCSDK.utils.generateId(serviceName); const routeName = 'Test Route'; - const oldService: ADCSDK.Service = { - name: serviceName, - plugins: { - test: { - testKey: 'serviceOldValue', - }, - }, - routes: [ - { - name: routeName, - uris: ['/test'], - plugins: { - test: { - testKey: 'oldValue', - }, - }, - }, - ], - }; - - const newService = structuredClone(oldService); - newService.path_prefix = '/test'; - newService.plugins.test.testKey = 'serviceNewValue'; - newService.routes[0].plugins.test.testKey = 'newValue'; + const routeId = ADCSDK.utils.generateId(`${serviceName}.${routeName}`); expect( DifferV3.diff( { - services: [structuredClone(newService)], + services: [ + { + name: serviceName, + path_prefix: '/test', + plugins: { + test: { + testKey: 'serviceNewValue', + }, + }, + routes: [ + { + name: routeName, + uris: ['/test'], + plugins: { + test: { + testKey: 'newValue', + }, + }, + }, + ], + }, + ], }, { - services: [structuredClone(oldService)], + services: [ + { + id: serviceId, + name: serviceName, + plugins: { + test: { + testKey: 'serviceOldValue', + }, + }, + routes: [ + { + id: routeId, + name: routeName, + uris: ['/test'], + plugins: { + test: { + testKey: 'oldValue', + }, + }, + }, + ], + }, + ], }, ), ).toEqual([ @@ -438,10 +489,26 @@ describe('Differ V3 - basic', () => { rhs: 'newValue', }, ], - newValue: newService.routes[0], - oldValue: oldService.routes[0], - parentId: ADCSDK.utils.generateId(serviceName), - resourceId: ADCSDK.utils.generateId(`${serviceName}.${routeName}`), + newValue: { + name: routeName, + uris: ['/test'], + plugins: { + test: { + testKey: 'newValue', + }, + }, + }, + oldValue: { + name: routeName, + uris: ['/test'], + plugins: { + test: { + testKey: 'oldValue', + }, + }, + }, + parentId: serviceId, + resourceId: routeId, resourceName: routeName, resourceType: ADCSDK.ResourceType.ROUTE, type: ADCSDK.EventType.UPDATE, @@ -469,7 +536,7 @@ describe('Differ V3 - basic', () => { name: serviceName, plugins: { test: { testKey: 'serviceOldValue' } }, }, - resourceId: ADCSDK.utils.generateId(serviceName), + resourceId: serviceId, resourceName: serviceName, resourceType: ADCSDK.ResourceType.SERVICE, type: ADCSDK.EventType.UPDATE, @@ -479,30 +546,36 @@ describe('Differ V3 - basic', () => { it('should keep plugins when plugins not be changed', () => { const serviceName = 'Test Service'; - const oldService: ADCSDK.Service = { - name: serviceName, - plugins: { - test: { - testKey: 'testValue', - added: 'added', - }, - }, - }; - - const newService = structuredClone(oldService); - newService.path_prefix = '/test'; - const origNewService = structuredClone(newService); - - // ensure local resource does not include default added field - unset(newService, 'plugins.test.added'); + const serviceId = ADCSDK.utils.generateId(serviceName); expect( DifferV3.diff( { - services: [structuredClone(newService)], + services: [ + { + name: serviceName, + path_prefix: '/test', + plugins: { + test: { + testKey: 'testValue', + }, + }, + }, + ], }, { - services: [structuredClone(oldService)], + services: [ + { + id: serviceId, + name: serviceName, + plugins: { + test: { + testKey: 'testValue', + added: 'added', + }, + }, + }, + ], }, { plugins: { @@ -521,9 +594,26 @@ describe('Differ V3 - basic', () => { rhs: '/test', }, ], - newValue: origNewService, - oldValue: oldService, - resourceId: ADCSDK.utils.generateId(serviceName), + newValue: { + name: serviceName, + path_prefix: '/test', + plugins: { + test: { + testKey: 'testValue', + added: 'added', + }, + }, + }, + oldValue: { + name: serviceName, + plugins: { + test: { + testKey: 'testValue', + added: 'added', + }, + }, + }, + resourceId: serviceId, resourceName: serviceName, resourceType: ADCSDK.ResourceType.SERVICE, type: ADCSDK.EventType.UPDATE, @@ -532,6 +622,8 @@ describe('Differ V3 - basic', () => { }); it('should selectively merge the objects in default values, for principle', () => { + const serviceName = 'Test Service'; + const serviceId = ADCSDK.utils.generateId(serviceName); expect( DifferV3.diff( { @@ -546,6 +638,7 @@ describe('Differ V3 - basic', () => { { services: [ { + id: serviceId, name: 'Test Service', test: 'test', test1: { @@ -571,189 +664,6 @@ describe('Differ V3 - basic', () => { ).toEqual([]); }); - it('ensure remote metadata does not affect the differ, delete', () => { - const serviceName = 'Test Service'; - const routeName = 'Test Route'; - const route1Name = `${routeName} 1`; - const route2Name = `${routeName} 2`; - const remoteServiceId = 'not_hashed_service_name'; - const remoteRouteId = 'not_hashed_route_name'; - expect( - DifferV3.diff( - {}, - { - services: [ - { - name: serviceName, - routes: [ - { - name: route1Name, - uris: ['/test1'], - }, - { - name: route2Name, - uris: ['/test2'], - metadata: { id: remoteRouteId }, - }, - ], - metadata: { - id: remoteServiceId, - }, - }, - ], - }, - ), - ).toEqual([ - { - oldValue: { name: route1Name, uris: ['/test1'] }, - parentId: remoteServiceId, - resourceId: ADCSDK.utils.generateId(`${serviceName}.${route1Name}`), - resourceName: route1Name, - resourceType: ADCSDK.ResourceType.ROUTE, - type: ADCSDK.EventType.DELETE, - }, - { - oldValue: { name: route2Name, uris: ['/test2'] }, - parentId: remoteServiceId, - resourceId: remoteRouteId, - resourceName: route2Name, - resourceType: ADCSDK.ResourceType.ROUTE, - type: ADCSDK.EventType.DELETE, - }, - { - oldValue: { - name: serviceName, - routes: [ - { name: route1Name, uris: ['/test1'] }, - { name: route2Name, uris: ['/test2'] }, - ], - }, - resourceId: remoteServiceId, - resourceName: serviceName, - resourceType: ADCSDK.ResourceType.SERVICE, - type: ADCSDK.EventType.DELETE, - }, - ]); - }); - - it('ensure remote metadata does not affect the differ, update', () => { - const serviceName = 'Test Service'; - const routeName = 'Test Route'; - const route1Name = `${routeName} 1`; - const route2Name = `${routeName} 2`; - const remoteServiceId = 'not_hashed_service_name'; - const remoteRouteId = 'not_hashed_route_name'; - expect( - DifferV3.diff( - { - services: [ - { - name: serviceName, - path_prefix: '/test', - routes: [ - { - name: route1Name, - uris: ['/test1u'], - }, - { - name: route2Name, - uris: ['/test2u'], - }, - ], - }, - ], - }, - { - services: [ - { - name: serviceName, - routes: [ - { - name: route1Name, - uris: ['/test1'], - }, - { - name: route2Name, - uris: ['/test2'], - metadata: { id: remoteRouteId }, - }, - ], - metadata: { - id: remoteServiceId, - }, - }, - ], - }, - ), - ).toEqual([ - { - oldValue: { name: route1Name, uris: ['/test1'] }, - parentId: remoteServiceId, - resourceId: ADCSDK.utils.generateId(`${serviceName}.${route1Name}`), - resourceName: route1Name, - resourceType: ADCSDK.ResourceType.ROUTE, - type: ADCSDK.EventType.DELETE, - }, - { - oldValue: { name: route2Name, uris: ['/test2'] }, - parentId: remoteServiceId, - resourceId: remoteRouteId, - resourceName: route2Name, - resourceType: ADCSDK.ResourceType.ROUTE, - type: ADCSDK.EventType.DELETE, - }, - { - oldValue: { - name: serviceName, - routes: [ - { name: route1Name, uris: ['/test1'] }, - { name: route2Name, uris: ['/test2'] }, - ], - }, - resourceId: remoteServiceId, - resourceName: serviceName, - resourceType: ADCSDK.ResourceType.SERVICE, - type: ADCSDK.EventType.DELETE, - }, - { - newValue: { - name: serviceName, - path_prefix: '/test', - routes: [ - { name: route1Name, uris: ['/test1u'] }, - { - name: route2Name, - uris: ['/test2u'], - }, - ], - }, - resourceId: ADCSDK.utils.generateId(serviceName), - resourceName: serviceName, - resourceType: ADCSDK.ResourceType.SERVICE, - type: ADCSDK.EventType.CREATE, - }, - { - newValue: { name: route1Name, uris: ['/test1u'] }, - parentId: ADCSDK.utils.generateId(serviceName), - resourceId: ADCSDK.utils.generateId(`${serviceName}.${route1Name}`), - resourceName: route1Name, - resourceType: ADCSDK.ResourceType.ROUTE, - type: ADCSDK.EventType.CREATE, - }, - { - newValue: { - name: route2Name, - uris: ['/test2u'], - }, - parentId: ADCSDK.utils.generateId(serviceName), - resourceId: ADCSDK.utils.generateId(`${serviceName}.${route2Name}`), - resourceName: route2Name, - resourceType: ADCSDK.ResourceType.ROUTE, - type: ADCSDK.EventType.CREATE, - }, - ]); - }); - it('ensure default values for array nested object formats are merged correctly', () => { const serviceName = 'Test Service'; const newNode: ADCSDK.UpstreamNode = { @@ -777,6 +687,7 @@ describe('Differ V3 - basic', () => { { services: [ { + id: ADCSDK.utils.generateId(serviceName), name: serviceName, upstream: { nodes: [oldNode] }, }, @@ -796,7 +707,7 @@ describe('Differ V3 - basic', () => { }); it('ensure route and stream route id generated currect', () => { - const services: Array = [ + const newServices: Array = [ { name: 'HTTP', routes: [ @@ -817,7 +728,16 @@ describe('Differ V3 - basic', () => { }, ]; - expect(DifferV3.diff({ services }, { services }, {})).toEqual([]); + const oldServices = structuredClone(newServices); + oldServices[0].id = ADCSDK.utils.generateId('HTTP'); + oldServices[0].routes[0].id = ADCSDK.utils.generateId('HTTP.HTTP 1'); + oldServices[1].id = ADCSDK.utils.generateId('Stream'); + oldServices[1].stream_routes[0].id = + ADCSDK.utils.generateId('Stream.Stream 1'); + + expect( + DifferV3.diff({ services: newServices }, { services: oldServices }, {}), + ).toEqual([]); }); it('ensure boolean defaults are merged correctly', () => { @@ -827,6 +747,7 @@ describe('Differ V3 - basic', () => { strip_path_prefix: false, }; const oldService = structuredClone(service); + oldService.id = ADCSDK.utils.generateId('HTTP'); oldService.strip_path_prefix = true; expect( diff --git a/apps/cli/src/differ/specs/consumer.spec.ts b/apps/cli/src/differ/specs/consumer.spec.ts index 2813659..3ae2cbd 100644 --- a/apps/cli/src/differ/specs/consumer.spec.ts +++ b/apps/cli/src/differ/specs/consumer.spec.ts @@ -38,6 +38,9 @@ describe('Differ V3 - consumer', () => { username: consumerName, credentials: [ { + id: ADCSDK.utils.generateId( + `${consumerName}.${ADCSDK.EventType.UPDATE}`, + ), name: ADCSDK.EventType.UPDATE, type: 'basic-auth', config: { @@ -46,6 +49,9 @@ describe('Differ V3 - consumer', () => { }, }, { + id: ADCSDK.utils.generateId( + `${consumerName}.${ADCSDK.EventType.DELETE}`, + ), name: ADCSDK.EventType.DELETE, type: 'jwt-auth', config: { diff --git a/apps/cli/src/differ/specs/custom-id.spec.ts b/apps/cli/src/differ/specs/custom-id.spec.ts new file mode 100644 index 0000000..2dd2f98 --- /dev/null +++ b/apps/cli/src/differ/specs/custom-id.spec.ts @@ -0,0 +1,70 @@ +import * as ADCSDK from '@api7/adc-sdk'; + +import { DifferV3 } from '../differv3'; + +describe('Differ V3 - resources custom id', () => { + it('should delete and create new resource when update resource id (with nested resource)', () => { + const service1Name = 'Test Service 1'; + const service1Id = ADCSDK.utils.generateId(service1Name); + const service2Name = 'Test Service 2'; + const service2Id = ADCSDK.utils.generateId(service2Name); + const customId1 = 'custom-id-1'; + const customId2 = 'custom-id-2'; + expect( + DifferV3.diff( + { + services: [ + { + id: customId1, + name: service1Name, + }, + { + name: service2Name, + }, + ], + }, + { + services: [ + { + id: service1Id, + name: service1Name, + }, + { + id: customId2, + name: service2Name, + }, + ], + }, + ), + ).toEqual([ + { + oldValue: { name: service1Name }, + resourceId: service1Id, + resourceName: service1Name, + resourceType: ADCSDK.ResourceType.SERVICE, + type: ADCSDK.EventType.DELETE, + }, + { + oldValue: { name: service2Name }, + resourceId: customId2, + resourceName: service2Name, + resourceType: ADCSDK.ResourceType.SERVICE, + type: ADCSDK.EventType.DELETE, + }, + { + newValue: { name: service1Name }, + resourceId: customId1, + resourceName: service1Name, + resourceType: ADCSDK.ResourceType.SERVICE, + type: ADCSDK.EventType.CREATE, + }, + { + newValue: { name: service2Name }, + resourceId: service2Id, + resourceName: service2Name, + resourceType: ADCSDK.ResourceType.SERVICE, + type: ADCSDK.EventType.CREATE, + }, + ]); + }); +}); diff --git a/apps/cli/src/differ/specs/usecase.spec.ts b/apps/cli/src/differ/specs/usecase.spec.ts index 04330d6..6292614 100644 --- a/apps/cli/src/differ/specs/usecase.spec.ts +++ b/apps/cli/src/differ/specs/usecase.spec.ts @@ -39,15 +39,18 @@ describe('Differ V3 - usecase', () => { { services: [ { + id: ADCSDK.utils.generateId('HTTPBIN Service'), name: 'HTTPBIN Service', description: '', routes: [ { + id: ADCSDK.utils.generateId('HTTPBIN Service.Anything'), name: 'Anything', methods: ['GET'], uris: ['/anything'], }, { + id: ADCSDK.utils.generateId('HTTPBIN Service.Generate UUID'), name: 'Generate UUID', methods: ['GET'], uris: ['/uuid'], @@ -175,6 +178,7 @@ describe('Differ V3 - usecase', () => { { services: [ { + id: ADCSDK.utils.generateId('Test Service'), name: 'Test Service', upstream: { name: 'default', @@ -194,6 +198,7 @@ describe('Differ V3 - usecase', () => { }, routes: [ { + id: ADCSDK.utils.generateId('Test Service.anything'), name: 'anything', uris: ['/anything'], }, diff --git a/libs/backend-api7/e2e/misc.e2e-spec.ts b/libs/backend-api7/e2e/misc.e2e-spec.ts index 0bdcca7..87d3e9e 100644 --- a/libs/backend-api7/e2e/misc.e2e-spec.ts +++ b/libs/backend-api7/e2e/misc.e2e-spec.ts @@ -5,6 +5,7 @@ import { createEvent, deleteEvent, dumpConfiguration, + overrideEventResourceId, syncEvents, } from './support/utils'; @@ -68,4 +69,67 @@ describe('Miscellaneous', () => { expect(result.services).toHaveLength(0); }); }); + + describe('Sync resources with custom id', () => { + const routeName = 'Test Route'; + const serviceName = 'Test Service'; + const route = { + id: 'custom-route', + name: routeName, + uris: ['/test'], + }; + const service = { + id: 'custom-service', + name: serviceName, + upstream: { + scheme: 'https', + nodes: [ + { + host: 'httpbin.org', + port: 443, + weight: 100, + }, + ], + }, + routes: [route], + } as ADCSDK.Service; + + it('Create services', async () => + syncEvents(backend, [ + overrideEventResourceId( + createEvent(ADCSDK.ResourceType.SERVICE, serviceName, service), + 'custom-service', + ), + overrideEventResourceId( + createEvent(ADCSDK.ResourceType.ROUTE, routeName, route, serviceName), + 'custom-route', + 'custom-service', + ), + ])); + + it('Dump', async () => { + const result = (await dumpConfiguration(backend)) as ADCSDK.Configuration; + expect(result.services).toHaveLength(1); + expect(result.services[0]).toMatchObject(service); + expect(result.services[0].routes[0]).toMatchObject(route); + }); + + it('Delete service', async () => + syncEvents(backend, [ + overrideEventResourceId( + deleteEvent(ADCSDK.ResourceType.ROUTE, routeName, serviceName), + 'custom-route', + 'custom-service', + ), + overrideEventResourceId( + deleteEvent(ADCSDK.ResourceType.SERVICE, serviceName), + 'custom-service', + ), + ])); + + it('Dump again', async () => { + const result = (await dumpConfiguration(backend)) as ADCSDK.Configuration; + expect(result.services).toHaveLength(0); + }); + }); }); diff --git a/libs/backend-api7/e2e/support/utils.ts b/libs/backend-api7/e2e/support/utils.ts index 5d4ab8f..ee4ed95 100644 --- a/libs/backend-api7/e2e/support/utils.ts +++ b/libs/backend-api7/e2e/support/utils.ts @@ -102,6 +102,16 @@ export const deleteEvent = ( : undefined, }); +export const overrideEventResourceId = ( + event: ADCSDK.Event, + resourceId: string, + parentId?: string, +) => { + event.resourceId = resourceId; + if (parentId) event.parentId = parentId; + return event; +}; + type cond = boolean | (() => boolean); export const conditionalDescribe = (cond: cond) => diff --git a/libs/backend-api7/src/operator.ts b/libs/backend-api7/src/operator.ts index 7bb7fe0..021e84d 100644 --- a/libs/backend-api7/src/operator.ts +++ b/libs/backend-api7/src/operator.ts @@ -277,20 +277,25 @@ export class Operator { case ADCSDK.ResourceType.PLUGIN_METADATA: return event.newValue; case ADCSDK.ResourceType.SERVICE: + (event.newValue as ADCSDK.Service).id = event.resourceId; return fromADC.transformService(event.newValue as ADCSDK.Service); case ADCSDK.ResourceType.ROUTE: + (event.newValue as ADCSDK.Route).id = event.resourceId; return fromADC.transformRoute( event.newValue as ADCSDK.Route, event.parentId, ); case ADCSDK.ResourceType.STREAM_ROUTE: + (event.newValue as ADCSDK.StreamRoute).id = event.resourceId; return fromADC.transformStreamRoute( event.newValue as ADCSDK.StreamRoute, event.parentId, ); case ADCSDK.ResourceType.SSL: + (event.newValue as ADCSDK.SSL).id = event.resourceId; return fromADC.transformSSL(event.newValue as ADCSDK.SSL); case ADCSDK.ResourceType.CONSUMER_CREDENTIAL: + (event.newValue as ADCSDK.ConsumerCredential).id = event.resourceId; return fromADC.transformConsumerCredential( event.newValue as ADCSDK.ConsumerCredential, ); diff --git a/libs/backend-api7/src/transformer.ts b/libs/backend-api7/src/transformer.ts index f3e4388..1e86ac0 100644 --- a/libs/backend-api7/src/transformer.ts +++ b/libs/backend-api7/src/transformer.ts @@ -17,6 +17,7 @@ export class ToADC { public transformRoute(route: typing.Route): ADCSDK.Route { return ADCSDK.utils.recursiveOmitUndefined({ + id: route.route_id, uris: route.paths, name: route.name, description: route.desc, @@ -26,24 +27,24 @@ export class ToADC { plugins: route.plugins, priority: route.priority, timeout: route.timeout, - metadata: { id: route.route_id }, }); } public transformStreamRoute(route: typing.StreamRoute): ADCSDK.StreamRoute { return ADCSDK.utils.recursiveOmitUndefined({ + id: route.stream_route_id, name: route.name, description: route.desc, labels: ToADC.transformLabels(route.labels), server_addr: route.server_addr, server_port: route.server_port, remote_addr: route.remote_addr, - metadata: { id: route.stream_route_id }, }); } public transformService(service: typing.Service): ADCSDK.Service { return ADCSDK.utils.recursiveOmitUndefined({ + id: service.service_id, name: service.name, description: service.desc, labels: ToADC.transformLabels(service.labels), @@ -56,7 +57,6 @@ export class ToADC { stream_routes: service.stream_routes?.map((item) => this.transformStreamRoute(item), ), - metadata: { id: service.service_id }, }); } @@ -77,17 +77,18 @@ export class ToADC { ): ADCSDK.ConsumerCredential { const [pluginName, config] = Object.entries(credential.plugins)[0]; return ADCSDK.utils.recursiveOmitUndefined({ + id: credential.id, name: credential.name, description: credential.desc, labels: credential.labels, type: pluginName as ADCSDK.ConsumerCredential['type'], config, - metadata: { id: credential.id }, }); } public transformSSL(ssl: typing.SSL): ADCSDK.SSL { return ADCSDK.utils.recursiveOmitUndefined({ + id: ssl.id, labels: ToADC.transformLabels(ssl.labels), type: ssl.type, snis: ssl.snis, @@ -101,7 +102,6 @@ export class ToADC { skip_mtls_uri_regex: undefined, } : undefined, - metadata: { id: ssl.id }, }); } @@ -137,7 +137,7 @@ export class FromADC { public transformRoute(route: ADCSDK.Route, serviceId: string): typing.Route { return ADCSDK.utils.recursiveOmitUndefined({ - route_id: ADCSDK.utils.generateId(route.name), + route_id: route.id, name: route.name, desc: route.description, labels: FromADC.transformLabels(route.labels), @@ -156,7 +156,7 @@ export class FromADC { serviceId: string, ): typing.StreamRoute { return ADCSDK.utils.recursiveOmitUndefined({ - stream_route_id: ADCSDK.utils.generateId(route.name), + stream_route_id: route.id, name: route.name, desc: route.description, labels: FromADC.transformLabels(route.labels), @@ -170,7 +170,7 @@ export class FromADC { public transformService(service: ADCSDK.Service): typing.Service { return ADCSDK.utils.recursiveOmitUndefined({ - service_id: ADCSDK.utils.generateId(service.name), + service_id: service.id, name: service.name, desc: service.description, labels: FromADC.transformLabels(service.labels), @@ -198,7 +198,7 @@ export class FromADC { credential: ADCSDK.ConsumerCredential, ): typing.ConsumerCredential { return ADCSDK.utils.recursiveOmitUndefined({ - id: ADCSDK.utils.generateId(credential.name), + id: credential.id, name: credential.name, desc: credential.description, labels: FromADC.transformLabels(credential.labels), @@ -210,10 +210,9 @@ export class FromADC { public transformSSL(ssl: ADCSDK.SSL): typing.SSL { return ADCSDK.utils.recursiveOmitUndefined({ - id: ADCSDK.utils.generateId(ssl.snis.join(',')), + id: ssl.id, labels: FromADC.transformLabels(ssl.labels), status: 1, - certificates: undefined, type: ssl.type, snis: ssl.snis, diff --git a/libs/backend-apisix/e2e/misc.e2e-spec.ts b/libs/backend-apisix/e2e/misc.e2e-spec.ts new file mode 100644 index 0000000..2ffa8ee --- /dev/null +++ b/libs/backend-apisix/e2e/misc.e2e-spec.ts @@ -0,0 +1,85 @@ +import * as ADCSDK from '@api7/adc-sdk'; + +import { BackendAPISIX } from '../src'; +import { server, token } from './support/constants'; +import { + createEvent, + deleteEvent, + dumpConfiguration, + overrideEventResourceId, + syncEvents, +} from './support/utils'; + +describe('Miscellaneous', () => { + let backend: BackendAPISIX; + + beforeAll(() => { + backend = new BackendAPISIX({ + server, + token, + }); + }); + + describe('Sync resources with custom id', () => { + const routeName = 'Test Route'; + const serviceName = 'Test Service'; + const route = { + id: 'custom-route', + name: routeName, + uris: ['/test'], + }; + const service = { + id: 'custom-service', + name: serviceName, + upstream: { + scheme: 'https', + nodes: [ + { + host: 'httpbin.org', + port: 443, + weight: 100, + }, + ], + }, + routes: [route], + } as ADCSDK.Service; + + it('Create services', async () => + syncEvents(backend, [ + overrideEventResourceId( + createEvent(ADCSDK.ResourceType.SERVICE, serviceName, service), + 'custom-service', + ), + overrideEventResourceId( + createEvent(ADCSDK.ResourceType.ROUTE, routeName, route, serviceName), + 'custom-route', + 'custom-service', + ), + ])); + + it('Dump', async () => { + const result = (await dumpConfiguration(backend)) as ADCSDK.Configuration; + expect(result.services).toHaveLength(1); + expect(result.services[0]).toMatchObject(service); + expect(result.services[0].routes[0]).toMatchObject(route); + }); + + it('Delete service', async () => + syncEvents(backend, [ + overrideEventResourceId( + deleteEvent(ADCSDK.ResourceType.ROUTE, routeName, serviceName), + 'custom-route', + 'custom-service', + ), + overrideEventResourceId( + deleteEvent(ADCSDK.ResourceType.SERVICE, serviceName), + 'custom-service', + ), + ])); + + it('Dump again', async () => { + const result = (await dumpConfiguration(backend)) as ADCSDK.Configuration; + expect(result.services).toHaveLength(0); + }); + }); +}); diff --git a/libs/backend-apisix/e2e/support/utils.ts b/libs/backend-apisix/e2e/support/utils.ts index 24dc14b..5d4d3fb 100644 --- a/libs/backend-apisix/e2e/support/utils.ts +++ b/libs/backend-apisix/e2e/support/utils.ts @@ -91,6 +91,16 @@ export const deleteEvent = ( : undefined, }); +export const overrideEventResourceId = ( + event: ADCSDK.Event, + resourceId: string, + parentId?: string, +) => { + event.resourceId = resourceId; + if (parentId) event.parentId = parentId; + return event; +}; + type cond = boolean | (() => boolean); export const conditionalDescribe = (cond: cond) => diff --git a/libs/backend-apisix/project.json b/libs/backend-apisix/project.json index a57b20f..f29014a 100644 --- a/libs/backend-apisix/project.json +++ b/libs/backend-apisix/project.json @@ -26,7 +26,8 @@ ], "options": { "jestConfig": "libs/backend-apisix/jest.config.e2e.ts", - "passWithNoTests": true + "passWithNoTests": true, + "runInBand": true } } }, diff --git a/libs/backend-apisix/src/operator.ts b/libs/backend-apisix/src/operator.ts index da1d3f1..2c2a83b 100644 --- a/libs/backend-apisix/src/operator.ts +++ b/libs/backend-apisix/src/operator.ts @@ -108,10 +108,12 @@ export class Operator { case ADCSDK.ResourceType.CONSUMER: return fromADC.transformConsumer(event.newValue as ADCSDK.Consumer); case ADCSDK.ResourceType.CONSUMER_GROUP: + (event.newValue as ADCSDK.ConsumerGroup).id = event.resourceId; return fromADC.transformConsumerGroup( event.newValue as ADCSDK.ConsumerGroup, )[0]; case ADCSDK.ResourceType.CONSUMER_CREDENTIAL: + (event.newValue as ADCSDK.ConsumerCredential).id = event.resourceId; return fromADC.transformConsumerCredential( event.newValue as ADCSDK.ConsumerCredential, ); @@ -124,15 +126,19 @@ export class Operator { case ADCSDK.ResourceType.PLUGIN_METADATA: return event.newValue; case ADCSDK.ResourceType.ROUTE: { + (event.newValue as ADCSDK.Route).id = event.resourceId; const route = fromADC.transformRoute(event.newValue as ADCSDK.Route); if (event.parentId) route.service_id = event.parentId; return route; } case ADCSDK.ResourceType.SERVICE: + (event.newValue as ADCSDK.Service).id = event.resourceId; return fromADC.transformService(event.newValue as ADCSDK.Service)[0]; case ADCSDK.ResourceType.SSL: + (event.newValue as ADCSDK.SSL).id = event.resourceId; return fromADC.transformSSL(event.newValue as ADCSDK.SSL); case ADCSDK.ResourceType.STREAM_ROUTE: { + (event.newValue as ADCSDK.StreamRoute).id = event.resourceId; const route = fromADC.transformStreamRoute( event.newValue as ADCSDK.StreamRoute, ); diff --git a/libs/backend-apisix/src/transformer.ts b/libs/backend-apisix/src/transformer.ts index d49f24c..3e9d1e0 100644 --- a/libs/backend-apisix/src/transformer.ts +++ b/libs/backend-apisix/src/transformer.ts @@ -17,6 +17,7 @@ export class ToADC { public transformRoute(route: typing.Route): ADCSDK.Route { return ADCSDK.utils.recursiveOmitUndefined({ + id: route.id, name: route.name ?? route.id, description: route.desc, labels: route.labels, @@ -34,21 +35,18 @@ export class ToADC { plugins: route.plugins, plugin_config_id: route.plugin_config_id, filter_func: route.filter_func, - - metadata: { id: route.id }, } as ADCSDK.Route); } public transformService(service: typing.Service): ADCSDK.Service { return ADCSDK.utils.recursiveOmitUndefined({ + id: service.id, name: service.name ?? service.id, description: service.desc, labels: service.labels, upstream: service.upstream, plugins: service.plugins, - - metadata: { id: service.id }, } as ADCSDK.Service); } @@ -82,12 +80,12 @@ export class ToADC { ) return; return ADCSDK.utils.recursiveOmitUndefined({ + id: credential.id, name: credential.name, description: credential.desc, labels: credential.labels, type: pluginName as ADCSDK.ConsumerCredential['type'], config, - metadata: { id: credential.id }, }); } @@ -104,6 +102,7 @@ export class ToADC { ]; return ADCSDK.utils.recursiveOmitUndefined({ + id: ssl.id, labels: ssl.labels, type: ssl.type, @@ -124,8 +123,9 @@ export class ToADC { consumerGroup: typing.ConsumerGroup, consumers?: Array, ): ADCSDK.ConsumerGroup { - const adcConsumerGroup: ADCSDK.ConsumerGroup = - ADCSDK.utils.recursiveOmitUndefined({ + const adcConsumerGroup = + ADCSDK.utils.recursiveOmitUndefined({ + id: consumerGroup.id, name: (consumerGroup.labels?.ADC_NAME as string) ?? consumerGroup.id, description: consumerGroup.desc, labels: consumerGroup.labels, @@ -156,6 +156,7 @@ export class ToADC { streamRoute: typing.StreamRoute, ): ADCSDK.StreamRoute { return ADCSDK.utils.recursiveOmitUndefined({ + id: streamRoute.id, name: streamRoute.labels?.__ADC_NAME ?? streamRoute.id, description: streamRoute.desc, labels: ToADC.transformLabels(streamRoute.labels), @@ -242,14 +243,24 @@ export class FromADC { } public transformRoute(route: ADCSDK.Route): typing.Route { - return ADCSDK.utils.recursiveOmitUndefined({ - ...route, - id: undefined, + return ADCSDK.utils.recursiveOmitUndefined({ + id: route.id, + name: route.name, + desc: route.description, labels: FromADC.transformLabels(route.labels), - status: 1, + uris: route.uris, + hosts: route.hosts, + methods: route.methods, + remote_addrs: route.remote_addrs, + vars: route.vars, + filter_func: route.filter_func, - desc: route.description, - description: undefined, + //service_id: '', + enable_websocket: route.enable_websocket, + plugins: route.plugins, + priority: route.priority, + timeout: route.timeout, + status: 1, }); } @@ -266,15 +277,14 @@ export class FromADC { ?.map(this.transformStreamRoute) .map((route) => ({ ...route, service_id: serviceId })) ?? []; return [ - ADCSDK.utils.recursiveOmitUndefined({ - ...service, - id: undefined, - labels: FromADC.transformLabels(service.labels), - routes: undefined, - stream_routes: undefined, - + ADCSDK.utils.recursiveOmitUndefined({ + id: service.id, + name: service.name, desc: service.description, - description: undefined, + labels: FromADC.transformLabels(service.labels), + upstream: service.upstream, + plugins: service.plugins, + hosts: service.hosts, }), routes, streamRoutes, @@ -286,7 +296,6 @@ export class FromADC { username: consumer.username, desc: consumer.description, labels: FromADC.transformLabels(consumer.labels), - plugins: consumer.plugins, } as typing.Consumer); } @@ -295,6 +304,7 @@ export class FromADC { credential: ADCSDK.ConsumerCredential, ): typing.ConsumerCredential { return ADCSDK.utils.recursiveOmitUndefined({ + id: credential.id, name: credential.name, desc: credential.description, labels: FromADC.transformLabels(credential.labels), @@ -305,13 +315,13 @@ export class FromADC { } public transformSSL(ssl: ADCSDK.SSL): typing.SSL { - return ADCSDK.utils.recursiveOmitUndefined({ - ...ssl, - id: undefined, + return ADCSDK.utils.recursiveOmitUndefined({ + id: ssl.id, labels: FromADC.transformLabels(ssl.labels), status: 1, - certificates: undefined, + type: ssl.type, + snis: ssl.snis, cert: ssl.certificates[0].certificate, key: ssl.certificates[0].key, ...(ssl.certificates.length > 1 @@ -324,6 +334,7 @@ export class FromADC { .map((certificate) => certificate.key), } : {}), + client: ssl.client, }); } diff --git a/libs/sdk/src/core/index.ts b/libs/sdk/src/core/index.ts index 2da1499..2104cad 100644 --- a/libs/sdk/src/core/index.ts +++ b/libs/sdk/src/core/index.ts @@ -7,6 +7,7 @@ export type Plugins = Record; export type Expr = Array>; export interface Route { + id?: string; name: string; description?: string; labels?: Labels; @@ -27,6 +28,7 @@ export interface Route { } export interface Service { + id?: string; name: string; description?: string; labels?: Labels; @@ -117,6 +119,7 @@ export type UpstreamHealthCheckActiveUnhealthy = { } & UpstreamHealthCheckPassiveUnhealthy; export interface Upstream { + id?: string; name?: string; description?: string; labels?: Labels; @@ -153,6 +156,7 @@ export interface SSLCertificate { key: string; } export interface SSL { + id?: string; labels?: Labels; type?: SSLType; @@ -165,6 +169,7 @@ export interface SSL { } export interface PluginConfig { + id?: string; name: string; description?: string; labels?: Labels; @@ -179,6 +184,7 @@ export type GlobalRule = Record; export type PluginMetadata = Record; export interface ConsumerCredential { + id?: string; name: string; description?: string; labels?: Labels; @@ -200,6 +206,7 @@ export interface Consumer { } export interface ConsumerGroup { + id?: string; name: string; description?: string; labels?: Labels; @@ -212,6 +219,7 @@ export interface ConsumerGroup { } export interface StreamRoute { + id?: string; name: string; description?: string; labels?: Labels; @@ -226,9 +234,8 @@ export interface StreamRoute { metadata?: ResourceMetadata; } -export interface ResourceMetadata { - readonly id: string; -} +// eslint-disable-next-line @typescript-eslint/no-empty-interface +export interface ResourceMetadata {} export interface Configuration { services?: Array; diff --git a/package.json b/package.json index f503b9d..048afd6 100644 --- a/package.json +++ b/package.json @@ -70,5 +70,5 @@ "yaml": "^2.4.2", "zod": "^3.23.8" }, - "packageManager": "pnpm@9.5.0+sha512.140036830124618d624a2187b50d04289d5a087f326c9edfc0ccd733d76c4f52c3a313d4fc148794a2a9d81553016004e6742e8cf850670268a7387fc220c903" + "packageManager": "pnpm@9.12.3+sha512.cce0f9de9c5a7c95bef944169cc5dfe8741abfb145078c0d508b868056848a87c81e626246cb60967cbd7fd29a6c062ef73ff840d96b3c86c40ac92cf4a813ee" }