Skip to content

Commit ef33517

Browse files
committed
fix: apisixroute backend service reference to apisixupstream (#2453)
(cherry picked from commit 76467f4)
1 parent a810bbb commit ef33517

File tree

4 files changed

+130
-71
lines changed

4 files changed

+130
-71
lines changed

internal/controller/apisixroute_controller.go

Lines changed: 29 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,9 @@ import (
2424
"fmt"
2525
"slices"
2626

27+
"github.com/api7/gopkg/pkg/log"
2728
"github.com/go-logr/logr"
29+
"go.uber.org/zap"
2830
corev1 "k8s.io/api/core/v1"
2931
discoveryv1 "k8s.io/api/discovery/v1"
3032
networkingv1 "k8s.io/api/networking/v1"
@@ -283,13 +285,12 @@ func (r *ApisixRouteReconciler) validateBackends(ctx context.Context, tc *provid
283285
var backends = make(map[types.NamespacedName]struct{})
284286
for _, backend := range http.Backends {
285287
var (
286-
service = corev1.Service{
287-
ObjectMeta: metav1.ObjectMeta{
288-
Name: backend.ServiceName,
289-
Namespace: in.Namespace,
290-
},
288+
au apiv2.ApisixUpstream
289+
service corev1.Service
290+
serviceNN = types.NamespacedName{
291+
Namespace: in.GetNamespace(),
292+
Name: backend.ServiceName,
291293
}
292-
serviceNN = utils.NamespacedName(&service)
293294
)
294295
if _, ok := backends[serviceNN]; ok {
295296
return ReasonError{
@@ -300,12 +301,24 @@ func (r *ApisixRouteReconciler) validateBackends(ctx context.Context, tc *provid
300301
backends[serviceNN] = struct{}{}
301302

302303
if err := r.Get(ctx, serviceNN, &service); err != nil {
303-
if err := client.IgnoreNotFound(err); err == nil {
304+
if err = client.IgnoreNotFound(err); err == nil {
304305
r.Log.Error(errors.New("service not found"), "Service", serviceNN)
305306
continue
306307
}
307308
return err
308309
}
310+
311+
// try to get apisixupstream with the same name as the backend service
312+
log.Debugw("try to get apisixupstream with the same name as the backend service", zap.Stringer("Service", serviceNN))
313+
if err := r.Get(ctx, serviceNN, &au); err != nil {
314+
log.Debugw("no ApisixUpstream with the same name as the backend service found", zap.Stringer("Service", serviceNN), zap.Error(err))
315+
if err = client.IgnoreNotFound(err); err != nil {
316+
return err
317+
}
318+
} else {
319+
tc.Upstreams[serviceNN] = &au
320+
}
321+
309322
if service.Spec.Type == corev1.ServiceTypeExternalName {
310323
tc.Services[serviceNN] = &service
311324
continue
@@ -339,11 +352,7 @@ func (r *ApisixRouteReconciler) validateBackends(ctx context.Context, tc *provid
339352

340353
// backend.subset specifies a subset of upstream nodes.
341354
// It specifies that the target pod's label should be a superset of the subset labels of the ApisixUpstream of the serviceName
342-
subsetLabels, err := r.getSubsetLabels(ctx, in, backend)
343-
if err != nil {
344-
return err
345-
}
346-
355+
subsetLabels := r.getSubsetLabels(tc, serviceNN, backend)
347356
tc.EndpointSlices[serviceNN] = r.filterEndpointSlicesBySubsetLabels(ctx, endpoints.Items, subsetLabels)
348357
}
349358

@@ -512,7 +521,7 @@ func (r *ApisixRouteReconciler) listApisixRouteForApisixUpstream(ctx context.Con
512521

513522
var arList apiv2.ApisixRouteList
514523
if err := r.List(ctx, &arList, client.MatchingFields{indexer.ApisixUpstreamRef: indexer.GenIndexKey(au.GetNamespace(), au.GetName())}); err != nil {
515-
r.Log.Error(err, "failed to list ApisixUpstreams")
524+
r.Log.Error(err, "failed to list ApisixRoutes")
516525
return nil
517526
}
518527

@@ -569,35 +578,24 @@ func (r *ApisixRouteReconciler) listApisixRoutesForPluginConfig(ctx context.Cont
569578
return pkgutils.DedupComparable(requests)
570579
}
571580

572-
func (r *ApisixRouteReconciler) getSubsetLabels(ctx context.Context, ar *apiv2.ApisixRoute, backend apiv2.ApisixRouteHTTPBackend) (map[string]string, error) {
573-
empty := make(map[string]string)
581+
func (r *ApisixRouteReconciler) getSubsetLabels(tctx *provider.TranslateContext, auNN types.NamespacedName, backend apiv2.ApisixRouteHTTPBackend) map[string]string {
574582
if backend.Subset == "" {
575-
return empty, nil
583+
return nil
576584
}
577585

578-
// Try to Get the ApisixUpstream with the same name as backend.ServiceName
579-
var (
580-
auNN = types.NamespacedName{
581-
Namespace: ar.GetNamespace(),
582-
Name: backend.ServiceName,
583-
}
584-
au apiv2.ApisixUpstream
585-
)
586-
if err := r.Get(ctx, auNN, &au); err != nil {
587-
if client.IgnoreNotFound(err) == nil {
588-
return empty, nil
589-
}
590-
return nil, err
586+
au, ok := tctx.Upstreams[auNN]
587+
if !ok {
588+
return nil
591589
}
592590

593591
// try to get the subset labels from the ApisixUpstream subsets
594592
for _, subset := range au.Spec.Subsets {
595593
if backend.Subset == subset.Name {
596-
return subset.Labels, nil
594+
return subset.Labels
597595
}
598596
}
599597

600-
return empty, nil
598+
return nil
601599
}
602600

603601
func (r *ApisixRouteReconciler) filterEndpointSlicesBySubsetLabels(ctx context.Context, in []discoveryv1.EndpointSlice, labels map[string]string) []discoveryv1.EndpointSlice {

internal/controller/indexer/indexer.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -568,7 +568,7 @@ func ApisixRouteApisixUpstreamIndexFunc(obj client.Object) (keys []string) {
568568
ar := obj.(*apiv2.ApisixRoute)
569569
for _, rule := range ar.Spec.HTTP {
570570
for _, backend := range rule.Backends {
571-
if backend.Subset != "" && backend.ServiceName != "" {
571+
if backend.ServiceName != "" {
572572
keys = append(keys, GenIndexKey(ar.GetNamespace(), backend.ServiceName))
573573
}
574574
}

internal/provider/adc/translator/apisixroute.go

Lines changed: 30 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -195,28 +195,35 @@ func (t *Translator) buildRoute(ar *apiv2.ApisixRoute, service *adc.Service, rul
195195

196196
func (t *Translator) buildUpstream(tctx *provider.TranslateContext, service *adc.Service, ar *apiv2.ApisixRoute, rule apiv2.ApisixRouteHTTP) {
197197
var (
198-
upstream = adc.NewDefaultUpstream()
199198
upstreams = make([]*adc.Upstream, 0)
200199
weightedUpstreams = make([]adc.TrafficSplitConfigRuleWeightedUpstream, 0)
201200
backendErr error
202201
)
203202

204203
for _, backend := range rule.Backends {
205-
var upNodes adc.UpstreamNodes
204+
upstream := adc.NewDefaultUpstream()
205+
// try to get the apisixupstream with the same name as the backend service to be upstream config.
206+
// err is ignored because it does not care about the externalNodes of the apisixupstream.
207+
auNN := types.NamespacedName{Namespace: ar.GetNamespace(), Name: backend.ServiceName}
208+
if au, ok := tctx.Upstreams[auNN]; ok {
209+
upstream, _ = t.translateApisixUpstream(tctx, au)
210+
}
211+
206212
if backend.ResolveGranularity == "service" {
207-
upNodes, backendErr = t.translateApisixRouteBackendResolveGranularityService(tctx, utils.NamespacedName(ar), backend)
213+
upstream.Nodes, backendErr = t.translateApisixRouteBackendResolveGranularityService(tctx, utils.NamespacedName(ar), backend)
208214
if backendErr != nil {
209215
t.Log.Error(backendErr, "failed to translate ApisixRoute backend with ResolveGranularity Service")
210216
continue
211217
}
212218
} else {
213-
upNodes, backendErr = t.translateApisixRouteBackendResolveGranularityEndpoint(tctx, utils.NamespacedName(ar), backend)
219+
upstream.Nodes, backendErr = t.translateApisixRouteBackendResolveGranularityEndpoint(tctx, utils.NamespacedName(ar), backend)
214220
if backendErr != nil {
215221
t.Log.Error(backendErr, "failed to translate ApisixRoute backend with ResolveGranularity Endpoint")
216222
continue
217223
}
218224
}
219-
upstream.Nodes = append(upstream.Nodes, upNodes...)
225+
226+
upstreams = append(upstreams, upstream)
220227
}
221228

222229
for _, upstreamRef := range rule.Upstreams {
@@ -241,21 +248,26 @@ func (t *Translator) buildUpstream(tctx *provider.TranslateContext, service *adc
241248
upstreams = append(upstreams, upstream)
242249
}
243250

244-
// If no .http[].backends is used and only .http[].upstreams is used, the first valid upstream is used as service.upstream;
245-
// Other upstreams are configured in the traffic-split plugin
246-
if len(rule.Backends) == 0 && len(upstreams) > 0 {
247-
upstream = upstreams[0]
248-
upstreams = upstreams[1:]
251+
// no valid upstream
252+
if backendErr != nil || len(upstreams) == 0 || len(upstreams[0].Nodes) == 0 {
253+
return
249254
}
250255

251-
// set the default upstream's weight in traffic-split
252-
weight, err := strconv.Atoi(upstream.Labels["meta_weight"])
253-
if err != nil {
254-
weight = apiv2.DefaultWeight
256+
// the first valid upstream is used as service.upstream;
257+
// the others are configured in the traffic-split plugin
258+
service.Upstream = upstreams[0]
259+
upstreams = upstreams[1:]
260+
261+
// set weight in traffic-split for the default upstream
262+
if len(upstreams) > 0 {
263+
weight, err := strconv.Atoi(service.Upstream.Labels["meta_weight"])
264+
if err != nil {
265+
weight = apiv2.DefaultWeight
266+
}
267+
weightedUpstreams = append(weightedUpstreams, adc.TrafficSplitConfigRuleWeightedUpstream{
268+
Weight: weight,
269+
})
255270
}
256-
weightedUpstreams = append(weightedUpstreams, adc.TrafficSplitConfigRuleWeightedUpstream{
257-
Weight: weight,
258-
})
259271

260272
// set others upstreams in traffic-split
261273
for _, item := range upstreams {
@@ -269,11 +281,6 @@ func (t *Translator) buildUpstream(tctx *provider.TranslateContext, service *adc
269281
})
270282
}
271283

272-
// set service
273-
service.Upstream = upstream
274-
if backendErr != nil && len(upstream.Nodes) == 0 {
275-
t.addFaultInjectionPlugin(service)
276-
}
277284
if len(weightedUpstreams) > 0 {
278285
service.Plugins["traffic-split"] = &adc.TrafficSplitConfig{
279286
Rules: []adc.TrafficSplitConfigRule{
@@ -291,21 +298,10 @@ func (t *Translator) buildService(ar *apiv2.ApisixRoute, rule apiv2.ApisixRouteH
291298
service.ID = id.GenID(service.Name)
292299
service.Labels = label.GenLabel(ar)
293300
service.Hosts = rule.Match.Hosts
301+
service.Upstream = adc.NewDefaultUpstream()
294302
return service
295303
}
296304

297-
func (t *Translator) addFaultInjectionPlugin(service *adc.Service) {
298-
if service.Plugins == nil {
299-
service.Plugins = make(map[string]any)
300-
}
301-
service.Plugins["fault-injection"] = map[string]any{
302-
"abort": map[string]any{
303-
"http_status": 500,
304-
"body": "No existing backendRef provided",
305-
},
306-
}
307-
}
308-
309305
func (t *Translator) translateApisixRouteBackendResolveGranularityService(tctx *provider.TranslateContext, arNN types.NamespacedName, backend apiv2.ApisixRouteHTTPBackend) (adc.UpstreamNodes, error) {
310306
serviceNN := types.NamespacedName{
311307
Namespace: arNN.Namespace,

test/e2e/apisix/route.go

Lines changed: 70 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@
1818
package apisix
1919

2020
import (
21+
"context"
2122
"fmt"
2223
"net"
2324
"net/http"
@@ -26,6 +27,7 @@ import (
2627
. "github.com/onsi/ginkgo/v2"
2728
. "github.com/onsi/gomega"
2829
"k8s.io/apimachinery/pkg/types"
30+
"k8s.io/apimachinery/pkg/util/wait"
2931

3032
apiv2 "github.com/apache/apisix-ingress-controller/api/v2"
3133
"github.com/apache/apisix-ingress-controller/test/e2e/framework"
@@ -265,11 +267,7 @@ spec:
265267
var apisixRoute apiv2.ApisixRoute
266268
applier.MustApplyAPIv2(types.NamespacedName{Namespace: s.Namespace(), Name: "default"}, &apisixRoute, fmt.Sprintf(apisixRouteSpec, "/get"))
267269

268-
By("when there is no replica got 500 by fault-injection")
269-
err := s.ScaleHTTPBIN(0)
270-
Expect(err).ShouldNot(HaveOccurred(), "scale httpbin to 0")
271-
Eventually(request).WithArguments("/get").WithTimeout(8 * time.Second).ProbeEvery(time.Second).Should(Equal(http.StatusInternalServerError))
272-
s.NewAPISIXClient().GET("/get").WithHost("httpbin").Expect().Body().IsEqual("No existing backendRef provided")
270+
Eventually(request).WithArguments("/get").WithTimeout(8 * time.Second).ProbeEvery(time.Second).Should(Equal(http.StatusServiceUnavailable))
273271
})
274272

275273
It("Test ApisixRoute resolveGranularity", func() {
@@ -519,5 +517,72 @@ spec:
519517
Eventually(upstreamAddrs).Should(HaveKey(endpoint))
520518
Eventually(upstreamAddrs).Should(HaveKey(clusterIP))
521519
})
520+
521+
It("Test backend implicit reference to apisixupstream", func() {
522+
var err error
523+
524+
const apisixRouteSpec = `
525+
apiVersion: apisix.apache.org/v2
526+
kind: ApisixRoute
527+
metadata:
528+
name: default
529+
spec:
530+
ingressClassName: apisix
531+
http:
532+
- name: rule0
533+
match:
534+
hosts:
535+
- httpbin
536+
paths:
537+
- /*
538+
backends:
539+
- serviceName: httpbin-service-e2e-test
540+
servicePort: 80
541+
plugins:
542+
- name: response-rewrite
543+
enable: true
544+
config:
545+
headers:
546+
set:
547+
"X-Upstream-Host": "$upstream_host"
548+
549+
`
550+
const apisixUpstreamSpec = `
551+
apiVersion: apisix.apache.org/v2
552+
kind: ApisixUpstream
553+
metadata:
554+
name: httpbin-service-e2e-test
555+
spec:
556+
ingressClassName: apisix
557+
passHost: rewrite
558+
upstreamHost: hello.httpbin.org
559+
loadbalancer:
560+
type: "chash"
561+
hashOn: "vars"
562+
key: "server_name"
563+
`
564+
expectUpstreamHostIs := func(expectedUpstreamHost string) func(ctx context.Context) (bool, error) {
565+
return func(ctx context.Context) (done bool, err error) {
566+
resp := s.NewAPISIXClient().GET("/get").WithHost("httpbin").Expect().Raw()
567+
return resp.StatusCode == http.StatusOK && resp.Header.Get("X-Upstream-Host") == expectedUpstreamHost, nil
568+
}
569+
}
570+
571+
By("apply apisixroute")
572+
applier.MustApplyAPIv2(types.NamespacedName{Namespace: s.Namespace(), Name: "default"}, new(apiv2.ApisixRoute), apisixRouteSpec)
573+
574+
By("verify ApisixRoute works")
575+
// expect upstream host is "httpbin"
576+
err = wait.PollUntilContextTimeout(context.Background(), time.Second, 10*time.Second, true, expectUpstreamHostIs("httpbin"))
577+
Expect(err).ShouldNot(HaveOccurred(), "verify ApisixRoute works")
578+
579+
By("apply apisixupstream")
580+
applier.MustApplyAPIv2(types.NamespacedName{Namespace: s.Namespace(), Name: "httpbin-service-e2e-test"}, new(apiv2.ApisixUpstream), apisixUpstreamSpec)
581+
582+
By("verify backend implicit reference to apisixupstream works")
583+
// expect upstream host is "hello.httpbin.org" which is rewritten by the apisixupstream
584+
err = wait.PollUntilContextTimeout(context.Background(), time.Second, 10*time.Second, true, expectUpstreamHostIs("hello.httpbin.org"))
585+
Expect(err).ShouldNot(HaveOccurred(), "check apisixupstream is referenced")
586+
})
522587
})
523588
})

0 commit comments

Comments
 (0)