Skip to content

Commit 90574fc

Browse files
authored
fix: HTTPRouteHostnameIntersection test case (#135)
Signed-off-by: ashing <[email protected]>
1 parent 80d84b9 commit 90574fc

File tree

3 files changed

+166
-23
lines changed

3 files changed

+166
-23
lines changed

internal/controller/httproute_controller.go

Lines changed: 16 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -17,8 +17,10 @@ import (
1717
"fmt"
1818
"strings"
1919

20+
"github.com/api7/gopkg/pkg/log"
2021
"github.com/go-logr/logr"
2122
"github.com/pkg/errors"
23+
"go.uber.org/zap"
2224
"golang.org/x/exp/slices"
2325
corev1 "k8s.io/api/core/v1"
2426
discoveryv1 "k8s.io/api/discovery/v1"
@@ -201,11 +203,24 @@ func (r *HTTPRouteReconciler) Reconcile(ctx context.Context, req ctrl.Request) (
201203
}
202204
ProcessBackendTrafficPolicy(r.Client, r.Log, tctx)
203205

204-
if err := r.Provider.Update(ctx, tctx, hr); err != nil {
206+
filteredHTTPRoute, err := filterHostnames(gateways, hr.DeepCopy())
207+
if err != nil {
205208
acceptStatus.status = false
206209
acceptStatus.msg = err.Error()
207210
}
208211

212+
if isRouteAccepted(gateways) && err == nil {
213+
routeToUpdate := hr
214+
if filteredHTTPRoute != nil {
215+
log.Debugw("filteredHTTPRoute", zap.Any("filteredHTTPRoute", filteredHTTPRoute))
216+
routeToUpdate = filteredHTTPRoute
217+
}
218+
if err := r.Provider.Update(ctx, tctx, routeToUpdate); err != nil {
219+
acceptStatus.status = false
220+
acceptStatus.msg = err.Error()
221+
}
222+
}
223+
209224
// TODO: diff the old and new status
210225
hr.Status.Parents = make([]gatewayv1.RouteParentStatus, 0, len(gateways))
211226
for _, gateway := range gateways {
@@ -214,9 +229,6 @@ func (r *HTTPRouteReconciler) Reconcile(ctx context.Context, req ctrl.Request) (
214229
for _, condition := range gateway.Conditions {
215230
parentStatus.Conditions = MergeCondition(parentStatus.Conditions, condition)
216231
}
217-
if gateway.ListenerName == "" {
218-
continue
219-
}
220232
SetRouteConditionAccepted(&parentStatus, hr.GetGeneration(), acceptStatus.status, acceptStatus.msg)
221233
SetRouteConditionResolvedRefs(&parentStatus, hr.GetGeneration(), resolveRefStatus.status, resolveRefStatus.msg)
222234
hr.Status.Parents = append(hr.Status.Parents, parentStatus)

internal/controller/utils.go

Lines changed: 150 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ package controller
1414

1515
import (
1616
"context"
17+
"errors"
1718
"fmt"
1819
"path"
1920
"reflect"
@@ -48,6 +49,10 @@ const (
4849

4950
const defaultIngressClassAnnotation = "ingressclass.kubernetes.io/is-default-class"
5051

52+
var (
53+
ErrNoMatchingListenerHostname = errors.New("no matching hostnames in listener")
54+
)
55+
5156
// IsDefaultIngressClass returns whether an IngressClass is the default IngressClass.
5257
func IsDefaultIngressClass(obj client.Object) bool {
5358
if ingressClass, ok := obj.(*networkingv1.IngressClass); ok {
@@ -229,10 +234,15 @@ func SetRouteConditionAccepted(routeParentStatus *gatewayv1.RouteParentStatus, g
229234
conditionStatus = metav1.ConditionFalse
230235
}
231236

237+
reason := gatewayv1.RouteReasonAccepted
238+
if message == ErrNoMatchingListenerHostname.Error() {
239+
reason = gatewayv1.RouteReasonNoMatchingListenerHostname
240+
}
241+
232242
condition := metav1.Condition{
233243
Type: string(gatewayv1.RouteConditionAccepted),
234244
Status: conditionStatus,
235-
Reason: string(gatewayv1.RouteReasonAccepted),
245+
Reason: string(reason),
236246
ObservedGeneration: generation,
237247
Message: message,
238248
LastTransitionTime: metav1.Now(),
@@ -458,39 +468,51 @@ func HostnamesIntersect(a, b string) bool {
458468
return HostnamesMatch(a, b) || HostnamesMatch(b, a)
459469
}
460470

471+
// HostnamesMatch checks that the hostnameB matches the hostnameA. HostnameA is treated as mask
472+
// to be checked against the hostnameB.
461473
func HostnamesMatch(hostnameA, hostnameB string) bool {
462-
labelsA := strings.Split(hostnameA, ".")
463-
labelsB := strings.Split(hostnameB, ".")
474+
// the hostnames are in the form of "foo.bar.com"; split them
475+
// in a slice of substrings
476+
hostnameALabels := strings.Split(hostnameA, ".")
477+
hostnameBLabels := strings.Split(hostnameB, ".")
464478

465-
var i, j int
479+
var a, b int
466480
var wildcard bool
467481

468-
for i, j = 0, 0; i < len(labelsA) && j < len(labelsB); i, j = i+1, j+1 {
482+
// iterate over the parts of both the hostnames
483+
for a, b = 0, 0; a < len(hostnameALabels) && b < len(hostnameBLabels); a, b = a+1, b+1 {
484+
var matchFound bool
485+
486+
// if the current part of B is a wildcard, we need to find the first
487+
// A part that matches with the following B part
469488
if wildcard {
470-
for ; j < len(labelsB); j++ {
471-
if labelsA[i] == labelsB[j] {
489+
for ; b < len(hostnameBLabels); b++ {
490+
if hostnameALabels[a] == hostnameBLabels[b] {
491+
matchFound = true
472492
break
473493
}
474494
}
475-
if j == len(labelsB) {
476-
return false
477-
}
478495
}
479496

480-
if labelsA[i] == "*" {
497+
// if no match was found, the hostnames don't match
498+
if wildcard && !matchFound {
499+
return false
500+
}
501+
502+
// check if at least on of the current parts are a wildcard; if so, continue
503+
if hostnameALabels[a] == "*" {
481504
wildcard = true
482-
j--
483505
continue
484506
}
485-
507+
// reset the wildcard variables
486508
wildcard = false
487509

488-
if labelsA[i] != labelsB[j] {
510+
// if the current a part is different from the b part, the hostnames are incompatible
511+
if hostnameALabels[a] != hostnameBLabels[b] {
489512
return false
490513
}
491514
}
492-
493-
return len(labelsA)-i == len(labelsB)-j
515+
return len(hostnameBLabels)-b == len(hostnameALabels)-a
494516
}
495517

496518
func routeMatchesListenerAllowedRoutes(
@@ -892,3 +914,115 @@ func IsInvalidKindError(err error) bool {
892914
_, ok := err.(*InvalidKindError)
893915
return ok
894916
}
917+
918+
// filterHostnames accepts a list of gateways and an HTTPRoute, and returns a copy of the HTTPRoute with only the hostnames that match the listener hostnames of the gateways.
919+
// If the HTTPRoute hostnames do not intersect with the listener hostnames of the gateways, it returns an ErrNoMatchingListenerHostname error.
920+
func filterHostnames(gateways []RouteParentRefContext, httpRoute *gatewayv1.HTTPRoute) (*gatewayv1.HTTPRoute, error) {
921+
filteredHostnames := make([]gatewayv1.Hostname, 0)
922+
923+
// If the HTTPRoute does not specify hostnames, we use the union of the listener hostnames of all supported gateways
924+
// If any supported listener does not specify a hostname, the HTTPRoute hostnames remain empty to match any hostname
925+
if len(httpRoute.Spec.Hostnames) == 0 {
926+
hostnames, matchAnyHost := getUnionOfGatewayHostnames(gateways)
927+
if matchAnyHost {
928+
return httpRoute, nil
929+
}
930+
filteredHostnames = hostnames
931+
} else {
932+
// If the HTTPRoute specifies hostnames, we need to find the intersection with the gateway listener hostnames
933+
for _, hostname := range httpRoute.Spec.Hostnames {
934+
if hostnameMatching := getMinimumHostnameIntersection(gateways, hostname); hostnameMatching != "" {
935+
filteredHostnames = append(filteredHostnames, hostnameMatching)
936+
}
937+
}
938+
if len(filteredHostnames) == 0 {
939+
return httpRoute, ErrNoMatchingListenerHostname
940+
}
941+
}
942+
943+
log.Debugw("filtered hostnames", zap.Any("httpRouteHostnames", httpRoute.Spec.Hostnames), zap.Any("hostnames", filteredHostnames))
944+
httpRoute.Spec.Hostnames = filteredHostnames
945+
return httpRoute, nil
946+
}
947+
948+
// getUnionOfGatewayHostnames returns the union of the hostnames specified in all supported gateways
949+
// The second return value indicates whether any listener can match any hostname
950+
func getUnionOfGatewayHostnames(gateways []RouteParentRefContext) ([]gatewayv1.Hostname, bool) {
951+
hostnames := make([]gatewayv1.Hostname, 0)
952+
953+
for _, gateway := range gateways {
954+
if gateway.ListenerName != "" {
955+
// If a listener name is specified, only check that listener
956+
for _, listener := range gateway.Gateway.Spec.Listeners {
957+
if string(listener.Name) == gateway.ListenerName {
958+
// If a listener does not specify a hostname, it can match any hostname
959+
if listener.Hostname == nil {
960+
return nil, true
961+
}
962+
hostnames = append(hostnames, *listener.Hostname)
963+
break
964+
}
965+
}
966+
} else {
967+
// Otherwise, check all listeners
968+
for _, listener := range gateway.Gateway.Spec.Listeners {
969+
// Only consider listeners that can effectively configure hostnames (HTTP, HTTPS, or TLS)
970+
if isListenerHostnameEffective(listener) {
971+
if listener.Hostname == nil {
972+
return nil, true
973+
}
974+
hostnames = append(hostnames, *listener.Hostname)
975+
}
976+
}
977+
}
978+
}
979+
980+
return hostnames, false
981+
}
982+
983+
// getMinimumHostnameIntersection returns the smallest intersection hostname
984+
// - If the listener hostname is empty, return the HTTPRoute hostname
985+
// - If the listener hostname is a wildcard of the HTTPRoute hostname, return the HTTPRoute hostname
986+
// - If the HTTPRoute hostname is a wildcard of the listener hostname, return the listener hostname
987+
// - If the HTTPRoute hostname and listener hostname are the same, return it
988+
// - If none of the above, return an empty string
989+
func getMinimumHostnameIntersection(gateways []RouteParentRefContext, hostname gatewayv1.Hostname) gatewayv1.Hostname {
990+
for _, gateway := range gateways {
991+
for _, listener := range gateway.Gateway.Spec.Listeners {
992+
// If a listener name is specified, only check that listener
993+
// If the listener name is not specified, check all listeners
994+
if gateway.ListenerName == "" || gateway.ListenerName == string(listener.Name) {
995+
if listener.Hostname == nil || *listener.Hostname == "" {
996+
return hostname
997+
}
998+
if HostnamesMatch(string(*listener.Hostname), string(hostname)) {
999+
return hostname
1000+
}
1001+
if HostnamesMatch(string(hostname), string(*listener.Hostname)) {
1002+
return *listener.Hostname
1003+
}
1004+
}
1005+
}
1006+
}
1007+
1008+
return ""
1009+
}
1010+
1011+
// isListenerHostnameEffective checks if a listener can specify a hostname to match the hostname in the request
1012+
// Basically, check if the listener uses HTTP, HTTPS, or TLS protocol
1013+
func isListenerHostnameEffective(listener gatewayv1.Listener) bool {
1014+
return listener.Protocol == gatewayv1.HTTPProtocolType ||
1015+
listener.Protocol == gatewayv1.HTTPSProtocolType ||
1016+
listener.Protocol == gatewayv1.TLSProtocolType
1017+
}
1018+
1019+
func isRouteAccepted(gateways []RouteParentRefContext) bool {
1020+
for _, gateway := range gateways {
1021+
for _, condition := range gateway.Conditions {
1022+
if condition.Type == string(gatewayv1.RouteConditionAccepted) && condition.Status == metav1.ConditionTrue {
1023+
return true
1024+
}
1025+
}
1026+
}
1027+
return false
1028+
}

test/conformance/conformance_test.go

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -34,11 +34,8 @@ var skippedTestsForTraditionalRoutes = []string{
3434
tests.HTTPRouteReferenceGrant.ShortName,
3535

3636
// TODO: HTTPRoute hostname intersection and listener hostname matching
37-
tests.HTTPRouteHostnameIntersection.ShortName,
38-
tests.HTTPRouteListenerHostnameMatching.ShortName,
3937

4038
tests.GatewayInvalidTLSConfiguration.ShortName,
41-
// tests.HTTPRouteInvalidBackendRefUnknownKind.ShortName,
4239
tests.HTTPRouteInvalidCrossNamespaceParentRef.ShortName,
4340
tests.HTTPRouteInvalidNonExistentBackendRef.ShortName,
4441
tests.HTTPRouteInvalidParentRefNotMatchingSectionName.ShortName,

0 commit comments

Comments
 (0)