Skip to content

Commit f556ffb

Browse files
committed
lrc + transforms
1 parent be812c3 commit f556ffb

File tree

12 files changed

+989
-14
lines changed

12 files changed

+989
-14
lines changed

docs/guide/gateway/l7gateway.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -186,7 +186,7 @@ information see the [Gateway API Conformance Page](https://gateway-api.sigs.k8s.
186186
| HTTPRouteRule - HTTPRouteFilter - ResponseHeaderModifier | Core | ❌ |
187187
| HTTPRouteRule - HTTPRouteFilter - RequestMirror | Extended | ❌ |
188188
| HTTPRouteRule - HTTPRouteFilter - RequestRedirect | Core | ✅ |
189-
| HTTPRouteRule - HTTPRouteFilter - UrlRewrite | Extended | |
189+
| HTTPRouteRule - HTTPRouteFilter - UrlRewrite | Extended | |
190190
| HTTPRouteRule - HTTPRouteFilter - CORS | Extended | ❌ |
191191
| HTTPRouteRule - HTTPRouteFilter - ExternalAuth | Extended | ❌ -- Use [ListenerRuleConfigurations](customization.md#customizing-l7-routing-rules) |
192192
| HTTPRouteRule - HTTPRouteFilter - ExtensionRef | Core | ✅ -- Use to attach [ListenerRuleConfigurations](customization.md#customizing-l7-routing-rules) |

main.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -278,7 +278,7 @@ func main() {
278278
enabledControllers := sets.Set[string]{}
279279

280280
routeLoaderCreator := sync.OnceValue(func() routeutils.Loader {
281-
return routeutils.NewLoader(mgr.GetClient(), routeReconciler, mgr.GetLogger().WithName("gateway-route-loader"))
281+
return routeutils.NewLoader(mgr.GetClient(), routeReconciler, routeutils.NewRouteValidator(), mgr.GetLogger().WithName("gateway-route-loader"))
282282
})
283283

284284
// Setup NLB Gateway controller if enabled

pkg/gateway/model/model_build_listener.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -272,6 +272,7 @@ func (l listenerBuilderImpl) buildListenerRules(ctx context.Context, stack core.
272272
albRules = append(albRules, elbv2model.Rule{
273273
Conditions: conditionsList,
274274
Actions: actions,
275+
Transforms: routeutils.BuildRoutingRuleTransforms(route, ruleWithPrecedence),
275276
Tags: tags,
276277
})
277278

@@ -285,6 +286,7 @@ func (l listenerBuilderImpl) buildListenerRules(ctx context.Context, stack core.
285286
Priority: priority,
286287
Conditions: rule.Conditions,
287288
Actions: rule.Actions,
289+
Transforms: rule.Transforms,
288290
Tags: rule.Tags,
289291
})
290292
priority += 1

pkg/gateway/routeutils/loader.go

Lines changed: 13 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -65,15 +65,17 @@ type loaderImpl struct {
6565
routeSubmitter RouteReconcilerSubmitter
6666
k8sClient client.Client
6767
logger logr.Logger
68+
routeValidator RouteValidator
6869
allRouteLoaders map[RouteKind]func(context context.Context, client client.Client, opts ...client.ListOption) ([]preLoadRouteDescriptor, error)
6970
}
7071

71-
func NewLoader(k8sClient client.Client, routeSubmitter RouteReconcilerSubmitter, logger logr.Logger) Loader {
72+
func NewLoader(k8sClient client.Client, routeSubmitter RouteReconcilerSubmitter, routeValidator RouteValidator, logger logr.Logger) Loader {
7273
return &loaderImpl{
7374
mapper: newListenerToRouteMapper(k8sClient, logger.WithName("route-mapper")),
7475
routeSubmitter: routeSubmitter,
7576
k8sClient: k8sClient,
7677
allRouteLoaders: allRoutes,
78+
routeValidator: routeValidator,
7779
logger: logger,
7880
}
7981
}
@@ -82,7 +84,7 @@ func NewLoader(k8sClient client.Client, routeSubmitter RouteReconcilerSubmitter,
8284
func (l *loaderImpl) LoadRoutesForGateway(ctx context.Context, gw gwv1.Gateway, filter LoadRouteFilter, controllerName string) (*LoaderResult, error) {
8385
// 1. Load all relevant routes according to the filter
8486

85-
loadedRoutes := make([]preLoadRouteDescriptor, 0)
87+
foo := make([]preLoadRouteDescriptor, 0)
8688

8789
routeStatusUpdates := make([]RouteData, 0)
8890

@@ -108,16 +110,20 @@ func (l *loaderImpl) LoadRoutesForGateway(ctx context.Context, gw gwv1.Gateway,
108110
if err != nil {
109111
return nil, err
110112
}
111-
loadedRoutes = append(loadedRoutes, data...)
113+
foo = append(foo, data...)
112114
}
113115
}
114116

115-
// validate listeners configuration and get listener status
117+
// 2. Validate listeners configuration and get listener status
116118
listenerValidationResults := ValidateListeners(gw, controllerName, ctx, l.k8sClient)
117119

118-
// 2. Remove routes that aren't granted attachment by the listener.
120+
// 3. Validate route configuration, filter out bad configurations.
121+
validatedRoutes, validationErrors := l.routeValidator.filterToValidRoutes(gw, foo)
122+
routeStatusUpdates = append(routeStatusUpdates, validationErrors...)
123+
124+
// 4. Remove routes that aren't granted attachment by the listener.
119125
// Map any routes that are granted attachment to the listener port that allows the attachment.
120-
mappedRoutes, statusUpdates, err := l.mapper.mapGatewayAndRoutes(ctx, gw, loadedRoutes)
126+
mappedRoutes, statusUpdates, err := l.mapper.mapGatewayAndRoutes(ctx, gw, validatedRoutes)
121127

122128
routeStatusUpdates = append(routeStatusUpdates, statusUpdates...)
123129

@@ -128,7 +134,7 @@ func (l *loaderImpl) LoadRoutesForGateway(ctx context.Context, gw gwv1.Gateway,
128134
// Count attached routes per listener for listener status update
129135
attachedRouteMap := buildAttachedRouteMap(gw, mappedRoutes)
130136

131-
// 3. Load the underlying resource(s) for each route that is configured.
137+
// 5. Load the underlying resource(s) for each route that is configured.
132138
loadedRoute, childRouteLoadUpdates, err := l.loadChildResources(ctx, mappedRoutes, gw)
133139
routeStatusUpdates = append(routeStatusUpdates, childRouteLoadUpdates...)
134140
if err != nil {

pkg/gateway/routeutils/loader_test.go

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ package routeutils
22

33
import (
44
"context"
5+
gwalpha2 "sigs.k8s.io/gateway-api/apis/v1alpha2"
56
"testing"
67
"time"
78

@@ -34,6 +35,7 @@ type mockRoute struct {
3435
routeKind RouteKind
3536
generation int64
3637
hostnames []gwv1.Hostname
38+
route interface{}
3739
}
3840

3941
func (m *mockRoute) loadAttachedRules(context context.Context, k8sClient client.Client) (RouteDescriptor, []routeLoadError) {
@@ -71,8 +73,7 @@ func (m *mockRoute) GetRouteGeneration() int64 {
7173
}
7274

7375
func (m *mockRoute) GetRawRoute() interface{} {
74-
//TODO implement me
75-
panic("implement me")
76+
return m.route
7677
}
7778

7879
func (m *mockRoute) GetAttachedRules() []RouteRule {
@@ -92,20 +93,23 @@ func TestLoadRoutesForGateway(t *testing.T) {
9293
Name: "http1",
9394
},
9495
routeKind: HTTPRouteKind,
96+
route: &gwv1.HTTPRoute{},
9597
},
9698
&mockRoute{
9799
namespacedName: types.NamespacedName{
98100
Namespace: "http2-ns",
99101
Name: "http2",
100102
},
101103
routeKind: HTTPRouteKind,
104+
route: &gwv1.HTTPRoute{},
102105
},
103106
&mockRoute{
104107
namespacedName: types.NamespacedName{
105108
Namespace: "http3-ns",
106109
Name: "http3",
107110
},
108111
routeKind: HTTPRouteKind,
112+
route: &gwv1.HTTPRoute{},
109113
},
110114
}
111115

@@ -122,20 +126,23 @@ func TestLoadRoutesForGateway(t *testing.T) {
122126
Name: "tcp1",
123127
},
124128
routeKind: TCPRouteKind,
129+
route: &gwalpha2.TCPRoute{},
125130
},
126131
&mockRoute{
127132
namespacedName: types.NamespacedName{
128133
Namespace: "tcp2-ns",
129134
Name: "tcp2",
130135
},
131136
routeKind: TCPRouteKind,
137+
route: &gwalpha2.TCPRoute{},
132138
},
133139
&mockRoute{
134140
namespacedName: types.NamespacedName{
135141
Namespace: "tcp3-ns",
136142
Name: "tcp3",
137143
},
138144
routeKind: TCPRouteKind,
145+
route: &gwalpha2.TCPRoute{},
139146
},
140147
}
141148

@@ -292,6 +299,7 @@ func TestLoadRoutesForGateway(t *testing.T) {
292299
mapToReturn: tc.expectedPreloadMap,
293300
routeStatusUpdates: tc.mapperRouteStatusUpdates,
294301
},
302+
routeValidator: NewRouteValidator(),
295303
allRouteLoaders: allRouteLoaders,
296304
logger: logr.Discard(),
297305
routeSubmitter: routeReconciler,

pkg/gateway/routeutils/mock_route.go

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,10 @@
11
package routeutils
22

33
import (
4+
"context"
45
"k8s.io/apimachinery/pkg/types"
56
elbv2gw "sigs.k8s.io/aws-load-balancer-controller/apis/gateway/v1beta1"
7+
"sigs.k8s.io/controller-runtime/pkg/client"
68
gwv1 "sigs.k8s.io/gateway-api/apis/v1"
79
"time"
810
)
@@ -32,10 +34,16 @@ type MockRoute struct {
3234
Name string
3335
Namespace string
3436
Hostnames []string
37+
Route interface{}
3538
CreationTime time.Time
3639
Rules []RouteRule
3740
}
3841

42+
func (m *MockRoute) loadAttachedRules(_ context.Context, k8sClient client.Client) (RouteDescriptor, []routeLoadError) {
43+
//TODO implement me
44+
panic("implement me")
45+
}
46+
3947
func (m *MockRoute) GetBackendRefs() []gwv1.BackendRef {
4048
//TODO implement me
4149
panic("implement me")
@@ -71,8 +79,7 @@ func (m *MockRoute) GetParentRefs() []gwv1.ParentReference {
7179
}
7280

7381
func (m *MockRoute) GetRawRoute() interface{} {
74-
//TODO implement me
75-
panic("implement me")
82+
return m.Route
7683
}
7784

7885
func (m *MockRoute) GetAttachedRules() []RouteRule {
@@ -81,7 +88,7 @@ func (m *MockRoute) GetAttachedRules() []RouteRule {
8188
}
8289

8390
func (m *MockRoute) GetRouteGeneration() int64 {
84-
panic("implement me")
91+
return 0
8592
}
8693

8794
func (m *MockRoute) GetRouteCreateTimestamp() time.Time {

pkg/gateway/routeutils/route_rule_action.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -213,6 +213,8 @@ func buildHttpRuleRedirectActionsBasedOnFilter(filters []gwv1.HTTPRouteFilter, r
213213
return buildHttpRedirectAction(filter.RequestRedirect, redirectConfig)
214214
case gwv1.HTTPRouteFilterExtensionRef:
215215
continue
216+
case gwv1.HTTPRouteFilterURLRewrite:
217+
continue
216218
default:
217219
return nil, errors.Errorf("Unsupported filter type: %v. Only request redirect is supported. To specify header modification, please configure it through LoadBalancerConfiguration.", filter.Type)
218220
}
Lines changed: 119 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,119 @@
1+
package routeutils
2+
3+
import (
4+
"fmt"
5+
elbv2model "sigs.k8s.io/aws-load-balancer-controller/pkg/model/elbv2"
6+
gwv1 "sigs.k8s.io/gateway-api/apis/v1"
7+
"strings"
8+
)
9+
10+
const (
11+
replaceWholeHostHeaderRegex = ".*"
12+
replaceWholePathMinusQueryParamsRegex = "^([^?]*)"
13+
)
14+
15+
func BuildRoutingRuleTransforms(gwRoute RouteDescriptor, gwRule RulePrecedence) []elbv2model.Transform {
16+
switch gwRoute.GetRouteKind() {
17+
case HTTPRouteKind:
18+
return buildHTTPRuleTransforms(gwRule.CommonRulePrecedence.Rule.GetRawRouteRule().(*gwv1.HTTPRouteRule), gwRule.HTTPMatch)
19+
default:
20+
return []elbv2model.Transform{}
21+
}
22+
}
23+
24+
func buildHTTPRuleTransforms(rule *gwv1.HTTPRouteRule, httpMatch *gwv1.HTTPRouteMatch) []elbv2model.Transform {
25+
var transforms []elbv2model.Transform
26+
27+
if rule != nil {
28+
for _, rf := range rule.Filters {
29+
if rf.URLRewrite != nil {
30+
if rf.URLRewrite.Path != nil {
31+
transforms = append(transforms, generateURLRewritePathTransform(*rf.URLRewrite.Path, httpMatch))
32+
}
33+
34+
if rf.URLRewrite.Hostname != nil {
35+
transforms = append(transforms, generateHostHeaderRewriteTransform(*rf.URLRewrite.Hostname))
36+
}
37+
}
38+
}
39+
}
40+
41+
return transforms
42+
}
43+
44+
func generateHostHeaderRewriteTransform(hostname gwv1.PreciseHostname) elbv2model.Transform {
45+
return elbv2model.Transform{
46+
Type: elbv2model.TransformTypeHostHeaderRewrite,
47+
HostHeaderRewriteConfig: &elbv2model.RewriteConfigObject{
48+
Rewrites: []elbv2model.RewriteConfig{
49+
{
50+
Regex: replaceWholeHostHeaderRegex,
51+
Replace: string(hostname),
52+
},
53+
},
54+
},
55+
}
56+
}
57+
58+
func generateURLRewritePathTransform(gwPathModifier gwv1.HTTPPathModifier, httpMatch *gwv1.HTTPRouteMatch) elbv2model.Transform {
59+
var replacementRegex string
60+
var replacement string
61+
62+
switch gwPathModifier.Type {
63+
case gwv1.FullPathHTTPPathModifier:
64+
// Capture just the path, not the query parameters
65+
replacementRegex = replaceWholePathMinusQueryParamsRegex
66+
replacement = *gwPathModifier.ReplaceFullPath
67+
break
68+
case gwv1.PrefixMatchHTTPPathModifier:
69+
replacementRegex, replacement = generatePrefixReplacementRegex(httpMatch, *gwPathModifier.ReplacePrefixMatch)
70+
break
71+
default:
72+
// Need to set route status as failed :blah:
73+
// Probably do this in the routeutils loader step and for validation.
74+
}
75+
return elbv2model.Transform{
76+
Type: elbv2model.TransformTypeUrlRewrite,
77+
UrlRewriteConfig: &elbv2model.RewriteConfigObject{
78+
Rewrites: []elbv2model.RewriteConfig{
79+
{
80+
Regex: replacementRegex,
81+
Replace: replacement,
82+
},
83+
},
84+
},
85+
}
86+
}
87+
88+
func generatePrefixReplacementRegex(httpMatch *gwv1.HTTPRouteMatch, replacement string) (string, string) {
89+
match := *httpMatch.Path.Value
90+
91+
/*
92+
If we're being asked to replace a prefix with "", we still need to keep one '/' to form a valid path.
93+
Consider getting the path '/foo' and having the replacement string being '', we would transform '/foo' => ''
94+
thereby leaving an invalid path of ''. We could (in theory) do this for all replacements, e.g. replace = 'cat'
95+
we could transform this into '/cat' here, but tbh the user can also do this, and I'm not entirely
96+
sure if we could handle all possible cases.
97+
98+
To explain the addition of $2, we set up an optional capture group after the initial prefix match. We only want
99+
to add back the value of the optional capture group when the replacement doesn't already have a '/' suffix.
100+
A couple examples:
101+
102+
Without the capture group, e.g. (^%s)
103+
input path = '/foo/', prefixRegex = '(^/foo)', replacement value = '/cat/' results in '/cat//'
104+
105+
To extend the example, now consider using having the capture group and always adding that to the result.
106+
input path = '/foo/', prefixRegex = '(^/foo(/)?)', replacement value = '/cat/$2' results in (again) '/cat//'
107+
108+
Without the capture group, we would have one '/' too few.
109+
input path = '/foo/bar', prefixRegex = '(^/foo(/)?)', replacement value = '/cat$2' results in '/catbar'
110+
111+
*/
112+
if replacement == "" {
113+
replacement = "/"
114+
} else if !strings.HasSuffix(replacement, "/") {
115+
replacement = fmt.Sprintf("%s$2", replacement)
116+
}
117+
118+
return fmt.Sprintf("(^%s(/)?)", match), replacement
119+
}

0 commit comments

Comments
 (0)