Skip to content

Commit aa946dd

Browse files
authored
feat(apisix): support consumer credentials (#200)
1 parent f22410a commit aa946dd

File tree

12 files changed

+330
-41
lines changed

12 files changed

+330
-41
lines changed

.github/workflows/e2e.yaml

+5-1
Original file line numberDiff line numberDiff line change
@@ -10,8 +10,12 @@ on:
1010
jobs:
1111
apisix:
1212
runs-on: ubuntu-latest
13+
strategy:
14+
matrix:
15+
version: [3.8.1, 3.9.1, 3.10.0, 3.11.0]
1316
env:
14-
BACKEND_APISIX_VERSION: 3.9.1-debian
17+
BACKEND_APISIX_VERSION: ${{ matrix.version }}
18+
BACKEND_APISIX_IMAGE: ${{ matrix.version }}-debian
1519
steps:
1620
- uses: actions/checkout@v4
1721

apps/cli/src/command/utils.ts

+4-1
Original file line numberDiff line numberDiff line change
@@ -253,7 +253,7 @@ export const recursiveRemoveMetadataField = (c: ADCSDK.Configuration) => {
253253
if ('metadata' in obj) delete obj.metadata;
254254
};
255255
Object.entries(c).forEach(([key, value]) => {
256-
if (['global_rules', 'plugin_metadata', 'consumers'].includes(key)) return;
256+
if (['global_rules', 'plugin_metadata'].includes(key)) return;
257257
if (Array.isArray(value))
258258
value.forEach((item) => {
259259
removeMetadata(item);
@@ -265,6 +265,9 @@ export const recursiveRemoveMetadataField = (c: ADCSDK.Configuration) => {
265265
} else if (key === 'consumer_groups') {
266266
if ('consumers' in item && Array.isArray(item.consumers))
267267
item.consumers.forEach((c) => removeMetadata(c));
268+
} else if (key === 'consumers') {
269+
if ('credentials' in item && Array.isArray(item.credentials))
270+
item.credentials.forEach((c) => removeMetadata(c));
268271
}
269272
});
270273
});

libs/backend-apisix/e2e/assets/docker-compose.yaml

+2-2
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
services:
22
apisix_http:
3-
image: apache/apisix:${BACKEND_APISIX_VERSION:-3.9.0-debian}
3+
image: apache/apisix:${BACKEND_APISIX_IMAGE:-3.9.0-debian}
44
restart: always
55
volumes:
66
- ./apisix_conf/http.yaml:/usr/local/apisix/conf/config.yaml:ro
@@ -13,7 +13,7 @@ services:
1313
apisix:
1414

1515
apisix_mtls:
16-
image: apache/apisix:${BACKEND_APISIX_VERSION:-3.9.0-debian}
16+
image: apache/apisix:${BACKEND_APISIX_IMAGE:-3.9.0-debian}
1717
restart: always
1818
volumes:
1919
- ./apisix_conf/mtls.yaml:/usr/local/apisix/conf/config.yaml:ro
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,116 @@
1+
import * as ADCSDK from '@api7/adc-sdk';
2+
import { gte, lt } from 'semver';
3+
4+
import { BackendAPISIX } from '../../src';
5+
import { server, token } from '../support/constants';
6+
import { conditionalDescribe, semverCondition } from '../support/utils';
7+
import {
8+
createEvent,
9+
deleteEvent,
10+
dumpConfiguration,
11+
syncEvents,
12+
updateEvent,
13+
} from '../support/utils';
14+
15+
describe('Consumer E2E', () => {
16+
let backend: BackendAPISIX;
17+
18+
beforeAll(() => {
19+
backend = new BackendAPISIX({
20+
server,
21+
token,
22+
tlsSkipVerify: true,
23+
});
24+
});
25+
26+
conditionalDescribe(semverCondition(gte, '3.11.0'))(
27+
'Sync and dump consumers (with credential support)',
28+
() => {
29+
const consumer1Name = 'consumer1';
30+
const consumer1Key = 'consumer1-key';
31+
const consumer1Cred = {
32+
name: consumer1Key,
33+
type: 'key-auth',
34+
config: { key: consumer1Key },
35+
};
36+
const consumer1 = {
37+
username: consumer1Name,
38+
credentials: [consumer1Cred],
39+
} as ADCSDK.Consumer;
40+
41+
it('Create consumers', async () =>
42+
syncEvents(backend, [
43+
createEvent(ADCSDK.ResourceType.CONSUMER, consumer1Name, consumer1),
44+
createEvent(
45+
ADCSDK.ResourceType.CONSUMER_CREDENTIAL,
46+
consumer1Key,
47+
consumer1Cred,
48+
consumer1Name,
49+
),
50+
]));
51+
52+
it('Dump', async () => {
53+
const result = (await dumpConfiguration(
54+
backend,
55+
)) as ADCSDK.Configuration;
56+
expect(result.consumers).toHaveLength(1);
57+
expect(result.consumers[0]).toMatchObject(consumer1);
58+
expect(result.consumers[0].credentials).toMatchObject(
59+
consumer1.credentials,
60+
);
61+
});
62+
63+
it('Update consumer credential', async () => {
64+
consumer1.credentials[0].config.key = 'new-key';
65+
await syncEvents(backend, [
66+
updateEvent(
67+
ADCSDK.ResourceType.CONSUMER_CREDENTIAL,
68+
consumer1Key,
69+
consumer1Cred,
70+
consumer1Name,
71+
),
72+
]);
73+
});
74+
75+
it('Dump again (consumer credential updated)', async () => {
76+
const result = (await dumpConfiguration(
77+
backend,
78+
)) as ADCSDK.Configuration;
79+
expect(result.consumers[0]).toMatchObject(consumer1);
80+
expect(result.consumers[0].credentials[0].config.key).toEqual(
81+
'new-key',
82+
);
83+
});
84+
85+
it('Delete consumer credential', async () =>
86+
syncEvents(backend, [
87+
deleteEvent(
88+
ADCSDK.ResourceType.CONSUMER_CREDENTIAL,
89+
consumer1Key,
90+
consumer1Name,
91+
),
92+
]));
93+
94+
it('Dump again (consumer credential should not exist)', async () => {
95+
const result = (await dumpConfiguration(
96+
backend,
97+
)) as ADCSDK.Configuration;
98+
expect(result.consumers).toHaveLength(1);
99+
console.log(result.consumers[0]);
100+
expect(result.consumers[0].credentials).toBeUndefined();
101+
});
102+
103+
it('Delete consumer', async () =>
104+
syncEvents(backend, [
105+
deleteEvent(ADCSDK.ResourceType.CONSUMER, consumer1Name),
106+
]));
107+
108+
it('Dump again (consumer should not exist)', async () => {
109+
const result = (await dumpConfiguration(
110+
backend,
111+
)) as ADCSDK.Configuration;
112+
expect(result.consumers).toHaveLength(0);
113+
});
114+
},
115+
);
116+
});

libs/backend-apisix/e2e/support/utils.ts

+24-2
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import * as ADCSDK from '@api7/adc-sdk';
22
import { Listr, SilentRenderer } from 'listr2';
3+
import semver from 'semver';
34

45
import { BackendAPISIX } from '../../src';
56

@@ -49,7 +50,11 @@ export const createEvent = (
4950
parentName ? `${parentName}.${resourceName}` : resourceName,
5051
),
5152
newValue: resource,
52-
parentId: parentName ? ADCSDK.utils.generateId(parentName) : undefined,
53+
parentId: parentName
54+
? resourceType === ADCSDK.ResourceType.CONSUMER_CREDENTIAL
55+
? parentName
56+
: ADCSDK.utils.generateId(parentName)
57+
: undefined,
5358
});
5459

