Skip to content

Commit

Permalink
Merge pull request #31453 from rromic/f-aws_acm_data_source-tags-filter
Browse files Browse the repository at this point in the history
F aws acm data source tags filter
  • Loading branch information
ewbankkit authored Sep 6, 2024
2 parents c7c410a + 6922f7c commit 433cf45
Show file tree
Hide file tree
Showing 5 changed files with 486 additions and 255 deletions.
3 changes: 3 additions & 0 deletions .changelog/31453.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
```release-note:enhancement
data-source/aws_acm_certificate: Mark `domain` and `tags` as Optional. This enables certificates to be matched based on tags
```
4 changes: 0 additions & 4 deletions docs/acc-test-environment-variables.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,6 @@ Environment variables (beyond standard AWS Go SDK ones) used by acceptance testi
| Variable | Description |
|----------|-------------|
| `ACM_CERTIFICATE_ROOT_DOMAIN` | Root domain name to use with ACM Certificate testing. |
| `ACM_CERTIFICATE_MULTIPLE_ISSUED_DOMAIN` | Domain name of ACM Certificate with multiple issued certificates. **DEPRECATED:** Should be replaced with `aws_acm_certificate` resource usage in tests. |
| `ACM_CERTIFICATE_MULTIPLE_ISSUED_MOST_RECENT_ARN` | Amazon Resource Name of most recent ACM Certificate with multiple issued certificates. **DEPRECATED:** Should be replaced with `aws_acm_certificate` resource usage in tests. |
| `ACM_CERTIFICATE_SINGLE_ISSUED_DOMAIN` | Domain name of ACM Certificate with a single issued certificate. **DEPRECATED:** Should be replaced with `aws_acm_certificate` resource usage in tests. |
| `ACM_CERTIFICATE_SINGLE_ISSUED_MOST_RECENT_ARN` | Amazon Resource Name of most recent ACM Certificate with a single issued certificate. **DEPRECATED:** Should be replaced with `aws_acm_certificate` resource usage in tests. |
| `ADM_CLIENT_ID` | Identifier for Amazon Device Manager Client in Pinpoint testing. |
| `AMPLIFY_DOMAIN_NAME` | Domain name to use for Amplify domain association testing. |
| `AMPLIFY_GITHUB_ACCESS_TOKEN` | GitHub access token used for AWS Amplify testing. |
Expand Down
182 changes: 93 additions & 89 deletions internal/service/acm/certificate_data_source.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,18 +6,20 @@ package acm
import (
"context"
"fmt"
"slices"
"time"

"github.com/aws/aws-sdk-go-v2/aws"
"github.com/aws/aws-sdk-go-v2/service/acm"
"github.com/aws/aws-sdk-go-v2/service/acm/types"
awstypes "github.com/aws/aws-sdk-go-v2/service/acm/types"
"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/enum"
"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"
tftags "github.com/hashicorp/terraform-provider-aws/internal/tags"
tfslices "github.com/hashicorp/terraform-provider-aws/internal/slices"
"github.com/hashicorp/terraform-provider-aws/internal/tfresource"
"github.com/hashicorp/terraform-provider-aws/names"
)
Expand All @@ -43,15 +45,17 @@ func dataSourceCertificate() *schema.Resource {
Computed: true,
},
names.AttrDomain: {
Type: schema.TypeString,
Required: true,
Type: schema.TypeString,
Optional: true,
Computed: true,
AtLeastOneOf: []string{names.AttrDomain, names.AttrTags},
},
"key_types": {
Type: schema.TypeSet,
Optional: true,
Elem: &schema.Schema{
Type: schema.TypeString,
ValidateDiagFunc: enum.Validate[types.KeyAlgorithm](),
ValidateDiagFunc: enum.Validate[awstypes.KeyAlgorithm](),
},
},
names.AttrMostRecent: {
Expand All @@ -68,7 +72,13 @@ func dataSourceCertificate() *schema.Resource {
Optional: true,
Elem: &schema.Schema{Type: schema.TypeString},
},
names.AttrTags: tftags.TagsSchemaComputed(),
names.AttrTags: {
Type: schema.TypeMap,
Optional: true,
Computed: true,
Elem: &schema.Schema{Type: schema.TypeString},
AtLeastOneOf: []string{names.AttrDomain, names.AttrTags},
},
"types": {
Type: schema.TypeList,
Optional: true,
Expand All @@ -80,115 +90,112 @@ func dataSourceCertificate() *schema.Resource {

func dataSourceCertificateRead(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics {
var diags diag.Diagnostics

conn := meta.(*conns.AWSClient).ACMClient(ctx)
ignoreTagsConfig := meta.(*conns.AWSClient).IgnoreTagsConfig

domain := d.Get(names.AttrDomain).(string)
input := acm.ListCertificatesInput{}
input := &acm.ListCertificatesInput{}

if v, ok := d.GetOk("key_types"); ok && v.(*schema.Set).Len() > 0 {
input.Includes = &types.Filters{
KeyTypes: flex.ExpandStringyValueSet[types.KeyAlgorithm](v.(*schema.Set)),
input.Includes = &awstypes.Filters{
KeyTypes: flex.ExpandStringyValueSet[awstypes.KeyAlgorithm](v.(*schema.Set)),
}
}

if v, ok := d.GetOk("statuses"); ok && len(v.([]interface{})) > 0 {
input.CertificateStatuses = flex.ExpandStringyValueList[types.CertificateStatus](v.([]interface{}))
input.CertificateStatuses = flex.ExpandStringyValueList[awstypes.CertificateStatus](v.([]interface{}))
} else {
input.CertificateStatuses = []types.CertificateStatus{types.CertificateStatusIssued}
input.CertificateStatuses = []awstypes.CertificateStatus{awstypes.CertificateStatusIssued}
}

arns, err := tfresource.RetryGWhenNotFound(ctx, 1*time.Minute,
func() ([]string, error) {
return listCertificates(ctx, conn, &input, domain)
},
)
if tfresource.NotFound(err) {
return sdkdiag.AppendErrorf(diags, "XXX no ACM Certificate matching domain (%s)", domain)
} else if err != nil {
return sdkdiag.AppendErrorf(diags, "reading ACM Certificates: %s", err)
f := tfslices.PredicateTrue[*awstypes.CertificateSummary]()
if domain, ok := d.GetOk(names.AttrDomain); ok {
f = func(v *awstypes.CertificateSummary) bool {
return aws.ToString(v.DomainName) == domain
}
}
if certificateTypes := flex.ExpandStringyValueList[awstypes.CertificateType](d.Get("types").([]interface{})); len(certificateTypes) > 0 {
f = tfslices.PredicateAnd(f, func(v *awstypes.CertificateSummary) bool {
return slices.Contains(certificateTypes, v.Type)
})
}

filterMostRecent := d.Get(names.AttrMostRecent).(bool)
certificateTypes := flex.ExpandStringyValueList[types.CertificateType](d.Get("types").([]interface{}))
const (
timeout = 1 * time.Minute
)
certificateSummaries, err := tfresource.RetryGWhenNotFound(ctx, timeout,
func() ([]awstypes.CertificateSummary, error) {
output, err := findCertificates(ctx, conn, input, f)
switch {
case err != nil:
return nil, err
case len(output) == 0:
return nil, tfresource.NewEmptyResultError(input)
default:
return output, nil
}
},
)

if !filterMostRecent && len(certificateTypes) == 0 && len(arns) > 1 {
return sdkdiag.AppendErrorf(diags, "multiple ACM Certificates matching domain (%s)", domain)
if err != nil {
return sdkdiag.AppendErrorf(diags, "reading ACM Certificates: %s", err)
}

var matchedCertificate *types.CertificateDetail
var certificates []*awstypes.CertificateDetail
for _, certificateSummary := range certificateSummaries {
certificateARN := aws.ToString(certificateSummary.CertificateArn)
certificate, err := findCertificateByARN(ctx, conn, certificateARN)

for _, arn := range arns {
input := &acm.DescribeCertificateInput{
CertificateArn: aws.String(arn),
if tfresource.NotFound(err) {
continue
}

certificate, err := findCertificate(ctx, conn, input)

if err != nil {
return sdkdiag.AppendErrorf(diags, "reading ACM Certificate (%s): %s", arn, err)
return sdkdiag.AppendErrorf(diags, "reading ACM Certificate (%s): %s", certificateARN, err)
}

if len(certificateTypes) > 0 {
for _, certificateType := range certificateTypes {
if certificate.Type == certificateType {
// We do not have a candidate certificate.
if matchedCertificate == nil {
matchedCertificate = certificate

break
}

// At this point, we already have a candidate certificate.
// Check if we are filtering by most recent and update if necessary.
if filterMostRecent {
matchedCertificate, err = mostRecentCertificate(certificate, matchedCertificate)

if err != nil {
return sdkdiag.AppendFromErr(diags, err)
}

break
}
// Now we have multiple candidate certificates and we only allow one certificate.
return sdkdiag.AppendErrorf(diags, "multiple ACM Certificates matching domain (%s)", domain)
}
if tagsToMatch := getTagsIn(ctx); len(tagsToMatch) > 0 {
tags, err := listTags(ctx, conn, certificateARN)

if errs.IsA[*awstypes.ResourceNotFoundException](err) {
continue
}

continue
if err != nil {
return sdkdiag.AppendErrorf(diags, "listing tags for ACM Certificate (%s): %s", certificateARN, err)
}

if !tags.ContainsAll(KeyValueTags(ctx, tagsToMatch)) {
continue
}
}

// We do not have a candidate certificate.
if matchedCertificate == nil {
matchedCertificate = certificate
certificates = append(certificates, certificate)
}

continue
}
if len(certificates) == 0 {
return sdkdiag.AppendErrorf(diags, "no matching ACM Certificate found")
}

// At this point, we already have a candidate certificate.
// Check if we are filtering by most recent and update if necessary.
if filterMostRecent {
var matchedCertificate *awstypes.CertificateDetail
if d.Get(names.AttrMostRecent).(bool) {
matchedCertificate = certificates[0]

for _, certificate := range certificates {
matchedCertificate, err = mostRecentCertificate(certificate, matchedCertificate)

if err != nil {
return sdkdiag.AppendFromErr(diags, err)
}

continue
}

// Now we have multiple candidate certificates and we only allow one certificate.
return sdkdiag.AppendErrorf(diags, "multiple ACM Certificates matching domain (%s)", domain)
}

if matchedCertificate == nil {
return sdkdiag.AppendErrorf(diags, "YYY no ACM Certificate matching domain (%s)", domain)
} else if n := len(certificates); n > 1 {
return sdkdiag.AppendErrorf(diags, "%d matching ACM Certificates found", n)
} else {
matchedCertificate = certificates[0]
}

// Get the certificate data if the status is issued
var output *acm.GetCertificateOutput
if matchedCertificate.Status == types.CertificateStatusIssued {
if matchedCertificate.Status == awstypes.CertificateStatusIssued {
arn := aws.ToString(matchedCertificate.CertificateArn)
input := &acm.GetCertificateInput{
CertificateArn: aws.String(arn),
Expand All @@ -211,6 +218,7 @@ func dataSourceCertificateRead(ctx context.Context, d *schema.ResourceData, meta

d.SetId(aws.ToString(matchedCertificate.CertificateArn))
d.Set(names.AttrARN, matchedCertificate.CertificateArn)
d.Set(names.AttrDomain, matchedCertificate.DomainName)
d.Set(names.AttrStatus, matchedCertificate.Status)

tags, err := listTags(ctx, conn, aws.ToString(matchedCertificate.CertificateArn))
Expand All @@ -226,12 +234,12 @@ func dataSourceCertificateRead(ctx context.Context, d *schema.ResourceData, meta
return diags
}

func mostRecentCertificate(i, j *types.CertificateDetail) (*types.CertificateDetail, error) {
func mostRecentCertificate(i, j *awstypes.CertificateDetail) (*awstypes.CertificateDetail, error) {
if i.Status != j.Status {
return nil, fmt.Errorf("most_recent filtering on different ACM certificate statues is not supported")
return nil, fmt.Errorf("most_recent filtering on different ACM certificate statuses is not supported")
}
// Cover IMPORTED and ISSUED AMAZON_ISSUED certificates
if i.Status == types.CertificateStatusIssued {
if i.Status == awstypes.CertificateStatusIssued {
if aws.ToTime(i.NotBefore).After(aws.ToTime(j.NotBefore)) {
return i, nil
}
Expand All @@ -244,27 +252,23 @@ func mostRecentCertificate(i, j *types.CertificateDetail) (*types.CertificateDet
return j, nil
}

func listCertificates(ctx context.Context, conn *acm.Client, input *acm.ListCertificatesInput, domain string) ([]string, error) {
var result []string
func findCertificates(ctx context.Context, conn *acm.Client, input *acm.ListCertificatesInput, filter tfslices.Predicate[*awstypes.CertificateSummary]) ([]awstypes.CertificateSummary, error) {
var output []awstypes.CertificateSummary

pages := acm.NewListCertificatesPaginator(conn, input)
for pages.HasMorePages() {
page, err := pages.NextPage(ctx)

if err != nil {
return []string{}, err
return nil, err
}

for _, v := range page.CertificateSummaryList {
if aws.ToString(v.DomainName) == domain {
result = append(result, aws.ToString(v.CertificateArn))
if filter(&v) {
output = append(output, v)
}
}
}

if len(result) == 0 {
return []string{}, tfresource.NewEmptyResultError(input)
}

return result, nil
return output, nil
}
Loading

0 comments on commit 433cf45

Please sign in to comment.