Skip to content

Commit 61c6f5b

Browse files
committed
feat: add support for named serviceport in ApisixRoute backend
1 parent bf426e4 commit 61c6f5b

File tree

3 files changed

+148
-44
lines changed

3 files changed

+148
-44
lines changed

internal/adc/translator/apisixroute.go

Lines changed: 39 additions & 2 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

@@ -305,6 +306,26 @@ func (t *Translator) buildService(ar *apiv2.ApisixRoute, rule apiv2.ApisixRouteH
305306
return service
306307
}
307308

309+
func getPortFromService(svc *v1.Service, backendSvcPort intstr.IntOrString) (int32, error) {
310+
var port int32
311+
if backendSvcPort.Type == intstr.Int {
312+
port = int32(backendSvcPort.IntValue())
313+
} else {
314+
found := false
315+
for _, servicePort := range svc.Spec.Ports {
316+
if servicePort.Name == backendSvcPort.StrVal {
317+
port = servicePort.Port
318+
found = true
319+
break
320+
}
321+
}
322+
if !found {
323+
return 0, errors.Errorf("named port '%s' not found in service %s", backendSvcPort.StrVal, svc.Name)
324+
}
325+
}
326+
return port, nil
327+
}
328+
308329
func (t *Translator) translateApisixRouteBackendResolveGranularityService(tctx *provider.TranslateContext, arNN types.NamespacedName, backend apiv2.ApisixRouteHTTPBackend) (adc.UpstreamNodes, error) {
309330
serviceNN := types.NamespacedName{
310331
Namespace: arNN.Namespace,
@@ -317,24 +338,40 @@ func (t *Translator) translateApisixRouteBackendResolveGranularityService(tctx *
317338
if svc.Spec.ClusterIP == "" {
318339
return nil, errors.Errorf("conflict headless service and backend resolve granularity, ApisixRoute: %s, Service: %s", arNN, serviceNN)
319340
}
341+
port, err := getPortFromService(svc, backend.ServicePort)
342+
if err != nil {
343+
return nil, err
344+
}
320345
return adc.UpstreamNodes{
321346
{
322347
Host: svc.Spec.ClusterIP,
323-
Port: backend.ServicePort.IntValue(),
348+
Port: int(port),
324349
Weight: *cmp.Or(backend.Weight, ptr.To(apiv2.DefaultWeight)),
325350
},
326351
}, nil
327352
}
328353

329354
func (t *Translator) translateApisixRouteBackendResolveGranularityEndpoint(tctx *provider.TranslateContext, arNN types.NamespacedName, backend apiv2.ApisixRouteHTTPBackend) (adc.UpstreamNodes, error) {
355+
serviceNN := types.NamespacedName{
356+
Namespace: arNN.Namespace,
357+
Name: backend.ServiceName,
358+
}
359+
svc, ok := tctx.Services[serviceNN]
360+
if !ok {
361+
return nil, errors.Errorf("service not found, ApisixRoute: %s, Service: %s", arNN, serviceNN)
362+
}
363+
port, err := getPortFromService(svc, backend.ServicePort)
364+
if err != nil {
365+
return nil, err
366+
}
330367
weight := int32(*cmp.Or(backend.Weight, ptr.To(apiv2.DefaultWeight)))
331368
backendRef := gatewayv1.BackendRef{
332369
BackendObjectReference: gatewayv1.BackendObjectReference{
333370
Group: (*gatewayv1.Group)(&apiv2.GroupVersion.Group),
334371
Kind: (*gatewayv1.Kind)(ptr.To("Service")),
335372
Name: gatewayv1.ObjectName(backend.ServiceName),
336373
Namespace: (*gatewayv1.Namespace)(&arNN.Namespace),
337-
Port: (*gatewayv1.PortNumber)(&backend.ServicePort.IntVal),
374+
Port: (*gatewayv1.PortNumber)(&port),
338375
},
339376
Weight: &weight,
340377
}

internal/controller/apisixroute_controller.go

Lines changed: 15 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@ import (
3636
"k8s.io/apimachinery/pkg/runtime"
3737
"k8s.io/apimachinery/pkg/runtime/schema"
3838
k8stypes "k8s.io/apimachinery/pkg/types"
39+
"k8s.io/apimachinery/pkg/util/intstr"
3940
ctrl "sigs.k8s.io/controller-runtime"
4041
"sigs.k8s.io/controller-runtime/pkg/builder"
4142
"sigs.k8s.io/controller-runtime/pkg/client"
@@ -417,10 +418,21 @@ func (r *ApisixRouteReconciler) validateBackends(ctx context.Context, tc *provid
417418
}
418419

419420
if !slices.ContainsFunc(service.Spec.Ports, func(port corev1.ServicePort) bool {
420-
return port.Port == int32(backend.ServicePort.IntValue())
421+
if backend.ServicePort.Type == intstr.Int {
422+
return port.Port == int32(backend.ServicePort.IntValue())
423+
}
424+
425+
if backend.ServicePort.Type == intstr.String {
426+
return port.Name == backend.ServicePort.StrVal
427+
}
428+
return false
421429
}) {
422-
r.Log.Error(errors.New("port not found in service"), "Service", serviceNN, "port", backend.ServicePort.String())
423-
continue
430+
if backend.ServicePort.Type == intstr.Int {
431+
r.Log.Error(errors.New("port not found in service"), "Service", serviceNN, "port", backend.ServicePort.IntValue())
432+
} else {
433+
r.Log.Error(errors.New("named port not found in service"), "Service", serviceNN, "port", backend.ServicePort.StrVal)
434+
}
435+
return nil
424436
}
425437
tc.Services[serviceNN] = &service
426438

test/e2e/crds/v2/route.go

Lines changed: 94 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -61,7 +61,7 @@ var _ = Describe("Test ApisixRoute", Label("apisix.apache.org", "v2", "apisixrou
6161

6262
Context("Test ApisixRoute", func() {
6363

64-
It("Basic tests", func() {
64+
Context("Basic tests", func() {
6565
const apisixRouteSpec = `
6666
apiVersion: apisix.apache.org/v2
6767
kind: ApisixRoute
@@ -81,56 +81,111 @@ spec:
8181
- serviceName: httpbin-service-e2e-test
8282
servicePort: 80
8383
`
84-
request := func(path string) int {
85-
return s.NewAPISIXClient().GET(path).WithHost("httpbin").Expect().Raw().StatusCode
86-
}
8784

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"))
85+
const apisixRouteSpecWithNameServiceAndGranularity = `
86+
apiVersion: apisix.apache.org/v2
87+
kind: ApisixRoute
88+
metadata:
89+
name: default
90+
namespace: %s
91+
spec:
92+
ingressClassName: %s
93+
http:
94+
- name: rule0
95+
match:
96+
hosts:
97+
- httpbin
98+
paths:
99+
- %s
100+
backends:
101+
- serviceName: httpbin-service-e2e-test
102+
servicePort: http
103+
resolveGranularity: service
104+
`
92105

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

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)
130+
By("apply ApisixRoute")
131+
var apisixRoute apiv2.ApisixRoute
132+
applier.MustApplyAPIv2(types.NamespacedName{Namespace: s.Namespace(), Name: "default"},
133+
&apisixRoute, fmt.Sprintf(apisixRouteSpec, s.Namespace(), s.Namespace(), "/get"))
101134

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))
135+
By("verify ApisixRoute works")
136+
Eventually(request).WithArguments("/get").WithTimeout(20 * time.Second).ProbeEvery(time.Second).Should(Equal(http.StatusOK))
106137

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

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

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-
}()
149+
By("request /metrics endpoint from controller")
118150

119-
Expect(resp.StatusCode).Should(Equal(http.StatusOK))
151+
// Get the metrics service endpoint
152+
metricsURL := s.GetMetricsEndpoint()
120153

121-
body, err := io.ReadAll(resp.Body)
122-
Expect(err).ShouldNot(HaveOccurred(), "read metrics response")
154+
By("verify metrics content")
155+
resp, err := http.Get(metricsURL)
156+
Expect(err).ShouldNot(HaveOccurred(), "request metrics endpoint")
157+
defer func() {
158+
_ = resp.Body.Close()
159+
}()
123160

124-
bodyStr := string(body)
161+
Expect(resp.StatusCode).Should(Equal(http.StatusOK))
125162

126-
// Verify prometheus format
127-
Expect(resp.Header.Get("Content-Type")).Should(ContainSubstring("text/plain; version=0.0.4; charset=utf-8"))
163+
body, err := io.ReadAll(resp.Body)
164+
Expect(err).ShouldNot(HaveOccurred(), "read metrics response")
128165

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"))
166+
bodyStr := string(body)
167+
168+
// Verify prometheus format
169+
Expect(resp.Header.Get("Content-Type")).Should(ContainSubstring("text/plain; version=0.0.4; charset=utf-8"))
170+
171+
// Verify specific metrics from metrics.go exist
172+
Expect(bodyStr).Should(ContainSubstring("apisix_ingress_adc_sync_duration_seconds"))
173+
Expect(bodyStr).Should(ContainSubstring("apisix_ingress_adc_sync_total"))
174+
Expect(bodyStr).Should(ContainSubstring("apisix_ingress_status_update_queue_length"))
175+
Expect(bodyStr).Should(ContainSubstring("apisix_ingress_file_io_duration_seconds"))
176+
177+
// Log metrics for debugging
178+
fmt.Printf("Metrics endpoint response:\n%s\n", bodyStr)
179+
}
180+
It("Basic", func() {
181+
test(apisixRouteSpec)
182+
})
183+
It("Basic: with named service port", func() {
184+
test(apisixRouteSpecWithNameServicePort)
185+
})
186+
It("Basic: with named service port and granularity service", func() {
187+
test(apisixRouteSpecWithNameServiceAndGranularity)
188+
})
134189
})
135190

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

0 commit comments

Comments
 (0)