Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(apisix): support apisix lower than 3.8 #213

Merged
merged 9 commits into from
Nov 29, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 11 additions & 1 deletion .github/workflows/e2e.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,17 @@ jobs:
runs-on: ubuntu-latest
strategy:
matrix:
version: [3.8.1, 3.9.1, 3.10.0, 3.11.0]
version:
- 3.2.2
- 3.3.0
- 3.4.0
- 3.5.0
- 3.6.0
- 3.7.0
- 3.8.1
- 3.9.1
- 3.10.0
- 3.11.0
env:
BACKEND_APISIX_VERSION: ${{ matrix.version }}
BACKEND_APISIX_IMAGE: ${{ matrix.version }}-debian
Expand Down
24 changes: 22 additions & 2 deletions libs/backend-apisix/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,28 @@

| Features | Supported |
| ------------- | --------- |
| Dump to ADC | ✅ |
| Sync from ADC | ✅ |
| Dump to ADC | ✅ |
| Sync from ADC | ✅ |

## Supported Versions

> Versions not listed below are untested.

| Versions | Supported | Status |
| -------- | --------- | ----------- |
| 3.2.x | ✅ | Partial [1] |
| 3.3.x | ✅ | Partial [1] |
| 3.4.x | ✅ | Partial [1] |
| 3.5.x | ✅ | Partial [1] |
| 3.6.x | ✅ | Partial [1] |
| 3.7.x | ✅ | Partial [2] |
| 3.8.x | ✅ | Full |
| 3.9.x | ✅ | Full |
| 3.10.x | ✅ | Full |
| 3.11.x | ✅ | Full |

1. The stream routes will be skipped during synchronization because they cannot be associated to the service on these versions.
2. The `name` field is lost when the stream route is dumped and synchronized, because it is not defined in the APISIX schema.

## Known Issues/Limitations

Expand Down
156 changes: 94 additions & 62 deletions libs/backend-apisix/e2e/sync-and-dump-1.e2e-spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,17 @@ import * as ADCSDK from '@api7/adc-sdk';
import { unset } from 'lodash';
import { readFileSync } from 'node:fs';
import { join } from 'node:path';
import { gte, lt } from 'semver';

import { BackendAPISIX } from '../src';
import { server, token } from './support/constants';
import {
conditionalDescribe,
conditionalIt,
createEvent,
deleteEvent,
dumpConfiguration,
semverCondition,
syncEvents,
updateEvent,
} from './support/utils';
Expand Down Expand Up @@ -163,68 +167,96 @@ describe('Sync and Dump - 1', () => {
});
});

describe('Sync and dump service with stream route', () => {
const serviceName = 'test';
const service = {
name: serviceName,
upstream: {
scheme: 'tcp',
nodes: [
{
host: '1.1.1.1',
port: 5432,
weight: 100,
},
],
},
} as ADCSDK.Service;
const route1Name = 'postgres';
const route1 = {
name: route1Name,
server_port: 54320,
} as ADCSDK.StreamRoute;

it('Create resources', async () =>
syncEvents(backend, [
createEvent(ADCSDK.ResourceType.SERVICE, serviceName, service),
createEvent(
ADCSDK.ResourceType.STREAM_ROUTE,
route1Name,
route1,
serviceName,
),
]));

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].stream_routes).toHaveLength(1);
expect(result.services[0].stream_routes[0]).toMatchObject(route1);
});

it('Delete stream route', async () =>
syncEvents(backend, [
deleteEvent(ADCSDK.ResourceType.STREAM_ROUTE, route1Name, serviceName),
]));

it('Dump again (non-route)', async () => {
const result = (await dumpConfiguration(backend)) as ADCSDK.Configuration;
expect(result.services).toHaveLength(1);
expect(result.services[0]).toMatchObject(service);
expect(result.services[0].stream_routes).toBeUndefined();
});

it('Delete service', async () =>
syncEvents(backend, [
deleteEvent(ADCSDK.ResourceType.SERVICE, serviceName),
]));

