Skip to content

Commit 6d25794

Browse files
authored
[feat gw-api]handle hostname intersection (#4417)
1 parent 373bdc8 commit 6d25794

19 files changed

+451
-104
lines changed

pkg/gateway/model/model_build_listener.go

Lines changed: 12 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -187,7 +187,7 @@ func (l listenerBuilderImpl) buildL4ListenerSpec(ctx context.Context, stack core
187187

188188
func (l listenerBuilderImpl) buildListenerRules(ctx context.Context, stack core.Stack, ls *elbv2model.Listener, ipAddressType elbv2model.IPAddressType, gw *gwv1.Gateway, port int32, lbCfg elbv2gw.LoadBalancerConfiguration, routes map[int32][]routeutils.RouteDescriptor) ([]types.NamespacedName, error) {
189189
// sort all rules based on precedence
190-
rulesWithPrecedenceOrder := routeutils.SortAllRulesByPrecedence(routes[port])
190+
rulesWithPrecedenceOrder := routeutils.SortAllRulesByPrecedence(routes[port], port)
191191
secrets := make([]types.NamespacedName, 0)
192192
var albRules []elbv2model.Rule
193193
for _, ruleWithPrecedence := range rulesWithPrecedenceOrder {
@@ -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+
compatibleHostnamesByPort := route.GetCompatibleHostnamesByPort()[port]
514+
if len(compatibleHostnamesByPort) > 0 {
515+
for _, hostname := range compatibleHostnamesByPort {
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/model/model_build_listener_test.go

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -431,7 +431,7 @@ func Test_buildListenerTags(t *testing.T) {
431431
},
432432
externalManagedTags: []string{"ExternalTag", "ManagedByTeam"},
433433
expectedTags: nil,
434-
expectedErr: errors.New("external managed tag key ExternalTag cannot be specified"),
434+
expectedErr: errors.New("external managed tag key"),
435435
},
436436
}
437437

@@ -448,6 +448,7 @@ func Test_buildListenerTags(t *testing.T) {
448448
if tt.expectedErr != nil {
449449
assert.Error(t, err)
450450
assert.Contains(t, err.Error(), tt.expectedErr.Error())
451+
assert.True(t, strings.Contains(err.Error(), "ExternalTag") || strings.Contains(err.Error(), "ManagedByTeam"))
451452
assert.Nil(t, got)
452453
} else {
453454
assert.NoError(t, err)

pkg/gateway/routeutils/descriptor.go

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

33
import (
44
"context"
5+
"time"
6+
57
"k8s.io/apimachinery/pkg/types"
68
"sigs.k8s.io/controller-runtime/pkg/client"
79
gwv1 "sigs.k8s.io/gateway-api/apis/v1"
8-
"time"
910
)
1011

1112
// routeMetadataDescriptor a common set of functions that will describe a route.
@@ -21,6 +22,15 @@ type routeMetadataDescriptor interface {
2122
GetRouteListenerRuleConfigRefs() []gwv1.LocalObjectReference
2223
GetRouteGeneration() int64
2324
GetRouteCreateTimestamp() time.Time
25+
// GetCompatibleHostnamesByPort returns the compatible hostnames for each listener port.
26+
// Compatible hostnames are computed during route attachment by intersecting listener hostnames
27+
// with route hostnames (considering wildcards). The map key is the listener port number.
28+
// When a route attaches to multiple listeners on the same port, hostnames are aggregated.
29+
// When a route attaches to listeners on different ports, each port has its own hostname list.
30+
GetCompatibleHostnamesByPort() map[int32][]gwv1.Hostname
31+
// setCompatibleHostnamesByPort is a package-private method to set compatible hostnames.
32+
// This is called by the loader after route attachment validation.
33+
setCompatibleHostnamesByPort(map[int32][]gwv1.Hostname)
2434
}
2535

2636
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+
compatibleHostnamesByPort map[int32][]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) GetCompatibleHostnamesByPort() map[int32][]gwv1.Hostname {
148+
return grpcRoute.compatibleHostnamesByPort
149+
}
150+
151+
func (grpcRoute *grpcRouteDescription) setCompatibleHostnamesByPort(hostnamesByPort map[int32][]gwv1.Hostname) {
152+
grpcRoute.compatibleHostnamesByPort = hostnamesByPort
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+
compatibleHostnamesByPort map[int32][]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) GetCompatibleHostnamesByPort() map[int32][]gwv1.Hostname {
141+
return httpRoute.compatibleHostnamesByPort
142+
}
143+
144+
func (httpRoute *httpRouteDescription) setCompatibleHostnamesByPort(hostnamesByPort map[int32][]gwv1.Hostname) {
145+
httpRoute.compatibleHostnamesByPort = hostnamesByPort
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: 40 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ import (
1515
// listenerAttachmentHelper is an internal utility interface that can be used to determine if a listener will allow
1616
// a route to attach to it.
1717
type listenerAttachmentHelper interface {
18-
listenerAllowsAttachment(ctx context.Context, gw gwv1.Gateway, listener gwv1.Listener, route preLoadRouteDescriptor, hostnamesFromHttpRoutes map[types.NamespacedName][]gwv1.Hostname, hostnamesFromGrpcRoutes map[types.NamespacedName][]gwv1.Hostname) (bool, *RouteData, error)
18+
listenerAllowsAttachment(ctx context.Context, gw gwv1.Gateway, listener gwv1.Listener, route preLoadRouteDescriptor, hostnamesFromHttpRoutes map[types.NamespacedName][]gwv1.Hostname, hostnamesFromGrpcRoutes map[types.NamespacedName][]gwv1.Hostname) ([]gwv1.Hostname, bool, *RouteData, error)
1919
}
2020

2121
var _ listenerAttachmentHelper = &listenerAttachmentHelperImpl{}
@@ -35,33 +35,36 @@ func newListenerAttachmentHelper(k8sClient client.Client, logger logr.Logger) li
3535

3636
// listenerAllowsAttachment utility method to determine if a listener will allow a route to connect using
3737
// Gateway API rules to determine compatibility between lister and route.
38-
func (attachmentHelper *listenerAttachmentHelperImpl) listenerAllowsAttachment(ctx context.Context, gw gwv1.Gateway, listener gwv1.Listener, route preLoadRouteDescriptor, hostnamesFromHttpRoutes map[types.NamespacedName][]gwv1.Hostname, hostnamesFromGrpcRoutes map[types.NamespacedName][]gwv1.Hostname) (bool, *RouteData, error) {
38+
// Returns: (compatibleHostnames, allowed, failedRouteData, error)
39+
func (attachmentHelper *listenerAttachmentHelperImpl) listenerAllowsAttachment(ctx context.Context, gw gwv1.Gateway, listener gwv1.Listener, route preLoadRouteDescriptor, hostnamesFromHttpRoutes map[types.NamespacedName][]gwv1.Hostname, hostnamesFromGrpcRoutes map[types.NamespacedName][]gwv1.Hostname) ([]gwv1.Hostname, bool, *RouteData, error) {
3940
// check namespace
4041
namespaceOK, err := attachmentHelper.namespaceCheck(ctx, gw, listener, route)
4142
if err != nil {
42-
return false, nil, err
43+
return nil, false, nil, err
4344
}
4445
if !namespaceOK {
4546
rd := GenerateRouteData(false, true, string(gwv1.RouteReasonNotAllowedByListeners), RouteStatusInfoRejectedMessageNamespaceNotMatch, route.GetRouteNamespacedName(), route.GetRouteKind(), route.GetRouteGeneration(), gw)
46-
return false, &rd, nil
47+
return nil, false, &rd, nil
4748
}
4849

4950
// check kind
5051
kindOK := attachmentHelper.kindCheck(listener, route)
5152
if !kindOK {
5253
rd := GenerateRouteData(false, true, string(gwv1.RouteReasonNotAllowedByListeners), RouteStatusInfoRejectedMessageKindNotMatch, route.GetRouteNamespacedName(), route.GetRouteKind(), route.GetRouteGeneration(), gw)
53-
return false, &rd, nil
54+
return nil, false, &rd, nil
5455
}
5556

56-
// check hostname
57-
if (route.GetRouteKind() == HTTPRouteKind || route.GetRouteKind() == GRPCRouteKind || route.GetRouteKind() == TLSRouteKind) && route.GetHostnames() != nil {
58-
hostnameOK, err := attachmentHelper.hostnameCheck(listener, route)
57+
// check hostname and get compatible hostnames
58+
var compatibleHostnames []gwv1.Hostname
59+
if route.GetRouteKind() == HTTPRouteKind || route.GetRouteKind() == GRPCRouteKind || route.GetRouteKind() == TLSRouteKind {
60+
var hostnameOK bool
61+
compatibleHostnames, hostnameOK, err = attachmentHelper.hostnameCheck(listener, route)
5962
if err != nil {
60-
return false, nil, err
63+
return nil, false, nil, err
6164
}
6265
if !hostnameOK {
6366
rd := GenerateRouteData(false, true, string(gwv1.RouteReasonNoMatchingListenerHostname), RouteStatusInfoRejectedMessageNoMatchingHostname, route.GetRouteNamespacedName(), route.GetRouteKind(), route.GetRouteGeneration(), gw)
64-
return false, &rd, nil
67+
return nil, false, &rd, nil
6568
}
6669
}
6770

@@ -71,11 +74,11 @@ func (attachmentHelper *listenerAttachmentHelperImpl) listenerAllowsAttachment(c
7174
if !hostnameUniquenessOK {
7275
message := fmt.Sprintf("HTTPRoute and GRPCRoute have overlap hostname, attachment is rejected. Conflict route: %s", conflictRoute)
7376
rd := GenerateRouteData(false, true, string(gwv1.RouteReasonNotAllowedByListeners), message, route.GetRouteNamespacedName(), route.GetRouteKind(), route.GetRouteGeneration(), gw)
74-
return false, &rd, nil
77+
return nil, false, &rd, nil
7578
}
7679
}
7780

78-
return true, nil, nil
81+
return compatibleHostnames, true, nil, nil
7982
}
8083

8184
// namespaceCheck namespace check implements the Gateway API spec for namespace matching between listener
@@ -170,24 +173,33 @@ func (attachmentHelper *listenerAttachmentHelperImpl) kindCheck(listener gwv1.Li
170173
return true
171174
}
172175

173-
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 {
176-
return true, nil
176+
func (attachmentHelper *listenerAttachmentHelperImpl) hostnameCheck(listener gwv1.Listener, route preLoadRouteDescriptor) ([]gwv1.Hostname, bool, error) {
177+
// If route has no hostnames but listener does, use listener hostname
178+
if route.GetHostnames() == nil || len(route.GetHostnames()) == 0 {
179+
if listener.Hostname != nil {
180+
return []gwv1.Hostname{*listener.Hostname}, true, nil
181+
}
182+
return nil, true, nil
183+
}
184+
185+
// If listener has no hostname, route can attach
186+
if listener.Hostname == nil {
187+
return nil, true, nil
177188
}
178189

179190
// validate listener hostname, return if listener hostname is not valid
180191
isListenerHostnameValid, err := IsHostNameInValidFormat(string(*listener.Hostname))
181192
if err != nil {
182193
attachmentHelper.logger.Error(err, "listener hostname is not valid", "listener", listener.Name, "hostname", *listener.Hostname)
183194
initialErrorMessage := fmt.Sprintf("listener hostname %s is not valid (listener name %s)", listener.Name, *listener.Hostname)
184-
return false, wrapError(errors.Errorf("%s", initialErrorMessage), gwv1.GatewayReasonListenersNotValid, gwv1.RouteReasonUnsupportedValue, nil, nil)
195+
return nil, false, wrapError(errors.Errorf("%s", initialErrorMessage), gwv1.GatewayReasonListenersNotValid, gwv1.RouteReasonUnsupportedValue, nil, nil)
185196
}
186197

187198
if !isListenerHostnameValid {
188-
return false, nil
199+
return nil, false, nil
189200
}
190201

202+
compatibleHostnames := []gwv1.Hostname{}
191203
for _, hostname := range route.GetHostnames() {
192204
// validate route hostname, skip invalid hostname
193205
isHostnameValid, err := IsHostNameInValidFormat(string(hostname))
@@ -196,12 +208,18 @@ func (attachmentHelper *listenerAttachmentHelperImpl) hostnameCheck(listener gwv
196208
continue
197209
}
198210

199-
// check if two hostnames have overlap (compatible)
200-
if isHostnameCompatible(string(hostname), string(*listener.Hostname)) {
201-
return true, nil
211+
// check if two hostnames have overlap (compatible) and get the more specific one
212+
if compatible, ok := getCompatibleHostname(string(hostname), string(*listener.Hostname)); ok {
213+
compatibleHostnames = append(compatibleHostnames, gwv1.Hostname(compatible))
202214
}
203215
}
204-
return false, nil
216+
217+
if len(compatibleHostnames) == 0 {
218+
return nil, false, nil
219+
}
220+
221+
// Return computed compatible hostnames without storing in route
222+
return compatibleHostnames, true, nil
205223
}
206224

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

0 commit comments

Comments
 (0)