Skip to content

Commit

Permalink
gateway-api: Add support for listener isolation
Browse files Browse the repository at this point in the history
This commit is to support Listener Isolation concept from the upstream,
which allows at most one Listener matches a request, and only Routes
attached to that Listener are used for routing.

Relates: kubernetes-sigs/gateway-api#2465
Relates: kubernetes-sigs/gateway-api#3047
Signed-off-by: Tam Mach <tam.mach@cilium.io>
  • Loading branch information
sayboras committed Jun 6, 2024
1 parent 3035b73 commit 0d76e3b
Show file tree
Hide file tree
Showing 6 changed files with 71 additions and 30 deletions.
11 changes: 6 additions & 5 deletions operator/pkg/gateway-api/gateway_reconcile.go
Original file line number Diff line number Diff line change
Expand Up @@ -226,7 +226,7 @@ func (r *gatewayReconciler) updateStatus(ctx context.Context, original *gatewayv
func (r *gatewayReconciler) filterHTTPRoutesByGateway(ctx context.Context, gw *gatewayv1.Gateway, routes []gatewayv1.HTTPRoute) []gatewayv1.HTTPRoute {
var filtered []gatewayv1.HTTPRoute
for _, route := range routes {
if isAttachable(ctx, gw, &route, route.Status.Parents) && isAllowed(ctx, r.Client, gw, &route) && len(computeHosts(gw, route.Spec.Hostnames)) > 0 {
if isAttachable(ctx, gw, &route, route.Status.Parents) && isAllowed(ctx, r.Client, gw, &route) && len(computeHosts(gw, route.Spec.Hostnames, nil)) > 0 {
filtered = append(filtered, route)
}
}
Expand All @@ -236,7 +236,7 @@ func (r *gatewayReconciler) filterHTTPRoutesByGateway(ctx context.Context, gw *g
func (r *gatewayReconciler) filterGRPCRoutesByGateway(ctx context.Context, gw *gatewayv1.Gateway, routes []gatewayv1.GRPCRoute) []gatewayv1.GRPCRoute {
var filtered []gatewayv1.GRPCRoute
for _, route := range routes {
if isAttachable(ctx, gw, &route, route.Status.Parents) && isAllowed(ctx, r.Client, gw, &route) && len(computeHosts(gw, route.Spec.Hostnames)) > 0 {
if isAttachable(ctx, gw, &route, route.Status.Parents) && isAllowed(ctx, r.Client, gw, &route) && len(computeHosts(gw, route.Spec.Hostnames, nil)) > 0 {
filtered = append(filtered, route)
}
}
Expand All @@ -248,7 +248,7 @@ func (r *gatewayReconciler) filterHTTPRoutesByListener(ctx context.Context, gw *
for _, route := range routes {
if isAttachable(ctx, gw, &route, route.Status.Parents) &&
isAllowed(ctx, r.Client, gw, &route) &&
len(computeHostsForListener(listener, route.Spec.Hostnames)) > 0 &&
len(computeHostsForListener(listener, route.Spec.Hostnames, nil)) > 0 &&
parentRefMatched(gw, listener, route.GetNamespace(), route.Spec.ParentRefs) {
filtered = append(filtered, route)
}
Expand All @@ -275,7 +275,8 @@ func parentRefMatched(gw *gatewayv1.Gateway, listener *gatewayv1.Listener, route
func (r *gatewayReconciler) filterTLSRoutesByGateway(ctx context.Context, gw *gatewayv1.Gateway, routes []gatewayv1alpha2.TLSRoute) []gatewayv1alpha2.TLSRoute {
var filtered []gatewayv1alpha2.TLSRoute
for _, route := range routes {
if isAttachable(ctx, gw, &route, route.Status.Parents) && isAllowed(ctx, r.Client, gw, &route) && len(computeHosts(gw, route.Spec.Hostnames)) > 0 {
if isAttachable(ctx, gw, &route, route.Status.Parents) && isAllowed(ctx, r.Client, gw, &route) &&
len(computeHosts(gw, route.Spec.Hostnames, nil)) > 0 {
filtered = append(filtered, route)
}
}
Expand All @@ -287,7 +288,7 @@ func (r *gatewayReconciler) filterTLSRoutesByListener(ctx context.Context, gw *g
for _, route := range routes {
if isAttachable(ctx, gw, &route, route.Status.Parents) &&
isAllowed(ctx, r.Client, gw, &route) &&
len(computeHostsForListener(listener, route.Spec.Hostnames)) > 0 &&
len(computeHostsForListener(listener, route.Spec.Hostnames, nil)) > 0 &&
parentRefMatched(gw, listener, route.GetNamespace(), route.Spec.ParentRefs) {
filtered = append(filtered, route)
}
Expand Down
8 changes: 4 additions & 4 deletions operator/pkg/gateway-api/helpers.go
Original file line number Diff line number Diff line change
Expand Up @@ -109,17 +109,17 @@ func isKindAllowed(listener gatewayv1.Listener, route metav1.Object) bool {
return false
}

func computeHosts[T ~string](gw *gatewayv1.Gateway, hostnames []T) []string {
func computeHosts[T ~string](gw *gatewayv1.Gateway, hostnames []T, excludeHostNames []T) []string {
hosts := make([]string, 0, len(hostnames))
for _, listener := range gw.Spec.Listeners {
hosts = append(hosts, computeHostsForListener(&listener, hostnames)...)
hosts = append(hosts, computeHostsForListener(&listener, hostnames, excludeHostNames)...)
}

return hosts
}

func computeHostsForListener[T ~string](listener *gatewayv1.Listener, hostnames []T) []string {
return model.ComputeHosts(toStringSlice(hostnames), (*string)(listener.Hostname))
func computeHostsForListener[T ~string](listener *gatewayv1.Listener, hostnames []T, excludeHostNames []T) []string {
return model.ComputeHosts(toStringSlice(hostnames), (*string)(listener.Hostname), toStringSlice(excludeHostNames))
}

func toStringSlice[T ~string](s []T) []string {
Expand Down
4 changes: 2 additions & 2 deletions operator/pkg/gateway-api/routechecks/gateway_checks.go
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@ func CheckGatewayAllowedForNamespace(input Input, parentRef gatewayv1.ParentRefe
continue
}

if listener.Hostname != nil && (len(computeHostsForListener(&listener, input.GetHostnames())) > 0) {
if listener.Hostname != nil && (len(computeHostsForListener(&listener, input.GetHostnames(), nil)) > 0) {
continue
}

Expand Down Expand Up @@ -150,7 +150,7 @@ func CheckGatewayMatchingHostnames(input Input, parentRef gatewayv1.ParentRefere
return false, nil
}

if len(computeHosts(gw, input.GetHostnames())) == 0 {
if len(computeHosts(gw, input.GetHostnames(), nil)) == 0 {

input.SetParentCondition(parentRef, metav1.Condition{
Type: string(gatewayv1.RouteConditionAccepted),
Expand Down
8 changes: 4 additions & 4 deletions operator/pkg/gateway-api/routechecks/hostnames.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,17 +9,17 @@ import (
"github.com/cilium/cilium/operator/pkg/model"
)

func computeHosts[T ~string](gw *gatewayv1.Gateway, hostnames []T) []string {
func computeHosts[T ~string](gw *gatewayv1.Gateway, hostnames []T, excludeHostNames []T) []string {
hosts := make([]string, 0, len(hostnames))
for _, listener := range gw.Spec.Listeners {
hosts = append(hosts, computeHostsForListener(&listener, hostnames)...)
hosts = append(hosts, computeHostsForListener(&listener, hostnames, excludeHostNames)...)
}

return hosts
}

func computeHostsForListener[T ~string](listener *gatewayv1.Listener, hostnames []T) []string {
return model.ComputeHosts(toStringSlice(hostnames), (*string)(listener.Hostname))
func computeHostsForListener[T ~string](listener *gatewayv1.Listener, hostnames []T, excludeHostNames []T) []string {
return model.ComputeHosts(toStringSlice(hostnames), (*string)(listener.Hostname), toStringSlice(excludeHostNames))
}

func toStringSlice[T ~string](s []T) []string {
Expand Down
31 changes: 27 additions & 4 deletions operator/pkg/model/helpers.go
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,9 @@ func AddSource(sourceList []FullyQualifiedResource, source FullyQualifiedResourc
// ComputeHosts returns a list of the intersecting hostnames between the route and the listener.
// The below function is inspired from https://github.com/envoyproxy/gateway/blob/main/internal/gatewayapi/helpers.go.
// Special thanks to Envoy team.
func ComputeHosts(routeHostnames []string, listenerHostname *string) []string {
// The function takes a list of route hostnames, a listener hostname, and a list of other listener hostnames.
// Note that the listenerHostname value will be skipped if it is present in the otherListenerHosts list.
func ComputeHosts(routeHostnames []string, listenerHostname *string, otherListenerHosts []string) []string {
var listenerHostnameVal string
if listenerHostname != nil {
listenerHostnameVal = *listenerHostname
Expand All @@ -46,21 +48,42 @@ func ComputeHosts(routeHostnames []string, listenerHostname *string) []string {

var hostnames []string

otherListenerIntersection := func(routeHostname, actualListenerHostname string) bool {
for _, h := range otherListenerHosts {
if h == listenerHostnameVal {
continue
}
if routeHostname == h {
return true
}
if strings.HasPrefix(h, allHosts) &&
hostnameMatchesWildcardHostname(routeHostname, h) &&
len(h) > len(actualListenerHostname) {
return true
}
}

return false
}

for i := range routeHostnames {
routeHostname := routeHostnames[i]

switch {
// No listener hostname: use the route hostname.
// No listener hostname: use the route hostname if there is no overlapping with other listener hostnames.
case len(listenerHostnameVal) == 0:
hostnames = append(hostnames, routeHostname)
if !otherListenerIntersection(routeHostname, listenerHostnameVal) {
hostnames = append(hostnames, routeHostname)
}

// Listener hostname matches the route hostname: use it.
case listenerHostnameVal == routeHostname:
hostnames = append(hostnames, routeHostname)

// Listener has a wildcard hostname: check if the route hostname matches.
case strings.HasPrefix(listenerHostnameVal, allHosts):
if hostnameMatchesWildcardHostname(routeHostname, listenerHostnameVal) {
if hostnameMatchesWildcardHostname(routeHostname, listenerHostnameVal) &&
!otherListenerIntersection(routeHostname, listenerHostnameVal) {
hostnames = append(hostnames, routeHostname)
}

Expand Down
39 changes: 28 additions & 11 deletions operator/pkg/model/ingestion/gateway.go
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,15 @@ func GatewayAPI(input Input) ([]model.HTTPListener, []model.TLSPassthroughListen
}
}

// Find all the listener host names, so that we can match them with the routes
// Gateway API spec guarantees that the hostnames are unique across all listeners
var allListenerHostNames []string
for _, l := range input.Gateway.Spec.Listeners {
if l.Hostname != nil {
allListenerHostNames = append(allListenerHostNames, toHostname(l.Hostname))
}
}

for _, l := range input.Gateway.Spec.Listeners {
if l.Protocol != gatewayv1.HTTPProtocolType &&
l.Protocol != gatewayv1.HTTPSProtocolType &&
Expand All @@ -62,8 +71,8 @@ func GatewayAPI(input Input) ([]model.HTTPListener, []model.TLSPassthroughListen
}

var httpRoutes []model.HTTPRoute
httpRoutes = append(httpRoutes, toHTTPRoutes(l, input.HTTPRoutes, input.Services, input.ServiceImports, input.ReferenceGrants)...)
httpRoutes = append(httpRoutes, toGRPCRoutes(l, input.GRPCRoutes, input.Services, input.ServiceImports, input.ReferenceGrants)...)
httpRoutes = append(httpRoutes, toHTTPRoutes(l, allListenerHostNames, input.HTTPRoutes, input.Services, input.ServiceImports, input.ReferenceGrants)...)
httpRoutes = append(httpRoutes, toGRPCRoutes(l, allListenerHostNames, input.GRPCRoutes, input.Services, input.ServiceImports, input.ReferenceGrants)...)
resHTTP = append(resHTTP, model.HTTPListener{
Name: string(l.Name),
Sources: []model.FullyQualifiedResource{
Expand Down Expand Up @@ -97,7 +106,7 @@ func GatewayAPI(input Input) ([]model.HTTPListener, []model.TLSPassthroughListen
},
Port: uint32(l.Port),
Hostname: toHostname(l.Hostname),
Routes: toTLSRoutes(l, input.TLSRoutes, input.Services, input.ServiceImports, input.ReferenceGrants),
Routes: toTLSRoutes(l, allListenerHostNames, input.TLSRoutes, input.Services, input.ServiceImports, input.ReferenceGrants),
Infrastructure: infra,
})
}
Expand Down Expand Up @@ -134,10 +143,14 @@ func getBackendServiceName(namespace string, services []corev1.Service, serviceI
return svcName, nil
}

func toHTTPRoutes(listener gatewayv1.Listener, input []gatewayv1.HTTPRoute, services []corev1.Service, serviceImports []mcsapiv1alpha1.ServiceImport, grants []gatewayv1beta1.ReferenceGrant) []model.HTTPRoute {
func toHTTPRoutes(listener gatewayv1.Listener,
allListenerHostNames []string,
input []gatewayv1.HTTPRoute,
services []corev1.Service,
serviceImports []mcsapiv1alpha1.ServiceImport,
grants []gatewayv1beta1.ReferenceGrant) []model.HTTPRoute {
var httpRoutes []model.HTTPRoute
for _, r := range input {

listenerIsParent := false
// Check parents to see if r can attach to them.
// We have to consider _both_ SectionName and Port
Expand All @@ -158,7 +171,6 @@ func toHTTPRoutes(listener gatewayv1.Listener, input []gatewayv1.HTTPRoute, serv

if parent.Port != nil && *parent.Port != listener.Port {
// If SectionName is set and equal, but Port is set and _unequal_,
//
continue
}

Expand All @@ -183,7 +195,7 @@ func toHTTPRoutes(listener gatewayv1.Listener, input []gatewayv1.HTTPRoute, serv
continue
}

computedHost := model.ComputeHosts(toStringSlice(r.Spec.Hostnames), (*string)(listener.Hostname))
computedHost := model.ComputeHosts(toStringSlice(r.Spec.Hostnames), (*string)(listener.Hostname), allListenerHostNames)
// No matching host, skip this route
if len(computedHost) == 0 {
continue
Expand Down Expand Up @@ -346,7 +358,12 @@ func toTimeout(timeouts *gatewayv1.HTTPRouteTimeouts) model.Timeout {
return res
}

func toGRPCRoutes(listener gatewayv1beta1.Listener, input []gatewayv1.GRPCRoute, services []corev1.Service, serviceImports []mcsapiv1alpha1.ServiceImport, grants []gatewayv1beta1.ReferenceGrant) []model.HTTPRoute {
func toGRPCRoutes(listener gatewayv1beta1.Listener,
allListenerHostNames []string,
input []gatewayv1.GRPCRoute,
services []corev1.Service,
serviceImports []mcsapiv1alpha1.ServiceImport,
grants []gatewayv1beta1.ReferenceGrant) []model.HTTPRoute {
var grpcRoutes []model.HTTPRoute
for _, r := range input {
isListener := false
Expand All @@ -360,7 +377,7 @@ func toGRPCRoutes(listener gatewayv1beta1.Listener, input []gatewayv1.GRPCRoute,
continue
}

computedHost := model.ComputeHosts(toStringSlice(r.Spec.Hostnames), (*string)(listener.Hostname))
computedHost := model.ComputeHosts(toStringSlice(r.Spec.Hostnames), (*string)(listener.Hostname), allListenerHostNames)
// No matching host, skip this route
if len(computedHost) == 0 {
continue
Expand Down Expand Up @@ -460,7 +477,7 @@ func toGRPCRoutes(listener gatewayv1beta1.Listener, input []gatewayv1.GRPCRoute,
return grpcRoutes
}

func toTLSRoutes(listener gatewayv1beta1.Listener, input []gatewayv1alpha2.TLSRoute, services []corev1.Service, serviceImports []mcsapiv1alpha1.ServiceImport, grants []gatewayv1beta1.ReferenceGrant) []model.TLSPassthroughRoute {
func toTLSRoutes(listener gatewayv1beta1.Listener, allListenerHostNames []string, input []gatewayv1alpha2.TLSRoute, services []corev1.Service, serviceImports []mcsapiv1alpha1.ServiceImport, grants []gatewayv1beta1.ReferenceGrant) []model.TLSPassthroughRoute {
var tlsRoutes []model.TLSPassthroughRoute
for _, r := range input {
isListener := false
Expand All @@ -474,7 +491,7 @@ func toTLSRoutes(listener gatewayv1beta1.Listener, input []gatewayv1alpha2.TLSRo
continue
}

computedHost := model.ComputeHosts(toStringSlice(r.Spec.Hostnames), (*string)(listener.Hostname))
computedHost := model.ComputeHosts(toStringSlice(r.Spec.Hostnames), (*string)(listener.Hostname), allListenerHostNames)
// No matching host, skip this route
if len(computedHost) == 0 {
continue
Expand Down

0 comments on commit 0d76e3b

Please sign in to comment.