You are viewing a plain text version of this content. The canonical link for it is here.
Posted to notifications@apisix.apache.org by ro...@apache.org on 2026/01/07 04:11:44 UTC
(apisix-ingress-controller) 02/04: fix: issue test case
This is an automated email from the ASF dual-hosted git repository.
ronething pushed a commit to branch fix/issue_2689
in repository https://gitbox.apache.org/repos/asf/apisix-ingress-controller.git
commit 3d5087a18022898aaa6d63a4f163914d81bbf836
Author: Ashing Zheng <ax...@gmail.com>
AuthorDate: Tue Jan 6 17:27:57 2026 +0800
fix: issue test case
Signed-off-by: Ashing Zheng <ax...@gmail.com>
---
internal/controller/apisixroute_controller.go | 4 +-
test/e2e/crds/v2/issue_2689.go | 281 ++++++++++++++++++++++++++
2 files changed, 283 insertions(+), 2 deletions(-)
diff --git a/internal/controller/apisixroute_controller.go b/internal/controller/apisixroute_controller.go
index 9ee672fd..4e007ed5 100644
--- a/internal/controller/apisixroute_controller.go
+++ b/internal/controller/apisixroute_controller.go
@@ -536,9 +536,9 @@ func (r *ApisixRouteReconciler) listApisixRoutesForService(ctx context.Context,
serviceName = endpointSlice.Labels[discoveryv1.LabelServiceName]
arList apiv2.ApisixRouteList
)
-
+
r.Log.V(1).Info("EndpointSlice changed, listing ApisixRoutes for service", "namespace", namespace, "service", serviceName, "endpointslice", endpointSlice.Name)
-
+
if err := r.List(ctx, &arList, client.MatchingFields{
indexer.ServiceIndexRef: indexer.GenIndexKey(namespace, serviceName),
}); err != nil {
diff --git a/test/e2e/crds/v2/issue_2689.go b/test/e2e/crds/v2/issue_2689.go
new file mode 100644
index 00000000..687a00cc
--- /dev/null
+++ b/test/e2e/crds/v2/issue_2689.go
@@ -0,0 +1,281 @@
+// 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 v2
+
+import (
+ "context"
+ "fmt"
+ "net/http"
+ "time"
+
+ . "github.com/onsi/ginkgo/v2"
+ . "github.com/onsi/gomega"
+ "k8s.io/apimachinery/pkg/types"
+ "k8s.io/apimachinery/pkg/util/wait"
+
+ adctypes "github.com/apache/apisix-ingress-controller/api/adc"
+ apiv2 "github.com/apache/apisix-ingress-controller/api/v2"
+ "github.com/apache/apisix-ingress-controller/test/e2e/framework"
+ "github.com/apache/apisix-ingress-controller/test/e2e/scaffold"
+)
+
+var _ = FDescribe("Issue 2689: Service Inline Upstream Not Updated on Endpoint Changes", Label("apisix.apache.org", "v2", "apisixroute", "issue-2689"), func() {
+ var (
+ s = scaffold.NewDefaultScaffold()
+ applier = framework.NewApplier(s.GinkgoT, s.K8sClient, s.CreateResourceFromString)
+ )
+
+ BeforeEach(func() {
+ By("create GatewayProxy")
+ err := s.CreateResourceFromString(s.GetGatewayProxySpec())
+ Expect(err).NotTo(HaveOccurred(), "creating GatewayProxy")
+ time.Sleep(5 * time.Second)
+
+ By("create IngressClass")
+ err = s.CreateResourceFromStringWithNamespace(s.GetIngressClassYaml(), "")
+ Expect(err).NotTo(HaveOccurred(), "creating IngressClass")
+ time.Sleep(5 * time.Second)
+ })
+
+ It("Service inline upstream nodes should be updated when Pod IP changes", func() {
+ const apisixRouteSpec = `
+apiVersion: apisix.apache.org/v2
+kind: ApisixRoute
+metadata:
+ name: issue-2689-test
+ namespace: %s
+spec:
+ ingressClassName: %s
+ http:
+ - name: rule0
+ match:
+ hosts:
+ - httpbin
+ paths:
+ - /*
+ backends:
+ - serviceName: httpbin-service-e2e-test
+ servicePort: 80
+`
+
+ By("apply ApisixRoute")
+ var apisixRoute apiv2.ApisixRoute
+ applier.MustApplyAPIv2(types.NamespacedName{Namespace: s.Namespace(), Name: "issue-2689-test"},
+ &apisixRoute, fmt.Sprintf(apisixRouteSpec, s.Namespace(), s.Namespace()))
+
+ By("verify ApisixRoute works")
+ s.RequestAssert(&scaffold.RequestAssert{
+ Method: "GET",
+ Path: "/get",
+ Host: "httpbin",
+ Check: scaffold.WithExpectedStatus(http.StatusOK),
+ })
+
+ By("get initial Kubernetes Service endpoints")
+ initialEndpoints, err := s.GetServiceEndpoints(types.NamespacedName{
+ Namespace: s.Namespace(),
+ Name: "httpbin-service-e2e-test",
+ })
+ Expect(err).NotTo(HaveOccurred(), "getting initial service endpoints")
+ Expect(initialEndpoints).NotTo(BeEmpty(), "initial endpoints should not be empty")
+ initialPodIP := initialEndpoints[0]
+ GinkgoWriter.Printf("Initial Pod IP: %s\n", initialPodIP)
+
+ By("get initial APISIX Service configuration")
+ var initialService *adctypes.Service
+ var serviceID string
+ var serviceName string
+ err = wait.PollUntilContextTimeout(context.Background(), 2*time.Second, 30*time.Second, true, func(ctx context.Context) (done bool, err error) {
+ services, err := s.DefaultDataplaneResource().Service().List(ctx)
+ if err != nil {
+ return false, err
+ }
+ if len(services) == 0 {
+ return false, nil
+ }
+ // Find the service that matches our route
+ // Service name should contain the namespace and route name
+ for _, svc := range services {
+ if svc.Upstream != nil && len(svc.Upstream.Nodes) > 0 {
+ // Check if this service's upstream nodes match the initial endpoint
+ for _, node := range svc.Upstream.Nodes {
+ if node.Host == initialPodIP {
+ serviceID = svc.ID
+ serviceName = svc.Name
+ initialService = svc
+ GinkgoWriter.Printf("Found matching Service: ID=%s, Name=%s\n", serviceID, serviceName)
+ GinkgoWriter.Printf("Initial Service inline upstream nodes: %v\n", svc.Upstream.Nodes)
+ return true, nil
+ }
+ }
+ }
+ }
+ return false, nil
+ })
+ Expect(err).NotTo(HaveOccurred(), "finding initial APISIX service")
+ Expect(initialService).NotTo(BeNil(), "initial service should be found")
+ Expect(serviceID).NotTo(BeEmpty(), "service ID should not be empty")
+
+ // Record initial upstream nodes
+ initialUpstreamNodes := make(map[string]int) // host -> port
+ if initialService.Upstream != nil {
+ for _, node := range initialService.Upstream.Nodes {
+ initialUpstreamNodes[node.Host] = node.Port
+ }
+ }
+ GinkgoWriter.Printf("Initial upstream nodes: %v\n", initialUpstreamNodes)
+
+ By("scale httpbin deployment to 0 to trigger pod deletion")
+ err = s.ScaleHTTPBIN(0)
+ Expect(err).NotTo(HaveOccurred(), "scaling httpbin deployment to 0")
+
+ By("wait for endpoints to be empty")
+ err = wait.PollUntilContextTimeout(context.Background(), 1*time.Second, 30*time.Second, true, func(ctx context.Context) (done bool, err error) {
+ endpoints, err := s.GetServiceEndpoints(types.NamespacedName{
+ Namespace: s.Namespace(),
+ Name: "httpbin-service-e2e-test",
+ })
+ if err != nil {
+ return false, err
+ }
+ return len(endpoints) == 0, nil
+ })
+ Expect(err).NotTo(HaveOccurred(), "waiting for endpoints to be empty")
+
+ By("scale httpbin deployment to 1 to trigger new pod creation")
+ err = s.ScaleHTTPBIN(1)
+ Expect(err).NotTo(HaveOccurred(), "scaling httpbin deployment to 1")
+
+ By("wait for new pod to be ready and get new endpoint IP")
+ var newPodIP string
+ err = wait.PollUntilContextTimeout(context.Background(), 2*time.Second, 60*time.Second, true, func(ctx context.Context) (done bool, err error) {
+ endpoints, err := s.GetServiceEndpoints(types.NamespacedName{
+ Namespace: s.Namespace(),
+ Name: "httpbin-service-e2e-test",
+ })
+ if err != nil {
+ return false, err
+ }
+ if len(endpoints) == 0 {
+ return false, nil
+ }
+ newPodIP = endpoints[0]
+ // Verify that the new IP is different from the old one
+ if newPodIP != initialPodIP {
+ GinkgoWriter.Printf("New Pod IP: %s (different from initial: %s)\n", newPodIP, initialPodIP)
+ return true, nil
+ }
+ return false, nil
+ })
+ Expect(err).NotTo(HaveOccurred(), "waiting for new pod IP")
+ Expect(newPodIP).NotTo(Equal(initialPodIP), "new pod IP should be different from initial IP")
+
+ By("wait for controller sync period (default 1m) plus some buffer")
+ // Wait for sync period to ensure controller has time to sync
+ time.Sleep(70 * time.Second)
+
+ By("verify APISIX Service inline upstream nodes are updated")
+ err = wait.PollUntilContextTimeout(context.Background(), 5*time.Second, 2*time.Minute, true, func(ctx context.Context) (done bool, err error) {
+ services, err := s.DefaultDataplaneResource().Service().List(ctx)
+ if err != nil {
+ GinkgoWriter.Printf("Error listing services: %v\n", err)
+ return false, err
+ }
+
+ // Find the service by ID or name
+ for _, svc := range services {
+ if svc.ID == serviceID || svc.Name == serviceName {
+ if svc.Upstream == nil {
+ GinkgoWriter.Printf("Service %s has nil upstream\n", svc.ID)
+ return false, nil
+ }
+ if len(svc.Upstream.Nodes) == 0 {
+ GinkgoWriter.Printf("Service %s has empty upstream nodes\n", svc.ID)
+ return false, nil
+ }
+
+ // Check if any node matches the new pod IP
+ foundNewIP := false
+ stillHasOldIP := false
+ currentNodes := make(map[string]int)
+ for _, node := range svc.Upstream.Nodes {
+ currentNodes[node.Host] = node.Port
+ if node.Host == newPodIP {
+ foundNewIP = true
+ }
+ if node.Host == initialPodIP {
+ stillHasOldIP = true
+ }
+ }
+
+ GinkgoWriter.Printf("Service %s current upstream nodes: %v\n", svc.ID, currentNodes)
+ GinkgoWriter.Printf("Expected new Pod IP: %s, Found: %v\n", newPodIP, foundNewIP)
+ GinkgoWriter.Printf("Old Pod IP still present: %v\n", stillHasOldIP)
+
+ // The service should have the new IP and not have the old IP
+ if foundNewIP && !stillHasOldIP {
+ return true, nil
+ }
+ return false, nil
+ }
+ }
+
+ GinkgoWriter.Printf("Service %s not found in APISIX\n", serviceID)
+ return false, nil
+ })
+ Expect(err).NotTo(HaveOccurred(), "waiting for service upstream nodes to update")
+
+ By("verify the route still works with new pod IP")
+ s.RequestAssert(&scaffold.RequestAssert{
+ Method: "GET",
+ Path: "/get",
+ Host: "httpbin",
+ Check: scaffold.WithExpectedStatus(http.StatusOK),
+ })
+
+ By("final verification: get APISIX Service configuration and verify nodes")
+ services, err := s.DefaultDataplaneResource().Service().List(context.Background())
+ Expect(err).NotTo(HaveOccurred(), "getting final service configuration")
+ foundService := false
+ for _, svc := range services {
+ if svc.ID == serviceID || svc.Name == serviceName {
+ foundService = true
+ Expect(svc.Upstream).NotTo(BeNil(), "service upstream should not be nil")
+ Expect(len(svc.Upstream.Nodes)).To(BeNumerically(">", 0), "service upstream should have nodes")
+
+ // Verify nodes contain new IP
+ hasNewIP := false
+ hasOldIP := false
+ for _, node := range svc.Upstream.Nodes {
+ if node.Host == newPodIP {
+ hasNewIP = true
+ }
+ if node.Host == initialPodIP {
+ hasOldIP = true
+ }
+ }
+
+ GinkgoWriter.Printf("Final Service %s upstream nodes: %v\n", svc.ID, svc.Upstream.Nodes)
+ Expect(hasNewIP).To(BeTrue(), fmt.Sprintf("service upstream should contain new pod IP %s", newPodIP))
+ Expect(hasOldIP).To(BeFalse(), fmt.Sprintf("service upstream should not contain old pod IP %s", initialPodIP))
+ break
+ }
+ }
+ Expect(foundService).To(BeTrue(), "service should be found in final verification")
+ })
+})