Skip to content
Merged
Show file tree
Hide file tree
Changes from 4 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 14 additions & 4 deletions internal/controller/httproute_controller.go
Original file line number Diff line number Diff line change
Expand Up @@ -201,11 +201,24 @@ func (r *HTTPRouteReconciler) Reconcile(ctx context.Context, req ctrl.Request) (
}
ProcessBackendTrafficPolicy(r.Client, r.Log, tctx)

if err := r.Provider.Update(ctx, tctx, hr); err != nil {
filteredHTTPRoute, err := filterHostnames(gateways, hr.DeepCopy())
if err != nil {
acceptStatus.status = false
acceptStatus.msg = err.Error()
}

if isRouteAccepted(gateways) && err == nil {
routeToUpdate := hr
if filteredHTTPRoute != nil {
r.Log.Info("filteredHTTPRoute", "filteredHTTPRoute", filteredHTTPRoute)
routeToUpdate = filteredHTTPRoute
}
if err := r.Provider.Update(ctx, tctx, routeToUpdate); err != nil {
acceptStatus.status = false
acceptStatus.msg = err.Error()
}
}

// TODO: diff the old and new status
hr.Status.Parents = make([]gatewayv1.RouteParentStatus, 0, len(gateways))
for _, gateway := range gateways {
Expand All @@ -214,9 +227,6 @@ func (r *HTTPRouteReconciler) Reconcile(ctx context.Context, req ctrl.Request) (
for _, condition := range gateway.Conditions {
parentStatus.Conditions = MergeCondition(parentStatus.Conditions, condition)
}
if gateway.ListenerName == "" {
continue
}
SetRouteConditionAccepted(&parentStatus, hr.GetGeneration(), acceptStatus.status, acceptStatus.msg)
SetRouteConditionResolvedRefs(&parentStatus, hr.GetGeneration(), resolveRefStatus.status, resolveRefStatus.msg)
hr.Status.Parents = append(hr.Status.Parents, parentStatus)
Expand Down
166 changes: 150 additions & 16 deletions internal/controller/utils.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ package controller

import (
"context"
"errors"
"fmt"
"path"
"reflect"
Expand Down Expand Up @@ -48,6 +49,10 @@ const (

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

var (
ErrNoMatchingListenerHostname = errors.New("no matching hostnames in listener")
)

// IsDefaultIngressClass returns whether an IngressClass is the default IngressClass.
func IsDefaultIngressClass(obj client.Object) bool {
if ingressClass, ok := obj.(*networkingv1.IngressClass); ok {
Expand Down Expand Up @@ -229,10 +234,15 @@ func SetRouteConditionAccepted(routeParentStatus *gatewayv1.RouteParentStatus, g
conditionStatus = metav1.ConditionFalse
}

reason := gatewayv1.RouteReasonAccepted
if message == ErrNoMatchingListenerHostname.Error() {
reason = gatewayv1.RouteReasonNoMatchingListenerHostname
}
Comment on lines +237 to +240
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

靠 message 来传递 reason 有点怪怪的,是否可以定义一种 error 类型,比如

type ReasonErr struct {
    Reason string
    Message string
}

func (e ReasonErr) Error() string { return e.Message }

在前文相关函数中直接返回这样类型的错误,后文中用 errors.As 来判断错误类型,如果是 ReasonErr,可以取出 Reson。

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

OK, but let's wait for the next PR to make a unified adjustment.


condition := metav1.Condition{
Type: string(gatewayv1.RouteConditionAccepted),
Status: conditionStatus,
Reason: string(gatewayv1.RouteReasonAccepted),
Reason: string(reason),
ObservedGeneration: generation,
Message: message,
LastTransitionTime: metav1.Now(),
Expand Down Expand Up @@ -458,39 +468,51 @@ func HostnamesIntersect(a, b string) bool {
return HostnamesMatch(a, b) || HostnamesMatch(b, a)
}

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

var i, j int
var a, b int
var wildcard bool

for i, j = 0, 0; i < len(labelsA) && j < len(labelsB); i, j = i+1, j+1 {
// iterate over the parts of both the hostnames
for a, b = 0, 0; a < len(hostnameALabels) && b < len(hostnameBLabels); a, b = a+1, b+1 {
var matchFound bool

// if the current part of B is a wildcard, we need to find the first
// A part that matches with the following B part
if wildcard {
for ; j < len(labelsB); j++ {
if labelsA[i] == labelsB[j] {
for ; b < len(hostnameBLabels); b++ {
if hostnameALabels[a] == hostnameBLabels[b] {
matchFound = true
break
}
}
if j == len(labelsB) {
return false
}
}

if labelsA[i] == "*" {
// if no match was found, the hostnames don't match
if wildcard && !matchFound {
return false
}

// check if at least on of the current parts are a wildcard; if so, continue
if hostnameALabels[a] == "*" {
wildcard = true
j--
continue
}

// reset the wildcard variables
wildcard = false

if labelsA[i] != labelsB[j] {
// if the current a part is different from the b part, the hostnames are incompatible
if hostnameALabels[a] != hostnameBLabels[b] {
return false
}
}

return len(labelsA)-i == len(labelsB)-j
return len(hostnameBLabels)-b == len(hostnameALabels)-a
}

func routeMatchesListenerAllowedRoutes(
Expand Down Expand Up @@ -892,3 +914,115 @@ func IsInvalidKindError(err error) bool {
_, ok := err.(*InvalidKindError)
return ok
}

// 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.
// If the HTTPRoute hostnames do not intersect with the listener hostnames of the gateways, it returns an ErrNoMatchingListenerHostname error.
func filterHostnames(gateways []RouteParentRefContext, httpRoute *gatewayv1.HTTPRoute) (*gatewayv1.HTTPRoute, error) {
filteredHostnames := make([]gatewayv1.Hostname, 0)

// If the HTTPRoute does not specify hostnames, we use the union of the listener hostnames of all supported gateways
// If any supported listener does not specify a hostname, the HTTPRoute hostnames remain empty to match any hostname
if len(httpRoute.Spec.Hostnames) == 0 {
hostnames, matchAnyHost := getUnionOfGatewayHostnames(gateways)
if matchAnyHost {
return httpRoute, nil
}
filteredHostnames = hostnames
} else {
// If the HTTPRoute specifies hostnames, we need to find the intersection with the gateway listener hostnames
for _, hostname := range httpRoute.Spec.Hostnames {
if hostnameMatching := getMinimumHostnameIntersection(gateways, hostname); hostnameMatching != "" {
filteredHostnames = append(filteredHostnames, hostnameMatching)
}
}
if len(filteredHostnames) == 0 {
return httpRoute, ErrNoMatchingListenerHostname
}
}

log.Debugw("filtered hostnames", zap.Any("httpRouteHostnames", httpRoute.Spec.Hostnames), zap.Any("hostnames", filteredHostnames))
httpRoute.Spec.Hostnames = filteredHostnames
return httpRoute, nil
}

// getUnionOfGatewayHostnames returns the union of the hostnames specified in all supported gateways
// The second return value indicates whether any listener can match any hostname
func getUnionOfGatewayHostnames(gateways []RouteParentRefContext) ([]gatewayv1.Hostname, bool) {
hostnames := make([]gatewayv1.Hostname, 0)

for _, gateway := range gateways {
if gateway.ListenerName != "" {
// If a listener name is specified, only check that listener
for _, listener := range gateway.Gateway.Spec.Listeners {
if string(listener.Name) == gateway.ListenerName {
// If a listener does not specify a hostname, it can match any hostname
if listener.Hostname == nil {
return nil, true
}
hostnames = append(hostnames, *listener.Hostname)
break
}
}
} else {
// Otherwise, check all listeners
for _, listener := range gateway.Gateway.Spec.Listeners {
// Only consider listeners that can effectively configure hostnames (HTTP, HTTPS, or TLS)
if isListenerHostnameEffective(listener) {
if listener.Hostname == nil {
return nil, true
}
hostnames = append(hostnames, *listener.Hostname)
}
}
}
}

return hostnames, false
}

// getMinimumHostnameIntersection returns the smallest intersection hostname
// - If the listener hostname is empty, return the HTTPRoute hostname
// - If the listener hostname is a wildcard of the HTTPRoute hostname, return the HTTPRoute hostname
// - If the HTTPRoute hostname is a wildcard of the listener hostname, return the listener hostname
// - If the HTTPRoute hostname and listener hostname are the same, return it
// - If none of the above, return an empty string
func getMinimumHostnameIntersection(gateways []RouteParentRefContext, hostname gatewayv1.Hostname) gatewayv1.Hostname {
for _, gateway := range gateways {
for _, listener := range gateway.Gateway.Spec.Listeners {
// If a listener name is specified, only check that listener
// If the listener name is not specified, check all listeners
if gateway.ListenerName == "" || gateway.ListenerName == string(listener.Name) {
if listener.Hostname == nil || *listener.Hostname == "" {
return hostname
}
if HostnamesMatch(string(*listener.Hostname), string(hostname)) {
return hostname
}
if HostnamesMatch(string(hostname), string(*listener.Hostname)) {
return *listener.Hostname
}
}
}
}

return ""
}

// isListenerHostnameEffective checks if a listener can specify a hostname to match the hostname in the request
// Basically, check if the listener uses HTTP, HTTPS, or TLS protocol
func isListenerHostnameEffective(listener gatewayv1.Listener) bool {
return listener.Protocol == gatewayv1.HTTPProtocolType ||
listener.Protocol == gatewayv1.HTTPSProtocolType ||
listener.Protocol == gatewayv1.TLSProtocolType
}

func isRouteAccepted(gateways []RouteParentRefContext) bool {
for _, gateway := range gateways {
for _, condition := range gateway.Conditions {
if condition.Type == string(gatewayv1.RouteConditionAccepted) && condition.Status == metav1.ConditionTrue {
return true
}
}
}
return false
}
5 changes: 2 additions & 3 deletions test/conformance/conformance_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -34,11 +34,10 @@ var skippedTestsForTraditionalRoutes = []string{
tests.HTTPRouteReferenceGrant.ShortName,

// TODO: HTTPRoute hostname intersection and listener hostname matching
tests.HTTPRouteHostnameIntersection.ShortName,
tests.HTTPRouteListenerHostnameMatching.ShortName,
// tests.HTTPRouteHostnameIntersection.ShortName,
// tests.HTTPRouteListenerHostnameMatching.ShortName,
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It can be deleted


tests.GatewayInvalidTLSConfiguration.ShortName,
// tests.HTTPRouteInvalidBackendRefUnknownKind.ShortName,
tests.HTTPRouteInvalidCrossNamespaceParentRef.ShortName,
tests.HTTPRouteInvalidNonExistentBackendRef.ShortName,
tests.HTTPRouteInvalidParentRefNotMatchingSectionName.ShortName,
Expand Down
Loading