Skip to content
Open
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
53 changes: 53 additions & 0 deletions apis/gateway/v1beta1/listenerruleconfig_types.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,52 @@ const (
ListenerRuleConditionFieldSourceIP ListenerRuleConditionField = "source-ip"
)

// ListenerRuleTransformType defines the type of transform for the listener rule
// +kubebuilder:validation:Enum=host-header-rewrite
type ListenerRuleTransformType string

const (
ListenerRuleTransformTypeHostHeaderRewrite ListenerRuleTransformType = "host-header-rewrite"
)

// ListenerRuleRewriteConfig defines a regex-based rewrite rule
type ListenerRuleRewriteConfig struct {
// Regex expression to match against the header value
// +kubebuilder:validation:MinLength=1
Regex string `json:"regex"`
// Replacement expression to use when the regex matches.
// When SourceHeader is specified, this field supports referencing the matched value
// from the source header using regex capture groups (e.g., "$1").
Replace string `json:"replace"`
}

// ListenerRuleHostHeaderRewriteConfig defines configuration for rewriting the Host header
type ListenerRuleHostHeaderRewriteConfig struct {
// Rewrites defines the regex-based rewrite rules for the Host header.
// +kubebuilder:validation:MinItems=1
Rewrites []ListenerRuleRewriteConfig `json:"rewrites"`

// SourceHeader specifies the name of the incoming request header whose value
// should be used to rewrite the Host header. When specified, the Host header
// will be set based on the value of this source header, processed through
// the regex/replace rules defined in Rewrites.
// For example, setting SourceHeader to "X-School-Domain" will use the value
// of the X-School-Domain header as input for the rewrite rules.
// +optional
SourceHeader *string `json:"sourceHeader,omitempty"`
}

// ListenerRuleTransform defines a transform to apply to the request
// +kubebuilder:validation:XValidation:rule="has(self.type) && self.type == 'host-header-rewrite' ? has(self.hostHeaderRewriteConfig) : !has(self.hostHeaderRewriteConfig)",message="hostHeaderRewriteConfig must be specified only when type is 'host-header-rewrite'"
type ListenerRuleTransform struct {
// The type of transform
Type ListenerRuleTransformType `json:"type"`

// Information for a host header rewrite transform.
// +optional
HostHeaderRewriteConfig *ListenerRuleHostHeaderRewriteConfig `json:"hostHeaderRewriteConfig,omitempty"`
}

// AuthenticateCognitoActionConditionalBehaviorEnum defines the behavior when a user is not authenticated
// +kubebuilder:validation:Enum=deny;allow;authenticate
type AuthenticateCognitoActionConditionalBehaviorEnum string
Expand Down Expand Up @@ -344,6 +390,13 @@ type ListenerRuleConfigurationSpec struct {
// +kubebuilder:validation:MinItems=1
Conditions []ListenerRuleCondition `json:"conditions,omitempty"`

// Transforms defines the set of transforms to apply to the request before forwarding.
// Transforms allow modifying request attributes such as the Host header
// using regex-based rewrite rules.
// +optional
// +kubebuilder:validation:MaxItems=1
Transforms []ListenerRuleTransform `json:"transforms,omitempty"`

// Tags are the AWS resource tags to be applied to all AWS resources created for this rule.
// +optional
Tags *map[string]string `json:"tags,omitempty"`
Expand Down
36 changes: 34 additions & 2 deletions pkg/gateway/model/model_build_listener.go
Original file line number Diff line number Diff line change
Expand Up @@ -312,8 +312,11 @@ 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,
Transforms: mergeTransforms(
routeutils.BuildRoutingRuleTransforms(route, ruleWithPrecedence),
routeutils.BuildListenerRuleConfigTransforms(ruleWithPrecedence),
),
Tags: tags,
})

}
Expand All @@ -334,6 +337,35 @@ func (l listenerBuilderImpl) buildListenerRules(ctx context.Context, stack core.
return secrets, nil
}

