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

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

commit 43bbe76801dd0b1243221277b91fc713bac1d157
Author: 悟空 <rainchan...@163.com>
AuthorDate: Sat Jul 5 10:02:00 2025 +0800

    fix: apisixroute backend service reference to apisixupstream (#2453)
---
 internal/controller/apisixroute_controller.go   | 60 ++++++++++----------
 internal/controller/indexer/indexer.go          |  2 +-
 internal/provider/adc/translator/apisixroute.go | 64 ++++++++++-----------
 test/e2e/apisix/route.go                        | 75 +++++++++++++++++++++++--
 test/e2e/apiv2/apisixconsumer.go                | 18 ------
 test/e2e/apiv2/apisixglobalrule.go              | 18 ------
 test/e2e/apiv2/apisixpluginconfig.go            | 18 ------
 test/e2e/apiv2/apisixroute.go                   | 18 ------
 test/e2e/apiv2/apisixtls.go                     | 18 ------
 test/e2e/apiv2/apisixupstream.go                | 18 ------
 10 files changed, 130 insertions(+), 179 deletions(-)

diff --git a/internal/controller/apisixroute_controller.go 
b/internal/controller/apisixroute_controller.go
index 99fee292..bce27913 100644
--- a/internal/controller/apisixroute_controller.go
+++ b/internal/controller/apisixroute_controller.go
@@ -24,7 +24,9 @@ import (
        "fmt"
        "slices"
 
+       "github.com/api7/gopkg/pkg/log"
        "github.com/go-logr/logr"
+       "go.uber.org/zap"
        corev1 "k8s.io/api/core/v1"
        discoveryv1 "k8s.io/api/discovery/v1"
        networkingv1 "k8s.io/api/networking/v1"
@@ -283,13 +285,12 @@ func (r *ApisixRouteReconciler) validateBackends(ctx 
context.Context, tc *provid
        var backends = make(map[types.NamespacedName]struct{})
        for _, backend := range http.Backends {
                var (
-                       service = corev1.Service{
-                               ObjectMeta: metav1.ObjectMeta{
-                                       Name:      backend.ServiceName,
-                                       Namespace: in.Namespace,
-                               },
+                       au        apiv2.ApisixUpstream
+                       service   corev1.Service
+                       serviceNN = types.NamespacedName{
+                               Namespace: in.GetNamespace(),
+                               Name:      backend.ServiceName,
                        }
-                       serviceNN = utils.NamespacedName(&service)
                )
                if _, ok := backends[serviceNN]; ok {
                        return ReasonError{
@@ -300,12 +301,24 @@ func (r *ApisixRouteReconciler) validateBackends(ctx 
context.Context, tc *provid
                backends[serviceNN] = struct{}{}
 
                if err := r.Get(ctx, serviceNN, &service); err != nil {
-                       if err := client.IgnoreNotFound(err); err == nil {
+                       if err = client.IgnoreNotFound(err); err == nil {
                                r.Log.Error(errors.New("service not found"), 
"Service", serviceNN)
                                continue
                        }
                        return err
                }
+
+               // try to get apisixupstream with the same name as the backend 
service
+               log.Debugw("try to get apisixupstream with the same name as the 
backend service", zap.Stringer("Service", serviceNN))
+               if err := r.Get(ctx, serviceNN, &au); err != nil {
+                       log.Debugw("no ApisixUpstream with the same name as the 
backend service found", zap.Stringer("Service", serviceNN), zap.Error(err))
+                       if err = client.IgnoreNotFound(err); err != nil {
+                               return err
+                       }
+               } else {
+                       tc.Upstreams[serviceNN] = &au
+               }
+
                if service.Spec.Type == corev1.ServiceTypeExternalName {
                        tc.Services[serviceNN] = &service
                        continue
@@ -339,11 +352,7 @@ func (r *ApisixRouteReconciler) validateBackends(ctx 
context.Context, tc *provid
 
                // backend.subset specifies a subset of upstream nodes.
                // It specifies that the target pod's label should be a 
superset of the subset labels of the ApisixUpstream of the serviceName
-               subsetLabels, err := r.getSubsetLabels(ctx, in, backend)
-               if err != nil {
-                       return err
-               }
-
+               subsetLabels := r.getSubsetLabels(tc, serviceNN, backend)
                tc.EndpointSlices[serviceNN] = 
r.filterEndpointSlicesBySubsetLabels(ctx, endpoints.Items, subsetLabels)
        }
 
@@ -512,7 +521,7 @@ func (r *ApisixRouteReconciler) 
listApisixRouteForApisixUpstream(ctx context.Con
 
        var arList apiv2.ApisixRouteList
        if err := r.List(ctx, &arList, 
client.MatchingFields{indexer.ApisixUpstreamRef: 
indexer.GenIndexKey(au.GetNamespace(), au.GetName())}); err != nil {
-               r.Log.Error(err, "failed to list ApisixUpstreams")
+               r.Log.Error(err, "failed to list ApisixRoutes")
                return nil
        }
 
@@ -569,35 +578,24 @@ func (r *ApisixRouteReconciler) 
listApisixRoutesForPluginConfig(ctx context.Cont
        return pkgutils.DedupComparable(requests)
 }
 
-func (r *ApisixRouteReconciler) getSubsetLabels(ctx context.Context, ar 
*apiv2.ApisixRoute, backend apiv2.ApisixRouteHTTPBackend) (map[string]string, 
error) {
-       empty := make(map[string]string)
+func (r *ApisixRouteReconciler) getSubsetLabels(tctx 
*provider.TranslateContext, auNN types.NamespacedName, backend 
apiv2.ApisixRouteHTTPBackend) map[string]string {
        if backend.Subset == "" {
-               return empty, nil
+               return nil
        }
 
-       // Try to Get the ApisixUpstream with the same name as 
backend.ServiceName
-       var (
-               auNN = types.NamespacedName{
-                       Namespace: ar.GetNamespace(),
-                       Name:      backend.ServiceName,
-               }
-               au apiv2.ApisixUpstream
-       )
-       if err := r.Get(ctx, auNN, &au); err != nil {
-               if client.IgnoreNotFound(err) == nil {
-                       return empty, nil
-               }
-               return nil, err
+       au, ok := tctx.Upstreams[auNN]
+       if !ok {
+               return nil
        }
 
        // try to get the subset labels from the ApisixUpstream subsets
        for _, subset := range au.Spec.Subsets {
                if backend.Subset == subset.Name {
-                       return subset.Labels, nil
+                       return subset.Labels
                }
        }
 
-       return empty, nil
+       return nil
 }
 
 func (r *ApisixRouteReconciler) filterEndpointSlicesBySubsetLabels(ctx 
context.Context, in []discoveryv1.EndpointSlice, labels map[string]string) 
[]discoveryv1.EndpointSlice {
diff --git a/internal/controller/indexer/indexer.go 
b/internal/controller/indexer/indexer.go
index d9dd1638..2f1b7574 100644
--- a/internal/controller/indexer/indexer.go
+++ b/internal/controller/indexer/indexer.go
@@ -568,7 +568,7 @@ func ApisixRouteApisixUpstreamIndexFunc(obj client.Object) 
(keys []string) {
        ar := obj.(*apiv2.ApisixRoute)
        for _, rule := range ar.Spec.HTTP {
                for _, backend := range rule.Backends {
-                       if backend.Subset != "" && backend.ServiceName != "" {
+                       if backend.ServiceName != "" {
                                keys = append(keys, 
GenIndexKey(ar.GetNamespace(), backend.ServiceName))
                        }
                }
diff --git a/internal/provider/adc/translator/apisixroute.go 
b/internal/provider/adc/translator/apisixroute.go
index f8348d6a..c622202e 100644
--- a/internal/provider/adc/translator/apisixroute.go
+++ b/internal/provider/adc/translator/apisixroute.go
@@ -195,28 +195,35 @@ func (t *Translator) buildRoute(ar *apiv2.ApisixRoute, 
service *adc.Service, rul
 
 func (t *Translator) buildUpstream(tctx *provider.TranslateContext, service 
*adc.Service, ar *apiv2.ApisixRoute, rule apiv2.ApisixRouteHTTP) {
        var (
-               upstream          = adc.NewDefaultUpstream()
                upstreams         = make([]*adc.Upstream, 0)
                weightedUpstreams = 
make([]adc.TrafficSplitConfigRuleWeightedUpstream, 0)
                backendErr        error
        )
 
        for _, backend := range rule.Backends {
-               var upNodes adc.UpstreamNodes
+               upstream := adc.NewDefaultUpstream()
+               // try to get the apisixupstream with the same name as the 
backend service to be upstream config.
+               // err is ignored because it does not care about the 
externalNodes of the apisixupstream.
+               auNN := types.NamespacedName{Namespace: ar.GetNamespace(), 
Name: backend.ServiceName}
+               if au, ok := tctx.Upstreams[auNN]; ok {
+                       upstream, _ = t.translateApisixUpstream(tctx, au)
+               }
+
                if backend.ResolveGranularity == "service" {
-                       upNodes, backendErr = 
t.translateApisixRouteBackendResolveGranularityService(tctx, 
utils.NamespacedName(ar), backend)
+                       upstream.Nodes, backendErr = 
t.translateApisixRouteBackendResolveGranularityService(tctx, 
utils.NamespacedName(ar), backend)
                        if backendErr != nil {
                                t.Log.Error(backendErr, "failed to translate 
ApisixRoute backend with ResolveGranularity Service")
                                continue
                        }
                } else {
-                       upNodes, backendErr = 
t.translateApisixRouteBackendResolveGranularityEndpoint(tctx, 
utils.NamespacedName(ar), backend)
+                       upstream.Nodes, backendErr = 
t.translateApisixRouteBackendResolveGranularityEndpoint(tctx, 
utils.NamespacedName(ar), backend)
                        if backendErr != nil {
                                t.Log.Error(backendErr, "failed to translate 
ApisixRoute backend with ResolveGranularity Endpoint")
                                continue
                        }
                }
-               upstream.Nodes = append(upstream.Nodes, upNodes...)
+
+               upstreams = append(upstreams, upstream)
        }
 
        for _, upstreamRef := range rule.Upstreams {
@@ -241,21 +248,26 @@ func (t *Translator) buildUpstream(tctx 
*provider.TranslateContext, service *adc
                upstreams = append(upstreams, upstream)
        }
 
-       // If no .http[].backends is used and only .http[].upstreams is used, 
the first valid upstream is used as service.upstream;
-       // Other upstreams are configured in the traffic-split plugin
-       if len(rule.Backends) == 0 && len(upstreams) > 0 {
-               upstream = upstreams[0]
-               upstreams = upstreams[1:]
+       // no valid upstream
+       if backendErr != nil || len(upstreams) == 0 || len(upstreams[0].Nodes) 
== 0 {
+               return
        }
 
-       // set the default upstream's weight in traffic-split
-       weight, err := strconv.Atoi(upstream.Labels["meta_weight"])
-       if err != nil {
-               weight = apiv2.DefaultWeight
+       // the first valid upstream is used as service.upstream;
+       // the others are configured in the traffic-split plugin
+       service.Upstream = upstreams[0]
+       upstreams = upstreams[1:]
+
+       // set weight in traffic-split for the default upstream
+       if len(upstreams) > 0 {
+               weight, err := 
strconv.Atoi(service.Upstream.Labels["meta_weight"])
+               if err != nil {
+                       weight = apiv2.DefaultWeight
+               }
+               weightedUpstreams = append(weightedUpstreams, 
adc.TrafficSplitConfigRuleWeightedUpstream{
+                       Weight: weight,
+               })
        }
-       weightedUpstreams = append(weightedUpstreams, 
adc.TrafficSplitConfigRuleWeightedUpstream{
-               Weight: weight,
-       })
 
        // set others upstreams in traffic-split
        for _, item := range upstreams {
@@ -269,11 +281,6 @@ func (t *Translator) buildUpstream(tctx 
*provider.TranslateContext, service *adc
                })
        }
 
-       // set service
-       service.Upstream = upstream
-       if backendErr != nil && len(upstream.Nodes) == 0 {
-               t.addFaultInjectionPlugin(service)
-       }
        if len(weightedUpstreams) > 0 {
                service.Plugins["traffic-split"] = &adc.TrafficSplitConfig{
                        Rules: []adc.TrafficSplitConfigRule{
@@ -291,21 +298,10 @@ func (t *Translator) buildService(ar *apiv2.ApisixRoute, 
rule apiv2.ApisixRouteH
        service.ID = id.GenID(service.Name)
        service.Labels = label.GenLabel(ar)
        service.Hosts = rule.Match.Hosts
+       service.Upstream = adc.NewDefaultUpstream()
        return service
 }
 
-func (t *Translator) addFaultInjectionPlugin(service *adc.Service) {
-       if service.Plugins == nil {
-               service.Plugins = make(map[string]any)
-       }
-       service.Plugins["fault-injection"] = map[string]any{
-               "abort": map[string]any{
-                       "http_status": 500,
-                       "body":        "No existing backendRef provided",
-               },
-       }
-}
-
 func (t *Translator) translateApisixRouteBackendResolveGranularityService(tctx 
*provider.TranslateContext, arNN types.NamespacedName, backend 
apiv2.ApisixRouteHTTPBackend) (adc.UpstreamNodes, error) {
        serviceNN := types.NamespacedName{
                Namespace: arNN.Namespace,
diff --git a/test/e2e/apisix/route.go b/test/e2e/apisix/route.go
index 12940bd0..a47e6a31 100644
--- a/test/e2e/apisix/route.go
+++ b/test/e2e/apisix/route.go
@@ -18,6 +18,7 @@
 package apisix
 
 import (
+       "context"
        "fmt"
        "net"
        "net/http"
@@ -26,6 +27,7 @@ import (
        . "github.com/onsi/ginkgo/v2"
        . "github.com/onsi/gomega"
        "k8s.io/apimachinery/pkg/types"
+       "k8s.io/apimachinery/pkg/util/wait"
 
        apiv2 "github.com/apache/apisix-ingress-controller/api/v2"
        "github.com/apache/apisix-ingress-controller/test/e2e/framework"
@@ -247,11 +249,7 @@ spec:
                        var apisixRoute apiv2.ApisixRoute
                        applier.MustApplyAPIv2(types.NamespacedName{Namespace: 
s.Namespace(), Name: "default"}, &apisixRoute, fmt.Sprintf(apisixRouteSpec, 
"/get"))
 
-                       By("when there is no replica got 500 by 
fault-injection")
-                       err := s.ScaleHTTPBIN(0)
-                       Expect(err).ShouldNot(HaveOccurred(), "scale httpbin to 
0")
-                       Eventually(request).WithArguments("/get").WithTimeout(8 
* 
time.Second).ProbeEvery(time.Second).Should(Equal(http.StatusInternalServerError))
-                       
s.NewAPISIXClient().GET("/get").WithHost("httpbin").Expect().Body().IsEqual("No 
existing backendRef provided")
+                       Eventually(request).WithArguments("/get").WithTimeout(8 
* 
time.Second).ProbeEvery(time.Second).Should(Equal(http.StatusServiceUnavailable))
                })
 
                It("Test ApisixRoute resolveGranularity", func() {
@@ -501,5 +499,72 @@ spec:
                        Eventually(upstreamAddrs).Should(HaveKey(endpoint))
                        Eventually(upstreamAddrs).Should(HaveKey(clusterIP))
                })
+
+               It("Test backend implicit reference to apisixupstream", func() {
+                       var err error
+
+                       const apisixRouteSpec = `
+apiVersion: apisix.apache.org/v2
+kind: ApisixRoute
+metadata:
+  name: default
+spec:
+  ingressClassName: apisix
+  http:
+  - name: rule0
+    match:
+      hosts:
+      - httpbin
+      paths:
+      - /*
+    backends:
+    - serviceName: httpbin-service-e2e-test
+      servicePort: 80
+    plugins:
+    - name: response-rewrite
+      enable: true
+      config:
+        headers:
+          set:
+            "X-Upstream-Host": "$upstream_host"
+
+`
+                       const apisixUpstreamSpec = `
+apiVersion: apisix.apache.org/v2
+kind: ApisixUpstream
+metadata:
+  name: httpbin-service-e2e-test
+spec:
+  ingressClassName: apisix
+  passHost: rewrite
+  upstreamHost: hello.httpbin.org
+  loadbalancer:
+    type: "chash"
+    hashOn: "vars"
+    key: "server_name"
+`
+                       expectUpstreamHostIs := func(expectedUpstreamHost 
string) func(ctx context.Context) (bool, error) {
+                               return func(ctx context.Context) (done bool, 
err error) {
+                                       resp := 
s.NewAPISIXClient().GET("/get").WithHost("httpbin").Expect().Raw()
+                                       return resp.StatusCode == http.StatusOK 
&& resp.Header.Get("X-Upstream-Host") == expectedUpstreamHost, nil
+                               }
+                       }
+
+                       By("apply apisixroute")
+                       applier.MustApplyAPIv2(types.NamespacedName{Namespace: 
s.Namespace(), Name: "default"}, new(apiv2.ApisixRoute), apisixRouteSpec)
+
+                       By("verify ApisixRoute works")
+                       // expect upstream host is "httpbin"
+                       err = 
wait.PollUntilContextTimeout(context.Background(), time.Second, 10*time.Second, 
true, expectUpstreamHostIs("httpbin"))
+                       Expect(err).ShouldNot(HaveOccurred(), "verify 
ApisixRoute works")
+
+                       By("apply apisixupstream")
+                       applier.MustApplyAPIv2(types.NamespacedName{Namespace: 
s.Namespace(), Name: "httpbin-service-e2e-test"}, new(apiv2.ApisixUpstream), 
apisixUpstreamSpec)
+
+                       By("verify backend implicit reference to apisixupstream 
works")
+                       // expect upstream host is "hello.httpbin.org" which is 
rewritten by the apisixupstream
+                       err = 
wait.PollUntilContextTimeout(context.Background(), time.Second, 10*time.Second, 
true, expectUpstreamHostIs("hello.httpbin.org"))
+                       Expect(err).ShouldNot(HaveOccurred(), "check 
apisixupstream is referenced")
+               })
        })
 })
diff --git a/test/e2e/apiv2/apisixconsumer.go b/test/e2e/apiv2/apisixconsumer.go
deleted file mode 100644
index 97cba5b3..00000000
--- a/test/e2e/apiv2/apisixconsumer.go
+++ /dev/null
@@ -1,18 +0,0 @@
-// 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 apiv2
diff --git a/test/e2e/apiv2/apisixglobalrule.go 
b/test/e2e/apiv2/apisixglobalrule.go
deleted file mode 100644
index 97cba5b3..00000000
--- a/test/e2e/apiv2/apisixglobalrule.go
+++ /dev/null
@@ -1,18 +0,0 @@
-// 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 apiv2
diff --git a/test/e2e/apiv2/apisixpluginconfig.go 
b/test/e2e/apiv2/apisixpluginconfig.go
deleted file mode 100644
index 97cba5b3..00000000
--- a/test/e2e/apiv2/apisixpluginconfig.go
+++ /dev/null
@@ -1,18 +0,0 @@
-// 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 apiv2
diff --git a/test/e2e/apiv2/apisixroute.go b/test/e2e/apiv2/apisixroute.go
deleted file mode 100644
index 97cba5b3..00000000
--- a/test/e2e/apiv2/apisixroute.go
+++ /dev/null
@@ -1,18 +0,0 @@
-// 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 apiv2
diff --git a/test/e2e/apiv2/apisixtls.go b/test/e2e/apiv2/apisixtls.go
deleted file mode 100644
index 97cba5b3..00000000
--- a/test/e2e/apiv2/apisixtls.go
+++ /dev/null
@@ -1,18 +0,0 @@
-// 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 apiv2
diff --git a/test/e2e/apiv2/apisixupstream.go b/test/e2e/apiv2/apisixupstream.go
deleted file mode 100644
index 97cba5b3..00000000
--- a/test/e2e/apiv2/apisixupstream.go
+++ /dev/null
@@ -1,18 +0,0 @@
-// 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 apiv2

Reply via email to