Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

F aws acm data source tags filter #31453

Merged
merged 41 commits into from
Sep 6, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
41 commits
Select commit Hold shift + click to select a range
02478e1
feat: using certificate tags as option to get certificates
rromic May 16, 2023
f0c574d
feat: update docs to use tags within acm data source
rromic May 16, 2023
9a080cb
feat: support for filtering by tags
rromic May 17, 2023
61df065
chore: comment cleanups
rromic May 17, 2023
c1557c6
Update acm_certificate.html.markdown
rromic Sep 30, 2023
87cc69c
fix: linter issues
rromic Oct 1, 2023
46544fa
fix: terrafmt linter findings
rromic Oct 1, 2023
c42ff88
fix: semgrep issues
rromic Oct 1, 2023
d480085
fix: providerlint issue
rromic Oct 1, 2023
1e2c46e
fix: AppendErrorF method
rromic Dec 3, 2023
9adf4ce
fix: use getOk method
rromic May 19, 2024
5167aac
fix: use names types
rromic May 19, 2024
9bcd16a
Revert "fix: use names types"
ewbankkit Aug 20, 2024
c149738
Revert "fix: use getOk method"
ewbankkit Aug 20, 2024
c601d93
Revert "fix: AppendErrorF method"
ewbankkit Aug 20, 2024
58a0676
Revert "fix: providerlint issue"
ewbankkit Aug 20, 2024
03d7054
Revert "fix: semgrep issues"
ewbankkit Aug 20, 2024
fa58b4c
Revert "fix: terrafmt linter findings"
ewbankkit Aug 20, 2024
9375262
Revert "fix: linter issues"
ewbankkit Aug 20, 2024
0da50d0
Revert "Update acm_certificate.html.markdown"
ewbankkit Aug 20, 2024
ed38df3
Revert "chore: comment cleanups"
ewbankkit Aug 20, 2024
e152a9c
Revert "feat: support for filtering by tags"
ewbankkit Aug 20, 2024
0304265
Revert "feat: update docs to use tags within acm data source"
ewbankkit Aug 20, 2024
4eec266
Revert "feat: using certificate tags as option to get certificates"
ewbankkit Aug 20, 2024
1999c78
Merge branch 'main' into HEAD
ewbankkit Aug 20, 2024
64c6fd1
Add CHANGELOG entry.
ewbankkit Aug 20, 2024
128d000
d/aws_acm_certificate: Start to add 'tags' filter.
ewbankkit Aug 23, 2024
8001452
Merge branch 'main' into HEAD
ewbankkit Sep 4, 2024
d7032b7
d/aws_acm_certificate: Complete filtering by tags.
ewbankkit Sep 4, 2024
a965aa0
Fix 'TestAccACMCertificateDataSource_noMatchReturnsError'.
ewbankkit Sep 4, 2024
923afe7
Fix 'TestAccACMCertificateDataSource_multipleIssued'.
ewbankkit Sep 4, 2024
c7094a8
Add 'TestAccACMCertificateDataSource_byTags' and 'TestAccACMCertifica…
ewbankkit Sep 5, 2024
9d7478c
Add 'TestAccACMCertificateDataSource_byDomain'.
ewbankkit Sep 5, 2024
deabe9d
Add 'TestAccACMCertificateDataSource_byDomainNoMatch'.
ewbankkit Sep 5, 2024
3ffb0a8
Add 'TestAccACMCertificateDataSource_byDomainAndStatuses'.
ewbankkit Sep 5, 2024
37305e3
Add 'TestAccACMCertificateDataSource_byDomainAndKeyTypes' and 'TestAc…
ewbankkit Sep 5, 2024
efd2fa2
Add 'TestAccACMCertificateDataSource_byDomainAndTypes'.
ewbankkit Sep 6, 2024
a31d3f7
Add 'TestAccACMCertificateDataSource_byDomainAndKeyTypesMostRecent'.
ewbankkit Sep 6, 2024
5b5a60a
d/aws_acm_certificate: Remove legacy acceptance tests.
ewbankkit Sep 6, 2024
3c9c649
Run 'make fix-constants PKG=acm'.
ewbankkit Sep 6, 2024
6922f7c
d/aws_acm_certificate: Tidy up.
ewbankkit Sep 6, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
Loading