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())
})
})