Skip to content

Commit bfc7e6b

Browse files
authored
Merge pull request #4440 from shuqz/http-conformance
[feat gw-api]add dedupe in route mapper
2 parents ac4b1f9 + 9e23994 commit bfc7e6b

File tree

4 files changed

+118
-14
lines changed

4 files changed

+118
-14
lines changed

pkg/gateway/routeutils/loader.go

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,6 @@ import (
66

77
"github.com/go-logr/logr"
88
"github.com/pkg/errors"
9-
"k8s.io/apimachinery/pkg/types"
109
"k8s.io/apimachinery/pkg/util/sets"
1110
"sigs.k8s.io/controller-runtime/pkg/client"
1211
gwv1 "sigs.k8s.io/gateway-api/apis/v1"
@@ -150,7 +149,7 @@ func (l *loaderImpl) LoadRoutesForGateway(ctx context.Context, gw gwv1.Gateway,
150149
}
151150

152151
// loadChildResources responsible for loading all resources that a route descriptor references.
153-
func (l *loaderImpl) loadChildResources(ctx context.Context, preloadedRoutes map[int][]preLoadRouteDescriptor, compatibleHostnamesByPort map[int32]map[types.NamespacedName][]gwv1.Hostname, gw gwv1.Gateway) (map[int32][]RouteDescriptor, []RouteData, error) {
152+
func (l *loaderImpl) loadChildResources(ctx context.Context, preloadedRoutes map[int][]preLoadRouteDescriptor, compatibleHostnamesByPort map[int32]map[string][]gwv1.Hostname, gw gwv1.Gateway) (map[int32][]RouteDescriptor, []RouteData, error) {
154153
// Cache to reduce duplicate route lookups.
155154
// Kind -> [NamespacedName:Previously Loaded Descriptor]
156155
resourceCache := make(map[string]RouteDescriptor)
@@ -215,7 +214,7 @@ func (l *loaderImpl) loadChildResources(ctx context.Context, preloadedRoutes map
215214
// Set compatible hostnames by port for all routes
216215
for _, route := range resourceCache {
217216
hostnamesByPort := make(map[int32][]gwv1.Hostname)
218-
routeKey := route.GetRouteNamespacedName()
217+
routeKey := fmt.Sprintf("%s-%s", route.GetRouteKind(), route.GetRouteNamespacedName())
219218
for port, compatibleHostnames := range compatibleHostnamesByPort {
220219
if hostnames, exists := compatibleHostnames[routeKey]; exists {
221220
hostnamesByPort[port] = hostnames

pkg/gateway/routeutils/loader_test.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -22,9 +22,9 @@ type mockMapper struct {
2222
routeStatusUpdates []RouteData
2323
}
2424

25-
func (m *mockMapper) mapGatewayAndRoutes(context context.Context, gw gwv1.Gateway, routes []preLoadRouteDescriptor) (map[int][]preLoadRouteDescriptor, map[int32]map[types.NamespacedName][]gwv1.Hostname, []RouteData, error) {
25+
func (m *mockMapper) mapGatewayAndRoutes(context context.Context, gw gwv1.Gateway, routes []preLoadRouteDescriptor) (map[int][]preLoadRouteDescriptor, map[int32]map[string][]gwv1.Hostname, []RouteData, error) {
2626
assert.ElementsMatch(m.t, m.expectedRoutes, routes)
27-
return m.mapToReturn, make(map[int32]map[types.NamespacedName][]gwv1.Hostname), m.routeStatusUpdates, nil
27+
return m.mapToReturn, make(map[int32]map[string][]gwv1.Hostname), m.routeStatusUpdates, nil
2828
}
2929

3030
var _ RouteDescriptor = &mockRoute{}

pkg/gateway/routeutils/route_listener_mapper.go

Lines changed: 17 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ package routeutils
22

33
import (
44
"context"
5+
"fmt"
56

67
"github.com/go-logr/logr"
78
"k8s.io/apimachinery/pkg/types"
@@ -12,7 +13,7 @@ import (
1213
// listenerToRouteMapper is an internal utility that will map a list of routes to the listeners of a gateway
1314
// if the gateway and/or route are incompatible, then the route is discarded.
1415
type listenerToRouteMapper interface {
15-
mapGatewayAndRoutes(context context.Context, gw gwv1.Gateway, routes []preLoadRouteDescriptor) (map[int][]preLoadRouteDescriptor, map[int32]map[types.NamespacedName][]gwv1.Hostname, []RouteData, error)
16+
mapGatewayAndRoutes(context context.Context, gw gwv1.Gateway, routes []preLoadRouteDescriptor) (map[int][]preLoadRouteDescriptor, map[int32]map[string][]gwv1.Hostname, []RouteData, error)
1617
}
1718

1819
var _ listenerToRouteMapper = &listenerToRouteMapperImpl{}
@@ -33,9 +34,9 @@ func newListenerToRouteMapper(k8sClient client.Client, logger logr.Logger) liste
3334

3435
// mapGatewayAndRoutes will map route to the corresponding listener ports using the Gateway API spec rules.
3536
// Returns: (routesByPort, compatibleHostnamesByPort, failedRoutes, error)
36-
func (ltr *listenerToRouteMapperImpl) mapGatewayAndRoutes(ctx context.Context, gw gwv1.Gateway, routes []preLoadRouteDescriptor) (map[int][]preLoadRouteDescriptor, map[int32]map[types.NamespacedName][]gwv1.Hostname, []RouteData, error) {
37+
func (ltr *listenerToRouteMapperImpl) mapGatewayAndRoutes(ctx context.Context, gw gwv1.Gateway, routes []preLoadRouteDescriptor) (map[int][]preLoadRouteDescriptor, map[int32]map[string][]gwv1.Hostname, []RouteData, error) {
3738
result := make(map[int][]preLoadRouteDescriptor)
38-
compatibleHostnamesByPort := make(map[int32]map[types.NamespacedName][]gwv1.Hostname)
39+
compatibleHostnamesByPort := make(map[int32]map[string][]gwv1.Hostname)
3940
failedRoutes := make([]RouteData, 0)
4041

4142
// First filter out any routes that are not intended for this Gateway.
@@ -48,6 +49,8 @@ func (ltr *listenerToRouteMapperImpl) mapGatewayAndRoutes(ctx context.Context, g
4849
}
4950
}
5051

52+
// Dedupe - Check if route already exists for this port before adding
53+
seenRoutesPerPort := make(map[int]map[string]bool)
5154
// Next, greedily looking for the route to attach to.
5255
for _, listener := range gw.Spec.Listeners {
5356
// used for cross serving check
@@ -74,16 +77,23 @@ func (ltr *listenerToRouteMapperImpl) mapGatewayAndRoutes(ctx context.Context, g
7477

7578
if allowedAttachment {
7679
port := int32(listener.Port)
77-
result[int(port)] = append(result[int(port)], route)
80+
routeKey := fmt.Sprintf("%s-%s", route.GetRouteKind(), route.GetRouteNamespacedName())
81+
if seenRoutesPerPort[int(port)] == nil {
82+
seenRoutesPerPort[int(port)] = make(map[string]bool)
83+
}
84+
if !seenRoutesPerPort[int(port)][routeKey] {
85+
seenRoutesPerPort[int(port)][routeKey] = true
86+
result[int(port)] = append(result[int(port)], route)
87+
}
7888

79-
// Store compatible hostnames per port per route
89+
// Store compatible hostnames per port per route per kind
8090
if compatibleHostnamesByPort[port] == nil {
81-
compatibleHostnamesByPort[port] = make(map[types.NamespacedName][]gwv1.Hostname)
91+
compatibleHostnamesByPort[port] = make(map[string][]gwv1.Hostname)
8292
}
8393
// Append hostnames for routes that attach to multiple listeners on the same port
84-
routeKey := route.GetRouteNamespacedName()
8594
compatibleHostnamesByPort[port][routeKey] = append(compatibleHostnamesByPort[port][routeKey], compatibleHostnames...)
8695
}
96+
8797
}
8898
}
8999
return result, compatibleHostnamesByPort, failedRoutes, nil

pkg/gateway/routeutils/route_listener_mapper_test.go

Lines changed: 97 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,12 +3,13 @@ package routeutils
33
import (
44
"context"
55
"fmt"
6+
"testing"
7+
68
"github.com/go-logr/logr"
79
"github.com/stretchr/testify/assert"
810
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
911
"k8s.io/apimachinery/pkg/types"
1012
gwv1 "sigs.k8s.io/gateway-api/apis/v1"
11-
"testing"
1213
)
1314

1415
type mockListenerAttachmentHelper struct {
@@ -299,6 +300,99 @@ func Test_mapGatewayAndRoutes(t *testing.T) {
299300
name: "no output",
300301
expected: make(map[int][]preLoadRouteDescriptor),
301302
},
303+
{
304+
name: "route attaches to multiple listeners on same port - verify deduplication",
305+
gw: gwv1.Gateway{
306+
ObjectMeta: metav1.ObjectMeta{
307+
Name: "gw1",
308+
Namespace: "ns-gw",
309+
},
310+
Spec: gwv1.GatewaySpec{
311+
Listeners: []gwv1.Listener{
312+
{
313+
Name: "listener1-port80",
314+
Port: gwv1.PortNumber(80),
315+
},
316+
{
317+
Name: "listener2-port80",
318+
Port: gwv1.PortNumber(80),
319+
},
320+
},
321+
},
322+
},
323+
routes: []preLoadRouteDescriptor{route1},
324+
listenerAttachmentMap: map[string]bool{
325+
"listener1-port80-80-route1-ns1": true,
326+
"listener2-port80-80-route1-ns1": true,
327+
},
328+
routeListenerMap: map[string]bool{
329+
"listener1-port80-80-route1-ns1": true,
330+
"listener2-port80-80-route1-ns1": true,
331+
},
332+
routeGatewayMap: map[string]bool{
333+
makeRouteGatewayMapKey(gateway, route1): true,
334+
},
335+
expected: map[int][]preLoadRouteDescriptor{
336+
80: {route1}, // Only one route1, not duplicated
337+
},
338+
},
339+
{
340+
name: "different route kinds with same name attach to same listener",
341+
gw: gwv1.Gateway{
342+
ObjectMeta: metav1.ObjectMeta{
343+
Name: "gw1",
344+
Namespace: "ns-gw",
345+
},
346+
Spec: gwv1.GatewaySpec{
347+
Listeners: []gwv1.Listener{
348+
{
349+
Name: "https-listener",
350+
Port: gwv1.PortNumber(443),
351+
Protocol: gwv1.HTTPSProtocolType,
352+
},
353+
},
354+
},
355+
},
356+
routes: []preLoadRouteDescriptor{
357+
convertHTTPRoute(gwv1.HTTPRoute{
358+
ObjectMeta: metav1.ObjectMeta{
359+
Name: "my-route",
360+
Namespace: "default",
361+
},
362+
}),
363+
convertGRPCRoute(gwv1.GRPCRoute{
364+
ObjectMeta: metav1.ObjectMeta{
365+
Name: "my-route",
366+
Namespace: "default",
367+
},
368+
}),
369+
},
370+
listenerAttachmentMap: map[string]bool{
371+
"https-listener-443-my-route-default": true,
372+
},
373+
routeListenerMap: map[string]bool{
374+
"https-listener-443-my-route-default": true,
375+
},
376+
routeGatewayMap: map[string]bool{
377+
"gw1-ns-gw-my-route-default": true,
378+
},
379+
expected: map[int][]preLoadRouteDescriptor{
380+
443: {
381+
convertHTTPRoute(gwv1.HTTPRoute{
382+
ObjectMeta: metav1.ObjectMeta{
383+
Name: "my-route",
384+
Namespace: "default",
385+
},
386+
}),
387+
convertGRPCRoute(gwv1.GRPCRoute{
388+
ObjectMeta: metav1.ObjectMeta{
389+
Name: "my-route",
390+
Namespace: "default",
391+
},
392+
}),
393+
},
394+
},
395+
},
302396
}
303397

304398
for _, tc := range testCases {
@@ -313,7 +407,7 @@ func Test_mapGatewayAndRoutes(t *testing.T) {
313407
},
314408
logger: logr.Discard(),
315409
}
316-
result, _, statusUpdates, err := mapper.mapGatewayAndRoutes(context.Background(), tc.gw, tc.routes)
410+
result, compatibleHostnames, statusUpdates, err := mapper.mapGatewayAndRoutes(context.Background(), tc.gw, tc.routes)
317411

318412
if tc.expectErr {
319413
assert.Error(t, err)
@@ -322,6 +416,7 @@ func Test_mapGatewayAndRoutes(t *testing.T) {
322416

323417
assert.NoError(t, err)
324418
assert.Equal(t, len(tc.expected), len(result))
419+
assert.NotNil(t, compatibleHostnames)
325420

326421
assert.Equal(t, 0, len(statusUpdates))
327422

0 commit comments

Comments
 (0)