5560
export const updateEvent = (
@@ -79,5 +84,22 @@ export const deleteEvent = (
7984
: ADCSDK.utils.generateId(
8085
parentName ? `${parentName}.${resourceName}` : resourceName,
8186
),
82-
parentId: parentName ? ADCSDK.utils.generateId(parentName) : undefined,
87+
parentId: parentName
88+
? resourceType === ADCSDK.ResourceType.CONSUMER_CREDENTIAL
89+
? parentName
90+
: ADCSDK.utils.generateId(parentName)
91+
: undefined,
8392
});
93+
94+
type cond = boolean | (() => boolean);
95+
96+
export const conditionalDescribe = (cond: cond) =>
97+
cond ? describe : describe.skip;
98+
99+
export const conditionalIt = (cond: cond) => (cond ? it : it.skip);
100+
101+
export const semverCondition = (
102+
op: (v1: string | semver.SemVer, v2: string | semver.SemVer) => boolean,
103+
base: string,
104+
target = semver.coerce(process.env.BACKEND_APISIX_VERSION) ?? '0.0.0',
105+
) => op(target, base);

libs/backend-apisix/e2e/sync-and-dump-1.e2e-spec.ts

-1
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,6 @@ describe('Sync and Dump - 1', () => {
2121
server,
2222
token,
2323
tlsSkipVerify: true,
24-
gatewayGroup: 'default',
2524
});
2625
});
2726

libs/backend-apisix/src/fetcher.ts

