Skip to content

Commit 76c695c

Browse files
authored
feat: add support for named servicePort in ApisixRoute backend (#2553)
1 parent 476783a commit 76c695c

File tree

4 files changed

+150
-49
lines changed

4 files changed

+150
-49
lines changed

api/adc/types.go

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ import (
2727
"time"
2828

2929
"github.com/incubator4/go-resty-expr/expr"
30+
"k8s.io/apimachinery/pkg/util/intstr"
3031
)
3132

3233
const (
@@ -823,8 +824,8 @@ var (
823824
// the upstream name.
824825
// the resolveGranularity is not composited in the upstream name when it is endpoint.
825826
// ref: https://github.com/apache/apisix-ingress-controller/blob/10059afe3e84b693cc61e6df7a0040890a9d16eb/pkg/types/apisix/v1/types.go#L595-L598
826-
func ComposeUpstreamName(namespace, name, subset string, port int32, resolveGranularity string) string {
827-
pstr := strconv.Itoa(int(port))
827+
func ComposeUpstreamName(namespace, name, subset string, port intstr.IntOrString, resolveGranularity string) string {
828+
pstr := port.String()
828829
// FIXME Use sync.Pool to reuse this buffer if the upstream
829830
// name composing code path is hot.
830831
var p []byte

internal/adc/translator/apisixroute.go

Lines changed: 40 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@ import (
2929
v1 "k8s.io/api/core/v1"
3030
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
3131
"k8s.io/apimachinery/pkg/types"
32+
"k8s.io/apimachinery/pkg/util/intstr"
3233
"k8s.io/utils/ptr"
3334
gatewayv1 "sigs.k8s.io/gateway-api/apis/v1"
3435

@@ -235,7 +236,7 @@ func (t *Translator) buildUpstream(tctx *provider.TranslateContext, service *adc
235236
upstream.Labels["meta_weight"] = strconv.FormatInt(int64(*backend.Weight), 10)
236237
}
237238

238-
upstreamName := adc.ComposeUpstreamName(ar.Namespace, backend.ServiceName, backend.Subset, int32(backend.ServicePort.IntValue()), backend.ResolveGranularity)
239+
upstreamName := adc.ComposeUpstreamName(ar.Namespace, backend.ServiceName, backend.Subset, backend.ServicePort, backend.ResolveGranularity)
239240
upstream.Name = upstreamName
240241
upstream.ID = id.GenID(upstreamName)
241242
upstreams = append(upstreams, upstream)
@@ -328,6 +329,26 @@ func (t *Translator) buildService(ar *apiv2.ApisixRoute, rule apiv2.ApisixRouteH
328329
return service
329330
}
330331

332+
func getPortFromService(svc *v1.Service, backendSvcPort intstr.IntOrString) (int32, error) {
333+
var port int32
334+
if backendSvcPort.Type == intstr.Int {
335+
port = int32(backendSvcPort.IntValue())
336+
} else {
337+
found := false
338+
for _, servicePort := range svc.Spec.Ports {
339+
if servicePort.Name == backendSvcPort.StrVal {
340+
port = servicePort.Port
341+
found = true
342+
break
343+
}
344+
}
345+
if !found {
346+
return 0, errors.Errorf("named port '%s' not found in service %s", backendSvcPort.StrVal, svc.Name)
347+
}
348+
}
349+
return port, nil
350+
}
351+
331352
func (t *Translator) translateApisixRouteBackendResolveGranularityService(tctx *provider.TranslateContext, arNN types.NamespacedName, backend apiv2.ApisixRouteHTTPBackend) (adc.UpstreamNodes, error) {
332353
serviceNN := types.NamespacedName{
333354
Namespace: arNN.Namespace,
@@ -340,10 +361,14 @@ func (t *Translator) translateApisixRouteBackendResolveGranularityService(tctx *
340361
if svc.Spec.ClusterIP == "" {
341362
return nil, errors.Errorf("conflict headless service and backend resolve granularity, ApisixRoute: %s, Service: %s", arNN, serviceNN)
342363
}
364+
port, err := getPortFromService(svc, backend.ServicePort)
365+
if err != nil {
366+
return nil, err
367+
}
343368
return adc.UpstreamNodes{
344369
{
345370
Host: svc.Spec.ClusterIP,
346-
Port: backend.ServicePort.IntValue(),
371+
Port: int(port),
347372
Weight: *cmp.Or(backend.Weight, ptr.To(apiv2.DefaultWeight)),
348373
},
349374
}, nil
@@ -364,14 +389,26 @@ func (t *Translator) translateApisixRouteStreamBackendResolveGranularity(tctx *p
364389
}
365390

366391
func (t *Translator) translateApisixRouteBackendResolveGranularityEndpoint(tctx *provider.TranslateContext, arNN types.NamespacedName, backend apiv2.ApisixRouteHTTPBackend) (adc.UpstreamNodes, error) {
392+
serviceNN := types.NamespacedName{
393+
Namespace: arNN.Namespace,
394+
Name: backend.ServiceName,
395+
}
396+
svc, ok := tctx.Services[serviceNN]
397+
if !ok {
398+
return nil, errors.Errorf("service not found, ApisixRoute: %s, Service: %s", arNN, serviceNN)
399+
}
400+
port, err := getPortFromService(svc, backend.ServicePort)
401+
if err != nil {
402+
return nil, err
403+
}
367404
weight := int32(*cmp.Or(backend.Weight, ptr.To(apiv2.DefaultWeight)))
368405
backendRef := gatewayv1.BackendRef{
369406
BackendObjectReference: gatewayv1.BackendObjectReference{
370407
Group: (*gatewayv1.Group)(&apiv2.GroupVersion.Group),
371408
Kind: (*gatewayv1.Kind)(ptr.To("Service")),
372409
Name: gatewayv1.ObjectName(backend.ServiceName),
373410
Namespace: (*gatewayv1.Namespace)(&arNN.Namespace),
374-
Port: (*gatewayv1.PortNumber)(&backend.ServicePort.IntVal),
411+
Port: (*gatewayv1.PortNumber)(&port),
375412
},
376413
Weight: &weight,
377414
}

internal/controller/apisixroute_controller.go

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@ import (
3333
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
3434
"k8s.io/apimachinery/pkg/runtime"
3535
k8stypes "k8s.io/apimachinery/pkg/types"
36+
"k8s.io/apimachinery/pkg/util/intstr"
3637
ctrl "sigs.k8s.io/controller-runtime"
3738
"sigs.k8s.io/controller-runtime/pkg/builder"
3839
"sigs.k8s.io/controller-runtime/pkg/client"
@@ -432,9 +433,20 @@ func (r *ApisixRouteReconciler) validateHTTPBackend(tctx *provider.TranslateCont
432433
}
433434

434435
if !slices.ContainsFunc(service.Spec.Ports, func(port corev1.ServicePort) bool {
435-
return port.Port == int32(backend.ServicePort.IntValue())
436+
if backend.ServicePort.Type == intstr.Int {
437+
return port.Port == int32(backend.ServicePort.IntValue())
438+
}
439+
440+
if backend.ServicePort.Type == intstr.String {
441+
return port.Name == backend.ServicePort.StrVal
442+
}
443+
return false
436444
}) {
437-
r.Log.Error(errors.New("port not found in service"), "Service", serviceNN, "port", backend.ServicePort.String())
445+
if backend.ServicePort.Type == intstr.Int {
446+
r.Log.Error(errors.New("port not found in service"), "Service", serviceNN, "port", backend.ServicePort.IntValue())
447+
} else {
448+
r.Log.Error(errors.New("named port not found in service"), "Service", serviceNN, "port", backend.ServicePort.StrVal)
449+
}
438450
return nil
439451
}
440452
tctx.Services[serviceNN] = &service

test/e2e/crds/v2/route.go

Lines changed: 93 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -60,8 +60,7 @@ var _ = Describe("Test ApisixRoute", Label("apisix.apache.org", "v2", "apisixrou
6060
})
6161

6262
Context("Test ApisixRoute", func() {
63-
64-
It("Basic tests", func() {
63+
Context("Basic tests", func() {
6564
const apisixRouteSpec = `
6665
apiVersion: apisix.apache.org/v2
6766
kind: ApisixRoute
@@ -81,59 +80,111 @@ spec:
8180
- serviceName: httpbin-service-e2e-test
8281
servicePort: 80
8382
`
84-
request := func(path string) int {
85-
return s.NewAPISIXClient().GET(path).WithHost("httpbin").Expect().Raw().StatusCode
86-
}
8783

88-
By("apply ApisixRoute")
89-
var apisixRoute apiv2.ApisixRoute
90-
applier.MustApplyAPIv2(types.NamespacedName{Namespace: s.Namespace(), Name: "default"},
91-
&apisixRoute, fmt.Sprintf(apisixRouteSpec, s.Namespace(), s.Namespace(), "/get"))
84+
const apisixRouteSpecWithNameServiceAndGranularity = `
85+
apiVersion: apisix.apache.org/v2
86+
kind: ApisixRoute
87+
metadata:
88+
name: default
89+
namespace: %s
90+
spec:
91+
ingressClassName: %s
92+
http:
93+
- name: rule0
94+
match:
95+
hosts:
96+
- httpbin
97+
paths:
98+
- %s
99+
backends:
100+
- serviceName: httpbin-service-e2e-test
101+
servicePort: http
102+
resolveGranularity: service
103+
`
92104

93-
By("verify ApisixRoute works")
94-
Eventually(request).WithArguments("/get").WithTimeout(20 * time.Second).ProbeEvery(time.Second).Should(Equal(http.StatusOK))
105+
const apisixRouteSpecWithNameServicePort = `
106+
apiVersion: apisix.apache.org/v2
107+
kind: ApisixRoute
108+
metadata:
109+
name: default
110+
namespace: %s
111+
spec:
112+
ingressClassName: %s
113+
http:
114+
- name: rule0
115+
match:
116+
hosts:
117+
- httpbin
118+
paths:
119+
- %s
120+
backends:
121+
- serviceName: httpbin-service-e2e-test
122+
servicePort: http
123+
`
124+
test := func(apisixRouteSpec string) {
125+
request := func(path string) int {
126+
return s.NewAPISIXClient().GET(path).WithHost("httpbin").Expect().Raw().StatusCode
127+
}
95128

96-
By("update ApisixRoute")
97-
applier.MustApplyAPIv2(types.NamespacedName{Namespace: s.Namespace(), Name: "default"},
98-
&apisixRoute, fmt.Sprintf(apisixRouteSpec, s.Namespace(), s.Namespace(), "/headers"))
99-
Eventually(request).WithArguments("/get").WithTimeout(20 * time.Second).ProbeEvery(time.Second).Should(Equal(http.StatusNotFound))
100-
s.NewAPISIXClient().GET("/headers").WithHost("httpbin").Expect().Status(http.StatusOK)
129+
By("apply ApisixRoute")
130+
var apisixRoute apiv2.ApisixRoute
131+
applier.MustApplyAPIv2(types.NamespacedName{Namespace: s.Namespace(), Name: "default"},
132+
&apisixRoute, fmt.Sprintf(apisixRouteSpec, s.Namespace(), s.Namespace(), "/get"))
101133

102-
By("delete ApisixRoute")
103-
err := s.DeleteResource("ApisixRoute", "default")
104-
Expect(err).ShouldNot(HaveOccurred(), "deleting ApisixRoute")
105-
Eventually(request).WithArguments("/headers").WithTimeout(20 * time.Second).ProbeEvery(time.Second).Should(Equal(http.StatusNotFound))
134+
By("verify ApisixRoute works")
135+
Eventually(request).WithArguments("/get").WithTimeout(20 * time.Second).ProbeEvery(time.Second).Should(Equal(http.StatusOK))
106136

107-
By("request /metrics endpoint from controller")
137+
By("update ApisixRoute")
138+
applier.MustApplyAPIv2(types.NamespacedName{Namespace: s.Namespace(), Name: "default"},
139+
&apisixRoute, fmt.Sprintf(apisixRouteSpec, s.Namespace(), s.Namespace(), "/headers"))
140+
Eventually(request).WithArguments("/get").WithTimeout(20 * time.Second).ProbeEvery(time.Second).Should(Equal(http.StatusNotFound))
141+
s.NewAPISIXClient().GET("/headers").WithHost("httpbin").Expect().Status(http.StatusOK)
108142

109-
// Get the metrics service endpoint
110-
metricsURL := s.GetMetricsEndpoint()
143+
By("delete ApisixRoute")
144+
err := s.DeleteResource("ApisixRoute", "default")
145+
Expect(err).ShouldNot(HaveOccurred(), "deleting ApisixRoute")
146+
Eventually(request).WithArguments("/headers").WithTimeout(20 * time.Second).ProbeEvery(time.Second).Should(Equal(http.StatusNotFound))
111147

112-
By("verify metrics content")
113-
resp, err := http.Get(metricsURL)
114-
Expect(err).ShouldNot(HaveOccurred(), "request metrics endpoint")
115-
defer func() {
116-
_ = resp.Body.Close()
117-
}()
148+
By("request /metrics endpoint from controller")
118149

119-
Expect(resp.StatusCode).Should(Equal(http.StatusOK))
150+
// Get the metrics service endpoint
151+
metricsURL := s.GetMetricsEndpoint()
152+
153+
By("verify metrics content")
154+
resp, err := http.Get(metricsURL)
155+
Expect(err).ShouldNot(HaveOccurred(), "request metrics endpoint")
156+
defer func() {
157+
_ = resp.Body.Close()
158+
}()
120159

121-
body, err := io.ReadAll(resp.Body)
122-
Expect(err).ShouldNot(HaveOccurred(), "read metrics response")
160+
Expect(resp.StatusCode).Should(Equal(http.StatusOK))
123161

124-
bodyStr := string(body)
162+
body, err := io.ReadAll(resp.Body)
163+
Expect(err).ShouldNot(HaveOccurred(), "read metrics response")
125164

126-
// Verify prometheus format
127-
Expect(resp.Header.Get("Content-Type")).Should(ContainSubstring("text/plain; version=0.0.4; charset=utf-8"))
165+
bodyStr := string(body)
128166

129-
// Verify specific metrics from metrics.go exist
130-
Expect(bodyStr).Should(ContainSubstring("apisix_ingress_adc_sync_duration_seconds"))
131-
Expect(bodyStr).Should(ContainSubstring("apisix_ingress_adc_sync_total"))
132-
Expect(bodyStr).Should(ContainSubstring("apisix_ingress_status_update_queue_length"))
133-
Expect(bodyStr).Should(ContainSubstring("apisix_ingress_file_io_duration_seconds"))
167+
// Verify prometheus format
168+
Expect(resp.Header.Get("Content-Type")).Should(ContainSubstring("text/plain; version=0.0.4; charset=utf-8"))
134169

135-
// Log metrics for debugging
136-
fmt.Printf("Metrics endpoint response:\n%s\n", bodyStr)
170+
// Verify specific metrics from metrics.go exist
171+
Expect(bodyStr).Should(ContainSubstring("apisix_ingress_adc_sync_duration_seconds"))
172+
Expect(bodyStr).Should(ContainSubstring("apisix_ingress_adc_sync_total"))
173+
Expect(bodyStr).Should(ContainSubstring("apisix_ingress_status_update_queue_length"))
174+
Expect(bodyStr).Should(ContainSubstring("apisix_ingress_file_io_duration_seconds"))
175+
176+
// Log metrics for debugging
177+
fmt.Printf("Metrics endpoint response:\n%s\n", bodyStr)
178+
}
179+
It("Basic", func() {
180+
test(apisixRouteSpec)
181+
})
182+
It("Basic: with named service port", func() {
183+
test(apisixRouteSpecWithNameServicePort)
184+
})
185+
It("Basic: with named service port and granularity service", func() {
186+
test(apisixRouteSpecWithNameServiceAndGranularity)
187+
})
137188
})
138189

139190
It("Test plugins in ApisixRoute", func() {

0 commit comments

Comments
 (0)