From 8a9597c67315f6fa5ad576a01c66064518a5b64d Mon Sep 17 00:00:00 2001 From: Jarvis Date: Fri, 10 Apr 2026 10:50:02 +0800 Subject: [PATCH 1/3] fix: remove redundant route.Hosts to prevent false diffs in ADC sync The translator was setting hosts on both Route and Service for ApisixRoute resources. Since both APISIX and API7 EE backends support service-level hosts for route matching (APISIX routes inherit hosts from their parent service), the route-level hosts is redundant. For backends that don't support route-level hosts (e.g., API7 EE where hosts is a service-level concept), this causes a false diff every sync cycle: the local state has route.hosts but the remote state never will, triggering unnecessary PUT requests and audit log bloat. Remove route.Hosts assignment; service.Hosts remains as the canonical location for host matching. Ref: https://github.com/api7/rfcs/pull/2 Ref: https://github.com/api7/adc/issues/427 --- internal/adc/translator/apisixroute.go | 1 - internal/adc/translator/apisixroute_test.go | 84 +++++++++++++++++++++ 2 files changed, 84 insertions(+), 1 deletion(-) create mode 100644 internal/adc/translator/apisixroute_test.go diff --git a/internal/adc/translator/apisixroute.go b/internal/adc/translator/apisixroute.go index e42551eb..a015f209 100644 --- a/internal/adc/translator/apisixroute.go +++ b/internal/adc/translator/apisixroute.go @@ -192,7 +192,6 @@ func (t *Translator) buildRoute(ar *apiv2.ApisixRoute, service *adc.Service, rul route.EnableWebsocket = *enableWebsocket } route.FilterFunc = rule.Match.FilterFunc - route.Hosts = rule.Match.Hosts route.Methods = rule.Match.Methods route.Plugins = plugins route.Priority = ptr.To(int64(rule.Priority)) diff --git a/internal/adc/translator/apisixroute_test.go b/internal/adc/translator/apisixroute_test.go new file mode 100644 index 00000000..7dd71d63 --- /dev/null +++ b/internal/adc/translator/apisixroute_test.go @@ -0,0 +1,84 @@ +// Licensed to the Apache Software Foundation (ASF) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The ASF licenses this file +// to you under the Apache License, Version 2.0 (the +// "License"); you may not use this file except in compliance +// with the License. You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. See the License for the +// specific language governing permissions and limitations +// under the License. + +package translator + +import ( + "testing" + + "github.com/go-logr/logr" + "github.com/stretchr/testify/assert" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + + adc "github.com/apache/apisix-ingress-controller/api/adc" + apiv2 "github.com/apache/apisix-ingress-controller/api/v2" +) + +func TestBuildRoute_HostsNotSet(t *testing.T) { + translator := NewTranslator(logr.Discard()) + + ar := &apiv2.ApisixRoute{ + ObjectMeta: metav1.ObjectMeta{ + Name: "test-route", + Namespace: "default", + }, + } + + service := &adc.Service{} + rule := apiv2.ApisixRouteHTTP{ + Name: "rule1", + Match: apiv2.ApisixRouteHTTPMatch{ + Hosts: []string{"example.com", "foo.com"}, + Paths: []string{"/api/*"}, + }, + } + + var enableWebsocket *bool + translator.buildRoute(ar, service, rule, nil, nil, nil, &enableWebsocket) + + assert.Len(t, service.Routes, 1) + route := service.Routes[0] + // route.Hosts should NOT be set — hosts belong on Service, not Route. + // Setting hosts on Route causes false diffs in backends that don't + // support route-level hosts (e.g., API7 EE). + assert.Nil(t, route.Hosts, "route.Hosts should not be set; hosts should only be on Service") + assert.Equal(t, []string{"/api/*"}, route.Uris) +} + +func TestBuildService_HostsSet(t *testing.T) { + translator := NewTranslator(logr.Discard()) + + ar := &apiv2.ApisixRoute{ + ObjectMeta: metav1.ObjectMeta{ + Name: "test-route", + Namespace: "default", + }, + } + + rule := apiv2.ApisixRouteHTTP{ + Name: "rule1", + Match: apiv2.ApisixRouteHTTPMatch{ + Hosts: []string{"example.com", "foo.com"}, + Paths: []string{"/api/*"}, + }, + } + + service := translator.buildService(ar, rule, 0) + + // service.Hosts SHOULD be set — this is the canonical location for hosts. + assert.Equal(t, []string{"example.com", "foo.com"}, service.Hosts) +} From b1af3daef6a9d7fe0bf33ed0f4b8e70bd2894342 Mon Sep 17 00:00:00 2001 From: Jarvis Date: Fri, 10 Apr 2026 13:36:15 +0800 Subject: [PATCH 2/3] =?UTF-8?q?ci:=20fix=20ADC=20binary=20extraction=20pat?= =?UTF-8?q?h=20(main.js=20=E2=86=92=20main.cjs)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The ghcr.io/api7/adc:dev Docker image now bundles main.cjs instead of main.js at the container root. Update all workflow files that extract the ADC binary via docker cp. --- .github/workflows/apisix-e2e-test.yml | 2 +- .github/workflows/e2e-test-k8s.yml | 2 +- .github/workflows/e2e-test.yml | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/apisix-e2e-test.yml b/.github/workflows/apisix-e2e-test.yml index 280122cc..29a156ad 100644 --- a/.github/workflows/apisix-e2e-test.yml +++ b/.github/workflows/apisix-e2e-test.yml @@ -103,7 +103,7 @@ jobs: if: ${{ env.ADC_VERSION == 'dev' }} run: | docker create --name adc-temp ghcr.io/api7/adc:dev - docker cp adc-temp:main.js adc.js + docker cp adc-temp:main.cjs adc.js docker rm adc-temp node $(pwd)/adc.js -v echo "ADC_BIN=node $(pwd)/adc.js" >> $GITHUB_ENV diff --git a/.github/workflows/e2e-test-k8s.yml b/.github/workflows/e2e-test-k8s.yml index 5e381977..134d60c6 100644 --- a/.github/workflows/e2e-test-k8s.yml +++ b/.github/workflows/e2e-test-k8s.yml @@ -100,7 +100,7 @@ jobs: if: ${{ env.ADC_VERSION == 'dev' }} run: | docker create --name adc-temp ghcr.io/api7/adc:dev - docker cp adc-temp:main.js adc.js + docker cp adc-temp:main.cjs adc.js docker rm adc-temp node $(pwd)/adc.js -v echo "ADC_BIN=node $(pwd)/adc.js" >> $GITHUB_ENV diff --git a/.github/workflows/e2e-test.yml b/.github/workflows/e2e-test.yml index c264c5be..94a7b383 100644 --- a/.github/workflows/e2e-test.yml +++ b/.github/workflows/e2e-test.yml @@ -102,7 +102,7 @@ jobs: if: ${{ env.ADC_VERSION == 'dev' }} run: | docker create --name adc-temp ghcr.io/api7/adc:dev - docker cp adc-temp:main.js adc.js + docker cp adc-temp:main.cjs adc.js docker rm adc-temp node $(pwd)/adc.js -v echo "ADC_BIN=node $(pwd)/adc.js" >> $GITHUB_ENV From 570089686ae1acb94a6de586717a13e2171da203 Mon Sep 17 00:00:00 2001 From: Jarvis Date: Mon, 13 Apr 2026 14:34:18 +0800 Subject: [PATCH 3/3] test: fix flaky e2e assertions in standalone mode In APISIX standalone mode, config application via /apisix/admin/configs is async (returns 202). Two tests asserted HTTP status immediately after config push without retry, causing intermittent 404s: - Basic test: direct assertion on /headers after route update - WebSocket test: direct dial assertion after route creation Replace direct assertions with Eventually retry (20s timeout, 1s interval) to match the pattern used elsewhere in the test suite. --- test/e2e/crds/v2/route.go | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/test/e2e/crds/v2/route.go b/test/e2e/crds/v2/route.go index c78b2fc4..d228885d 100644 --- a/test/e2e/crds/v2/route.go +++ b/test/e2e/crds/v2/route.go @@ -141,7 +141,7 @@ spec: applier.MustApplyAPIv2(types.NamespacedName{Namespace: s.Namespace(), Name: "default"}, &apisixRoute, fmt.Sprintf(apisixRouteSpec, s.Namespace(), s.Namespace(), "/headers")) Eventually(request).WithArguments("/get").WithTimeout(20 * time.Second).ProbeEvery(time.Second).Should(Equal(http.StatusNotFound)) - s.NewAPISIXClient().GET("/headers").WithHost("httpbin").Expect().Status(http.StatusOK) + Eventually(request).WithArguments("/headers").WithTimeout(20 * time.Second).ProbeEvery(time.Second).Should(Equal(http.StatusOK)) By("delete ApisixRoute") err := s.DeleteResource("ApisixRoute", "default") @@ -1448,9 +1448,14 @@ spec: Path: "/echo", } headers := http.Header{"Host": []string{"httpbin.org"}} - _, resp, _ := websocket.DefaultDialer.Dial(u.String(), headers) - // should receive 200 instead of 101 - Expect(resp.StatusCode).Should(Equal(http.StatusOK)) + // In standalone mode, config application is async — retry until the route is active + Eventually(func() int { + _, resp, _ := websocket.DefaultDialer.Dial(u.String(), headers) + if resp == nil { + return 0 + } + return resp.StatusCode + }).WithTimeout(20 * time.Second).ProbeEvery(time.Second).Should(Equal(http.StatusOK)) By("apply ApisixRoute for WebSocket") var apisixRoute apiv2.ApisixRoute applier.MustApplyAPIv2(