Skip to content

Commit f69e899

Browse files
authored
[backport] gateway2: allow route delegation using well known label (#10561) (#10567)
Signed-off-by: Shashank Ram <[email protected]>
1 parent 5d4f634 commit f69e899

File tree

14 files changed

+364
-46
lines changed

14 files changed

+364
-46
lines changed

changelog/v1.18.4/deleg-label.yaml

+17
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
changelog:
2+
- type: FIX
3+
issueLink: https://github.com/solo-io/solo-projects/issues/7626
4+
resolvesIssue: false
5+
description: |
6+
gateway2: allow route delegation using wellknown label
7+
8+
There is a product requirement to enable users to use
9+
a label to select HTTPRoutes to delegate to instead
10+
of GVK ref to other HTTPRoutes (includes wildcards).
11+
12+
To strike a balance between flexibility and performance,
13+
this change implements the proposal to use a well known
14+
label `delegation.gateway.solo.io/label=<value>` to
15+
allow users to delegate to other HTTPRoutes using a label.
16+
HTTPRoutes are indexed using this well known label key that
17+
enable O(1) lookups of routes matching this label value.

projects/gateway2/controller/controller.go

+4
Original file line numberDiff line numberDiff line change
@@ -123,6 +123,10 @@ func (c *controllerBuilder) addIndexes(ctx context.Context) error {
123123
if err := c.cfg.Mgr.GetFieldIndexer().IndexField(ctx, &apiv1.HTTPRoute{}, query.HttpRouteTargetField, query.IndexerByObjType); err != nil {
124124
errs = append(errs, err)
125125
}
126+
// Index HTTPRoutes by the delegation.gateway.solo.io/label label value to lookup delegatee routes using the label
127+
if err := c.cfg.Mgr.GetFieldIndexer().IndexField(ctx, &apiv1.HTTPRoute{}, query.HttpRouteDelegatedLabelSelector, query.IndexByHTTPRouteDelegationLabelSelector); err != nil {
128+
errs = append(errs, err)
129+
}
126130

127131
// Index for ReferenceGrant
128132
if err := c.cfg.Mgr.GetFieldIndexer().IndexField(ctx, &apiv1beta1.ReferenceGrant{}, query.ReferenceGrantFromField, query.IndexerByObjType); err != nil {

projects/gateway2/query/httproute.go

+27-21
Original file line numberDiff line numberDiff line change
@@ -257,8 +257,8 @@ func (r *gatewayQueries) getDelegatedChildren(
257257
for _, parentRule := range parent.Spec.Rules {
258258
var refChildren []*RouteInfo
259259
for _, backendRef := range parentRule.BackendRefs {
260-
// Check if the backend reference is an HTTPRoute
261-
if !backendref.RefIsHTTPRoute(backendRef.BackendObjectReference) {
260+
// Check if the backend delegated route reference
261+
if !backendref.RefIsDelegatedHTTPRoute(backendRef.BackendObjectReference) {
262262
continue
263263
}
264264
// Fetch child routes based on the backend reference
@@ -302,35 +302,41 @@ func (r *gatewayQueries) fetchChildRoutes(
302302
backendRef gwv1.HTTPBackendRef,
303303
) ([]gwv1.HTTPRoute, error) {
304304
delegatedNs := parentNamespace
305-
if !backendref.RefIsHTTPRoute(backendRef.BackendObjectReference) {
306-
return nil, nil
307-
}
308305
// Use the namespace specified in the backend reference if available
309306
if backendRef.Namespace != nil {
310307
delegatedNs = string(*backendRef.Namespace)
311308
}
312309

313310
var refChildren []gwv1.HTTPRoute
314-
if string(backendRef.Name) == "" || string(backendRef.Name) == "*" {
315-
// Handle wildcard references by listing all HTTPRoutes in the specified namespace
316-
var hrlist gwv1.HTTPRouteList
317-
err := r.client.List(ctx, &hrlist, client.InNamespace(delegatedNs))
318-
if err != nil {
319-
return nil, err
320-
}
321-
refChildren = append(refChildren, hrlist.Items...)
322-
} else {
323-
// Lookup a specific child route by its name
324-
delegatedRef := types.NamespacedName{
325-
Namespace: delegatedNs,
326-
Name: string(backendRef.Name),
311+
if backendref.RefIsHTTPRoute(backendRef.BackendObjectReference) {
312+
if string(backendRef.Name) == "" || string(backendRef.Name) == "*" {
313+
// Handle wildcard references by listing all HTTPRoutes in the specified namespace
314+
var hrlist gwv1.HTTPRouteList
315+
err := r.client.List(ctx, &hrlist, client.InNamespace(delegatedNs))
316+
if err != nil {
317+
return nil, err
318+
}
319+
refChildren = hrlist.Items
320+
} else {
321+
// Lookup a specific child route by its name
322+
delegatedRef := types.NamespacedName{
323+
Namespace: delegatedNs,
324+
Name: string(backendRef.Name),
325+
}
326+
child := &gwv1.HTTPRoute{}
327+
err := r.client.Get(ctx, delegatedRef, child)
328+
if err != nil {
329+
return nil, err
330+
}
331+
refChildren = append(refChildren, *child)
327332
}
328-
child := &gwv1.HTTPRoute{}
329-
err := r.client.Get(ctx, delegatedRef, child)
333+
} else if backendref.RefIsHTTPRouteDelegationLabelSelector(backendRef.BackendObjectReference) {
334+
var hrlist gwv1.HTTPRouteList
335+
err := r.client.List(ctx, &hrlist, client.InNamespace(delegatedNs), client.MatchingFields{HttpRouteDelegatedLabelSelector: string(backendRef.Name)})
330336
if err != nil {
331337
return nil, err
332338
}
333-
refChildren = append(refChildren, *child)
339+
refChildren = hrlist.Items
334340
}
335341
// Check if no child routes were resolved and log an error if needed
336342
if len(refChildren) == 0 {

projects/gateway2/query/indexers.go

+14-3
Original file line numberDiff line numberDiff line change
@@ -13,15 +13,17 @@ import (
1313
)
1414

1515
const (
16-
HttpRouteTargetField = "http-route-target"
17-
TcpRouteTargetField = "tcp-route-target"
18-
ReferenceGrantFromField = "ref-grant-from"
16+
HttpRouteTargetField = "http-route-target"
17+
HttpRouteDelegatedLabelSelector = "http-route-delegated-label-selector"
18+
TcpRouteTargetField = "tcp-route-target"
19+
ReferenceGrantFromField = "ref-grant-from"
1920
)
2021

2122
// IterateIndices calls the provided function for each indexable object with the appropriate indexer function.
2223
func IterateIndices(f func(client.Object, string, client.IndexerFunc) error) error {
2324
return errors.Join(
2425
f(&gwv1.HTTPRoute{}, HttpRouteTargetField, IndexerByObjType),
26+
f(&gwv1.HTTPRoute{}, HttpRouteDelegatedLabelSelector, IndexByHTTPRouteDelegationLabelSelector),
2527
f(&gwv1a2.TCPRoute{}, TcpRouteTargetField, IndexerByObjType),
2628
f(&gwv1b1.ReferenceGrant{}, ReferenceGrantFromField, IndexerByObjType),
2729
)
@@ -86,6 +88,15 @@ func IndexerByObjType(obj client.Object) []string {
8688
return results
8789
}
8890

91+
func IndexByHTTPRouteDelegationLabelSelector(obj client.Object) []string {
92+
route := obj.(*gwv1.HTTPRoute)
93+
value, ok := route.Labels[wellknown.RouteDelegationLabelSelector]
94+
if !ok {
95+
return nil
96+
}
97+
return []string{value}
98+
}
99+
89100
// resolveNs resolves the namespace from an optional Namespace field.
90101
func resolveNs(ns *gwv1.Namespace) string {
91102
if ns == nil {

projects/gateway2/translator/backendref/types.go

+12
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,18 @@ func RefIsHTTPRoute(ref gwv1.BackendObjectReference) bool {
2727
return (ref.Kind != nil && *ref.Kind == wellknown.HTTPRouteKind) && (ref.Group != nil && *ref.Group == gwv1.GroupName)
2828
}
2929

30+
// RefIsHTTPRouteDelegationLabelSelector checks if the BackendObjectReference is an HTTPRoute delegation label selector
31+
// Parent routes may delegate to child routes using an HTTPRoute backend reference.
32+
func RefIsHTTPRouteDelegationLabelSelector(ref gwv1.BackendObjectReference) bool {
33+
return ref.Group != nil && ref.Kind != nil && (string(*ref.Group)+"/"+string(*ref.Kind)) == wellknown.RouteDelegationLabelSelector
34+
}
35+
36+
// RefIsDelegatedHTTPRoute checks if the BackendObjectReference is a delegated HTTPRoute
37+
// selected by an HTTPRoute GVK reference or a delegation label selector.
38+
func RefIsDelegatedHTTPRoute(ref gwv1.BackendObjectReference) bool {
39+
return RefIsHTTPRoute(ref) || RefIsHTTPRouteDelegationLabelSelector(ref)
40+
}
41+
3042
// ToString returns a string representation of the BackendObjectReference
3143
func ToString(ref gwv1.BackendObjectReference) string {
3244
var group, kind, namespace string

projects/gateway2/translator/gateway_translator_test.go

+1
Original file line numberDiff line numberDiff line change
@@ -329,4 +329,5 @@ var _ = DescribeTable("Route Delegation translator",
329329
Entry("RouteOptions prefer child override when allowed", "route_options_inheritance_child_override_allow.yaml"),
330330
Entry("RouteOptions multi level inheritance with child override when allowed", "route_options_multi_level_inheritance_override_allow.yaml"),
331331
Entry("RouteOptions multi level inheritance with partial child override", "route_options_multi_level_inheritance_override_partial.yaml"),
332+
Entry("Label based delegation", "label_based.yaml"),
332333
)

projects/gateway2/translator/httproute/delegation_helpers.go

+1-6
Original file line numberDiff line numberDiff line change
@@ -13,11 +13,6 @@ import (
1313
gwv1 "sigs.k8s.io/gateway-api/apis/v1"
1414
)
1515

16-
// inheritMatcherAnnotation is the annotation used on an child HTTPRoute that
17-
// participates in a delegation chain to indicate that child route should inherit
18-
// the route matcher from the parent route.
19-
const inheritMatcherAnnotation = "delegation.gateway.solo.io/inherit-parent-matcher"
20-
2116
// filterDelegatedChildren filters the referenced children and their rules based
2217
// on parent matchers, filters their hostnames, and applies parent matcher
2318
// inheritance
@@ -184,7 +179,7 @@ func isDelegatedRouteMatch(
184179
// shouldInheritMatcher returns true if the route indicates that it should inherit
185180
// its parent's matcher.
186181
func shouldInheritMatcher(route *gwv1.HTTPRoute) bool {
187-
val, ok := route.Annotations[inheritMatcherAnnotation]
182+
val, ok := route.Annotations[wellknown.InheritMatcherAnnotation]
188183
if !ok {
189184
return false
190185
}

projects/gateway2/translator/httproute/gateway_http_route_translator.go

+1-1
Original file line numberDiff line numberDiff line change
@@ -329,7 +329,7 @@ func setRouteAction(
329329
for _, backendRef := range backendRefs {
330330
// If the backend is an HTTPRoute, it implies route delegation
331331
// for which delegated routes are recursively flattened and translated
332-
if backendref.RefIsHTTPRoute(backendRef.BackendObjectReference) {
332+
if backendref.RefIsDelegatedHTTPRoute(backendRef.BackendObjectReference) {
333333
delegates = true
334334
// Flatten delegated HTTPRoute references
335335
err := flattenDelegatedRoutes(

projects/gateway2/translator/plugins/routeoptions/route_options_plugin.go

+4-7
Original file line numberDiff line numberDiff line change
@@ -28,16 +28,13 @@ import (
2828
rtoptquery "github.com/solo-io/gloo/projects/gateway2/translator/plugins/routeoptions/query"
2929
"github.com/solo-io/gloo/projects/gateway2/translator/plugins/utils"
3030
"github.com/solo-io/gloo/projects/gateway2/translator/routeutils"
31+
"github.com/solo-io/gloo/projects/gateway2/wellknown"
3132
"github.com/solo-io/gloo/projects/gloo/pkg/api/grpc/validation"
3233
gloov1 "github.com/solo-io/gloo/projects/gloo/pkg/api/v1"
3334
glooutils "github.com/solo-io/gloo/projects/gloo/pkg/utils"
3435
)
3536

3637
const (
37-
// policyOverrideAnnotation can be set by parent routes to allow child routes to override
38-
// all (wildcard *) or specific fields (comma separated field names) in RouteOptions inherited from the parent route.
39-
policyOverrideAnnotation = "delegation.gateway.solo.io/enable-policy-overrides"
40-
4138
// wildcardField is used to enable overriding all fields in RouteOptions inherited from the parent route.
4239
wildcardField = "*"
4340
)
@@ -131,7 +128,7 @@ func mergeOptionsForRoute(
131128
// and can only augment them during a merge such that fields unset in the higher
132129
// priority options can be merged in from the lower priority options.
133130
// In the case of delegated routes, a parent route can enable child routes to override
134-
// all (wildcard *) or specific fields using the policyOverrideAnnotation.
131+
// all (wildcard *) or specific fields using the wellknown.PolicyOverrideAnnotation.
135132
fieldsAllowedToOverride := sets.New[string]()
136133

137134
// If the route already has options set, we should override/augment them.
@@ -141,13 +138,13 @@ func mergeOptionsForRoute(
141138
//
142139
// By default, parent options (routeOptions) are preferred, unless the parent explicitly
143140
// enabled child routes (outputRoute.Options) to override parent options.
144-
fieldsStr, delegatedPolicyOverride := route.Annotations[policyOverrideAnnotation]
141+
fieldsStr, delegatedPolicyOverride := route.Annotations[wellknown.PolicyOverrideAnnotation]
145142
if delegatedPolicyOverride {
146143
delegatedFieldsToOverride := parseDelegationFieldOverrides(fieldsStr)
147144
if delegatedFieldsToOverride.Len() == 0 {
148145
// Invalid annotation value, so log an error but enforce the default behavior of preferring the parent options.
149146
contextutils.LoggerFrom(ctx).Errorf("invalid value %q for annotation %s on route %s; must be %s or a comma-separated list of field names",
150-
fieldsStr, policyOverrideAnnotation, client.ObjectKeyFromObject(route), wildcardField)
147+
fieldsStr, wellknown.PolicyOverrideAnnotation, client.ObjectKeyFromObject(route), wildcardField)
151148
} else {
152149
fieldsAllowedToOverride = delegatedFieldsToOverride
153150
}

projects/gateway2/translator/plugins/routeoptions/route_options_plugin_test.go

+5-5
Original file line numberDiff line numberDiff line change
@@ -773,7 +773,7 @@ var _ = DescribeTable("mergeOptionsForRoute",
773773
Entry("override dst options with annotation: full override",
774774
&gwv1.HTTPRoute{
775775
ObjectMeta: metav1.ObjectMeta{
776-
Annotations: map[string]string{policyOverrideAnnotation: "*"},
776+
Annotations: map[string]string{wellknown.PolicyOverrideAnnotation: "*"},
777777
},
778778
},
779779
&v1.RouteOptions{
@@ -804,7 +804,7 @@ var _ = DescribeTable("mergeOptionsForRoute",
804804
Entry("override dst options with annotation: partial override",
805805
&gwv1.HTTPRoute{
806806
ObjectMeta: metav1.ObjectMeta{
807-
Annotations: map[string]string{policyOverrideAnnotation: "*"},
807+
Annotations: map[string]string{wellknown.PolicyOverrideAnnotation: "*"},
808808
},
809809
},
810810
&v1.RouteOptions{
@@ -837,7 +837,7 @@ var _ = DescribeTable("mergeOptionsForRoute",
837837
Entry("override dst options with annotation: no override",
838838
&gwv1.HTTPRoute{
839839
ObjectMeta: metav1.ObjectMeta{
840-
Annotations: map[string]string{policyOverrideAnnotation: "*"},
840+
Annotations: map[string]string{wellknown.PolicyOverrideAnnotation: "*"},
841841
},
842842
},
843843
&v1.RouteOptions{
@@ -860,7 +860,7 @@ var _ = DescribeTable("mergeOptionsForRoute",
860860
Entry("override dst options with annotation: specific fields",
861861
&gwv1.HTTPRoute{
862862
ObjectMeta: metav1.ObjectMeta{
863-
Annotations: map[string]string{policyOverrideAnnotation: "faults,timeout"},
863+
Annotations: map[string]string{wellknown.PolicyOverrideAnnotation: "faults,timeout"},
864864
},
865865
},
866866
&v1.RouteOptions{
@@ -895,7 +895,7 @@ var _ = DescribeTable("mergeOptionsForRoute",
895895
Entry("override and augment dst options with annotation: specific fields",
896896
&gwv1.HTTPRoute{
897897
ObjectMeta: metav1.ObjectMeta{
898-
Annotations: map[string]string{policyOverrideAnnotation: "faults,timeout"},
898+
Annotations: map[string]string{wellknown.PolicyOverrideAnnotation: "faults,timeout"},
899899
},
900900
},
901901
&v1.RouteOptions{

0 commit comments

Comments
 (0)