Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

gateway2: allow route delegation using well known label #10561

Merged
merged 1 commit into from
Jan 10, 2025
Merged
Show file tree
Hide file tree
Changes from all 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
17 changes: 17 additions & 0 deletions changelog/v1.19.0-beta3/deleg-label.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
changelog:
- type: NEW_FEATURE
issueLink: https://github.com/solo-io/solo-projects/issues/7626
resolvesIssue: false
description: |
gateway2: allow route delegation using wellknown label
There is a product requirement to enable users to use
a label to select HTTPRoutes to delegate to instead
of GVK ref to other HTTPRoutes (includes wildcards).
To strike a balance between flexibility and performance,
this change implements the proposal to use a well known
label `delegation.gateway.solo.io/label=<value>` to
allow users to delegate to other HTTPRoutes using a label.
HTTPRoutes are indexed using this well known label key that
enable O(1) lookups of routes matching this label value.
4 changes: 4 additions & 0 deletions projects/gateway2/controller/controller.go
Original file line number Diff line number Diff line change
Expand Up @@ -123,6 +123,10 @@ func (c *controllerBuilder) addIndexes(ctx context.Context) error {
if err := c.cfg.Mgr.GetFieldIndexer().IndexField(ctx, &apiv1.HTTPRoute{}, query.HttpRouteTargetField, query.IndexerByObjType); err != nil {
errs = append(errs, err)
}
// Index HTTPRoutes by the delegation.gateway.solo.io/label label value to lookup delegatee routes using the label
if err := c.cfg.Mgr.GetFieldIndexer().IndexField(ctx, &apiv1.HTTPRoute{}, query.HttpRouteDelegatedLabelSelector, query.IndexByHTTPRouteDelegationLabelSelector); err != nil {
errs = append(errs, err)
}

// Index for ReferenceGrant
if err := c.cfg.Mgr.GetFieldIndexer().IndexField(ctx, &apiv1beta1.ReferenceGrant{}, query.ReferenceGrantFromField, query.IndexerByObjType); err != nil {
Expand Down
48 changes: 27 additions & 21 deletions projects/gateway2/query/httproute.go
Original file line number Diff line number Diff line change
Expand Up @@ -257,8 +257,8 @@ func (r *gatewayQueries) getDelegatedChildren(
for _, parentRule := range parent.Spec.Rules {
var refChildren []*RouteInfo
for _, backendRef := range parentRule.BackendRefs {
// Check if the backend reference is an HTTPRoute
if !backendref.RefIsHTTPRoute(backendRef.BackendObjectReference) {
// Check if the backend delegated route reference
if !backendref.RefIsDelegatedHTTPRoute(backendRef.BackendObjectReference) {
continue
}
// Fetch child routes based on the backend reference
Expand Down Expand Up @@ -302,35 +302,41 @@ func (r *gatewayQueries) fetchChildRoutes(
backendRef gwv1.HTTPBackendRef,
) ([]gwv1.HTTPRoute, error) {
delegatedNs := parentNamespace
if !backendref.RefIsHTTPRoute(backendRef.BackendObjectReference) {
return nil, nil
}
// Use the namespace specified in the backend reference if available
if backendRef.Namespace != nil {
delegatedNs = string(*backendRef.Namespace)
}

var refChildren []gwv1.HTTPRoute
if string(backendRef.Name) == "" || string(backendRef.Name) == "*" {
// Handle wildcard references by listing all HTTPRoutes in the specified namespace
var hrlist gwv1.HTTPRouteList
err := r.client.List(ctx, &hrlist, client.InNamespace(delegatedNs))
if err != nil {
return nil, err
}
refChildren = append(refChildren, hrlist.Items...)
} else {
// Lookup a specific child route by its name
delegatedRef := types.NamespacedName{
Namespace: delegatedNs,
Name: string(backendRef.Name),
if backendref.RefIsHTTPRoute(backendRef.BackendObjectReference) {
if string(backendRef.Name) == "" || string(backendRef.Name) == "*" {
// Handle wildcard references by listing all HTTPRoutes in the specified namespace
var hrlist gwv1.HTTPRouteList
err := r.client.List(ctx, &hrlist, client.InNamespace(delegatedNs))
if err != nil {
return nil, err
}
refChildren = hrlist.Items
} else {
// Lookup a specific child route by its name
delegatedRef := types.NamespacedName{
Namespace: delegatedNs,
Name: string(backendRef.Name),
}
child := &gwv1.HTTPRoute{}
err := r.client.Get(ctx, delegatedRef, child)
if err != nil {
return nil, err
}
refChildren = append(refChildren, *child)
}
child := &gwv1.HTTPRoute{}
err := r.client.Get(ctx, delegatedRef, child)
} else if backendref.RefIsHTTPRouteDelegationLabelSelector(backendRef.BackendObjectReference) {
var hrlist gwv1.HTTPRouteList
err := r.client.List(ctx, &hrlist, client.InNamespace(delegatedNs), client.MatchingFields{HttpRouteDelegatedLabelSelector: string(backendRef.Name)})
if err != nil {
return nil, err
}
refChildren = append(refChildren, *child)
refChildren = hrlist.Items
}
// Check if no child routes were resolved and log an error if needed
if len(refChildren) == 0 {
Expand Down
17 changes: 14 additions & 3 deletions projects/gateway2/query/indexers.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,15 +13,17 @@ import (
)

const (
HttpRouteTargetField = "http-route-target"
TcpRouteTargetField = "tcp-route-target"
ReferenceGrantFromField = "ref-grant-from"
HttpRouteTargetField = "http-route-target"
HttpRouteDelegatedLabelSelector = "http-route-delegated-label-selector"
TcpRouteTargetField = "tcp-route-target"
ReferenceGrantFromField = "ref-grant-from"
)

// IterateIndices calls the provided function for each indexable object with the appropriate indexer function.
func IterateIndices(f func(client.Object, string, client.IndexerFunc) error) error {
return errors.Join(
f(&gwv1.HTTPRoute{}, HttpRouteTargetField, IndexerByObjType),
f(&gwv1.HTTPRoute{}, HttpRouteDelegatedLabelSelector, IndexByHTTPRouteDelegationLabelSelector),
f(&gwv1a2.TCPRoute{}, TcpRouteTargetField, IndexerByObjType),
f(&gwv1b1.ReferenceGrant{}, ReferenceGrantFromField, IndexerByObjType),
)
Expand Down Expand Up @@ -86,6 +88,15 @@ func IndexerByObjType(obj client.Object) []string {
return results
}

func IndexByHTTPRouteDelegationLabelSelector(obj client.Object) []string {
route := obj.(*gwv1.HTTPRoute)
value, ok := route.Labels[wellknown.RouteDelegationLabelSelector]
if !ok {
return nil
}
return []string{value}
}

// resolveNs resolves the namespace from an optional Namespace field.
func resolveNs(ns *gwv1.Namespace) string {
if ns == nil {
Expand Down
12 changes: 12 additions & 0 deletions projects/gateway2/translator/backendref/types.go
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,18 @@ func RefIsHTTPRoute(ref gwv1.BackendObjectReference) bool {
return (ref.Kind != nil && *ref.Kind == wellknown.HTTPRouteKind) && (ref.Group != nil && *ref.Group == gwv1.GroupName)
}

// RefIsHTTPRouteDelegationLabelSelector checks if the BackendObjectReference is an HTTPRoute delegation label selector
// Parent routes may delegate to child routes using an HTTPRoute backend reference.
func RefIsHTTPRouteDelegationLabelSelector(ref gwv1.BackendObjectReference) bool {
return ref.Group != nil && ref.Kind != nil && (string(*ref.Group)+"/"+string(*ref.Kind)) == wellknown.RouteDelegationLabelSelector
}

// RefIsDelegatedHTTPRoute checks if the BackendObjectReference is a delegated HTTPRoute
// selected by an HTTPRoute GVK reference or a delegation label selector.
func RefIsDelegatedHTTPRoute(ref gwv1.BackendObjectReference) bool {
return RefIsHTTPRoute(ref) || RefIsHTTPRouteDelegationLabelSelector(ref)
}

// ToString returns a string representation of the BackendObjectReference
func ToString(ref gwv1.BackendObjectReference) string {
var group, kind, namespace string
Expand Down
1 change: 1 addition & 0 deletions projects/gateway2/translator/gateway_translator_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -329,4 +329,5 @@ var _ = DescribeTable("Route Delegation translator",
Entry("RouteOptions prefer child override when allowed", "route_options_inheritance_child_override_allow.yaml"),
Entry("RouteOptions multi level inheritance with child override when allowed", "route_options_multi_level_inheritance_override_allow.yaml"),
Entry("RouteOptions multi level inheritance with partial child override", "route_options_multi_level_inheritance_override_partial.yaml"),
Entry("Label based delegation", "label_based.yaml"),
)
7 changes: 1 addition & 6 deletions projects/gateway2/translator/httproute/delegation_helpers.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,11 +13,6 @@ import (
gwv1 "sigs.k8s.io/gateway-api/apis/v1"
)

// inheritMatcherAnnotation is the annotation used on an child HTTPRoute that
// participates in a delegation chain to indicate that child route should inherit
// the route matcher from the parent route.
const inheritMatcherAnnotation = "delegation.gateway.solo.io/inherit-parent-matcher"

// filterDelegatedChildren filters the referenced children and their rules based
// on parent matchers, filters their hostnames, and applies parent matcher
// inheritance
Expand Down Expand Up @@ -184,7 +179,7 @@ func isDelegatedRouteMatch(
// shouldInheritMatcher returns true if the route indicates that it should inherit
// its parent's matcher.
func shouldInheritMatcher(route *gwv1.HTTPRoute) bool {
val, ok := route.Annotations[inheritMatcherAnnotation]
val, ok := route.Annotations[wellknown.InheritMatcherAnnotation]
if !ok {
return false
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -329,7 +329,7 @@ func setRouteAction(
for _, backendRef := range backendRefs {
// If the backend is an HTTPRoute, it implies route delegation
// for which delegated routes are recursively flattened and translated
if backendref.RefIsHTTPRoute(backendRef.BackendObjectReference) {
if backendref.RefIsDelegatedHTTPRoute(backendRef.BackendObjectReference) {
delegates = true
// Flatten delegated HTTPRoute references
err := flattenDelegatedRoutes(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -28,16 +28,13 @@ import (
rtoptquery "github.com/solo-io/gloo/projects/gateway2/translator/plugins/routeoptions/query"
"github.com/solo-io/gloo/projects/gateway2/translator/plugins/utils"
"github.com/solo-io/gloo/projects/gateway2/translator/routeutils"
"github.com/solo-io/gloo/projects/gateway2/wellknown"
"github.com/solo-io/gloo/projects/gloo/pkg/api/grpc/validation"
gloov1 "github.com/solo-io/gloo/projects/gloo/pkg/api/v1"
glooutils "github.com/solo-io/gloo/projects/gloo/pkg/utils"
)

const (
// policyOverrideAnnotation can be set by parent routes to allow child routes to override
// all (wildcard *) or specific fields (comma separated field names) in RouteOptions inherited from the parent route.
policyOverrideAnnotation = "delegation.gateway.solo.io/enable-policy-overrides"

// wildcardField is used to enable overriding all fields in RouteOptions inherited from the parent route.
wildcardField = "*"
)
Expand Down Expand Up @@ -131,7 +128,7 @@ func mergeOptionsForRoute(
// and can only augment them during a merge such that fields unset in the higher
// priority options can be merged in from the lower priority options.
// In the case of delegated routes, a parent route can enable child routes to override
// all (wildcard *) or specific fields using the policyOverrideAnnotation.
// all (wildcard *) or specific fields using the wellknown.PolicyOverrideAnnotation.
fieldsAllowedToOverride := sets.New[string]()

// If the route already has options set, we should override/augment them.
Expand All @@ -141,13 +138,13 @@ func mergeOptionsForRoute(
//
// By default, parent options (routeOptions) are preferred, unless the parent explicitly
// enabled child routes (outputRoute.Options) to override parent options.
fieldsStr, delegatedPolicyOverride := route.Annotations[policyOverrideAnnotation]
fieldsStr, delegatedPolicyOverride := route.Annotations[wellknown.PolicyOverrideAnnotation]
if delegatedPolicyOverride {
delegatedFieldsToOverride := parseDelegationFieldOverrides(fieldsStr)
if delegatedFieldsToOverride.Len() == 0 {
// Invalid annotation value, so log an error but enforce the default behavior of preferring the parent options.
contextutils.LoggerFrom(ctx).Errorf("invalid value %q for annotation %s on route %s; must be %s or a comma-separated list of field names",
fieldsStr, policyOverrideAnnotation, client.ObjectKeyFromObject(route), wildcardField)
fieldsStr, wellknown.PolicyOverrideAnnotation, client.ObjectKeyFromObject(route), wildcardField)
} else {
fieldsAllowedToOverride = delegatedFieldsToOverride
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -773,7 +773,7 @@ var _ = DescribeTable("mergeOptionsForRoute",
Entry("override dst options with annotation: full override",
&gwv1.HTTPRoute{
ObjectMeta: metav1.ObjectMeta{
Annotations: map[string]string{policyOverrideAnnotation: "*"},
Annotations: map[string]string{wellknown.PolicyOverrideAnnotation: "*"},
},
},
&v1.RouteOptions{
Expand Down Expand Up @@ -804,7 +804,7 @@ var _ = DescribeTable("mergeOptionsForRoute",
Entry("override dst options with annotation: partial override",
&gwv1.HTTPRoute{
ObjectMeta: metav1.ObjectMeta{
Annotations: map[string]string{policyOverrideAnnotation: "*"},
Annotations: map[string]string{wellknown.PolicyOverrideAnnotation: "*"},
},
},
&v1.RouteOptions{
Expand Down Expand Up @@ -837,7 +837,7 @@ var _ = DescribeTable("mergeOptionsForRoute",
Entry("override dst options with annotation: no override",
&gwv1.HTTPRoute{
ObjectMeta: metav1.ObjectMeta{
Annotations: map[string]string{policyOverrideAnnotation: "*"},
Annotations: map[string]string{wellknown.PolicyOverrideAnnotation: "*"},
},
},
&v1.RouteOptions{
Expand All @@ -860,7 +860,7 @@ var _ = DescribeTable("mergeOptionsForRoute",
Entry("override dst options with annotation: specific fields",
&gwv1.HTTPRoute{
ObjectMeta: metav1.ObjectMeta{
Annotations: map[string]string{policyOverrideAnnotation: "faults,timeout"},
Annotations: map[string]string{wellknown.PolicyOverrideAnnotation: "faults,timeout"},
},
},
&v1.RouteOptions{
Expand Down Expand Up @@ -895,7 +895,7 @@ var _ = DescribeTable("mergeOptionsForRoute",
Entry("override and augment dst options with annotation: specific fields",
&gwv1.HTTPRoute{
ObjectMeta: metav1.ObjectMeta{
Annotations: map[string]string{policyOverrideAnnotation: "faults,timeout"},
Annotations: map[string]string{wellknown.PolicyOverrideAnnotation: "faults,timeout"},
},
},
&v1.RouteOptions{
Expand Down
Loading