// mergeTransforms combines transforms from Gateway API HTTPRoute filters and
// ListenerRuleConfiguration CRD. CRD transforms take precedence: if the CRD
// specifies a transform of the same type as one from the HTTPRoute, the CRD
// version replaces it.
func mergeTransforms(routeTransforms, crdTransforms []elbv2model.Transform) []elbv2model.Transform {
if len(crdTransforms) == 0 {
return routeTransforms
}
if len(routeTransforms) == 0 {
return crdTransforms
}

// Build a set of transform types from CRD transforms
crdTypes := make(map[elbv2model.TransformType]bool, len(crdTransforms))
for _, t := range crdTransforms {
crdTypes[t.Type] = true
}

// Keep route transforms that are not overridden by CRD
merged := make([]elbv2model.Transform, 0, len(routeTransforms)+len(crdTransforms))
for _, t := range routeTransforms {
if !crdTypes[t.Type] {
merged = append(merged, t)
}
}
merged = append(merged, crdTransforms...)
return merged
}

func (l listenerBuilderImpl) buildListenerTags(lbCfg elbv2gw.LoadBalancerConfiguration) (map[string]string, error) {
// We dont have tags at listener level cfg. Hence we add all the load balancer level tags to listeners.
return l.tagHelper.getLoadBalancerTags(lbCfg)
Expand Down
145 changes: 145 additions & 0 deletions pkg/gateway/model/model_build_listener_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -2461,3 +2461,148 @@ func TestMergeProtocols_WithQuic(t *testing.T) {
})
}
}

