diff --git a/aws/provider.go b/aws/provider.go index e4c80bfcceb..37b60fef307 100644 --- a/aws/provider.go +++ b/aws/provider.go @@ -237,6 +237,8 @@ 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.go b/aws/resource_aws_acm_certificate.go new file mode 100644 index 00000000000..4b2c06b3624 --- /dev/null +++ b/aws/resource_aws_acm_certificate.go @@ -0,0 +1,244 @@ +package aws + +import ( + "fmt" + "log" + "time" + + "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" +) + +func resourceAwsAcmCertificate() *schema.Resource { + return &schema.Resource{ + Create: resourceAwsAcmCertificateCreate, + Read: resourceAwsAcmCertificateRead, + Update: resourceAwsAcmCertificateUpdate, + Delete: resourceAwsAcmCertificateDelete, + Importer: &schema.ResourceImporter{ + State: schema.ImportStatePassthrough, + }, + + Schema: map[string]*schema.Schema{ + "domain_name": { + Type: schema.TypeString, + Required: true, + ForceNew: true, + }, + "subject_alternative_names": { + Type: schema.TypeList, + Optional: true, + ForceNew: true, + Elem: &schema.Schema{Type: schema.TypeString}, + }, + "validation_method": { + Type: schema.TypeString, + Required: true, + ForceNew: true, + }, + "arn": { + Type: schema.TypeString, + Computed: true, + }, + "domain_validation_options": { + Type: schema.TypeList, + Computed: true, + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "domain_name": { + Type: schema.TypeString, + Computed: true, + }, + "resource_record_name": { + Type: schema.TypeString, + Computed: true, + }, + "resource_record_type": { + Type: schema.TypeString, + Computed: true, + }, + "resource_record_value": { + Type: schema.TypeString, + Computed: true, + }, + }, + }, + }, + "validation_emails": { + Type: schema.TypeList, + Computed: true, + Elem: &schema.Schema{Type: schema.TypeString}, + }, + "tags": tagsSchema(), + }, + } +} + +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(d.Get("validation_method").(string)), + } + + 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) + if v, ok := d.GetOk("tags"); ok { + params := &acm.AddTagsToCertificateInput{ + CertificateArn: resp.CertificateArn, + Tags: tagsFromMapACM(v.(map[string]interface{})), + } + _, err := acmconn.AddTagsToCertificate(params) + + if err != nil { + return fmt.Errorf("Error requesting certificate: %s", err) + } + } + + 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 { + if isAWSErr(err, acm.ErrCodeResourceNotFoundException, "") { + d.SetId("") + return nil + } + return resource.NonRetryableError(fmt.Errorf("Error describing certificate: %s", err)) + } + + d.Set("domain_name", resp.Certificate.DomainName) + d.Set("arn", resp.Certificate.CertificateArn) + + if err := d.Set("subject_alternative_names", cleanUpSubjectAlternativeNames(resp.Certificate)); err != nil { + return resource.NonRetryableError(err) + } + + domainValidationOptions, emailValidationOptions, err := convertValidationOptions(resp.Certificate) + + if err != nil { + return resource.RetryableError(err) + } + + if err := d.Set("domain_validation_options", domainValidationOptions); err != nil { + return resource.NonRetryableError(err) + } + if err := d.Set("validation_emails", emailValidationOptions); err != nil { + return resource.NonRetryableError(err) + } + d.Set("validation_method", resourceAwsAcmCertificateGuessValidationMethod(domainValidationOptions, emailValidationOptions)) + + params := &acm.ListTagsForCertificateInput{ + CertificateArn: aws.String(d.Id()), + } + + tagResp, err := acmconn.ListTagsForCertificate(params) + if err := d.Set("tags", tagsToMapACM(tagResp.Tags)); err != nil { + return resource.NonRetryableError(err) + } + + return nil + }) +} +func resourceAwsAcmCertificateGuessValidationMethod(domainValidationOptions []map[string]interface{}, emailValidationOptions []string) string { + // The DescribeCertificate Response doesn't have information on what validation method was used + // so we need to guess from the validation options we see... + if len(domainValidationOptions) > 0 { + return acm.ValidationMethodDns + } else if len(emailValidationOptions) > 0 { + return acm.ValidationMethodEmail + } else { + return "NONE" + } +} + +func resourceAwsAcmCertificateUpdate(d *schema.ResourceData, meta interface{}) error { + if d.HasChange("tags") { + acmconn := meta.(*AWSClient).acmconn + err := setTagsACM(acmconn, d) + if err != nil { + return 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 convertValidationOptions(certificate *acm.CertificateDetail) ([]map[string]interface{}, []string, error) { + var domainValidationResult []map[string]interface{} + var emailValidationResult []string + + if *certificate.Type == acm.CertificateTypeAmazonIssued { + for _, o := range certificate.DomainValidationOptions { + if o.ResourceRecord != nil { + validationOption := map[string]interface{}{ + "domain_name": *o.DomainName, + "resource_record_name": *o.ResourceRecord.Name, + "resource_record_type": *o.ResourceRecord.Type, + "resource_record_value": *o.ResourceRecord.Value, + } + domainValidationResult = append(domainValidationResult, validationOption) + } else if o.ValidationEmails != nil && len(o.ValidationEmails) > 0 { + for _, validationEmail := range o.ValidationEmails { + emailValidationResult = append(emailValidationResult, *validationEmail) + } + } else { + log.Printf("[DEBUG] No validation options need to retry: %#v", o) + return nil, nil, fmt.Errorf("No validation options need to retry: %#v", o) + } + } + } + + return domainValidationResult, emailValidationResult, nil +} + +func resourceAwsAcmCertificateDelete(d *schema.ResourceData, meta interface{}) error { + acmconn := meta.(*AWSClient).acmconn + + params := &acm.DeleteCertificateInput{ + CertificateArn: aws.String(d.Id()), + } + + _, err := acmconn.DeleteCertificate(params) + + if err != nil && !isAWSErr(err, acm.ErrCodeResourceNotFoundException, "") { + 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 00000000000..d0b6fb561c7 --- /dev/null +++ b/aws/resource_aws_acm_certificate_test.go @@ -0,0 +1,156 @@ +package aws + +import ( + "fmt" + "testing" + + "os" + "regexp" + + "github.com/aws/aws-sdk-go/aws" + "github.com/aws/aws-sdk-go/service/acm" + "github.com/hashicorp/terraform/helper/acctest" + "github.com/hashicorp/terraform/helper/resource" + "github.com/hashicorp/terraform/terraform" +) + +var certificateArnRegex = regexp.MustCompile(`^arn:aws:acm:[^:]+:[^:]+:certificate/.+$`) + +func TestAccAwsAcmResource_emailValidation(t *testing.T) { + if os.Getenv("ACM_CERTIFICATE_ROOT_DOMAIN") == "" { + t.Skip("Environment variable ACM_CERTIFICATE_ROOT_DOMAIN is not set") + } + + root_zone_domain := os.Getenv("ACM_CERTIFICATE_ROOT_DOMAIN") + + rInt1 := acctest.RandInt() + + domain := fmt.Sprintf("tf-acc-%d.%s", rInt1, root_zone_domain) + + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + Providers: testAccProviders, + CheckDestroy: testAccCheckAcmCertificateDestroy, + Steps: []resource.TestStep{ + // Test that we can request a certificate + resource.TestStep{ + Config: testAccAcmCertificateConfigWithEMailValidation(domain), + Check: resource.ComposeTestCheckFunc( + resource.TestMatchResourceAttr("aws_acm_certificate.cert", "arn", certificateArnRegex), + resource.TestCheckResourceAttr("aws_acm_certificate.cert", "domain_name", domain), + resource.TestCheckResourceAttr("aws_acm_certificate.cert", "subject_alternative_names.#", "0"), + resource.TestMatchResourceAttr("aws_acm_certificate.cert", "validation_emails.0", regexp.MustCompile(`^[^@]+@.+$`)), + ), + }, + resource.TestStep{ + ResourceName: "aws_acm_certificate.cert", + ImportState: true, + ImportStateVerify: true, + }, + }, + }) + +} + +func TestAccAwsAcmResource_dnsValidationAndTagging(t *testing.T) { + if os.Getenv("ACM_CERTIFICATE_ROOT_DOMAIN") == "" { + t.Skip("Environment variable ACM_CERTIFICATE_ROOT_DOMAIN is not set") + } + + root_zone_domain := os.Getenv("ACM_CERTIFICATE_ROOT_DOMAIN") + + rInt1 := acctest.RandInt() + + domain := fmt.Sprintf("tf-acc-%d.%s", rInt1, root_zone_domain) + sanDomain := fmt.Sprintf("tf-acc-%d-san.%s", rInt1, root_zone_domain) + + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + Providers: testAccProviders, + CheckDestroy: testAccCheckAcmCertificateDestroy, + Steps: []resource.TestStep{ + // Test that we can request a certificate + resource.TestStep{ + Config: testAccAcmCertificateConfig( + domain, sanDomain, + "Hello", "World", + "Foo", "Bar"), + Check: resource.ComposeTestCheckFunc( + resource.TestMatchResourceAttr("aws_acm_certificate.cert", "arn", certificateArnRegex), + resource.TestCheckResourceAttr("aws_acm_certificate.cert", "domain_name", domain), + resource.TestCheckResourceAttr("aws_acm_certificate.cert", "subject_alternative_names.#", "1"), + resource.TestCheckResourceAttr("aws_acm_certificate.cert", "subject_alternative_names.0", sanDomain), + resource.TestCheckResourceAttr("aws_acm_certificate.cert", "tags.%", "2"), + resource.TestCheckResourceAttr("aws_acm_certificate.cert", "tags.Hello", "World"), + resource.TestCheckResourceAttr("aws_acm_certificate.cert", "tags.Foo", "Bar"), + ), + }, + // Test that we can change the tags + resource.TestStep{ + Config: testAccAcmCertificateConfig( + domain, sanDomain, + "Environment", "Test", + "Foo", "Baz"), + Check: resource.ComposeTestCheckFunc( + resource.TestCheckResourceAttr("aws_acm_certificate.cert", "tags.%", "2"), + resource.TestCheckResourceAttr("aws_acm_certificate.cert", "tags.Environment", "Test"), + resource.TestCheckResourceAttr("aws_acm_certificate.cert", "tags.Foo", "Baz"), + ), + }, + resource.TestStep{ + ResourceName: "aws_acm_certificate.cert", + ImportState: true, + ImportStateVerify: true, + }, + }, + }) +} + +func testAccAcmCertificateConfigWithEMailValidation(domain string) string { + return fmt.Sprintf(` +resource "aws_acm_certificate" "cert" { + domain_name = "%s" + validation_method = "EMAIL" +} +`, domain) + +} + +func testAccAcmCertificateConfig(domain string, sanDomain string, tag1Key, tag1Value, tag2Key, tag2Value string) string { + return fmt.Sprintf(` +resource "aws_acm_certificate" "cert" { + domain_name = "%s" + validation_method = "DNS" + subject_alternative_names = ["%s"] + + tags { + "%s" = "%s" + "%s" = "%s" + } +} +`, domain, sanDomain, tag1Key, tag1Value, tag2Key, tag2Value) +} + +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 + if !isAWSErr(err, acm.ErrCodeResourceNotFoundException, "") { + return err + } + } + + return nil +} diff --git a/aws/resource_aws_acm_certificate_validation.go b/aws/resource_aws_acm_certificate_validation.go new file mode 100644 index 00000000000..f71367d6135 --- /dev/null +++ b/aws/resource_aws_acm_certificate_validation.go @@ -0,0 +1,142 @@ +package aws + +import ( + "fmt" + "log" + "reflect" + "sort" + "strings" + "time" + + "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" +) + +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_fqdns": { + Type: schema.TypeSet, + Optional: true, + ForceNew: true, + Elem: &schema.Schema{Type: schema.TypeString}, + Set: schema.HashString, + }, + }, + Timeouts: &schema.ResourceTimeout{ + Create: schema.DefaultTimeout(45 * time.Minute), + }, + } +} + +func resourceAwsAcmCertificateValidationCreate(d *schema.ResourceData, meta interface{}) error { + certificate_arn := d.Get("certificate_arn").(string) + + acmconn := meta.(*AWSClient).acmconn + params := &acm.DescribeCertificateInput{ + CertificateArn: aws.String(certificate_arn), + } + + resp, err := acmconn.DescribeCertificate(params) + + if err != nil { + return fmt.Errorf("Error describing certificate: %s", err) + } + + if *resp.Certificate.Type != "AMAZON_ISSUED" { + return fmt.Errorf("Certificate %s has type %s, no validation necessary", *resp.Certificate.CertificateArn, *resp.Certificate.Type) + } + + if validation_record_fqdns, ok := d.GetOk("validation_record_fqdns"); ok { + err := resourceAwsAcmCertificateCheckValidationRecords(validation_record_fqdns.(*schema.Set).List(), resp.Certificate) + if err != nil { + return err + } + } else { + log.Printf("[INFO] No validation_record_fqdns set, skipping check") + } + + return resource.Retry(d.Timeout(schema.TimeoutCreate), func() *resource.RetryError { + 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 resourceAwsAcmCertificateCheckValidationRecords(validation_record_fqdns []interface{}, cert *acm.CertificateDetail) error { + expected_fqdns := make([]string, len(cert.DomainValidationOptions)) + for i, v := range cert.DomainValidationOptions { + if *v.ValidationMethod == acm.ValidationMethodDns { + expected_fqdns[i] = strings.TrimSuffix(*v.ResourceRecord.Name, ".") + } + } + + actual_validation_record_fqdns := make([]string, 0, len(validation_record_fqdns)) + + for _, v := range validation_record_fqdns { + val := v.(string) + actual_validation_record_fqdns = append(actual_validation_record_fqdns, strings.TrimSuffix(val, ".")) + } + + sort.Strings(expected_fqdns) + sort.Strings(actual_validation_record_fqdns) + + log.Printf("[DEBUG] Checking validation_record_fqdns. Expected: %v, Actual: %v", expected_fqdns, actual_validation_record_fqdns) + + if !reflect.DeepEqual(expected_fqdns, actual_validation_record_fqdns) { + return fmt.Errorf("Certificate needs %v to be set but only %v was passed to validation_record_fqdns", expected_fqdns, actual_validation_record_fqdns) + } + + return nil +} + +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 && isAWSErr(err, acm.ErrCodeResourceNotFoundException, "") { + d.SetId("") + return nil + } else 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 +} diff --git a/aws/resource_aws_acm_certificate_validation_test.go b/aws/resource_aws_acm_certificate_validation_test.go new file mode 100644 index 00000000000..2a2f17113a7 --- /dev/null +++ b/aws/resource_aws_acm_certificate_validation_test.go @@ -0,0 +1,128 @@ +package aws + +import ( + "fmt" + "testing" + + "os" + "regexp" + + "github.com/hashicorp/terraform/helper/acctest" + "github.com/hashicorp/terraform/helper/resource" +) + +func TestAccAwsAcmResource_certificateIssuingAndValidationFlow(t *testing.T) { + if os.Getenv("ACM_CERTIFICATE_ROOT_DOMAIN") == "" { + t.Skip("Environment variable ACM_CERTIFICATE_ROOT_DOMAIN is not set") + } + + root_zone_domain := os.Getenv("ACM_CERTIFICATE_ROOT_DOMAIN") + + rInt1 := acctest.RandInt() + + domain := fmt.Sprintf("tf-acc-%d.%s", rInt1, root_zone_domain) + sanDomain := fmt.Sprintf("tf-acc-%d-san.%s", rInt1, root_zone_domain) + + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + Providers: testAccProviders, + CheckDestroy: testAccCheckAcmCertificateDestroy, + Steps: []resource.TestStep{ + // Test that validation times out if certificate can't be validated + resource.TestStep{ + Config: testAccAcmCertificateWithValidationConfig(domain, sanDomain), + ExpectError: regexp.MustCompile("Expected certificate to be issued but was in state PENDING_VALIDATION"), + }, + // Test that validation fails if given validation_fqdns don't match + resource.TestStep{ + Config: testAccAcmCertificateWithValidationConfigAndWrongFQDN(domain, sanDomain), + ExpectError: regexp.MustCompile("Certificate needs .* to be set but only .* was passed to validation_record_fqdns"), + }, + // Test that validation succeeds once we provide the right DNS validation records + resource.TestStep{ + Config: testAccAcmCertificateWithValidationAndRecordsConfig(root_zone_domain, domain, sanDomain), + Check: resource.ComposeTestCheckFunc( + resource.TestMatchResourceAttr("aws_acm_certificate_validation.cert", "certificate_arn", certificateArnRegex), + ), + }, + // Test that we can import a validated certificate + resource.TestStep{ + ResourceName: "aws_acm_certificate.cert", + ImportState: true, + ImportStateVerify: true, + }, + }, + }) +} + +func testAccAcmCertificateWithValidationConfig(domain string, sanDomain string) string { + return fmt.Sprintf(` +%s + +resource "aws_acm_certificate_validation" "cert" { + certificate_arn = "${aws_acm_certificate.cert.arn}" + timeouts { + create = "20s" + } +} +`, testAccAcmCertificateConfig( + domain, sanDomain, + "Environment", "Test", + "Foo", "Baz")) +} + +func testAccAcmCertificateWithValidationConfigAndWrongFQDN(domain string, sanDomain string) string { + return fmt.Sprintf(` +%s + +resource "aws_acm_certificate_validation" "cert" { + certificate_arn = "${aws_acm_certificate.cert.arn}" + validation_record_fqdns = ["some-wrong-fqdn.example.com"] + timeouts { + create = "20s" + } +} +`, testAccAcmCertificateConfig( + domain, sanDomain, + "Environment", "Test", + "Foo", "Baz")) +} + +func testAccAcmCertificateWithValidationAndRecordsConfig(rootZoneDomain string, domain string, sanDomain string) string { + return fmt.Sprintf(` +%s + +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_route53_record" "cert_validation_san" { + name = "${aws_acm_certificate.cert.domain_validation_options.1.resource_record_name}" + type = "${aws_acm_certificate.cert.domain_validation_options.1.resource_record_type}" + zone_id = "${data.aws_route53_zone.zone.id}" + records = ["${aws_acm_certificate.cert.domain_validation_options.1.resource_record_value}"] + ttl = 60 +} + +resource "aws_acm_certificate_validation" "cert" { + certificate_arn = "${aws_acm_certificate.cert.arn}" + validation_record_fqdns = [ + "${aws_route53_record.cert_validation.fqdn}", + "${aws_route53_record.cert_validation_san.fqdn}" + ] +} +`, testAccAcmCertificateConfig( + domain, sanDomain, + "Environment", "Test", + "Foo", "Baz"), + rootZoneDomain) +} diff --git a/aws/tagsACM.go b/aws/tagsACM.go new file mode 100644 index 00000000000..5f8a88fa90f --- /dev/null +++ b/aws/tagsACM.go @@ -0,0 +1,103 @@ +package aws + +import ( + "log" + "regexp" + + "github.com/aws/aws-sdk-go/aws" + "github.com/aws/aws-sdk-go/service/acm" + "github.com/hashicorp/terraform/helper/schema" +) + +func setTagsACM(conn *acm.ACM, d *schema.ResourceData) error { + if d.HasChange("tags") { + oraw, nraw := d.GetChange("tags") + o := oraw.(map[string]interface{}) + n := nraw.(map[string]interface{}) + create, remove := diffTagsACM(tagsFromMapACM(o), tagsFromMapACM(n)) + + // Set tags + if len(remove) > 0 { + input := acm.RemoveTagsFromCertificateInput{ + CertificateArn: aws.String(d.Get("arn").(string)), + Tags: remove, + } + log.Printf("[DEBUG] Removing ACM tags: %s", input) + _, err := conn.RemoveTagsFromCertificate(&input) + if err != nil { + return err + } + } + if len(create) > 0 { + input := acm.AddTagsToCertificateInput{ + CertificateArn: aws.String(d.Get("arn").(string)), + Tags: create, + } + log.Printf("[DEBUG] Adding ACM tags: %s", input) + _, err := conn.AddTagsToCertificate(&input) + if err != nil { + return err + } + } + } + + return nil +} + +// diffTags takes our tags locally and the ones remotely and returns +// the set of tags that must be created, and the set of tags that must +// be destroyed. +func diffTagsACM(oldTags, newTags []*acm.Tag) ([]*acm.Tag, []*acm.Tag) { + // First, we're creating everything we have + create := make(map[string]interface{}) + for _, t := range newTags { + create[*t.Key] = *t.Value + } + + // Build the list of what to remove + var remove []*acm.Tag + for _, t := range oldTags { + old, ok := create[*t.Key] + if !ok || old != *t.Value { + // Delete it! + remove = append(remove, t) + } + } + + return tagsFromMapACM(create), remove +} + +func tagsFromMapACM(m map[string]interface{}) []*acm.Tag { + result := []*acm.Tag{} + for k, v := range m { + result = append(result, &acm.Tag{ + Key: aws.String(k), + Value: aws.String(v.(string)), + }) + } + + return result +} + +func tagsToMapACM(ts []*acm.Tag) map[string]string { + result := map[string]string{} + for _, t := range ts { + result[*t.Key] = *t.Value + } + + return result +} + +// compare a tag against a list of strings and checks if it should +// be ignored or not +func tagIgnoredACM(t *acm.Tag) bool { + filter := []string{"^aws:"} + for _, v := range filter { + log.Printf("[DEBUG] Matching %v with %v\n", v, *t.Key) + if r, _ := regexp.MatchString(v, *t.Key); r == true { + log.Printf("[DEBUG] Found AWS specific tag %s (val: %s), ignoring.\n", *t.Key, *t.Value) + return true + } + } + return false +} diff --git a/aws/tagsACM_test.go b/aws/tagsACM_test.go new file mode 100644 index 00000000000..14213437d76 --- /dev/null +++ b/aws/tagsACM_test.go @@ -0,0 +1,77 @@ +package aws + +import ( + "reflect" + "testing" + + "github.com/aws/aws-sdk-go/aws" + "github.com/aws/aws-sdk-go/service/acm" +) + +func TestDiffTagsACM(t *testing.T) { + cases := []struct { + Old, New map[string]interface{} + Create, Remove map[string]string + }{ + // Basic add/remove + { + Old: map[string]interface{}{ + "foo": "bar", + }, + New: map[string]interface{}{ + "bar": "baz", + }, + Create: map[string]string{ + "bar": "baz", + }, + Remove: map[string]string{ + "foo": "bar", + }, + }, + + // Modify + { + Old: map[string]interface{}{ + "foo": "bar", + }, + New: map[string]interface{}{ + "foo": "baz", + }, + Create: map[string]string{ + "foo": "baz", + }, + Remove: map[string]string{ + "foo": "bar", + }, + }, + } + + for i, tc := range cases { + c, r := diffTagsACM(tagsFromMapACM(tc.Old), tagsFromMapACM(tc.New)) + cm := tagsToMapACM(c) + rm := tagsToMapACM(r) + if !reflect.DeepEqual(cm, tc.Create) { + t.Fatalf("%d: bad create: %#v", i, cm) + } + if !reflect.DeepEqual(rm, tc.Remove) { + t.Fatalf("%d: bad remove: %#v", i, rm) + } + } +} + +func TestIgnoringTagsACM(t *testing.T) { + var ignoredTags []*acm.Tag + ignoredTags = append(ignoredTags, &acm.Tag{ + Key: aws.String("aws:cloudformation:logical-id"), + Value: aws.String("foo"), + }) + ignoredTags = append(ignoredTags, &acm.Tag{ + Key: aws.String("aws:foo:bar"), + Value: aws.String("baz"), + }) + for _, tag := range ignoredTags { + if !tagIgnoredACM(tag) { + t.Fatalf("Tag %v with value %v not ignored, but should be!", *tag.Key, *tag.Value) + } + } +} diff --git a/website/aws.erb b/website/aws.erb index 5bf9ba7af53..6c086b8cc13 100644 --- a/website/aws.erb +++ b/website/aws.erb @@ -233,6 +233,18 @@ + > + ACM Resources + + + > API Gateway Resources