From ea2f157b0fda37d7c72473e95815d594801fdd26 Mon Sep 17 00:00:00 2001 From: Jarvis Date: Fri, 10 Apr 2026 10:51:42 +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 APISIX routes inherit hosts from their parent service, the route-level hosts is redundant. For backends that don't support route-level hosts, this causes a false diff every sync cycle: the local state has route.hosts but the remote state never does, triggering unnecessary PUT requests and audit log bloat. Remove route.Hosts assignment; service.Hosts remains as the canonical location for host matching. --- 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 575d26e44d..76b32970ed 100644 --- a/internal/adc/translator/apisixroute.go +++ b/internal/adc/translator/apisixroute.go @@ -190,7 +190,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 0000000000..6ea15aec51 --- /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, triggering unnecessary PUT requests. + 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 a287ccde36f542d11602fa3e2c9d0fc2467c2806 Mon Sep 17 00:00:00 2001 From: Nic 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 | 4 ++-- .github/workflows/benchmark-test.yml | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/apisix-e2e-test.yml b/.github/workflows/apisix-e2e-test.yml index c16bb6255f..ff4fb8c431 100644 --- a/.github/workflows/apisix-e2e-test.yml +++ b/.github/workflows/apisix-e2e-test.yml @@ -95,7 +95,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 @@ -165,7 +165,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/benchmark-test.yml b/.github/workflows/benchmark-test.yml index 565fb1d034..618ea96c65 100644 --- a/.github/workflows/benchmark-test.yml +++ b/.github/workflows/benchmark-test.yml @@ -98,7 +98,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 914020c3b192dff8cade25ec04b8b0a797f1bbb2 Mon Sep 17 00:00:00 2001 From: Nic 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+sleep 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 | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/test/e2e/crds/v2/route.go b/test/e2e/crds/v2/route.go index a1a9edc643..7f693cf8d1 100644 --- a/test/e2e/crds/v2/route.go +++ b/test/e2e/crds/v2/route.go @@ -139,7 +139,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") @@ -1347,7 +1347,6 @@ spec: &apisixRouteWithoutWS, fmt.Sprintf(apisixRouteSpec2, s.Namespace(), s.Namespace()), ) - time.Sleep(12 * time.Second) By("verify WebSocket connection fails without WebSocket enabled") u := url.URL{ @@ -1356,9 +1355,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(