func Test_mergeTransforms(t *testing.T) {
testCases := []struct {
name string
routeTransforms []elbv2model.Transform
crdTransforms []elbv2model.Transform
expected []elbv2model.Transform
}{
{
name: "both nil",
routeTransforms: nil,
crdTransforms: nil,
expected: nil,
},
{
name: "crd transforms nil, route transforms returned",
routeTransforms: []elbv2model.Transform{
{
Type: elbv2model.TransformTypeHostHeaderRewrite,
HostHeaderRewriteConfig: &elbv2model.RewriteConfigObject{
Rewrites: []elbv2model.RewriteConfig{
{Regex: ".*", Replace: "route.example.com"},
},
},
},
},
crdTransforms: nil,
expected: []elbv2model.Transform{
{
Type: elbv2model.TransformTypeHostHeaderRewrite,
HostHeaderRewriteConfig: &elbv2model.RewriteConfigObject{
Rewrites: []elbv2model.RewriteConfig{
{Regex: ".*", Replace: "route.example.com"},
},
},
},
},
},
{
name: "route transforms nil, crd transforms returned",
routeTransforms: nil,
crdTransforms: []elbv2model.Transform{
{
Type: elbv2model.TransformTypeHostHeaderRewrite,
HostHeaderRewriteConfig: &elbv2model.RewriteConfigObject{
Rewrites: []elbv2model.RewriteConfig{
{Regex: ".*", Replace: "crd.example.com"},
},
},
},
},
expected: []elbv2model.Transform{
{
Type: elbv2model.TransformTypeHostHeaderRewrite,
HostHeaderRewriteConfig: &elbv2model.RewriteConfigObject{
Rewrites: []elbv2model.RewriteConfig{
{Regex: ".*", Replace: "crd.example.com"},
},
},
},
},
},
{
name: "crd overrides route transform of same type",
routeTransforms: []elbv2model.Transform{
{
Type: elbv2model.TransformTypeHostHeaderRewrite,
HostHeaderRewriteConfig: &elbv2model.RewriteConfigObject{
Rewrites: []elbv2model.RewriteConfig{
{Regex: ".*", Replace: "route.example.com"},
},
},
},
},
crdTransforms: []elbv2model.Transform{
{
Type: elbv2model.TransformTypeHostHeaderRewrite,
HostHeaderRewriteConfig: &elbv2model.RewriteConfigObject{
Rewrites: []elbv2model.RewriteConfig{
{Regex: ".*", Replace: "crd.example.com"},
},
},
},
},
expected: []elbv2model.Transform{
{
Type: elbv2model.TransformTypeHostHeaderRewrite,
HostHeaderRewriteConfig: &elbv2model.RewriteConfigObject{
Rewrites: []elbv2model.RewriteConfig{
{Regex: ".*", Replace: "crd.example.com"},
},
},
},
},
},
{
name: "different types are merged together",
routeTransforms: []elbv2model.Transform{
{
Type: elbv2model.TransformTypeUrlRewrite,
UrlRewriteConfig: &elbv2model.RewriteConfigObject{
Rewrites: []elbv2model.RewriteConfig{
{Regex: "^/old/(.*)", Replace: "/new/$1"},
},
},
},
},
crdTransforms: []elbv2model.Transform{
{
Type: elbv2model.TransformTypeHostHeaderRewrite,
HostHeaderRewriteConfig: &elbv2model.RewriteConfigObject{
Rewrites: []elbv2model.RewriteConfig{
{Regex: ".*", Replace: "crd.example.com"},
},
},
},
},
expected: []elbv2model.Transform{
{
Type: elbv2model.TransformTypeUrlRewrite,
UrlRewriteConfig: &elbv2model.RewriteConfigObject{
Rewrites: []elbv2model.RewriteConfig{
{Regex: "^/old/(.*)", Replace: "/new/$1"},
},
},
},
{
Type: elbv2model.TransformTypeHostHeaderRewrite,
HostHeaderRewriteConfig: &elbv2model.RewriteConfigObject{
Rewrites: []elbv2model.RewriteConfig{
{Regex: ".*", Replace: "crd.example.com"},
},
},
},
},
},
}

for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
result := mergeTransforms(tc.routeTransforms, tc.crdTransforms)
assert.Equal(t, tc.expected, result)
})
}
}
47 changes: 47 additions & 0 deletions pkg/gateway/routeutils/route_rule_transform.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import (
"fmt"
"strings"

elbv2gw "sigs.k8s.io/aws-load-balancer-controller/apis/gateway/v1beta1"
elbv2model "sigs.k8s.io/aws-load-balancer-controller/pkg/model/elbv2"
gwv1 "sigs.k8s.io/gateway-api/apis/v1"
)
Expand Down Expand Up @@ -122,3 +123,49 @@ func generatePrefixReplacementRegex(httpMatch *gwv1.HTTPRouteMatch, replacement

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

// BuildListenerRuleConfigTransforms converts transforms from the ListenerRuleConfiguration CRD
// into internal model transforms. These are merged with any transforms generated from
// Gateway API HTTPRoute filters (e.g., URLRewrite).
func BuildListenerRuleConfigTransforms(ruleWithPrecedence RulePrecedence) []elbv2model.Transform {
rule := ruleWithPrecedence.CommonRulePrecedence.Rule
if rule.GetListenerRuleConfig() == nil {
return nil
}
crdTransforms := rule.GetListenerRuleConfig().Spec.Transforms
if len(crdTransforms) == 0 {
return nil
}
var transforms []elbv2model.Transform
for _, crdTransform := range crdTransforms {
transforms = append(transforms, convertCRDTransform(crdTransform))
}
return transforms
}

func convertCRDTransform(crdTransform elbv2gw.ListenerRuleTransform) elbv2model.Transform {
switch crdTransform.Type {
case elbv2gw.ListenerRuleTransformTypeHostHeaderRewrite:
return convertCRDHostHeaderRewriteTransform(crdTransform)
default:
return elbv2model.Transform{}
}
}

func convertCRDHostHeaderRewriteTransform(crdTransform elbv2gw.ListenerRuleTransform) elbv2model.Transform {
rewrites := make([]elbv2model.RewriteConfig, 0, len(crdTransform.HostHeaderRewriteConfig.Rewrites))
for _, r := range crdTransform.HostHeaderRewriteConfig.Rewrites {
rewrites = append(rewrites, elbv2model.RewriteConfig{
Regex: r.Regex,
Replace: r.Replace,
})
}
rewriteObj := &elbv2model.RewriteConfigObject{
Rewrites: rewrites,
SourceHeader: crdTransform.HostHeaderRewriteConfig.SourceHeader,
}
return elbv2model.Transform{
Type: elbv2model.TransformTypeHostHeaderRewrite,
HostHeaderRewriteConfig: rewriteObj,
}
}
Loading