Skip to content
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
2 changes: 1 addition & 1 deletion docs/guide/gateway/l7gateway.md
Original file line number Diff line number Diff line change
Expand Up @@ -186,7 +186,7 @@ information see the [Gateway API Conformance Page](https://gateway-api.sigs.k8s.
| HTTPRouteRule - HTTPRouteFilter - ResponseHeaderModifier | Core | ❌ |
| HTTPRouteRule - HTTPRouteFilter - RequestMirror | Extended | ❌ |
| HTTPRouteRule - HTTPRouteFilter - RequestRedirect | Core | ✅ |
| HTTPRouteRule - HTTPRouteFilter - UrlRewrite | Extended | |
| HTTPRouteRule - HTTPRouteFilter - UrlRewrite | Extended | |
| HTTPRouteRule - HTTPRouteFilter - CORS | Extended | ❌ |
| HTTPRouteRule - HTTPRouteFilter - ExternalAuth | Extended | ❌ -- Use [ListenerRuleConfigurations](customization.md#customizing-l7-routing-rules) |
| HTTPRouteRule - HTTPRouteFilter - ExtensionRef | Core | ✅ -- Use to attach [ListenerRuleConfigurations](customization.md#customizing-l7-routing-rules) |
Expand Down
2 changes: 2 additions & 0 deletions pkg/gateway/model/model_build_listener.go
Original file line number Diff line number Diff line change
Expand Up @@ -272,6 +272,7 @@ func (l listenerBuilderImpl) buildListenerRules(ctx context.Context, stack core.
albRules = append(albRules, elbv2model.Rule{
Conditions: conditionsList,
Actions: actions,
Transforms: routeutils.BuildRoutingRuleTransforms(route, ruleWithPrecedence),
Tags: tags,
})

Expand All @@ -285,6 +286,7 @@ func (l listenerBuilderImpl) buildListenerRules(ctx context.Context, stack core.
Priority: priority,
Conditions: rule.Conditions,
Actions: rule.Actions,
Transforms: rule.Transforms,
Tags: rule.Tags,
})
priority += 1
Expand Down
2 changes: 2 additions & 0 deletions pkg/gateway/routeutils/route_rule_action.go
Original file line number Diff line number Diff line change
Expand Up @@ -213,6 +213,8 @@ func buildHttpRuleRedirectActionsBasedOnFilter(filters []gwv1.HTTPRouteFilter, r
return buildHttpRedirectAction(filter.RequestRedirect, redirectConfig)
case gwv1.HTTPRouteFilterExtensionRef:
continue
case gwv1.HTTPRouteFilterURLRewrite:
continue
default:
return nil, errors.Errorf("Unsupported filter type: %v. Only request redirect is supported. To specify header modification, please configure it through LoadBalancerConfiguration.", filter.Type)
}
Expand Down
119 changes: 119 additions & 0 deletions pkg/gateway/routeutils/route_rule_transform.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,119 @@
package routeutils

import (
"fmt"
elbv2model "sigs.k8s.io/aws-load-balancer-controller/pkg/model/elbv2"
gwv1 "sigs.k8s.io/gateway-api/apis/v1"
"strings"
)

const (
replaceWholeHostHeaderRegex = ".*"
replaceWholePathMinusQueryParamsRegex = "^([^?]*)"
)

func BuildRoutingRuleTransforms(gwRoute RouteDescriptor, gwRule RulePrecedence) []elbv2model.Transform {
switch gwRoute.GetRouteKind() {
case HTTPRouteKind:
return buildHTTPRuleTransforms(gwRule.CommonRulePrecedence.Rule.GetRawRouteRule().(*gwv1.HTTPRouteRule), gwRule.HTTPMatch)
default:
return []elbv2model.Transform{}
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

for any other cases, do we want to add a v(1) logging or warning saying only httpRoute is supported for transform?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think so. This is already documented within the gateway api

}
}

func buildHTTPRuleTransforms(rule *gwv1.HTTPRouteRule, httpMatch *gwv1.HTTPRouteMatch) []elbv2model.Transform {
var transforms []elbv2model.Transform

if rule != nil {
for _, rf := range rule.Filters {
if rf.URLRewrite != nil {
if rf.URLRewrite.Path != nil {
transforms = append(transforms, generateURLRewritePathTransform(*rf.URLRewrite.Path, httpMatch))
}

if rf.URLRewrite.Hostname != nil {
transforms = append(transforms, generateHostHeaderRewriteTransform(*rf.URLRewrite.Hostname))
}
}
}
}

return transforms
}

func generateHostHeaderRewriteTransform(hostname gwv1.PreciseHostname) elbv2model.Transform {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

maybe rename it to generateHttpHostHeaderRewriteTransform, same for generateURLRewritePathTransform

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

any explanation here? :)

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

because it feels like this function is only for HTTPRoute, but it is a nit change

return elbv2model.Transform{
Type: elbv2model.TransformTypeHostHeaderRewrite,
HostHeaderRewriteConfig: &elbv2model.RewriteConfigObject{
Rewrites: []elbv2model.RewriteConfig{
{
Regex: replaceWholeHostHeaderRegex,
Replace: string(hostname),
},
},
},
}
}

func generateURLRewritePathTransform(gwPathModifier gwv1.HTTPPathModifier, httpMatch *gwv1.HTTPRouteMatch) elbv2model.Transform {
var replacementRegex string
var replacement string

switch gwPathModifier.Type {
case gwv1.FullPathHTTPPathModifier:
// Capture just the path, not the query parameters
replacementRegex = replaceWholePathMinusQueryParamsRegex
replacement = *gwPathModifier.ReplaceFullPath
break
case gwv1.PrefixMatchHTTPPathModifier:
replacementRegex, replacement = generatePrefixReplacementRegex(httpMatch, *gwPathModifier.ReplacePrefixMatch)
break
default:
// Need to set route status as failed :blah:
// Probably do this in the routeutils loader step and for validation.
}
return elbv2model.Transform{
Type: elbv2model.TransformTypeUrlRewrite,
UrlRewriteConfig: &elbv2model.RewriteConfigObject{
Rewrites: []elbv2model.RewriteConfig{
{
Regex: replacementRegex,
Replace: replacement,
},
},
},
}
}

func generatePrefixReplacementRegex(httpMatch *gwv1.HTTPRouteMatch, replacement string) (string, string) {
match := *httpMatch.Path.Value
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

i did not see where we are checking nil for httpMatch

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

it's done in the routeValidator


/*
If we're being asked to replace a prefix with "", we still need to keep one '/' to form a valid path.
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Great job on explaining this in details.
We should also add good documentation with these samples in our docs for this feature once we implement it with ELB gaps.

Consider getting the path '/foo' and having the replacement string being '', we would transform '/foo' => ''
thereby leaving an invalid path of ''. We could (in theory) do this for all replacements, e.g. replace = 'cat'
we could transform this into '/cat' here, but tbh the user can also do this, and I'm not entirely
sure if we could handle all possible cases.

To explain the addition of $2, we set up an optional capture group after the initial prefix match. We only want
to add back the value of the optional capture group when the replacement doesn't already have a '/' suffix.
A couple examples:

Without the capture group, e.g. (^%s)
input path = '/foo/', prefixRegex = '(^/foo)', replacement value = '/cat/' results in '/cat//'

To extend the example, now consider using having the capture group and always adding that to the result.
input path = '/foo/', prefixRegex = '(^/foo(/)?)', replacement value = '/cat/$2' results in (again) '/cat//'

Without the capture group, we would have one '/' too few.
input path = '/foo/bar', prefixRegex = '(^/foo(/)?)', replacement value = '/cat$2' results in '/catbar'
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

thanks for all explanation. can you verify if *httpMatch.Path.Value(match) can be something like /foo/ or replacement can be with Trailing Slash? those cases will lead to double slash right?

Copy link
Collaborator Author

@zac-nixon zac-nixon Oct 30, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yup it can, I actually have an explicit test case for this, name = prefix path rewrite with explicit '/' on suffix. To answer your question, no we won't add a double slash, because we check if the replacement ends with a /, in that case we don't append the optional capture group.


*/
if replacement == "" {
replacement = "/"
} else if !strings.HasSuffix(replacement, "/") {
replacement = fmt.Sprintf("%s$2", replacement)
}

return fmt.Sprintf("(^%s(/)?)", match), replacement
}
Loading
Loading