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

ashishtiwari 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 76c695c8 feat: add support for named servicePort in ApisixRoute 
backend (#2553)
76c695c8 is described below

commit 76c695c8b12d368598ffda5794a45920e1a097c0
Author: Ashish Tiwari <[email protected]>
AuthorDate: Mon Sep 15 13:01:10 2025 +0530

    feat: add support for named servicePort in ApisixRoute backend (#2553)
---
 api/adc/types.go                              |   5 +-
 internal/adc/translator/apisixroute.go        |  43 +++++++-
 internal/controller/apisixroute_controller.go |  16 ++-
 test/e2e/crds/v2/route.go                     | 135 ++++++++++++++++++--------
 4 files changed, 150 insertions(+), 49 deletions(-)

diff --git a/api/adc/types.go b/api/adc/types.go
index 7133e72e..70fc4ce4 100644
--- a/api/adc/types.go
+++ b/api/adc/types.go
@@ -27,6 +27,7 @@ import (
        "time"
 
        "github.com/incubator4/go-resty-expr/expr"
+       "k8s.io/apimachinery/pkg/util/intstr"
 )
 
 const (
@@ -823,8 +824,8 @@ var (
 // the upstream name.
 // the resolveGranularity is not composited in the upstream name when it is 
endpoint.
 // ref: 
https://github.com/apache/apisix-ingress-controller/blob/10059afe3e84b693cc61e6df7a0040890a9d16eb/pkg/types/apisix/v1/types.go#L595-L598
-func ComposeUpstreamName(namespace, name, subset string, port int32, 
resolveGranularity string) string {
-       pstr := strconv.Itoa(int(port))
+func ComposeUpstreamName(namespace, name, subset string, port 
intstr.IntOrString, resolveGranularity string) string {
+       pstr := port.String()
        // FIXME Use sync.Pool to reuse this buffer if the upstream
        // name composing code path is hot.
        var p []byte
diff --git a/internal/adc/translator/apisixroute.go 
b/internal/adc/translator/apisixroute.go
index efbaa4a0..e5aa70c9 100644
--- a/internal/adc/translator/apisixroute.go
+++ b/internal/adc/translator/apisixroute.go
@@ -29,6 +29,7 @@ import (
        v1 "k8s.io/api/core/v1"
        metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
        "k8s.io/apimachinery/pkg/types"
+       "k8s.io/apimachinery/pkg/util/intstr"
        "k8s.io/utils/ptr"
        gatewayv1 "sigs.k8s.io/gateway-api/apis/v1"
 
@@ -235,7 +236,7 @@ func (t *Translator) buildUpstream(tctx 
*provider.TranslateContext, service *adc
                        upstream.Labels["meta_weight"] = 
strconv.FormatInt(int64(*backend.Weight), 10)
                }
 
-               upstreamName := adc.ComposeUpstreamName(ar.Namespace, 
backend.ServiceName, backend.Subset, int32(backend.ServicePort.IntValue()), 
backend.ResolveGranularity)
+               upstreamName := adc.ComposeUpstreamName(ar.Namespace, 
backend.ServiceName, backend.Subset, backend.ServicePort, 
backend.ResolveGranularity)
                upstream.Name = upstreamName
                upstream.ID = id.GenID(upstreamName)
                upstreams = append(upstreams, upstream)
@@ -328,6 +329,26 @@ func (t *Translator) buildService(ar *apiv2.ApisixRoute, 
rule apiv2.ApisixRouteH
        return service
 }
 
+func getPortFromService(svc *v1.Service, backendSvcPort intstr.IntOrString) 
(int32, error) {
+       var port int32
+       if backendSvcPort.Type == intstr.Int {
+               port = int32(backendSvcPort.IntValue())
+       } else {
+               found := false
+               for _, servicePort := range svc.Spec.Ports {
+                       if servicePort.Name == backendSvcPort.StrVal {
+                               port = servicePort.Port
+                               found = true
+                               break
+                       }
+               }
+               if !found {
+                       return 0, errors.Errorf("named port '%s' not found in 
service %s", backendSvcPort.StrVal, svc.Name)
+               }
+       }
+       return port, nil
+}
+
 func (t *Translator) translateApisixRouteBackendResolveGranularityService(tctx 
*provider.TranslateContext, arNN types.NamespacedName, backend 
apiv2.ApisixRouteHTTPBackend) (adc.UpstreamNodes, error) {
        serviceNN := types.NamespacedName{
                Namespace: arNN.Namespace,
@@ -340,10 +361,14 @@ func (t *Translator) 
translateApisixRouteBackendResolveGranularityService(tctx *
        if svc.Spec.ClusterIP == "" {
                return nil, errors.Errorf("conflict headless service and 
backend resolve granularity, ApisixRoute: %s, Service: %s", arNN, serviceNN)
        }
+       port, err := getPortFromService(svc, backend.ServicePort)
+       if err != nil {
+               return nil, err
+       }
        return adc.UpstreamNodes{
                {
                        Host:   svc.Spec.ClusterIP,
-                       Port:   backend.ServicePort.IntValue(),
+                       Port:   int(port),
                        Weight: *cmp.Or(backend.Weight, 
ptr.To(apiv2.DefaultWeight)),
                },
        }, nil
@@ -364,6 +389,18 @@ func (t *Translator) 
translateApisixRouteStreamBackendResolveGranularity(tctx *p
 }
 
 func (t *Translator) 
translateApisixRouteBackendResolveGranularityEndpoint(tctx 
*provider.TranslateContext, arNN types.NamespacedName, backend 
apiv2.ApisixRouteHTTPBackend) (adc.UpstreamNodes, error) {
+       serviceNN := types.NamespacedName{
+               Namespace: arNN.Namespace,
+               Name:      backend.ServiceName,
+       }
+       svc, ok := tctx.Services[serviceNN]
+       if !ok {
+               return nil, errors.Errorf("service not found, ApisixRoute: %s, 
Service: %s", arNN, serviceNN)
+       }
+       port, err := getPortFromService(svc, backend.ServicePort)
+       if err != nil {
+               return nil, err
+       }
        weight := int32(*cmp.Or(backend.Weight, ptr.To(apiv2.DefaultWeight)))
        backendRef := gatewayv1.BackendRef{
                BackendObjectReference: gatewayv1.BackendObjectReference{
@@ -371,7 +408,7 @@ func (t *Translator) 
translateApisixRouteBackendResolveGranularityEndpoint(tctx
                        Kind:      (*gatewayv1.Kind)(ptr.To("Service")),
                        Name:      gatewayv1.ObjectName(backend.ServiceName),
                        Namespace: (*gatewayv1.Namespace)(&arNN.Namespace),
-                       Port:      
(*gatewayv1.PortNumber)(&backend.ServicePort.IntVal),
+                       Port:      (*gatewayv1.PortNumber)(&port),
                },
                Weight: &weight,
        }
diff --git a/internal/controller/apisixroute_controller.go 
b/internal/controller/apisixroute_controller.go
index 4a0fc417..8fed3181 100644
--- a/internal/controller/apisixroute_controller.go
+++ b/internal/controller/apisixroute_controller.go
@@ -33,6 +33,7 @@ import (
        metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
        "k8s.io/apimachinery/pkg/runtime"
        k8stypes "k8s.io/apimachinery/pkg/types"
+       "k8s.io/apimachinery/pkg/util/intstr"
        ctrl "sigs.k8s.io/controller-runtime"
        "sigs.k8s.io/controller-runtime/pkg/builder"
        "sigs.k8s.io/controller-runtime/pkg/client"
@@ -432,9 +433,20 @@ func (r *ApisixRouteReconciler) validateHTTPBackend(tctx 
*provider.TranslateCont
        }
 
        if !slices.ContainsFunc(service.Spec.Ports, func(port 
corev1.ServicePort) bool {
-               return port.Port == int32(backend.ServicePort.IntValue())
+               if backend.ServicePort.Type == intstr.Int {
+                       return port.Port == 
int32(backend.ServicePort.IntValue())
+               }
+
+               if backend.ServicePort.Type == intstr.String {
+                       return port.Name == backend.ServicePort.StrVal
+               }
+               return false
        }) {
-               r.Log.Error(errors.New("port not found in service"), "Service", 
serviceNN, "port", backend.ServicePort.String())
+               if backend.ServicePort.Type == intstr.Int {
+                       r.Log.Error(errors.New("port not found in service"), 
"Service", serviceNN, "port", backend.ServicePort.IntValue())
+               } else {
+                       r.Log.Error(errors.New("named port not found in 
service"), "Service", serviceNN, "port", backend.ServicePort.StrVal)
+               }
                return nil
        }
        tctx.Services[serviceNN] = &service
diff --git a/test/e2e/crds/v2/route.go b/test/e2e/crds/v2/route.go
index 728d56e1..eacf0f05 100644
--- a/test/e2e/crds/v2/route.go
+++ b/test/e2e/crds/v2/route.go
@@ -60,8 +60,7 @@ var _ = Describe("Test ApisixRoute", 
Label("apisix.apache.org", "v2", "apisixrou
        })
 
        Context("Test ApisixRoute", func() {
-
-               It("Basic tests", func() {
+               Context("Basic tests", func() {
                        const apisixRouteSpec = `
 apiVersion: apisix.apache.org/v2
 kind: ApisixRoute
@@ -81,59 +80,111 @@ spec:
     - serviceName: httpbin-service-e2e-test
       servicePort: 80
 `
-                       request := func(path string) int {
-                               return 
s.NewAPISIXClient().GET(path).WithHost("httpbin").Expect().Raw().StatusCode
-                       }
 
-                       By("apply ApisixRoute")
-                       var apisixRoute apiv2.ApisixRoute
-                       applier.MustApplyAPIv2(types.NamespacedName{Namespace: 
s.Namespace(), Name: "default"},
-                               &apisixRoute, fmt.Sprintf(apisixRouteSpec, 
s.Namespace(), s.Namespace(), "/get"))
+                       const apisixRouteSpecWithNameServiceAndGranularity = `
+apiVersion: apisix.apache.org/v2
+kind: ApisixRoute
+metadata:
+  name: default
+  namespace: %s
+spec:
+  ingressClassName: %s
+  http:
+  - name: rule0
+    match:
+      hosts:
+      - httpbin
+      paths:
+      - %s
+    backends:
+    - serviceName: httpbin-service-e2e-test
+      servicePort: http
+      resolveGranularity: service
+`
 
-                       By("verify ApisixRoute works")
-                       
Eventually(request).WithArguments("/get").WithTimeout(20 * 
time.Second).ProbeEvery(time.Second).Should(Equal(http.StatusOK))
+                       const apisixRouteSpecWithNameServicePort = `
+apiVersion: apisix.apache.org/v2
+kind: ApisixRoute
+metadata:
+  name: default
+  namespace: %s
+spec:
+  ingressClassName: %s
+  http:
+  - name: rule0
+    match:
+      hosts:
+      - httpbin
+      paths:
+      - %s
+    backends:
+    - serviceName: httpbin-service-e2e-test
+      servicePort: http
+`
+                       test := func(apisixRouteSpec string) {
+                               request := func(path string) int {
+                                       return 
s.NewAPISIXClient().GET(path).WithHost("httpbin").Expect().Raw().StatusCode
+                               }
 
-                       By("update ApisixRoute")
-                       applier.MustApplyAPIv2(types.NamespacedName{Namespace: 
s.Namespace(), Name: "default"},
-                               &apisixRoute, fmt.Sprintf(apisixRouteSpec, 
s.Namespace(), s.Namespace(), "/headers"))
-                       
Eventually(request).WithArguments("/get").WithTimeout(20 * 
time.Second).ProbeEvery(time.Second).Should(Equal(http.StatusNotFound))
-                       
s.NewAPISIXClient().GET("/headers").WithHost("httpbin").Expect().Status(http.StatusOK)
+                               By("apply ApisixRoute")
+                               var apisixRoute apiv2.ApisixRoute
+                               
applier.MustApplyAPIv2(types.NamespacedName{Namespace: s.Namespace(), Name: 
"default"},
+                                       &apisixRoute, 
fmt.Sprintf(apisixRouteSpec, s.Namespace(), s.Namespace(), "/get"))
 
-                       By("delete ApisixRoute")
-                       err := s.DeleteResource("ApisixRoute", "default")
-                       Expect(err).ShouldNot(HaveOccurred(), "deleting 
ApisixRoute")
-                       
Eventually(request).WithArguments("/headers").WithTimeout(20 * 
time.Second).ProbeEvery(time.Second).Should(Equal(http.StatusNotFound))
+                               By("verify ApisixRoute works")
+                               
Eventually(request).WithArguments("/get").WithTimeout(20 * 
time.Second).ProbeEvery(time.Second).Should(Equal(http.StatusOK))
 
-                       By("request /metrics endpoint from controller")
+                               By("update ApisixRoute")
+                               
applier.MustApplyAPIv2(types.NamespacedName{Namespace: s.Namespace(), Name: 
"default"},
+                                       &apisixRoute, 
fmt.Sprintf(apisixRouteSpec, s.Namespace(), s.Namespace(), "/headers"))
+                               
Eventually(request).WithArguments("/get").WithTimeout(20 * 
time.Second).ProbeEvery(time.Second).Should(Equal(http.StatusNotFound))
+                               
s.NewAPISIXClient().GET("/headers").WithHost("httpbin").Expect().Status(http.StatusOK)
 
-                       // Get the metrics service endpoint
-                       metricsURL := s.GetMetricsEndpoint()
+                               By("delete ApisixRoute")
+                               err := s.DeleteResource("ApisixRoute", 
"default")
+                               Expect(err).ShouldNot(HaveOccurred(), "deleting 
ApisixRoute")
+                               
Eventually(request).WithArguments("/headers").WithTimeout(20 * 
time.Second).ProbeEvery(time.Second).Should(Equal(http.StatusNotFound))
 
-                       By("verify metrics content")
-                       resp, err := http.Get(metricsURL)
-                       Expect(err).ShouldNot(HaveOccurred(), "request metrics 
endpoint")
-                       defer func() {
-                               _ = resp.Body.Close()
-                       }()
+                               By("request /metrics endpoint from controller")
 
-                       Expect(resp.StatusCode).Should(Equal(http.StatusOK))
+                               // Get the metrics service endpoint
+                               metricsURL := s.GetMetricsEndpoint()
+
+                               By("verify metrics content")
+                               resp, err := http.Get(metricsURL)
+                               Expect(err).ShouldNot(HaveOccurred(), "request 
metrics endpoint")
+                               defer func() {
+                                       _ = resp.Body.Close()
+                               }()
 
-                       body, err := io.ReadAll(resp.Body)
-                       Expect(err).ShouldNot(HaveOccurred(), "read metrics 
response")
+                               
Expect(resp.StatusCode).Should(Equal(http.StatusOK))
 
-                       bodyStr := string(body)
+                               body, err := io.ReadAll(resp.Body)
+                               Expect(err).ShouldNot(HaveOccurred(), "read 
metrics response")
 
-                       // Verify prometheus format
-                       
Expect(resp.Header.Get("Content-Type")).Should(ContainSubstring("text/plain; 
version=0.0.4; charset=utf-8"))
+                               bodyStr := string(body)
 
-                       // Verify specific metrics from metrics.go exist
-                       
Expect(bodyStr).Should(ContainSubstring("apisix_ingress_adc_sync_duration_seconds"))
-                       
Expect(bodyStr).Should(ContainSubstring("apisix_ingress_adc_sync_total"))
-                       
Expect(bodyStr).Should(ContainSubstring("apisix_ingress_status_update_queue_length"))
-                       
Expect(bodyStr).Should(ContainSubstring("apisix_ingress_file_io_duration_seconds"))
+                               // Verify prometheus format
+                               
Expect(resp.Header.Get("Content-Type")).Should(ContainSubstring("text/plain; 
version=0.0.4; charset=utf-8"))
 
-                       // Log metrics for debugging
-                       fmt.Printf("Metrics endpoint response:\n%s\n", bodyStr)
+                               // Verify specific metrics from metrics.go exist
+                               
Expect(bodyStr).Should(ContainSubstring("apisix_ingress_adc_sync_duration_seconds"))
+                               
Expect(bodyStr).Should(ContainSubstring("apisix_ingress_adc_sync_total"))
+                               
Expect(bodyStr).Should(ContainSubstring("apisix_ingress_status_update_queue_length"))
+                               
Expect(bodyStr).Should(ContainSubstring("apisix_ingress_file_io_duration_seconds"))
+
+                               // Log metrics for debugging
+                               fmt.Printf("Metrics endpoint response:\n%s\n", 
bodyStr)
+                       }
+                       It("Basic", func() {
+                               test(apisixRouteSpec)
+                       })
+                       It("Basic: with named service port", func() {
+                               test(apisixRouteSpecWithNameServicePort)
+                       })
+                       It("Basic: with named service port and granularity 
service", func() {
+                               
test(apisixRouteSpecWithNameServiceAndGranularity)
+                       })
                })
 
                It("Test plugins in ApisixRoute", func() {

Reply via email to