Skip to content

Commit ad3e227

Browse files
committed
feat: support apisixroute resolveGranularity
1 parent 6e87cc6 commit ad3e227

File tree

4 files changed

+139
-37
lines changed

4 files changed

+139
-37
lines changed

internal/controller/apisixroute_controller.go

Lines changed: 8 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ package controller
1515
import (
1616
"cmp"
1717
"context"
18+
"errors"
1819
"fmt"
1920
"slices"
2021

@@ -209,9 +210,9 @@ func (r *ApisixRouteReconciler) processApisixRoute(ctx context.Context, tc *prov
209210
backends[serviceNN] = struct{}{}
210211

211212
if err := r.Get(ctx, serviceNN, &service); err != nil {
212-
return ReasonError{
213-
Reason: string(apiv2.ConditionReasonInvalidSpec),
214-
Message: fmt.Sprintf("failed to get Service: %s", serviceNN),
213+
if err := client.IgnoreNotFound(err); err == nil {
214+
r.Log.Error(errors.New("service not found"), "Service", serviceNN)
215+
continue
215216
}
216217
}
217218
if service.Spec.Type == corev1.ServiceTypeExternalName {
@@ -220,19 +221,15 @@ func (r *ApisixRouteReconciler) processApisixRoute(ctx context.Context, tc *prov
220221
}
221222

222223
if backend.ResolveGranularity == "service" && service.Spec.ClusterIP == "" {
223-
return ReasonError{
224-
Reason: string(apiv2.ConditionReasonInvalidSpec),
225-
Message: fmt.Sprintf("service %s has no cluster IP", serviceNN),
226-
}
224+
r.Log.Error(errors.New("service has no ClusterIP"), "Service", serviceNN, "ResolveGranularity", backend.ResolveGranularity)
225+
continue
227226
}
228227

229228
if !slices.ContainsFunc(service.Spec.Ports, func(port corev1.ServicePort) bool {
230229
return port.Port == int32(backend.ServicePort.IntValue())
231230
}) {
232-
return ReasonError{
233-
Reason: string(apiv2.ConditionReasonInvalidSpec),
234-
Message: fmt.Sprintf("port %s not found in service %s", backend.ServicePort.String(), serviceNN),
235-
}
231+
r.Log.Error(errors.New("port not found in service"), "Service", serviceNN, "port", backend.ServicePort.String())
232+
continue
236233
}
237234
tc.Services[serviceNN] = &service
238235

internal/provider/adc/translator/apisixroute.go

Lines changed: 57 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ import (
1717
"encoding/json"
1818
"fmt"
1919

20+
"github.com/pkg/errors"
2021
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
2122
"k8s.io/apimachinery/pkg/types"
2223
"k8s.io/utils/ptr"
@@ -26,8 +27,9 @@ import (
2627
apiv2 "github.com/apache/apisix-ingress-controller/api/v2"
2728
"github.com/apache/apisix-ingress-controller/internal/controller/label"
2829
"github.com/apache/apisix-ingress-controller/internal/provider"
30+
"github.com/apache/apisix-ingress-controller/internal/utils"
2931
"github.com/apache/apisix-ingress-controller/pkg/id"
30-
"github.com/apache/apisix-ingress-controller/pkg/utils"
32+
pkgutils "github.com/apache/apisix-ingress-controller/pkg/utils"
3133
)
3234

3335
func (t *Translator) TranslateApisixRoute(tctx *provider.TranslateContext, ar *apiv2.ApisixRoute) (result *TranslateResult, err error) {
@@ -58,7 +60,7 @@ func (t *Translator) TranslateApisixRoute(tctx *provider.TranslateContext, ar *a
5860
if plugin.SecretRef != "" {
5961
if secret, ok := tctx.Secrets[types.NamespacedName{Namespace: ar.Namespace, Name: plugin.SecretRef}]; ok {
6062
for key, value := range secret.Data {
61-
utils.InsertKeyInMap(key, string(value), config)
63+
pkgutils.InsertKeyInMap(key, string(value), config)
6264
}
6365
}
6466
}
@@ -115,25 +117,26 @@ func (t *Translator) TranslateApisixRoute(tctx *provider.TranslateContext, ar *a
115117
// translate to adc.Upstream
116118
var backendErr error
117119
for _, backend := range rule.Backends {
118-
weight := int32(*cmp.Or(backend.Weight, ptr.To(apiv2.DefaultWeight)))
119-
backendRef := gatewayv1.BackendRef{
120-
BackendObjectReference: gatewayv1.BackendObjectReference{
121-
Group: (*gatewayv1.Group)(&apiv2.GroupVersion.Group),
122-
Kind: (*gatewayv1.Kind)(ptr.To("Service")),
123-
Name: gatewayv1.ObjectName(backend.ServiceName),
124-
Namespace: (*gatewayv1.Namespace)(&ar.Namespace),
125-
Port: (*gatewayv1.PortNumber)(&backend.ServicePort.IntVal),
126-
},
127-
Weight: &weight,
128-
}
129-
upNodes, err := t.translateBackendRef(tctx, backendRef)
130-
if err != nil {
131-
backendErr = err
132-
continue
120+
var (
121+
upNodes adc.UpstreamNodes
122+
)
123+
if backend.ResolveGranularity == "service" {
124+
upNodes, backendErr = t.translateApisixBackendResolveGranularityService(tctx, utils.NamespacedName(ar), backend)
125+
if backendErr != nil {
126+
t.Log.Error(backendErr, "failed to translate ApisixRoute backend with ResolveGranularity Service")
127+
continue
128+
}
129+
} else {
130+
upNodes, backendErr = t.translateApisixBackendResolveGranularityEndpint(tctx, utils.NamespacedName(ar), backend)
131+
if backendErr != nil {
132+
t.Log.Error(backendErr, "failed to translate ApisixRoute backend with ResolveGranularity Service")
133+
continue
134+
}
133135
}
134-
t.AttachBackendTrafficPolicyToUpstream(backendRef, tctx.BackendTrafficPolicies, upstream)
136+
135137
upstream.Nodes = append(upstream.Nodes, upNodes...)
136138
}
139+
137140
//nolint:staticcheck
138141
if len(rule.Backends) == 0 && len(rule.Upstreams) > 0 {
139142
// FIXME: when the API ApisixUpstream is supported
@@ -164,3 +167,39 @@ func (t *Translator) TranslateApisixRoute(tctx *provider.TranslateContext, ar *a
164167

165168
return result, nil
166169
}
170+
171+
func (t *Translator) translateApisixBackendResolveGranularityService(tctx *provider.TranslateContext, arNN types.NamespacedName, backend apiv2.ApisixRouteHTTPBackend) (adc.UpstreamNodes, error) {
172+
serviceNN := types.NamespacedName{
173+
Namespace: arNN.Namespace,
174+
Name: backend.ServiceName,
175+
}
176+
svc, ok := tctx.Services[serviceNN]
177+
if !ok {
178+
return nil, errors.Errorf("service not found, ApisixRoute: %s, Service: %s", arNN, serviceNN)
179+
}
180+
if svc.Spec.ClusterIP == "" {
181+
return nil, errors.Errorf("conflict headless service and backend resolve granularity, Apisixroute: %s, Service: %s", arNN, serviceNN)
182+
}
183+
return adc.UpstreamNodes{
184+
{
185+
Host: svc.Spec.ClusterIP,
186+
Port: backend.ServicePort.IntValue(),
187+
Weight: *cmp.Or(backend.Weight, ptr.To(apiv2.DefaultWeight)),
188+
},
189+
}, nil
190+
}
191+
192+
func (t *Translator) translateApisixBackendResolveGranularityEndpint(tctx *provider.TranslateContext, arNN types.NamespacedName, backend apiv2.ApisixRouteHTTPBackend) (adc.UpstreamNodes, error) {
193+
weight := int32(*cmp.Or(backend.Weight, ptr.To(apiv2.DefaultWeight)))
194+
backendRef := gatewayv1.BackendRef{
195+
BackendObjectReference: gatewayv1.BackendObjectReference{
196+
Group: (*gatewayv1.Group)(&apiv2.GroupVersion.Group),
197+
Kind: (*gatewayv1.Kind)(ptr.To("Service")),
198+
Name: gatewayv1.ObjectName(backend.ServiceName),
199+
Namespace: (*gatewayv1.Namespace)(&arNN.Namespace),
200+
Port: (*gatewayv1.PortNumber)(&backend.ServicePort.IntVal),
201+
},
202+
Weight: &weight,
203+
}
204+
return t.translateBackendRef(tctx, backendRef)
205+
}

test/e2e/apisix/route.go

Lines changed: 74 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ package apisix
1414

1515
import (
1616
"fmt"
17+
"net"
1718
"net/http"
1819
"time"
1920

@@ -213,12 +214,80 @@ spec:
213214
s.NewAPISIXClient().GET("/get").Expect().Status(http.StatusNotFound)
214215
})
215216

216-
PIt("Test ApisixRoute resolveGranularity", func() {
217-
// The `.Spec.HTTP[0].Backends[0].ResolveGranularity` can be "endpoints" or "service",
218-
// when set to "endpoints", the pod ips will be used; or the service ClusterIP or ExternalIP will be used when it set to "service",
217+
It("Test ApisixRoute service not found", func() {
218+
const apisixRouteSpec = `
219+
apiVersion: apisix.apache.org/v2
220+
kind: ApisixRoute
221+
metadata:
222+
name: default
223+
spec:
224+
ingressClassName: apisix
225+
http:
226+
- name: rule0
227+
match:
228+
hosts:
229+
- httpbin
230+
paths:
231+
- %s
232+
backends:
233+
- serviceName: service-not-found
234+
servicePort: 80
235+
`
236+
request := func(path string) int {
237+
return s.NewAPISIXClient().GET(path).WithHost("httpbin").Expect().Raw().StatusCode
238+
}
219239

220-
// In the current implementation, pod ips are always used.
221-
// So the case is pending for now.
240+
By("apply ApisixRoute")
241+
var apisixRoute apiv2.ApisixRoute
242+
applier.MustApplyAPIv2(types.NamespacedName{Namespace: s.Namespace(), Name: "default"}, &apisixRoute, fmt.Sprintf(apisixRouteSpec, "/get"))
243+
244+
By("when there is no replica got 500 by fault-injection")
245+
err := s.ScaleHTTPBIN(0)
246+
Expect(err).ShouldNot(HaveOccurred(), "scale httpbin to 0")
247+
Eventually(request).WithArguments("/get").WithTimeout(8 * time.Second).ProbeEvery(time.Second).Should(Equal(http.StatusInternalServerError))
248+
s.NewAPISIXClient().GET("/get").WithHost("httpbin").Expect().Body().IsEqual("No existing backendRef provided")
249+
})
250+
251+
It("Test ApisixRoute resolveGranularity", func() {
252+
const apisixRouteSpec = `
253+
apiVersion: apisix.apache.org/v2
254+
kind: ApisixRoute
255+
metadata:
256+
name: default
257+
spec:
258+
ingressClassName: apisix
259+
http:
260+
- name: rule0
261+
match:
262+
paths:
263+
- /*
264+
backends:
265+
- serviceName: httpbin-service-e2e-test
266+
servicePort: 80
267+
resolveGranularity: service
268+
plugins:
269+
- name: response-rewrite
270+
enable: true
271+
config:
272+
headers:
273+
set:
274+
"X-Upstream-IP": "$upstream_addr"
275+
`
276+
By("apply ApisixRoute")
277+
var apisixRoute apiv2.ApisixRoute
278+
applier.MustApplyAPIv2(types.NamespacedName{Namespace: s.Namespace(), Name: "default"}, &apisixRoute, apisixRouteSpec)
279+
280+
By("verify ApisixRoute works")
281+
request := func() int {
282+
return s.NewAPISIXClient().GET("/get").Expect().Raw().StatusCode
283+
}
284+
Eventually(request).WithTimeout(8 * time.Second).ProbeEvery(time.Second).Should(Equal(http.StatusOK))
285+
286+
By("assert that the request is proxied to the Service ClusterIP")
287+
service, err := s.GetServiceByName("httpbin-service-e2e-test")
288+
Expect(err).ShouldNot(HaveOccurred(), "get service")
289+
clusterIP := net.JoinHostPort(service.Spec.ClusterIP, "80")
290+
s.NewAPISIXClient().GET("/get").Expect().Header("X-Upstream-IP").IsEqual(clusterIP)
222291
})
223292

224293
PIt("Test ApisixRoute subset", func() {

test/e2e/framework/assertion.go

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -99,9 +99,6 @@ func PollUntilHTTPRoutePolicyHaveStatus(cli client.Client, timeout time.Duration
9999

100100
func APIv2MustHaveCondition(t testing.TestingT, cli client.Client, timeout time.Duration, nn types.NamespacedName, obj client.Object, cond metav1.Condition) {
101101
f := func(object client.Object) bool {
102-
if !apiv2.Is(object) {
103-
return false
104-
}
105102
value := reflect.Indirect(reflect.ValueOf(object))
106103
status, ok := value.FieldByName("Status").Interface().(apiv2.ApisixStatus)
107104
if !ok {

0 commit comments

Comments
 (0)