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

Implement aws_acm_certificate flag to query for most recent certificate #1837

118 changes: 91 additions & 27 deletions aws/data_source_aws_acm_certificate.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,6 @@ import (

"github.com/aws/aws-sdk-go/aws"
"github.com/aws/aws-sdk-go/service/acm"
"github.com/hashicorp/errwrap"
"github.com/hashicorp/terraform/helper/schema"
)

Expand All @@ -33,6 +32,11 @@ func dataSourceAwsAcmCertificate() *schema.Resource {
Optional: true,
Elem: &schema.Schema{Type: schema.TypeString},
},
"most_recent": {
Type: schema.TypeBool,
Optional: true,
Default: false,
},
},
}
}
Expand All @@ -50,55 +54,115 @@ func dataSourceAwsAcmCertificateRead(d *schema.ResourceData, meta interface{}) e
params.CertificateStatuses = []*string{aws.String("ISSUED")}
}

var arns []string
var arns []*string
log.Printf("[DEBUG] Reading ACM Certificate: %s", params)
err := conn.ListCertificatesPages(params, func(page *acm.ListCertificatesOutput, lastPage bool) bool {
for _, cert := range page.CertificateSummaryList {
if *cert.DomainName == target {
arns = append(arns, *cert.CertificateArn)
arns = append(arns, cert.CertificateArn)
}
}

return true
})
if err != nil {
return errwrap.Wrapf("Error describing certificates: {{err}}", err)
return fmt.Errorf("Error listing certificates: %q", err)
}

