Skip to content

Commit bd1a1dc

Browse files
committed
Merge remote-tracking branch 'origin/release-v2-dev' into fix/conformance-test-case-HTTPRouteInvalidCrossNamespaceParentRef
# Conflicts: # internal/controller/httproute_controller.go # internal/controller/utils.go # test/conformance/conformance_test.go
2 parents f797ffe + 90574fc commit bd1a1dc

File tree

3 files changed

+163
-22
lines changed

3 files changed

+163
-22
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
SetRouteConditionResolvedRefs(&parentStatus, hr.GetGeneration(), resolveRefStatus.status, resolveRefStatus.msg)
221233
SetRouteConditionAccepted(&parentStatus, hr.GetGeneration(), acceptStatus.status, acceptStatus.msg)
222234
var accepted = !slices.ContainsFunc(parentStatus.Conditions, func(condition metav1.Condition) bool {

internal/controller/utils.go

Lines changed: 147 additions & 15 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"
@@ -49,6 +50,10 @@ const (
4950

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

53+
var (
54+
ErrNoMatchingListenerHostname = errors.New("no matching hostnames in listener")
55+
)
56+
5257
// IsDefaultIngressClass returns whether an IngressClass is the default IngressClass.
5358
func IsDefaultIngressClass(obj client.Object) bool {
5459
if ingressClass, ok := obj.(*networkingv1.IngressClass); ok {
@@ -240,6 +245,9 @@ func SetRouteConditionAccepted(routeParentStatus *gatewayv1.RouteParentStatus, g
240245
Message: message,
241246
LastTransitionTime: metav1.Now(),
242247
}
248+
if message == ErrNoMatchingListenerHostname.Error() {
249+
condition.Reason = string(gatewayv1.RouteReasonNoMatchingListenerHostname)
250+
}
243251

244252
if !IsConditionPresentAndEqual(routeParentStatus.Conditions, condition) && !slices.ContainsFunc(routeParentStatus.Conditions, func(item metav1.Condition) bool {
245253
return item.Type == condition.Type && item.Status == metav1.ConditionFalse && condition.Status == metav1.ConditionTrue
@@ -461,39 +469,51 @@ func HostnamesIntersect(a, b string) bool {
461469
return HostnamesMatch(a, b) || HostnamesMatch(b, a)
462470
}
463471

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

468-
var i, j int
480+
var a, b int
469481
var wildcard bool
470482

471-
for i, j = 0, 0; i < len(labelsA) && j < len(labelsB); i, j = i+1, j+1 {
483+
// iterate over the parts of both the hostnames
484+
for a, b = 0, 0; a < len(hostnameALabels) && b < len(hostnameBLabels); a, b = a+1, b+1 {
485+
var matchFound bool
486+
487+
// if the current part of B is a wildcard, we need to find the first
488+
// A part that matches with the following B part
472489
if wildcard {
473-
for ; j < len(labelsB); j++ {
474-
if labelsA[i] == labelsB[j] {
490+
for ; b < len(hostnameBLabels); b++ {
491+
if hostnameALabels[a] == hostnameBLabels[b] {
492+
matchFound = true
475493
break
476494
}
477495
}
478-
if j == len(labelsB) {
479-
return false
480-
}
481496
}
482497

483-
if labelsA[i] == "*" {
498+
// if no match was found, the hostnames don't match
499+
if wildcard && !matchFound {
500+
return false
501+
}
502+
503+
// check if at least on of the current parts are a wildcard; if so, continue
504+
if hostnameALabels[a] == "*" {
484505
wildcard = true
485-
j--
486506
continue
487507
}
488-
508+
// reset the wildcard variables
489509
wildcard = false
490510

491-
if labelsA[i] != labelsB[j] {
511+
// if the current a part is different from the b part, the hostnames are incompatible
512+
if hostnameALabels[a] != hostnameBLabels[b] {
492513
return false
493514
}
494515
}
495-
496-
return len(labelsA)-i == len(labelsB)-j
516+
return len(hostnameBLabels)-b == len(hostnameALabels)-a
497517
}
498518

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

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.HTTPRouteInvalidNonExistentBackendRef.ShortName,
4340
tests.HTTPRouteInvalidParentRefNotMatchingSectionName.ShortName,
4441
}

0 commit comments

Comments
 (0)