Skip to content

Commit

Permalink
[backport] gateway2: allow route delegation using well known label (#…
Browse files Browse the repository at this point in the history
…10561) (#10567)

Signed-off-by: Shashank Ram <[email protected]>
  • Loading branch information
shashankram authored Jan 10, 2025
1 parent 5d4f634 commit f69e899
Show file tree
Hide file tree
Showing 14 changed files with 364 additions and 46 deletions.
17 changes: 17 additions & 0 deletions changelog/v1.18.4/deleg-label.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
changelog:
- type: FIX
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

0 comments on commit f69e899

Please sign in to comment.