From 90c3d822680fdd696a75b36ec81257bb601f62b0 Mon Sep 17 00:00:00 2001 From: Zachary Nixon Date: Fri, 24 Oct 2025 18:36:10 -0700 Subject: [PATCH 1/2] lrc + transforms --- docs/guide/gateway/l7gateway.md | 2 +- pkg/gateway/model/model_build_listener.go | 2 + pkg/gateway/routeutils/route_rule_action.go | 2 + .../routeutils/route_rule_transform.go | 119 +++++ .../routeutils/route_rule_transform_test.go | 491 ++++++++++++++++++ pkg/model/elbv2/listener_rule.go | 1 + 6 files changed, 616 insertions(+), 1 deletion(-) create mode 100644 pkg/gateway/routeutils/route_rule_transform.go create mode 100644 pkg/gateway/routeutils/route_rule_transform_test.go diff --git a/docs/guide/gateway/l7gateway.md b/docs/guide/gateway/l7gateway.md index 0700c8d1c..63c2900bd 100644 --- a/docs/guide/gateway/l7gateway.md +++ b/docs/guide/gateway/l7gateway.md @@ -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) | diff --git a/pkg/gateway/model/model_build_listener.go b/pkg/gateway/model/model_build_listener.go index 3a5471725..7ebe37e7e 100644 --- a/pkg/gateway/model/model_build_listener.go +++ b/pkg/gateway/model/model_build_listener.go @@ -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, }) @@ -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 diff --git a/pkg/gateway/routeutils/route_rule_action.go b/pkg/gateway/routeutils/route_rule_action.go index 1484d60ea..7bdbdb641 100644 --- a/pkg/gateway/routeutils/route_rule_action.go +++ b/pkg/gateway/routeutils/route_rule_action.go @@ -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) } diff --git a/pkg/gateway/routeutils/route_rule_transform.go b/pkg/gateway/routeutils/route_rule_transform.go new file mode 100644 index 000000000..8fdcaed5a --- /dev/null +++ b/pkg/gateway/routeutils/route_rule_transform.go @@ -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{} + } +} + +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 { + 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 + + /* + If we're being asked to replace a prefix with "", we still need to keep one '/' to form a valid path. + 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' + + */ + if replacement == "" { + replacement = "/" + } else if !strings.HasSuffix(replacement, "/") { + replacement = fmt.Sprintf("%s$2", replacement) + } + + return fmt.Sprintf("(^%s(/)?)", match), replacement +} diff --git a/pkg/gateway/routeutils/route_rule_transform_test.go b/pkg/gateway/routeutils/route_rule_transform_test.go new file mode 100644 index 000000000..6c00977bd --- /dev/null +++ b/pkg/gateway/routeutils/route_rule_transform_test.go @@ -0,0 +1,491 @@ +package routeutils + +import ( + awssdk "github.com/aws/aws-sdk-go-v2/aws" + "github.com/stretchr/testify/assert" + "regexp" + "sigs.k8s.io/aws-load-balancer-controller/pkg/model/elbv2" + gwv1 "sigs.k8s.io/gateway-api/apis/v1" + "testing" +) + +func Test_BuildRoutingRuleTransforms(t *testing.T) { + exact := gwv1.PathMatchExact + testCases := []struct { + name string + route RouteDescriptor + rule RulePrecedence + expected []elbv2.Transform + }{ + { + name: "unsupported route", + route: &mockRoute{routeKind: GRPCRouteKind}, + rule: RulePrecedence{}, + expected: []elbv2.Transform{}, + }, + { + name: "no transforms", + route: &mockRoute{ + routeKind: HTTPRouteKind, + }, + rule: RulePrecedence{ + CommonRulePrecedence: CommonRulePrecedence{ + Rule: convertHTTPRouteRule(&gwv1.HTTPRouteRule{ + Matches: []gwv1.HTTPRouteMatch{ + { + Path: &gwv1.HTTPPathMatch{ + Type: &exact, + Value: awssdk.String("/foo"), + }, + }, + }, + }, nil, nil), + }, + }, + }, + { + name: "path rewrite", + route: &mockRoute{ + routeKind: HTTPRouteKind, + }, + rule: RulePrecedence{ + CommonRulePrecedence: CommonRulePrecedence{ + Rule: convertHTTPRouteRule(&gwv1.HTTPRouteRule{ + Matches: []gwv1.HTTPRouteMatch{ + { + Path: &gwv1.HTTPPathMatch{ + Type: &exact, + Value: awssdk.String("/foo"), + }, + }, + }, + Filters: []gwv1.HTTPRouteFilter{ + { + URLRewrite: &gwv1.HTTPURLRewriteFilter{ + Hostname: nil, + Path: &gwv1.HTTPPathModifier{ + Type: gwv1.FullPathHTTPPathModifier, + ReplaceFullPath: awssdk.String("/bar"), + }, + }, + }, + }, + }, nil, nil), + }, + }, + expected: []elbv2.Transform{ + { + Type: elbv2.TransformTypeUrlRewrite, + UrlRewriteConfig: &elbv2.RewriteConfigObject{ + Rewrites: []elbv2.RewriteConfig{ + { + Regex: "^([^?]*)", + Replace: "/bar", + }, + }, + }, + }, + }, + }, + { + name: "header rewrite", + route: &mockRoute{ + routeKind: HTTPRouteKind, + }, + rule: RulePrecedence{ + CommonRulePrecedence: CommonRulePrecedence{ + Rule: convertHTTPRouteRule(&gwv1.HTTPRouteRule{ + Matches: []gwv1.HTTPRouteMatch{ + { + Path: &gwv1.HTTPPathMatch{ + Type: &exact, + Value: awssdk.String("/foo"), + }, + }, + }, + Filters: []gwv1.HTTPRouteFilter{ + { + URLRewrite: &gwv1.HTTPURLRewriteFilter{ + Hostname: (*gwv1.PreciseHostname)(awssdk.String("foo.com")), + }, + }, + }, + }, nil, nil), + }, + }, + expected: []elbv2.Transform{ + { + Type: elbv2.TransformTypeHostHeaderRewrite, + HostHeaderRewriteConfig: &elbv2.RewriteConfigObject{ + Rewrites: []elbv2.RewriteConfig{ + { + Regex: ".*", + Replace: "foo.com", + }, + }, + }, + }, + }, + }, + { + name: "header and url rewrite", + route: &mockRoute{ + routeKind: HTTPRouteKind, + }, + rule: RulePrecedence{ + CommonRulePrecedence: CommonRulePrecedence{ + Rule: convertHTTPRouteRule(&gwv1.HTTPRouteRule{ + Matches: []gwv1.HTTPRouteMatch{ + { + Path: &gwv1.HTTPPathMatch{ + Type: &exact, + Value: awssdk.String("/foo"), + }, + }, + }, + Filters: []gwv1.HTTPRouteFilter{ + { + URLRewrite: &gwv1.HTTPURLRewriteFilter{ + Hostname: (*gwv1.PreciseHostname)(awssdk.String("foo.com")), + Path: &gwv1.HTTPPathModifier{ + Type: gwv1.FullPathHTTPPathModifier, + ReplaceFullPath: awssdk.String("/bar"), + }, + }, + }, + }, + }, nil, nil), + }, + }, + expected: []elbv2.Transform{ + { + Type: elbv2.TransformTypeUrlRewrite, + UrlRewriteConfig: &elbv2.RewriteConfigObject{ + Rewrites: []elbv2.RewriteConfig{ + { + Regex: "^([^?]*)", + Replace: "/bar", + }, + }, + }, + }, + { + Type: elbv2.TransformTypeHostHeaderRewrite, + HostHeaderRewriteConfig: &elbv2.RewriteConfigObject{ + Rewrites: []elbv2.RewriteConfig{ + { + Regex: ".*", + Replace: "foo.com", + }, + }, + }, + }, + }, + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + result := BuildRoutingRuleTransforms(tc.route, tc.rule) + assert.Equal(t, tc.expected, result) + }) + } +} + +func Test_generateURLRewritePathTransform(t *testing.T) { + + type pathWriteCase struct { + input string + output string + } + + prefix := gwv1.PathMatchPathPrefix + testCases := []struct { + name string + gwPathModifier gwv1.HTTPPathModifier + httpMatch *gwv1.HTTPRouteMatch + expected elbv2.Transform + rewriteCases []pathWriteCase + }{ + { + name: "full path rewrite", + gwPathModifier: gwv1.HTTPPathModifier{ + Type: gwv1.FullPathHTTPPathModifier, + ReplaceFullPath: awssdk.String("/cat"), + }, + httpMatch: &gwv1.HTTPRouteMatch{ + Path: &gwv1.HTTPPathMatch{}, + }, + expected: elbv2.Transform{ + Type: elbv2.TransformTypeUrlRewrite, + UrlRewriteConfig: &elbv2.RewriteConfigObject{ + Rewrites: []elbv2.RewriteConfig{ + { + Regex: "^([^?]*)", + Replace: "/cat", + }, + }, + }, + }, + rewriteCases: []pathWriteCase{ + { + input: "/foo", + output: "/cat", + }, + { + input: "/foo/bar/baz/bat/", + output: "/cat", + }, + { + input: "/", + output: "/cat", + }, + { + input: "/foo?q1=q2", + output: "/cat?q1=q2", + }, + { + input: "/foo?q1=q2&q3=q4", + output: "/cat?q1=q2&q3=q4", + }, + }, + }, + { + name: "prefix path rewrite", + httpMatch: &gwv1.HTTPRouteMatch{ + Path: &gwv1.HTTPPathMatch{ + Type: &prefix, + Value: awssdk.String("/foo"), + }, + }, + gwPathModifier: gwv1.HTTPPathModifier{ + Type: gwv1.PrefixMatchHTTPPathModifier, + ReplacePrefixMatch: awssdk.String("/cat"), + }, + expected: elbv2.Transform{ + Type: elbv2.TransformTypeUrlRewrite, + UrlRewriteConfig: &elbv2.RewriteConfigObject{ + Rewrites: []elbv2.RewriteConfig{ + { + Regex: "(^/foo(/)?)", + Replace: "/cat$2", + }, + }, + }, + }, + rewriteCases: []pathWriteCase{ + { + input: "/foo", + output: "/cat", + }, + { + input: "/foo/bar/baz/bat/", + output: "/cat/bar/baz/bat/", + }, + { + input: "/foo?q1=q2", + output: "/cat?q1=q2", + }, + { + input: "/foo?q1=q2&q3=q4", + output: "/cat?q1=q2&q3=q4", + }, + { + input: "/foo/bar", + output: "/cat/bar", + }, + }, + }, + { + name: "prefix path rewrite with explicit '/' on suffix", + httpMatch: &gwv1.HTTPRouteMatch{ + Path: &gwv1.HTTPPathMatch{ + Type: &prefix, + Value: awssdk.String("/foo"), + }, + }, + gwPathModifier: gwv1.HTTPPathModifier{ + Type: gwv1.PrefixMatchHTTPPathModifier, + ReplacePrefixMatch: awssdk.String("/cat/"), + }, + expected: elbv2.Transform{ + Type: elbv2.TransformTypeUrlRewrite, + UrlRewriteConfig: &elbv2.RewriteConfigObject{ + Rewrites: []elbv2.RewriteConfig{ + { + Regex: "(^/foo(/)?)", + Replace: "/cat/", + }, + }, + }, + }, + rewriteCases: []pathWriteCase{ + { + input: "/foo", + output: "/cat/", + }, + { + input: "/foo/bar/baz/bat/", + output: "/cat/bar/baz/bat/", + }, + { + input: "/foo?q1=q2", + output: "/cat/?q1=q2", + }, + { + input: "/foo?q1=q2&q3=q4", + output: "/cat/?q1=q2&q3=q4", + }, + { + input: "/foo/bar", + output: "/cat/bar", + }, + }, + }, + { + name: "prefix path rewrite - empty replace", + httpMatch: &gwv1.HTTPRouteMatch{ + Path: &gwv1.HTTPPathMatch{ + Type: &prefix, + Value: awssdk.String("/foo"), + }, + }, + gwPathModifier: gwv1.HTTPPathModifier{ + Type: gwv1.PrefixMatchHTTPPathModifier, + ReplacePrefixMatch: awssdk.String(""), + }, + expected: elbv2.Transform{ + Type: elbv2.TransformTypeUrlRewrite, + UrlRewriteConfig: &elbv2.RewriteConfigObject{ + Rewrites: []elbv2.RewriteConfig{ + { + Regex: "(^/foo(/)?)", + Replace: "/", + }, + }, + }, + }, + rewriteCases: []pathWriteCase{ + { + input: "/foo", + output: "/", + }, + { + input: "/foo/bar/baz/bat/", + output: "/bar/baz/bat/", + }, + { + input: "/foo?q1=q2", + output: "/?q1=q2", + }, + { + input: "/foo?q1=q2&q3=q4", + output: "/?q1=q2&q3=q4", + }, + { + input: "/foo/bar", + output: "/bar", + }, + { + input: "/foo/bar/", + output: "/bar/", + }, + }, + }, + { + name: "prefix path rewrite - '/' replace", + httpMatch: &gwv1.HTTPRouteMatch{ + Path: &gwv1.HTTPPathMatch{ + Type: &prefix, + Value: awssdk.String("/foo"), + }, + }, + gwPathModifier: gwv1.HTTPPathModifier{ + Type: gwv1.PrefixMatchHTTPPathModifier, + ReplacePrefixMatch: awssdk.String("/"), + }, + expected: elbv2.Transform{ + Type: elbv2.TransformTypeUrlRewrite, + UrlRewriteConfig: &elbv2.RewriteConfigObject{ + Rewrites: []elbv2.RewriteConfig{ + { + Regex: "(^/foo(/)?)", + Replace: "/", + }, + }, + }, + }, + rewriteCases: []pathWriteCase{ + { + input: "/foo", + output: "/", + }, + { + input: "/foo/bar/baz/bat/", + output: "/bar/baz/bat/", + }, + { + input: "/foo?q1=q2", + output: "/?q1=q2", + }, + { + input: "/foo?q1=q2&q3=q4", + output: "/?q1=q2&q3=q4", + }, + { + input: "/foo/bar", + output: "/bar", + }, + { + input: "/foo/bar/", + output: "/bar/", + }, + }, + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + result := generateURLRewritePathTransform(tc.gwPathModifier, tc.httpMatch) + assert.Equal(t, tc.expected, result) + + for _, rwCase := range tc.rewriteCases { + re, err := regexp.Compile(result.UrlRewriteConfig.Rewrites[0].Regex) + assert.NoError(t, err) + rewriteValue := re.ReplaceAllString(rwCase.input, result.UrlRewriteConfig.Rewrites[0].Replace) + assert.Equal(t, rwCase.output, rewriteValue) + } + }) + } +} + +func Test_generateHostHeaderRewriteTransform(t *testing.T) { + testCases := []struct { + name string + hostname gwv1.PreciseHostname + expected elbv2.Transform + }{ + { + name: "with header rewrite", + hostname: "foo.com", + expected: elbv2.Transform{ + Type: elbv2.TransformTypeHostHeaderRewrite, + HostHeaderRewriteConfig: &elbv2.RewriteConfigObject{ + Rewrites: []elbv2.RewriteConfig{ + { + Regex: ".*", + Replace: "foo.com", + }, + }, + }, + }, + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + result := generateHostHeaderRewriteTransform(tc.hostname) + assert.Equal(t, tc.expected, result) + }) + } +} diff --git a/pkg/model/elbv2/listener_rule.go b/pkg/model/elbv2/listener_rule.go index 5cce310f6..10923952f 100644 --- a/pkg/model/elbv2/listener_rule.go +++ b/pkg/model/elbv2/listener_rule.go @@ -194,5 +194,6 @@ type ListenerRuleStatus struct { type Rule struct { Conditions []RuleCondition Actions []Action + Transforms []Transform Tags map[string]string } From fb3074e36c75a920eea81851bc1ab258f7ba08c6 Mon Sep 17 00:00:00 2001 From: Zachary Nixon Date: Wed, 29 Oct 2025 22:28:00 -0700 Subject: [PATCH 2/2] add e2e tests, remove route validator --- test/e2e/gateway/alb_ip_target_test.go | 138 +++++++++++++++++- .../verifier/aws_resource_verifier.go | 52 ++++++- 2 files changed, 185 insertions(+), 5 deletions(-) diff --git a/test/e2e/gateway/alb_ip_target_test.go b/test/e2e/gateway/alb_ip_target_test.go index 6436f0e77..adc25f006 100644 --- a/test/e2e/gateway/alb_ip_target_test.go +++ b/test/e2e/gateway/alb_ip_target_test.go @@ -4,9 +4,6 @@ import ( "context" "crypto/tls" "fmt" - "strings" - "time" - awssdk "github.com/aws/aws-sdk-go-v2/aws" elbv2types "github.com/aws/aws-sdk-go-v2/service/elasticloadbalancingv2/types" "github.com/gavv/httpexpect/v2" @@ -24,6 +21,8 @@ import ( "sigs.k8s.io/aws-load-balancer-controller/test/framework/utils" "sigs.k8s.io/aws-load-balancer-controller/test/framework/verifier" gwv1 "sigs.k8s.io/gateway-api/apis/v1" + "strings" + "time" ) var _ = Describe("test k8s alb gateway using ip targets reconciled by the aws load balancer controller", func() { @@ -195,7 +194,6 @@ var _ = Describe("test k8s alb gateway using ip targets reconciled by the aws lo }) }) }) - Context("with ALB ip target configuration with HTTPRoute specified matches", func() { BeforeEach(func() {}) It("should provision internet-facing load balancer resources", func() { @@ -666,7 +664,139 @@ var _ = Describe("test k8s alb gateway using ip targets reconciled by the aws lo }) }) }) + Context("with ALB ip target configuration with path and url transforms", func() { + BeforeEach(func() {}) + It("should provision internet-facing load balancer resources", func() { + interf := elbv2gw.LoadBalancerSchemeInternetFacing + lbcSpec := elbv2gw.LoadBalancerConfigurationSpec{ + Scheme: &interf, + } + ipTargetType := elbv2gw.TargetTypeIP + tgSpec := elbv2gw.TargetGroupConfigurationSpec{ + DefaultConfiguration: elbv2gw.TargetGroupProps{ + TargetType: &ipTargetType, + }, + } + gwListeners := []gwv1.Listener{ + { + Name: "test-listener", + Port: 80, + Protocol: gwv1.HTTPProtocolType, + }, + } + + lrcSpec := elbv2gw.ListenerRuleConfigurationSpec{} + httpr := buildHTTPRoute([]string{}, []gwv1.HTTPRouteRule{}, &gwListeners[0].Name) + httpr.Spec.Rules[0].Filters = []gwv1.HTTPRouteFilter{ + { + Type: gwv1.HTTPRouteFilterURLRewrite, + URLRewrite: &gwv1.HTTPURLRewriteFilter{ + Hostname: (*gwv1.PreciseHostname)(awssdk.String("foo.com")), + Path: &gwv1.HTTPPathModifier{ + Type: gwv1.FullPathHTTPPathModifier, + ReplaceFullPath: awssdk.String("/my/cool/path"), + }, + }, + }, + } + + By("deploying stack", func() { + err := stack.DeployHTTP(ctx, nil, tf, gwListeners, []*gwv1.HTTPRoute{httpr}, lbcSpec, tgSpec, lrcSpec, nil, true) + Expect(err).NotTo(HaveOccurred()) + }) + + By("checking gateway status for lb dns name", func() { + dnsName = stack.GetLoadBalancerIngressHostName() + Expect(dnsName).ToNot(BeEmpty()) + }) + + By("querying AWS loadbalancer from the dns name", func() { + var err error + lbARN, err = tf.LBManager.FindLoadBalancerByDNSName(ctx, dnsName) + Expect(err).NotTo(HaveOccurred()) + Expect(lbARN).ToNot(BeEmpty()) + }) + By("verifying AWS loadbalancer resources", func() { + expectedTargetGroups := []verifier.ExpectedTargetGroup{ + { + Protocol: "HTTP", + Port: 80, + NumTargets: int(*stack.albResourceStack.commonStack.dps[0].Spec.Replicas), + TargetType: "ip", + TargetGroupHC: DEFAULT_ALB_TARGET_GROUP_HC, + }, + } + err := verifier.VerifyAWSLoadBalancerResources(ctx, tf, lbARN, verifier.LoadBalancerExpectation{ + Type: "application", + Scheme: "internet-facing", + Listeners: stack.albResourceStack.getListenersPortMap(), + TargetGroups: expectedTargetGroups, + }) + Expect(err).NotTo(HaveOccurred()) + }) + + By("verifying HTTP load balancer listener", func() { + err := verifier.VerifyLoadBalancerListener(ctx, tf, lbARN, int32(gwListeners[0].Port), &verifier.ListenerExpectation{ + ProtocolPort: "HTTP:80", + }) + Expect(err).NotTo(HaveOccurred()) + }) + By("verifying listener rules", func() { + err := verifier.VerifyLoadBalancerListenerRules(ctx, tf, lbARN, int32(gwListeners[0].Port), []verifier.ListenerRuleExpectation{ + { + Conditions: []elbv2types.RuleCondition{ + { + Field: awssdk.String(string(elbv2model.RuleConditionFieldPathPattern)), + PathPatternConfig: &elbv2types.PathPatternConditionConfig{ + Values: []string{"/*"}, + }, + }, + }, + Actions: []elbv2types.Action{ + { + Type: elbv2types.ActionTypeEnum(elbv2model.ActionTypeForward), + ForwardConfig: &elbv2types.ForwardActionConfig{ + TargetGroups: []elbv2types.TargetGroupTuple{ + { + TargetGroupArn: awssdk.String(testTargetGroupArn), + Weight: awssdk.Int32(1), + }, + }, + }, + }, + }, + Transforms: []elbv2types.RuleTransform{ + { + Type: elbv2types.TransformTypeEnumHostHeaderRewrite, + HostHeaderRewriteConfig: &elbv2types.HostHeaderRewriteConfig{ + Rewrites: []elbv2types.RewriteConfig{ + { + Replace: awssdk.String("foo.com"), + Regex: awssdk.String(".*"), + }, + }, + }, + }, + { + Type: elbv2types.TransformTypeEnumUrlRewrite, + UrlRewriteConfig: &elbv2types.UrlRewriteConfig{ + Rewrites: []elbv2types.RewriteConfig{ + { + Replace: awssdk.String("/my/cool/path"), + Regex: awssdk.String("^([^?]*)"), + }, + }, + }, + }, + }, + Priority: 1, + }, + }) + Expect(err).NotTo(HaveOccurred()) + }) + }) + }) Context("with ALB ip target configuration with secure HTTPRoute", func() { BeforeEach(func() {}) It("should provision internet-facing load balancer resources", func() { diff --git a/test/framework/verifier/aws_resource_verifier.go b/test/framework/verifier/aws_resource_verifier.go index ef8f7aaa1..fe0ebd815 100644 --- a/test/framework/verifier/aws_resource_verifier.go +++ b/test/framework/verifier/aws_resource_verifier.go @@ -68,6 +68,7 @@ type MutualAuthenticationExpectation struct { type ListenerRuleExpectation struct { Conditions []elbv2types.RuleCondition Actions []elbv2types.Action + Transforms []elbv2types.RuleTransform Priority int32 } @@ -424,7 +425,12 @@ func VerifyLoadBalancerListenerRules(ctx context.Context, f *framework.Framework if err := verifyListenerRuleConditions(actualRule.Conditions, expectedRule.Conditions); err != nil { return err } - if err := verifyListenerRuleActions(actualRule.Actions, expectedRule.Actions); err != nil { + + if err := verifyListenerRulePriority(int32(actualPriority), expectedRule.Priority); err != nil { + return err + } + + if err := verifyListenerRuleTransforms(actualRule.Transforms, expectedRule.Transforms); err != nil { return err } } @@ -566,6 +572,50 @@ func verifyListenerRuleConditions(actual, expected []elbv2types.RuleCondition) e return nil } +func verifyListenerRuleTransforms(actual, expected []elbv2types.RuleTransform) error { + if len(actual) != len(expected) { + return errors.Errorf("expected %d listener rule conditions, got %d", len(expected), len(actual)) + } + + sort.Slice(actual, func(i, j int) bool { + return actual[i].Type < actual[j].Type + }) + sort.Slice(expected, func(i, j int) bool { + return expected[i].Type < expected[j].Type + }) + + for i := 0; i < len(actual); i++ { + + if actual[i].Type != expected[i].Type { + return errors.Errorf("unexpected transform type. got %s", actual[i].Type) + } + + var actualRewriteConfig []elbv2types.RewriteConfig + var expectedRewriteConfig []elbv2types.RewriteConfig + + if actual[i].Type == elbv2types.TransformTypeEnumUrlRewrite { + actualRewriteConfig = actual[i].UrlRewriteConfig.Rewrites + expectedRewriteConfig = expected[i].UrlRewriteConfig.Rewrites + } else { + actualRewriteConfig = actual[i].HostHeaderRewriteConfig.Rewrites + expectedRewriteConfig = expected[i].HostHeaderRewriteConfig.Rewrites + } + + for rewriteIndx := 0; rewriteIndx < len(actualRewriteConfig); rewriteIndx++ { + if *actualRewriteConfig[rewriteIndx].Regex != *expectedRewriteConfig[rewriteIndx].Regex { + return errors.Errorf("expected regex %+v, got %+v", expected[i], actual[i]) + } + + if *actualRewriteConfig[rewriteIndx].Replace != *expectedRewriteConfig[rewriteIndx].Replace { + return errors.Errorf("expected replace %+v, got %+v", expected[i], actual[i]) + } + + } + } + + return nil +} + func verifyListenerRuleActions(actual, expected []elbv2types.Action) error { if len(actual) != len(expected) { return errors.Errorf("expected %d listener rule actions, got %d", len(expected), len(actual))