Skip to content

Commit 1b22891

Browse files
authored
feat: add support for named serviceport in ApisixRoute backend (#256)
1 parent de8abd5 commit 1b22891

File tree

4 files changed

+146
-44
lines changed

4 files changed

+146
-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

@@ -313,6 +314,26 @@ func (t *Translator) buildService(ar *apiv2.ApisixRoute, rule apiv2.ApisixRouteH
313314
return service
314315
}
315316

317+
func getPortFromService(svc *v1.Service, backendSvcPort intstr.IntOrString) (int32, error) {
318+
var port int32
319+
if backendSvcPort.Type == intstr.Int {
320+
port = int32(backendSvcPort.IntValue())
321+
} else {
322+
found := false
323+
for _, servicePort := range svc.Spec.Ports {
324+
if servicePort.Name == backendSvcPort.StrVal {
325+
port = servicePort.Port
326+
found = true
327+
break
328+
}
329+
}
330+
if !found {
331+
return 0, errors.Errorf("named port '%s' not found in service %s", backendSvcPort.StrVal, svc.Name)
332+
}
333+
}
334+
return port, nil
335+
}
336+
316337
func (t *Translator) translateApisixRouteBackendResolveGranularityService(tctx *provider.TranslateContext, arNN types.NamespacedName, backend apiv2.ApisixRouteHTTPBackend) (adc.UpstreamNodes, error) {
317338
serviceNN := types.NamespacedName{
318339
Namespace: arNN.Namespace,
@@ -325,10 +346,14 @@ func (t *Translator) translateApisixRouteBackendResolveGranularityService(tctx *
325346
if svc.Spec.ClusterIP == "" {
326347
return nil, errors.Errorf("conflict headless service and backend resolve granularity, ApisixRoute: %s, Service: %s", arNN, serviceNN)
327348
}
349+
port, err := getPortFromService(svc, backend.ServicePort)
350+
if err != nil {
351+
return nil, err
352+
}
328353
return adc.UpstreamNodes{
329354
{
330355
Host: svc.Spec.ClusterIP,
331-
Port: backend.ServicePort.IntValue(),
356+
Port: int(port),
332357
Weight: *cmp.Or(backend.Weight, ptr.To(apiv2.DefaultWeight)),
333358
},
334359
}, nil
@@ -349,14 +374,26 @@ func (t *Translator) translateApisixRouteStreamBackendResolveGranularity(tctx *p
349374
}
350375

351376
func (t *Translator) translateApisixRouteBackendResolveGranularityEndpoint(tctx *provider.TranslateContext, arNN types.NamespacedName, backend apiv2.ApisixRouteHTTPBackend) (adc.UpstreamNodes, error) {
377+
serviceNN := types.NamespacedName{
378+
Namespace: arNN.Namespace,
379+
Name: backend.ServiceName,
380+
}
381+
svc, ok := tctx.Services[serviceNN]
382+
if !ok {
383+
return nil, errors.Errorf("service not found, ApisixRoute: %s, Service: %s", arNN, serviceNN)
384+
}
385+
port, err := getPortFromService(svc, backend.ServicePort)
386+
if err != nil {
387+
return nil, err
388+
}
352389
weight := int32(*cmp.Or(backend.Weight, ptr.To(apiv2.DefaultWeight)))
353390
backendRef := gatewayv1.BackendRef{
354391
BackendObjectReference: gatewayv1.BackendObjectReference{
355392
Group: (*gatewayv1.Group)(&apiv2.GroupVersion.Group),
356393
Kind: (*gatewayv1.Kind)(ptr.To("Service")),
357394
Name: gatewayv1.ObjectName(backend.ServiceName),
358395
Namespace: (*gatewayv1.Namespace)(&arNN.Namespace),
359-
Port: (*gatewayv1.PortNumber)(&backend.ServicePort.IntVal),
396+
Port: (*gatewayv1.PortNumber)(&port),
360397
},
361398
Weight: &weight,
362399
}

internal/controller/apisixroute_controller.go

Lines changed: 14 additions & 2 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"
@@ -464,9 +465,20 @@ func (r *ApisixRouteReconciler) validateHTTPBackend(tctx *provider.TranslateCont
464465
}
465466

466467
if !slices.ContainsFunc(service.Spec.Ports, func(port corev1.ServicePort) bool {
467-
return port.Port == int32(backend.ServicePort.IntValue())
468+
if backend.ServicePort.Type == intstr.Int {
469+
return port.Port == int32(backend.ServicePort.IntValue())
470+
}
471+
472+
if backend.ServicePort.Type == intstr.String {
473+
return port.Name == backend.ServicePort.StrVal
474+
}
475+
return false
468476
}) {
469-
r.Log.Error(errors.New("port not found in service"), "Service", serviceNN, "port", backend.ServicePort.String())
477+
if backend.ServicePort.Type == intstr.Int {
478+
r.Log.Error(errors.New("port not found in service"), "Service", serviceNN, "port", backend.ServicePort.IntValue())
479+
} else {
480+
r.Log.Error(errors.New("named port not found in service"), "Service", serviceNN, "port", backend.ServicePort.StrVal)
481+
}
470482
return nil
471483
}
472484
tctx.Services[serviceNN] = &service

test/e2e/crds/v2/route.go

Lines changed: 91 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,108 @@ 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+
It("Basic", func() {
178+
test(apisixRouteSpec)
179+
})
180+
It("Basic: with named service port", func() {
181+
test(apisixRouteSpecWithNameServicePort)
182+
})
183+
It("Basic: with named service port and granularity service", func() {
184+
test(apisixRouteSpecWithNameServiceAndGranularity)
185+
})
134186
})
135187

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

test/e2e/scaffold/httpbin.go

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -83,7 +83,8 @@ spec:
8383
selector:
8484
app: httpbin-deployment-e2e-test
8585
ports:
86-
- port: 80
86+
- name: http
87+
port: 80
8788
protocol: TCP
8889
targetPort: 80
8990
type: ClusterIP

0 commit comments

Comments
 (0)