diff --git a/.changelog/44741.txt b/.changelog/44741.txt new file mode 100644 index 000000000000..f617fd0f6956 --- /dev/null +++ b/.changelog/44741.txt @@ -0,0 +1,11 @@ +```release-note:enhancement +resource/aws_lb_listener_rule: Add `regex_values` argument to `condition.host_header`, `condition.http_header` and `condition.path_pattern` blocks +``` + +```release-note:enhancement +resource/aws_lb_listener_rule: The `values` argument in `condition.host_header`, `condition.http_header` and `condition.path_pattern` is now optional +``` + +```release-note:enhancement +data-source/aws_lb_listener_rule: Add `regex_values` attribute to `condition.host_header`, `condition.http_header` and `condition.path_pattern` blocks +``` diff --git a/internal/service/elbv2/listener_rule.go b/internal/service/elbv2/listener_rule.go index 9f26aca54656..e51fff04dbe9 100644 --- a/internal/service/elbv2/listener_rule.go +++ b/internal/service/elbv2/listener_rule.go @@ -342,9 +342,17 @@ func resourceListenerRule() *schema.Resource { Optional: true, Elem: &schema.Resource{ Schema: map[string]*schema.Schema{ + "regex_values": { + Type: schema.TypeSet, + Optional: true, + Elem: &schema.Schema{ + Type: schema.TypeString, + ValidateFunc: validation.StringLenBetween(1, 128), + }, + }, names.AttrValues: { Type: schema.TypeSet, - Required: true, + Optional: true, MinItems: 1, Elem: &schema.Schema{ Type: schema.TypeString, @@ -365,13 +373,21 @@ func resourceListenerRule() *schema.Resource { Required: true, ValidateFunc: validation.StringMatch(regexache.MustCompile("^[0-9A-Za-z_!#$%&'*+,.^`|~-]{1,40}$"), ""), // was "," meant to be included? +-. creates a range including: +,-. }, + "regex_values": { + Type: schema.TypeSet, + Optional: true, + Elem: &schema.Schema{ + Type: schema.TypeString, + ValidateFunc: validation.StringLenBetween(1, 128), + }, + }, names.AttrValues: { Type: schema.TypeSet, Elem: &schema.Schema{ Type: schema.TypeString, ValidateFunc: validation.StringLenBetween(1, 128), }, - Required: true, + Optional: true, }, }, }, @@ -399,9 +415,17 @@ func resourceListenerRule() *schema.Resource { Optional: true, Elem: &schema.Resource{ Schema: map[string]*schema.Schema{ + "regex_values": { + Type: schema.TypeSet, + Optional: true, + Elem: &schema.Schema{ + Type: schema.TypeString, + ValidateFunc: validation.StringLenBetween(1, 128), + }, + }, names.AttrValues: { Type: schema.TypeSet, - Required: true, + Optional: true, MinItems: 1, Elem: &schema.Schema{ Type: schema.TypeString, @@ -585,19 +609,10 @@ func resourceListenerRuleRead(ctx context.Context, d *schema.ResourceData, meta switch aws.ToString(condition.Field) { case "host-header": - conditionMap["host_header"] = []any{ - map[string]any{ - names.AttrValues: flex.FlattenStringValueSet(condition.HostHeaderConfig.Values), - }, - } + conditionMap["host_header"] = []any{flattenHostHeaderConditionConfig(condition.HostHeaderConfig)} case "http-header": - conditionMap["http_header"] = []any{ - map[string]any{ - "http_header_name": aws.ToString(condition.HttpHeaderConfig.HttpHeaderName), - names.AttrValues: flex.FlattenStringValueSet(condition.HttpHeaderConfig.Values), - }, - } + conditionMap["http_header"] = []any{flattenHTTPHeaderConditionConfig(condition.HttpHeaderConfig)} case "http-request-method": conditionMap["http_request_method"] = []any{ @@ -607,11 +622,7 @@ func resourceListenerRuleRead(ctx context.Context, d *schema.ResourceData, meta } case "path-pattern": - conditionMap["path_pattern"] = []any{ - map[string]any{ - names.AttrValues: flex.FlattenStringValueSet(condition.PathPatternConfig.Values), - }, - } + conditionMap["path_pattern"] = []any{flattenPathPatternConditionConfig(condition.PathPatternConfig)} case "query-string": values := make([]any, len(condition.QueryStringConfig.Values)) @@ -860,23 +871,15 @@ func expandRuleConditions(tfList []any) ([]awstypes.RuleCondition, error) { if hostHeader, ok := tfMap["host_header"].([]any); ok && len(hostHeader) > 0 { field = "host-header" attrs += 1 - values := hostHeader[0].(map[string]any)[names.AttrValues].(*schema.Set) - apiObjects[i].HostHeaderConfig = &awstypes.HostHeaderConditionConfig{ - Values: flex.ExpandStringValueSet(values), - } + apiObjects[i].HostHeaderConfig = expandHostHeaderConditionConfig(hostHeader[0].(map[string]any)) } if httpHeader, ok := tfMap["http_header"].([]any); ok && len(httpHeader) > 0 { field = "http-header" attrs += 1 - httpHeaderMap := httpHeader[0].(map[string]any) - values := httpHeaderMap[names.AttrValues].(*schema.Set) - apiObjects[i].HttpHeaderConfig = &awstypes.HttpHeaderConditionConfig{ - HttpHeaderName: aws.String(httpHeaderMap["http_header_name"].(string)), - Values: flex.ExpandStringValueSet(values), - } + apiObjects[i].HttpHeaderConfig = expandHTTPHeaderConditionConfig(httpHeader[0].(map[string]any)) } if httpRequestMethod, ok := tfMap["http_request_method"].([]any); ok && len(httpRequestMethod) > 0 { @@ -892,11 +895,8 @@ func expandRuleConditions(tfList []any) ([]awstypes.RuleCondition, error) { if pathPattern, ok := tfMap["path_pattern"].([]any); ok && len(pathPattern) > 0 { field = "path-pattern" attrs += 1 - values := pathPattern[0].(map[string]any)[names.AttrValues].(*schema.Set) - apiObjects[i].PathPatternConfig = &awstypes.PathPatternConditionConfig{ - Values: flex.ExpandStringValueSet(values), - } + apiObjects[i].PathPatternConfig = expandPathPatternConditionConfig(pathPattern[0].(map[string]any)) } if queryString, ok := tfMap["query_string"].(*schema.Set); ok && queryString.Len() > 0 { @@ -943,3 +943,99 @@ func expandRuleConditions(tfList []any) ([]awstypes.RuleCondition, error) { return apiObjects, nil } + +func expandHostHeaderConditionConfig(tfMap map[string]any) *awstypes.HostHeaderConditionConfig { + if tfMap == nil { + return nil + } + + apiObject := &awstypes.HostHeaderConditionConfig{} + + if v, ok := tfMap[names.AttrValues].(*schema.Set); ok && v.Len() > 0 { + apiObject.Values = flex.ExpandStringValueSet(v) + } + if v, ok := tfMap["regex_values"].(*schema.Set); ok && v.Len() > 0 { + apiObject.RegexValues = flex.ExpandStringValueSet(v) + } + return apiObject +} + +func expandHTTPHeaderConditionConfig(tfMap map[string]any) *awstypes.HttpHeaderConditionConfig { + if tfMap == nil { + return nil + } + + apiObject := &awstypes.HttpHeaderConditionConfig{ + HttpHeaderName: aws.String(tfMap["http_header_name"].(string)), + } + + if v, ok := tfMap[names.AttrValues].(*schema.Set); ok && v.Len() > 0 { + apiObject.Values = flex.ExpandStringValueSet(v) + } + if v, ok := tfMap["regex_values"].(*schema.Set); ok && v.Len() > 0 { + apiObject.RegexValues = flex.ExpandStringValueSet(v) + } + return apiObject +} + +func expandPathPatternConditionConfig(tfMap map[string]any) *awstypes.PathPatternConditionConfig { + if tfMap == nil { + return nil + } + + apiObject := &awstypes.PathPatternConditionConfig{} + if v, ok := tfMap[names.AttrValues].(*schema.Set); ok && v.Len() > 0 { + apiObject.Values = flex.ExpandStringValueSet(v) + } + if v, ok := tfMap["regex_values"].(*schema.Set); ok && v.Len() > 0 { + apiObject.RegexValues = flex.ExpandStringValueSet(v) + } + return apiObject +} + +func flattenHostHeaderConditionConfig(apiObject *awstypes.HostHeaderConditionConfig) map[string]any { + if apiObject == nil { + return nil + } + + tfMap := map[string]any{} + if apiObject.Values != nil { + tfMap[names.AttrValues] = flex.FlattenStringValueSet(apiObject.Values) + } + if apiObject.RegexValues != nil { + tfMap["regex_values"] = flex.FlattenStringValueSet(apiObject.RegexValues) + } + + return tfMap +} + +func flattenHTTPHeaderConditionConfig(apiObject *awstypes.HttpHeaderConditionConfig) map[string]any { + if apiObject == nil { + return nil + } + tfMap := map[string]any{ + "http_header_name": aws.ToString(apiObject.HttpHeaderName), + } + if apiObject.Values != nil { + tfMap[names.AttrValues] = flex.FlattenStringValueSet(apiObject.Values) + } + if apiObject.RegexValues != nil { + tfMap["regex_values"] = flex.FlattenStringValueSet(apiObject.RegexValues) + } + return tfMap +} + +func flattenPathPatternConditionConfig(apiObject *awstypes.PathPatternConditionConfig) map[string]any { + if apiObject == nil { + return nil + } + + tfMap := map[string]any{} + if apiObject.Values != nil { + tfMap[names.AttrValues] = flex.FlattenStringValueSet(apiObject.Values) + } + if apiObject.RegexValues != nil { + tfMap["regex_values"] = flex.FlattenStringValueSet(apiObject.RegexValues) + } + return tfMap +} diff --git a/internal/service/elbv2/listener_rule_data_source.go b/internal/service/elbv2/listener_rule_data_source.go index d68a9d06389d..3974d4a46411 100644 --- a/internal/service/elbv2/listener_rule_data_source.go +++ b/internal/service/elbv2/listener_rule_data_source.go @@ -224,6 +224,10 @@ func (d *listenerRuleDataSource) Schema(ctx context.Context, req datasource.Sche CustomType: fwtypes.NewListNestedObjectTypeOf[hostHeaderConfigModel](ctx), NestedObject: schema.NestedBlockObject{ Attributes: map[string]schema.Attribute{ + "regex_values": schema.SetAttribute{ + ElementType: types.StringType, + Computed: true, + }, names.AttrValues: schema.SetAttribute{ ElementType: types.StringType, Computed: true, @@ -238,6 +242,10 @@ func (d *listenerRuleDataSource) Schema(ctx context.Context, req datasource.Sche "http_header_name": schema.StringAttribute{ Computed: true, }, + "regex_values": schema.SetAttribute{ + ElementType: types.StringType, + Computed: true, + }, names.AttrValues: schema.SetAttribute{ ElementType: types.StringType, Computed: true, @@ -260,6 +268,10 @@ func (d *listenerRuleDataSource) Schema(ctx context.Context, req datasource.Sche CustomType: fwtypes.NewListNestedObjectTypeOf[pathPatternConfigModel](ctx), NestedObject: schema.NestedBlockObject{ Attributes: map[string]schema.Attribute{ + "regex_values": schema.SetAttribute{ + ElementType: types.StringType, + Computed: true, + }, names.AttrValues: schema.SetAttribute{ ElementType: types.StringType, Computed: true, @@ -464,11 +476,13 @@ type ruleConditionModel struct { } type hostHeaderConfigModel struct { - Values fwtypes.SetValueOf[types.String] `tfsdk:"values"` + RegexValues fwtypes.SetValueOf[types.String] `tfsdk:"regex_values"` + Values fwtypes.SetValueOf[types.String] `tfsdk:"values"` } type httpHeaderConfigModel struct { HTTPHeaderName types.String `tfsdk:"http_header_name"` + RegexValues fwtypes.SetValueOf[types.String] `tfsdk:"regex_values"` Values fwtypes.SetValueOf[types.String] `tfsdk:"values"` } @@ -477,7 +491,8 @@ type httpRquestMethodConfigModel struct { } type pathPatternConfigModel struct { - Values fwtypes.SetValueOf[types.String] `tfsdk:"values"` + RegexValues fwtypes.SetValueOf[types.String] `tfsdk:"regex_values"` + Values fwtypes.SetValueOf[types.String] `tfsdk:"values"` } type queryStringConfigModel struct { diff --git a/internal/service/elbv2/listener_rule_data_source_test.go b/internal/service/elbv2/listener_rule_data_source_test.go index c4e3152f9d65..c9986317b19d 100644 --- a/internal/service/elbv2/listener_rule_data_source_test.go +++ b/internal/service/elbv2/listener_rule_data_source_test.go @@ -93,6 +93,7 @@ func TestAccELBV2ListenerRuleDataSource_byARN(t *testing.T) { statecheck.ExpectKnownValue(dataSourceName, tfjsonpath.New(names.AttrCondition), knownvalue.SetExact([]knownvalue.Check{ expectKnownCondition("host_header", knownvalue.ListExact([]knownvalue.Check{ knownvalue.ObjectExact(map[string]knownvalue.Check{ + "regex_values": knownvalue.Null(), names.AttrValues: knownvalue.SetExact([]knownvalue.Check{ knownvalue.StringExact("example.com"), }), @@ -183,6 +184,7 @@ func TestAccELBV2ListenerRuleDataSource_byListenerAndPriority(t *testing.T) { statecheck.ExpectKnownValue(dataSourceName, tfjsonpath.New(names.AttrCondition), knownvalue.SetExact([]knownvalue.Check{ expectKnownCondition("host_header", knownvalue.ListExact([]knownvalue.Check{ knownvalue.ObjectExact(map[string]knownvalue.Check{ + "regex_values": knownvalue.Null(), names.AttrValues: knownvalue.SetExact([]knownvalue.Check{ knownvalue.StringExact("example.com"), }), @@ -588,6 +590,7 @@ func TestAccELBV2ListenerRuleDataSource_conditionHostHeader(t *testing.T) { statecheck.ExpectKnownValue(dataSourceName, tfjsonpath.New(names.AttrCondition), knownvalue.SetExact([]knownvalue.Check{ expectKnownCondition("host_header", knownvalue.ListExact([]knownvalue.Check{ knownvalue.ObjectExact(map[string]knownvalue.Check{ + "regex_values": knownvalue.Null(), names.AttrValues: knownvalue.SetExact([]knownvalue.Check{ knownvalue.StringExact("example.com"), knownvalue.StringExact("www.example.com"), @@ -601,6 +604,45 @@ func TestAccELBV2ListenerRuleDataSource_conditionHostHeader(t *testing.T) { }) } +func TestAccELBV2ListenerRuleDataSource_conditionHostHeaderRegex(t *testing.T) { + ctx := acctest.Context(t) + if testing.Short() { + t.Skip("skipping long-running test in short mode") + } + + var listenerRule awstypes.Rule + rName := sdkacctest.RandomWithPrefix(acctest.ResourcePrefix) + dataSourceName := "data.aws_lb_listener_rule.test" + + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { acctest.PreCheck(ctx, t) }, + ErrorCheck: acctest.ErrorCheck(t, names.ELBV2ServiceID), + ProtoV5ProviderFactories: acctest.ProtoV5ProviderFactories, + CheckDestroy: testAccCheckListenerRuleDestroy(ctx), + Steps: []resource.TestStep{ + { + Config: testAccListenerRuleDataSourceConfig_conditionHostHeaderRegex(rName), + Check: resource.ComposeAggregateTestCheckFunc( + testAccCheckListenerRuleExists(ctx, dataSourceName, &listenerRule), + ), + ConfigStateChecks: []statecheck.StateCheck{ + statecheck.ExpectKnownValue(dataSourceName, tfjsonpath.New(names.AttrCondition), knownvalue.SetExact([]knownvalue.Check{ + expectKnownCondition("host_header", knownvalue.ListExact([]knownvalue.Check{ + knownvalue.ObjectExact(map[string]knownvalue.Check{ + "regex_values": knownvalue.SetExact([]knownvalue.Check{ + knownvalue.StringExact("^example\\.com$"), + knownvalue.StringExact("^www[0-9]+\\.example\\.com$"), + }), + names.AttrValues: knownvalue.Null(), + }), + })), + })), + }, + }, + }, + }) +} + func TestAccELBV2ListenerRuleDataSource_conditionHTTPHeader(t *testing.T) { ctx := acctest.Context(t) if testing.Short() { @@ -627,6 +669,7 @@ func TestAccELBV2ListenerRuleDataSource_conditionHTTPHeader(t *testing.T) { expectKnownCondition("http_header", knownvalue.ListExact([]knownvalue.Check{ knownvalue.ObjectExact(map[string]knownvalue.Check{ "http_header_name": knownvalue.StringExact("X-Forwarded-For"), + "regex_values": knownvalue.Null(), names.AttrValues: knownvalue.SetExact([]knownvalue.Check{ knownvalue.StringExact("192.168.1.*"), knownvalue.StringExact("10.0.0.*"), @@ -640,6 +683,46 @@ func TestAccELBV2ListenerRuleDataSource_conditionHTTPHeader(t *testing.T) { }) } +func TestAccELBV2ListenerRuleDataSource_conditionHTTPHeaderRegex(t *testing.T) { + ctx := acctest.Context(t) + if testing.Short() { + t.Skip("skipping long-running test in short mode") + } + + var listenerRule awstypes.Rule + rName := sdkacctest.RandomWithPrefix(acctest.ResourcePrefix) + dataSourceName := "data.aws_lb_listener_rule.test" + + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { acctest.PreCheck(ctx, t) }, + ErrorCheck: acctest.ErrorCheck(t, names.ELBV2ServiceID), + ProtoV5ProviderFactories: acctest.ProtoV5ProviderFactories, + CheckDestroy: testAccCheckListenerRuleDestroy(ctx), + Steps: []resource.TestStep{ + { + Config: testAccListenerRuleDataSourceConfig_conditionHTTPHeaderRegex(rName), + Check: resource.ComposeAggregateTestCheckFunc( + testAccCheckListenerRuleExists(ctx, dataSourceName, &listenerRule), + ), + ConfigStateChecks: []statecheck.StateCheck{ + statecheck.ExpectKnownValue(dataSourceName, tfjsonpath.New(names.AttrCondition), knownvalue.SetExact([]knownvalue.Check{ + expectKnownCondition("http_header", knownvalue.ListExact([]knownvalue.Check{ + knownvalue.ObjectExact(map[string]knownvalue.Check{ + "http_header_name": knownvalue.StringExact("User-Agent"), + "regex_values": knownvalue.SetExact([]knownvalue.Check{ + knownvalue.StringExact("A.+"), + knownvalue.StringExact("B.*C"), + }), + names.AttrValues: knownvalue.Null(), + }), + })), + })), + }, + }, + }, + }) +} + func TestAccELBV2ListenerRuleDataSource_conditionHTTPRequestMethod(t *testing.T) { ctx := acctest.Context(t) if testing.Short() { @@ -703,6 +786,7 @@ func TestAccELBV2ListenerRuleDataSource_conditionPathPattern(t *testing.T) { statecheck.ExpectKnownValue(dataSourceName, tfjsonpath.New(names.AttrCondition), knownvalue.SetExact([]knownvalue.Check{ expectKnownCondition("path_pattern", knownvalue.ListExact([]knownvalue.Check{ knownvalue.ObjectExact(map[string]knownvalue.Check{ + "regex_values": knownvalue.Null(), names.AttrValues: knownvalue.SetExact([]knownvalue.Check{ knownvalue.StringExact("/public/*"), knownvalue.StringExact("/cgi-bin/*"), @@ -716,6 +800,45 @@ func TestAccELBV2ListenerRuleDataSource_conditionPathPattern(t *testing.T) { }) } +func TestAccELBV2ListenerRuleDataSource_conditionPathPatternRegex(t *testing.T) { + ctx := acctest.Context(t) + if testing.Short() { + t.Skip("skipping long-running test in short mode") + } + + var listenerRule awstypes.Rule + rName := sdkacctest.RandomWithPrefix(acctest.ResourcePrefix) + dataSourceName := "data.aws_lb_listener_rule.test" + + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { acctest.PreCheck(ctx, t) }, + ErrorCheck: acctest.ErrorCheck(t, names.ELBV2ServiceID), + ProtoV5ProviderFactories: acctest.ProtoV5ProviderFactories, + CheckDestroy: testAccCheckListenerRuleDestroy(ctx), + Steps: []resource.TestStep{ + { + Config: testAccListenerRuleDataSourceConfig_conditionPathPatternRegex(rName), + Check: resource.ComposeAggregateTestCheckFunc( + testAccCheckListenerRuleExists(ctx, dataSourceName, &listenerRule), + ), + ConfigStateChecks: []statecheck.StateCheck{ + statecheck.ExpectKnownValue(dataSourceName, tfjsonpath.New(names.AttrCondition), knownvalue.SetExact([]knownvalue.Check{ + expectKnownCondition("path_pattern", knownvalue.ListExact([]knownvalue.Check{ + knownvalue.ObjectExact(map[string]knownvalue.Check{ + "regex_values": knownvalue.SetExact([]knownvalue.Check{ + knownvalue.StringExact("^\\/api\\/(.*)$"), + knownvalue.StringExact("^\\/api2\\/(.*)$"), + }), + names.AttrValues: knownvalue.Null(), + }), + })), + })), + }, + }, + }, + }) +} + func TestAccELBV2ListenerRuleDataSource_conditionQueryString(t *testing.T) { ctx := acctest.Context(t) if testing.Short() { @@ -1184,6 +1307,30 @@ resource "aws_lb_listener_rule" "test" { `) } +func testAccListenerRuleDataSourceConfig_conditionHostHeaderRegex(rName string) string { + return acctest.ConfigCompose(testAccListenerRuleConfig_baseWithHTTPListener(rName), ` +data "aws_lb_listener_rule" "test" { + arn = aws_lb_listener_rule.test.arn +} + +resource "aws_lb_listener_rule" "test" { + listener_arn = aws_lb_listener.test.arn + priority = 100 + + action { + type = "forward" + target_group_arn = aws_lb_target_group.test.arn + } + + condition { + host_header { + regex_values = ["^example\\.com$", "^www[0-9]+\\.example\\.com$"] + } + } +} +`) +} + func testAccListenerRuleDataSourceConfig_conditionHTTPHeader(rName string) string { return acctest.ConfigCompose(testAccListenerRuleConfig_baseWithHTTPListener(rName), ` data "aws_lb_listener_rule" "test" { @@ -1209,6 +1356,31 @@ resource "aws_lb_listener_rule" "test" { `) } +func testAccListenerRuleDataSourceConfig_conditionHTTPHeaderRegex(rName string) string { + return acctest.ConfigCompose(testAccListenerRuleConfig_baseWithHTTPListener(rName), ` +data "aws_lb_listener_rule" "test" { + arn = aws_lb_listener_rule.test.arn +} + +resource "aws_lb_listener_rule" "test" { + listener_arn = aws_lb_listener.test.arn + priority = 100 + + action { + type = "forward" + target_group_arn = aws_lb_target_group.test.arn + } + + condition { + http_header { + http_header_name = "User-Agent" + regex_values = ["A.+", "B.*C"] + } + } +} +`) +} + func testAccListenerRuleDataSourceConfig_conditionHTTPRequestMethod(rName string) string { return acctest.ConfigCompose(testAccListenerRuleConfig_baseWithHTTPListener(rName), ` data "aws_lb_listener_rule" "test" { @@ -1257,6 +1429,30 @@ resource "aws_lb_listener_rule" "test" { `) } +func testAccListenerRuleDataSourceConfig_conditionPathPatternRegex(rName string) string { + return acctest.ConfigCompose(testAccListenerRuleConfig_baseWithHTTPListener(rName), ` +data "aws_lb_listener_rule" "test" { + arn = aws_lb_listener_rule.test.arn +} + +resource "aws_lb_listener_rule" "test" { + listener_arn = aws_lb_listener.test.arn + priority = 100 + + action { + type = "forward" + target_group_arn = aws_lb_target_group.test.arn + } + + condition { + path_pattern { + regex_values = ["^\\/api\\/(.*)$", "^\\/api2\\/(.*)$"] + } + } +} +`) +} + func testAccListenerRuleDataSourceConfig_conditionQueryString(rName string) string { return acctest.ConfigCompose(testAccListenerRuleConfig_baseWithHTTPListener(rName), ` data "aws_lb_listener_rule" "test" { diff --git a/internal/service/elbv2/listener_rule_test.go b/internal/service/elbv2/listener_rule_test.go index 38181652614f..76cbe995e26c 100644 --- a/internal/service/elbv2/listener_rule_test.go +++ b/internal/service/elbv2/listener_rule_test.go @@ -1571,13 +1571,14 @@ func TestAccELBV2ListenerRule_conditionHostHeader(t *testing.T) { resource.TestCheckResourceAttr(resourceName, "action.#", "1"), resource.TestCheckResourceAttr(resourceName, "condition.#", "1"), resource.TestCheckTypeSetElemNestedAttrs(resourceName, "condition.*", map[string]string{ - "host_header.#": "1", - "host_header.0.values.#": "2", - "http_header.#": "0", - "http_request_method.#": "0", - "path_pattern.#": "0", - "query_string.#": "0", - "source_ip.#": "0", + "host_header.#": "1", + "host_header.0.values.#": "2", + "host_header.0.regex_values.#": "0", + "http_header.#": "0", + "http_request_method.#": "0", + "path_pattern.#": "0", + "query_string.#": "0", + "source_ip.#": "0", }), resource.TestCheckTypeSetElemAttr(resourceName, "condition.*.host_header.0.values.*", "example.com"), resource.TestCheckTypeSetElemAttr(resourceName, "condition.*.host_header.0.values.*", "www.example.com"), @@ -1587,6 +1588,47 @@ func TestAccELBV2ListenerRule_conditionHostHeader(t *testing.T) { }) } +func TestAccELBV2ListenerRule_conditionHostHeaderRegex(t *testing.T) { + ctx := acctest.Context(t) + var conf awstypes.Rule + lbName := fmt.Sprintf("testrule-hostHeader-%s", sdkacctest.RandString(12)) + + resourceName := "aws_lb_listener_rule.static" + frontEndListenerResourceName := "aws_lb_listener.front_end" + + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { acctest.PreCheck(ctx, t) }, + ErrorCheck: acctest.ErrorCheck(t, names.ELBV2ServiceID), + ProtoV5ProviderFactories: acctest.ProtoV5ProviderFactories, + CheckDestroy: testAccCheckListenerRuleDestroy(ctx), + Steps: []resource.TestStep{ + { + Config: testAccListenerRuleConfig_conditionHostHeaderRegex(lbName), + Check: resource.ComposeAggregateTestCheckFunc( + testAccCheckListenerRuleExists(ctx, resourceName, &conf), + acctest.MatchResourceAttrRegionalARN(ctx, resourceName, names.AttrARN, "elasticloadbalancing", regexache.MustCompile(fmt.Sprintf(`listener-rule/app/%s/.+$`, lbName))), + resource.TestCheckResourceAttrPair(resourceName, "listener_arn", frontEndListenerResourceName, names.AttrARN), + resource.TestCheckResourceAttr(resourceName, names.AttrPriority, "100"), + resource.TestCheckResourceAttr(resourceName, "action.#", "1"), + resource.TestCheckResourceAttr(resourceName, "condition.#", "1"), + resource.TestCheckTypeSetElemNestedAttrs(resourceName, "condition.*", map[string]string{ + "host_header.#": "1", + "host_header.0.values.#": "0", + "host_header.0.regex_values.#": "2", + "http_header.#": "0", + "http_request_method.#": "0", + "path_pattern.#": "0", + "query_string.#": "0", + "source_ip.#": "0", + }), + resource.TestCheckTypeSetElemAttr(resourceName, "condition.*.host_header.0.regex_values.*", "^example\\.com$"), + resource.TestCheckTypeSetElemAttr(resourceName, "condition.*.host_header.0.regex_values.*", "^www[0-9]+\\.example\\.com$"), + ), + }, + }, + }) +} + func TestAccELBV2ListenerRule_conditionHTTPHeader(t *testing.T) { ctx := acctest.Context(t) var conf awstypes.Rule @@ -1615,6 +1657,7 @@ func TestAccELBV2ListenerRule_conditionHTTPHeader(t *testing.T) { "http_header.#": "1", "http_header.0.http_header_name": "X-Forwarded-For", "http_header.0.values.#": "2", + "http_header.0.regex_values.#": "0", "http_request_method.#": "0", "path_pattern.#": "0", "query_string.#": "0", @@ -1627,6 +1670,7 @@ func TestAccELBV2ListenerRule_conditionHTTPHeader(t *testing.T) { "http_header.#": "1", "http_header.0.http_header_name": "Zz9~|_^.-+*'&%$#!0aA", "http_header.0.values.#": "1", + "http_header.0.regex_values.#": "0", "http_request_method.#": "0", "path_pattern.#": "0", "query_string.#": "0", @@ -1639,6 +1683,48 @@ func TestAccELBV2ListenerRule_conditionHTTPHeader(t *testing.T) { }) } +func TestAccELBV2ListenerRule_conditionHTTPHeaderRegex(t *testing.T) { + ctx := acctest.Context(t) + var conf awstypes.Rule + lbName := fmt.Sprintf("testrule-httpHeader-%s", sdkacctest.RandString(12)) + + resourceName := "aws_lb_listener_rule.static" + frontEndListenerResourceName := "aws_lb_listener.front_end" + + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { acctest.PreCheck(ctx, t) }, + ErrorCheck: acctest.ErrorCheck(t, names.ELBV2ServiceID), + ProtoV5ProviderFactories: acctest.ProtoV5ProviderFactories, + CheckDestroy: testAccCheckListenerRuleDestroy(ctx), + Steps: []resource.TestStep{ + { + Config: testAccListenerRuleConfig_conditionHTTPHeaderRegex(lbName), + Check: resource.ComposeAggregateTestCheckFunc( + testAccCheckListenerRuleExists(ctx, resourceName, &conf), + acctest.MatchResourceAttrRegionalARN(ctx, resourceName, names.AttrARN, "elasticloadbalancing", regexache.MustCompile(fmt.Sprintf(`listener-rule/app/%s/.+$`, lbName))), + resource.TestCheckResourceAttrPair(resourceName, "listener_arn", frontEndListenerResourceName, names.AttrARN), + resource.TestCheckResourceAttr(resourceName, names.AttrPriority, "100"), + resource.TestCheckResourceAttr(resourceName, "action.#", "1"), + resource.TestCheckResourceAttr(resourceName, "condition.#", "1"), + resource.TestCheckTypeSetElemNestedAttrs(resourceName, "condition.*", map[string]string{ + "host_header.#": "0", + "http_header.#": "1", + "http_header.0.http_header_name": "User-Agent", + "http_header.0.values.#": "0", + "http_header.0.regex_values.#": "2", + "http_request_method.#": "0", + "path_pattern.#": "0", + "query_string.#": "0", + "source_ip.#": "0", + }), + resource.TestCheckTypeSetElemAttr(resourceName, "condition.*.http_header.0.regex_values.*", "A.+"), + resource.TestCheckTypeSetElemAttr(resourceName, "condition.*.http_header.0.regex_values.*", "B.*C"), + ), + }, + }, + }) +} + func TestAccELBV2ListenerRule_ConditionHTTPHeader_invalid(t *testing.T) { ctx := acctest.Context(t) resource.ParallelTest(t, resource.TestCase{ @@ -1735,6 +1821,47 @@ func TestAccELBV2ListenerRule_conditionPathPattern(t *testing.T) { }) } +func TestAccELBV2ListenerRule_conditionPathPatternRegex(t *testing.T) { + ctx := acctest.Context(t) + var conf awstypes.Rule + lbName := fmt.Sprintf("testrule-pathPattern-%s", sdkacctest.RandString(11)) + + resourceName := "aws_lb_listener_rule.static" + frontEndListenerResourceName := "aws_lb_listener.front_end" + + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { acctest.PreCheck(ctx, t) }, + ErrorCheck: acctest.ErrorCheck(t, names.ELBV2ServiceID), + ProtoV5ProviderFactories: acctest.ProtoV5ProviderFactories, + CheckDestroy: testAccCheckListenerRuleDestroy(ctx), + Steps: []resource.TestStep{ + { + Config: testAccListenerRuleConfig_conditionPathPatternRegex(lbName), + Check: resource.ComposeAggregateTestCheckFunc( + testAccCheckListenerRuleExists(ctx, resourceName, &conf), + acctest.MatchResourceAttrRegionalARN(ctx, resourceName, names.AttrARN, "elasticloadbalancing", regexache.MustCompile(fmt.Sprintf(`listener-rule/app/%s/.+$`, lbName))), + resource.TestCheckResourceAttrPair(resourceName, "listener_arn", frontEndListenerResourceName, names.AttrARN), + resource.TestCheckResourceAttr(resourceName, names.AttrPriority, "100"), + resource.TestCheckResourceAttr(resourceName, "action.#", "1"), + resource.TestCheckResourceAttr(resourceName, "condition.#", "1"), + resource.TestCheckTypeSetElemNestedAttrs(resourceName, "condition.*", map[string]string{ + "host_header.#": "0", + "http_header.#": "0", + "http_request_method.#": "0", + "path_pattern.#": "1", + "path_pattern.0.values.#": "0", + "path_pattern.0.regex_values.#": "2", + "query_string.#": "0", + "source_ip.#": "0", + }), + resource.TestCheckTypeSetElemAttr(resourceName, "condition.*.path_pattern.0.regex_values.*", "^\\/api\\/(.*)$"), + resource.TestCheckTypeSetElemAttr(resourceName, "condition.*.path_pattern.0.regex_values.*", "^\\/api2\\/(.*)$"), + ), + }, + }, + }) +} + func TestAccELBV2ListenerRule_conditionQueryString(t *testing.T) { ctx := acctest.Context(t) var conf awstypes.Rule @@ -4500,6 +4627,16 @@ condition { `, lbName) } +func testAccListenerRuleConfig_conditionHostHeaderRegex(lbName string) string { + return testAccListenerRuleConfig_conditionBase(` +condition { + host_header { + regex_values = ["^example\\.com$", "^www[0-9]+\\.example\\.com$"] + } +} +`, lbName) +} + func testAccListenerRuleConfig_conditionHTTPHeader(lbName string) string { return testAccListenerRuleConfig_conditionBase(` condition { @@ -4518,6 +4655,17 @@ condition { `, lbName) } +func testAccListenerRuleConfig_conditionHTTPHeaderRegex(lbName string) string { + return testAccListenerRuleConfig_conditionBase(` +condition { + http_header { + http_header_name = "User-Agent" + regex_values = ["A.+", "B.*C"] + } +} +`, lbName) +} + func testAccListenerRuleConfig_conditionHTTPHeaderInvalid() string { return ` data "aws_partition" "current" {} @@ -4568,6 +4716,16 @@ condition { `, lbName) } +func testAccListenerRuleConfig_conditionPathPatternRegex(lbName string) string { + return testAccListenerRuleConfig_conditionBase(` +condition { + path_pattern { + regex_values = ["^\\/api\\/(.*)$", "^\\/api2\\/(.*)$"] + } +} +`, lbName) +} + func testAccListenerRuleConfig_conditionQueryString(lbName string) string { return testAccListenerRuleConfig_conditionBase(` condition { diff --git a/website/docs/d/lb_listener_rule.html.markdown b/website/docs/d/lb_listener_rule.html.markdown index cebd3c359abf..7c4428da4c8b 100644 --- a/website/docs/d/lb_listener_rule.html.markdown +++ b/website/docs/d/lb_listener_rule.html.markdown @@ -143,20 +143,33 @@ This data source exports the following attributes in addition to the arguments a ### `condition` -* `host_header` - Contains a single attribute `values`, which contains a set of host names. +* `host_header` - Host header patterns to match. + [Detailed below](#host_header). * `http_header` - HTTP header and values to match. [Detailed below](#http_header). * `http_request_method` - Contains a single attribute `values`, which contains a set of HTTP request methods. -* `path_pattern` - Contains a single attribute `values`, which contains a set of path patterns to compare against the request URL. +* `path_pattern` - Path patterns to compare against the request URL. + [Detailed below](#path_pattern). * `query_string` - Query string parameters to match. [Detailed below](#query_string). * `source_ip` - Contains a single attribute `values`, which contains a set of source IPs in CIDR notation. +#### `host_header` + +* `regex_values` - Set of regular expressions to compare against the host header. +* `values` - Set of host header value patterns to match. + #### `http_header` * `http_header_name` - Name of the HTTP header to match. +* `regex_values` - Set of regular expression to compare against the HTTP header. * `values` - Set of values to compare against the value of the HTTP header. +#### `path_pattern` + +* `regex_values` - Set of regular expressions to compare against the request URL. +* `values` - Set of path patterns to compare against the request URL. + #### `query_string` * `values` - Set of `key`-`value` pairs indicating the query string parameters to match. diff --git a/website/docs/r/lb_listener_rule.html.markdown b/website/docs/r/lb_listener_rule.html.markdown index 34d996f0eae0..262b599cea3a 100644 --- a/website/docs/r/lb_listener_rule.html.markdown +++ b/website/docs/r/lb_listener_rule.html.markdown @@ -304,21 +304,36 @@ One or more condition blocks can be set per rule. Most condition types can only Condition Blocks (for `condition`) support the following: -* `host_header` - (Optional) Contains a single `values` item which is a list of host header patterns to match. The maximum size of each pattern is 128 characters. Comparison is case insensitive. Wildcard characters supported: * (matches 0 or more characters) and ? (matches exactly 1 character). Only one pattern needs to match for the condition to be satisfied. +* `host_header` - (Optional) Host header patterns to match. [Host Header block](#host-header-blocks) fields documented below. * `http_header` - (Optional) HTTP headers to match. [HTTP Header block](#http-header-blocks) fields documented below. * `http_request_method` - (Optional) Contains a single `values` item which is a list of HTTP request methods or verbs to match. Maximum size is 40 characters. Only allowed characters are A-Z, hyphen (-) and underscore (\_). Comparison is case sensitive. Wildcards are not supported. Only one needs to match for the condition to be satisfied. AWS recommends that GET and HEAD requests are routed in the same way because the response to a HEAD request may be cached. -* `path_pattern` - (Optional) Contains a single `values` item which is a list of path patterns to match against the request URL. Maximum size of each pattern is 128 characters. Comparison is case sensitive. Wildcard characters supported: * (matches 0 or more characters) and ? (matches exactly 1 character). Only one pattern needs to match for the condition to be satisfied. Path pattern is compared only to the path of the URL, not to its query string. To compare against the query string, use a `query_string` condition. +* `path_pattern` - (Optional) Path patterns to match against the request URL. [Path Pattern block](#path-pattern-blocks) fields documented below. * `query_string` - (Optional) Query strings to match. [Query String block](#query-string-blocks) fields documented below. * `source_ip` - (Optional) Contains a single `values` item which is a list of source IP CIDR notations to match. You can use both IPv4 and IPv6 addresses. Wildcards are not supported. Condition is satisfied if the source IP address of the request matches one of the CIDR blocks. Condition is not satisfied by the addresses in the `X-Forwarded-For` header, use `http_header` condition instead. ~> **NOTE::** Exactly one of `host_header`, `http_header`, `http_request_method`, `path_pattern`, `query_string` or `source_ip` must be set per condition. +#### Host Header Blocks + +Host Header Blocks (for `host_header`) support the following: + +* `regex_values` - (Optional) List of regular expressions to compare against the host header. The maximum length of each string is 128 characters. Conflicts with `values`. +* `values` - (Optional) List of host header value patterns to match. Maximum size of each pattern is 128 characters. Comparison is case-insensitive. Wildcard characters supported: * (matches 0 or more characters) and ? (matches exactly 1 character). Only one pattern needs to match for the condition to be satisfied. Conflicts with `regex_values`. + #### HTTP Header Blocks HTTP Header Blocks (for `http_header`) support the following: -* `http_header_name` - (Required) Name of HTTP header to search. The maximum size is 40 characters. Comparison is case insensitive. Only RFC7240 characters are supported. Wildcards are not supported. You cannot use HTTP header condition to specify the host header, use a `host-header` condition instead. -* `values` - (Required) List of header value patterns to match. Maximum size of each pattern is 128 characters. Comparison is case insensitive. Wildcard characters supported: * (matches 0 or more characters) and ? (matches exactly 1 character). If the same header appears multiple times in the request they will be searched in order until a match is found. Only one pattern needs to match for the condition to be satisfied. To require that all of the strings are a match, create one condition block per string. +* `http_header_name` - (Required) Name of HTTP header to search. The maximum size is 40 characters. Comparison is case-insensitive. Only RFC7240 characters are supported. Wildcards are not supported. You cannot use HTTP header condition to specify the host header, use a `host-header` condition instead. +* `regex_values` - (Optional) List of regular expression to compare against the HTTP header. The maximum length of each string is 128 characters. Conflicts with `values`. +* `values` - (Optional) List of header value patterns to match. Maximum size of each pattern is 128 characters. Comparison is case-insensitive. Wildcard characters supported: * (matches 0 or more characters) and ? (matches exactly 1 character). If the same header appears multiple times in the request they will be searched in order until a match is found. Only one pattern needs to match for the condition to be satisfied. To require that all of the strings are a match, create one condition block per string. Conflicts with `regex_values`. + +#### Path Pattern Blocks + +Path Pattern Blocks (for `path_pattern`) support the following: + +* `regex_values` - (Optional) List of regular expressions to compare against the request URL. The maximum length of each string is 128 characters. Conflicts with `values`. +* `values` - (Optional) List of path patterns to compare against the request URL. Maximum size of each pattern is 128 characters. Comparison is case-sensitive. Wildcard characters supported: * (matches 0 or more characters) and ? (matches exactly 1 character). Only one pattern needs to match for the condition to be satisfied. Path pattern is compared only to the path of the URL, not to its query string. To compare against the query string, use a `query_string` condition. Conflicts with `regex_values`. #### Query String Blocks