This is an automated email from the ASF dual-hosted git repository.

guoqqqi pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/apisix-ingress-controller.git


The following commit(s) were added to refs/heads/master by this push:
     new 23b18dd7 fix: scope APISIX synchronization to controller-managed 
resources (#2906)
23b18dd7 is described below

commit 23b18dd76ab092ed92a6c3e4c02719883b8c186f
Author: AlinsRan <[email protected]>
AuthorDate: Thu Oct 29 17:14:19 2026 +0800

    fix: scope APISIX synchronization to controller-managed resources (#2906)
---
 docs/en/latest/FAQ.md                     |  25 ++++++
 internal/controller/label/label.go        |   3 +-
 internal/provider/apisix/provider.go      |  12 +++
 internal/provider/apisix/provider_test.go | 109 ++++++++++++++++-------
 internal/provider/common/retrier.go       |  21 ++++-
 internal/provider/common/retrier_test.go  |  82 ++++++++++++++++++
 test/e2e/apisix/managed_resources.go      | 139 ++++++++++++++++++++++++++++++
 test/e2e/crds/v2/pluginconfig.go          | 114 ++++++++++++------------
 test/e2e/crds/v2/route.go                 |  64 ++++++--------
 test/e2e/crds/v2/upstream.go              |  10 ++-
 10 files changed, 448 insertions(+), 131 deletions(-)

diff --git a/docs/en/latest/FAQ.md b/docs/en/latest/FAQ.md
index 4ad421cd..3c976204 100644
--- a/docs/en/latest/FAQ.md
+++ b/docs/en/latest/FAQ.md
@@ -54,3 +54,28 @@ When both filters and a PluginConfig CRD are applied:
 
 * If filters are applied first, PluginConfig overrides any overlapping plugin 
settings.
 * If PluginConfig is applied first, filters merge with PluginConfig settings, 
and overlapping fields from filters take precedence.
+
+### Can Admin API resources coexist with controller-managed resources?
+
+With `provider.type: apisix`, synchronization selects resources labeled
+`manager-by: apisix-ingress-controller`. Resources created outside the 
controller
+without this label are excluded from deletion, including when a Gateway or
+IngressClass is deleted. Do not reuse controller-generated resource IDs for
+externally managed resources: a matching ID can still be overwritten.
+
+ADC filters top-level resources. Routes nested under a selected Service are
+included with that Service, so externally managed routes should not share a
+controller-managed Service. Global rules and plugin metadata do not support 
this
+label-based isolation and remain subject to full reconciliation. The label also
+does not distinguish separate controller installations sharing one APISIX 
instance,
+or separate GatewayProxy resources that point to the same Admin API endpoint: 
each
+GatewayProxy is synchronized on its own and can delete resources the other one 
created.
+Give each GatewayProxy its own APISIX instance.
+This behavior does not apply to `apisix-standalone`, which manages a complete
+configuration.
+
+When upgrading, unlabeled historical resources with IDs still present in the
+controller's desired configuration are updated and labeled during 
synchronization.
+Unlabeled orphaned resources are preserved. Review their ownership before 
deleting
+them or adding the controller label; adding the label allows the next 
synchronization
+to delete resources absent from the controller's desired configuration.
diff --git a/internal/controller/label/label.go 
b/internal/controller/label/label.go
index 0c636f72..aa0040ff 100644
--- a/internal/controller/label/label.go
+++ b/internal/controller/label/label.go
@@ -31,6 +31,7 @@ const (
        LabelNamespace      = "k8s/namespace"
        LabelControllerName = "k8s/controller-name"
        LabelManagedBy      = "manager-by"
+       LabelManagedByValue = "apisix-ingress-controller"
 )
 
 func GenLabel(client client.Object, args ...string) Label {
@@ -39,7 +40,7 @@ func GenLabel(client client.Object, args ...string) Label {
        label[LabelNamespace] = client.GetNamespace()
        label[LabelName] = client.GetName()
        label[LabelControllerName] = config.ControllerConfig.ControllerName
-       label[LabelManagedBy] = "apisix-ingress-controller"
+       label[LabelManagedBy] = LabelManagedByValue
        for i := 0; i+1 < len(args); i += 2 {
                label[args[i]] = args[i+1]
        }
diff --git a/internal/provider/apisix/provider.go 
b/internal/provider/apisix/provider.go
index 29f67d2e..177ce080 100644
--- a/internal/provider/apisix/provider.go
+++ b/internal/provider/apisix/provider.go
@@ -21,6 +21,7 @@ import (
        "context"
        "errors"
        "fmt"
+       "maps"
        "net/http"
        "strings"
        "sync"
@@ -440,6 +441,17 @@ func (d *apisixProvider) pushConfig(ctx context.Context, 
input adcclient.SyncInp
                backend = d.DefaultBackendMode
        }
 
+       if backend != adcclient.BackendAPISIXStandalone {
+               // Scope both regular syncs and empty-config cleanup to 
controller-owned resources;
+               // only standalone, which replaces the whole configuration, 
stays unscoped.
+               // ADC does not apply label selectors to global rules or plugin 
metadata.
+               input.Labels = maps.Clone(input.Labels)
+               if input.Labels == nil {
+                       input.Labels = make(map[string]string)
+               }
+               input.Labels[label.LabelManagedBy] = label.LabelManagedByValue
+       }
+
        startTime := time.Now()
        resourceType := strings.Join(input.ResourceTypes, ",")
        if resourceType == "" {
diff --git a/internal/provider/apisix/provider_test.go 
b/internal/provider/apisix/provider_test.go
index 55eba787..f8faf159 100644
--- a/internal/provider/apisix/provider_test.go
+++ b/internal/provider/apisix/provider_test.go
@@ -31,9 +31,11 @@ import (
        "github.com/go-logr/logr/funcr"
        "github.com/stretchr/testify/assert"
        "github.com/stretchr/testify/require"
+       networkingv1 "k8s.io/api/networking/v1"
        apiextensionsv1 
"k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1"
        metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
        k8stypes "k8s.io/apimachinery/pkg/types"
+       "sigs.k8s.io/controller-runtime/pkg/client"
        gatewayv1 "sigs.k8s.io/gateway-api/apis/v1"
 
        adctypes "github.com/apache/apisix-ingress-controller/api/adc"
@@ -206,43 +208,45 @@ func TestDeleteLogsObjectIdentity(t *testing.T) {
 // Delete: a Gateway going away must reach the data plane right away -- an 
empty resource
 // set for the config it referenced -- not wait for the next scheduled sync 
round.
 func TestDeleteTriggersImmediateSyncForEvictedConfigs(t *testing.T) {
-       var mu sync.Mutex
-       var received []adcclient.ADCServerRequest
+       for _, gw := range []client.Object{
+               &gatewayv1.Gateway{TypeMeta: metav1.TypeMeta{Kind: "Gateway", 
APIVersion: gatewayv1.GroupVersion.String()}, ObjectMeta: 
metav1.ObjectMeta{Namespace: "default", Name: "gw"}},
+               &networkingv1.IngressClass{TypeMeta: metav1.TypeMeta{Kind: 
"IngressClass", APIVersion: networkingv1.SchemeGroupVersion.String()}, 
ObjectMeta: metav1.ObjectMeta{Name: "class"}},
+       } {
+               t.Run(utils.NamespacedNameKind(gw).String(), func(t *testing.T) 
{
+                       var mu sync.Mutex
+                       var received []adcclient.ADCServerRequest
+
+                       withMockADCServer(t, func(w http.ResponseWriter, r 
*http.Request) {
+                               var req adcclient.ADCServerRequest
+                               require.NoError(t, 
json.NewDecoder(r.Body).Decode(&req))
+                               mu.Lock()
+                               received = append(received, req)
+                               mu.Unlock()
+                               w.WriteHeader(http.StatusOK)
+                               _ = 
json.NewEncoder(w).Encode(adctypes.SyncResult{Status: adctypes.StatusSuccess})
+                       })
 
-       withMockADCServer(t, func(w http.ResponseWriter, r *http.Request) {
-               var req adcclient.ADCServerRequest
-               require.NoError(t, json.NewDecoder(r.Body).Decode(&req))
-               mu.Lock()
-               received = append(received, req)
-               mu.Unlock()
-               w.WriteHeader(http.StatusOK)
-               _ = json.NewEncoder(w).Encode(adctypes.SyncResult{Status: 
adctypes.StatusSuccess})
-       })
+                       d := newTestProvider(t)
 
-       d := newTestProvider(t)
+                       d.configManager.Update(utils.NamespacedNameKind(gw), 
map[types.NamespacedNameKind]adctypes.Config{
+                               {Namespace: "default", Name: "proxy", Kind: 
"GatewayProxy"}: {
+                                       Name:        "proxy",
+                                       BackendType: "apisix",
+                                       ServerAddrs: 
[]string{"http://apisix:9080"},
+                               },
+                       })
 
-       gw := &gatewayv1.Gateway{
-               TypeMeta: metav1.TypeMeta{
-                       Kind:       "Gateway",
-                       APIVersion: gatewayv1.GroupVersion.String(),
-               },
-               ObjectMeta: metav1.ObjectMeta{Namespace: "default", Name: "gw"},
-       }
-       d.configManager.Update(utils.NamespacedNameKind(gw), 
map[types.NamespacedNameKind]adctypes.Config{
-               {Namespace: "default", Name: "proxy", Kind: "GatewayProxy"}: {
-                       Name:        "proxy",
-                       BackendType: "apisix",
-                       ServerAddrs: []string{"http://apisix:9080"},
-               },
-       })
+                       require.NoError(t, d.Delete(context.Background(), gw))
 
-       require.NoError(t, d.Delete(context.Background(), gw))
+                       mu.Lock()
+                       defer mu.Unlock()
+                       require.Len(t, received, 1, "deleting a Gateway or 
IngressClass must push immediately")
+                       assert.Equal(t, "proxy", received[0].Task.Opts.CacheKey)
+                       assert.Equal(t, map[string]string{label.LabelManagedBy: 
"apisix-ingress-controller"}, received[0].Task.Opts.LabelSelector)
+                       assert.Empty(t, received[0].Task.Config.Services, "the 
evicted config's push must carry an empty resource set")
 
-       mu.Lock()
-       defer mu.Unlock()
-       require.Len(t, received, 1, "deleting a Gateway must push immediately, 
not wait for the next scheduled round")
-       assert.Equal(t, "proxy", received[0].Task.Opts.CacheKey)
-       assert.Empty(t, received[0].Task.Config.Services, "the evicted config's 
push must carry an empty resource set")
+               })
+       }
 }
 
 // TestSyncStillPushesHealthyConfigsWhenAnotherFails covers sync's error 
aggregation: one
@@ -255,6 +259,7 @@ func TestSyncStillPushesHealthyConfigsWhenAnotherFails(t 
*testing.T) {
        withMockADCServer(t, func(w http.ResponseWriter, r *http.Request) {
                var req adcclient.ADCServerRequest
                require.NoError(t, json.NewDecoder(r.Body).Decode(&req))
+               assert.Equal(t, map[string]string{label.LabelManagedBy: 
"apisix-ingress-controller"}, req.Task.Opts.LabelSelector)
                mu.Lock()
                seen[req.Task.Opts.CacheKey] = true
                mu.Unlock()
@@ -348,6 +353,46 @@ func 
TestRemoveResourceStateRemovesAnApisixGlobalRulesPlugins(t *testing.T) {
        assert.Equal(t, adctypes.GlobalRule{"cors": map[string]any{}}, 
resources.GlobalRules, "only the deleted resource's own plugins go away")
 }
 
+func TestPushConfigScopesAPISIXResources(t *testing.T) {
+       for _, tc := range []struct {
+               name           string
+               backend        string
+               defaultBackend string
+               scoped         bool
+       }{
+               {"explicit APISIX", "apisix", "apisix-standalone", true},
+               {"default APISIX", "", "apisix", true},
+               {"other backend", "custom", "apisix-standalone", true},
+               {"standalone", "apisix-standalone", "apisix", false},
+               {"default standalone", "", "apisix-standalone", false},
+       } {
+               t.Run(tc.name, func(t *testing.T) {
+                       var received adcclient.ADCServerRequest
+                       withMockADCServer(t, func(w http.ResponseWriter, r 
*http.Request) {
+                               assert.NoError(t, 
json.NewDecoder(r.Body).Decode(&received))
+                               _ = 
json.NewEncoder(w).Encode(adctypes.SyncResult{Status: adctypes.StatusSuccess})
+                       })
+                       d := newTestProvider(t)
+                       d.DefaultBackendMode = tc.defaultBackend
+                       labels := map[string]string{label.LabelNamespace: 
"default"}
+                       input := adcclient.SyncInput{
+                               Name:      "proxy",
+                               Config:    adctypes.Config{Name: "proxy", 
BackendType: tc.backend, ServerAddrs: []string{"http://apisix:9180"}},
+                               Resources: &adctypes.Resources{},
+                               Labels:    labels,
+                       }
+                       require.Empty(t, d.pushConfig(context.Background(), 
input))
+                       assert.Equal(t, "default", 
received.Task.Opts.LabelSelector[label.LabelNamespace])
+                       if tc.scoped {
+                               assert.Equal(t, "apisix-ingress-controller", 
received.Task.Opts.LabelSelector[label.LabelManagedBy])
+                       } else {
+                               assert.NotContains(t, 
received.Task.Opts.LabelSelector, label.LabelManagedBy)
+                       }
+                       assert.Equal(t, map[string]string{label.LabelNamespace: 
"default"}, labels, "must not mutate the caller's selector")
+               })
+       }
+}
+
 func TestDeleteGatewayPreservesSharedProxyResources(t *testing.T) {
        var mu sync.Mutex
        var received adcclient.ADCServerRequest
diff --git a/internal/provider/common/retrier.go 
b/internal/provider/common/retrier.go
index 1277ee93..bda03cc2 100644
--- a/internal/provider/common/retrier.go
+++ b/internal/provider/common/retrier.go
@@ -70,6 +70,10 @@ func (r *Retrier) Reset() {
                r.timer.Stop()
                r.timer = nil
        }
+       select {
+       case <-r.ch:
+       default:
+       }
        r.backoff.Reset()
 }
 
@@ -77,18 +81,27 @@ func (r *Retrier) Next() {
        r.mu.Lock()
        defer r.mu.Unlock()
 
-       if r.timer != nil {
-               r.timer.Stop()
-               r.timer = nil
+       // Event-triggered failures must not postpone an already scheduled 
retry.
+       if r.timer != nil || len(r.ch) > 0 {
+               return
        }
 
        delay := r.backoff.Next()
-       r.timer = time.AfterFunc(delay, func() {
+       var timer *time.Timer
+       timer = time.AfterFunc(delay, func() {
+               r.mu.Lock()
+               defer r.mu.Unlock()
+               // Reset may have canceled this callback while it waited for 
the lock.
+               if r.timer != timer {
+                       return
+               }
+               r.timer = nil
                select {
                case r.ch <- struct{}{}:
                default:
                }
        })
+       r.timer = timer
 }
 
 func (r *Retrier) C() <-chan struct{} {
diff --git a/internal/provider/common/retrier_test.go 
b/internal/provider/common/retrier_test.go
new file mode 100644
index 00000000..1cdd0143
--- /dev/null
+++ b/internal/provider/common/retrier_test.go
@@ -0,0 +1,82 @@
+// 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 common
+
+import (
+       "testing"
+       "testing/synctest"
+       "time"
+)
+
+func TestRetrierPreservesPendingDeadline(t *testing.T) {
+       synctest.Test(t, func(t *testing.T) {
+               r := NewRetrier(NewExponentialBackoff(10*time.Second, 
time.Minute))
+               defer r.Reset()
+               r.Next()
+               for range 9 {
+                       time.Sleep(time.Second)
+                       r.Next()
+               }
+               time.Sleep(time.Second)
+               synctest.Wait()
+               select {
+               case <-r.C():
+               default:
+                       t.Fatal("event-triggered failures postponed the pending 
retry")
+               }
+               r.Next()
+               time.Sleep(20*time.Second - time.Millisecond)
+               synctest.Wait()
+               select {
+               case <-r.C():
+                       t.Fatal("the next retry fired before its backoff delay")
+               default:
+               }
+               time.Sleep(time.Millisecond)
+               synctest.Wait()
+               select {
+               case <-r.C():
+               default:
+                       t.Fatal("the next retry must advance backoff only once")
+               }
+       })
+}
+
+func TestRetrierResetClearsPendingRetry(t *testing.T) {
+       synctest.Test(t, func(t *testing.T) {
+               r := NewRetrier(NewExponentialBackoff(time.Second, time.Minute))
+               defer r.Reset()
+               r.Next()
+               time.Sleep(time.Second)
+               synctest.Wait()
+               r.Reset()
+               select {
+               case <-r.C():
+                       t.Fatal("reset left a stale retry notification")
+               default:
+               }
+               r.Next()
+               time.Sleep(time.Second)
+               synctest.Wait()
+               select {
+               case <-r.C():
+               default:
+                       t.Fatal("reset did not restore the base delay")
+               }
+       })
+}
diff --git a/test/e2e/apisix/managed_resources.go 
b/test/e2e/apisix/managed_resources.go
new file mode 100644
index 00000000..b9ebf8d0
--- /dev/null
+++ b/test/e2e/apisix/managed_resources.go
@@ -0,0 +1,139 @@
+// 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 apisix
+
+import (
+       "fmt"
+       "net/http"
+
+       . "github.com/onsi/ginkgo/v2"
+       . "github.com/onsi/gomega"
+
+       "github.com/apache/apisix-ingress-controller/test/e2e/framework"
+       "github.com/apache/apisix-ingress-controller/test/e2e/scaffold"
+)
+
+var _ = Describe("Test managed resource isolation", Label("networking.k8s.io", 
"ingress"), func() {
+       if framework.ProviderType != framework.ProviderTypeAPISIX {
+               return
+       }
+       s := scaffold.NewDefaultScaffold()
+       const gatewayProxyYaml = `
+apiVersion: apisix.apache.org/v1alpha1
+kind: GatewayProxy
+metadata:
+  name: apisix-proxy-config
+spec:
+  provider:
+    type: ControlPlane
+    controlPlane:
+      endpoints:
+      - %s
+      auth:
+        type: AdminKey
+        adminKey:
+          value: "%s"
+`
+
+       const ingressClassYaml = `
+apiVersion: networking.k8s.io/v1
+kind: IngressClass
+metadata:
+  name: %s
+spec:
+  controller: "%s"
+  parameters:
+    apiGroup: "apisix.apache.org"
+    kind: "GatewayProxy"
+    name: "apisix-proxy-config"
+    namespace: "%s"
+    scope: "Namespace"
+`
+
+       const ingressYaml = `
+apiVersion: networking.k8s.io/v1
+kind: Ingress
+metadata:
+  name: httpbin
+spec:
+  ingressClassName: %s
+  rules:
+  - host: managed-resources.example
+    http:
+      paths:
+      - path: %s
+        pathType: Exact
+        backend:
+          service:
+            name: httpbin-service-e2e-test
+            port:
+              number: 80
+`
+
+       It("preserves external routes while reconciling and deleting an 
IngressClass", func() {
+               admin := s.Deployer.AdminAPIClient()
+               const externalID = "external-route"
+               admin.PUT("/apisix/admin/services/external-service").
+                       WithHeader("X-API-KEY", s.AdminKey()).
+                       WithJSON(map[string]any{
+                               "upstream": map[string]any{"type": 
"roundrobin", "nodes": map[string]int{"127.0.0.1:1980": 1}},
+                       }).Expect().Status(http.StatusCreated)
+               admin.PUT("/apisix/admin/routes/"+externalID).
+                       WithHeader("X-API-KEY", s.AdminKey()).
+                       WithJSON(map[string]any{
+                               "uri":        "/external",
+                               "service_id": "external-service",
+                               "plugins":    map[string]any{"redirect": 
map[string]any{"uri": "/preserved"}},
+                       }).Expect().Status(http.StatusCreated)
+
+               proxy := fmt.Sprintf(gatewayProxyYaml, 
s.Deployer.GetAdminEndpoint(), s.AdminKey())
+               class := fmt.Sprintf(ingressClassYaml, s.Namespace(), 
s.GetControllerName(), s.Namespace())
+               ingress := fmt.Sprintf(ingressYaml, s.Namespace(), "/get")
+               Expect(s.CreateResourceFromString(proxy)).To(Succeed())
+               Expect(s.CreateResourceFromStringWithNamespace(class, 
"")).To(Succeed())
+               Expect(s.CreateResourceFromString(ingress)).To(Succeed())
+               s.RequestAssert(&scaffold.RequestAssert{
+                       Method: "GET", Path: "/get", Host: 
"managed-resources.example",
+                       Check: scaffold.WithExpectedStatus(http.StatusOK),
+               })
+               assertExternal := func() {
+                       admin.GET("/apisix/admin/services/external-service").
+                               WithHeader("X-API-KEY", 
s.AdminKey()).Expect().Status(http.StatusOK)
+                       admin.GET("/apisix/admin/routes/"+externalID).
+                               WithHeader("X-API-KEY", 
s.AdminKey()).Expect().Status(http.StatusOK).
+                               
JSON().Object().Value("value").Object().Value("uri").IsEqual("/external")
+               }
+               assertExternal()
+
+               By("update the managed route without deleting the external 
route")
+               Expect(s.CreateResourceFromString(fmt.Sprintf(ingressYaml, 
s.Namespace(), "/anything"))).To(Succeed())
+               s.RequestAssert(&scaffold.RequestAssert{
+                       Method: "GET", Path: "/anything", Host: 
"managed-resources.example",
+                       Check: scaffold.WithExpectedStatus(http.StatusOK),
+               })
+               assertExternal()
+
+               By("delete the IngressClass and wait for managed routes to 
disappear")
+               Expect(s.DeleteResourceFromStringWithNamespace(class, 
"")).To(Succeed())
+               s.RequestAssert(&scaffold.RequestAssert{
+                       Method: "GET", Path: "/anything", Host: 
"managed-resources.example",
+                       Check: scaffold.WithExpectedStatus(http.StatusNotFound),
+               })
+               assertExternal()
+       })
+})
diff --git a/test/e2e/crds/v2/pluginconfig.go b/test/e2e/crds/v2/pluginconfig.go
index 8112a1ef..592a659f 100644
--- a/test/e2e/crds/v2/pluginconfig.go
+++ b/test/e2e/crds/v2/pluginconfig.go
@@ -94,15 +94,15 @@ spec:
                                &apisixRoute, fmt.Sprintf(apisixRouteSpec, 
s.Namespace()))
 
                        By("verify ApisixRoute works with plugin config")
-                       request := func() int {
-                               return 
s.NewAPISIXClient().GET("/get").Expect().Raw().StatusCode
-                       }
-                       Eventually(request).WithTimeout(30 * 
time.Second).ProbeEvery(1 * time.Second).Should(Equal(http.StatusOK))
-
-                       By("verify plugin from ApisixPluginConfig works")
-                       resp := 
s.NewAPISIXClient().GET("/get").Expect().Status(http.StatusOK)
-                       
resp.Header("X-Plugin-Config").IsEqual("test-response-rewrite")
-                       resp.Header("X-Plugin-Test").IsEqual("enabled")
+                       s.RequestAssert(&scaffold.RequestAssert{
+                               Method: "GET",
+                               Path:   "/get",
+                               Checks: []scaffold.ResponseCheckFunc{
+                                       
scaffold.WithExpectedStatus(http.StatusOK),
+                                       
scaffold.WithExpectedHeader("X-Plugin-Config", "test-response-rewrite"),
+                                       
scaffold.WithExpectedHeader("X-Plugin-Test", "enabled"),
+                               },
+                       })
 
                        By("delete ApisixRoute")
                        err := s.DeleteResource("ApisixRoute", "test-route")
@@ -112,7 +112,11 @@ spec:
                        err = s.DeleteResource("ApisixPluginConfig", 
"test-plugin-config")
                        Expect(err).ShouldNot(HaveOccurred(), "deleting 
ApisixPluginConfig")
 
-                       Eventually(request).WithTimeout(30 * 
time.Second).ProbeEvery(1 * time.Second).Should(Equal(http.StatusNotFound))
+                       s.RequestAssert(&scaffold.RequestAssert{
+                               Method: "GET",
+                               Path:   "/get",
+                               Check:  
scaffold.WithExpectedStatus(http.StatusNotFound),
+                       })
                })
 
                It("Test ApisixRoute stops serving when its ApisixPluginConfig 
is deleted", func() {
@@ -261,14 +265,15 @@ spec:
                                &apisixRoute, fmt.Sprintf(apisixRouteSpec, 
s.Namespace()))
 
                        By("verify initial plugin config works")
-                       request := func() int {
-                               return 
s.NewAPISIXClient().GET("/get").Expect().Raw().StatusCode
-                       }
-                       Eventually(request).WithTimeout(30 * 
time.Second).ProbeEvery(1 * time.Second).Should(Equal(http.StatusOK))
-
-                       resp := 
s.NewAPISIXClient().GET("/get").Expect().Status(http.StatusOK)
-                       resp.Header("X-Version").IsEqual("v1")
-                       resp.Header("X-Updated").IsEmpty()
+                       s.RequestAssert(&scaffold.RequestAssert{
+                               Method: "GET",
+                               Path:   "/get",
+                               Checks: []scaffold.ResponseCheckFunc{
+                                       
scaffold.WithExpectedStatus(http.StatusOK),
+                                       
scaffold.WithExpectedHeader("X-Version", "v1"),
+                                       
scaffold.WithExpectedHeader("X-Updated", ""),
+                               },
+                       })
 
                        By("update ApisixPluginConfig")
                        applier.MustApplyAPIv2(types.NamespacedName{Namespace: 
s.Namespace(), Name: "test-plugin-config-update"},
@@ -342,17 +347,16 @@ spec:
                                &apisixRoute, fmt.Sprintf(apisixRouteSpec, 
s.Namespace()))
 
                        By("verify ApisixRoute works")
-                       request := func() int {
-                               return 
s.NewAPISIXClient().GET("/get").Expect().Raw().StatusCode
-                       }
-                       Eventually(request).WithTimeout(60 * 
time.Second).ProbeEvery(2 * time.Second).Should(Equal(http.StatusOK))
-
-                       By("verify disabled plugin is not applied")
-                       resp := 
s.NewAPISIXClient().GET("/get").Expect().Status(http.StatusOK)
-                       resp.Header("X-Should-Not-Exist").IsEmpty()
-
-                       By("verify enabled plugin is applied")
-                       resp.Header("Access-Control-Allow-Origin").IsEqual("*")
+                       s.RequestAssert(&scaffold.RequestAssert{
+                               Method:  "GET",
+                               Path:    "/get",
+                               Timeout: 60 * time.Second,
+                               Checks: []scaffold.ResponseCheckFunc{
+                                       
scaffold.WithExpectedStatus(http.StatusOK),
+                                       
scaffold.WithExpectedNotHeader("X-Should-Not-Exist"),
+                                       
scaffold.WithExpectedHeader("Access-Control-Allow-Origin", "*"),
+                               },
+                       })
 
                        By("delete resources")
                        err := s.DeleteResource("ApisixRoute", 
"test-route-disabled")
@@ -414,16 +418,16 @@ spec:
                                &apisixRoute, fmt.Sprintf(apisixRouteSpec, 
s.Namespace()))
 
                        By("verify ApisixRoute works")
-                       request := func() int {
-                               return 
s.NewAPISIXClient().GET("/get").Expect().Raw().StatusCode
-                       }
-                       Eventually(request).WithTimeout(30 * 
time.Second).ProbeEvery(1 * time.Second).Should(Equal(http.StatusOK))
-
-                       By("verify route plugins override plugin config")
-                       resp := 
s.NewAPISIXClient().GET("/get").Expect().Status(http.StatusOK)
-                       resp.Header("X-From-Config").IsEmpty()
-                       resp.Header("X-From-Route").IsEqual("route")
-                       resp.Header("X-Shared").IsEqual("from-route")
+                       s.RequestAssert(&scaffold.RequestAssert{
+                               Method: "GET",
+                               Path:   "/get",
+                               Checks: []scaffold.ResponseCheckFunc{
+                                       
scaffold.WithExpectedStatus(http.StatusOK),
+                                       
scaffold.WithExpectedNotHeader("X-From-Config"),
+                                       
scaffold.WithExpectedHeader("X-From-Route", "route"),
+                                       scaffold.WithExpectedHeader("X-Shared", 
"from-route"),
+                               },
+                       })
 
                        By("delete resources")
                        err := s.DeleteResource("ApisixRoute", 
"test-route-override")
@@ -479,14 +483,15 @@ spec:
                                &apisixRoute, fmt.Sprintf(apisixRouteSpec, 
s.Namespace()))
 
                        By("verify cross-namespace reference works")
-                       request := func() int {
-                               return 
s.NewAPISIXClient().GET("/get").Expect().Raw().StatusCode
-                       }
-                       Eventually(request).WithTimeout(30 * 
time.Second).ProbeEvery(1 * time.Second).Should(Equal(http.StatusOK))
-
-                       resp := 
s.NewAPISIXClient().GET("/get").Expect().Status(http.StatusOK)
-                       resp.Header("X-Cross-Namespace").IsEqual("true")
-                       resp.Header("X-Namespace").IsEqual("default")
+                       s.RequestAssert(&scaffold.RequestAssert{
+                               Method: "GET",
+                               Path:   "/get",
+                               Checks: []scaffold.ResponseCheckFunc{
+                                       
scaffold.WithExpectedStatus(http.StatusOK),
+                                       
scaffold.WithExpectedHeader("X-Cross-Namespace", "true"),
+                                       
scaffold.WithExpectedHeader("X-Namespace", "default"),
+                               },
+                       })
 
                        By("delete resources")
                        err = s.DeleteResource("ApisixRoute", 
"test-route-cross-ns")
@@ -557,13 +562,14 @@ spec:
                                &apisixRoute, fmt.Sprintf(apisixRouteSpec, 
s.Namespace()))
 
                        By("verify ApisixRoute works with SecretRef")
-                       request := func() int {
-                               return 
s.NewAPISIXClient().GET("/get").Expect().Raw().StatusCode
-                       }
-                       Eventually(request).WithTimeout(30 * 
time.Second).ProbeEvery(1 * time.Second).Should(Equal(http.StatusOK))
-
-                       resp := 
s.NewAPISIXClient().GET("/get").Expect().Status(http.StatusOK)
-                       resp.Header("X-Secret-Ref").IsEqual("true")
+                       s.RequestAssert(&scaffold.RequestAssert{
+                               Method: "GET",
+                               Path:   "/get",
+                               Checks: []scaffold.ResponseCheckFunc{
+                                       
scaffold.WithExpectedStatus(http.StatusOK),
+                                       
scaffold.WithExpectedHeader("X-Secret-Ref", "true"),
+                               },
+                       })
 
                        By("delete resources")
                        err = s.DeleteResource("ApisixRoute", 
"test-route-secret")
diff --git a/test/e2e/crds/v2/route.go b/test/e2e/crds/v2/route.go
index 262550e8..5c7e6f90 100644
--- a/test/e2e/crds/v2/route.go
+++ b/test/e2e/crds/v2/route.go
@@ -812,26 +812,22 @@ spec:
                                &apisixRoute, fmt.Sprintf(apisixRouteSpec, 
s.Namespace(), s.Namespace()))
 
                        By("verify ApisixRoute with secretRef works")
-                       Eventually(func() *http.Response {
-                               return s.NewAPISIXClient().GET("/ip").
-                                       WithHeader("Host", "httpbin.org").
-                                       Expect().
-                                       Raw()
-                       }).WithTimeout(20 * 
time.Second).ProbeEvery(time.Second).Should(And(
-                               HaveHTTPStatus(http.StatusOK),
-                               HaveHTTPHeaderWithValue("X-Foo", "v1"),
-                               HaveHTTPHeaderWithValue("X-Foo2", "v2"),
-                       ))
-
-                       // Verify the response body contains the secret values
-                       resp := s.NewAPISIXClient().GET("/ip").
-                               WithHeader("Host", "httpbin.org").
-                               Expect().
-                               Status(http.StatusOK)
-
-                       resp.Body().Contains("This is the replaced preface") // 
From secret
-                       resp.Body().Contains("This is the epilogue")         // 
From config
-                       resp.Body().Contains("my custom body")               // 
From secret
+                       s.RequestAssert(&scaffold.RequestAssert{
+                               Method:  "GET",
+                               Path:    "/ip",
+                               Host:    "httpbin.org",
+                               Timeout: 20 * time.Second,
+                               Checks: []scaffold.ResponseCheckFunc{
+                                       
scaffold.WithExpectedStatus(http.StatusOK),
+                                       scaffold.WithExpectedHeader("X-Foo", 
"v1"),
+                                       scaffold.WithExpectedHeader("X-Foo2", 
"v2"),
+                                       scaffold.WithExpectedBodyContains(
+                                               "This is the replaced preface", 
// From secret
+                                               "This is the epilogue",         
// From config
+                                               "my custom body",               
// From secret
+                                       ),
+                               },
+                       })
                })
 
                It("Test ApisixRoute with secretRef - nested keys", func() {
@@ -885,23 +881,17 @@ spec:
                                &apisixRoute, fmt.Sprintf(apisixRouteSpec, 
s.Namespace(), s.Namespace()))
 
                        By("verify ApisixRoute with secretRef (nested keys) 
works")
-                       Eventually(func() *http.Response {
-                               return s.NewAPISIXClient().GET("/ip").
-                                       WithHeader("Host", "httpbin.org").
-                                       Expect().
-                                       Raw()
-                       }).WithTimeout(20 * 
time.Second).ProbeEvery(time.Second).Should(And(
-                               HaveHTTPStatus(http.StatusOK),
-                               HaveHTTPHeaderWithValue("X-Foo", "v2"), // From 
secret, overriding config value "v1"
-                       ))
-
-                       // Verify the response body contains the secret values
-                       resp := s.NewAPISIXClient().GET("/ip").
-                               WithHeader("Host", "httpbin.org").
-                               Expect().
-                               Status(http.StatusOK)
-
-                       resp.Body().Contains("my custom body") // From secret
+                       s.RequestAssert(&scaffold.RequestAssert{
+                               Method:  "GET",
+                               Path:    "/ip",
+                               Host:    "httpbin.org",
+                               Timeout: 20 * time.Second,
+                               Checks: []scaffold.ResponseCheckFunc{
+                                       
scaffold.WithExpectedStatus(http.StatusOK),
+                                       scaffold.WithExpectedHeader("X-Foo", 
"v2"),          // From secret, overriding config value "v1"
+                                       scaffold.WithExpectedBodyContains("my 
custom body"), // From secret
+                               },
+                       })
                })
        })
 
diff --git a/test/e2e/crds/v2/upstream.go b/test/e2e/crds/v2/upstream.go
index e96f90de..d5bf1023 100644
--- a/test/e2e/crds/v2/upstream.go
+++ b/test/e2e/crds/v2/upstream.go
@@ -131,9 +131,13 @@ spec:
                                
g.Expect(ups[0].Checks.Passive.Unhealthy.HTTPStatuses).To(Equal([]int{502}), 
"the passive unhealthy http status")
                        
}).WithTimeout(scaffold.DefaultTimeout).ProbeEvery(scaffold.DefaultInterval).Should(Succeed())
 
-                       for range 100 {
-                               
s.NewAPISIXClient().GET("/ip").WithHost("httpbin.org").Expect().Status(200)
-                       }
+                       // Workers pick up the route at different times; retry 
until a full round succeeds.
+                       Eventually(func(g Gomega) {
+                               for range 100 {
+                                       
g.Expect(s.NewAPISIXClient().GET("/ip").WithHost("httpbin.org").Expect().Raw().StatusCode).
+                                               To(Equal(200))
+                               }
+                       
}).WithTimeout(scaffold.DefaultTimeout).ProbeEvery(scaffold.DefaultInterval).Should(Succeed())
                })
        })
 

Reply via email to