Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
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
41 changes: 38 additions & 3 deletions docs/guide/gateway/l7gateway.md
Original file line number Diff line number Diff line change
Expand Up @@ -185,7 +185,7 @@ information see the [Gateway API Conformance Page](https://gateway-api.sigs.k8s.
| HTTPRouteRule - HTTPRouteFilter - RequestHeaderModifier | Core | ❌-- [Limited Support](https://docs.aws.amazon.com/elasticloadbalancing/latest/application/header-modification.html) |
| HTTPRouteRule - HTTPRouteFilter - ResponseHeaderModifier | Core | ❌ |
| HTTPRouteRule - HTTPRouteFilter - RequestMirror | Extended | ❌ |
| HTTPRouteRule - HTTPRouteFilter - RequestRedirect | Core | |
| HTTPRouteRule - HTTPRouteFilter - RequestRedirect | Core | ✅ -- See [ReplacePrefixMatch Limitation](#requestredirect-path-modification-replaceprefixmatch-limitation) below |
| HTTPRouteRule - HTTPRouteFilter - UrlRewrite | Extended | ✅ |
| HTTPRouteRule - HTTPRouteFilter - CORS | Extended | ❌ |
| HTTPRouteRule - HTTPRouteFilter - ExternalAuth | Extended | ❌ -- Use [ListenerRuleConfigurations](customization.md#customizing-l7-routing-rules) |
Expand All @@ -200,8 +200,43 @@ information see the [Gateway API Conformance Page](https://gateway-api.sigs.k8s.
Backend TLS is not supported by AWS ALB Gateway. For more information on how AWS ALB communicates with targets using encryption,
please see the [AWS documentation](https://docs.aws.amazon.com/elasticloadbalancing/latest/application/load-balancer-target-groups.html#target-group-routing-configuration).



##### RequestRedirect Path Modification ReplacePrefixMatch Limitation

The AWS Load Balancer Controller supports HTTPRoute RequestRedirect filters with both `ReplaceFullPath` and `ReplacePrefixMatch` path modification types.

**ReplacePrefixMatch Behavior:**

The behavior of `ReplacePrefixMatch` depends on whether other redirect components are modified:

1. **With scheme/port/hostname changes** - Path suffixes are preserved:
```yaml
filters:
- type: RequestRedirect
requestRedirect:
scheme: HTTPS # or port/hostname
path:
type: ReplacePrefixMatch
replacePrefixMatch: /new-prefix
```
- Request: `/old-prefix/path/to/resource`
- Redirects to: `/new-prefix/path/to/resource` ✅ (suffix preserved)

2. **Without other component changes** - Only prefix is replaced, suffixes are NOT preserved:
```yaml
filters:
- type: RequestRedirect
requestRedirect:
path:
type: ReplacePrefixMatch
replacePrefixMatch: /new-prefix
```
- Request: `/old-prefix/path/to/resource`
- Redirects to: `/new-prefix` ❌ (suffix lost)

**Recommendations:**

- For path-only redirects with exact paths, use `ReplaceFullPath`
- To preserve path suffixes with prefix replacement, also modify `scheme`, `port`, or `hostname`

#### Examples

Expand Down
12 changes: 7 additions & 5 deletions pkg/gateway/routeutils/route_rule_action.go
Original file line number Diff line number Diff line change
Expand Up @@ -259,12 +259,14 @@ func buildHttpRedirectAction(filter *gwv1.HTTPRequestRedirectFilter, redirectCon
path = filter.Path.ReplaceFullPath
isComponentSpecified = true
} else if filter.Path.ReplacePrefixMatch != nil {
pathValue := *filter.Path.ReplacePrefixMatch
if strings.ContainsAny(pathValue, "*?") {
return nil, errors.Errorf("ReplacePrefixMatch shouldn't contain wildcards: %v", pathValue)
// Use #{path} if other components are modified (avoids redirect loop)
// Otherwise use literal prefix (no suffix preservation)
if filter.Scheme != nil || filter.Port != nil || filter.Hostname != nil {
pathVariable := "/#{path}"
path = &pathVariable
} else {
path = filter.Path.ReplacePrefixMatch
Copy link
Collaborator

Choose a reason for hiding this comment

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

how is this different than a full path replacement?

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

good catch, i meant path = filter.Path.ReplacePrefixMatch + /* i will update it

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

actually, though path = filter.Path.ReplacePrefixMatch + /* makes more sense logically, but it will fail cases like below

  - matches:
    - path:
        type: PathPrefix
        value: /path-and-status
    filters:
    - type: RequestRedirect
      requestRedirect:
        path:
          type: ReplacePrefixMatch
          replacePrefixMatch: /replacement-prefix
        statusCode: 301

does it brings more value to have path = filter.Path.ReplacePrefixMatch + /*? if not, we can leave it same as full path replacement?

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

synced offline, updated

}
processedPath := fmt.Sprintf("%s/*", pathValue)
path = &processedPath
isComponentSpecified = true
}
}
Expand Down
73 changes: 58 additions & 15 deletions pkg/gateway/routeutils/route_rule_action_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@ package routeutils

import (
"context"
"testing"

awssdk "github.com/aws/aws-sdk-go-v2/aws"
"github.com/stretchr/testify/assert"
corev1 "k8s.io/api/core/v1"
Expand All @@ -13,7 +15,6 @@ import (
"sigs.k8s.io/controller-runtime/pkg/client"
"sigs.k8s.io/controller-runtime/pkg/client/fake"
gwv1 "sigs.k8s.io/gateway-api/apis/v1"
"testing"
)

func Test_buildHttpRedirectAction(t *testing.T) {
Expand All @@ -27,7 +28,6 @@ func Test_buildHttpRedirectAction(t *testing.T) {
query := "test-query"
replaceFullPath := "/new-path"
replacePrefixPath := "/new-prefix-path"
replacePrefixPathAfterProcessing := "/new-prefix-path/*"
invalidPath := "/invalid-path*"

tests := []struct {
Expand Down Expand Up @@ -66,8 +66,43 @@ func Test_buildHttpRedirectAction(t *testing.T) {
wantErr: false,
},
{
name: "redirect with prefix match",
name: "redirect with prefix match only - uses literal prefix",
filter: &gwv1.HTTPRequestRedirectFilter{
Path: &gwv1.HTTPPathModifier{
Type: gwv1.PrefixMatchHTTPPathModifier,
ReplacePrefixMatch: &replacePrefixPath,
},
},
want: &elbv2model.Action{
Type: elbv2model.ActionTypeRedirect,
RedirectConfig: &elbv2model.RedirectActionConfig{
Path: &replacePrefixPath,
},
},
wantErr: false,
},
{
name: "redirect with prefix match and scheme - uses #{path}",
filter: &gwv1.HTTPRequestRedirectFilter{
Scheme: &scheme,
Path: &gwv1.HTTPPathModifier{
Type: gwv1.PrefixMatchHTTPPathModifier,
ReplacePrefixMatch: &replacePrefixPath,
},
},
want: &elbv2model.Action{
Type: elbv2model.ActionTypeRedirect,
RedirectConfig: &elbv2model.RedirectActionConfig{
Path: awssdk.String("/#{path}"),
Protocol: &expectedScheme,
},
},
wantErr: false,
},
{
name: "redirect with prefix match and port - uses #{path}",
filter: &gwv1.HTTPRequestRedirectFilter{
Port: (*gwv1.PortNumber)(&port),
Path: &gwv1.HTTPPathModifier{
Type: gwv1.PrefixMatchHTTPPathModifier,
ReplacePrefixMatch: &replacePrefixPath,
Expand All @@ -76,7 +111,26 @@ func Test_buildHttpRedirectAction(t *testing.T) {
want: &elbv2model.Action{
Type: elbv2model.ActionTypeRedirect,
RedirectConfig: &elbv2model.RedirectActionConfig{
Path: &replacePrefixPathAfterProcessing,
Path: awssdk.String("/#{path}"),
Port: &portString,
},
},
wantErr: false,
},
{
name: "redirect with prefix match and hostname - uses #{path}",
filter: &gwv1.HTTPRequestRedirectFilter{
Hostname: (*gwv1.PreciseHostname)(&hostname),
Path: &gwv1.HTTPPathModifier{
Type: gwv1.PrefixMatchHTTPPathModifier,
ReplacePrefixMatch: &replacePrefixPath,
},
},
want: &elbv2model.Action{
Type: elbv2model.ActionTypeRedirect,
RedirectConfig: &elbv2model.RedirectActionConfig{
Path: awssdk.String("/#{path}"),
Host: &hostname,
},
},
wantErr: false,
Expand Down Expand Up @@ -106,17 +160,6 @@ func Test_buildHttpRedirectAction(t *testing.T) {
want: nil,
wantErr: true,
},
{
name: "path with wildcards in ReplacePrefixMatch",
filter: &gwv1.HTTPRequestRedirectFilter{
Path: &gwv1.HTTPPathModifier{
Type: gwv1.PrefixMatchHTTPPathModifier,
ReplacePrefixMatch: &invalidPath,
},
},
want: nil,
wantErr: true,
},
}

for _, tt := range tests {
Expand Down
7 changes: 6 additions & 1 deletion pkg/gateway/routeutils/route_rule_transform.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,10 @@ package routeutils

import (
"fmt"
"strings"

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

const (
Expand Down Expand Up @@ -35,6 +36,10 @@ func buildHTTPRuleTransforms(rule *gwv1.HTTPRouteRule, httpMatch *gwv1.HTTPRoute
transforms = append(transforms, generateHostHeaderRewriteTransform(*rf.URLRewrite.Hostname))
}
}
// Handle RequestRedirect with ReplacePrefixMatch as URLRewrite
if rf.RequestRedirect != nil && rf.RequestRedirect.Path != nil && rf.RequestRedirect.Path.ReplacePrefixMatch != nil {
transforms = append(transforms, generateURLRewritePathTransform(*rf.RequestRedirect.Path, httpMatch))
}
}
}

Expand Down
2 changes: 1 addition & 1 deletion test/e2e/gateway/alb_instance_target_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -473,7 +473,7 @@ var _ = Describe("test k8s alb gateway using instance targets reconciled by the
httpExp := httpexpect.New(tf.LoggerReporter, fmt.Sprintf("http://%v", dnsName))
httpExp.GET("/api/v1/users").WithRedirectPolicy(httpexpect.DontFollowRedirects).Expect().
Status(302).
Header("Location").Equal("https://api.example.com:80/v2/*")
Header("Location").Equal("https://api.example.com:80/v2/v1/users")
})

By("testing redirect with scheme and port change", func() {
Expand Down
2 changes: 1 addition & 1 deletion test/e2e/gateway/alb_ip_target_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -459,7 +459,7 @@ var _ = Describe("test k8s alb gateway using ip targets reconciled by the aws lo
httpExp := httpexpect.New(tf.LoggerReporter, fmt.Sprintf("http://%v", dnsName))
httpExp.GET("/api/v1/users").WithRedirectPolicy(httpexpect.DontFollowRedirects).Expect().
Status(302).
Header("Location").Equal("https://api.example.com:80/v2/*")
Header("Location").Equal("https://api.example.com:80/v2/v1/users")
})

By("testing redirect with scheme and port change", func() {
Expand Down
Loading