Skip to content

Commit c80daf2

Browse files
authored
feat(apisix): support multiple upstream of service (#254)
1 parent e0d4f3a commit c80daf2

8 files changed

Lines changed: 189 additions & 4 deletions

File tree

apps/cli/src/differ/differv3.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -318,6 +318,7 @@ export class DifferV3 {
318318

319319
const checkedRemoteId: Array<ADCSDK.ResourceId> = [];
320320
remote.forEach(([remoteName, remoteId, remoteItem]) => {
321+
remoteItem = cloneDeep(remoteItem); //TODO handle potentially immutable objects systematically
321322
unset(remoteItem, 'metadata');
322323
unset(remoteItem, 'id');
323324

apps/cli/src/differ/specs/basic.spec.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -768,7 +768,7 @@ describe('Differ V3 - basic', () => {
768768
{ kind: 'E', lhs: true, path: ['strip_path_prefix'], rhs: false },
769769
],
770770
newValue: service,
771-
oldValue: oldService,
771+
oldValue: { ...service, strip_path_prefix: true },
772772
resourceId: ADCSDK.utils.generateId(service.name),
773773
resourceName: service.name,
774774
resourceType: ADCSDK.ResourceType.SERVICE,

apps/cli/src/differ/specs/usecase.spec.ts

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -93,8 +93,14 @@ describe('Differ V3 - usecase', () => {
9393
description: '',
9494
name: 'HTTPBIN Service',
9595
routes: [
96-
{ methods: ['GET'], name: 'Anything', uris: ['/anything'] },
9796
{
97+
id: ADCSDK.utils.generateId('HTTPBIN Service.Anything'),
98+
methods: ['GET'],
99+
name: 'Anything',
100+
uris: ['/anything'],
101+
},
102+
{
103+
id: ADCSDK.utils.generateId('HTTPBIN Service.Generate UUID'),
98104
name: 'Generate UUID',
99105
methods: ['GET'],
100106
uris: ['/uuid'],
Lines changed: 139 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,139 @@
1+
import * as ADCSDK from '@api7/adc-sdk';
2+
3+
import { BackendAPISIX } from '../../src';
4+
import { server, token } from '../support/constants';
5+
import {
6+
createEvent,
7+
deleteEvent,
8+
dumpConfiguration,
9+
sortResult,
10+
syncEvents,
11+
updateEvent,
12+
} from '../support/utils';
13+
14+
describe('Service-Upstreams E2E', () => {
15+
let backend: BackendAPISIX;
16+
17+
beforeAll(() => {
18+
backend = new BackendAPISIX({
19+
server,
20+
token,
21+
tlsSkipVerify: true,
22+
});
23+
});
24+
25+
describe('Service multiple upstreams', () => {
26+
const serviceName = 'test';
27+
const service = {
28+
name: serviceName,
29+
upstream: {
30+
type: 'roundrobin',
31+
nodes: [
32+
{
33+
host: 'httpbin.org',
34+
port: 443,
35+
weight: 100,
36+
},
37+
],
38+
},
39+
} satisfies ADCSDK.Service;
40+
const upstreamND1Name = 'nd-upstream1';
41+
const upstreamND1 = {
42+
name: upstreamND1Name,
43+
type: 'roundrobin',
44+
scheme: 'https',
45+
nodes: [
46+
{
47+
host: '1.1.1.1',
48+
port: 443,
49+
weight: 100,
50+
},
51+
],
52+
} satisfies ADCSDK.Upstream;
53+
const upstreamND2Name = 'nd-upstream2';
54+
const upstreamND2 = {
55+
name: upstreamND2Name,
56+
type: 'roundrobin',
57+
scheme: 'https',
58+
nodes: [
59+
{
60+
host: '1.0.0.1',
61+
port: 443,
62+
weight: 100,
63+
},
64+
],
65+
} satisfies ADCSDK.Upstream;
66+
67+
it('Create service and upstreams', async () =>
68+
syncEvents(backend, [
69+
createEvent(ADCSDK.ResourceType.SERVICE, serviceName, service),
70+
createEvent(
71+
ADCSDK.ResourceType.UPSTREAM,
72+
upstreamND1Name,
73+
upstreamND1,
74+
serviceName,
75+
),
76+
createEvent(
77+
ADCSDK.ResourceType.UPSTREAM,
78+
upstreamND2Name,
79+
upstreamND2,
80+
serviceName,
81+
),
82+
]));
83+
84+
it('Dump', async () => {
85+
const result = await dumpConfiguration(backend);
86+
expect(result.services).toHaveLength(1);
87+
expect(result.services[0]).toMatchObject(service);
88+
expect(result.services[0].upstreams).toHaveLength(2);
89+
90+
const upstreams = sortResult(result.services[0].upstreams, 'name');
91+
expect(upstreams[0]).toMatchObject(upstreamND1);
92+
expect(upstreams[1]).toMatchObject(upstreamND2);
93+
});
94+
95+
const newUpstreamND1 = {
96+
...structuredClone(upstreamND1),
97+
retry_timeout: 100,
98+
} as ADCSDK.Upstream;
99+
it('Update service non-default upstream 1', async () =>
100+
syncEvents(backend, [
101+
updateEvent(
102+
ADCSDK.ResourceType.UPSTREAM,
103+
upstreamND1Name,
104+
newUpstreamND1,
105+
serviceName,
106+
),
107+
]));
108+
109+
it('Dump (updated non-default upstream 1)', async () => {
110+
const result = await dumpConfiguration(backend);
111+
expect(result.services).toHaveLength(1);
112+
113+
const upstreams = sortResult(result.services[0].upstreams, 'name');
114+
expect(upstreams[0]).toMatchObject(newUpstreamND1);
115+
});
116+
117+
it('Delete non-default upstream 2', async () =>
118+
syncEvents(backend, [
119+
deleteEvent(ADCSDK.ResourceType.UPSTREAM, upstreamND2Name, serviceName),
120+
]));
121+
122+
it('Dump (non-default upstream 2 should not exist)', async () => {
123+
const result = await dumpConfiguration(backend);
124+
expect(result.services).toHaveLength(1);
125+
expect(result.services[0].upstreams).toHaveLength(1);
126+
expect(result.services[0].upstreams[0]).toMatchObject(newUpstreamND1);
127+
});
128+
129+
it('Delete', async () =>
130+
syncEvents(backend, [
131+
deleteEvent(ADCSDK.ResourceType.SERVICE, serviceName),
132+
]));
133+
134+
it('Dump again (service should not exist)', async () => {
135+
const result = await dumpConfiguration(backend);
136+
expect(result.services).toHaveLength(0);
137+
});
138+
});
139+
});

libs/backend-apisix/src/fetcher.ts

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -239,6 +239,20 @@ export class Fetcher extends ADCSDK.backend.BackendEventSource {
239239
this.toADC.transformUpstream(item),
240240
]),
241241
);
242+
243+
// If upstreams are explicitly specified with associated service, index them separately
244+
const upstreamServiceIdMap = resources?.upstream?.reduce<
245+
Record<string, ADCSDK.Upstream[]>
246+
>((pv, cv) => {
247+
const serviceId = cv.labels?.[
248+
typing.ADC_UPSTREAM_SERVICE_ID_LABEL
249+
] as string;
250+
if (serviceId) {
251+
if (!pv[serviceId]) pv[serviceId] = [];
252+
pv[serviceId].push(upstreamIdMap[cv.id]);
253+
}
254+
return pv;
255+
}, {});
242256
return produce(resources, (draft) => {
243257
draft.route = resources?.[ADCSDK.ResourceType.ROUTE]?.map((route) =>
244258
produce(route, (routeDraft) => {
@@ -251,6 +265,8 @@ export class Fetcher extends ADCSDK.backend.BackendEventSource {
251265
produce(service, (serviceDraft) => {
252266
if (service.upstream_id)
253267
serviceDraft.upstream = upstreamIdMap[service.upstream_id];
268+
if (upstreamServiceIdMap[service.id])
269+
serviceDraft.upstreams = upstreamServiceIdMap[service.id];
254270
}),
255271
);
256272
});

libs/backend-apisix/src/operator.ts

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ import {
1616
import { SemVer, gte as semVerGTE, lt as semVerLT } from 'semver';
1717

1818
import { FromADC } from './transformer';
19+
import * as typing from './typing';
1920
import { capitalizeFirstLetter, resourceTypeToAPIName } from './utils';
2021

2122
export interface OperatorOptions {
@@ -38,7 +39,7 @@ export class Operator extends ADCSDK.backend.BackendEventSource {
3839
const path = `/apisix/admin/${
3940
resourceType === ADCSDK.ResourceType.CONSUMER_CREDENTIAL
4041
? `consumers/${parentId}/credentials/${resourceId}`
41-
: `${resourceType === ADCSDK.ResourceType.STREAM_ROUTE ? 'stream_routes' : resourceTypeToAPIName(resourceType)}/${resourceId}`
42+
: `${resourceTypeToAPIName(resourceType)}/${resourceId}`
4243
}`;
4344

4445
return from(
@@ -204,6 +205,17 @@ export class Operator extends ADCSDK.backend.BackendEventSource {
204205
if (event.parentId) route.service_id = event.parentId;
205206
return route;
206207
}
208+
case ADCSDK.ResourceType.UPSTREAM: {
209+
const upstream = fromADC.transformUpstream(
210+
event.newValue as ADCSDK.Upstream,
211+
);
212+
if (event.parentId)
213+
upstream.labels = {
214+
...upstream.labels,
215+
[typing.ADC_UPSTREAM_SERVICE_ID_LABEL]: event.parentId,
216+
};
217+
return upstream;
218+
}
207219
}
208220
}
209221
}

libs/backend-apisix/src/transformer.ts

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,7 @@ export class ToADC {
4848
hosts: service.hosts,
4949

5050
upstream: service.upstream,
51+
upstreams: service.upstreams,
5152
plugins: service.plugins,
5253
} as ADCSDK.Service);
5354
}
@@ -196,10 +197,15 @@ export class ToADC {
196197
};
197198
})
198199
: undefined;
200+
const labels = ADCSDK.utils.recursiveOmitUndefined({
201+
...upstream.labels,
202+
[typing.ADC_UPSTREAM_SERVICE_ID_LABEL]: undefined,
203+
});
199204
return ADCSDK.utils.recursiveOmitUndefined({
205+
id: upstream.id,
200206
name: upstream.name ?? upstream.id,
201207
description: upstream.desc,
202-
labels: upstream.labels,
208+
labels: Object.keys(labels).length > 0 ? labels : undefined,
203209

204210
type: upstream.type,
205211
hash_on: upstream.hash_on,

libs/backend-apisix/src/typing.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,8 @@ import {
1414
UpstreamTimeout,
1515
} from '@api7/adc-sdk';
1616

17+
export const ADC_UPSTREAM_SERVICE_ID_LABEL = '__ADC_UPSTREAM_SERVICE_ID';
18+
1719
export interface Route {
1820
id: string;
1921
name?: string;
@@ -58,6 +60,9 @@ export interface Service {
5860
plugins?: Plugins;
5961
script?: string;
6062
enable_websocket?: boolean;
63+
64+
// internal use only
65+
upstreams?: Array<ADCUpstream>;
6166
}
6267
export interface ConsumerCredential {
6368
id?: string;

0 commit comments

Comments
 (0)