// filter based on certificate type (imported or aws-issued)
types, ok := d.GetOk("types")
if ok {
typesStrings := expandStringList(types.([]interface{}))
var matchedArns []string
for _, arn := range arns {
params := &acm.DescribeCertificateInput{}
params.CertificateArn = &arn
if len(arns) == 0 {
return fmt.Errorf("No certificate for domain %q found in this region", target)
}

description, err := conn.DescribeCertificate(params)
if err != nil {
return errwrap.Wrapf("Error describing certificates: {{err}}", err)
}
filterMostRecent := d.Get("most_recent").(bool)
filterTypes, filterTypesOk := d.GetOk("types")

var matchedCertificate *acm.CertificateDetail

if !filterMostRecent && !filterTypesOk && len(arns) > 1 {
// Multiple certificates have been found and no additional filtering set
return fmt.Errorf("Multiple certificates for domain %q found in this region", target)
}

typesStrings := expandStringList(filterTypes.([]interface{}))

for _, arn := range arns {
var err error

input := &acm.DescribeCertificateInput{
CertificateArn: aws.String(*arn),
}
log.Printf("[DEBUG] Describing ACM Certificate: %s", input)
output, err := conn.DescribeCertificate(input)
if err != nil {
return fmt.Errorf("Error describing ACM certificate: %q", err)
}
certificate := output.Certificate

if filterTypesOk {
for _, certType := range typesStrings {
if *description.Certificate.Type == *certType {
matchedArns = append(matchedArns, arn)
break
if *certificate.Type == *certType {
// 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 = mostRecentAcmCertificate(certificate, matchedCertificate)
if err != nil {
return err
}
break
}
// Now we have multiple candidate certificates and we only allow one certificate
return fmt.Errorf("Multiple certificates for domain %q found in this region", target)
}
}
continue
}

arns = matchedArns
// We do not have a candidate certificate
if matchedCertificate == nil {
matchedCertificate = certificate
continue
}
// 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 = mostRecentAcmCertificate(certificate, matchedCertificate)
if err != nil {
return err
}
continue
}
// Now we have multiple candidate certificates and we only allow one certificate
return fmt.Errorf("Multiple certificates for domain %q found in this region", target)
}

if len(arns) == 0 {
return fmt.Errorf("No certificate for domain %q found in this region.", target)
}
if len(arns) > 1 {
return fmt.Errorf("Multiple certificates for domain %q found in this region.", target)
if matchedCertificate == nil {
return fmt.Errorf("No certificate for domain %q found in this region", target)
}

d.SetId(time.Now().UTC().String())
d.Set("arn", arns[0])
d.Set("arn", matchedCertificate.CertificateArn)

return nil
}

func mostRecentAcmCertificate(i, j *acm.CertificateDetail) (*acm.CertificateDetail, error) {
if *i.Status != *j.Status {
return nil, fmt.Errorf("most_recent filtering on different ACM certificate statues is not supported")
}
// Cover IMPORTED and ISSUED AMAZON_ISSUED certificates
if *i.Status == acm.CertificateStatusIssued {
if (*i.NotBefore).After(*j.NotBefore) {
return i, nil
}
return j, nil
}
// Cover non-ISSUED AMAZON_ISSUED certificates
if (*i.CreatedAt).After(*j.CreatedAt) {
return i, nil
}
return j, nil
}
198 changes: 186 additions & 12 deletions aws/data_source_aws_acm_certificate_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,30 +2,175 @@ package aws

import (
"fmt"
"os"
"regexp"
"testing"

"github.com/aws/aws-sdk-go/service/acm"
"github.com/hashicorp/terraform/helper/resource"
)

func TestAccAwsAcmCertificateDataSource_noMatchReturnsError(t *testing.T) {
domain := "hashicorp.com"
const ACMCertificateRe = `^arn:[^:]+:acm:[^:]+:[^:]+:certificate/.+$`

func TestAccAwsAcmCertificateDataSource_singleIssued(t *testing.T) {
if os.Getenv("ACM_CERTIFICATE_ROOT_DOMAIN") == "" {
t.Skip("Environment variable ACM_CERTIFICATE_ROOT_DOMAIN is not set")
}

var arnRe *regexp.Regexp
var domain string

if os.Getenv("ACM_CERTIFICATE_SINGLE_ISSUED_MOST_RECENT_ARN") != "" {
arnRe = regexp.MustCompile(fmt.Sprintf("^%s$", os.Getenv("ACM_CERTIFICATE_SINGLE_ISSUED_MOST_RECENT_ARN")))
} else {
arnRe = regexp.MustCompile(ACMCertificateRe)
}

if os.Getenv("ACM_CERTIFICATE_SINGLE_ISSUED_DOMAIN") != "" {
domain = os.Getenv("ACM_CERTIFICATE_SINGLE_ISSUED_DOMAIN")
} else {
domain = fmt.Sprintf("tf-acc-single-issued.%s", os.Getenv("ACM_CERTIFICATE_ROOT_DOMAIN"))
}

resourceName := "data.aws_acm_certificate.test"

resource.Test(t, resource.TestCase{
PreCheck: func() {
testAccPreCheck(t)
PreCheck: func() { testAccPreCheck(t) },
Providers: testAccProviders,
Steps: []resource.TestStep{
{
Config: testAccCheckAwsAcmCertificateDataSourceConfig(domain),
Check: resource.ComposeTestCheckFunc(
resource.TestMatchResourceAttr(resourceName, "arn", arnRe),
),
},
{
Config: testAccCheckAwsAcmCertificateDataSourceConfigWithStatus(domain, acm.CertificateStatusIssued),
Check: resource.ComposeTestCheckFunc(
resource.TestMatchResourceAttr(resourceName, "arn", arnRe),
),
},
{
Config: testAccCheckAwsAcmCertificateDataSourceConfigWithTypes(domain, acm.CertificateTypeAmazonIssued),
Check: resource.ComposeTestCheckFunc(
resource.TestMatchResourceAttr(resourceName, "arn", arnRe),
),
},
{
Config: testAccCheckAwsAcmCertificateDataSourceConfigWithMostRecent(domain, true),
Check: resource.ComposeTestCheckFunc(
resource.TestMatchResourceAttr(resourceName, "arn", arnRe),
),
},
{
Config: testAccCheckAwsAcmCertificateDataSourceConfigWithMostRecentAndStatus(domain, acm.CertificateStatusIssued, true),
Check: resource.ComposeTestCheckFunc(
resource.TestMatchResourceAttr(resourceName, "arn", arnRe),
),
},
{
Config: testAccCheckAwsAcmCertificateDataSourceConfigWithMostRecentAndTypes(domain, acm.CertificateTypeAmazonIssued, true),
Check: resource.ComposeTestCheckFunc(
resource.TestMatchResourceAttr(resourceName, "arn", arnRe),
),
},
},
})
}

func TestAccAwsAcmCertificateDataSource_multipleIssued(t *testing.T) {
if os.Getenv("ACM_CERTIFICATE_ROOT_DOMAIN") == "" {
t.Skip("Environment variable ACM_CERTIFICATE_ROOT_DOMAIN is not set")
}

var arnRe *regexp.Regexp
var domain string

if os.Getenv("ACM_CERTIFICATE_MULTIPLE_ISSUED_MOST_RECENT_ARN") != "" {
arnRe = regexp.MustCompile(fmt.Sprintf("^%s$", os.Getenv("ACM_CERTIFICATE_MULTIPLE_ISSUED_MOST_RECENT_ARN")))
} else {
arnRe = regexp.MustCompile(ACMCertificateRe)
}

if os.Getenv("ACM_CERTIFICATE_MULTIPLE_ISSUED_DOMAIN") != "" {
domain = os.Getenv("ACM_CERTIFICATE_MULTIPLE_ISSUED_DOMAIN")
} else {
domain = fmt.Sprintf("tf-acc-multiple-issued.%s", os.Getenv("ACM_CERTIFICATE_ROOT_DOMAIN"))
}

resourceName := "data.aws_acm_certificate.test"

resource.Test(t, resource.TestCase{
PreCheck: func() { testAccPreCheck(t) },
Providers: testAccProviders,
Steps: []resource.TestStep{
{
Config: testAccCheckAwsAcmCertificateDataSourceConfig(domain),
ExpectError: regexp.MustCompile(`Multiple certificates for domain`),
},
{
Config: testAccCheckAwsAcmCertificateDataSourceConfigWithStatus(domain, acm.CertificateStatusIssued),
ExpectError: regexp.MustCompile(`Multiple certificates for domain`),
},
{
Config: testAccCheckAwsAcmCertificateDataSourceConfigWithTypes(domain, acm.CertificateTypeAmazonIssued),
ExpectError: regexp.MustCompile(`Multiple certificates for domain`),
},
{
Config: testAccCheckAwsAcmCertificateDataSourceConfigWithMostRecent(domain, true),
Check: resource.ComposeTestCheckFunc(
resource.TestMatchResourceAttr(resourceName, "arn", arnRe),
),
},
{
Config: testAccCheckAwsAcmCertificateDataSourceConfigWithMostRecentAndStatus(domain, acm.CertificateStatusIssued, true),
Check: resource.ComposeTestCheckFunc(
resource.TestMatchResourceAttr(resourceName, "arn", arnRe),
),
},
{
Config: testAccCheckAwsAcmCertificateDataSourceConfigWithMostRecentAndTypes(domain, acm.CertificateTypeAmazonIssued, true),
Check: resource.ComposeTestCheckFunc(
resource.TestMatchResourceAttr(resourceName, "arn", arnRe),
),
},
},
})
}

func TestAccAwsAcmCertificateDataSource_noMatchReturnsError(t *testing.T) {
if os.Getenv("ACM_CERTIFICATE_ROOT_DOMAIN") == "" {
t.Skip("Environment variable ACM_CERTIFICATE_ROOT_DOMAIN is not set")
}

domain := fmt.Sprintf("tf-acc-nonexistent.%s", os.Getenv("ACM_CERTIFICATE_ROOT_DOMAIN"))

resource.Test(t, resource.TestCase{
PreCheck: func() { testAccPreCheck(t) },
Providers: testAccProviders,
Steps: []resource.TestStep{
{
Config: testAccCheckAwsAcmCertificateDataSourceConfig(domain),
ExpectError: regexp.MustCompile(`No certificate for domain`),
},
{
Config: testAccCheckAwsAcmCertificateDataSourceConfigWithStatus(domain),
Config: testAccCheckAwsAcmCertificateDataSourceConfigWithStatus(domain, acm.CertificateStatusIssued),
ExpectError: regexp.MustCompile(`No certificate for domain`),
},
{
Config: testAccCheckAwsAcmCertificateDataSourceConfigWithTypes(domain, acm.CertificateTypeAmazonIssued),
ExpectError: regexp.MustCompile(`No certificate for domain`),
},
{
Config: testAccCheckAwsAcmCertificateDataSourceConfigWithMostRecent(domain, true),
ExpectError: regexp.MustCompile(`No certificate for domain`),
},
{
Config: testAccCheckAwsAcmCertificateDataSourceConfigWithMostRecentAndStatus(domain, acm.CertificateStatusIssued, true),
ExpectError: regexp.MustCompile(`No certificate for domain`),
},
{
Config: testAccCheckAwsAcmCertificateDataSourceConfigWithTypes(domain),
Config: testAccCheckAwsAcmCertificateDataSourceConfigWithMostRecentAndTypes(domain, acm.CertificateTypeAmazonIssued, true),
ExpectError: regexp.MustCompile(`No certificate for domain`),
},
},
Expand All @@ -40,20 +185,49 @@ data "aws_acm_certificate" "test" {
`, domain)
}

func testAccCheckAwsAcmCertificateDataSourceConfigWithStatus(domain string) string {
func testAccCheckAwsAcmCertificateDataSourceConfigWithStatus(domain, status string) string {
return fmt.Sprintf(`
data "aws_acm_certificate" "test" {
domain = "%s"
statuses = ["ISSUED"]
statuses = ["%s"]
}
`, domain)
`, domain, status)
}

func testAccCheckAwsAcmCertificateDataSourceConfigWithTypes(domain string) string {
func testAccCheckAwsAcmCertificateDataSourceConfigWithTypes(domain, certType string) string {
return fmt.Sprintf(`
data "aws_acm_certificate" "test" {
domain = "%s"
types = ["IMPORTED"]
types = ["%s"]
}
`, domain)
`, domain, certType)
}

func testAccCheckAwsAcmCertificateDataSourceConfigWithMostRecent(domain string, mostRecent bool) string {
return fmt.Sprintf(`
data "aws_acm_certificate" "test" {
domain = "%s"
most_recent = %v
}
`, domain, mostRecent)
}

func testAccCheckAwsAcmCertificateDataSourceConfigWithMostRecentAndStatus(domain, status string, mostRecent bool) string {
return fmt.Sprintf(`
data "aws_acm_certificate" "test" {
domain = "%s"
statuses = ["%s"]
most_recent = %v
}
`, domain, status, mostRecent)
}

func testAccCheckAwsAcmCertificateDataSourceConfigWithMostRecentAndTypes(domain, certType string, mostRecent bool) string {
return fmt.Sprintf(`
data "aws_acm_certificate" "test" {
domain = "%s"
types = ["%s"]
most_recent = %v
}
`, domain, certType, mostRecent)
}
Loading