diff --git a/.changelog/34584.txt b/.changelog/34584.txt new file mode 100644 index 00000000000..d64fa966924 --- /dev/null +++ b/.changelog/34584.txt @@ -0,0 +1,19 @@ +```release-note:new-resource +aws_lb_trust_store +``` + +```release-note:new-resource +aws_lb_trust_store_revocation +``` + +```release-note:new-data-source +aws_lb_trust_store +``` + +```release-note:enhancement +resource/aws_lb_listener: Add `mutual_authentication` configuration block +``` + +```release-note:enhancement +data-source/aws_lb_listener: Add `mutual_authentication` attribute +``` \ No newline at end of file diff --git a/.github/labeler-issue-triage.yml b/.github/labeler-issue-triage.yml index a88f3d70529..e6621c7c79f 100644 --- a/.github/labeler-issue-triage.yml +++ b/.github/labeler-issue-triage.yml @@ -252,7 +252,7 @@ service/elastictranscoder: service/elb: - '((\*|-)\s*`?|(data|resource)\s+"?)aws_(app_cookie_stickiness_policy|elb|lb_cookie_stickiness_policy|lb_ssl_negotiation_policy|load_balancer_|proxy_protocol_policy)' service/elbv2: - - '((\*|-)\s*`?|(data|resource)\s+"?)aws_a?lb(\b|_listener|_target_group|s)' + - '((\*|-)\s*`?|(data|resource)\s+"?)aws_a?lb(\b|_listener|_target_group|s|_trust_store)' service/emr: - '((\*|-)\s*`?|(data|resource)\s+"?)aws_emr_' service/emrcontainers: diff --git a/.github/labeler-pr-triage.yml b/.github/labeler-pr-triage.yml index 1cf4d794c6c..443a7680b61 100644 --- a/.github/labeler-pr-triage.yml +++ b/.github/labeler-pr-triage.yml @@ -438,6 +438,7 @@ service/elbv2: - 'website/**/lb_listener*' - 'website/**/lb_target_group*' - 'website/**/lb_hosted*' + - 'website/**/lb_trust_store*' service/emr: - 'internal/service/emr/**/*' - 'website/**/emr_*' diff --git a/.github/workflows/provider.yml b/.github/workflows/provider.yml index 76c1ddc824c..97f8f94fb84 100644 --- a/.github/workflows/provider.yml +++ b/.github/workflows/provider.yml @@ -292,8 +292,8 @@ jobs: tfproviderdocs check \ -allowed-resource-subcategories-file website/allowed-subcategories.txt \ -enable-contents-check \ - -ignore-file-missing-data-sources aws_alb,aws_alb_listener,aws_alb_target_group,aws_albs \ - -ignore-file-missing-resources aws_alb,aws_alb_listener,aws_alb_listener_certificate,aws_alb_listener_rule,aws_alb_target_group,aws_alb_target_group_attachment \ + -ignore-file-missing-data-sources aws_alb,aws_alb_listener,aws_alb_target_group,aws_alb_trust_store,aws_alb_trust_store_revocation,aws_albs \ + -ignore-file-missing-resources aws_alb,aws_alb_listener,aws_alb_listener_certificate,aws_alb_listener_rule,aws_alb_target_group,aws_alb_target_group_attachment,aws_alb_trust_store,aws_alb_trust_store_revocation \ -provider-source registry.terraform.io/hashicorp/aws \ -providers-schema-json terraform-providers-schema/schema.json \ -require-resource-subcategory \ diff --git a/internal/service/elbv2/find.go b/internal/service/elbv2/find.go deleted file mode 100644 index fa1ecb3814b..00000000000 --- a/internal/service/elbv2/find.go +++ /dev/null @@ -1,40 +0,0 @@ -// Copyright (c) HashiCorp, Inc. -// SPDX-License-Identifier: MPL-2.0 - -package elbv2 - -import ( - "context" - - "github.com/aws/aws-sdk-go/aws" - "github.com/aws/aws-sdk-go/service/elbv2" -) - -func FindListenerByARN(ctx context.Context, conn *elbv2.ELBV2, arn string) (*elbv2.Listener, error) { - input := &elbv2.DescribeListenersInput{ - ListenerArns: aws.StringSlice([]string{arn}), - } - - var result *elbv2.Listener - - err := conn.DescribeListenersPagesWithContext(ctx, input, func(page *elbv2.DescribeListenersOutput, lastPage bool) bool { - if page == nil { - return !lastPage - } - - for _, l := range page.Listeners { - if l == nil { - continue - } - - if aws.StringValue(l.ListenerArn) == arn { - result = l - return false - } - } - - return !lastPage - }) - - return result, err -} diff --git a/internal/service/elbv2/listener.go b/internal/service/elbv2/listener.go index 5764f3c4b62..a9e297a703e 100644 --- a/internal/service/elbv2/listener.go +++ b/internal/service/elbv2/listener.go @@ -6,7 +6,6 @@ package elbv2 import ( "context" "errors" - "fmt" "log" "sort" "strconv" @@ -26,6 +25,7 @@ import ( "github.com/hashicorp/terraform-provider-aws/internal/errs" "github.com/hashicorp/terraform-provider-aws/internal/errs/sdkdiag" "github.com/hashicorp/terraform-provider-aws/internal/flex" + tfslices "github.com/hashicorp/terraform-provider-aws/internal/slices" tftags "github.com/hashicorp/terraform-provider-aws/internal/tags" "github.com/hashicorp/terraform-provider-aws/internal/tfresource" "github.com/hashicorp/terraform-provider-aws/internal/verify" @@ -47,7 +47,8 @@ func ResourceListener() *schema.Resource { }, Timeouts: &schema.ResourceTimeout{ - Read: schema.DefaultTimeout(10 * time.Minute), + Create: schema.DefaultTimeout(5 * time.Minute), + Update: schema.DefaultTimeout(5 * time.Minute), }, CustomizeDiff: customdiff.Sequence( @@ -355,6 +356,32 @@ func ResourceListener() *schema.Resource { ForceNew: true, ValidateFunc: verify.ValidARN, }, + "mutual_authentication": { + Type: schema.TypeList, + Optional: true, + Computed: true, + MaxItems: 1, + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "ignore_client_certificate_expiry": { + Type: schema.TypeBool, + Optional: true, + Default: false, + }, + "mode": { + Type: schema.TypeString, + Required: true, + ValidateFunc: validation.StringInSlice(mutualAuthenticationModeEnum_Values(), true), + }, + "trust_store_arn": { + Type: schema.TypeString, + Optional: true, + ValidateFunc: verify.ValidARN, + }, + }, + }, + }, + "port": { Type: schema.TypeInt, Optional: true, @@ -405,16 +432,14 @@ func resourceListenerCreate(ctx context.Context, d *schema.ResourceData, meta in Tags: getTagsIn(ctx), } - if alpnPolicy, ok := d.GetOk("alpn_policy"); ok { - input.AlpnPolicy = make([]*string, 1) - input.AlpnPolicy[0] = aws.String(alpnPolicy.(string)) + if v, ok := d.GetOk("alpn_policy"); ok { + input.AlpnPolicy = aws.StringSlice([]string{v.(string)}) } - if certificateArn, ok := d.GetOk("certificate_arn"); ok { - input.Certificates = make([]*elbv2.Certificate, 1) - input.Certificates[0] = &elbv2.Certificate{ - CertificateArn: aws.String(certificateArn.(string)), - } + if v, ok := d.GetOk("certificate_arn"); ok { + input.Certificates = []*elbv2.Certificate{{ + CertificateArn: aws.String(v.(string)), + }} } if v, ok := d.GetOk("default_action"); ok && len(v.([]interface{})) > 0 { @@ -425,6 +450,10 @@ func resourceListenerCreate(ctx context.Context, d *schema.ResourceData, meta in } } + if v, ok := d.GetOk("mutual_authentication"); ok { + input.MutualAuthentication = expandMutualAuthenticationAttributes(v.([]interface{})) + } + if v, ok := d.GetOk("port"); ok { input.Port = aws.Int64(int64(v.(int))) } @@ -432,21 +461,21 @@ func resourceListenerCreate(ctx context.Context, d *schema.ResourceData, meta in if v, ok := d.GetOk("protocol"); ok { input.Protocol = aws.String(v.(string)) } else if strings.Contains(lbARN, "loadbalancer/app/") { - // Keep previous default of HTTP for Application Load Balancers + // Keep previous default of HTTP for Application Load Balancers. input.Protocol = aws.String(elbv2.ProtocolEnumHttp) } - if sslPolicy, ok := d.GetOk("ssl_policy"); ok { - input.SslPolicy = aws.String(sslPolicy.(string)) + if v, ok := d.GetOk("ssl_policy"); ok { + input.SslPolicy = aws.String(v.(string)) } - output, err := retryListenerCreate(ctx, conn, input) + output, err := retryListenerCreate(ctx, conn, input, d.Timeout(schema.TimeoutCreate)) // Some partitions (e.g. ISO) may not support tag-on-create. if input.Tags != nil && errs.IsUnsupportedOperationInPartitionError(conn.PartitionID, err) { input.Tags = nil - output, err = retryListenerCreate(ctx, conn, input) + output, err = retryListenerCreate(ctx, conn, input, d.Timeout(schema.TimeoutCreate)) } // Tags are not supported on creation with some load balancer types (i.e. Gateway) @@ -454,7 +483,7 @@ func resourceListenerCreate(ctx context.Context, d *schema.ResourceData, meta in if input.Tags != nil && tfawserr.ErrMessageContains(err, ErrValidationError, TagsOnCreationErrMessage) { input.Tags = nil - output, err = retryListenerCreate(ctx, conn, input) + output, err = retryListenerCreate(ctx, conn, input, d.Timeout(schema.TimeoutCreate)) } if err != nil { @@ -463,6 +492,14 @@ func resourceListenerCreate(ctx context.Context, d *schema.ResourceData, meta in d.SetId(aws.StringValue(output.Listeners[0].ListenerArn)) + _, err = tfresource.RetryWhenNotFound(ctx, d.Timeout(schema.TimeoutCreate), func() (interface{}, error) { + return FindListenerByARN(ctx, conn, d.Id()) + }) + + if err != nil { + return sdkdiag.AppendErrorf(diags, "waiting for ELBv2 Listener (%s) create: %s", d.Id(), err) + } + // For partitions not supporting tag-on-create, attempt tag after create. if tags := getTagsIn(ctx); input.Tags == nil && len(tags) > 0 { err := createTags(ctx, conn, d.Id(), tags) @@ -482,81 +519,46 @@ func resourceListenerCreate(ctx context.Context, d *schema.ResourceData, meta in func resourceListenerRead(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics { var diags diag.Diagnostics - const ( - loadBalancerListenerReadTimeout = 2 * time.Minute - ) conn := meta.(*conns.AWSClient).ELBV2Conn(ctx) - var listener *elbv2.Listener - - err := retry.RetryContext(ctx, loadBalancerListenerReadTimeout, func() *retry.RetryError { - var err error - listener, err = FindListenerByARN(ctx, conn, d.Id()) - - if d.IsNewResource() && tfawserr.ErrCodeEquals(err, elbv2.ErrCodeListenerNotFoundException) { - return retry.RetryableError(err) - } - - if err != nil { - return retry.NonRetryableError(err) - } + listener, err := FindListenerByARN(ctx, conn, d.Id()) - return nil - }) - - if tfresource.TimedOut(err) { - listener, err = FindListenerByARN(ctx, conn, d.Id()) - } - - if !d.IsNewResource() && tfawserr.ErrCodeEquals(err, elbv2.ErrCodeListenerNotFoundException) { + if !d.IsNewResource() && tfresource.NotFound(err) { log.Printf("[WARN] ELBv2 Listener (%s) not found, removing from state", d.Id()) d.SetId("") return diags } if err != nil { - return sdkdiag.AppendErrorf(diags, "describing ELBv2 Listener (%s): %s", d.Id(), err) + return sdkdiag.AppendErrorf(diags, "reading ELBv2 Listener (%s): %s", d.Id(), err) } - if listener == nil { - if d.IsNewResource() { - return sdkdiag.AppendErrorf(diags, "describing ELBv2 Listener (%s): empty response", d.Id()) - } - log.Printf("[WARN] ELBv2 Listener (%s) not found, removing from state", d.Id()) - d.SetId("") - return diags + if listener.AlpnPolicy != nil && len(listener.AlpnPolicy) == 1 && listener.AlpnPolicy[0] != nil { + d.Set("alpn_policy", listener.AlpnPolicy[0]) } - d.Set("arn", listener.ListenerArn) - d.Set("load_balancer_arn", listener.LoadBalancerArn) - d.Set("port", listener.Port) - d.Set("protocol", listener.Protocol) - d.Set("ssl_policy", listener.SslPolicy) - if listener.Certificates != nil && len(listener.Certificates) == 1 && listener.Certificates[0] != nil { d.Set("certificate_arn", listener.Certificates[0].CertificateArn) } - - if listener.AlpnPolicy != nil && len(listener.AlpnPolicy) == 1 && listener.AlpnPolicy[0] != nil { - d.Set("alpn_policy", listener.AlpnPolicy[0]) - } - sort.Slice(listener.DefaultActions, func(i, j int) bool { return aws.Int64Value(listener.DefaultActions[i].Order) < aws.Int64Value(listener.DefaultActions[j].Order) }) - if err := d.Set("default_action", flattenLbListenerActions(d, listener.DefaultActions)); err != nil { - return sdkdiag.AppendErrorf(diags, "setting default_action for ELBv2 listener (%s): %s", d.Id(), err) + return sdkdiag.AppendErrorf(diags, "setting default_action: %s", err) } + d.Set("load_balancer_arn", listener.LoadBalancerArn) + if err := d.Set("mutual_authentication", flattenMutualAuthenticationAttributes(listener.MutualAuthentication)); err != nil { + return sdkdiag.AppendErrorf(diags, "setting mutual_authentication: %s", err) + } + d.Set("port", listener.Port) + d.Set("protocol", listener.Protocol) + d.Set("ssl_policy", listener.SslPolicy) return diags } func resourceListenerUpdate(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics { var diags diag.Diagnostics - const ( - loadBalancerListenerUpdateTimeout = 5 * time.Minute - ) conn := meta.(*conns.AWSClient).ELBV2Conn(ctx) if d.HasChangesExcept("tags", "tags_all") { @@ -564,27 +566,14 @@ func resourceListenerUpdate(ctx context.Context, d *schema.ResourceData, meta in ListenerArn: aws.String(d.Id()), } - if v, ok := d.GetOk("port"); ok { - input.Port = aws.Int64(int64(v.(int))) - } - - if v, ok := d.GetOk("protocol"); ok { - input.Protocol = aws.String(v.(string)) - } - - if v, ok := d.GetOk("ssl_policy"); ok { - input.SslPolicy = aws.String(v.(string)) + if v, ok := d.GetOk("alpn_policy"); ok { + input.AlpnPolicy = aws.StringSlice([]string{v.(string)}) } if v, ok := d.GetOk("certificate_arn"); ok { - input.Certificates = make([]*elbv2.Certificate, 1) - input.Certificates[0] = &elbv2.Certificate{ + input.Certificates = []*elbv2.Certificate{{ CertificateArn: aws.String(v.(string)), - } - } - - if v, ok := d.GetOk("alpn_policy"); ok { - input.AlpnPolicy = aws.StringSlice([]string{v.(string)}) + }} } if d.HasChange("default_action") { @@ -595,24 +584,26 @@ func resourceListenerUpdate(ctx context.Context, d *schema.ResourceData, meta in } } - err := retry.RetryContext(ctx, loadBalancerListenerUpdateTimeout, func() *retry.RetryError { - _, err := conn.ModifyListenerWithContext(ctx, input) - - if tfawserr.ErrCodeEquals(err, elbv2.ErrCodeCertificateNotFoundException) { - return retry.RetryableError(err) - } + if d.HasChange("mutual_authentication") { + input.MutualAuthentication = expandMutualAuthenticationAttributes(d.Get("mutual_authentication").([]interface{})) + } - if err != nil { - return retry.NonRetryableError(err) - } + if v, ok := d.GetOk("port"); ok { + input.Port = aws.Int64(int64(v.(int))) + } - return nil - }) + if v, ok := d.GetOk("protocol"); ok { + input.Protocol = aws.String(v.(string)) + } - if tfresource.TimedOut(err) { - _, err = conn.ModifyListenerWithContext(ctx, input) + if v, ok := d.GetOk("ssl_policy"); ok { + input.SslPolicy = aws.String(v.(string)) } + _, err := tfresource.RetryWhenAWSErrCodeEquals(ctx, d.Timeout(schema.TimeoutUpdate), func() (interface{}, error) { + return conn.ModifyListenerWithContext(ctx, input) + }, elbv2.ErrCodeCertificateNotFoundException) + if err != nil { return sdkdiag.AppendErrorf(diags, "modifying ELBv2 Listener (%s): %s", d.Id(), err) } @@ -625,50 +616,88 @@ func resourceListenerDelete(ctx context.Context, d *schema.ResourceData, meta in var diags diag.Diagnostics conn := meta.(*conns.AWSClient).ELBV2Conn(ctx) + log.Printf("[INFO] Deleting ELBv2 Listener: %s", d.Id()) _, err := conn.DeleteListenerWithContext(ctx, &elbv2.DeleteListenerInput{ ListenerArn: aws.String(d.Id()), }) + if err != nil { - return sdkdiag.AppendErrorf(diags, "deleting Listener (%s): %s", d.Id(), err) + return sdkdiag.AppendErrorf(diags, "deleting ELBv2 Listener (%s): %s", d.Id(), err) } return diags } -func retryListenerCreate(ctx context.Context, conn *elbv2.ELBV2, params *elbv2.CreateListenerInput) (*elbv2.CreateListenerOutput, error) { - const ( - loadBalancerListenerCreateTimeout = 5 * time.Minute - ) - var output *elbv2.CreateListenerOutput +func retryListenerCreate(ctx context.Context, conn *elbv2.ELBV2, input *elbv2.CreateListenerInput, timeout time.Duration) (*elbv2.CreateListenerOutput, error) { + outputRaw, err := tfresource.RetryWhenAWSErrCodeEquals(ctx, timeout, func() (interface{}, error) { + return conn.CreateListenerWithContext(ctx, input) + }, elbv2.ErrCodeCertificateNotFoundException) - err := retry.RetryContext(ctx, loadBalancerListenerCreateTimeout, func() *retry.RetryError { - var err error + if err != nil { + return nil, err + } + + return outputRaw.(*elbv2.CreateListenerOutput), nil +} + +func FindListenerByARN(ctx context.Context, conn *elbv2.ELBV2, arn string) (*elbv2.Listener, error) { + input := &elbv2.DescribeListenersInput{ + ListenerArns: aws.StringSlice([]string{arn}), + } + output, err := findListener(ctx, conn, input, tfslices.PredicateTrue[*elbv2.Listener]()) - output, err = conn.CreateListenerWithContext(ctx, params) + if err != nil { + return nil, err + } - if tfawserr.ErrCodeEquals(err, elbv2.ErrCodeCertificateNotFoundException) { - return retry.RetryableError(err) + // Eventual consistency check. + if aws.StringValue(output.ListenerArn) != arn { + return nil, &retry.NotFoundError{ + LastRequest: input, } + } - if err != nil { - return retry.NonRetryableError(err) + return output, nil +} + +func findListener(ctx context.Context, conn *elbv2.ELBV2, input *elbv2.DescribeListenersInput, filter tfslices.Predicate[*elbv2.Listener]) (*elbv2.Listener, error) { + output, err := findListeners(ctx, conn, input, filter) + + if err != nil { + return nil, err + } + + return tfresource.AssertSinglePtrResult(output) +} + +func findListeners(ctx context.Context, conn *elbv2.ELBV2, input *elbv2.DescribeListenersInput, filter tfslices.Predicate[*elbv2.Listener]) ([]*elbv2.Listener, error) { + var output []*elbv2.Listener + + err := conn.DescribeListenersPagesWithContext(ctx, input, func(page *elbv2.DescribeListenersOutput, lastPage bool) bool { + if page == nil { + return !lastPage } - return nil + for _, v := range page.Listeners { + if v != nil && filter(v) { + output = append(output, v) + } + } + + return !lastPage }) - if tfresource.TimedOut(err) { - output, err = conn.CreateListenerWithContext(ctx, params) + if tfawserr.ErrCodeEquals(err, elbv2.ErrCodeListenerNotFoundException) { + return nil, &retry.NotFoundError{ + LastError: err, + LastRequest: input, + } } if err != nil { return nil, err } - if output == nil || len(output.Listeners) == 0 { - return nil, fmt.Errorf("creating ELBv2 Listener: no listeners returned in response") - } - return output, nil } @@ -879,6 +908,30 @@ func expandLbListenerActionForwardConfig(l []interface{}) *elbv2.ForwardActionCo return config } +func expandMutualAuthenticationAttributes(l []interface{}) *elbv2.MutualAuthenticationAttributes { + if len(l) == 0 || l[0] == nil { + return nil + } + + tfMap, ok := l[0].(map[string]interface{}) + if !ok { + return nil + } + + mode := tfMap["mode"].(string) + if mode == mutualAuthenticationOff { + return &elbv2.MutualAuthenticationAttributes{ + Mode: aws.String(mode), + } + } + + return &elbv2.MutualAuthenticationAttributes{ + Mode: aws.String(mode), + TrustStoreArn: aws.String(tfMap["trust_store_arn"].(string)), + IgnoreClientCertificateExpiry: aws.Bool(tfMap["ignore_client_certificate_expiry"].(bool)), + } +} + func expandLbListenerActionForwardConfigTargetGroups(l []interface{}) []*elbv2.TargetGroupTuple { if len(l) == 0 { return nil @@ -966,6 +1019,29 @@ func flattenLbListenerActions(d *schema.ResourceData, Actions []*elbv2.Action) [ return vActions } +func flattenMutualAuthenticationAttributes(description *elbv2.MutualAuthenticationAttributes) []interface{} { + if description == nil { + return []interface{}{} + } + + mode := aws.StringValue(description.Mode) + if mode == mutualAuthenticationOff { + return []interface{}{ + map[string]interface{}{ + "mode": mode, + }, + } + } + + m := map[string]interface{}{ + "mode": aws.StringValue(description.Mode), + "trust_store_arn": aws.StringValue(description.TrustStoreArn), + "ignore_client_certificate_expiry": aws.BoolValue(description.IgnoreClientCertificateExpiry), + } + + return []interface{}{m} +} + func flattenAuthenticateOIDCActionConfig(config *elbv2.AuthenticateOidcActionConfig, clientSecret string) []interface{} { if config == nil { return []interface{}{} @@ -1086,3 +1162,17 @@ func flattenLbListenerActionRedirectConfig(config *elbv2.RedirectActionConfig) [ return []interface{}{m} } + +const ( + mutualAuthenticationOff = "off" + mutualAuthenticationVerify = "verify" + mutualAuthenticationPassthrough = "passthrough" +) + +func mutualAuthenticationModeEnum_Values() []string { + return []string{ + mutualAuthenticationOff, + mutualAuthenticationVerify, + mutualAuthenticationPassthrough, + } +} diff --git a/internal/service/elbv2/listener_data_source.go b/internal/service/elbv2/listener_data_source.go index 2aaabd46ace..3b5eb18f3ab 100644 --- a/internal/service/elbv2/listener_data_source.go +++ b/internal/service/elbv2/listener_data_source.go @@ -16,7 +16,9 @@ import ( "github.com/hashicorp/terraform-provider-aws/internal/conns" "github.com/hashicorp/terraform-provider-aws/internal/errs" "github.com/hashicorp/terraform-provider-aws/internal/errs/sdkdiag" + tfslices "github.com/hashicorp/terraform-provider-aws/internal/slices" tftags "github.com/hashicorp/terraform-provider-aws/internal/tags" + "github.com/hashicorp/terraform-provider-aws/internal/tfresource" ) // @SDKDataSource("aws_alb_listener") @@ -256,12 +258,34 @@ func DataSourceListener() *schema.Resource { Optional: true, Computed: true, ConflictsWith: []string{"arn"}, + RequiredWith: []string{"port"}, + }, + "mutual_authentication": { + Type: schema.TypeList, + Computed: true, + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "mode": { + Type: schema.TypeString, + Computed: true, + }, + "trust_store_arn": { + Type: schema.TypeString, + Computed: true, + }, + "ignore_client_certificate_expiry": { + Type: schema.TypeBool, + Computed: true, + }, + }, + }, }, "port": { Type: schema.TypeInt, Optional: true, Computed: true, ConflictsWith: []string{"arn"}, + RequiredWith: []string{"load_balancer_arn"}, }, "protocol": { Type: schema.TypeString, @@ -285,71 +309,44 @@ func dataSourceListenerRead(ctx context.Context, d *schema.ResourceData, meta in if v, ok := d.GetOk("arn"); ok { input.ListenerArns = aws.StringSlice([]string{v.(string)}) - } else { - lbArn, lbOk := d.GetOk("load_balancer_arn") - _, portOk := d.GetOk("port") - - if !lbOk || !portOk { - return sdkdiag.AppendErrorf(diags, "both load_balancer_arn and port must be set") - } - - input.LoadBalancerArn = aws.String(lbArn.(string)) + } else if v, ok := d.GetOk("load_balancer_arn"); ok { + input.LoadBalancerArn = aws.String(v.(string)) } - var results []*elbv2.Listener - - err := conn.DescribeListenersPagesWithContext(ctx, input, func(page *elbv2.DescribeListenersOutput, lastPage bool) bool { - if page == nil { - return !lastPage - } - - for _, l := range page.Listeners { - if l == nil { - continue - } - - if v, ok := d.GetOk("port"); ok && v.(int) != int(aws.Int64Value(l.Port)) { - continue - } - - results = append(results, l) + filter := tfslices.PredicateTrue[*elbv2.Listener]() + if v, ok := d.GetOk("port"); ok { + port := v.(int) + filter = func(v *elbv2.Listener) bool { + return int(aws.Int64Value(v.Port)) == port } - - return !lastPage - }) - - if err != nil { - return sdkdiag.AppendErrorf(diags, "reading Listener: %s", err) } + listener, err := findListener(ctx, conn, input, filter) - if len(results) != 1 { - return sdkdiag.AppendErrorf(diags, "Search returned %d results, please revise so only one is returned", len(results)) + if err != nil { + return sdkdiag.AppendFromErr(diags, tfresource.SingularDataSourceFindError("ELBv2 Listener", err)) } - listener := results[0] - d.SetId(aws.StringValue(listener.ListenerArn)) + if listener.AlpnPolicy != nil && len(listener.AlpnPolicy) == 1 && listener.AlpnPolicy[0] != nil { + d.Set("alpn_policy", listener.AlpnPolicy[0]) + } d.Set("arn", listener.ListenerArn) - d.Set("load_balancer_arn", listener.LoadBalancerArn) - d.Set("port", listener.Port) - d.Set("protocol", listener.Protocol) - d.Set("ssl_policy", listener.SslPolicy) - if listener.Certificates != nil && len(listener.Certificates) == 1 && listener.Certificates[0] != nil { d.Set("certificate_arn", listener.Certificates[0].CertificateArn) } - - if listener.AlpnPolicy != nil && len(listener.AlpnPolicy) == 1 && listener.AlpnPolicy[0] != nil { - d.Set("alpn_policy", listener.AlpnPolicy[0]) - } - sort.Slice(listener.DefaultActions, func(i, j int) bool { return aws.Int64Value(listener.DefaultActions[i].Order) < aws.Int64Value(listener.DefaultActions[j].Order) }) - if err := d.Set("default_action", flattenLbListenerActions(d, listener.DefaultActions)); err != nil { return sdkdiag.AppendErrorf(diags, "setting default_action: %s", err) } + d.Set("load_balancer_arn", listener.LoadBalancerArn) + if err := d.Set("mutual_authentication", flattenMutualAuthenticationAttributes(listener.MutualAuthentication)); err != nil { + return sdkdiag.AppendErrorf(diags, "setting mutual_authentication: %s", err) + } + d.Set("port", listener.Port) + d.Set("protocol", listener.Protocol) + d.Set("ssl_policy", listener.SslPolicy) tags, err := listTags(ctx, conn, d.Id()) diff --git a/internal/service/elbv2/listener_data_source_test.go b/internal/service/elbv2/listener_data_source_test.go index d1b92809330..2e1198de534 100644 --- a/internal/service/elbv2/listener_data_source_test.go +++ b/internal/service/elbv2/listener_data_source_test.go @@ -16,43 +16,8 @@ import ( func TestAccELBV2ListenerDataSource_basic(t *testing.T) { ctx := acctest.Context(t) rName := sdkacctest.RandomWithPrefix(acctest.ResourcePrefix) + resourceName := "aws_lb_listener.test" dataSourceName := "data.aws_lb_listener.test" - dataSourceName2 := "data.aws_lb_listener.from_lb_and_port" - - resource.ParallelTest(t, resource.TestCase{ - PreCheck: func() { acctest.PreCheck(ctx, t) }, - ErrorCheck: acctest.ErrorCheck(t, elbv2.EndpointsID), - ProtoV5ProviderFactories: acctest.ProtoV5ProviderFactories, - Steps: []resource.TestStep{ - { - Config: testAccListenerDataSourceConfig_basic(rName), - Check: resource.ComposeAggregateTestCheckFunc( - resource.TestCheckResourceAttrSet(dataSourceName, "load_balancer_arn"), - resource.TestCheckResourceAttrSet(dataSourceName, "arn"), - resource.TestCheckResourceAttrSet(dataSourceName, "default_action.0.target_group_arn"), - resource.TestCheckResourceAttr(dataSourceName, "protocol", "HTTP"), - resource.TestCheckResourceAttr(dataSourceName, "port", "80"), - resource.TestCheckResourceAttr(dataSourceName, "default_action.#", "1"), - resource.TestCheckResourceAttr(dataSourceName, "default_action.0.type", "forward"), - resource.TestCheckResourceAttr(dataSourceName, "tags.%", "0"), - resource.TestCheckResourceAttrSet(dataSourceName2, "load_balancer_arn"), - resource.TestCheckResourceAttrSet(dataSourceName2, "arn"), - resource.TestCheckResourceAttrSet(dataSourceName2, "default_action.0.target_group_arn"), - resource.TestCheckResourceAttr(dataSourceName2, "protocol", "HTTP"), - resource.TestCheckResourceAttr(dataSourceName2, "port", "80"), - resource.TestCheckResourceAttr(dataSourceName2, "default_action.#", "1"), - resource.TestCheckResourceAttr(dataSourceName2, "default_action.0.type", "forward"), - resource.TestCheckResourceAttr(dataSourceName2, "tags.%", "0"), - ), - }, - }, - }) -} - -func TestAccELBV2ListenerDataSource_backwardsCompatibility(t *testing.T) { - ctx := acctest.Context(t) - rName := sdkacctest.RandomWithPrefix(acctest.ResourcePrefix) - dataSourceName := "data.aws_alb_listener.test" dataSourceName2 := "data.aws_alb_listener.from_lb_and_port" resource.ParallelTest(t, resource.TestCase{ @@ -61,84 +26,28 @@ func TestAccELBV2ListenerDataSource_backwardsCompatibility(t *testing.T) { ProtoV5ProviderFactories: acctest.ProtoV5ProviderFactories, Steps: []resource.TestStep{ { - Config: testAccListenerDataSourceConfig_backwardsCompatibility(rName), - Check: resource.ComposeAggregateTestCheckFunc( - resource.TestCheckResourceAttrSet(dataSourceName, "load_balancer_arn"), - resource.TestCheckResourceAttrSet(dataSourceName, "arn"), - resource.TestCheckResourceAttrSet(dataSourceName, "default_action.0.target_group_arn"), - resource.TestCheckResourceAttr(dataSourceName, "protocol", "HTTP"), - resource.TestCheckResourceAttr(dataSourceName, "port", "80"), - resource.TestCheckResourceAttr(dataSourceName, "default_action.#", "1"), - resource.TestCheckResourceAttr(dataSourceName, "default_action.0.type", "forward"), - resource.TestCheckResourceAttrSet(dataSourceName2, "load_balancer_arn"), - resource.TestCheckResourceAttrSet(dataSourceName2, "arn"), - resource.TestCheckResourceAttrSet(dataSourceName2, "default_action.0.target_group_arn"), - resource.TestCheckResourceAttr(dataSourceName2, "protocol", "HTTP"), - resource.TestCheckResourceAttr(dataSourceName2, "port", "80"), - resource.TestCheckResourceAttr(dataSourceName2, "default_action.#", "1"), - resource.TestCheckResourceAttr(dataSourceName2, "default_action.0.type", "forward"), - ), - }, - }, - }) -} - -func TestAccELBV2ListenerDataSource_https(t *testing.T) { - ctx := acctest.Context(t) - rName := sdkacctest.RandomWithPrefix(acctest.ResourcePrefix) - key := acctest.TLSRSAPrivateKeyPEM(t, 2048) - certificate := acctest.TLSRSAX509SelfSignedCertificatePEM(t, key, "example.com") - dataSourceName := "data.aws_lb_listener.test" - dataSourceName2 := "data.aws_lb_listener.from_lb_and_port" - - resource.ParallelTest(t, resource.TestCase{ - PreCheck: func() { acctest.PreCheck(ctx, t) }, - ErrorCheck: acctest.ErrorCheck(t, elbv2.EndpointsID), - ProtoV5ProviderFactories: acctest.ProtoV5ProviderFactories, - Steps: []resource.TestStep{ - { - Config: testAccListenerDataSourceConfig_https(rName, acctest.TLSPEMEscapeNewlines(certificate), acctest.TLSPEMEscapeNewlines(key)), - Check: resource.ComposeAggregateTestCheckFunc( - resource.TestCheckResourceAttrSet(dataSourceName, "load_balancer_arn"), - resource.TestCheckResourceAttrSet(dataSourceName, "arn"), - resource.TestCheckResourceAttrSet(dataSourceName, "default_action.0.target_group_arn"), - resource.TestCheckResourceAttrSet(dataSourceName, "certificate_arn"), - resource.TestCheckResourceAttr(dataSourceName, "protocol", "HTTPS"), - resource.TestCheckResourceAttr(dataSourceName, "port", "443"), - resource.TestCheckResourceAttr(dataSourceName, "default_action.#", "1"), - resource.TestCheckResourceAttr(dataSourceName, "default_action.0.type", "forward"), - resource.TestCheckResourceAttr(dataSourceName, "ssl_policy", "ELBSecurityPolicy-2016-08"), - resource.TestCheckResourceAttrSet(dataSourceName2, "load_balancer_arn"), - resource.TestCheckResourceAttrSet(dataSourceName2, "arn"), - resource.TestCheckResourceAttrSet(dataSourceName2, "default_action.0.target_group_arn"), - resource.TestCheckResourceAttrSet(dataSourceName2, "certificate_arn"), - resource.TestCheckResourceAttr(dataSourceName2, "protocol", "HTTPS"), - resource.TestCheckResourceAttr(dataSourceName2, "port", "443"), - resource.TestCheckResourceAttr(dataSourceName2, "default_action.#", "1"), - resource.TestCheckResourceAttr(dataSourceName2, "default_action.0.type", "forward"), - resource.TestCheckResourceAttr(dataSourceName2, "ssl_policy", "ELBSecurityPolicy-2016-08"), - ), - }, - }, - }) -} - -func TestAccELBV2ListenerDataSource_DefaultAction_forward(t *testing.T) { - ctx := acctest.Context(t) - rName := sdkacctest.RandomWithPrefix(acctest.ResourcePrefix) - dataSourceName := "data.aws_lb_listener.test" - resourceName := "aws_lb_listener.test" - - resource.ParallelTest(t, resource.TestCase{ - PreCheck: func() { acctest.PreCheck(ctx, t) }, - ErrorCheck: acctest.ErrorCheck(t, elbv2.EndpointsID), - ProtoV5ProviderFactories: acctest.ProtoV5ProviderFactories, - Steps: []resource.TestStep{ - { - Config: testAccListenerDataSourceConfig_defaultActionForward(rName), + Config: testAccListenerDataSourceConfig_basic(rName), Check: resource.ComposeAggregateTestCheckFunc( + resource.TestCheckResourceAttrPair(dataSourceName, "alpn_policy", resourceName, "alpn_policy"), + resource.TestCheckResourceAttrPair(dataSourceName, "arn", resourceName, "arn"), + resource.TestCheckResourceAttrPair(dataSourceName, "certificate_arn", resourceName, "certificate_arn"), resource.TestCheckResourceAttrPair(dataSourceName, "default_action.#", resourceName, "default_action.#"), - resource.TestCheckResourceAttrPair(dataSourceName, "default_action.0.forward.#", resourceName, "default_action.0.forward.#"), + resource.TestCheckResourceAttrPair(dataSourceName, "load_balancer_arn", resourceName, "load_balancer_arn"), + resource.TestCheckResourceAttrPair(dataSourceName, "mutual_authentication.#", resourceName, "mutual_authentication.#"), + resource.TestCheckResourceAttrPair(dataSourceName, "port", resourceName, "port"), + resource.TestCheckResourceAttrPair(dataSourceName, "protocol", resourceName, "protocol"), + resource.TestCheckResourceAttrPair(dataSourceName, "ssl_policy", resourceName, "ssl_policy"), + resource.TestCheckResourceAttrPair(dataSourceName, "tags.%", resourceName, "tags.%"), + resource.TestCheckResourceAttrPair(dataSourceName2, "alpn_policy", dataSourceName, "alpn_policy"), + resource.TestCheckResourceAttrPair(dataSourceName2, "arn", dataSourceName, "arn"), + resource.TestCheckResourceAttrPair(dataSourceName2, "certificate_arn", dataSourceName, "certificate_arn"), + resource.TestCheckResourceAttrPair(dataSourceName2, "default_action.#", dataSourceName, "default_action.#"), + resource.TestCheckResourceAttrPair(dataSourceName2, "load_balancer_arn", dataSourceName, "load_balancer_arn"), + resource.TestCheckResourceAttrPair(dataSourceName2, "mutual_authentication.#", dataSourceName, "mutual_authentication.#"), + resource.TestCheckResourceAttrPair(dataSourceName2, "port", dataSourceName, "port"), + resource.TestCheckResourceAttrPair(dataSourceName2, "protocol", dataSourceName, "protocol"), + resource.TestCheckResourceAttrPair(dataSourceName2, "ssl_policy", dataSourceName, "ssl_policy"), + resource.TestCheckResourceAttrPair(dataSourceName2, "tags.%", dataSourceName, "tags.%"), ), }, }, @@ -156,136 +65,20 @@ resource "aws_lb_listener" "test" { target_group_arn = aws_lb_target_group.test.id type = "forward" } -} - -resource "aws_lb" "test" { - name = %[1]q - internal = true - security_groups = [aws_security_group.test.id] - subnets = aws_subnet.test[*].id - - idle_timeout = 30 - enable_deletion_protection = false tags = { - TestName = "TestAccELBV2ListenerDataSource_basic" - } -} - -resource "aws_lb_target_group" "test" { - name = %[1]q - port = 8080 - protocol = "HTTP" - vpc_id = aws_vpc.test.id - - health_check { - path = "/health" - interval = 60 - port = 8081 - protocol = "HTTP" - timeout = 3 - healthy_threshold = 3 - unhealthy_threshold = 3 - matcher = "200-299" - } -} - -data "aws_lb_listener" "test" { - arn = aws_lb_listener.test.arn -} - -data "aws_lb_listener" "from_lb_and_port" { - load_balancer_arn = aws_lb.test.arn - port = aws_lb_listener.test.port -} -`, rName)) -} - -func testAccListenerDataSourceConfig_backwardsCompatibility(rName string) string { - return acctest.ConfigCompose(testAccListenerConfig_base(rName), fmt.Sprintf(` -resource "aws_alb_listener" "test" { - load_balancer_arn = aws_alb.test.id - protocol = "HTTP" - port = "80" - - default_action { - target_group_arn = aws_alb_target_group.test.id - type = "forward" - } -} - -resource "aws_alb" "test" { - name = %[1]q - internal = true - security_groups = [aws_security_group.test.id] - subnets = aws_subnet.test[*].id - - idle_timeout = 30 - enable_deletion_protection = false - - tags = { - TestName = "TestAccELBV2ListenerDataSource_basic" - } -} - -resource "aws_alb_target_group" "test" { - name = %[1]q - port = 8080 - protocol = "HTTP" - vpc_id = aws_vpc.test.id - - health_check { - path = "/health" - interval = 60 - port = 8081 - protocol = "HTTP" - timeout = 3 - healthy_threshold = 3 - unhealthy_threshold = 3 - matcher = "200-299" - } -} - -data "aws_alb_listener" "test" { - arn = aws_alb_listener.test.arn -} - -data "aws_alb_listener" "from_lb_and_port" { - load_balancer_arn = aws_alb.test.arn - port = aws_alb_listener.test.port -} -`, rName)) -} - -func testAccListenerDataSourceConfig_https(rName, certificate, key string) string { - return acctest.ConfigCompose(testAccListenerConfig_base(rName), fmt.Sprintf(` -resource "aws_lb_listener" "test" { - load_balancer_arn = aws_lb.test.id - protocol = "HTTPS" - port = "443" - ssl_policy = "ELBSecurityPolicy-2016-08" - certificate_arn = aws_iam_server_certificate.test.arn - - default_action { - target_group_arn = aws_lb_target_group.test.id - type = "forward" + Name = %[1]q } } resource "aws_lb" "test" { name = %[1]q - internal = false + internal = true security_groups = [aws_security_group.test.id] subnets = aws_subnet.test[*].id idle_timeout = 30 enable_deletion_protection = false - - tags = { - TestName = "TestAccELBV2ListenerDataSource_basic" - } - - depends_on = [aws_internet_gateway.gw] } resource "aws_lb_target_group" "test" { @@ -306,101 +99,13 @@ resource "aws_lb_target_group" "test" { } } -resource "aws_internet_gateway" "gw" { - vpc_id = aws_vpc.test.id - - tags = { - Name = %[1]q - TestName = "TestAccELBV2ListenerDataSource_basic" - } -} - -resource "aws_iam_server_certificate" "test" { - name = %[1]q - certificate_body = "%[2]s" - private_key = "%[3]s" -} - data "aws_lb_listener" "test" { arn = aws_lb_listener.test.arn } -data "aws_lb_listener" "from_lb_and_port" { +data "aws_alb_listener" "from_lb_and_port" { load_balancer_arn = aws_lb.test.arn port = aws_lb_listener.test.port } -`, rName, certificate, key)) -} - -func testAccListenerDataSourceConfig_defaultActionForward(rName string) string { - return acctest.ConfigCompose( - acctest.ConfigAvailableAZsNoOptIn(), - fmt.Sprintf(` -resource "aws_vpc" "test" { - cidr_block = "10.0.0.0/16" - - tags = { - Name = %[1]q - } -} - -resource "aws_subnet" "test" { - count = 2 - - availability_zone = data.aws_availability_zones.available.names[count.index] - cidr_block = cidrsubnet(aws_vpc.test.cidr_block, 8, count.index) - vpc_id = aws_vpc.test.id - - tags = { - Name = %[1]q - } -} - -resource "aws_lb" "test" { - internal = true - name = %[1]q - - subnet_mapping { - subnet_id = aws_subnet.test[0].id - } - - subnet_mapping { - subnet_id = aws_subnet.test[1].id - } -} - -resource "aws_lb_target_group" "test" { - count = 2 - - port = 80 - protocol = "HTTP" - vpc_id = aws_vpc.test.id -} - -resource "aws_lb_listener" "test" { - load_balancer_arn = aws_lb.test.id - port = 80 - protocol = "HTTP" - - default_action { - type = "forward" - - forward { - target_group { - arn = aws_lb_target_group.test[0].arn - weight = 1 - } - - target_group { - arn = aws_lb_target_group.test[1].arn - weight = 2 - } - } - } -} - -data "aws_lb_listener" "test" { - arn = aws_lb_listener.test.arn -} `, rName)) } diff --git a/internal/service/elbv2/listener_test.go b/internal/service/elbv2/listener_test.go index dd476a634b7..cc1cd4a967c 100644 --- a/internal/service/elbv2/listener_test.go +++ b/internal/service/elbv2/listener_test.go @@ -5,20 +5,19 @@ package elbv2_test import ( "context" - "errors" "fmt" "testing" "github.com/YakDriver/regexache" "github.com/aws/aws-sdk-go/aws" "github.com/aws/aws-sdk-go/service/elbv2" - "github.com/hashicorp/aws-sdk-go-base/v2/awsv1shim/v2/tfawserr" sdkacctest "github.com/hashicorp/terraform-plugin-testing/helper/acctest" "github.com/hashicorp/terraform-plugin-testing/helper/resource" "github.com/hashicorp/terraform-plugin-testing/terraform" "github.com/hashicorp/terraform-provider-aws/internal/acctest" "github.com/hashicorp/terraform-provider-aws/internal/conns" tfelbv2 "github.com/hashicorp/terraform-provider-aws/internal/service/elbv2" + "github.com/hashicorp/terraform-provider-aws/internal/tfresource" "golang.org/x/exp/slices" ) @@ -48,7 +47,8 @@ func TestAccELBV2Listener_basic(t *testing.T) { resource.TestCheckResourceAttrPair(resourceName, "default_action.0.target_group_arn", "aws_lb_target_group.test", "arn"), resource.TestCheckResourceAttr(resourceName, "default_action.0.redirect.#", "0"), resource.TestCheckResourceAttr(resourceName, "default_action.0.fixed_response.#", "0"), - resource.TestCheckResourceAttr(resourceName, "tags.#", "0"), + resource.TestCheckResourceAttr(resourceName, "tags.%", "0"), + resource.TestCheckResourceAttr(resourceName, "mutual_authentication.#", "0"), ), }, { @@ -302,6 +302,53 @@ func TestAccELBV2Listener_Protocol_https(t *testing.T) { }) } +func TestAccELBV2Listener_mutualAuthentication(t *testing.T) { + ctx := acctest.Context(t) + var conf elbv2.Listener + key := acctest.TLSRSAPrivateKeyPEM(t, 2048) + resourceName := "aws_lb_listener.test" + certificate := acctest.TLSRSAX509SelfSignedCertificatePEM(t, key, "example.com") + rName := sdkacctest.RandomWithPrefix(acctest.ResourcePrefix) + + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { acctest.PreCheck(ctx, t) }, + ErrorCheck: acctest.ErrorCheck(t, elbv2.EndpointsID), + ProtoV5ProviderFactories: acctest.ProtoV5ProviderFactories, + CheckDestroy: testAccCheckListenerDestroy(ctx), + Steps: []resource.TestStep{ + { + Config: testAccListenerConfig_mutualAuthentication(rName, key, certificate), + Check: resource.ComposeAggregateTestCheckFunc( + testAccCheckListenerExists(ctx, resourceName, &conf), + resource.TestCheckResourceAttr(resourceName, "mutual_authentication.#", "1"), + resource.TestCheckResourceAttr(resourceName, "mutual_authentication.0.mode", "verify"), + resource.TestCheckResourceAttr(resourceName, "mutual_authentication.0.ignore_client_certificate_expiry", "false"), + resource.TestCheckResourceAttrPair(resourceName, "mutual_authentication.0.trust_store_arn", "aws_lb_trust_store.test", "arn"), + + resource.TestCheckResourceAttrPair(resourceName, "load_balancer_arn", "aws_lb.test", "arn"), + + acctest.MatchResourceAttrRegionalARN(resourceName, "arn", "elasticloadbalancing", regexache.MustCompile("listener/.+$")), + resource.TestCheckResourceAttr(resourceName, "protocol", "HTTPS"), + resource.TestCheckResourceAttr(resourceName, "port", "443"), + resource.TestCheckResourceAttr(resourceName, "default_action.#", "1"), + resource.TestCheckResourceAttr(resourceName, "default_action.0.order", "1"), + resource.TestCheckResourceAttr(resourceName, "default_action.0.type", "forward"), + resource.TestCheckResourceAttrPair(resourceName, "default_action.0.target_group_arn", "aws_lb_target_group.test", "arn"), + resource.TestCheckResourceAttr(resourceName, "default_action.0.redirect.#", "0"), + resource.TestCheckResourceAttr(resourceName, "default_action.0.fixed_response.#", "0"), + resource.TestCheckResourceAttrPair(resourceName, "certificate_arn", "aws_iam_server_certificate.test", "arn"), + resource.TestCheckResourceAttr(resourceName, "ssl_policy", "ELBSecurityPolicy-2016-08"), + ), + }, + { + ResourceName: resourceName, + ImportState: true, + ImportStateVerify: true, + }, + }, + }) +} + func TestAccELBV2Listener_LoadBalancerARN_gatewayLoadBalancer(t *testing.T) { ctx := acctest.Context(t) var conf elbv2.Listener @@ -636,30 +683,23 @@ func testAccCheckListenerDefaultActionOrderDisappears(ctx context.Context, liste } } -func testAccCheckListenerExists(ctx context.Context, n string, res *elbv2.Listener) resource.TestCheckFunc { +func testAccCheckListenerExists(ctx context.Context, n string, v *elbv2.Listener) resource.TestCheckFunc { return func(s *terraform.State) error { rs, ok := s.RootModule().Resources[n] if !ok { return fmt.Errorf("Not found: %s", n) } - if rs.Primary.ID == "" { - return errors.New("No Listener ID is set") - } - conn := acctest.Provider.Meta().(*conns.AWSClient).ELBV2Conn(ctx) - listener, err := tfelbv2.FindListenerByARN(ctx, conn, rs.Primary.ID) + output, err := tfelbv2.FindListenerByARN(ctx, conn, rs.Primary.ID) if err != nil { - return fmt.Errorf("reading ELBv2 Listener (%s): %w", rs.Primary.ID, err) + return err } - if listener == nil { - return fmt.Errorf("ELBv2 Listener (%s) not found", rs.Primary.ID) - } + *v = *output - *res = *listener return nil } } @@ -673,21 +713,17 @@ func testAccCheckListenerDestroy(ctx context.Context) resource.TestCheckFunc { continue } - listener, err := tfelbv2.FindListenerByARN(ctx, conn, rs.Primary.ID) + _, err := tfelbv2.FindListenerByARN(ctx, conn, rs.Primary.ID) - if tfawserr.ErrCodeEquals(err, elbv2.ErrCodeListenerNotFoundException) { + if tfresource.NotFound(err) { continue } if err != nil { - return fmt.Errorf("reading ELBv2 Listener (%s): %w", rs.Primary.ID, err) + return err } - if listener == nil { - continue - } - - return fmt.Errorf("ELBv2 Listener %q still exists", rs.Primary.ID) + return fmt.Errorf("ELBv2 Listener %s still exists", rs.Primary.ID) } return nil @@ -695,27 +731,7 @@ func testAccCheckListenerDestroy(ctx context.Context) resource.TestCheckFunc { } func testAccListenerConfig_base(rName string) string { - return acctest.ConfigCompose(acctest.ConfigAvailableAZsNoOptIn(), fmt.Sprintf(` -resource "aws_vpc" "test" { - cidr_block = "10.0.0.0/16" - - tags = { - Name = %[1]q - } -} - -resource "aws_subnet" "test" { - count = 2 - - vpc_id = aws_vpc.test.id - cidr_block = cidrsubnet(aws_vpc.test.cidr_block, 2, count.index) - availability_zone = data.aws_availability_zones.available.names[count.index] - - tags = { - Name = "%[1]s-${count.index}" - } -} - + return acctest.ConfigCompose(acctest.ConfigVPCWithSubnets(rName, 2), fmt.Sprintf(` resource "aws_security_group" "test" { name = %[1]q description = "Used for ALB Testing" @@ -1210,6 +1226,79 @@ resource "aws_internet_gateway" "test" { `, rName, acctest.TLSPEMEscapeNewlines(certificate), acctest.TLSPEMEscapeNewlines(key))) } +func testAccListenerConfig_mutualAuthentication(rName string, key, certificate string) string { + return acctest.ConfigCompose( + testAccListenerConfig_base(rName), + testAccTrustStoreConfig_baseS3BucketCA(rName), + fmt.Sprintf(` +resource "aws_lb_trust_store" "test" { + name = %[1]q + ca_certificates_bundle_s3_bucket = aws_s3_bucket.test.bucket + ca_certificates_bundle_s3_key = aws_s3_object.test.key +} + +resource "aws_lb_listener" "test" { + load_balancer_arn = aws_lb.test.id + protocol = "HTTPS" + port = "443" + ssl_policy = "ELBSecurityPolicy-2016-08" + certificate_arn = aws_iam_server_certificate.test.arn + + default_action { + target_group_arn = aws_lb_target_group.test.id + type = "forward" + } + + mutual_authentication { + mode = "verify" + trust_store_arn = aws_lb_trust_store.test.arn + } +} + +resource "aws_lb" "test" { + name = %[1]q + internal = true + security_groups = [aws_security_group.test.id] + subnets = aws_subnet.test[*].id + + idle_timeout = 30 + enable_deletion_protection = false + + tags = { + Name = %[1]q + } +} + +resource "aws_lb_target_group" "test" { + name = %[1]q + port = 8080 + protocol = "HTTP" + vpc_id = aws_vpc.test.id + + health_check { + path = "/health" + interval = 60 + port = 8081 + protocol = "HTTP" + timeout = 3 + healthy_threshold = 3 + unhealthy_threshold = 3 + matcher = "200-299" + } + + tags = { + Name = %[1]q + } +} + +resource "aws_iam_server_certificate" "test" { + name = %[1]q + certificate_body = "%[2]s" + private_key = "%[3]s" +} +`, rName, acctest.TLSPEMEscapeNewlines(certificate), acctest.TLSPEMEscapeNewlines(key))) +} + func testAccListenerConfig_arnGateway(rName string) string { return acctest.ConfigCompose( acctest.ConfigAvailableAZsNoOptIn(), diff --git a/internal/service/elbv2/service_package_gen.go b/internal/service/elbv2/service_package_gen.go index 57e5abc0a71..7180fb36b6f 100644 --- a/internal/service/elbv2/service_package_gen.go +++ b/internal/service/elbv2/service_package_gen.go @@ -53,6 +53,11 @@ func (p *servicePackage) SDKDataSources(ctx context.Context) []*types.ServicePac Factory: DataSourceTargetGroup, TypeName: "aws_lb_target_group", }, + { + Factory: DataSourceTrustStore, + TypeName: "aws_lb_trust_store", + Name: "Trust Store", + }, { Factory: DataSourceLoadBalancers, TypeName: "aws_lbs", @@ -142,6 +147,19 @@ func (p *servicePackage) SDKResources(ctx context.Context) []*types.ServicePacka Factory: ResourceTargetGroupAttachment, TypeName: "aws_lb_target_group_attachment", }, + { + Factory: ResourceTrustStore, + TypeName: "aws_lb_trust_store", + Name: "Trust Store", + Tags: &types.ServicePackageResourceTags{ + IdentifierAttribute: "id", + }, + }, + { + Factory: ResourceTrustStoreRevocation, + TypeName: "aws_lb_trust_store_revocation", + Name: "Trust Store Revocation", + }, } } diff --git a/internal/service/elbv2/trust_store.go b/internal/service/elbv2/trust_store.go new file mode 100644 index 00000000000..00176394bdf --- /dev/null +++ b/internal/service/elbv2/trust_store.go @@ -0,0 +1,343 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package elbv2 + +import ( + "context" + "log" + "time" + + "github.com/aws/aws-sdk-go/aws" + "github.com/aws/aws-sdk-go/service/elbv2" + "github.com/hashicorp/aws-sdk-go-base/v2/awsv1shim/v2/tfawserr" + "github.com/hashicorp/terraform-plugin-sdk/v2/diag" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/customdiff" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/retry" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/validation" + "github.com/hashicorp/terraform-provider-aws/internal/conns" + "github.com/hashicorp/terraform-provider-aws/internal/create" + "github.com/hashicorp/terraform-provider-aws/internal/errs" + "github.com/hashicorp/terraform-provider-aws/internal/errs/sdkdiag" + tftags "github.com/hashicorp/terraform-provider-aws/internal/tags" + "github.com/hashicorp/terraform-provider-aws/internal/tfresource" + "github.com/hashicorp/terraform-provider-aws/internal/verify" + "github.com/hashicorp/terraform-provider-aws/names" +) + +// @SDKResource("aws_lb_trust_store", name="Trust Store") +// @Tags(identifierAttribute="id") +func ResourceTrustStore() *schema.Resource { + return &schema.Resource{ + CreateWithoutTimeout: resourceTrustStoreCreate, + ReadWithoutTimeout: resourceTrustStoreRead, + UpdateWithoutTimeout: resourceTrustStoreUpdate, + DeleteWithoutTimeout: resourceTrustStoreDelete, + + Importer: &schema.ResourceImporter{ + StateContext: schema.ImportStatePassthroughContext, + }, + + Timeouts: &schema.ResourceTimeout{ + Create: schema.DefaultTimeout(2 * time.Minute), + Delete: schema.DefaultTimeout(2 * time.Minute), + }, + + CustomizeDiff: customdiff.Sequence( + verify.SetTagsDiff, + ), + + Schema: map[string]*schema.Schema{ + "arn": { + Type: schema.TypeString, + Computed: true, + }, + "arn_suffix": { + Type: schema.TypeString, + Computed: true, + }, + "ca_certificates_bundle_s3_bucket": { + Type: schema.TypeString, + Required: true, + ValidateFunc: validation.NoZeroValues, + }, + "ca_certificates_bundle_s3_key": { + Type: schema.TypeString, + Required: true, + ValidateFunc: validation.NoZeroValues, + }, + "ca_certificates_bundle_s3_object_version": { + Type: schema.TypeString, + Optional: true, + ValidateFunc: validation.NoZeroValues, + }, + "name": { + Type: schema.TypeString, + Optional: true, + Computed: true, + ForceNew: true, + ConflictsWith: []string{"name_prefix"}, + ValidateFunc: validName, + }, + "name_prefix": { + Type: schema.TypeString, + Optional: true, + Computed: true, + ForceNew: true, + ConflictsWith: []string{"name"}, + ValidateFunc: validNamePrefix, + }, + names.AttrTags: tftags.TagsSchema(), + names.AttrTagsAll: tftags.TagsSchemaComputed(), + }, + } +} + +func resourceTrustStoreCreate(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics { + var diags diag.Diagnostics + conn := meta.(*conns.AWSClient).ELBV2Conn(ctx) + + name := create.NewNameGenerator( + create.WithConfiguredName(d.Get("name").(string)), + create.WithConfiguredPrefix(d.Get("name_prefix").(string)), + create.WithDefaultPrefix("tf-"), + ).Generate() + input := &elbv2.CreateTrustStoreInput{ + CaCertificatesBundleS3Bucket: aws.String(d.Get("ca_certificates_bundle_s3_bucket").(string)), + CaCertificatesBundleS3Key: aws.String(d.Get("ca_certificates_bundle_s3_key").(string)), + Name: aws.String(name), + Tags: getTagsIn(ctx), + } + + if v, ok := d.GetOk("ca_certificates_bundle_s3_object_version"); ok { + input.CaCertificatesBundleS3ObjectVersion = aws.String(v.(string)) + } + + output, err := conn.CreateTrustStoreWithContext(ctx, input) + + // Some partitions (e.g. ISO) may not support tag-on-create. + if input.Tags != nil && errs.IsUnsupportedOperationInPartitionError(conn.PartitionID, err) { + input.Tags = nil + + output, err = conn.CreateTrustStoreWithContext(ctx, input) + } + + // Tags are not supported on creation with some protocol types(i.e. GENEVE) + // Retry creation without tags + if input.Tags != nil && tfawserr.ErrMessageContains(err, ErrValidationError, TagsOnCreationErrMessage) { + input.Tags = nil + + output, err = conn.CreateTrustStoreWithContext(ctx, input) + } + + if err != nil { + return sdkdiag.AppendErrorf(diags, "creating ELBv2 Trust Store (%s): %s", name, err) + } + + d.SetId(aws.StringValue(output.TrustStores[0].TrustStoreArn)) + + _, err = tfresource.RetryWhenNotFound(ctx, d.Timeout(schema.TimeoutCreate), func() (interface{}, error) { + return FindTrustStoreByARN(ctx, conn, d.Id()) + }) + + if err != nil { + return sdkdiag.AppendErrorf(diags, "waiting for ELBv2 Trust Store (%s) create: %s", d.Id(), err) + } + + // For partitions not supporting tag-on-create, attempt tag after create. + if tags := getTagsIn(ctx); input.Tags == nil && len(tags) > 0 { + err := createTags(ctx, conn, d.Id(), tags) + + // If default tags only, continue. Otherwise, error. + if v, ok := d.GetOk(names.AttrTags); (!ok || len(v.(map[string]interface{})) == 0) && errs.IsUnsupportedOperationInPartitionError(conn.PartitionID, err) { + return append(diags, resourceTrustStoreRead(ctx, d, meta)...) + } + + if err != nil { + return sdkdiag.AppendErrorf(diags, "setting ELBv2 Trust Store (%s) tags: %s", d.Id(), err) + } + } + + return append(diags, resourceTrustStoreRead(ctx, d, meta)...) +} + +func resourceTrustStoreRead(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics { + var diags diag.Diagnostics + conn := meta.(*conns.AWSClient).ELBV2Conn(ctx) + + trustStore, err := FindTrustStoreByARN(ctx, conn, d.Id()) + + if !d.IsNewResource() && tfresource.NotFound(err) { + log.Printf("[WARN] ELBv2 Trust Store %s not found, removing from state", d.Id()) + d.SetId("") + return diags + } + + if err != nil { + return sdkdiag.AppendErrorf(diags, "reading ELBv2 Trust Store (%s): %s", d.Id(), err) + } + + d.Set("arn", trustStore.TrustStoreArn) + d.Set("name", trustStore.Name) + d.Set("name_prefix", create.NamePrefixFromName(aws.StringValue(trustStore.Name))) + + return diags +} + +func resourceTrustStoreUpdate(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics { + var diags diag.Diagnostics + conn := meta.(*conns.AWSClient).ELBV2Conn(ctx) + + if d.HasChangesExcept("tags", "tags_all") { + input := &elbv2.ModifyTrustStoreInput{ + CaCertificatesBundleS3Bucket: aws.String(d.Get("ca_certificates_bundle_s3_bucket").(string)), + CaCertificatesBundleS3Key: aws.String(d.Get("ca_certificates_bundle_s3_key").(string)), + TrustStoreArn: aws.String(d.Id()), + } + + if v, ok := d.GetOk("ca_certificates_bundle_s3_object_version"); ok { + input.CaCertificatesBundleS3ObjectVersion = aws.String(v.(string)) + } + + _, err := conn.ModifyTrustStoreWithContext(ctx, input) + + if err != nil { + return sdkdiag.AppendErrorf(diags, "modifying ELBv2 Trust Store (%s): %s", d.Id(), err) + } + } + + return append(diags, resourceTrustStoreRead(ctx, d, meta)...) +} + +func resourceTrustStoreDelete(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics { + var diags diag.Diagnostics + conn := meta.(*conns.AWSClient).ELBV2Conn(ctx) + + if err := waitForNoTrustStoreAssociations(ctx, conn, d.Id(), d.Timeout(schema.TimeoutDelete)); err != nil { + return sdkdiag.AppendErrorf(diags, "waiting for ELBV2 Trust Store (%s) associations delete: %s", d.Id(), err) + } + + log.Printf("[DEBUG] Deleting ELBv2 Trust Store: %s", d.Id()) + _, err := tfresource.RetryWhenAWSErrMessageContains(ctx, d.Timeout(schema.TimeoutDelete), func() (interface{}, error) { + return conn.DeleteTrustStoreWithContext(ctx, &elbv2.DeleteTrustStoreInput{ + TrustStoreArn: aws.String(d.Id()), + }) + }, elbv2.ErrCodeTrustStoreInUseException, "is currently in use by a listener") + + if err != nil { + return sdkdiag.AppendErrorf(diags, "deleting ELBv2 Trust Store (%s): %s", d.Id(), err) + } + + return diags +} + +func FindTrustStoreByARN(ctx context.Context, conn *elbv2.ELBV2, arn string) (*elbv2.TrustStore, error) { + input := &elbv2.DescribeTrustStoresInput{ + TrustStoreArns: aws.StringSlice([]string{arn}), + } + output, err := findTrustStore(ctx, conn, input) + + if err != nil { + return nil, err + } + + // Eventual consistency check. + if aws.StringValue(output.TrustStoreArn) != arn { + return nil, &retry.NotFoundError{ + LastRequest: input, + } + } + + return output, nil +} + +func findTrustStore(ctx context.Context, conn *elbv2.ELBV2, input *elbv2.DescribeTrustStoresInput) (*elbv2.TrustStore, error) { + output, err := findTrustStores(ctx, conn, input) + + if err != nil { + return nil, err + } + + return tfresource.AssertSinglePtrResult(output) +} + +func findTrustStores(ctx context.Context, conn *elbv2.ELBV2, input *elbv2.DescribeTrustStoresInput) ([]*elbv2.TrustStore, error) { + var output []*elbv2.TrustStore + + err := conn.DescribeTrustStoresPagesWithContext(ctx, input, func(page *elbv2.DescribeTrustStoresOutput, lastPage bool) bool { + if page == nil { + return !lastPage + } + + for _, v := range page.TrustStores { + if v != nil { + output = append(output, v) + } + } + + return !lastPage + }) + + if tfawserr.ErrCodeEquals(err, elbv2.ErrCodeTrustStoreNotFoundException) { + return nil, &retry.NotFoundError{ + LastError: err, + LastRequest: input, + } + } + + if err != nil { + return nil, err + } + + return output, nil +} + +func findTrustStoreAssociations(ctx context.Context, conn *elbv2.ELBV2, input *elbv2.DescribeTrustStoreAssociationsInput) ([]*elbv2.TrustStoreAssociation, error) { + var output []*elbv2.TrustStoreAssociation + + err := conn.DescribeTrustStoreAssociationsPagesWithContext(ctx, input, func(page *elbv2.DescribeTrustStoreAssociationsOutput, lastPage bool) bool { + if page == nil { + return !lastPage + } + + for _, v := range page.TrustStoreAssociations { + if v != nil { + output = append(output, v) + } + } + + return !lastPage + }) + + if tfawserr.ErrCodeEquals(err, elbv2.ErrCodeTrustStoreNotFoundException) { + return nil, &retry.NotFoundError{ + LastError: err, + LastRequest: input, + } + } + + if err != nil { + return nil, err + } + + return output, nil +} + +func waitForNoTrustStoreAssociations(ctx context.Context, conn *elbv2.ELBV2, arn string, timeout time.Duration) error { + input := &elbv2.DescribeTrustStoreAssociationsInput{ + TrustStoreArn: aws.String(arn), + } + + _, err := tfresource.RetryUntilEqual(ctx, timeout, 0, func() (int, error) { + associations, err := findTrustStoreAssociations(ctx, conn, input) + + if err != nil { + return 0, err + } + + return len(associations), nil + }) + + return err +} diff --git a/internal/service/elbv2/trust_store_data_source.go b/internal/service/elbv2/trust_store_data_source.go new file mode 100644 index 00000000000..ba3b92c8237 --- /dev/null +++ b/internal/service/elbv2/trust_store_data_source.go @@ -0,0 +1,61 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package elbv2 + +import ( + "context" + + "github.com/aws/aws-sdk-go/aws" + "github.com/aws/aws-sdk-go/service/elbv2" + "github.com/hashicorp/terraform-plugin-sdk/v2/diag" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" + "github.com/hashicorp/terraform-provider-aws/internal/conns" + "github.com/hashicorp/terraform-provider-aws/internal/errs/sdkdiag" + "github.com/hashicorp/terraform-provider-aws/internal/tfresource" +) + +// @SDKDataSource("aws_lb_trust_store", name="Trust Store") +func DataSourceTrustStore() *schema.Resource { + return &schema.Resource{ + ReadWithoutTimeout: dataSourceTrustStoreRead, + + Schema: map[string]*schema.Schema{ + "arn": { + Type: schema.TypeString, + Optional: true, + Computed: true, + }, + "name": { + Type: schema.TypeString, + Optional: true, + Computed: true, + }, + }, + } +} + +func dataSourceTrustStoreRead(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics { + var diags diag.Diagnostics + conn := meta.(*conns.AWSClient).ELBV2Conn(ctx) + + input := &elbv2.DescribeTrustStoresInput{} + + if v, ok := d.GetOk("arn"); ok { + input.TrustStoreArns = aws.StringSlice([]string{v.(string)}) + } else if v, ok := d.GetOk("name"); ok { + input.Names = aws.StringSlice([]string{v.(string)}) + } + + trustStore, err := findTrustStore(ctx, conn, input) + + if err != nil { + return sdkdiag.AppendFromErr(diags, tfresource.SingularDataSourceFindError("ELBv2 Trust Store", err)) + } + + d.SetId(aws.StringValue(trustStore.TrustStoreArn)) + d.Set("arn", trustStore.TrustStoreArn) + d.Set("name", trustStore.Name) + + return diags +} diff --git a/internal/service/elbv2/trust_store_data_source_test.go b/internal/service/elbv2/trust_store_data_source_test.go new file mode 100644 index 00000000000..cdc0bf1e4e1 --- /dev/null +++ b/internal/service/elbv2/trust_store_data_source_test.go @@ -0,0 +1,73 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package elbv2_test + +import ( + "fmt" + "testing" + + "github.com/aws/aws-sdk-go/service/elbv2" + sdkacctest "github.com/hashicorp/terraform-plugin-testing/helper/acctest" + "github.com/hashicorp/terraform-plugin-testing/helper/resource" + "github.com/hashicorp/terraform-provider-aws/internal/acctest" + _ "github.com/hashicorp/terraform-provider-aws/internal/service/elbv2" +) + +func TestAccELBV2TrustStoreDataSource_basic(t *testing.T) { + ctx := acctest.Context(t) + rName := sdkacctest.RandomWithPrefix(acctest.ResourcePrefix) + datasourceNameByName := "data.aws_lb_trust_store.named" + datasourceNameByArn := "data.aws_lb_trust_store.with_arn" + + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { acctest.PreCheck(ctx, t) }, + ErrorCheck: acctest.ErrorCheck(t, elbv2.EndpointsID), + ProtoV5ProviderFactories: acctest.ProtoV5ProviderFactories, + Steps: []resource.TestStep{ + + { + Config: testAccTrustStoreDataSourceConfig_withName(rName), + Check: resource.ComposeTestCheckFunc( + resource.TestCheckResourceAttr(datasourceNameByName, "name", rName), + resource.TestCheckResourceAttrSet(datasourceNameByName, "arn"), + ), + }, + { + Config: testAccTrustStoreDataSourceConfig_withARN(rName), + Check: resource.ComposeTestCheckFunc( + resource.TestCheckResourceAttr(datasourceNameByArn, "name", rName), + resource.TestCheckResourceAttrSet(datasourceNameByArn, "arn"), + ), + }, + }, + }) +} + +func testAccTrustStoreDataSourceConfig_base(rName string) string { + return acctest.ConfigCompose(testAccTrustStoreConfig_baseS3BucketCA(rName), fmt.Sprintf(` +resource "aws_lb_trust_store" "test" { + name = %[1]q + ca_certificates_bundle_s3_bucket = aws_s3_bucket.test.bucket + ca_certificates_bundle_s3_key = aws_s3_object.test.key +} +`, rName)) +} + +func testAccTrustStoreDataSourceConfig_withName(rName string) string { + return acctest.ConfigCompose(testAccTrustStoreDataSourceConfig_base(rName), fmt.Sprintf(` +data "aws_lb_trust_store" "named" { + name = %[1]q + depends_on = [aws_lb_trust_store.test] +} +`, rName)) +} + +func testAccTrustStoreDataSourceConfig_withARN(rName string) string { + return acctest.ConfigCompose(testAccTrustStoreDataSourceConfig_base(rName), ` +data "aws_lb_trust_store" "with_arn" { + arn = aws_lb_trust_store.test.arn + depends_on = [aws_lb_trust_store.test] +} +`) +} diff --git a/internal/service/elbv2/trust_store_revocation.go b/internal/service/elbv2/trust_store_revocation.go new file mode 100644 index 00000000000..ca39bb8ee9f --- /dev/null +++ b/internal/service/elbv2/trust_store_revocation.go @@ -0,0 +1,234 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package elbv2 + +import ( + "context" + "log" + "strconv" + "time" + + "github.com/aws/aws-sdk-go/aws" + "github.com/aws/aws-sdk-go/service/elbv2" + "github.com/hashicorp/aws-sdk-go-base/v2/awsv1shim/v2/tfawserr" + "github.com/hashicorp/terraform-plugin-sdk/v2/diag" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/retry" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/validation" + "github.com/hashicorp/terraform-provider-aws/internal/conns" + "github.com/hashicorp/terraform-provider-aws/internal/errs" + "github.com/hashicorp/terraform-provider-aws/internal/errs/sdkdiag" + "github.com/hashicorp/terraform-provider-aws/internal/flex" + "github.com/hashicorp/terraform-provider-aws/internal/tfresource" + "github.com/hashicorp/terraform-provider-aws/internal/verify" +) + +// @SDKResource("aws_lb_trust_store_revocation", name="Trust Store Revocation") +func ResourceTrustStoreRevocation() *schema.Resource { + return &schema.Resource{ + CreateWithoutTimeout: resourceTrustStoreRevocationCreate, + ReadWithoutTimeout: resourceTrustStoreRevocationRead, + DeleteWithoutTimeout: resourceTrustStoreRevocationDelete, + + Importer: &schema.ResourceImporter{ + StateContext: schema.ImportStatePassthroughContext, + }, + + Timeouts: &schema.ResourceTimeout{ + Create: schema.DefaultTimeout(2 * time.Minute), + }, + + Schema: map[string]*schema.Schema{ + "revocation_id": { + Type: schema.TypeInt, + Computed: true, + }, + "revocations_s3_bucket": { + Type: schema.TypeString, + Required: true, + ForceNew: true, + ValidateFunc: validation.NoZeroValues, + }, + "revocations_s3_key": { + Type: schema.TypeString, + Required: true, + ForceNew: true, + ValidateFunc: validation.NoZeroValues, + }, + "revocations_s3_object_version": { + Type: schema.TypeString, + Optional: true, + ForceNew: true, + ValidateFunc: validation.NoZeroValues, + }, + "trust_store_arn": { + Type: schema.TypeString, + Required: true, + ForceNew: true, + ValidateFunc: verify.ValidARN, + }, + }, + } +} + +const ( + trustStoreRevocationResourceIDPartCount = 2 +) + +func resourceTrustStoreRevocationCreate(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics { + var diags diag.Diagnostics + conn := meta.(*conns.AWSClient).ELBV2Conn(ctx) + + s3Bucket := d.Get("revocations_s3_bucket").(string) + s3Key := d.Get("revocations_s3_key").(string) + trustStoreARN := d.Get("trust_store_arn").(string) + input := &elbv2.AddTrustStoreRevocationsInput{ + RevocationContents: []*elbv2.RevocationContent{{ + S3Bucket: aws.String(s3Bucket), + S3Key: aws.String(s3Key), + }}, + TrustStoreArn: aws.String(trustStoreARN), + } + + if v, ok := d.GetOk("revocations_s3_object_version"); ok { + input.RevocationContents[0].S3ObjectVersion = aws.String(v.(string)) + } + + output, err := conn.AddTrustStoreRevocationsWithContext(ctx, input) + + if err != nil { + sdkdiag.AppendErrorf(diags, "creating ELBv2 Trust Store (%s) Revocation (s3://%s/%s): %s", trustStoreARN, s3Bucket, s3Key, err) + } + + revocationID := aws.Int64Value(output.TrustStoreRevocations[0].RevocationId) + id := errs.Must(flex.FlattenResourceId([]string{trustStoreARN, strconv.FormatInt(revocationID, 10)}, trustStoreRevocationResourceIDPartCount, false)) + + d.SetId(id) + + _, err = tfresource.RetryWhenNotFound(ctx, d.Timeout(schema.TimeoutCreate), func() (interface{}, error) { + return FindTrustStoreRevocationByTwoPartKey(ctx, conn, trustStoreARN, revocationID) + }) + + if err != nil { + return sdkdiag.AppendErrorf(diags, "waiting for ELBv2 Trust Store Revocation (%s) create: %s", d.Id(), err) + } + + return append(diags, resourceTrustStoreRevocationRead(ctx, d, meta)...) +} + +func resourceTrustStoreRevocationRead(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics { + var diags diag.Diagnostics + conn := meta.(*conns.AWSClient).ELBV2Conn(ctx) + + parts, err := flex.ExpandResourceId(d.Id(), trustStoreRevocationResourceIDPartCount, false) + if err != nil { + return sdkdiag.AppendFromErr(diags, err) + } + + trustStoreARN := parts[0] + revocationID := errs.Must(strconv.ParseInt(parts[1], 10, 64)) + revocation, err := FindTrustStoreRevocationByTwoPartKey(ctx, conn, trustStoreARN, revocationID) + + if !d.IsNewResource() && tfresource.NotFound(err) { + log.Printf("[WARN] ELBv2 Trust Store Revocation %s not found, removing from state", d.Id()) + d.SetId("") + return diags + } + + if err != nil { + return sdkdiag.AppendErrorf(diags, "reading ELBv2 Trust Store Revocation (%s): %s", d.Id(), err) + } + + d.Set("revocation_id", revocation.RevocationId) + d.Set("trust_store_arn", revocation.TrustStoreArn) + + return diags +} + +func resourceTrustStoreRevocationDelete(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics { + var diags diag.Diagnostics + conn := meta.(*conns.AWSClient).ELBV2Conn(ctx) + + parts, err := flex.ExpandResourceId(d.Id(), trustStoreRevocationResourceIDPartCount, false) + if err != nil { + return sdkdiag.AppendFromErr(diags, err) + } + + trustStoreARN := parts[0] + revocationID := errs.Must(strconv.ParseInt(parts[1], 10, 64)) + + log.Printf("[DEBUG] Deleting ELBv2 Trust Store Revocation: %s", d.Id()) + _, err = conn.RemoveTrustStoreRevocationsWithContext(ctx, &elbv2.RemoveTrustStoreRevocationsInput{ + RevocationIds: aws.Int64Slice([]int64{revocationID}), + TrustStoreArn: aws.String(trustStoreARN), + }) + + if err != nil { + return sdkdiag.AppendErrorf(diags, "deleting ELBv2 Trust Store Revocation (%s): %s", d.Id(), err) + } + + return diags +} + +func FindTrustStoreRevocationByTwoPartKey(ctx context.Context, conn *elbv2.ELBV2, trustStoreARN string, revocationID int64) (*elbv2.DescribeTrustStoreRevocation, error) { + input := &elbv2.DescribeTrustStoreRevocationsInput{ + RevocationIds: aws.Int64Slice([]int64{revocationID}), + TrustStoreArn: aws.String(trustStoreARN), + } + output, err := findTrustStoreRevocation(ctx, conn, input) + + if err != nil { + return nil, err + } + + // Eventual consistency check. + if aws.StringValue(output.TrustStoreArn) != trustStoreARN || aws.Int64Value(output.RevocationId) != revocationID { + return nil, &retry.NotFoundError{ + LastRequest: input, + } + } + + return output, nil +} + +func findTrustStoreRevocation(ctx context.Context, conn *elbv2.ELBV2, input *elbv2.DescribeTrustStoreRevocationsInput) (*elbv2.DescribeTrustStoreRevocation, error) { + output, err := findTrustStoreRevocations(ctx, conn, input) + + if err != nil { + return nil, err + } + + return tfresource.AssertSinglePtrResult(output) +} + +func findTrustStoreRevocations(ctx context.Context, conn *elbv2.ELBV2, input *elbv2.DescribeTrustStoreRevocationsInput) ([]*elbv2.DescribeTrustStoreRevocation, error) { + var output []*elbv2.DescribeTrustStoreRevocation + + err := conn.DescribeTrustStoreRevocationsPagesWithContext(ctx, input, func(page *elbv2.DescribeTrustStoreRevocationsOutput, lastPage bool) bool { + if page == nil { + return !lastPage + } + + for _, v := range page.TrustStoreRevocations { + if v != nil { + output = append(output, v) + } + } + + return !lastPage + }) + + if tfawserr.ErrCodeEquals(err, elbv2.ErrCodeTrustStoreNotFoundException) { + return nil, &retry.NotFoundError{ + LastError: err, + LastRequest: input, + } + } + + if err != nil { + return nil, err + } + + return output, nil +} diff --git a/internal/service/elbv2/trust_store_revocation_test.go b/internal/service/elbv2/trust_store_revocation_test.go new file mode 100644 index 00000000000..132022675b6 --- /dev/null +++ b/internal/service/elbv2/trust_store_revocation_test.go @@ -0,0 +1,153 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package elbv2_test + +import ( + "context" + "fmt" + "strconv" + "testing" + + "github.com/aws/aws-sdk-go/service/elbv2" + sdkacctest "github.com/hashicorp/terraform-plugin-testing/helper/acctest" + "github.com/hashicorp/terraform-plugin-testing/helper/resource" + "github.com/hashicorp/terraform-plugin-testing/terraform" + "github.com/hashicorp/terraform-provider-aws/internal/acctest" + "github.com/hashicorp/terraform-provider-aws/internal/conns" + tfelbv2 "github.com/hashicorp/terraform-provider-aws/internal/service/elbv2" + "github.com/hashicorp/terraform-provider-aws/internal/tfresource" +) + +func TestAccELBV2TrustStoreRevocation_basic(t *testing.T) { + ctx := acctest.Context(t) + var conf elbv2.DescribeTrustStoreRevocation + resourceName := "aws_lb_trust_store_revocation.test" + rName := sdkacctest.RandomWithPrefix(acctest.ResourcePrefix) + + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { acctest.PreCheck(ctx, t) }, + ErrorCheck: acctest.ErrorCheck(t, elbv2.EndpointsID), + ProtoV5ProviderFactories: acctest.ProtoV5ProviderFactories, + CheckDestroy: testAccCheckTrustStoreRevocationDestroy(ctx), + Steps: []resource.TestStep{ + { + Config: testAccTrustStoreRevocationConfig_basic(rName), + Check: resource.ComposeAggregateTestCheckFunc( + testAccCheckTrustStoreRevocationExists(ctx, resourceName, &conf), + resource.TestCheckResourceAttrSet(resourceName, "trust_store_arn"), + resource.TestCheckResourceAttrSet(resourceName, "revocation_id"), + ), + }, + { + ResourceName: resourceName, + ImportState: true, + ImportStateVerify: false, + }, + }, + }) +} + +func testAccCheckTrustStoreRevocationExists(ctx context.Context, n string, v *elbv2.DescribeTrustStoreRevocation) resource.TestCheckFunc { + return func(s *terraform.State) error { + rs, ok := s.RootModule().Resources[n] + if !ok { + return fmt.Errorf("Not found: %s", n) + } + + conn := acctest.Provider.Meta().(*conns.AWSClient).ELBV2Conn(ctx) + + trustStoreARN := rs.Primary.Attributes["trust_store_arn"] + revocationID, err := strconv.ParseInt(rs.Primary.Attributes["revocation_id"], 10, 64) + + if err != nil { + return err + } + + output, err := tfelbv2.FindTrustStoreRevocationByTwoPartKey(ctx, conn, trustStoreARN, revocationID) + + if err != nil { + return err + } + + *v = *output + + return nil + } +} + +func testAccCheckTrustStoreRevocationDestroy(ctx context.Context) resource.TestCheckFunc { + return func(s *terraform.State) error { + conn := acctest.Provider.Meta().(*conns.AWSClient).ELBV2Conn(ctx) + + for _, rs := range s.RootModule().Resources { + if rs.Type != "aws_lb_trust_store_revocation" { + continue + } + + trustStoreARN := rs.Primary.Attributes["trust_store_arn"] + revocationID, err := strconv.ParseInt(rs.Primary.Attributes["revocation_id"], 10, 64) + + if err != nil { + return err + } + + _, err = tfelbv2.FindTrustStoreRevocationByTwoPartKey(ctx, conn, trustStoreARN, revocationID) + + if tfresource.NotFound(err) { + continue + } + + if err != nil { + return err + } + + return fmt.Errorf("ELBv2 Trust Store Revocation %s still exists", rs.Primary.ID) + } + + return nil + } +} + +func testAccTrustStoreRevocationConfig_basic(rName string) string { + return acctest.ConfigCompose(testAccTrustStoreConfig_baseS3BucketCA(rName), fmt.Sprintf(` +resource "aws_lb_trust_store" "test" { + name = %[1]q + ca_certificates_bundle_s3_bucket = aws_s3_bucket.test.bucket + ca_certificates_bundle_s3_key = aws_s3_object.test.key +} + +resource "aws_s3_object" "crl" { + bucket = aws_s3_bucket.test.bucket + key = "%[1]s-crl.pem" + content = < **Note:** `aws_alb_trust_store` is known as `aws_lb_trust_store`. The functionality is identical. + +Provides information about a Load Balancer Trust Store. + +This data source can prove useful when a module accepts an LB Trust Store as an +input variable and needs to know its attributes. It can also be used to get the ARN of +an LB Trust Store for use in other resources, given LB Trust Store name. + +## Example Usage + +```terraform +variable "lb_ts_arn" { + type = string + default = "" +} + +variable "lb_ts_name" { + type = string + default = "" +} + +data "aws_lb_trust_store" "test" { + arn = var.lb_ts_arn + name = var.lb_ts_name +} +``` + +## Argument Reference + +This data source supports the following arguments: + +* `arn` - (Optional) Full ARN of the trust store. +* `name` - (Optional) Unique name of the trust store. + +~> **NOTE:** When both `arn` and `name` are specified, `arn` takes precedence. + +## Attribute Reference + +See the [LB Trust Store Resource](/docs/providers/aws/r/lb_trust_store.html) for details +on the returned attributes - they are identical. + +## Timeouts + +[Configuration options](https://developer.hashicorp.com/terraform/language/resources/syntax#operation-timeouts): + +- `read` - (Default `20m`) diff --git a/website/docs/r/lb_listener.html.markdown b/website/docs/r/lb_listener.html.markdown index 34220c6c6b6..c98b41fea46 100644 --- a/website/docs/r/lb_listener.html.markdown +++ b/website/docs/r/lb_listener.html.markdown @@ -219,6 +219,35 @@ resource "aws_lb_listener" "example" { } ``` +### Mutual TLS Authentication + +```terraform + +resource "aws_lb" "example" { + load_balancer_type = "application" + + # ... +} + +resource "aws_lb_target_group" "example" { + # ... +} + +resource "aws_lb_listener" "example" { + load_balancer_arn = aws_lb.example.id + + default_action { + target_group_arn = aws_lb_target_group.example.id + type = "forward" + } + + mutual_authentication = { + mode = "verify" + trust_store_arn = "..." + } +} +``` + ## Argument Reference The following arguments are required: @@ -230,6 +259,7 @@ The following arguments are optional: * `alpn_policy` - (Optional) Name of the Application-Layer Protocol Negotiation (ALPN) policy. Can be set if `protocol` is `TLS`. Valid values are `HTTP1Only`, `HTTP2Only`, `HTTP2Optional`, `HTTP2Preferred`, and `None`. * `certificate_arn` - (Optional) ARN of the default SSL server certificate. Exactly one certificate is required if the protocol is HTTPS. For adding additional SSL certificates, see the [`aws_lb_listener_certificate` resource](/docs/providers/aws/r/lb_listener_certificate.html). +* `mutual_authentication` - (Optional) The mutual authentication configuration information. Detailed below. * `port` - (Optional) Port on which the load balancer is listening. Not valid for Gateway Load Balancers. * `protocol` - (Optional) Protocol for connections from clients to the load balancer. For Application Load Balancers, valid values are `HTTP` and `HTTPS`, with a default of `HTTP`. For Network Load Balancers, valid values are `TCP`, `TLS`, `UDP`, and `TCP_UDP`. Not valid to use `UDP` or `TCP_UDP` if dual-stack mode is enabled. Not valid for Gateway Load Balancers. * `ssl_policy` - (Optional) Name of the SSL Policy for the listener. Required if `protocol` is `HTTPS` or `TLS`. @@ -350,6 +380,12 @@ The following arguments are optional: * `protocol` - (Optional) Protocol. Valid values are `HTTP`, `HTTPS`, or `#{protocol}`. Defaults to `#{protocol}`. * `query` - (Optional) Query parameters, URL-encoded when necessary, but not percent-encoded. Do not include the leading "?". Defaults to `#{query}`. +### mutual_authentication + +* `mode` - (Required) Valid values are `off`, `verify` and `passthrough`. +* `trust_store_arn` - (Required) ARN of the elbv2 Trust Store. +* `ignore_client_certificate_expiry` - (Optional) Whether client certificate expiry is ignored. Default is `false`. + ## Attribute Reference This resource exports the following attributes in addition to the arguments above: diff --git a/website/docs/r/lb_trust_store.html.markdown b/website/docs/r/lb_trust_store.html.markdown new file mode 100644 index 00000000000..c417812d43c --- /dev/null +++ b/website/docs/r/lb_trust_store.html.markdown @@ -0,0 +1,78 @@ +--- +subcategory: "ELB (Elastic Load Balancing)" +layout: "aws" +page_title: "AWS: aws_lb_trust_store" +description: |- + Provides a Trust Store resource for use with Load Balancers. +--- + +# Resource: aws_lb_trust_store + +Provides a ELBv2 Trust Store for use with Application Load Balancer Listener resources. + +## Example Usage + +### Trust Store Load Balancer Listener + +```terraform +resource "aws_lb_trust_store" "test" { + name = "tf-example-lb-ts" + + ca_certificates_bundle_s3_bucket = "..." + ca_certificates_bundle_s3_key = "..." + +} + +resource "aws_lb_listener" "example" { + load_balancer_arn = aws_lb.example.id + + default_action { + target_group_arn = aws_lb_target_group.example.id + type = "forward" + } + + mutual_authentication = { + mode = "verify" + trust_store_arn = aws_lb_trust_store.test.arn + } +} +``` + +## Argument Reference + +This resource supports the following arguments: + +* `ca_certificates_bundle_s3_bucket` - (Required) S3 Bucket name holding the client certificate CA bundle. +* `ca_certificates_bundle_s3_key` - (Required) S3 Bucket name holding the client certificate CA bundle. +* `ca_certificates_bundle_s3_object_version` - (Optional) Version Id of CA bundle S3 bucket object, if versioned, defaults to latest if omitted. + +* `name_prefix` - (Optional, Forces new resource) Creates a unique name beginning with the specified prefix. Conflicts with `name`. Cannot be longer than 6 characters. +* `name` - (Optional, Forces new resource) Name of the Trust Store. If omitted, Terraform will assign a random, unique name. This name must be unique per region per account, can have a maximum of 32 characters, must contain only alphanumeric characters or hyphens, and must not begin or end with a hyphen. +* `tags` - (Optional) Map of tags to assign to the resource. If configured with a provider [`default_tags` configuration block](https://registry.terraform.io/providers/hashicorp/aws/latest/docs#default_tags-configuration-block) present, tags with matching keys will overwrite those defined at the provider-level. + +## Attribute Reference + +This resource exports the following attributes in addition to the arguments above: + +* `arn_suffix` - ARN suffix for use with CloudWatch Metrics. +* `arn` - ARN of the Trust Store (matches `id`). +* `id` - ARN of the Trust Store (matches `arn`). +* `name` - Name of the Trust Store. +* `tags_all` - A map of tags assigned to the resource, including those inherited from the provider [`default_tags` configuration block](https://registry.terraform.io/providers/hashicorp/aws/latest/docs#default_tags-configuration-block). + +## Import + +In Terraform v1.5.0 and later, use an [`import` block](https://developer.hashicorp.com/terraform/language/import) to import Trust Stores using their ARN. For example: + +```terraform +import { + to = aws_lb_trust_store.example + id = "arn:aws:elasticloadbalancing:us-west-2:187416307283:truststore/my-trust-store/20cfe21448b66314" +} +``` + +Using `terraform import`, import Target Groups using their ARN. For example: + +```console +% terraform import aws_lb_trust_store.example arn:aws:elasticloadbalancing:us-west-2:187416307283:truststore/my-trust-store/20cfe21448b66314 +``` diff --git a/website/docs/r/lb_trust_store_revocation.html.markdown b/website/docs/r/lb_trust_store_revocation.html.markdown new file mode 100644 index 00000000000..a4c9b387220 --- /dev/null +++ b/website/docs/r/lb_trust_store_revocation.html.markdown @@ -0,0 +1,67 @@ +--- +subcategory: "ELB (Elastic Load Balancing)" +layout: "aws" +page_title: "AWS: aws_lb_trust_store_revocation" +description: |- + Provides a Trust Store Revocation resource for use with Load Balancers. +--- + +# Resource: aws_lb_trust_store_revocation + +Provides a ELBv2 Trust Store Revocation for use with Application Load Balancer Listener resources. + +## Example Usage + +### Trust Store With Revocations + +```terraform +resource "aws_lb_trust_store" "test" { + name = "tf-example-lb-ts" + + ca_certificates_bundle_s3_bucket = "..." + ca_certificates_bundle_s3_key = "..." + +} + +resource "aws_lb_trust_store_revocation" "test" { + trust_store_arn = aws_lb_trust_store.test.arn + + revocations_s3_bucket = "..." + revocations_s3_key = "..." + +} + +``` + +## Argument Reference + +This resource supports the following arguments: + +* `trust_store_arn` - (Required) Trust Store ARN. +* `revocations_s3_bucket` - (Required) S3 Bucket name holding the client certificate CA bundle. +* `revocations_s3_key` - (Required) S3 Bucket name holding the client certificate CA bundle. +* `revocations_s3_object_version` - (Optional) Version Id of CA bundle S3 bucket object, if versioned, defaults to latest if omitted. + +## Attribute Reference + +This resource exports the following attributes in addition to the arguments above: + +* `revocation_id` - AWS assigned RevocationId, (number). +* `id` - "combination of the Trust Store ARN and RevocationId `${trust_store_arn},{revocation_id}`" + +## Import + +In Terraform v1.5.0 and later, use an [`import` block](https://developer.hashicorp.com/terraform/language/import) to import Trust Store Revocations using their ARN. For example: + +```terraform +import { + to = aws_lb_trust_store_revocation.example + id = "arn:aws:elasticloadbalancing:us-west-2:187416307283:truststore/my-trust-store/20cfe21448b66314,6" +} +``` + +Using `terraform import`, import Trust Store Revocations using their ARN. For example: + +```console +% terraform import aws_lb_trust_store_revocation.example arn:aws:elasticloadbalancing:us-west-2:187416307283:truststore/my-trust-store/20cfe21448b66314,6 +```