it('Dump again (service should not exist)', async () => {
const result = (await dumpConfiguration(backend)) as ADCSDK.Configuration;
expect(result.services).toHaveLength(0);
});
});
conditionalDescribe(semverCondition(gte, '3.7.0'))(
'Sync and dump service with stream route',
() => {
const serviceName = 'test';
const service = {
name: serviceName,
upstream: {
scheme: 'tcp',
nodes: [
{
host: '1.1.1.1',
port: 5432,
weight: 100,
},
],
},
} as ADCSDK.Service;
const route1Name = 'postgres';
const route1 = {
name: route1Name,
server_port: 54320,
} as ADCSDK.StreamRoute;

it('Create resources', async () =>
syncEvents(backend, [
createEvent(ADCSDK.ResourceType.SERVICE, serviceName, service),
createEvent(
ADCSDK.ResourceType.STREAM_ROUTE,
route1Name,
route1,
serviceName,
),
]));

conditionalIt(semverCondition(lt, '3.8.0'))('Dump (<3.8.0)', async () => {
const result = (await dumpConfiguration(
backend,
)) as ADCSDK.Configuration;
expect(result.services).toHaveLength(1);
expect(result.services[0]).toMatchObject(service);
expect(result.services[0].stream_routes).toHaveLength(1);
const route = structuredClone(route1);
route.name = result.services[0].stream_routes[0].id;
expect(result.services[0].stream_routes[0]).toMatchObject(route);
});

conditionalIt(semverCondition(gte, '3.8.0'))(
'Dump (>=3.8.0)',
async () => {
const result = (await dumpConfiguration(
backend,
)) as ADCSDK.Configuration;
expect(result.services).toHaveLength(1);
expect(result.services[0]).toMatchObject(service);
expect(result.services[0].stream_routes).toHaveLength(1);
expect(result.services[0].stream_routes[0]).toMatchObject(route1);
},
);

it('Delete stream route', async () =>
syncEvents(backend, [
deleteEvent(
ADCSDK.ResourceType.STREAM_ROUTE,
route1Name,
serviceName,
),
]));

it('Dump again (non-route)', async () => {
const result = (await dumpConfiguration(
backend,
)) as ADCSDK.Configuration;
expect(result.services).toHaveLength(1);
expect(result.services[0]).toMatchObject(service);
expect(result.services[0].stream_routes).toBeUndefined();
});

it('Delete service', async () =>
syncEvents(backend, [
deleteEvent(ADCSDK.ResourceType.SERVICE, serviceName),
]));

it('Dump again (service should not exist)', async () => {
const result = (await dumpConfiguration(
backend,
)) as ADCSDK.Configuration;
expect(result.services).toHaveLength(0);
});
},
);

