Skip to content

Commit

Permalink
feat(apisix): support apisix lower than 3.8 (#213)
Browse files Browse the repository at this point in the history
  • Loading branch information
bzp2010 authored Nov 29, 2024
1 parent b39249f commit df39a26
Show file tree
Hide file tree
Showing 5 changed files with 167 additions and 102 deletions.
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

0 comments on commit df39a26

Please sign in to comment.