From d81071b18b7e0e349d72ed421de8cbf8424c8976 Mon Sep 17 00:00:00 2001 From: Jarvis Date: Mon, 20 Apr 2026 10:38:15 +0800 Subject: [PATCH 1/4] feat(apisix,apisix-standalone): add validate command support Add server-side configuration validation for both APISIX and APISIX Standalone backends, reusing the same /apisix/admin/configs/validate endpoint. - Create shared Validator class in backend-apisix that handles transforming ADC events to APISIX native format and calling the validate API - backend-apisix-standalone imports and reuses the Validator with custom request config (baseURL + auth header) - Handle APISIX-specific transformService tuple return which produces separate upstream resources - Add e2e test suites for both backends --- .../e2e/validate.e2e-spec.ts | 214 ++++++++++++++++ libs/backend-apisix-standalone/package.json | 1 + libs/backend-apisix-standalone/src/index.ts | 16 ++ libs/backend-apisix/e2e/validate.e2e-spec.ts | 236 ++++++++++++++++++ libs/backend-apisix/src/index.ts | 12 + libs/backend-apisix/src/validator.ts | 181 ++++++++++++++ pnpm-lock.yaml | 3 + 7 files changed, 663 insertions(+) create mode 100644 libs/backend-apisix-standalone/e2e/validate.e2e-spec.ts create mode 100644 libs/backend-apisix/e2e/validate.e2e-spec.ts create mode 100644 libs/backend-apisix/src/validator.ts diff --git a/libs/backend-apisix-standalone/e2e/validate.e2e-spec.ts b/libs/backend-apisix-standalone/e2e/validate.e2e-spec.ts new file mode 100644 index 00000000..3d691ecc --- /dev/null +++ b/libs/backend-apisix-standalone/e2e/validate.e2e-spec.ts @@ -0,0 +1,214 @@ +import { DifferV3 } from '@api7/adc-differ'; +import * as ADCSDK from '@api7/adc-sdk'; +import { lastValueFrom } from 'rxjs'; + +import { BackendAPISIXStandalone } from '../src'; +import { + defaultBackendOptions, + server1, + token1, +} from './support/constants'; + +const configToEvents = (config: ADCSDK.Configuration): Array => { + return DifferV3.diff( + config as ADCSDK.InternalConfiguration, + {} as ADCSDK.InternalConfiguration, + ); +}; + +describe('Validate', () => { + let backend: BackendAPISIXStandalone; + + beforeAll(() => { + backend = new BackendAPISIXStandalone({ + server: server1, + token: token1, + cacheKey: 'validate-test', + ...defaultBackendOptions, + }); + }); + + it('should succeed with empty configuration', async () => { + const result = await lastValueFrom(backend.validate([])); + expect(result.success).toBe(true); + expect(result.errors).toEqual([]); + }); + + it('should succeed with valid service and route', async () => { + const config: ADCSDK.Configuration = { + services: [ + { + name: 'validate-test-svc', + upstream: { + scheme: 'http', + nodes: [{ host: 'httpbin.org', port: 80, weight: 100 }], + }, + routes: [ + { + name: 'validate-test-route', + uris: ['/validate-test'], + methods: ['GET'], + }, + ], + }, + ], + }; + + const result = await lastValueFrom( + backend.validate(configToEvents(config)), + ); + expect(result.success).toBe(true); + expect(result.errors).toEqual([]); + }); + + it('should succeed with valid consumer', async () => { + const config: ADCSDK.Configuration = { + consumers: [ + { + username: 'validate-test-consumer', + plugins: { + 'key-auth': { key: 'test-key-123' }, + }, + }, + ], + }; + + const result = await lastValueFrom( + backend.validate(configToEvents(config)), + ); + expect(result.success).toBe(true); + expect(result.errors).toEqual([]); + }); + + it('should fail with invalid plugin configuration', async () => { + const config: ADCSDK.Configuration = { + services: [ + { + name: 'validate-bad-plugin-svc', + upstream: { + scheme: 'http', + nodes: [{ host: 'httpbin.org', port: 80, weight: 100 }], + }, + routes: [ + { + name: 'validate-bad-plugin-route', + uris: ['/bad-plugin'], + plugins: { + 'limit-count': { + // missing required fields: count, time_window + }, + }, + }, + ], + }, + ], + }; + + const result = await lastValueFrom( + backend.validate(configToEvents(config)), + ); + expect(result.success).toBe(false); + expect(result.errors.length).toBeGreaterThan(0); + expect(result.errors[0].resource_type).toBe('routes'); + }); + + it('should fail with invalid route (bad uri type)', async () => { + const config: ADCSDK.Configuration = { + services: [ + { + name: 'validate-bad-route-svc', + upstream: { + scheme: 'http', + nodes: [{ host: 'httpbin.org', port: 80, weight: 100 }], + }, + routes: [ + { + name: 'validate-bad-route', + uris: [123 as unknown as string], + }, + ], + }, + ], + }; + + const result = await lastValueFrom( + backend.validate(configToEvents(config)), + ); + expect(result.success).toBe(false); + expect(result.errors.length).toBeGreaterThan(0); + }); + + it('should collect multiple errors', async () => { + const config: ADCSDK.Configuration = { + services: [ + { + name: 'validate-multi-err-svc', + upstream: { + scheme: 'http', + nodes: [{ host: 'httpbin.org', port: 80, weight: 100 }], + }, + routes: [ + { + name: 'validate-multi-err-route1', + uris: ['/multi-err-1'], + plugins: { + 'limit-count': {}, + }, + }, + { + name: 'validate-multi-err-route2', + uris: ['/multi-err-2'], + plugins: { + 'limit-count': {}, + }, + }, + ], + }, + ], + }; + + const result = await lastValueFrom( + backend.validate(configToEvents(config)), + ); + expect(result.success).toBe(false); + expect(result.errors.length).toBeGreaterThanOrEqual(2); + }); + + it('should succeed with mixed resource types', async () => { + const config: ADCSDK.Configuration = { + services: [ + { + name: 'validate-mixed-svc', + upstream: { + scheme: 'https', + nodes: [{ host: 'httpbin.org', port: 443, weight: 100 }], + }, + routes: [ + { + name: 'validate-mixed-route', + uris: ['/mixed-test'], + methods: ['GET', 'POST'], + }, + ], + }, + ], + consumers: [ + { + username: 'validate-mixed-consumer', + plugins: { + 'key-auth': { key: 'mixed-key-456' }, + }, + }, + ], + global_rules: { + prometheus: { prefer_name: false }, + } as ADCSDK.Configuration['global_rules'], + }; + + const result = await lastValueFrom( + backend.validate(configToEvents(config)), + ); + expect(result.success).toBe(true); + expect(result.errors).toEqual([]); + }); +}); diff --git a/libs/backend-apisix-standalone/package.json b/libs/backend-apisix-standalone/package.json index 19bb1996..10e11e94 100644 --- a/libs/backend-apisix-standalone/package.json +++ b/libs/backend-apisix-standalone/package.json @@ -21,6 +21,7 @@ "vitest": "catalog:" }, "dependencies": { + "@api7/adc-backend-apisix": "workspace:*", "@api7/adc-sdk": "workspace:*", "axios": "catalog:", "rxjs": "catalog:", diff --git a/libs/backend-apisix-standalone/src/index.ts b/libs/backend-apisix-standalone/src/index.ts index f7abcecd..56fef9a4 100644 --- a/libs/backend-apisix-standalone/src/index.ts +++ b/libs/backend-apisix-standalone/src/index.ts @@ -1,4 +1,5 @@ import * as ADCSDK from '@api7/adc-sdk'; +import { Validator } from '@api7/adc-backend-apisix'; import axios, { type AxiosInstance } from 'axios'; import { type Observable, Subject, from, map, of, switchMap } from 'rxjs'; import semver, { SemVer, eq as semverEQ } from 'semver'; @@ -166,6 +167,21 @@ export class BackendAPISIXStandalone implements ADCSDK.Backend { }); } + public validate(events: Array) { + const server = this.serverTokenMap.keys().next().value as string; + const token = this.serverTokenMap.get(server)!; + return from( + new Validator({ + client: this.client, + eventSubject: this.subject, + requestConfig: { + baseURL: server, + headers: { 'X-API-KEY': token }, + }, + }).validate(events), + ); + } + supportStreamRoute?: () => Promise; public __TEST_ONLY = { diff --git a/libs/backend-apisix/e2e/validate.e2e-spec.ts b/libs/backend-apisix/e2e/validate.e2e-spec.ts new file mode 100644 index 00000000..b219b633 --- /dev/null +++ b/libs/backend-apisix/e2e/validate.e2e-spec.ts @@ -0,0 +1,236 @@ +import { DifferV3 } from '@api7/adc-differ'; +import * as ADCSDK from '@api7/adc-sdk'; +import { lastValueFrom } from 'rxjs'; + +import { BackendAPISIX } from '../src'; +import { defaultBackendOptions } from './support/constants'; + +const configToEvents = (config: ADCSDK.Configuration): Array => { + return DifferV3.diff( + config as ADCSDK.InternalConfiguration, + {} as ADCSDK.InternalConfiguration, + ); +}; + +describe('Validate', () => { + let backend: BackendAPISIX; + + beforeAll(() => { + backend = new BackendAPISIX(defaultBackendOptions); + }); + + it('should succeed with empty configuration', async () => { + const result = await lastValueFrom(backend.validate([])); + expect(result.success).toBe(true); + expect(result.errors).toEqual([]); + }); + + it('should succeed with valid service and route', async () => { + const config: ADCSDK.Configuration = { + services: [ + { + name: 'validate-test-svc', + upstream: { + scheme: 'http', + nodes: [{ host: 'httpbin.org', port: 80, weight: 100 }], + }, + routes: [ + { + name: 'validate-test-route', + uris: ['/validate-test'], + methods: ['GET'], + }, + ], + }, + ], + }; + + const result = await lastValueFrom( + backend.validate(configToEvents(config)), + ); + expect(result.success).toBe(true); + expect(result.errors).toEqual([]); + }); + + it('should succeed with valid consumer', async () => { + const config: ADCSDK.Configuration = { + consumers: [ + { + username: 'validate-test-consumer', + plugins: { + 'key-auth': { key: 'test-key-123' }, + }, + }, + ], + }; + + const result = await lastValueFrom( + backend.validate(configToEvents(config)), + ); + expect(result.success).toBe(true); + expect(result.errors).toEqual([]); + }); + + it('should fail with invalid plugin configuration', async () => { + const config: ADCSDK.Configuration = { + services: [ + { + name: 'validate-bad-plugin-svc', + upstream: { + scheme: 'http', + nodes: [{ host: 'httpbin.org', port: 80, weight: 100 }], + }, + routes: [ + { + name: 'validate-bad-plugin-route', + uris: ['/bad-plugin'], + plugins: { + 'limit-count': { + // missing required fields: count, time_window + }, + }, + }, + ], + }, + ], + }; + + const result = await lastValueFrom( + backend.validate(configToEvents(config)), + ); + expect(result.success).toBe(false); + expect(result.errors.length).toBeGreaterThan(0); + expect(result.errors[0].resource_type).toBe('routes'); + }); + + it('should fail with invalid route (bad uri type)', async () => { + const config: ADCSDK.Configuration = { + services: [ + { + name: 'validate-bad-route-svc', + upstream: { + scheme: 'http', + nodes: [{ host: 'httpbin.org', port: 80, weight: 100 }], + }, + routes: [ + { + name: 'validate-bad-route', + uris: [123 as unknown as string], + }, + ], + }, + ], + }; + + const result = await lastValueFrom( + backend.validate(configToEvents(config)), + ); + expect(result.success).toBe(false); + expect(result.errors.length).toBeGreaterThan(0); + }); + + it('should collect multiple errors', async () => { + const config: ADCSDK.Configuration = { + services: [ + { + name: 'validate-multi-err-svc', + upstream: { + scheme: 'http', + nodes: [{ host: 'httpbin.org', port: 80, weight: 100 }], + }, + routes: [ + { + name: 'validate-multi-err-route1', + uris: ['/multi-err-1'], + plugins: { + 'limit-count': {}, + }, + }, + { + name: 'validate-multi-err-route2', + uris: ['/multi-err-2'], + plugins: { + 'limit-count': {}, + }, + }, + ], + }, + ], + }; + + const result = await lastValueFrom( + backend.validate(configToEvents(config)), + ); + expect(result.success).toBe(false); + expect(result.errors.length).toBeGreaterThanOrEqual(2); + }); + + it('should succeed with mixed resource types', async () => { + const config: ADCSDK.Configuration = { + services: [ + { + name: 'validate-mixed-svc', + upstream: { + scheme: 'https', + nodes: [{ host: 'httpbin.org', port: 443, weight: 100 }], + }, + routes: [ + { + name: 'validate-mixed-route', + uris: ['/mixed-test'], + methods: ['GET', 'POST'], + }, + ], + }, + ], + consumers: [ + { + username: 'validate-mixed-consumer', + plugins: { + 'key-auth': { key: 'mixed-key-456' }, + }, + }, + ], + global_rules: { + prometheus: { prefer_name: false }, + } as ADCSDK.Configuration['global_rules'], + }; + + const result = await lastValueFrom( + backend.validate(configToEvents(config)), + ); + expect(result.success).toBe(true); + expect(result.errors).toEqual([]); + }); + + it('should be a dry-run (no side effects on server)', async () => { + const serviceName = 'validate-dryrun-svc'; + + const config: ADCSDK.Configuration = { + services: [ + { + name: serviceName, + upstream: { + scheme: 'http', + nodes: [{ host: 'httpbin.org', port: 80, weight: 100 }], + }, + routes: [ + { + name: 'validate-dryrun-route', + uris: ['/dryrun-test'], + }, + ], + }, + ], + }; + + const result = await lastValueFrom( + backend.validate(configToEvents(config)), + ); + expect(result.success).toBe(true); + + const dumped = await lastValueFrom(backend.dump()); + const found = dumped.services?.find((s) => s.name === serviceName); + expect(found).toBeUndefined(); + }); +}); diff --git a/libs/backend-apisix/src/index.ts b/libs/backend-apisix/src/index.ts index 2b4f3473..ba524e68 100644 --- a/libs/backend-apisix/src/index.ts +++ b/libs/backend-apisix/src/index.ts @@ -5,6 +5,9 @@ import semver, { SemVer } from 'semver'; import { Fetcher } from './fetcher'; import { Operator } from './operator'; +import { Validator } from './validator'; + +export { Validator } from './validator'; export class BackendAPISIX implements ADCSDK.Backend { private static logScope = ['APISIX']; @@ -102,5 +105,14 @@ export class BackendAPISIX implements ADCSDK.Backend { }); } + public validate(events: Array) { + return from( + new Validator({ + client: this.client, + eventSubject: this.subject, + }).validate(events), + ); + } + supportStreamRoute?: () => Promise; } diff --git a/libs/backend-apisix/src/validator.ts b/libs/backend-apisix/src/validator.ts new file mode 100644 index 00000000..a0288e24 --- /dev/null +++ b/libs/backend-apisix/src/validator.ts @@ -0,0 +1,181 @@ +import * as ADCSDK from '@api7/adc-sdk'; +import axios, { type AxiosInstance, type AxiosRequestConfig } from 'axios'; +import { Subject } from 'rxjs'; + +import { FromADC } from './transformer'; +import * as typing from './typing'; + +export interface ValidatorOptions { + client: AxiosInstance; + eventSubject: Subject; + requestConfig?: AxiosRequestConfig; +} + +interface ValidateRequestBody { + routes: Array; + services: Array; + consumers: Array; + ssls: Array; + global_rules: Array; + stream_routes: Array; + plugin_metadata: Array>; + upstreams: Array; +} + +export class Validator extends ADCSDK.backend.BackendEventSource { + private readonly client: AxiosInstance; + private readonly fromADC = new FromADC(); + + constructor(private readonly opts: ValidatorOptions) { + super(); + this.client = opts.client; + this.subject = opts.eventSubject; + } + + public async validate( + events: Array, + ): Promise { + const { body, nameIndex } = this.buildRequestBody(events); + + try { + const resp = await this.client.post( + '/apisix/admin/configs/validate', + body, + this.opts.requestConfig, + ); + this.subject.next({ + type: ADCSDK.BackendEventType.AXIOS_DEBUG, + event: { response: resp, description: 'Validate configuration' }, + }); + return { success: true, errors: [] }; + } catch (error) { + if (axios.isAxiosError(error) && error.response?.status === 400) { + this.subject.next({ + type: ADCSDK.BackendEventType.AXIOS_DEBUG, + event: { + response: error.response, + description: 'Validate configuration (failed)', + }, + }); + const data = error.response.data; + const errors: ADCSDK.BackendValidationError[] = ( + data?.errors ?? [] + ).map((e: ADCSDK.BackendValidationError) => { + const name = nameIndex[e.resource_type]?.[e.index]; + return name ? { ...e, resource_name: name } : e; + }); + return { + success: false, + errorMessage: data?.error_msg, + errors, + }; + } + throw error; + } + } + + private buildRequestBody(events: Array): { + body: ValidateRequestBody; + nameIndex: Record; + } { + const body: ValidateRequestBody = { + routes: [], + services: [], + consumers: [], + ssls: [], + global_rules: [], + stream_routes: [], + plugin_metadata: [], + upstreams: [], + }; + const nameIndex: Record = { + routes: [], + services: [], + consumers: [], + ssls: [], + global_rules: [], + stream_routes: [], + plugin_metadata: [], + upstreams: [], + }; + + const flat = events.filter( + (e) => + e.type === ADCSDK.EventType.CREATE || + e.type === ADCSDK.EventType.UPDATE, + ); + + for (const event of flat) { + switch (event.resourceType) { + case ADCSDK.ResourceType.SERVICE: { + (event.newValue as ADCSDK.Service).id = event.resourceId; + const [service, upstream] = this.fromADC.transformService( + event.newValue as ADCSDK.Service, + ); + body.services.push(service); + nameIndex.services.push(event.resourceName); + if (upstream) { + body.upstreams.push(upstream); + nameIndex.upstreams.push(event.resourceName); + } + break; + } + case ADCSDK.ResourceType.ROUTE: { + (event.newValue as ADCSDK.Route).id = event.resourceId; + body.routes.push( + this.fromADC.transformRoute( + event.newValue as ADCSDK.Route, + event.parentId!, + ), + ); + nameIndex.routes.push(event.resourceName); + break; + } + case ADCSDK.ResourceType.STREAM_ROUTE: { + (event.newValue as ADCSDK.StreamRoute).id = event.resourceId; + body.stream_routes.push( + this.fromADC.transformStreamRoute( + event.newValue as ADCSDK.StreamRoute, + event.parentId!, + ), + ); + nameIndex.stream_routes.push(event.resourceName); + break; + } + case ADCSDK.ResourceType.CONSUMER: { + body.consumers.push( + this.fromADC.transformConsumer(event.newValue as ADCSDK.Consumer), + ); + nameIndex.consumers.push(event.resourceName); + break; + } + case ADCSDK.ResourceType.SSL: { + (event.newValue as ADCSDK.SSL).id = event.resourceId; + body.ssls.push( + this.fromADC.transformSSL(event.newValue as ADCSDK.SSL), + ); + nameIndex.ssls.push(event.resourceName); + break; + } + case ADCSDK.ResourceType.GLOBAL_RULE: { + body.global_rules.push({ + plugins: { [event.resourceId]: event.newValue }, + } as unknown as typing.GlobalRule); + nameIndex.global_rules.push(event.resourceName); + break; + } + case ADCSDK.ResourceType.PLUGIN_METADATA: { + body.plugin_metadata.push({ + id: event.resourceId, + ...ADCSDK.utils.recursiveOmitUndefined( + event.newValue as Record, + ), + }); + nameIndex.plugin_metadata.push(event.resourceName); + break; + } + } + } + return { body, nameIndex }; + } +} diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 972f8b13..f0596961 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -329,6 +329,9 @@ importers: libs/backend-apisix-standalone: dependencies: + '@api7/adc-backend-apisix': + specifier: workspace:* + version: link:../backend-apisix '@api7/adc-sdk': specifier: workspace:* version: link:../sdk From a07e2cf887c620457a74e78d8a3c0a6e63d12387 Mon Sep 17 00:00:00 2001 From: Jarvis Date: Mon, 20 Apr 2026 10:50:01 +0800 Subject: [PATCH 2/4] fix: add version gating for validate e2e tests and 404 handling The configs/validate endpoint is not available in released APISIX versions yet (only on master). Skip validate e2e tests on APISIX versions below 3.15.0, and handle 404 responses gracefully with a user-friendly error message. --- libs/backend-apisix-standalone/e2e/validate.e2e-spec.ts | 4 +++- libs/backend-apisix/e2e/validate.e2e-spec.ts | 4 +++- libs/backend-apisix/src/validator.ts | 5 +++++ 3 files changed, 11 insertions(+), 2 deletions(-) diff --git a/libs/backend-apisix-standalone/e2e/validate.e2e-spec.ts b/libs/backend-apisix-standalone/e2e/validate.e2e-spec.ts index 3d691ecc..9d4b2e81 100644 --- a/libs/backend-apisix-standalone/e2e/validate.e2e-spec.ts +++ b/libs/backend-apisix-standalone/e2e/validate.e2e-spec.ts @@ -1,6 +1,7 @@ import { DifferV3 } from '@api7/adc-differ'; import * as ADCSDK from '@api7/adc-sdk'; import { lastValueFrom } from 'rxjs'; +import { gte } from 'semver'; import { BackendAPISIXStandalone } from '../src'; import { @@ -8,6 +9,7 @@ import { server1, token1, } from './support/constants'; +import { conditionalDescribe, semverCondition } from './support/utils'; const configToEvents = (config: ADCSDK.Configuration): Array => { return DifferV3.diff( @@ -16,7 +18,7 @@ const configToEvents = (config: ADCSDK.Configuration): Array => { ); }; -describe('Validate', () => { +conditionalDescribe(semverCondition(gte, '3.15.0'))('Validate', () => { let backend: BackendAPISIXStandalone; beforeAll(() => { diff --git a/libs/backend-apisix/e2e/validate.e2e-spec.ts b/libs/backend-apisix/e2e/validate.e2e-spec.ts index b219b633..e0e05a4f 100644 --- a/libs/backend-apisix/e2e/validate.e2e-spec.ts +++ b/libs/backend-apisix/e2e/validate.e2e-spec.ts @@ -1,9 +1,11 @@ import { DifferV3 } from '@api7/adc-differ'; import * as ADCSDK from '@api7/adc-sdk'; import { lastValueFrom } from 'rxjs'; +import { gte } from 'semver'; import { BackendAPISIX } from '../src'; import { defaultBackendOptions } from './support/constants'; +import { conditionalDescribe, semverCondition } from './support/utils'; const configToEvents = (config: ADCSDK.Configuration): Array => { return DifferV3.diff( @@ -12,7 +14,7 @@ const configToEvents = (config: ADCSDK.Configuration): Array => { ); }; -describe('Validate', () => { +conditionalDescribe(semverCondition(gte, '3.15.0'))('Validate', () => { let backend: BackendAPISIX; beforeAll(() => { diff --git a/libs/backend-apisix/src/validator.ts b/libs/backend-apisix/src/validator.ts index a0288e24..3d32af7b 100644 --- a/libs/backend-apisix/src/validator.ts +++ b/libs/backend-apisix/src/validator.ts @@ -49,6 +49,11 @@ export class Validator extends ADCSDK.backend.BackendEventSource { }); return { success: true, errors: [] }; } catch (error) { + if (axios.isAxiosError(error) && error.response?.status === 404) { + throw new Error( + 'Validate is not supported by the current APISIX version. Please upgrade to a newer version.', + ); + } if (axios.isAxiosError(error) && error.response?.status === 400) { this.subject.next({ type: ADCSDK.BackendEventType.AXIOS_DEBUG, From f8d5c671344d73c8628fee31077489142374f742 Mon Sep 17 00:00:00 2001 From: Jarvis Date: Mon, 20 Apr 2026 11:34:43 +0800 Subject: [PATCH 3/4] fix: add id to global_rules, update CI matrix with 3.15/3.16/dev, bump semver to 3.17.0 - Add missing id field to global_rules in validator (required by APISIX schema) - Add APISIX 3.15.0, 3.16.0, and dev to apisix backend CI matrix - Add APISIX 3.15.0, 3.16.0 to standalone CI matrix - Update validate test semver condition to gte 3.17.0 (validate endpoint not in any released version yet) --- .github/workflows/e2e.yaml | 18 +++++++++++++++--- .../e2e/validate.e2e-spec.ts | 2 +- libs/backend-apisix/e2e/validate.e2e-spec.ts | 2 +- libs/backend-apisix/src/validator.ts | 1 + 4 files changed, 18 insertions(+), 5 deletions(-) diff --git a/.github/workflows/e2e.yaml b/.github/workflows/e2e.yaml index 053d8038..f4e195fe 100644 --- a/.github/workflows/e2e.yaml +++ b/.github/workflows/e2e.yaml @@ -48,10 +48,20 @@ jobs: - 3.12.0 - 3.13.0 - 3.14.1 - env: - BACKEND_APISIX_VERSION: ${{ matrix.version }} - BACKEND_APISIX_IMAGE: ${{ matrix.version }}-debian + - 3.15.0 + - 3.16.0 + - dev steps: + - name: Determine APISIX image tag + run: | + if [ "${{ matrix.version }}" = "dev" ]; then + echo "BACKEND_APISIX_VERSION=999.999.999" >> $GITHUB_ENV + echo "BACKEND_APISIX_IMAGE=dev" >> $GITHUB_ENV + else + echo "BACKEND_APISIX_VERSION=${{ matrix.version }}" >> $GITHUB_ENV + echo "BACKEND_APISIX_IMAGE=${{ matrix.version }}-debian" >> $GITHUB_ENV + fi + - uses: actions/checkout@v4 # Setup backend environment @@ -80,6 +90,8 @@ jobs: version: - 3.13.0 - 3.14.1 + - 3.15.0 + - 3.16.0 - dev steps: - name: Determine APISIX image tag diff --git a/libs/backend-apisix-standalone/e2e/validate.e2e-spec.ts b/libs/backend-apisix-standalone/e2e/validate.e2e-spec.ts index 9d4b2e81..2f18001c 100644 --- a/libs/backend-apisix-standalone/e2e/validate.e2e-spec.ts +++ b/libs/backend-apisix-standalone/e2e/validate.e2e-spec.ts @@ -18,7 +18,7 @@ const configToEvents = (config: ADCSDK.Configuration): Array => { ); }; -conditionalDescribe(semverCondition(gte, '3.15.0'))('Validate', () => { +conditionalDescribe(semverCondition(gte, '3.17.0'))('Validate', () => { let backend: BackendAPISIXStandalone; beforeAll(() => { diff --git a/libs/backend-apisix/e2e/validate.e2e-spec.ts b/libs/backend-apisix/e2e/validate.e2e-spec.ts index e0e05a4f..ee73ec66 100644 --- a/libs/backend-apisix/e2e/validate.e2e-spec.ts +++ b/libs/backend-apisix/e2e/validate.e2e-spec.ts @@ -14,7 +14,7 @@ const configToEvents = (config: ADCSDK.Configuration): Array => { ); }; -conditionalDescribe(semverCondition(gte, '3.15.0'))('Validate', () => { +conditionalDescribe(semverCondition(gte, '3.17.0'))('Validate', () => { let backend: BackendAPISIX; beforeAll(() => { diff --git a/libs/backend-apisix/src/validator.ts b/libs/backend-apisix/src/validator.ts index 3d32af7b..0d5debc8 100644 --- a/libs/backend-apisix/src/validator.ts +++ b/libs/backend-apisix/src/validator.ts @@ -164,6 +164,7 @@ export class Validator extends ADCSDK.backend.BackendEventSource { } case ADCSDK.ResourceType.GLOBAL_RULE: { body.global_rules.push({ + id: event.resourceId, plugins: { [event.resourceId]: event.newValue }, } as unknown as typing.GlobalRule); nameIndex.global_rules.push(event.resourceName); From ed238ce17c58f023b623de72304a3f8dadc2c6e3 Mon Sep 17 00:00:00 2001 From: Jarvis Date: Mon, 20 Apr 2026 14:23:12 +0800 Subject: [PATCH 4/4] fix: relax standalone validate version gate to 3.16.0 The validate endpoint is available in standalone mode (config_provider: yaml) starting from APISIX 3.16.0, while etcd mode requires 3.17.0+. Split the version gating accordingly. --- libs/backend-apisix-standalone/e2e/validate.e2e-spec.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/libs/backend-apisix-standalone/e2e/validate.e2e-spec.ts b/libs/backend-apisix-standalone/e2e/validate.e2e-spec.ts index 2f18001c..d1bfe16d 100644 --- a/libs/backend-apisix-standalone/e2e/validate.e2e-spec.ts +++ b/libs/backend-apisix-standalone/e2e/validate.e2e-spec.ts @@ -18,7 +18,7 @@ const configToEvents = (config: ADCSDK.Configuration): Array => { ); }; -conditionalDescribe(semverCondition(gte, '3.17.0'))('Validate', () => { +conditionalDescribe(semverCondition(gte, '3.16.0'))('Validate', () => { let backend: BackendAPISIXStandalone; beforeAll(() => {