Skip to content

Commit a9fd313

Browse files
committed
[feat gw-api]handle hostname intersection
1 parent 65b2f92 commit a9fd313

File tree

14 files changed

+277
-63
lines changed

14 files changed

+277
-63
lines changed

pkg/gateway/model/model_build_listener.go

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -509,8 +509,17 @@ func mapGatewayListenerConfigsByPort(gw *gwv1.Gateway, routes map[int32][]routeu
509509

510510
if listenerRoutes != nil {
511511
for _, route := range listenerRoutes {
512-
for _, routeHostname := range route.GetHostnames() {
513-
gwListenerConfigs[port].hostnames.Insert(string(routeHostname))
512+
// Use compatible hostnames (intersection) instead of raw route hostnames
513+
compatibleHostnames := route.GetCompatibleHostnames()
514+
if len(compatibleHostnames) > 0 {
515+
for _, hostname := range compatibleHostnames {
516+
gwListenerConfigs[port].hostnames.Insert(string(hostname))
517+
}
518+
} else {
519+
// Fallback to route hostnames if no compatible hostnames
520+
for _, routeHostname := range route.GetHostnames() {
521+
gwListenerConfigs[port].hostnames.Insert(string(routeHostname))
522+
}
514523
}
515524
}
516525
}

pkg/gateway/routeutils/descriptor.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,8 @@ type routeMetadataDescriptor interface {
2121
GetRouteListenerRuleConfigRefs() []gwv1.LocalObjectReference
2222
GetRouteGeneration() int64
2323
GetRouteCreateTimestamp() time.Time
24+
GetCompatibleHostnames() []gwv1.Hostname
25+
SetCompatibleHostnames([]gwv1.Hostname)
2426
}
2527

2628
type routeLoadError struct {

pkg/gateway/routeutils/grpc.go

Lines changed: 12 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -51,9 +51,10 @@ func (t *convertedGRPCRouteRule) GetListenerRuleConfig() *elbv2gw.ListenerRuleCo
5151
/* Route Description */
5252

5353
type grpcRouteDescription struct {
54-
route *gwv1.GRPCRoute
55-
rules []RouteRule
56-
ruleAccumulator attachedRuleAccumulator[gwv1.GRPCRouteRule]
54+
route *gwv1.GRPCRoute
55+
rules []RouteRule
56+
ruleAccumulator attachedRuleAccumulator[gwv1.GRPCRouteRule]
57+
compatibleHostnames []gwv1.Hostname
5758
}
5859

5960
func (grpcRoute *grpcRouteDescription) loadAttachedRules(ctx context.Context, k8sClient client.Client) (RouteDescriptor, []routeLoadError) {
@@ -143,6 +144,14 @@ func (grpcRoute *grpcRouteDescription) GetRouteCreateTimestamp() time.Time {
143144
return grpcRoute.route.CreationTimestamp.Time
144145
}
145146

147+
func (grpcRoute *grpcRouteDescription) GetCompatibleHostnames() []gwv1.Hostname {
148+
return grpcRoute.compatibleHostnames
149+
}
150+
151+
func (grpcRoute *grpcRouteDescription) SetCompatibleHostnames(hostnames []gwv1.Hostname) {
152+
grpcRoute.compatibleHostnames = hostnames
153+
}
154+
146155
var _ RouteDescriptor = &grpcRouteDescription{}
147156

148157
// Can we use an indexer here to query more efficiently?

pkg/gateway/routeutils/http.go

Lines changed: 12 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -51,9 +51,10 @@ func (t *convertedHTTPRouteRule) GetListenerRuleConfig() *elbv2gw.ListenerRuleCo
5151
/* Route Description */
5252

5353
type httpRouteDescription struct {
54-
route *gwv1.HTTPRoute
55-
rules []RouteRule
56-
ruleAccumulator attachedRuleAccumulator[gwv1.HTTPRouteRule]
54+
route *gwv1.HTTPRoute
55+
rules []RouteRule
56+
ruleAccumulator attachedRuleAccumulator[gwv1.HTTPRouteRule]
57+
compatibleHostnames []gwv1.Hostname
5758
}
5859

5960
func (httpRoute *httpRouteDescription) GetAttachedRules() []RouteRule {
@@ -136,6 +137,14 @@ func (httpRoute *httpRouteDescription) GetRouteCreateTimestamp() time.Time {
136137
return httpRoute.route.CreationTimestamp.Time
137138
}
138139

140+
func (httpRoute *httpRouteDescription) GetCompatibleHostnames() []gwv1.Hostname {
141+
return httpRoute.compatibleHostnames
142+
}
143+
144+
func (httpRoute *httpRouteDescription) SetCompatibleHostnames(hostnames []gwv1.Hostname) {
145+
httpRoute.compatibleHostnames = hostnames
146+
}
147+
139148
func convertHTTPRoute(r gwv1.HTTPRoute) *httpRouteDescription {
140149
return &httpRouteDescription{route: &r, ruleAccumulator: defaultHTTPRuleAccumulator}
141150
}

pkg/gateway/routeutils/listener_attachment_helper.go

Lines changed: 24 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -54,7 +54,7 @@ func (attachmentHelper *listenerAttachmentHelperImpl) listenerAllowsAttachment(c
5454
}
5555

5656
// check hostname
57-
if (route.GetRouteKind() == HTTPRouteKind || route.GetRouteKind() == GRPCRouteKind || route.GetRouteKind() == TLSRouteKind) && route.GetHostnames() != nil {
57+
if route.GetRouteKind() == HTTPRouteKind || route.GetRouteKind() == GRPCRouteKind || route.GetRouteKind() == TLSRouteKind {
5858
hostnameOK, err := attachmentHelper.hostnameCheck(listener, route)
5959
if err != nil {
6060
return false, nil, err
@@ -171,8 +171,17 @@ func (attachmentHelper *listenerAttachmentHelperImpl) kindCheck(listener gwv1.Li
171171
}
172172

173173
func (attachmentHelper *listenerAttachmentHelperImpl) hostnameCheck(listener gwv1.Listener, route preLoadRouteDescriptor) (bool, error) {
174-
// A route can attach to listener if it does not have hostname or listener does not have hostname
175-
if listener.Hostname == nil || len(route.GetHostnames()) == 0 {
174+
// If route has no hostnames but listener does, use listener hostname
175+
if len(route.GetHostnames()) == 0 {
176+
if listener.Hostname != nil {
177+
existing := route.GetCompatibleHostnames()
178+
route.SetCompatibleHostnames(append(existing, *listener.Hostname))
179+
}
180+
return true, nil
181+
}
182+
183+
// If listener has no hostname, route can attach
184+
if listener.Hostname == nil {
176185
return true, nil
177186
}
178187

@@ -188,6 +197,7 @@ func (attachmentHelper *listenerAttachmentHelperImpl) hostnameCheck(listener gwv
188197
return false, nil
189198
}
190199

200+
compatibleHostnames := []gwv1.Hostname{}
191201
for _, hostname := range route.GetHostnames() {
192202
// validate route hostname, skip invalid hostname
193203
isHostnameValid, err := IsHostNameInValidFormat(string(hostname))
@@ -196,12 +206,19 @@ func (attachmentHelper *listenerAttachmentHelperImpl) hostnameCheck(listener gwv
196206
continue
197207
}
198208

199-
// check if two hostnames have overlap (compatible)
200-
if isHostnameCompatible(string(hostname), string(*listener.Hostname)) {
201-
return true, nil
209+
// check if two hostnames have overlap (compatible) and get the more specific one
210+
if compatible, ok := getCompatibleHostname(string(hostname), string(*listener.Hostname)); ok {
211+
compatibleHostnames = append(compatibleHostnames, gwv1.Hostname(compatible))
202212
}
203213
}
204-
return false, nil
214+
215+
if len(compatibleHostnames) == 0 {
216+
return false, nil
217+
}
218+
219+
// Store computed compatible hostnames in route
220+
route.SetCompatibleHostnames(compatibleHostnames)
221+
return true, nil
205222
}
206223

207224
func (attachmentHelper *listenerAttachmentHelperImpl) crossServingHostnameUniquenessCheck(route preLoadRouteDescriptor, hostnamesFromHttpRoutes map[types.NamespacedName][]gwv1.Hostname, hostnamesFromGrpcRoutes map[types.NamespacedName][]gwv1.Hostname) (bool, string) {

pkg/gateway/routeutils/listener_attachment_helper_test.go

Lines changed: 108 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,14 +2,15 @@ package routeutils
22

33
import (
44
"context"
5+
"testing"
6+
57
"github.com/go-logr/logr"
68
"github.com/pkg/errors"
79
"github.com/stretchr/testify/assert"
810
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
911
"k8s.io/apimachinery/pkg/types"
1012
"k8s.io/apimachinery/pkg/util/sets"
1113
gwv1 "sigs.k8s.io/gateway-api/apis/v1"
12-
"testing"
1314
)
1415

1516
type mockNamespaceSelector struct {
@@ -547,6 +548,112 @@ func Test_hostnameCheck(t *testing.T) {
547548
}
548549
}
549550

551+
func Test_hostnameIntersection(t *testing.T) {
552+
tests := []struct {
553+
name string
554+
listenerHostname *gwv1.Hostname
555+
routeHostnames []gwv1.Hostname
556+
expectedAttachment bool
557+
expectedCompatibleHostnames []gwv1.Hostname
558+
expectEmpty bool
559+
}{
560+
{
561+
name: "Scenario 1: Route has NO hostnames - inherits listener hostname",
562+
listenerHostname: ptr(gwv1.Hostname("bar.com")),
563+
routeHostnames: []gwv1.Hostname{},
564+
expectedAttachment: true,
565+
expectedCompatibleHostnames: []gwv1.Hostname{"bar.com"},
566+
},
567+
{
568+
name: "Scenario 2: Listener has NO hostname",
569+
listenerHostname: nil,
570+
routeHostnames: []gwv1.Hostname{"foo.com"},
571+
expectedAttachment: true,
572+
expectEmpty: true,
573+
},
574+
{
575+
name: "Scenario 3: Both have NO hostnames",
576+
listenerHostname: nil,
577+
routeHostnames: []gwv1.Hostname{},
578+
expectedAttachment: true,
579+
expectEmpty: true,
580+
},
581+
{
582+
name: "Scenario 4: Exact match",
583+
listenerHostname: ptr(gwv1.Hostname("bar.com")),
584+
routeHostnames: []gwv1.Hostname{"bar.com"},
585+
expectedAttachment: true,
586+
expectedCompatibleHostnames: []gwv1.Hostname{"bar.com"},
587+
},
588+
{
589+
name: "Scenario 5: Listener wildcard matches route",
590+
listenerHostname: ptr(gwv1.Hostname("*.bar.com")),
591+
routeHostnames: []gwv1.Hostname{"foo.bar.com"},
592+
expectedAttachment: true,
593+
expectedCompatibleHostnames: []gwv1.Hostname{"foo.bar.com"},
594+
},
595+
{
596+
name: "Scenario 6: Route wildcard matches listener",
597+
listenerHostname: ptr(gwv1.Hostname("foo.bar.com")),
598+
routeHostnames: []gwv1.Hostname{"*.bar.com"},
599+
expectedAttachment: true,
600+
expectedCompatibleHostnames: []gwv1.Hostname{"foo.bar.com"},
601+
},
602+
{
603+
name: "Scenario 7: Both wildcards, compatible",
604+
listenerHostname: ptr(gwv1.Hostname("*.bar.com")),
605+
routeHostnames: []gwv1.Hostname{"*.bar.com"},
606+
expectedAttachment: true,
607+
expectedCompatibleHostnames: []gwv1.Hostname{"*.bar.com"},
608+
},
609+
{
610+
name: "Scenario 8: No overlap - rejected",
611+
listenerHostname: ptr(gwv1.Hostname("bar.com")),
612+
routeHostnames: []gwv1.Hostname{"foo.com"},
613+
expectedAttachment: false,
614+
expectEmpty: true,
615+
},
616+
{
617+
name: "Scenario 9: Multiple route hostnames, partial match",
618+
listenerHostname: ptr(gwv1.Hostname("*.bar.com")),
619+
routeHostnames: []gwv1.Hostname{"foo.bar.com", "baz.bar.com", "unrelated.com"},
620+
expectedAttachment: true,
621+
expectedCompatibleHostnames: []gwv1.Hostname{"foo.bar.com", "baz.bar.com"},
622+
},
623+
}
624+
625+
for _, tt := range tests {
626+
t.Run(tt.name, func(t *testing.T) {
627+
helper := &listenerAttachmentHelperImpl{
628+
logger: logr.Discard(),
629+
}
630+
631+
listener := gwv1.Listener{
632+
Hostname: tt.listenerHostname,
633+
}
634+
635+
route := &mockRoute{
636+
hostnames: tt.routeHostnames,
637+
}
638+
639+
result, err := helper.hostnameCheck(listener, route)
640+
641+
assert.NoError(t, err)
642+
assert.Equal(t, tt.expectedAttachment, result)
643+
644+
if tt.expectEmpty {
645+
assert.Empty(t, route.GetCompatibleHostnames())
646+
} else {
647+
assert.Equal(t, tt.expectedCompatibleHostnames, route.GetCompatibleHostnames())
648+
}
649+
})
650+
}
651+
}
652+
653+
func ptr[T any](v T) *T {
654+
return &v
655+
}
656+
550657
func Test_crossServingHostnameUniquenessCheck(t *testing.T) {
551658
hostnames := []gwv1.Hostname{"example.com"}
552659
namespace := "test-namespace"

pkg/gateway/routeutils/loader_test.go

Lines changed: 13 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -30,10 +30,19 @@ func (m *mockMapper) mapGatewayAndRoutes(context context.Context, gw gwv1.Gatewa
3030
var _ RouteDescriptor = &mockRoute{}
3131

3232
type mockRoute struct {
33-
namespacedName types.NamespacedName
34-
routeKind RouteKind
35-
generation int64
36-
hostnames []gwv1.Hostname
33+
namespacedName types.NamespacedName
34+
routeKind RouteKind
35+
generation int64
36+
hostnames []gwv1.Hostname
37+
CompatibleHostnames []gwv1.Hostname
38+
}
39+
40+
func (m *mockRoute) GetCompatibleHostnames() []gwv1.Hostname {
41+
return m.CompatibleHostnames
42+
}
43+
44+
func (m *mockRoute) SetCompatibleHostnames(hostnames []gwv1.Hostname) {
45+
m.CompatibleHostnames = hostnames
3746
}
3847

3948
func (m *mockRoute) loadAttachedRules(context context.Context, k8sClient client.Client) (RouteDescriptor, []routeLoadError) {

pkg/gateway/routeutils/mock_route.go

Lines changed: 15 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -28,12 +28,13 @@ func (m *MockRule) GetListenerRuleConfig() *elbv2gw.ListenerRuleConfiguration {
2828
var _ RouteRule = &MockRule{}
2929

3030
type MockRoute struct {
31-
Kind RouteKind
32-
Name string
33-
Namespace string
34-
Hostnames []string
35-
CreationTime time.Time
36-
Rules []RouteRule
31+
Kind RouteKind
32+
Name string
33+
Namespace string
34+
Hostnames []string
35+
CreationTime time.Time
36+
Rules []RouteRule
37+
CompatibleHostnames []gwv1.Hostname
3738
}
3839

3940
func (m *MockRoute) GetBackendRefs() []gwv1.BackendRef {
@@ -88,4 +89,12 @@ func (m *MockRoute) GetRouteCreateTimestamp() time.Time {
8889
return m.CreationTime
8990
}
9091

92+
func (m *MockRoute) GetCompatibleHostnames() []gwv1.Hostname {
93+
return m.CompatibleHostnames
94+
}
95+
96+
func (m *MockRoute) SetCompatibleHostnames(hostnames []gwv1.Hostname) {
97+
m.CompatibleHostnames = hostnames
98+
}
99+
91100
var _ RouteDescriptor = &MockRoute{}

pkg/gateway/routeutils/route_rule_precedence.go

Lines changed: 13 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -2,10 +2,11 @@ package routeutils
22

33
import (
44
"math"
5-
v1 "sigs.k8s.io/gateway-api/apis/v1"
65
"sort"
76
"strings"
87
"time"
8+
9+
v1 "sigs.k8s.io/gateway-api/apis/v1"
910
)
1011

1112
type RulePrecedence struct {
@@ -260,10 +261,17 @@ func compareCommonTieBreakers(ruleOne RulePrecedence, ruleTwo RulePrecedence) bo
260261
func getCommonRouteInfo(route RouteDescriptor) CommonRulePrecedence {
261262
routeNamespacedName := route.GetRouteNamespacedName().String()
262263
routeCreateTimestamp := route.GetRouteCreateTimestamp()
263-
// get hostname in string array format
264-
hostnames := make([]string, len(route.GetHostnames()))
265-
for i, hostname := range route.GetHostnames() {
266-
hostnames[i] = string(hostname)
264+
// Use compatible hostnames computed during route attachment
265+
compatibleHostnames := route.GetCompatibleHostnames()
266+
hostnames := make([]string, len(compatibleHostnames))
267+
for i, h := range compatibleHostnames {
268+
hostnames[i] = string(h)
269+
}
270+
// If no compatible hostnames, use route hostnames
271+
if len(hostnames) == 0 {
272+
for _, h := range route.GetHostnames() {
273+
hostnames = append(hostnames, string(h))
274+
}
267275
}
268276
return CommonRulePrecedence{
269277
RouteDescriptor: route,

pkg/gateway/routeutils/tcp.go

Lines changed: 12 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -52,9 +52,10 @@ func (t *convertedTCPRouteRule) GetListenerRuleConfig() *elbv2gw.ListenerRuleCon
5252
/* Route Description */
5353

5454
type tcpRouteDescription struct {
55-
route *gwalpha2.TCPRoute
56-
rules []RouteRule
57-
ruleAccumulator attachedRuleAccumulator[gwalpha2.TCPRouteRule]
55+
route *gwalpha2.TCPRoute
56+
rules []RouteRule
57+
ruleAccumulator attachedRuleAccumulator[gwalpha2.TCPRouteRule]
58+
compatibleHostnames []gwv1.Hostname
5859
}
5960

6061
func (tcpRoute *tcpRouteDescription) GetAttachedRules() []RouteRule {
@@ -119,6 +120,14 @@ func (tcpRoute *tcpRouteDescription) GetRouteCreateTimestamp() time.Time {
119120
return tcpRoute.route.CreationTimestamp.Time
120121
}
121122

123+
func (tcpRoute *tcpRouteDescription) GetCompatibleHostnames() []gwv1.Hostname {
124+
return tcpRoute.compatibleHostnames
125+
}
126+
127+
func (tcpRoute *tcpRouteDescription) SetCompatibleHostnames(hostnames []gwv1.Hostname) {
128+
tcpRoute.compatibleHostnames = hostnames
129+
}
130+
122131
var _ RouteDescriptor = &tcpRouteDescription{}
123132

124133
// Can we use an indexer here to query more efficiently?

0 commit comments

Comments
 (0)