diff --git a/docs/guide/gateway/l7gateway.md b/docs/guide/gateway/l7gateway.md index 63c2900bd..e40b8d3a1 100644 --- a/docs/guide/gateway/l7gateway.md +++ b/docs/guide/gateway/l7gateway.md @@ -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) | @@ -200,8 +200,44 @@ 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:** + +We support `ReplacePrefixMatch` with limitations: + +1. **With scheme/port/hostname changes** - Works as expected: + ```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** - AWS ALB will reject with redirect loop error: + ```yaml + filters: + - type: RequestRedirect + requestRedirect: + path: + type: ReplacePrefixMatch + replacePrefixMatch: /new-prefix + ``` + - This configuration will be rejected by the API with "InvalidLoadBalancerAction: The redirect configuration is not valid because it creates a loop." ❌ + +**Recommendations:** + +- For path-only redirects, use `ReplaceFullPath` instead +- To use `ReplacePrefixMatch`, you must also modify `scheme`, `port`, or `hostname` + +**Important**: If one HTTPRoute rule has an invalid redirect configuration (e.g., path-only redirect with `ReplacePrefixMatch` that cause redirect loop), the controller will fail to create that listener rule and stop processing subsequent rules in the same HTTPRoute. This means valid rules with lower precedence (shorter paths, later in the route) will not be created. #### Examples diff --git a/pkg/gateway/routeutils/route_rule_action.go b/pkg/gateway/routeutils/route_rule_action.go index 7bdbdb641..6b8702de6 100644 --- a/pkg/gateway/routeutils/route_rule_action.go +++ b/pkg/gateway/routeutils/route_rule_action.go @@ -225,7 +225,6 @@ func buildHttpRuleRedirectActionsBasedOnFilter(filters []gwv1.HTTPRouteFilter, r // buildHttpRedirectAction configure filter attributes to RedirectActionConfig // gateway api has no attribute to specify query, use listener rule configuration func buildHttpRedirectAction(filter *gwv1.HTTPRequestRedirectFilter, redirectConfig *elbv2gw.RedirectActionConfig) (*elbv2model.Action, error) { - isComponentSpecified := false var statusCode string if filter.StatusCode != nil { statusCodeStr := fmt.Sprintf("HTTP_%d", *filter.StatusCode) @@ -236,7 +235,6 @@ func buildHttpRedirectAction(filter *gwv1.HTTPRequestRedirectFilter, redirectCon if filter.Port != nil { portStr := fmt.Sprintf("%d", *filter.Port) port = &portStr - isComponentSpecified = true } var protocol *string @@ -246,7 +244,6 @@ func buildHttpRedirectAction(filter *gwv1.HTTPRequestRedirectFilter, redirectCon return nil, errors.Errorf("unsupported redirect scheme: %v", upperScheme) } protocol = &upperScheme - isComponentSpecified = true } var path *string @@ -257,26 +254,18 @@ func buildHttpRedirectAction(filter *gwv1.HTTPRequestRedirectFilter, redirectCon return nil, errors.Errorf("ReplaceFullPath shouldn't contain wildcards: %v", pathValue) } path = filter.Path.ReplaceFullPath - isComponentSpecified = true } else if filter.Path.ReplacePrefixMatch != nil { + //url rewrite will handle path transform pathValue := *filter.Path.ReplacePrefixMatch if strings.ContainsAny(pathValue, "*?") { return nil, errors.Errorf("ReplacePrefixMatch shouldn't contain wildcards: %v", pathValue) } - processedPath := fmt.Sprintf("%s/*", pathValue) - path = &processedPath - isComponentSpecified = true } } var hostname *string if filter.Hostname != nil { hostname = (*string)(filter.Hostname) - isComponentSpecified = true - } - - if !isComponentSpecified { - return nil, errors.Errorf("To avoid a redirect loop, you must modify at least one of the following components: protocol, port, hostname or path.") } var query *string diff --git a/pkg/gateway/routeutils/route_rule_action_test.go b/pkg/gateway/routeutils/route_rule_action_test.go index 32a0c000a..24431e5ec 100644 --- a/pkg/gateway/routeutils/route_rule_action_test.go +++ b/pkg/gateway/routeutils/route_rule_action_test.go @@ -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" @@ -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) { @@ -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 { @@ -66,8 +66,9 @@ func Test_buildHttpRedirectAction(t *testing.T) { wantErr: false, }, { - name: "redirect with prefix match", + name: "redirect with prefix - no path in redirect config", filter: &gwv1.HTTPRequestRedirectFilter{ + Hostname: (*gwv1.PreciseHostname)(&hostname), Path: &gwv1.HTTPPathModifier{ Type: gwv1.PrefixMatchHTTPPathModifier, ReplacePrefixMatch: &replacePrefixPath, @@ -76,17 +77,11 @@ func Test_buildHttpRedirectAction(t *testing.T) { want: &elbv2model.Action{ Type: elbv2model.ActionTypeRedirect, RedirectConfig: &elbv2model.RedirectActionConfig{ - Path: &replacePrefixPathAfterProcessing, + Host: &hostname, }, }, wantErr: false, }, - { - name: "redirect with no component provided", - filter: &gwv1.HTTPRequestRedirectFilter{}, - want: nil, - wantErr: true, - }, { name: "invalid scheme provided", filter: &gwv1.HTTPRequestRedirectFilter{ @@ -106,17 +101,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 { diff --git a/pkg/gateway/routeutils/route_rule_transform.go b/pkg/gateway/routeutils/route_rule_transform.go index 8fdcaed5a..a6f82b49d 100644 --- a/pkg/gateway/routeutils/route_rule_transform.go +++ b/pkg/gateway/routeutils/route_rule_transform.go @@ -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 ( @@ -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)) + } } } diff --git a/test/e2e/gateway/alb_instance_target_test.go b/test/e2e/gateway/alb_instance_target_test.go index e65d030f0..6d30e836a 100644 --- a/test/e2e/gateway/alb_instance_target_test.go +++ b/test/e2e/gateway/alb_instance_target_test.go @@ -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() { diff --git a/test/e2e/gateway/alb_ip_target_test.go b/test/e2e/gateway/alb_ip_target_test.go index fc61676dc..25aa68bb9 100644 --- a/test/e2e/gateway/alb_ip_target_test.go +++ b/test/e2e/gateway/alb_ip_target_test.go @@ -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() {