Skip to content

Commit dca4cdc

Browse files
LiteSunbzp2010
andauthored
feat(apisix): separate inline upstream (#354)
Co-authored-by: bzp2010 <bzp2010@apache.org>
1 parent 0083f9a commit dca4cdc

File tree

6 files changed

+168
-37
lines changed

6 files changed

+168
-37
lines changed

libs/backend-apisix/e2e/resources/service-upstream.e2e-spec.ts

Lines changed: 87 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import { Differ } from '@api7/adc-differ';
12
import * as ADCSDK from '@api7/adc-sdk';
23

34
import { BackendAPISIX } from '../../src';
@@ -22,6 +23,92 @@ describe('Service-Upstreams E2E', () => {
2223
});
2324
});
2425

26+
describe('Service inline upstream', () => {
27+
const serviceName = 'test-inline-upstream';
28+
const service = {
29+
name: serviceName,
30+
upstream: {
31+
type: 'roundrobin',
32+
nodes: [
33+
{
34+
host: 'httpbin.org',
35+
port: 443,
36+
weight: 100,
37+
},
38+
],
39+
},
40+
} satisfies ADCSDK.Service;
41+
42+
it('Create service with inline upstream', async () =>
43+
syncEvents(backend, Differ.diff({ services: [service] }, {})));
44+
45+
it('Dump (inline upstream should exist)', async () => {
46+
const result = await dumpConfiguration(backend);
47+
const testService = result.services?.find((s) => s.name === serviceName);
48+
expect(testService).toBeDefined();
49+
expect(testService?.upstream).toMatchObject({
50+
type: 'roundrobin',
51+
nodes: [
52+
{
53+
host: 'httpbin.org',
54+
port: 443,
55+
weight: 100,
56+
},
57+
],
58+
});
59+
// Verify that inline upstream has no id and name
60+
expect(testService?.upstream?.id).toBeUndefined();
61+
expect(testService?.upstream?.name).toBeUndefined();
62+
});
63+
64+
const updatedService = {
65+
name: serviceName,
66+
upstream: {
67+
type: 'roundrobin',
68+
nodes: [
69+
{
70+
host: 'httpbin.org',
71+
port: 443,
72+
weight: 50,
73+
},
74+
{
75+
host: 'example.com',
76+
port: 80,
77+
weight: 50,
78+
},
79+
],
80+
},
81+
} satisfies ADCSDK.Service;
82+
it('Update service inline upstream', async () =>
83+
syncEvents(
84+
backend,
85+
Differ.diff(
86+
{ services: [updatedService] },
87+
await dumpConfiguration(backend),
88+
),
89+
));
90+
91+
it('Dump (inline upstream should be updated)', async () => {
92+
const result = await dumpConfiguration(backend);
93+
const testService = result.services?.find((s) => s.name === serviceName);
94+
expect(testService).toBeDefined();
95+
expect(testService?.upstream?.nodes).toHaveLength(2);
96+
expect(testService?.upstream).toMatchObject(updatedService.upstream);
97+
// Verify that inline upstream still has no id and name
98+
expect(testService?.upstream?.id).toBeUndefined();
99+
expect(testService?.upstream?.name).toBeUndefined();
100+
});
101+
102+
it('Delete service with inline upstream', async () =>
103+
syncEvents(backend, Differ.diff({}, await dumpConfiguration(backend))));
104+
105+
it('Dump again (service should not exist)', async () => {
106+
const result = await dumpConfiguration(backend);
107+
const testService = result.services?.find((s) => s.name === serviceName);
108+
expect(testService).toBeUndefined();
109+
});
110+
});
111+
25112
describe('Service multiple upstreams', () => {
26113
const serviceName = 'test';
27114
const service = {

libs/backend-apisix/package.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,8 @@
99
}
1010
},
1111
"devDependencies": {
12-
"@api7/adc-sdk": "workspace:*"
12+
"@api7/adc-sdk": "workspace:*",
13+
"@api7/adc-differ": "workspace:*"
1314
},
1415
"nx": {
1516
"name": "backend-apisix",

libs/backend-apisix/src/fetcher.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import * as ADCSDK from '@api7/adc-sdk';
22
import { type AxiosInstance } from 'axios';
33
import { produce } from 'immer';
4+
import { unset } from 'lodash';
45
import {
56
Subject,
67
combineLatest,
@@ -271,6 +272,8 @@ export class Fetcher extends ADCSDK.backend.BackendEventSource {
271272
produce(service, (serviceDraft) => {
272273
if (service.upstream_id)
273274
serviceDraft.upstream = upstreamIdMap[service.upstream_id];
275+
unset(serviceDraft, 'upstream.id');
276+
unset(serviceDraft, 'upstream.name');
274277
if (upstreamServiceIdMap[service.id])
275278
serviceDraft.upstreams = upstreamServiceIdMap[service.id];
276279
}),

libs/backend-apisix/src/operator.ts

Lines changed: 47 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -6,11 +6,13 @@ import {
66
Subject,
77
catchError,
88
concatMap,
9+
defer,
910
from,
1011
map,
1112
mergeMap,
1213
of,
1314
reduce,
15+
retry,
1416
tap,
1517
throwError,
1618
} from 'rxjs';
@@ -37,21 +39,42 @@ export class Operator extends ADCSDK.backend.BackendEventSource {
3739
private operate(event: ADCSDK.Event) {
3840
const { type, resourceType, resourceId, parentId } = event;
3941
const isUpdate = type !== ADCSDK.EventType.DELETE;
40-
const path = `/apisix/admin/${
41-
resourceType === ADCSDK.ResourceType.CONSUMER_CREDENTIAL
42-
? `consumers/${parentId}/credentials/${resourceId}`
43-
: `${resourceTypeToAPIName(resourceType)}/${resourceId}`
44-
}`;
42+
const PATH_PREFIX = '/apisix/admin';
43+
const paths = [
44+
`${PATH_PREFIX}/${
45+
resourceType === ADCSDK.ResourceType.CONSUMER_CREDENTIAL
46+
? `consumers/${parentId}/credentials/${resourceId}`
47+
: `${resourceTypeToAPIName(resourceType)}/${resourceId}`
48+
}`,
49+
];
4550

46-
return from(
47-
this.client.request({
48-
method: 'DELETE',
49-
url: path,
50-
...(isUpdate && {
51-
method: 'PUT',
52-
data: this.fromADC(event, this.opts.version),
53-
}),
54-
}),
51+
if (event.resourceType === ADCSDK.ResourceType.SERVICE) {
52+
const path = `${PATH_PREFIX}/upstreams/${event.resourceId}`;
53+
if (event.type === ADCSDK.EventType.DELETE)
54+
paths.push(path); // services will be deleted before upstreams
55+
else paths.unshift(path); // services will be created/updated after upstreams
56+
}
57+
58+
const operateWithRetry = (op: () => Promise<AxiosResponse>) =>
59+
defer(op).pipe(retry({ count: 3, delay: 100 }));
60+
return from(paths).pipe(
61+
map(
62+
(path) => () =>
63+
this.client.request({
64+
method: 'DELETE',
65+
url: path,
66+
...(isUpdate && {
67+
method: 'PUT',
68+
data:
69+
event.resourceType === ADCSDK.ResourceType.SERVICE &&
70+
path.includes('/upstreams')
71+
? (this.fromADC(event, this.opts.version) as typing.Service)
72+
.upstream
73+
: this.fromADC(event, this.opts.version),
74+
}),
75+
}),
76+
),
77+
concatMap(operateWithRetry),
5578
);
5679
}
5780

@@ -192,14 +215,21 @@ export class Operator extends ADCSDK.backend.BackendEventSource {
192215
(event.newValue as ADCSDK.Route).id = event.resourceId;
193216
const route = fromADC.transformRoute(
194217
event.newValue as ADCSDK.Route,
195-
event.parentId,
218+
event.parentId!,
196219
);
197220
if (event.parentId) route.service_id = event.parentId;
198221
return route;
199222
}
200-
case ADCSDK.ResourceType.SERVICE:
223+
case ADCSDK.ResourceType.SERVICE: {
201224
(event.newValue as ADCSDK.Service).id = event.resourceId;
202-
return fromADC.transformService(event.newValue as ADCSDK.Service);
225+
const [service, upstream] = fromADC.transformService(
226+
event.newValue as ADCSDK.Service,
227+
);
228+
return {
229+
...service,
230+
...(upstream && { upstream: upstream }),
231+
};
232+
}
203233
case ADCSDK.ResourceType.SSL:
204234
(event.newValue as ADCSDK.SSL).id = event.resourceId;
205235
return fromADC.transformSSL(event.newValue as ADCSDK.SSL);

libs/backend-apisix/src/transformer.ts

Lines changed: 24 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -47,7 +47,9 @@ export class ToADC {
4747

4848
hosts: service.hosts,
4949

50-
upstream: this.transformUpstream(service.upstream),
50+
upstream: service.upstream
51+
? this.transformUpstream(service.upstream)
52+
: undefined,
5153
upstreams: service.upstreams,
5254
plugins: service.plugins,
5355
} as ADCSDK.Service);
@@ -276,16 +278,27 @@ export class FromADC {
276278
});
277279
}
278280

279-
public transformService(service: ADCSDK.Service): typing.Service {
280-
return ADCSDK.utils.recursiveOmitUndefined<typing.Service>({
281-
id: service.id,
282-
name: service.name,
283-
desc: service.description,
284-
labels: FromADC.transformLabels(service.labels),
285-
upstream: this.transformUpstream(service.upstream),
286-
plugins: service.plugins,
287-
hosts: service.hosts,
288-
});
281+
public transformService(
282+
service: ADCSDK.Service,
283+
): [typing.Service, typing.Upstream | undefined] {
284+
return [
285+
ADCSDK.utils.recursiveOmitUndefined<typing.Service>({
286+
id: service.id,
287+
name: service.name,
288+
desc: service.description,
289+
labels: FromADC.transformLabels(service.labels),
290+
upstream_id: service.id,
291+
plugins: service.plugins,
292+
hosts: service.hosts,
293+
}),
294+
service.upstream
295+
? ({
296+
...this.transformUpstream(service.upstream),
297+
id: service.id,
298+
name: service.name,
299+
} as typing.Upstream)
300+
: undefined,
301+
];
289302
}
290303

291304
public transformConsumer(consumer: ADCSDK.Consumer): typing.Consumer {

pnpm-lock.yaml

Lines changed: 5 additions & 8 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)