describe('Sync and dump consumers', () => {
const consumer1Name = 'consumer1';
Expand Down
24 changes: 18 additions & 6 deletions libs/backend-apisix/src/operator.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import * as ADCSDK from '@api7/adc-sdk';
import { Axios } from 'axios';
import { ListrTask } from 'listr2';
import { SemVer, lt as semVerLT } from 'semver';
import { SemVer, lt, gte as semVerGTE, lt as semVerLT } from 'semver';

import { FromADC } from './transformer';
import * as typing from './typing';
Expand All @@ -26,13 +26,20 @@ export class Operator {
public updateResource(event: ADCSDK.Event): OperateTask {
return {
title: this.generateTaskName(event),
skip: (ctx) => {
if (
lt(ctx.apisixVersion, '3.7.0') &&
event.resourceType === ADCSDK.ResourceType.STREAM_ROUTE
)
return 'The stream routes on versions below 3.7.0 are not supported as they are not supported configured on the service.';
},
task: async (ctx, task) => {
if (event.resourceType === ADCSDK.ResourceType.CONSUMER_CREDENTIAL) {
if (semVerLT(ctx.apisixVersion, '3.11.0')) return;

const resp = await this.client.put(
`/apisix/admin/consumers/${event.parentId}/credentials/${event.resourceId}`,
this.fromADC(event),
this.fromADC(event, ctx.apisixVersion),
{
validateStatus: () => true,
},
Expand All @@ -43,7 +50,7 @@ export class Operator {
} else {
const resp = await this.client.put(
`/apisix/admin/${resourceTypeToAPIName(event.resourceType)}/${event.resourceId}`,
this.fromADC(event),
this.fromADC(event, ctx.apisixVersion),
{
validateStatus: () => true,
},
Expand Down Expand Up @@ -102,7 +109,7 @@ export class Operator {
)} ${event.resourceType}: "${event.resourceName}"`;
}

private fromADC(event: ADCSDK.Event) {
private fromADC(event: ADCSDK.Event, version: SemVer) {
const fromADC = new FromADC();
switch (event.resourceType) {
case ADCSDK.ResourceType.CONSUMER:
Expand All @@ -127,20 +134,25 @@ export class Operator {
return event.newValue;
case ADCSDK.ResourceType.ROUTE: {
(event.newValue as ADCSDK.Route).id = event.resourceId;
const route = fromADC.transformRoute(event.newValue as ADCSDK.Route);
const route = fromADC.transformRoute(
event.newValue as ADCSDK.Route,
event.parentId,
);
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];
return fromADC.transformService(event.newValue as ADCSDK.Service);
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,
event.parentId,
semVerGTE(version, '3.8.0'),
);
if (event.parentId) route.service_id = event.parentId;
return route;
Expand Down
53 changes: 22 additions & 31 deletions libs/backend-apisix/src/transformer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -242,7 +242,7 @@ export class FromADC {
}, {});
}

public transformRoute(route: ADCSDK.Route): typing.Route {
public transformRoute(route: ADCSDK.Route, parentId: string): typing.Route {
return ADCSDK.utils.recursiveOmitUndefined<typing.Route>({
id: route.id,
name: route.name,
Expand All @@ -255,7 +255,7 @@ export class FromADC {
vars: route.vars,
filter_func: route.filter_func,

//service_id: '',
service_id: parentId,
enable_websocket: route.enable_websocket,
plugins: route.plugins,
priority: route.priority,
Expand All @@ -264,31 +264,16 @@ export class FromADC {
});
}

public transformService(
service: ADCSDK.Service,
): [typing.Service, Array<typing.Route>, Array<typing.StreamRoute>] {
const serviceId = ADCSDK.utils.generateId(service.name);
const routes: Array<typing.Route> =
service.routes
?.map(this.transformRoute)
.map((route) => ({ ...route, service_id: serviceId })) ?? [];
const streamRoutes: Array<typing.StreamRoute> =
service.stream_routes
?.map(this.transformStreamRoute)
.map((route) => ({ ...route, service_id: serviceId })) ?? [];
return [
ADCSDK.utils.recursiveOmitUndefined<typing.Service>({
id: service.id,
name: service.name,
desc: service.description,
labels: FromADC.transformLabels(service.labels),
upstream: service.upstream,
plugins: service.plugins,
hosts: service.hosts,
}),
routes,
streamRoutes,
];
public transformService(service: ADCSDK.Service): typing.Service {
return ADCSDK.utils.recursiveOmitUndefined<typing.Service>({
id: service.id,
name: service.name,
desc: service.description,
labels: FromADC.transformLabels(service.labels),
upstream: service.upstream,
plugins: service.plugins,
hosts: service.hosts,
});
}

public transformConsumer(consumer: ADCSDK.Consumer): typing.Consumer {
Expand Down Expand Up @@ -400,19 +385,25 @@ export class FromADC {

public transformStreamRoute(
streamRoute: ADCSDK.StreamRoute,
parentId: string,
injectName = true,
): typing.StreamRoute {
const labels = FromADC.transformLabels(streamRoute.labels);
return ADCSDK.utils.recursiveOmitUndefined({
id: undefined,
desc: streamRoute.description,
labels: {
...FromADC.transformLabels(streamRoute.labels),
__ADC_NAME: streamRoute.name,
},
labels: injectName
? {
...labels,
__ADC_NAME: streamRoute.name,
}
: labels,
plugins: streamRoute.plugins,
remote_addr: streamRoute.remote_addr,
server_addr: streamRoute.server_addr,
server_port: streamRoute.server_port,
sni: streamRoute.sni,
service_id: parentId,
} as typing.StreamRoute);
}

Expand Down