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