From a74249c97d776a620f5792f9d77b1d35ce660b94 Mon Sep 17 00:00:00 2001 From: Florian Sellmayr Date: Sun, 31 Dec 2017 15:36:15 +0100 Subject: [PATCH 01/30] Add basic aws_acm_certificate resource --- aws/provider.go | 1 + aws/resource_aws_acm_certificate.go | 190 +++++++++++++++++++++++ aws/resource_aws_acm_certificate_test.go | 130 ++++++++++++++++ 3 files changed, 321 insertions(+) create mode 100644 aws/resource_aws_acm_certificate.go create mode 100644 aws/resource_aws_acm_certificate_test.go diff --git a/aws/provider.go b/aws/provider.go index e4c80bfccebf..28620f857175 100644 --- a/aws/provider.go +++ b/aws/provider.go @@ -237,6 +237,7 @@ func Provider() terraform.ResourceProvider { }, ResourcesMap: map[string]*schema.Resource{ + "aws_acm_certificate": resourceAwsAcmCertificate(), "aws_ami": resourceAwsAmi(), "aws_ami_copy": resourceAwsAmiCopy(), "aws_ami_from_instance": resourceAwsAmiFromInstance(), diff --git a/aws/resource_aws_acm_certificate.go b/aws/resource_aws_acm_certificate.go new file mode 100644 index 000000000000..0afc0a058cef --- /dev/null +++ b/aws/resource_aws_acm_certificate.go @@ -0,0 +1,190 @@ +package aws + +import ( + "fmt" + "github.com/aws/aws-sdk-go/aws" + "github.com/aws/aws-sdk-go/service/acm" + "github.com/hashicorp/terraform/helper/resource" + "github.com/hashicorp/terraform/helper/schema" + "log" + "time" +) + +func resourceAwsAcmCertificate() *schema.Resource { + return &schema.Resource{ + Create: resourceAwsAcmCertificateCreate, + Read: resourceAwsAcmCertificateRead, + Delete: resourceAwsAcmCertificateDelete, + + Schema: map[string]*schema.Schema{ + "domain_name": &schema.Schema{ + Type: schema.TypeString, + Required: true, + ForceNew: true, + }, + + "subject_alternative_names": &schema.Schema{ + Type: schema.TypeList, + Optional: true, + ForceNew: true, + Elem: &schema.Schema{Type: schema.TypeString}, + }, + "validation_method": &schema.Schema{ + Type: schema.TypeString, + Required: true, + ForceNew: true, + }, + "certificate_arn": &schema.Schema{ + Type: schema.TypeString, + Computed: true, + ForceNew: true, + }, + "domain_validation_options": &schema.Schema{ + Type: schema.TypeList, + Computed: true, + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "domain_name": &schema.Schema{ + Type: schema.TypeString, + Computed: true, + }, + "resource_record_name": &schema.Schema{ + Type: schema.TypeString, + Computed: true, + }, + "resource_record_type": &schema.Schema{ + Type: schema.TypeString, + Computed: true, + }, + "resource_record_value": &schema.Schema{ + Type: schema.TypeString, + Computed: true, + }, + }, + }, + }, + }, + } +} + +func resourceAwsAcmCertificateCreate(d *schema.ResourceData, meta interface{}) error { + acmconn := meta.(*AWSClient).acmconn + params := &acm.RequestCertificateInput{ + DomainName: aws.String(d.Get("domain_name").(string)), + ValidationMethod: aws.String("DNS"), + } + + // TODO: check that validation method is DNS, nothing else supported at the moment + + sans, ok := d.GetOk("subject_alternative_names") + if ok { + sanStrings := sans.([]interface{}) + params.SubjectAlternativeNames = expandStringList(sanStrings) + } + + log.Printf("[DEBUG] ACM Certificate Request: %#v", params) + resp, err := acmconn.RequestCertificate(params) + + if err != nil { + return fmt.Errorf("Error requesting certificate: %s", err) + } + + d.SetId(*resp.CertificateArn) + d.Set("certificate_arn", *resp.CertificateArn) + + return resourceAwsAcmCertificateRead(d, meta) +} + +func resourceAwsAcmCertificateRead(d *schema.ResourceData, meta interface{}) error { + acmconn := meta.(*AWSClient).acmconn + + params := &acm.DescribeCertificateInput{ + CertificateArn: aws.String(d.Id()), + } + + return resource.Retry(time.Duration(1)*time.Minute, func() *resource.RetryError { + resp, err := acmconn.DescribeCertificate(params) + + if err != nil { + return resource.NonRetryableError(fmt.Errorf("Error describing certificate: %s", err)) + } + + if err := d.Set("domain_name", resp.Certificate.DomainName); err != nil { + return resource.NonRetryableError(err) + } + if err := d.Set("subject_alternative_names", cleanUpSubjectAlternativeNames(resp.Certificate)); err != nil { + return resource.NonRetryableError(err) + } + + domainValidationOptions, err := convertDomainValidationOptions(resp.Certificate.DomainValidationOptions) + + if err != nil { + return resource.RetryableError(err) + } + + if err := d.Set("domain_validation_options", domainValidationOptions); err != nil { + return resource.NonRetryableError(err) + } + + return nil + }) + +} + +func cleanUpSubjectAlternativeNames(cert *acm.CertificateDetail) []string { + sans := cert.SubjectAlternativeNames + vs := make([]string, 0, len(sans)-1) + for _, v := range sans { + if *v != *cert.DomainName { + vs = append(vs, *v) + } + } + return vs + +} + +func convertDomainValidationOptions(validations []*acm.DomainValidation) ([]map[string]interface{}, error) { + result := make([]map[string]interface{}, 0, len(validations)) + + for _, o := range validations { + validationOption := make(map[string]interface{}) + validationOption["domain_name"] = *o.DomainName + if o.ResourceRecord != nil { + validationOption["resource_record_name"] = *o.ResourceRecord.Name + validationOption["resource_record_type"] = *o.ResourceRecord.Type + validationOption["resource_record_value"] = *o.ResourceRecord.Value + } else { + log.Printf("[DEBUG] No resource record found in validation options, need to retry: %#v", o) + return nil, fmt.Errorf("No resource record found in DNS DomainValidationOptions: %v", o) + } + + result = append(result, validationOption) + } + + return result, nil +} + +func resourceAwsAcmCertificateDelete(d *schema.ResourceData, meta interface{}) error { + acmconn := meta.(*AWSClient).acmconn + + if err := resourceAwsAcmCertificateRead(d, meta); err != nil { + return err + } + if d.Id() == "" { + // This might happen from the read + return nil + } + + params := &acm.DeleteCertificateInput{ + CertificateArn: aws.String(d.Id()), + } + + _, err := acmconn.DeleteCertificate(params) + + if err != nil { + return fmt.Errorf("Error deleting certificate: %s", err) + } + + d.SetId("") + return nil +} diff --git a/aws/resource_aws_acm_certificate_test.go b/aws/resource_aws_acm_certificate_test.go new file mode 100644 index 000000000000..888b0af44eb6 --- /dev/null +++ b/aws/resource_aws_acm_certificate_test.go @@ -0,0 +1,130 @@ +package aws + +import ( + "fmt" + "testing" + + "github.com/aws/aws-sdk-go/aws" + "github.com/aws/aws-sdk-go/aws/awserr" + "github.com/aws/aws-sdk-go/service/acm" + "github.com/hashicorp/terraform/helper/resource" + "github.com/hashicorp/terraform/terraform" +) + +func TestAccAwsAcmResource_certificateIssuingFlow(t *testing.T) { + var conf acm.DescribeCertificateOutput + + domain := "certtest.sandbox.sellmayr.net" + + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + Providers: testAccProviders, + CheckDestroy: testAccCheckAcmCertificateDestroy, + Steps: []resource.TestStep{ + resource.TestStep{ + Config: testAccAcmCertificateConfig(domain), + Check: resource.ComposeTestCheckFunc( + testAccCheckAcmCertificateExists("aws_acm_certificate.cert", &conf), + testAccCheckAcmCertificateAttributes("aws_acm_certificate.cert", &conf, domain, "PENDING_VALIDATION"), + ), + }, + }, + }) +} + +func testAccAcmCertificateConfig(domain string) string { + return fmt.Sprintf(` +resource "aws_acm_certificate" "cert" { + domain_name = "%s" + validation_method = "DNS" +} +`, domain) +} + +func testAccCheckAcmCertificateExists(n string, res *acm.DescribeCertificateOutput) 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 fmt.Errorf("No id is set") + } + + if rs.Primary.Attributes["certificate_arn"] == "" { + return fmt.Errorf("No certificate_arn is set") + } + + if rs.Primary.Attributes["certificate_arn"] != rs.Primary.ID { + return fmt.Errorf("No certificate_arn and ID are different: %s %s", rs.Primary.Attributes["certificate_arn"], rs.Primary.ID) + } + + acmconn := testAccProvider.Meta().(*AWSClient).acmconn + + resp, err := acmconn.DescribeCertificate(&acm.DescribeCertificateInput{ + CertificateArn: aws.String(rs.Primary.Attributes["certificate_arn"]), + }) + + if err != nil { + return err + } + + *res = *resp + + return nil + } +} + +func testAccCheckAcmCertificateAttributes(n string, cert *acm.DescribeCertificateOutput, domain string, status string) resource.TestCheckFunc { + return func(s *terraform.State) error { + rs, ok := s.RootModule().Resources[n] + if !ok { + return fmt.Errorf("Not found: %s", n) + } + attrs := rs.Primary.Attributes + + if *cert.Certificate.DomainName != domain { + return fmt.Errorf("Domain name is %s but expected %s", *cert.Certificate.DomainName, domain) + } + if *cert.Certificate.Status != status { + return fmt.Errorf("Status is %s but expected %s", *cert.Certificate.Status, status) + } + if attrs["domain_name"] != domain { + return fmt.Errorf("Domain name in state is %s but expected %s", attrs["domain_name"], domain) + } + + // TODO: check other attributes? + + return nil + } +} + +func testAccCheckAcmCertificateDestroy(s *terraform.State) error { + acmconn := testAccProvider.Meta().(*AWSClient).acmconn + + for _, rs := range s.RootModule().Resources { + if rs.Type != "aws_acm_certificate" { + continue + } + _, err := acmconn.DescribeCertificate(&acm.DescribeCertificateInput{ + CertificateArn: aws.String(rs.Primary.ID), + }) + + if err == nil { + return fmt.Errorf("Certificate still exists.") + } + + // Verify the error is what we want + acmerr, ok := err.(awserr.Error) + + if !ok { + return err + } + if acmerr.Code() != "ResourceNotFoundException" { + return err + } + } + + return nil +} From 4e49e55dd7fdff91e720e7391bc7e46bd900d25a Mon Sep 17 00:00:00 2001 From: Florian Sellmayr Date: Sun, 31 Dec 2017 16:13:13 +0100 Subject: [PATCH 02/30] Add basic aws_acm_certificate_validation resource that waits until a certificate can be validated and is issued --- aws/provider.go | 1 + aws/resource_aws_acm_certificate_test.go | 75 ++++++++++++++ ...resource_aws_acm_certificate_validation.go | 98 +++++++++++++++++++ 3 files changed, 174 insertions(+) create mode 100644 aws/resource_aws_acm_certificate_validation.go diff --git a/aws/provider.go b/aws/provider.go index 28620f857175..37b60fef3075 100644 --- a/aws/provider.go +++ b/aws/provider.go @@ -238,6 +238,7 @@ func Provider() terraform.ResourceProvider { ResourcesMap: map[string]*schema.Resource{ "aws_acm_certificate": resourceAwsAcmCertificate(), + "aws_acm_certificate_validation": resourceAwsAcmCertificateValidation(), "aws_ami": resourceAwsAmi(), "aws_ami_copy": resourceAwsAmiCopy(), "aws_ami_from_instance": resourceAwsAmiFromInstance(), diff --git a/aws/resource_aws_acm_certificate_test.go b/aws/resource_aws_acm_certificate_test.go index 888b0af44eb6..cb340b3ef602 100644 --- a/aws/resource_aws_acm_certificate_test.go +++ b/aws/resource_aws_acm_certificate_test.go @@ -9,11 +9,14 @@ import ( "github.com/aws/aws-sdk-go/service/acm" "github.com/hashicorp/terraform/helper/resource" "github.com/hashicorp/terraform/terraform" + "regexp" ) func TestAccAwsAcmResource_certificateIssuingFlow(t *testing.T) { var conf acm.DescribeCertificateOutput + var confAfterValidation acm.DescribeCertificateOutput + root_zone_domain := "sandbox.sellmayr.net" domain := "certtest.sandbox.sellmayr.net" resource.Test(t, resource.TestCase{ @@ -21,6 +24,7 @@ func TestAccAwsAcmResource_certificateIssuingFlow(t *testing.T) { Providers: testAccProviders, CheckDestroy: testAccCheckAcmCertificateDestroy, Steps: []resource.TestStep{ + // Test that we can request a certificate resource.TestStep{ Config: testAccAcmCertificateConfig(domain), Check: resource.ComposeTestCheckFunc( @@ -28,6 +32,20 @@ func TestAccAwsAcmResource_certificateIssuingFlow(t *testing.T) { testAccCheckAcmCertificateAttributes("aws_acm_certificate.cert", &conf, domain, "PENDING_VALIDATION"), ), }, + // Test that validation times out if certificate can't be validated + resource.TestStep{ + Config: testAccAcmCertificateWithValidationConfig(domain), + ExpectError: regexp.MustCompile("Expected certificate to be issued but was in state PENDING_VALIDATION"), + }, + // Test that validation succeeds once we provide the right DNS validation records + resource.TestStep{ + Config: testAccAcmCertificateWithValidationAndRecordsConfig(root_zone_domain, domain), + Check: resource.ComposeTestCheckFunc( + testAccCheckAcmCertificateExists("aws_acm_certificate.cert", &confAfterValidation), + testAccCheckAcmCertificateAttributes("aws_acm_certificate.cert", &confAfterValidation, domain, "ISSUED"), + testAccCheckAcmCertificateValidationAttributes("aws_acm_certificate_validation.cert", &confAfterValidation), + ), + }, }, }) } @@ -41,6 +59,47 @@ resource "aws_acm_certificate" "cert" { `, domain) } +func testAccAcmCertificateWithValidationConfig(domain string) string { + return fmt.Sprintf(` +resource "aws_acm_certificate" "cert" { + domain_name = "%s" + validation_method = "DNS" +} + +resource "aws_acm_certificate_validation" "cert" { + certificate_arn = "${aws_acm_certificate.cert.certificate_arn}" + timeout = "20s" +} +`, domain) +} + +func testAccAcmCertificateWithValidationAndRecordsConfig(rootZoneDomain string, domain string) string { + return fmt.Sprintf(` +resource "aws_acm_certificate" "cert" { + domain_name = "%s" + validation_method = "DNS" +} + +data "aws_route53_zone" "zone" { + name = "%s." + private_zone = false +} + +resource "aws_route53_record" "cert_validation" { + name = "${aws_acm_certificate.cert.domain_validation_options.0.resource_record_name}" + type = "${aws_acm_certificate.cert.domain_validation_options.0.resource_record_type}" + zone_id = "${data.aws_route53_zone.zone.id}" + records = ["${aws_acm_certificate.cert.domain_validation_options.0.resource_record_value}"] + ttl = 60 +} + +resource "aws_acm_certificate_validation" "cert" { + certificate_arn = "${aws_acm_certificate.cert.certificate_arn}" + validation_record_fqdn = "${aws_route53_record.cert_validation.fqdn}" # This wouldn't strictly be necessary but it can enforce a dependency +} +`, domain, rootZoneDomain) +} + func testAccCheckAcmCertificateExists(n string, res *acm.DescribeCertificateOutput) resource.TestCheckFunc { return func(s *terraform.State) error { rs, ok := s.RootModule().Resources[n] @@ -100,6 +159,22 @@ func testAccCheckAcmCertificateAttributes(n string, cert *acm.DescribeCertificat } } +func testAccCheckAcmCertificateValidationAttributes(n string, cert *acm.DescribeCertificateOutput) resource.TestCheckFunc { + return func(s *terraform.State) error { + rs, ok := s.RootModule().Resources[n] + if !ok { + return fmt.Errorf("Not found: %s.", n) + } + attrs := rs.Primary.Attributes + + if attrs["certificate_arn"] != *cert.Certificate.CertificateArn { + return fmt.Errorf("Certificate ARN in state is %s but expected %s", attrs["arn"], *cert.Certificate.CertificateArn) + } + + return nil + } +} + func testAccCheckAcmCertificateDestroy(s *terraform.State) error { acmconn := testAccProvider.Meta().(*AWSClient).acmconn diff --git a/aws/resource_aws_acm_certificate_validation.go b/aws/resource_aws_acm_certificate_validation.go new file mode 100644 index 000000000000..c85a4a80fbbb --- /dev/null +++ b/aws/resource_aws_acm_certificate_validation.go @@ -0,0 +1,98 @@ +package aws + +import ( + "fmt" + "github.com/aws/aws-sdk-go/aws" + "github.com/aws/aws-sdk-go/service/acm" + "github.com/hashicorp/terraform/helper/resource" + "github.com/hashicorp/terraform/helper/schema" + "log" + "time" +) + +func resourceAwsAcmCertificateValidation() *schema.Resource { + return &schema.Resource{ + Create: resourceAwsAcmCertificateValidationCreate, + Read: resourceAwsAcmCertificateValidationRead, + Delete: resourceAwsAcmCertificateValidationDelete, + + Schema: map[string]*schema.Schema{ + "certificate_arn": { + Type: schema.TypeString, + Required: true, + ForceNew: true, + }, + "validation_record_fqdn": { + Type: schema.TypeString, + Optional: true, + ForceNew: true, + }, + "timeout": { + Type: schema.TypeString, + Optional: true, + ForceNew: true, + Default: "45m", + }, + }, + } +} + +func resourceAwsAcmCertificateValidationCreate(d *schema.ResourceData, meta interface{}) error { + // TODO: check that validation_record_fqdn (if set) matches the domain validation information for cert + // TODO: check that certificate is AMAZON_ISSUED and has Domain Validation enabled (should we also allow to wait for E-Mail validation?) + + timeout, err := time.ParseDuration(d.Get("timeout").(string)) + if err != nil { + return err + } + + return resource.Retry(timeout, func() *resource.RetryError { + acmconn := meta.(*AWSClient).acmconn + + certificate_arn := d.Get("certificate_arn").(string) + params := &acm.DescribeCertificateInput{ + CertificateArn: aws.String(certificate_arn), + } + + resp, err := acmconn.DescribeCertificate(params) + + if err != nil { + return resource.NonRetryableError(fmt.Errorf("Error describing certificate: %s", err)) + } + + if *resp.Certificate.Status != "ISSUED" { + return resource.RetryableError(fmt.Errorf("Expected certificate to be issued but was in state %s", *resp.Certificate.Status)) + } + + log.Printf("[INFO] ACM Certificate validation for %s done, certificate was issued", certificate_arn) + return resource.NonRetryableError(resourceAwsAcmCertificateValidationRead(d, meta)) + }) +} + +func resourceAwsAcmCertificateValidationRead(d *schema.ResourceData, meta interface{}) error { + acmconn := meta.(*AWSClient).acmconn + + params := &acm.DescribeCertificateInput{ + CertificateArn: aws.String(d.Get("certificate_arn").(string)), + } + + resp, err := acmconn.DescribeCertificate(params) + + if err != nil { + return fmt.Errorf("Error describing certificate: %s", err) + } + + if *resp.Certificate.Status != "ISSUED" { + log.Printf("[INFO] Certificate status not issued, was %s, tainting validation", *resp.Certificate.Status) + d.SetId("") + } else { + d.SetId((*resp.Certificate.IssuedAt).String()) + } + return nil +} + +func resourceAwsAcmCertificateValidationDelete(d *schema.ResourceData, meta interface{}) error { + // No need to do anything, certificate will be deleted when acm_certificate is deleted + d.SetId("") + return nil +} From d4ef13aa323d2a895cde6f7e5ce358bbf958ccb4 Mon Sep 17 00:00:00 2001 From: Florian Sellmayr Date: Sat, 13 Jan 2018 18:57:50 +0100 Subject: [PATCH 03/30] Add documentation for aws_acm_certificate and aws_acm_certificate_validation --- website/aws.erb | 12 +++ website/docs/r/acm_certificate.html.markdown | 77 +++++++++++++++++++ .../acm_certificate_validation.html.markdown | 65 ++++++++++++++++ 3 files changed, 154 insertions(+) create mode 100644 website/docs/r/acm_certificate.html.markdown create mode 100644 website/docs/r/acm_certificate_validation.html.markdown diff --git a/website/aws.erb b/website/aws.erb index 5bf9ba7af53f..6c086b8cc134 100644 --- a/website/aws.erb +++ b/website/aws.erb @@ -233,6 +233,18 @@ + > + ACM Resources + + + > API Gateway Resources