Skip to content

Commit d37f75c

Browse files
authored
Merge pull request #4418 from zac-nixon/znixon/gw-api-transforms-ga-version
Support URLRewrite in HTTPRoutes
2 parents 16e8b1f + fb3074e commit d37f75c

File tree

8 files changed

+801
-6
lines changed

8 files changed

+801
-6
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) |

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/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)