+32-1
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,16 @@
11
import * as ADCSDK from '@api7/adc-sdk';
22
import { Axios } from 'axios';
33
import { ListrTask } from 'listr2';
4+
import { SemVer, gte as semVerGTE } from 'semver';
45

56
import { ToADC } from './transformer';
67
import * as typing from './typing';
78
import { buildReqAndRespDebugOutput, resourceTypeToAPIName } from './utils';
89

910
type FetchTask = ListrTask<{
1011
remote: ADCSDK.Configuration;
12+
13+
apisixVersion: SemVer;
1114
apisixResources?: typing.Resources;
1215
}>;
1316

@@ -65,7 +68,6 @@ export class Fetcher {
6568
)
6669
return;
6770

68-
// resourceType === ADCSDK.ResourceType.GLOBAL_RULE ||
6971
if (resourceType === ADCSDK.ResourceType.PLUGIN_METADATA) {
7072
ctx.apisixResources[ADCSDK.ResourceType.PLUGIN_METADATA] =
7173
Object.fromEntries(
@@ -92,6 +94,35 @@ export class Fetcher {
9294
(item) => item.value,
9395
);
9496
}
97+
98+
if (
99+
resourceType === ADCSDK.ResourceType.CONSUMER &&
100+
semVerGTE(ctx.apisixVersion, '3.11.0')
101+
) {
102+
await Promise.all(
103+
ctx.apisixResources[resourceType].map(async (item) => {
104+
const resp = await this.client.get<{
105+
list: Array<{
106+
key: string;
107+
value: typing.ConsumerCredential;
108+
createdIndex: number;
109+
modifiedIndex: number;
110+
}>;
111+
total: number;
112+
}>(`/apisix/admin/consumers/${item.username}/credentials`, {
113+
validateStatus: () => true,
114+
});
115+
task.output = buildReqAndRespDebugOutput(
116+
resp,
117+
`Get credentials of consumer "${item.username}"`,
118+
);
119+
if (resp.status === 200)
120+
item.credentials = resp.data.list.map(
121+
(credential) => credential.value,
122+
);
123+
}),
124+
);
125+
}
95126
},
96127
}),
97128
);

libs/backend-apisix/src/index.ts

+26-1
Original file line numberDiff line numberDiff line change
@@ -3,9 +3,11 @@ import axios, { Axios, CreateAxiosDefaults } from 'axios';
33
import { Listr, ListrTask } from 'listr2';
44
import { readFileSync } from 'node:fs';
55
import { AgentOptions, Agent as httpsAgent } from 'node:https';
6+
import semver from 'semver';
67

78
import { Fetcher } from './fetcher';
89
import { Operator } from './operator';
10+
import { buildReqAndRespDebugOutput } from './utils';
911

1012
export class BackendAPISIX implements ADCSDK.Backend {
1113
private readonly client: Axios;
@@ -45,14 +47,36 @@ export class BackendAPISIX implements ADCSDK.Backend {
4547
await this.client.get(`/apisix/admin/routes`);
4648
}
4749

50+
private getAPISIXVersionTask(): ListrTask {
51+
return {
52+
enabled: (ctx) => !ctx.apisixVersion,
53+
task: async (ctx, task) => {
54+
const resp = await this.client.get<{ value: string }>(
55+
'/apisix/admin/routes',
56+
);
57+
task.output = buildReqAndRespDebugOutput(resp, `Get APISIX version`);
58+
59+
ctx.apisixVersion = semver.coerce('0.0.0');
60+
if (resp.headers.server) {
61+
const version = (resp.headers.server as string).match(/APISIX\/(.*)/);
62+
if (version) ctx.apisixVersion = semver.coerce(version[1]);
63+
}
64+
},
65+
};
66+
}
67+
4868
public getResourceDefaultValueTask(): Array<ListrTask> {
4969
return [];
5070
}
5171

5272
public async dump(): Promise<Listr<{ remote: ADCSDK.Configuration }>> {
5373
const fetcher = new Fetcher(this.client);
5474
return new Listr(
55-
[...this.getResourceDefaultValueTask(), ...fetcher.fetch()],
75+
[
76+
this.getAPISIXVersionTask(),
77+
...this.getResourceDefaultValueTask(),
78+
...fetcher.fetch(),
79+
],
5680
{
5781
rendererOptions: { scope: BackendAPISIX.logScope },
5882
},
@@ -63,6 +87,7 @@ export class BackendAPISIX implements ADCSDK.Backend {
6387
const operator = new Operator(this.client);
6488
return new Listr(
6589
[
90+
this.getAPISIXVersionTask(),
6691
...this.getResourceDefaultValueTask(),
6792
{
6893
task: (ctx, task) =>

0 commit comments

Comments
 (0)