From 1443e8cb197ebaf78390340ea449cab10d7030a8 Mon Sep 17 00:00:00 2001 From: bzp2010 Date: Mon, 8 Sep 2025 23:49:38 +0800 Subject: [PATCH] fix(apisix-standalone): duplicate upstreams when use service multiple upstreams --- .../resources/service-upstream.e2e-spec.ts | 187 ++++++++++++++++++ .../src/transformer.ts | 15 +- libs/differ/src/index.ts | 2 +- 3 files changed, 200 insertions(+), 4 deletions(-) create mode 100644 libs/backend-apisix-standalone/e2e/resources/service-upstream.e2e-spec.ts diff --git a/libs/backend-apisix-standalone/e2e/resources/service-upstream.e2e-spec.ts b/libs/backend-apisix-standalone/e2e/resources/service-upstream.e2e-spec.ts new file mode 100644 index 00000000..1ce7c96a --- /dev/null +++ b/libs/backend-apisix-standalone/e2e/resources/service-upstream.e2e-spec.ts @@ -0,0 +1,187 @@ +import { Differ } from '@api7/adc-differ'; +import * as ADCSDK from '@api7/adc-sdk'; + +import { BackendAPISIXStandalone } from '../../src'; +import { + config as configCache, + rawConfig as rawConfigCache, +} from '../../src/cache'; +import * as typing from '../../src/typing'; +import { server1, token1 } from '../support/constants'; +import { + createEvent, + deleteEvent, + dumpConfiguration, + restartAPISIX, + syncEvents, + updateEvent, +} from '../support/utils'; + +const cacheKey = 'default'; +describe('Service-Upstreams E2E', () => { + let backend: BackendAPISIXStandalone; + + beforeAll(async () => { + await restartAPISIX(); + backend = new BackendAPISIXStandalone({ + server: server1, + token: token1, + tlsSkipVerify: true, + cacheKey, + }); + }); + + describe('Sync and dump service with multiple upstreams', () => { + const upstreamND1Name = 'nd-upstream1'; + const upstreamND1 = { + name: upstreamND1Name, + type: 'roundrobin', + scheme: 'https', + nodes: [ + { + host: '1.1.1.1', + port: 443, + weight: 100, + }, + ], + } satisfies ADCSDK.Upstream; + const upstreamND2Name = 'nd-upstream2'; + const upstreamND2 = { + //@ts-expect-error custom id + id: upstreamND2Name, + name: upstreamND2Name, + type: 'roundrobin', + scheme: 'https', + nodes: [ + { + host: '1.0.0.1', + port: 443, + weight: 100, + }, + ], + } satisfies ADCSDK.Upstream; + const serviceName = 'test'; + const service = { + name: serviceName, + upstream: { + type: 'roundrobin', + nodes: [ + { + host: 'httpbin.org', + port: 443, + weight: 100, + }, + ], + }, + upstreams: [upstreamND1, upstreamND2], + } satisfies ADCSDK.Service; + + it('Initialize cache', () => + expect(dumpConfiguration(backend)).resolves.not.toThrow()); + + it('Create', async () => + syncEvents( + backend, + Differ.diff( + { + services: [service], + }, + await dumpConfiguration(backend), + ), + )); + + const checkOriginalConfig = () => { + const rawConfig = rawConfigCache.get(cacheKey); + expect(rawConfig?.services?.[0].id).toEqual( + ADCSDK.utils.generateId(serviceName), + ); + expect(rawConfig?.upstreams).not.toBeUndefined(); + expect(rawConfig?.upstreams).toHaveLength(3); + expect(rawConfig?.upstreams?.[0].name).toEqual(serviceName); + expect(rawConfig?.upstreams?.[1].name).toEqual(upstreamND1Name); + expect(rawConfig?.upstreams?.[2].name).toEqual(upstreamND2Name); + expect(rawConfig?.upstreams?.[0].labels).toBeUndefined(); + expect( + rawConfig?.upstreams?.[1].labels?.[ + typing.ADC_UPSTREAM_SERVICE_ID_LABEL + ], + ).toEqual(ADCSDK.utils.generateId(serviceName)); + expect( + rawConfig?.upstreams?.[2].labels?.[ + typing.ADC_UPSTREAM_SERVICE_ID_LABEL + ], + ).toEqual(ADCSDK.utils.generateId(serviceName)); + + const config = configCache.get(cacheKey); + expect(config?.services).not.toBeUndefined(); + expect(config?.services).toHaveLength(1); + expect(config?.services?.[0].upstreams).toHaveLength(2); + expect( + config?.services?.[0].upstreams?.[0].labels?.[ + typing.ADC_UPSTREAM_SERVICE_ID_LABEL + ], + ).toBeUndefined(); + expect( + config?.services?.[0].upstreams?.[1].labels?.[ + typing.ADC_UPSTREAM_SERVICE_ID_LABEL + ], + ).toBeUndefined(); + }; + it('Check cache', checkOriginalConfig); + + it('Try update (without any change)', async () => + syncEvents( + backend, + Differ.diff( + { + services: [service], + }, + await dumpConfiguration(backend), + ), + )); + + it('Check cache 2', checkOriginalConfig); + + it('Try update', async () => { + const newService = structuredClone(service); + newService.upstreams[0].nodes[0].host = '8.8.8.8'; + await syncEvents( + backend, + Differ.diff( + { + services: [newService], + }, + await dumpConfiguration(backend), + ), + ); + }); + + it('Check updated cache', () => { + const rawConfig = rawConfigCache.get(cacheKey); + expect(rawConfig?.services?.[0].id).toEqual( + ADCSDK.utils.generateId(serviceName), + ); + expect(rawConfig?.upstreams).not.toBeUndefined(); + expect(rawConfig?.upstreams).toHaveLength(3); + expect( + rawConfig?.upstreams?.[1].labels?.[ + typing.ADC_UPSTREAM_SERVICE_ID_LABEL + ], + ).toEqual(ADCSDK.utils.generateId(serviceName)); + expect(rawConfig?.upstreams?.[1].nodes[0].host).toEqual('8.8.8.8'); + + const config = configCache.get(cacheKey); + expect(config?.services).not.toBeUndefined(); + expect(config?.services).toHaveLength(1); + expect(config?.services?.[0].upstreams).toHaveLength(2); + expect( + config?.services?.[0].upstreams?.[0].labels?.[ + typing.ADC_UPSTREAM_SERVICE_ID_LABEL + ], + ).toBeUndefined(); + expect(config?.services?.[0].upstreams?.[0].nodes?.[0].host).toEqual( + '8.8.8.8', + ); + }); + }); +}); diff --git a/libs/backend-apisix-standalone/src/transformer.ts b/libs/backend-apisix-standalone/src/transformer.ts index 4ddfcde0..31b8f784 100644 --- a/libs/backend-apisix-standalone/src/transformer.ts +++ b/libs/backend-apisix-standalone/src/transformer.ts @@ -1,5 +1,5 @@ import * as ADCSDK from '@api7/adc-sdk'; -import { isEmpty } from 'lodash'; +import { cloneDeep, isEmpty, unset } from 'lodash'; import * as typing from './typing'; @@ -12,7 +12,7 @@ export const toADC = (input: typing.APISIXStandalone) => { upstream: Omit & { name?: string; }, - ) => ({ + ): ADCSDK.Upstream => ({ name: upstream.name, description: upstream.desc, labels: upstream.labels, @@ -96,7 +96,16 @@ export const toADC = (input: typing.APISIXStandalone) => { upstream.labels?.[typing.ADC_UPSTREAM_SERVICE_ID_LABEL] === service.id, ) - .map(transformUpstream) + .map((upstream) => { + const up = transformUpstream( + cloneDeep(upstream), + ) as ADCSDK.Upstream & { + id: string; + }; + up.id = upstream.id; + unset(up, `labels.${typing.ADC_UPSTREAM_SERVICE_ID_LABEL}`); + return up; + }) .map(ADCSDK.utils.recursiveOmitUndefined), })) .map(ADCSDK.utils.recursiveOmitUndefined) ?? [], diff --git a/libs/differ/src/index.ts b/libs/differ/src/index.ts index f3d5c6d0..183706ec 100644 --- a/libs/differ/src/index.ts +++ b/libs/differ/src/index.ts @@ -1 +1 @@ -export { DifferV3 } from './differv3.js'; +export { DifferV3, DifferV3 as Differ } from './differv3.js';