diff --git a/aws/data_source_aws_acmpca_certificate_authority.go b/aws/data_source_aws_acmpca_certificate_authority.go new file mode 100644 index 00000000000..b64902e2ef1 --- /dev/null +++ b/aws/data_source_aws_acmpca_certificate_authority.go @@ -0,0 +1,176 @@ +package aws + +import ( + "fmt" + "log" + + "github.com/aws/aws-sdk-go/aws" + "github.com/aws/aws-sdk-go/service/acmpca" + "github.com/hashicorp/terraform/helper/schema" +) + +func dataSourceAwsAcmpcaCertificateAuthority() *schema.Resource { + return &schema.Resource{ + Read: dataSourceAwsAcmpcaCertificateAuthorityRead, + + Schema: map[string]*schema.Schema{ + "arn": { + Type: schema.TypeString, + Required: true, + }, + "certificate": { + Type: schema.TypeString, + Computed: true, + }, + "certificate_chain": { + Type: schema.TypeString, + Computed: true, + }, + "certificate_signing_request": { + Type: schema.TypeString, + Computed: true, + }, + "not_after": { + Type: schema.TypeString, + Computed: true, + }, + "not_before": { + Type: schema.TypeString, + Computed: true, + }, + // https://docs.aws.amazon.com/acm-pca/latest/APIReference/API_RevocationConfiguration.html + "revocation_configuration": { + Type: schema.TypeList, + Optional: true, + Computed: true, + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + // https://docs.aws.amazon.com/acm-pca/latest/APIReference/API_CrlConfiguration.html + "crl_configuration": { + Type: schema.TypeList, + Optional: true, + Computed: true, + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "custom_cname": { + Type: schema.TypeString, + Computed: true, + }, + "enabled": { + Type: schema.TypeBool, + Computed: true, + }, + "expiration_in_days": { + Type: schema.TypeInt, + Computed: true, + }, + "s3_bucket_name": { + Type: schema.TypeString, + Computed: true, + }, + }, + }, + }, + }, + }, + }, + "serial": { + Type: schema.TypeString, + Computed: true, + }, + "status": { + Type: schema.TypeString, + Computed: true, + }, + "tags": tagsSchemaComputed(), + "type": { + Type: schema.TypeString, + Computed: true, + }, + }, + } +} + +func dataSourceAwsAcmpcaCertificateAuthorityRead(d *schema.ResourceData, meta interface{}) error { + conn := meta.(*AWSClient).acmpcaconn + certificateAuthorityArn := d.Get("arn").(string) + + describeCertificateAuthorityInput := &acmpca.DescribeCertificateAuthorityInput{ + CertificateAuthorityArn: aws.String(certificateAuthorityArn), + } + + log.Printf("[DEBUG] Reading ACMPCA Certificate Authority: %s", describeCertificateAuthorityInput) + + describeCertificateAuthorityOutput, err := conn.DescribeCertificateAuthority(describeCertificateAuthorityInput) + if err != nil { + return fmt.Errorf("error reading ACMPCA Certificate Authority: %s", err) + } + + if describeCertificateAuthorityOutput.CertificateAuthority == nil { + return fmt.Errorf("error reading ACMPCA Certificate Authority: not found") + } + certificateAuthority := describeCertificateAuthorityOutput.CertificateAuthority + + d.Set("arn", certificateAuthority.Arn) + d.Set("not_after", certificateAuthority.NotAfter) + d.Set("not_before", certificateAuthority.NotBefore) + + if err := d.Set("revocation_configuration", flattenAcmpcaRevocationConfiguration(certificateAuthority.RevocationConfiguration)); err != nil { + return fmt.Errorf("error setting tags: %s", err) + } + + d.Set("serial", certificateAuthority.Serial) + d.Set("status", certificateAuthority.Status) + d.Set("type", certificateAuthority.Type) + + getCertificateAuthorityCertificateInput := &acmpca.GetCertificateAuthorityCertificateInput{ + CertificateAuthorityArn: aws.String(certificateAuthorityArn), + } + + log.Printf("[DEBUG] Reading ACMPCA Certificate Authority Certificate: %s", getCertificateAuthorityCertificateInput) + + getCertificateAuthorityCertificateOutput, err := conn.GetCertificateAuthorityCertificate(getCertificateAuthorityCertificateInput) + if err != nil { + // Returned when in PENDING_CERTIFICATE status + // InvalidStateException: The certificate authority XXXXX is not in the correct state to have a certificate signing request. + if !isAWSErr(err, acmpca.ErrCodeInvalidStateException, "") { + return fmt.Errorf("error reading ACMPCA Certificate Authority Certificate: %s", err) + } + } + + d.Set("certificate", "") + d.Set("certificate_chain", "") + if getCertificateAuthorityCertificateOutput != nil { + d.Set("certificate", getCertificateAuthorityCertificateOutput.Certificate) + d.Set("certificate_chain", getCertificateAuthorityCertificateOutput.CertificateChain) + } + + getCertificateAuthorityCsrInput := &acmpca.GetCertificateAuthorityCsrInput{ + CertificateAuthorityArn: aws.String(certificateAuthorityArn), + } + + log.Printf("[DEBUG] Reading ACMPCA Certificate Authority Certificate Signing Request: %s", getCertificateAuthorityCsrInput) + + getCertificateAuthorityCsrOutput, err := conn.GetCertificateAuthorityCsr(getCertificateAuthorityCsrInput) + if err != nil { + return fmt.Errorf("error reading ACMPCA Certificate Authority Certificate Signing Request: %s", err) + } + + d.Set("certificate_signing_request", "") + if getCertificateAuthorityCsrOutput != nil { + d.Set("certificate_signing_request", getCertificateAuthorityCsrOutput.Csr) + } + + tags, err := listAcmpcaTags(conn, certificateAuthorityArn) + if err != nil { + return fmt.Errorf("error reading ACMPCA Certificate Authority %q tags: %s", certificateAuthorityArn, err) + } + + if err := d.Set("tags", tagsToMapACMPCA(tags)); err != nil { + return fmt.Errorf("error setting tags: %s", err) + } + + d.SetId(certificateAuthorityArn) + + return nil +} diff --git a/aws/data_source_aws_acmpca_certificate_authority_test.go b/aws/data_source_aws_acmpca_certificate_authority_test.go new file mode 100644 index 00000000000..b35cf3d31dc --- /dev/null +++ b/aws/data_source_aws_acmpca_certificate_authority_test.go @@ -0,0 +1,109 @@ +package aws + +import ( + "fmt" + "regexp" + "testing" + + "github.com/hashicorp/terraform/helper/resource" + "github.com/hashicorp/terraform/terraform" +) + +func TestAccDataSourceAwsAcmpcaCertificateAuthority_Basic(t *testing.T) { + resourceName := "aws_acmpca_certificate_authority.test" + datasourceName := "data.aws_acmpca_certificate_authority.test" + + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + Providers: testAccProviders, + Steps: []resource.TestStep{ + { + Config: testAccDataSourceAwsAcmpcaCertificateAuthorityConfig_NonExistent, + ExpectError: regexp.MustCompile(`ResourceNotFoundException`), + }, + { + Config: testAccDataSourceAwsAcmpcaCertificateAuthorityConfig_ARN, + Check: resource.ComposeTestCheckFunc( + testAccDataSourceAwsAcmpcaCertificateAuthorityCheck(datasourceName, resourceName), + ), + }, + }, + }) +} + +func testAccDataSourceAwsAcmpcaCertificateAuthorityCheck(datasourceName, resourceName string) resource.TestCheckFunc { + return func(s *terraform.State) error { + resource, ok := s.RootModule().Resources[datasourceName] + if !ok { + return fmt.Errorf("root module has no resource called %s", datasourceName) + } + + dataSource, ok := s.RootModule().Resources[resourceName] + if !ok { + return fmt.Errorf("root module has no resource called %s", resourceName) + } + + attrNames := []string{ + "arn", + "certificate", + "certificate_chain", + "certificate_signing_request", + "not_after", + "not_before", + "revocation_configuration.#", + "revocation_configuration.0.crl_configuration.#", + "revocation_configuration.0.crl_configuration.0.enabled", + "serial", + "status", + "tags.%", + "type", + } + + for _, attrName := range attrNames { + if resource.Primary.Attributes[attrName] != dataSource.Primary.Attributes[attrName] { + return fmt.Errorf( + "%s is %s; want %s", + attrName, + resource.Primary.Attributes[attrName], + dataSource.Primary.Attributes[attrName], + ) + } + } + + return nil + } +} + +const testAccDataSourceAwsAcmpcaCertificateAuthorityConfig_ARN = ` +resource "aws_acmpca_certificate_authority" "wrong" { + certificate_authority_configuration { + key_algorithm = "RSA_4096" + signing_algorithm = "SHA512WITHRSA" + + subject { + common_name = "terraformtesting.com" + } + } +} + +resource "aws_acmpca_certificate_authority" "test" { + certificate_authority_configuration { + key_algorithm = "RSA_4096" + signing_algorithm = "SHA512WITHRSA" + + subject { + common_name = "terraformtesting.com" + } + } +} + +data "aws_acmpca_certificate_authority" "test" { + arn = "${aws_acmpca_certificate_authority.test.arn}" +} +` + +const testAccDataSourceAwsAcmpcaCertificateAuthorityConfig_NonExistent = ` +data "aws_acmpca_certificate_authority" "test" { + arn = "arn:aws:acm-pca:us-east-1:123456789012:certificate-authority/tf-acc-test-does-not-exist" +} +` diff --git a/aws/provider.go b/aws/provider.go index c093890c444..31c12dbad7e 100644 --- a/aws/provider.go +++ b/aws/provider.go @@ -161,6 +161,7 @@ func Provider() terraform.ResourceProvider { DataSourcesMap: map[string]*schema.Resource{ "aws_acm_certificate": dataSourceAwsAcmCertificate(), + "aws_acmpca_certificate_authority": dataSourceAwsAcmpcaCertificateAuthority(), "aws_ami": dataSourceAwsAmi(), "aws_ami_ids": dataSourceAwsAmiIds(), "aws_api_gateway_rest_api": dataSourceAwsApiGatewayRestApi(), @@ -255,6 +256,7 @@ func Provider() terraform.ResourceProvider { ResourcesMap: map[string]*schema.Resource{ "aws_acm_certificate": resourceAwsAcmCertificate(), "aws_acm_certificate_validation": resourceAwsAcmCertificateValidation(), + "aws_acmpca_certificate_authority": resourceAwsAcmpcaCertificateAuthority(), "aws_ami": resourceAwsAmi(), "aws_ami_copy": resourceAwsAmiCopy(), "aws_ami_from_instance": resourceAwsAmiFromInstance(), diff --git a/aws/resource_aws_acmpca_certificate_authority.go b/aws/resource_aws_acmpca_certificate_authority.go new file mode 100644 index 00000000000..81bc3479f41 --- /dev/null +++ b/aws/resource_aws_acmpca_certificate_authority.go @@ -0,0 +1,736 @@ +package aws + +import ( + "fmt" + "log" + "time" + + "github.com/aws/aws-sdk-go/aws" + "github.com/aws/aws-sdk-go/service/acmpca" + "github.com/hashicorp/terraform/helper/resource" + "github.com/hashicorp/terraform/helper/schema" + "github.com/hashicorp/terraform/helper/validation" +) + +func resourceAwsAcmpcaCertificateAuthority() *schema.Resource { + return &schema.Resource{ + Create: resourceAwsAcmpcaCertificateAuthorityCreate, + Read: resourceAwsAcmpcaCertificateAuthorityRead, + Update: resourceAwsAcmpcaCertificateAuthorityUpdate, + Delete: resourceAwsAcmpcaCertificateAuthorityDelete, + Importer: &schema.ResourceImporter{ + State: schema.ImportStatePassthrough, + }, + Timeouts: &schema.ResourceTimeout{ + Create: schema.DefaultTimeout(1 * time.Minute), + }, + + Schema: map[string]*schema.Schema{ + "arn": { + Type: schema.TypeString, + Computed: true, + }, + "certificate": { + Type: schema.TypeString, + Computed: true, + }, + // https://docs.aws.amazon.com/acm-pca/latest/APIReference/API_CertificateAuthorityConfiguration.html + "certificate_authority_configuration": { + Type: schema.TypeList, + Required: true, + MaxItems: 1, + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "key_algorithm": { + Type: schema.TypeString, + Required: true, + ForceNew: true, + ValidateFunc: validation.StringInSlice([]string{ + acmpca.KeyAlgorithmEcPrime256v1, + acmpca.KeyAlgorithmEcSecp384r1, + acmpca.KeyAlgorithmRsa2048, + acmpca.KeyAlgorithmRsa4096, + }, false), + }, + "signing_algorithm": { + Type: schema.TypeString, + Required: true, + ForceNew: true, + ValidateFunc: validation.StringInSlice([]string{ + acmpca.SigningAlgorithmSha256withecdsa, + acmpca.SigningAlgorithmSha256withrsa, + acmpca.SigningAlgorithmSha384withecdsa, + acmpca.SigningAlgorithmSha384withrsa, + acmpca.SigningAlgorithmSha512withecdsa, + acmpca.SigningAlgorithmSha512withrsa, + }, false), + }, + // https://docs.aws.amazon.com/acm-pca/latest/APIReference/API_ASN1Subject.html + "subject": { + Type: schema.TypeList, + Required: true, + ForceNew: true, + MaxItems: 1, + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "common_name": { + Type: schema.TypeString, + Optional: true, + ForceNew: true, + ValidateFunc: validation.StringLenBetween(0, 64), + }, + "country": { + Type: schema.TypeString, + Optional: true, + ForceNew: true, + ValidateFunc: validation.StringLenBetween(0, 2), + }, + "distinguished_name_qualifier": { + Type: schema.TypeString, + Optional: true, + ForceNew: true, + ValidateFunc: validation.StringLenBetween(0, 64), + }, + "generation_qualifier": { + Type: schema.TypeString, + Optional: true, + ForceNew: true, + ValidateFunc: validation.StringLenBetween(0, 3), + }, + "given_name": { + Type: schema.TypeString, + Optional: true, + ForceNew: true, + ValidateFunc: validation.StringLenBetween(0, 16), + }, + "initials": { + Type: schema.TypeString, + Optional: true, + ForceNew: true, + ValidateFunc: validation.StringLenBetween(0, 5), + }, + "locality": { + Type: schema.TypeString, + Optional: true, + ForceNew: true, + ValidateFunc: validation.StringLenBetween(0, 128), + }, + "organization": { + Type: schema.TypeString, + Optional: true, + ForceNew: true, + ValidateFunc: validation.StringLenBetween(0, 64), + }, + "organizational_unit": { + Type: schema.TypeString, + Optional: true, + ForceNew: true, + ValidateFunc: validation.StringLenBetween(0, 64), + }, + "pseudonym": { + Type: schema.TypeString, + Optional: true, + ForceNew: true, + ValidateFunc: validation.StringLenBetween(0, 128), + }, + "state": { + Type: schema.TypeString, + Optional: true, + ForceNew: true, + ValidateFunc: validation.StringLenBetween(0, 128), + }, + "surname": { + Type: schema.TypeString, + Optional: true, + ForceNew: true, + ValidateFunc: validation.StringLenBetween(0, 40), + }, + "title": { + Type: schema.TypeString, + Optional: true, + ForceNew: true, + ValidateFunc: validation.StringLenBetween(0, 64), + }, + }, + }, + }, + }, + }, + }, + "certificate_chain": { + Type: schema.TypeString, + Computed: true, + }, + "certificate_signing_request": { + Type: schema.TypeString, + Computed: true, + }, + "enabled": { + Type: schema.TypeBool, + Optional: true, + Default: true, + }, + "not_after": { + Type: schema.TypeString, + Computed: true, + }, + "not_before": { + Type: schema.TypeString, + Computed: true, + }, + // https://docs.aws.amazon.com/acm-pca/latest/APIReference/API_RevocationConfiguration.html + "revocation_configuration": { + Type: schema.TypeList, + Optional: true, + MaxItems: 1, + DiffSuppressFunc: func(k, old, new string, d *schema.ResourceData) bool { + if old == "1" && new == "0" { + return true + } + return false + }, + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + // https://docs.aws.amazon.com/acm-pca/latest/APIReference/API_CrlConfiguration.html + "crl_configuration": { + Type: schema.TypeList, + Optional: true, + MaxItems: 1, + DiffSuppressFunc: func(k, old, new string, d *schema.ResourceData) bool { + if old == "1" && new == "0" { + return true + } + return false + }, + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "custom_cname": { + Type: schema.TypeString, + Optional: true, + ValidateFunc: validation.StringLenBetween(0, 253), + }, + "enabled": { + Type: schema.TypeBool, + Optional: true, + }, + // ValidationException: 1 validation error detected: Value null or empty at 'expirationInDays' failed to satisfy constraint: Member must not be null or empty. + // InvalidParameter: 1 validation error(s) found. minimum field value of 1, CreateCertificateAuthorityInput.RevocationConfiguration.CrlConfiguration.ExpirationInDays. + "expiration_in_days": { + Type: schema.TypeInt, + Required: true, + ValidateFunc: validation.IntBetween(1, 5000), + }, + "s3_bucket_name": { + Type: schema.TypeString, + Optional: true, + ValidateFunc: validation.StringLenBetween(0, 255), + }, + }, + }, + }, + }, + }, + }, + "serial": { + Type: schema.TypeString, + Computed: true, + }, + "status": { + Type: schema.TypeString, + Computed: true, + }, + "tags": tagsSchema(), + "type": { + Type: schema.TypeString, + Optional: true, + Default: acmpca.CertificateAuthorityTypeSubordinate, + ValidateFunc: validation.StringInSlice([]string{ + acmpca.CertificateAuthorityTypeSubordinate, + }, false), + }, + }, + } +} + +func resourceAwsAcmpcaCertificateAuthorityCreate(d *schema.ResourceData, meta interface{}) error { + conn := meta.(*AWSClient).acmpcaconn + + input := &acmpca.CreateCertificateAuthorityInput{ + CertificateAuthorityConfiguration: expandAcmpcaCertificateAuthorityConfiguration(d.Get("certificate_authority_configuration").([]interface{})), + CertificateAuthorityType: aws.String(d.Get("type").(string)), + RevocationConfiguration: expandAcmpcaRevocationConfiguration(d.Get("revocation_configuration").([]interface{})), + } + + log.Printf("[DEBUG] Creating ACMPCA Certificate Authority: %s", input) + var output *acmpca.CreateCertificateAuthorityOutput + err := resource.Retry(1*time.Minute, func() *resource.RetryError { + var err error + output, err = conn.CreateCertificateAuthority(input) + if err != nil { + // ValidationException: The ACM Private CA service account 'acm-pca-prod-pdx' requires getBucketAcl permissions for your S3 bucket 'tf-acc-test-5224996536060125340'. Check your S3 bucket permissions and try again. + if isAWSErr(err, "ValidationException", "Check your S3 bucket permissions and try again") { + return resource.RetryableError(err) + } + return resource.NonRetryableError(err) + } + return nil + }) + if err != nil { + return fmt.Errorf("error creating ACMPCA Certificate Authority: %s", err) + } + + d.SetId(aws.StringValue(output.CertificateAuthorityArn)) + + if v, ok := d.GetOk("tags"); ok { + input := &acmpca.TagCertificateAuthorityInput{ + CertificateAuthorityArn: aws.String(d.Id()), + Tags: tagsFromMapACMPCA(v.(map[string]interface{})), + } + + log.Printf("[DEBUG] Tagging ACMPCA Certificate Authority: %s", input) + _, err := conn.TagCertificateAuthority(input) + if err != nil { + return fmt.Errorf("error tagging ACMPCA Certificate Authority %q: %s", d.Id(), input) + } + } + + stateConf := &resource.StateChangeConf{ + Pending: []string{ + "", + acmpca.CertificateAuthorityStatusCreating, + }, + Target: []string{ + acmpca.CertificateAuthorityStatusActive, + acmpca.CertificateAuthorityStatusPendingCertificate, + }, + Refresh: acmpcaCertificateAuthorityRefreshFunc(conn, d.Id()), + Timeout: d.Timeout(schema.TimeoutCreate), + } + + _, err = stateConf.WaitForState() + if err != nil { + return fmt.Errorf("error waiting for ACMPCA Certificate Authority %q to be active or pending certificate: %s", d.Id(), err) + } + + return resourceAwsAcmpcaCertificateAuthorityRead(d, meta) +} + +func resourceAwsAcmpcaCertificateAuthorityRead(d *schema.ResourceData, meta interface{}) error { + conn := meta.(*AWSClient).acmpcaconn + + describeCertificateAuthorityInput := &acmpca.DescribeCertificateAuthorityInput{ + CertificateAuthorityArn: aws.String(d.Id()), + } + + log.Printf("[DEBUG] Reading ACMPCA Certificate Authority: %s", describeCertificateAuthorityInput) + + describeCertificateAuthorityOutput, err := conn.DescribeCertificateAuthority(describeCertificateAuthorityInput) + if err != nil { + if isAWSErr(err, acmpca.ErrCodeResourceNotFoundException, "") { + log.Printf("[WARN] ACMPCA Certificate Authority %q not found - removing from state", d.Id()) + d.SetId("") + return nil + } + return fmt.Errorf("error reading ACMPCA Certificate Authority: %s", err) + } + + if describeCertificateAuthorityOutput.CertificateAuthority == nil { + log.Printf("[WARN] ACMPCA Certificate Authority %q not found - removing from state", d.Id()) + d.SetId("") + return nil + } + certificateAuthority := describeCertificateAuthorityOutput.CertificateAuthority + + d.Set("arn", certificateAuthority.Arn) + + if err := d.Set("certificate_authority_configuration", flattenAcmpcaCertificateAuthorityConfiguration(certificateAuthority.CertificateAuthorityConfiguration)); err != nil { + return fmt.Errorf("error setting tags: %s", err) + } + + d.Set("enabled", (aws.StringValue(certificateAuthority.Status) != acmpca.CertificateAuthorityStatusDisabled)) + d.Set("not_after", certificateAuthority.NotAfter) + d.Set("not_before", certificateAuthority.NotBefore) + + if err := d.Set("revocation_configuration", flattenAcmpcaRevocationConfiguration(certificateAuthority.RevocationConfiguration)); err != nil { + return fmt.Errorf("error setting tags: %s", err) + } + + d.Set("serial", certificateAuthority.Serial) + d.Set("status", certificateAuthority.Status) + d.Set("type", certificateAuthority.Type) + + getCertificateAuthorityCertificateInput := &acmpca.GetCertificateAuthorityCertificateInput{ + CertificateAuthorityArn: aws.String(d.Id()), + } + + log.Printf("[DEBUG] Reading ACMPCA Certificate Authority Certificate: %s", getCertificateAuthorityCertificateInput) + + getCertificateAuthorityCertificateOutput, err := conn.GetCertificateAuthorityCertificate(getCertificateAuthorityCertificateInput) + if err != nil { + if isAWSErr(err, acmpca.ErrCodeResourceNotFoundException, "") { + log.Printf("[WARN] ACMPCA Certificate Authority %q not found - removing from state", d.Id()) + d.SetId("") + return nil + } + // Returned when in PENDING_CERTIFICATE status + // InvalidStateException: The certificate authority XXXXX is not in the correct state to have a certificate signing request. + if !isAWSErr(err, acmpca.ErrCodeInvalidStateException, "") { + return fmt.Errorf("error reading ACMPCA Certificate Authority Certificate: %s", err) + } + } + + d.Set("certificate", "") + d.Set("certificate_chain", "") + if getCertificateAuthorityCertificateOutput != nil { + d.Set("certificate", getCertificateAuthorityCertificateOutput.Certificate) + d.Set("certificate_chain", getCertificateAuthorityCertificateOutput.CertificateChain) + } + + getCertificateAuthorityCsrInput := &acmpca.GetCertificateAuthorityCsrInput{ + CertificateAuthorityArn: aws.String(d.Id()), + } + + log.Printf("[DEBUG] Reading ACMPCA Certificate Authority Certificate Signing Request: %s", getCertificateAuthorityCsrInput) + + getCertificateAuthorityCsrOutput, err := conn.GetCertificateAuthorityCsr(getCertificateAuthorityCsrInput) + if err != nil { + if isAWSErr(err, acmpca.ErrCodeResourceNotFoundException, "") { + log.Printf("[WARN] ACMPCA Certificate Authority %q not found - removing from state", d.Id()) + d.SetId("") + return nil + } + return fmt.Errorf("error reading ACMPCA Certificate Authority Certificate Signing Request: %s", err) + } + + d.Set("certificate_signing_request", "") + if getCertificateAuthorityCsrOutput != nil { + d.Set("certificate_signing_request", getCertificateAuthorityCsrOutput.Csr) + } + + tags, err := listAcmpcaTags(conn, d.Id()) + if err != nil { + return fmt.Errorf("error reading ACMPCA Certificate Authority %q tags: %s", d.Id(), err) + } + + if err := d.Set("tags", tagsToMapACMPCA(tags)); err != nil { + return fmt.Errorf("error setting tags: %s", err) + } + + return nil +} + +func resourceAwsAcmpcaCertificateAuthorityUpdate(d *schema.ResourceData, meta interface{}) error { + conn := meta.(*AWSClient).acmpcaconn + updateCertificateAuthority := false + + input := &acmpca.UpdateCertificateAuthorityInput{ + CertificateAuthorityArn: aws.String(d.Id()), + } + + if d.HasChange("enabled") { + input.Status = aws.String(acmpca.CertificateAuthorityStatusActive) + if !d.Get("enabled").(bool) { + input.Status = aws.String(acmpca.CertificateAuthorityStatusDisabled) + } + updateCertificateAuthority = true + } + + if d.HasChange("revocation_configuration") { + input.RevocationConfiguration = expandAcmpcaRevocationConfiguration(d.Get("revocation_configuration").([]interface{})) + updateCertificateAuthority = true + } + + if updateCertificateAuthority { + log.Printf("[DEBUG] Updating ACMPCA Certificate Authority: %s", input) + _, err := conn.UpdateCertificateAuthority(input) + if err != nil { + return fmt.Errorf("error updating ACMPCA Certificate Authority: %s", err) + } + } + + if d.HasChange("tags") { + oraw, nraw := d.GetChange("tags") + o := oraw.(map[string]interface{}) + n := nraw.(map[string]interface{}) + create, remove := diffTagsACMPCA(tagsFromMapACMPCA(o), tagsFromMapACMPCA(n)) + + if len(remove) > 0 { + log.Printf("[DEBUG] Removing ACMPCA Certificate Authority %q tags: %#v", d.Id(), remove) + _, err := conn.UntagCertificateAuthority(&acmpca.UntagCertificateAuthorityInput{ + CertificateAuthorityArn: aws.String(d.Id()), + Tags: remove, + }) + if err != nil { + return fmt.Errorf("error updating ACMPCA Certificate Authority %q tags: %s", d.Id(), err) + } + } + if len(create) > 0 { + log.Printf("[DEBUG] Creating ACMPCA Certificate Authority %q tags: %#v", d.Id(), create) + _, err := conn.TagCertificateAuthority(&acmpca.TagCertificateAuthorityInput{ + CertificateAuthorityArn: aws.String(d.Id()), + Tags: create, + }) + if err != nil { + return fmt.Errorf("error updating ACMPCA Certificate Authority %q tags: %s", d.Id(), err) + } + } + } + + return resourceAwsAcmpcaCertificateAuthorityRead(d, meta) +} + +func resourceAwsAcmpcaCertificateAuthorityDelete(d *schema.ResourceData, meta interface{}) error { + conn := meta.(*AWSClient).acmpcaconn + + input := &acmpca.DeleteCertificateAuthorityInput{ + CertificateAuthorityArn: aws.String(d.Id()), + } + + log.Printf("[DEBUG] Deleting ACMPCA Certificate Authority: %s", input) + _, err := conn.DeleteCertificateAuthority(input) + if err != nil { + if isAWSErr(err, acmpca.ErrCodeResourceNotFoundException, "") { + return nil + } + return fmt.Errorf("error deleting ACMPCA Certificate Authority: %s", err) + } + + return nil +} + +func acmpcaCertificateAuthorityRefreshFunc(conn *acmpca.ACMPCA, certificateAuthorityArn string) resource.StateRefreshFunc { + return func() (interface{}, string, error) { + input := &acmpca.DescribeCertificateAuthorityInput{ + CertificateAuthorityArn: aws.String(certificateAuthorityArn), + } + + log.Printf("[DEBUG] Reading ACMPCA Certificate Authority: %s", input) + output, err := conn.DescribeCertificateAuthority(input) + if err != nil { + if isAWSErr(err, acmpca.ErrCodeResourceNotFoundException, "") { + return nil, "", nil + } + return nil, "", err + } + + if output == nil || output.CertificateAuthority == nil { + return nil, "", nil + } + + return output.CertificateAuthority, aws.StringValue(output.CertificateAuthority.Status), nil + } +} + +func expandAcmpcaASN1Subject(l []interface{}) *acmpca.ASN1Subject { + if len(l) == 0 { + return nil + } + + m := l[0].(map[string]interface{}) + + subject := &acmpca.ASN1Subject{} + if v, ok := m["common_name"]; ok && v.(string) != "" { + subject.CommonName = aws.String(v.(string)) + } + if v, ok := m["country"]; ok && v.(string) != "" { + subject.Country = aws.String(v.(string)) + } + if v, ok := m["distinguished_name_qualifier"]; ok && v.(string) != "" { + subject.DistinguishedNameQualifier = aws.String(v.(string)) + } + if v, ok := m["generation_qualifier"]; ok && v.(string) != "" { + subject.GenerationQualifier = aws.String(v.(string)) + } + if v, ok := m["given_name"]; ok && v.(string) != "" { + subject.GivenName = aws.String(v.(string)) + } + if v, ok := m["initials"]; ok && v.(string) != "" { + subject.Initials = aws.String(v.(string)) + } + if v, ok := m["locality"]; ok && v.(string) != "" { + subject.Locality = aws.String(v.(string)) + } + if v, ok := m["organization"]; ok && v.(string) != "" { + subject.Organization = aws.String(v.(string)) + } + if v, ok := m["organizational_unit"]; ok && v.(string) != "" { + subject.OrganizationalUnit = aws.String(v.(string)) + } + if v, ok := m["pseudonym"]; ok && v.(string) != "" { + subject.Pseudonym = aws.String(v.(string)) + } + if v, ok := m["state"]; ok && v.(string) != "" { + subject.State = aws.String(v.(string)) + } + if v, ok := m["surname"]; ok && v.(string) != "" { + subject.Surname = aws.String(v.(string)) + } + if v, ok := m["title"]; ok && v.(string) != "" { + subject.Title = aws.String(v.(string)) + } + + return subject +} + +func expandAcmpcaCertificateAuthorityConfiguration(l []interface{}) *acmpca.CertificateAuthorityConfiguration { + if len(l) == 0 { + return nil + } + + m := l[0].(map[string]interface{}) + + config := &acmpca.CertificateAuthorityConfiguration{ + KeyAlgorithm: aws.String(m["key_algorithm"].(string)), + SigningAlgorithm: aws.String(m["signing_algorithm"].(string)), + Subject: expandAcmpcaASN1Subject(m["subject"].([]interface{})), + } + + return config +} + +func expandAcmpcaCrlConfiguration(l []interface{}) *acmpca.CrlConfiguration { + if len(l) == 0 { + return nil + } + + m := l[0].(map[string]interface{}) + + config := &acmpca.CrlConfiguration{ + Enabled: aws.Bool(m["enabled"].(bool)), + } + + if v, ok := m["custom_cname"]; ok && v.(string) != "" { + config.CustomCname = aws.String(v.(string)) + } + if v, ok := m["expiration_in_days"]; ok && v.(int) > 0 { + config.ExpirationInDays = aws.Int64(int64(v.(int))) + } + if v, ok := m["s3_bucket_name"]; ok && v.(string) != "" { + config.S3BucketName = aws.String(v.(string)) + } + + return config +} + +func expandAcmpcaRevocationConfiguration(l []interface{}) *acmpca.RevocationConfiguration { + if len(l) == 0 { + return nil + } + + m := l[0].(map[string]interface{}) + + config := &acmpca.RevocationConfiguration{ + CrlConfiguration: expandAcmpcaCrlConfiguration(m["crl_configuration"].([]interface{})), + } + + return config +} + +func flattenAcmpcaASN1Subject(subject *acmpca.ASN1Subject) []interface{} { + if subject == nil { + return []interface{}{} + } + + m := map[string]interface{}{ + "common_name": aws.StringValue(subject.CommonName), + "country": aws.StringValue(subject.Country), + "distinguished_name_qualifier": aws.StringValue(subject.DistinguishedNameQualifier), + "generation_qualifier": aws.StringValue(subject.GenerationQualifier), + "given_name": aws.StringValue(subject.GivenName), + "initials": aws.StringValue(subject.Initials), + "locality": aws.StringValue(subject.Locality), + "organization": aws.StringValue(subject.Organization), + "organizational_unit": aws.StringValue(subject.OrganizationalUnit), + "pseudonym": aws.StringValue(subject.Pseudonym), + "state": aws.StringValue(subject.State), + "surname": aws.StringValue(subject.Surname), + "title": aws.StringValue(subject.Title), + } + + return []interface{}{m} +} + +func flattenAcmpcaCertificateAuthorityConfiguration(config *acmpca.CertificateAuthorityConfiguration) []interface{} { + if config == nil { + return []interface{}{} + } + + m := map[string]interface{}{ + "key_algorithm": aws.StringValue(config.KeyAlgorithm), + "signing_algorithm": aws.StringValue(config.SigningAlgorithm), + "subject": flattenAcmpcaASN1Subject(config.Subject), + } + + return []interface{}{m} +} + +func flattenAcmpcaCrlConfiguration(config *acmpca.CrlConfiguration) []interface{} { + if config == nil { + return []interface{}{} + } + + m := map[string]interface{}{ + "custom_cname": aws.StringValue(config.CustomCname), + "enabled": aws.BoolValue(config.Enabled), + "expiration_in_days": int(aws.Int64Value(config.ExpirationInDays)), + "s3_bucket_name": aws.StringValue(config.S3BucketName), + } + + return []interface{}{m} +} + +func flattenAcmpcaRevocationConfiguration(config *acmpca.RevocationConfiguration) []interface{} { + if config == nil { + return []interface{}{} + } + + m := map[string]interface{}{ + "crl_configuration": flattenAcmpcaCrlConfiguration(config.CrlConfiguration), + } + + return []interface{}{m} +} + +func listAcmpcaCertificateAuthorities(conn *acmpca.ACMPCA) ([]*acmpca.CertificateAuthority, error) { + certificateAuthorities := []*acmpca.CertificateAuthority{} + input := &acmpca.ListCertificateAuthoritiesInput{} + + for { + output, err := conn.ListCertificateAuthorities(input) + if err != nil { + return certificateAuthorities, err + } + for _, certificateAuthority := range output.CertificateAuthorities { + certificateAuthorities = append(certificateAuthorities, certificateAuthority) + } + if output.NextToken == nil { + break + } + input.NextToken = output.NextToken + } + + return certificateAuthorities, nil +} + +func listAcmpcaTags(conn *acmpca.ACMPCA, certificateAuthorityArn string) ([]*acmpca.Tag, error) { + tags := []*acmpca.Tag{} + input := &acmpca.ListTagsInput{ + CertificateAuthorityArn: aws.String(certificateAuthorityArn), + } + + for { + output, err := conn.ListTags(input) + if err != nil { + return tags, err + } + for _, tag := range output.Tags { + tags = append(tags, tag) + } + if output.NextToken == nil { + break + } + input.NextToken = output.NextToken + } + + return tags, nil +} diff --git a/aws/resource_aws_acmpca_certificate_authority_test.go b/aws/resource_aws_acmpca_certificate_authority_test.go new file mode 100644 index 00000000000..ccee6ae9eab --- /dev/null +++ b/aws/resource_aws_acmpca_certificate_authority_test.go @@ -0,0 +1,643 @@ +package aws + +import ( + "fmt" + "log" + "regexp" + "testing" + + "github.com/aws/aws-sdk-go/aws" + "github.com/aws/aws-sdk-go/service/acmpca" + "github.com/hashicorp/terraform/helper/acctest" + "github.com/hashicorp/terraform/helper/resource" + "github.com/hashicorp/terraform/terraform" +) + +func init() { + resource.AddTestSweepers("aws_acmpca_certificate_authority", &resource.Sweeper{ + Name: "aws_acmpca_certificate_authority", + F: testSweepAcmpcaCertificateAuthorities, + }) +} + +func testSweepAcmpcaCertificateAuthorities(region string) error { + client, err := sharedClientForRegion(region) + if err != nil { + return fmt.Errorf("error getting client: %s", err) + } + conn := client.(*AWSClient).acmpcaconn + + certificateAuthorities, err := listAcmpcaCertificateAuthorities(conn) + if err != nil { + return fmt.Errorf("Error retrieving ACMPCA Certificate Authorities: %s", err) + } + if len(certificateAuthorities) == 0 { + log.Print("[DEBUG] No ACMPCA Certificate Authorities to sweep") + return nil + } + + for _, certificateAuthority := range certificateAuthorities { + arn := aws.StringValue(certificateAuthority.Arn) + log.Printf("[INFO] Deleting ACMPCA Certificate Authority: %s", arn) + input := &acmpca.DeleteCertificateAuthorityInput{ + CertificateAuthorityArn: aws.String(arn), + } + + _, err := conn.DeleteCertificateAuthority(input) + if err != nil { + if isAWSErr(err, acmpca.ErrCodeResourceNotFoundException, "") { + continue + } + log.Printf("[ERROR] Failed to delete ACMPCA Certificate Authority (%s): %s", arn, err) + } + } + + return nil +} + +func TestAccAwsAcmpcaCertificateAuthority_Basic(t *testing.T) { + var certificateAuthority acmpca.CertificateAuthority + resourceName := "aws_acmpca_certificate_authority.test" + + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + Providers: testAccProviders, + CheckDestroy: testAccCheckAwsAcmpcaCertificateAuthorityDestroy, + Steps: []resource.TestStep{ + { + Config: testAccAwsAcmpcaCertificateAuthorityConfig_Required, + Check: resource.ComposeTestCheckFunc( + testAccCheckAwsAcmpcaCertificateAuthorityExists(resourceName, &certificateAuthority), + resource.TestMatchResourceAttr(resourceName, "arn", regexp.MustCompile(`^arn:[^:]+:acm-pca:[^:]+:[^:]+:certificate-authority/.+$`)), + resource.TestCheckResourceAttr(resourceName, "certificate_authority_configuration.#", "1"), + resource.TestCheckResourceAttr(resourceName, "certificate_authority_configuration.0.key_algorithm", "RSA_4096"), + resource.TestCheckResourceAttr(resourceName, "certificate_authority_configuration.0.signing_algorithm", "SHA512WITHRSA"), + resource.TestCheckResourceAttr(resourceName, "certificate_authority_configuration.0.subject.#", "1"), + resource.TestCheckResourceAttr(resourceName, "certificate_authority_configuration.0.subject.0.common_name", "terraformtesting.com"), + resource.TestCheckResourceAttr(resourceName, "certificate", ""), + resource.TestCheckResourceAttr(resourceName, "certificate_chain", ""), + resource.TestCheckResourceAttrSet(resourceName, "certificate_signing_request"), + resource.TestCheckResourceAttr(resourceName, "enabled", "true"), + resource.TestCheckResourceAttr(resourceName, "not_after", ""), + resource.TestCheckResourceAttr(resourceName, "not_before", ""), + resource.TestCheckResourceAttr(resourceName, "revocation_configuration.#", "1"), + resource.TestCheckResourceAttr(resourceName, "revocation_configuration.0.crl_configuration.#", "1"), + resource.TestCheckResourceAttr(resourceName, "revocation_configuration.0.crl_configuration.0.enabled", "false"), + resource.TestCheckResourceAttr(resourceName, "serial", ""), + resource.TestCheckResourceAttr(resourceName, "status", "PENDING_CERTIFICATE"), + resource.TestCheckResourceAttr(resourceName, "tags.%", "0"), + resource.TestCheckResourceAttr(resourceName, "type", "SUBORDINATE"), + ), + }, + { + ResourceName: resourceName, + ImportState: true, + ImportStateVerify: true, + }, + }, + }) +} + +func TestAccAwsAcmpcaCertificateAuthority_Enabled(t *testing.T) { + var certificateAuthority acmpca.CertificateAuthority + resourceName := "aws_acmpca_certificate_authority.test" + + // error updating ACMPCA Certificate Authority: InvalidStateException: The certificate authority must be in the Active or DISABLED state to be updated + t.Skip("We need to fully sign the certificate authority CSR from another CA in order to test this functionality, which requires another resource") + + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + Providers: testAccProviders, + CheckDestroy: testAccCheckAwsAcmpcaCertificateAuthorityDestroy, + Steps: []resource.TestStep{ + { + Config: testAccAwsAcmpcaCertificateAuthorityConfig_Enabled(true), + Check: resource.ComposeTestCheckFunc( + testAccCheckAwsAcmpcaCertificateAuthorityExists(resourceName, &certificateAuthority), + resource.TestCheckResourceAttr(resourceName, "enabled", "true"), + resource.TestCheckResourceAttr(resourceName, "status", "PENDING_CERTIFICATE"), + ), + }, + { + Config: testAccAwsAcmpcaCertificateAuthorityConfig_Enabled(false), + Check: resource.ComposeTestCheckFunc( + testAccCheckAwsAcmpcaCertificateAuthorityExists(resourceName, &certificateAuthority), + resource.TestCheckResourceAttr(resourceName, "enabled", "false"), + resource.TestCheckResourceAttr(resourceName, "status", "DISABLED"), + ), + }, + { + ResourceName: resourceName, + ImportState: true, + ImportStateVerify: true, + }, + }, + }) +} + +func TestAccAwsAcmpcaCertificateAuthority_RevocationConfiguration_CrlConfiguration_CustomCname(t *testing.T) { + var certificateAuthority acmpca.CertificateAuthority + rName := acctest.RandomWithPrefix("tf-acc-test") + resourceName := "aws_acmpca_certificate_authority.test" + + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + Providers: testAccProviders, + CheckDestroy: testAccCheckAwsAcmpcaCertificateAuthorityDestroy, + Steps: []resource.TestStep{ + // Test creating revocation configuration on resource creation + { + Config: testAccAwsAcmpcaCertificateAuthorityConfig_RevocationConfiguration_CrlConfiguration_CustomCname(rName, "crl.terraformtesting.com"), + Check: resource.ComposeTestCheckFunc( + testAccCheckAwsAcmpcaCertificateAuthorityExists(resourceName, &certificateAuthority), + resource.TestCheckResourceAttr(resourceName, "revocation_configuration.#", "1"), + resource.TestCheckResourceAttr(resourceName, "revocation_configuration.0.crl_configuration.#", "1"), + resource.TestCheckResourceAttr(resourceName, "revocation_configuration.0.crl_configuration.0.custom_cname", "crl.terraformtesting.com"), + resource.TestCheckResourceAttr(resourceName, "revocation_configuration.0.crl_configuration.0.enabled", "true"), + resource.TestCheckResourceAttr(resourceName, "revocation_configuration.0.crl_configuration.0.expiration_in_days", "1"), + resource.TestCheckResourceAttr(resourceName, "revocation_configuration.0.crl_configuration.0.s3_bucket_name", rName), + ), + }, + // Test importing revocation configuration + { + ResourceName: resourceName, + ImportState: true, + ImportStateVerify: true, + }, + // Test updating revocation configuration + { + Config: testAccAwsAcmpcaCertificateAuthorityConfig_RevocationConfiguration_CrlConfiguration_CustomCname(rName, "crl2.terraformtesting.com"), + Check: resource.ComposeTestCheckFunc( + testAccCheckAwsAcmpcaCertificateAuthorityExists(resourceName, &certificateAuthority), + resource.TestCheckResourceAttr(resourceName, "revocation_configuration.#", "1"), + resource.TestCheckResourceAttr(resourceName, "revocation_configuration.0.crl_configuration.#", "1"), + resource.TestCheckResourceAttr(resourceName, "revocation_configuration.0.crl_configuration.0.custom_cname", "crl2.terraformtesting.com"), + resource.TestCheckResourceAttr(resourceName, "revocation_configuration.0.crl_configuration.0.enabled", "true"), + resource.TestCheckResourceAttr(resourceName, "revocation_configuration.0.crl_configuration.0.expiration_in_days", "1"), + resource.TestCheckResourceAttr(resourceName, "revocation_configuration.0.crl_configuration.0.s3_bucket_name", rName), + ), + }, + // Test removing custom cname on resource update + { + Config: testAccAwsAcmpcaCertificateAuthorityConfig_RevocationConfiguration_CrlConfiguration_Enabled(rName, true), + Check: resource.ComposeTestCheckFunc( + testAccCheckAwsAcmpcaCertificateAuthorityExists(resourceName, &certificateAuthority), + resource.TestCheckResourceAttr(resourceName, "revocation_configuration.#", "1"), + resource.TestCheckResourceAttr(resourceName, "revocation_configuration.0.crl_configuration.#", "1"), + resource.TestCheckResourceAttr(resourceName, "revocation_configuration.0.crl_configuration.0.custom_cname", ""), + resource.TestCheckResourceAttr(resourceName, "revocation_configuration.0.crl_configuration.0.enabled", "true"), + resource.TestCheckResourceAttr(resourceName, "revocation_configuration.0.crl_configuration.0.expiration_in_days", "1"), + resource.TestCheckResourceAttr(resourceName, "revocation_configuration.0.crl_configuration.0.s3_bucket_name", rName), + ), + }, + // Test adding custom cname on resource update + { + Config: testAccAwsAcmpcaCertificateAuthorityConfig_RevocationConfiguration_CrlConfiguration_CustomCname(rName, "crl.terraformtesting.com"), + Check: resource.ComposeTestCheckFunc( + testAccCheckAwsAcmpcaCertificateAuthorityExists(resourceName, &certificateAuthority), + resource.TestCheckResourceAttr(resourceName, "revocation_configuration.#", "1"), + resource.TestCheckResourceAttr(resourceName, "revocation_configuration.0.crl_configuration.#", "1"), + resource.TestCheckResourceAttr(resourceName, "revocation_configuration.0.crl_configuration.0.custom_cname", "crl.terraformtesting.com"), + resource.TestCheckResourceAttr(resourceName, "revocation_configuration.0.crl_configuration.0.enabled", "true"), + resource.TestCheckResourceAttr(resourceName, "revocation_configuration.0.crl_configuration.0.expiration_in_days", "1"), + resource.TestCheckResourceAttr(resourceName, "revocation_configuration.0.crl_configuration.0.s3_bucket_name", rName), + ), + }, + // Test removing revocation configuration on resource update + { + Config: testAccAwsAcmpcaCertificateAuthorityConfig_Required, + Check: resource.ComposeTestCheckFunc( + testAccCheckAwsAcmpcaCertificateAuthorityExists(resourceName, &certificateAuthority), + resource.TestCheckResourceAttr(resourceName, "revocation_configuration.#", "1"), + resource.TestCheckResourceAttr(resourceName, "revocation_configuration.0.crl_configuration.#", "1"), + resource.TestCheckResourceAttr(resourceName, "revocation_configuration.0.crl_configuration.0.enabled", "false"), + ), + }, + }, + }) +} + +func TestAccAwsAcmpcaCertificateAuthority_RevocationConfiguration_CrlConfiguration_Enabled(t *testing.T) { + var certificateAuthority acmpca.CertificateAuthority + rName := acctest.RandomWithPrefix("tf-acc-test") + resourceName := "aws_acmpca_certificate_authority.test" + + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + Providers: testAccProviders, + CheckDestroy: testAccCheckAwsAcmpcaCertificateAuthorityDestroy, + Steps: []resource.TestStep{ + // Test creating revocation configuration on resource creation + { + Config: testAccAwsAcmpcaCertificateAuthorityConfig_RevocationConfiguration_CrlConfiguration_Enabled(rName, true), + Check: resource.ComposeTestCheckFunc( + testAccCheckAwsAcmpcaCertificateAuthorityExists(resourceName, &certificateAuthority), + resource.TestCheckResourceAttr(resourceName, "revocation_configuration.#", "1"), + resource.TestCheckResourceAttr(resourceName, "revocation_configuration.0.crl_configuration.#", "1"), + resource.TestCheckResourceAttr(resourceName, "revocation_configuration.0.crl_configuration.0.custom_cname", ""), + resource.TestCheckResourceAttr(resourceName, "revocation_configuration.0.crl_configuration.0.enabled", "true"), + resource.TestCheckResourceAttr(resourceName, "revocation_configuration.0.crl_configuration.0.expiration_in_days", "1"), + resource.TestCheckResourceAttr(resourceName, "revocation_configuration.0.crl_configuration.0.s3_bucket_name", rName), + ), + }, + // Test importing revocation configuration + { + ResourceName: resourceName, + ImportState: true, + ImportStateVerify: true, + }, + // Test disabling revocation configuration + { + Config: testAccAwsAcmpcaCertificateAuthorityConfig_RevocationConfiguration_CrlConfiguration_Enabled(rName, false), + Check: resource.ComposeTestCheckFunc( + testAccCheckAwsAcmpcaCertificateAuthorityExists(resourceName, &certificateAuthority), + resource.TestCheckResourceAttr(resourceName, "revocation_configuration.#", "1"), + resource.TestCheckResourceAttr(resourceName, "revocation_configuration.0.crl_configuration.#", "1"), + resource.TestCheckResourceAttr(resourceName, "revocation_configuration.0.crl_configuration.0.enabled", "false"), + ), + }, + // Test enabling revocation configuration + { + Config: testAccAwsAcmpcaCertificateAuthorityConfig_RevocationConfiguration_CrlConfiguration_Enabled(rName, true), + Check: resource.ComposeTestCheckFunc( + testAccCheckAwsAcmpcaCertificateAuthorityExists(resourceName, &certificateAuthority), + resource.TestCheckResourceAttr(resourceName, "revocation_configuration.#", "1"), + resource.TestCheckResourceAttr(resourceName, "revocation_configuration.0.crl_configuration.#", "1"), + resource.TestCheckResourceAttr(resourceName, "revocation_configuration.0.crl_configuration.0.custom_cname", ""), + resource.TestCheckResourceAttr(resourceName, "revocation_configuration.0.crl_configuration.0.enabled", "true"), + resource.TestCheckResourceAttr(resourceName, "revocation_configuration.0.crl_configuration.0.expiration_in_days", "1"), + resource.TestCheckResourceAttr(resourceName, "revocation_configuration.0.crl_configuration.0.s3_bucket_name", rName), + ), + }, + // Test removing revocation configuration on resource update + { + Config: testAccAwsAcmpcaCertificateAuthorityConfig_Required, + Check: resource.ComposeTestCheckFunc( + testAccCheckAwsAcmpcaCertificateAuthorityExists(resourceName, &certificateAuthority), + resource.TestCheckResourceAttr(resourceName, "revocation_configuration.#", "1"), + resource.TestCheckResourceAttr(resourceName, "revocation_configuration.0.crl_configuration.#", "1"), + resource.TestCheckResourceAttr(resourceName, "revocation_configuration.0.crl_configuration.0.enabled", "false"), + ), + }, + }, + }) +} + +func TestAccAwsAcmpcaCertificateAuthority_RevocationConfiguration_CrlConfiguration_ExpirationInDays(t *testing.T) { + var certificateAuthority acmpca.CertificateAuthority + rName := acctest.RandomWithPrefix("tf-acc-test") + resourceName := "aws_acmpca_certificate_authority.test" + + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + Providers: testAccProviders, + CheckDestroy: testAccCheckAwsAcmpcaCertificateAuthorityDestroy, + Steps: []resource.TestStep{ + // Test creating revocation configuration on resource creation + { + Config: testAccAwsAcmpcaCertificateAuthorityConfig_RevocationConfiguration_CrlConfiguration_ExpirationInDays(rName, 1), + Check: resource.ComposeTestCheckFunc( + testAccCheckAwsAcmpcaCertificateAuthorityExists(resourceName, &certificateAuthority), + resource.TestCheckResourceAttr(resourceName, "revocation_configuration.#", "1"), + resource.TestCheckResourceAttr(resourceName, "revocation_configuration.0.crl_configuration.#", "1"), + resource.TestCheckResourceAttr(resourceName, "revocation_configuration.0.crl_configuration.0.custom_cname", ""), + resource.TestCheckResourceAttr(resourceName, "revocation_configuration.0.crl_configuration.0.enabled", "true"), + resource.TestCheckResourceAttr(resourceName, "revocation_configuration.0.crl_configuration.0.expiration_in_days", "1"), + resource.TestCheckResourceAttr(resourceName, "revocation_configuration.0.crl_configuration.0.s3_bucket_name", rName), + ), + }, + // Test importing revocation configuration + { + ResourceName: resourceName, + ImportState: true, + ImportStateVerify: true, + }, + // Test updating revocation configuration + { + Config: testAccAwsAcmpcaCertificateAuthorityConfig_RevocationConfiguration_CrlConfiguration_ExpirationInDays(rName, 2), + Check: resource.ComposeTestCheckFunc( + testAccCheckAwsAcmpcaCertificateAuthorityExists(resourceName, &certificateAuthority), + resource.TestCheckResourceAttr(resourceName, "revocation_configuration.#", "1"), + resource.TestCheckResourceAttr(resourceName, "revocation_configuration.0.crl_configuration.#", "1"), + resource.TestCheckResourceAttr(resourceName, "revocation_configuration.0.crl_configuration.0.enabled", "true"), + resource.TestCheckResourceAttr(resourceName, "revocation_configuration.0.crl_configuration.0.expiration_in_days", "2"), + resource.TestCheckResourceAttr(resourceName, "revocation_configuration.0.crl_configuration.0.s3_bucket_name", rName), + ), + }, + // Test removing revocation configuration on resource update + { + Config: testAccAwsAcmpcaCertificateAuthorityConfig_Required, + Check: resource.ComposeTestCheckFunc( + testAccCheckAwsAcmpcaCertificateAuthorityExists(resourceName, &certificateAuthority), + resource.TestCheckResourceAttr(resourceName, "revocation_configuration.#", "1"), + resource.TestCheckResourceAttr(resourceName, "revocation_configuration.0.crl_configuration.#", "1"), + resource.TestCheckResourceAttr(resourceName, "revocation_configuration.0.crl_configuration.0.enabled", "false"), + ), + }, + }, + }) +} + +func TestAccAwsAcmpcaCertificateAuthority_Tags(t *testing.T) { + var certificateAuthority acmpca.CertificateAuthority + resourceName := "aws_acmpca_certificate_authority.test" + + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + Providers: testAccProviders, + CheckDestroy: testAccCheckAwsAcmpcaCertificateAuthorityDestroy, + Steps: []resource.TestStep{ + { + Config: testAccAwsAcmpcaCertificateAuthorityConfig_Tags_Single, + Check: resource.ComposeTestCheckFunc( + testAccCheckAwsAcmpcaCertificateAuthorityExists(resourceName, &certificateAuthority), + resource.TestCheckResourceAttr(resourceName, "tags.%", "1"), + resource.TestCheckResourceAttr(resourceName, "tags.tag1", "tag1value"), + ), + }, + { + Config: testAccAwsAcmpcaCertificateAuthorityConfig_Tags_SingleUpdated, + Check: resource.ComposeTestCheckFunc( + testAccCheckAwsAcmpcaCertificateAuthorityExists(resourceName, &certificateAuthority), + resource.TestCheckResourceAttr(resourceName, "tags.%", "1"), + resource.TestCheckResourceAttr(resourceName, "tags.tag1", "tag1value-updated"), + ), + }, + { + Config: testAccAwsAcmpcaCertificateAuthorityConfig_Tags_Multiple, + Check: resource.ComposeTestCheckFunc( + testAccCheckAwsAcmpcaCertificateAuthorityExists(resourceName, &certificateAuthority), + resource.TestCheckResourceAttr(resourceName, "tags.%", "2"), + resource.TestCheckResourceAttr(resourceName, "tags.tag1", "tag1value"), + resource.TestCheckResourceAttr(resourceName, "tags.tag2", "tag2value"), + ), + }, + { + Config: testAccAwsAcmpcaCertificateAuthorityConfig_Tags_Single, + Check: resource.ComposeTestCheckFunc( + testAccCheckAwsAcmpcaCertificateAuthorityExists(resourceName, &certificateAuthority), + resource.TestCheckResourceAttr(resourceName, "tags.%", "1"), + resource.TestCheckResourceAttr(resourceName, "tags.tag1", "tag1value"), + ), + }, + { + ResourceName: resourceName, + ImportState: true, + ImportStateVerify: true, + }, + }, + }) +} + +func testAccCheckAwsAcmpcaCertificateAuthorityDestroy(s *terraform.State) error { + conn := testAccProvider.Meta().(*AWSClient).acmpcaconn + + for _, rs := range s.RootModule().Resources { + if rs.Type != "aws_acmpca_certificate_authority" { + continue + } + + input := &acmpca.DescribeCertificateAuthorityInput{ + CertificateAuthorityArn: aws.String(rs.Primary.ID), + } + + output, err := conn.DescribeCertificateAuthority(input) + + if err != nil { + if isAWSErr(err, acmpca.ErrCodeResourceNotFoundException, "") { + return nil + } + return err + } + + if output != nil { + return fmt.Errorf("ACMPCA Certificate Authority %q still exists", rs.Primary.ID) + } + } + + return nil + +} + +func testAccCheckAwsAcmpcaCertificateAuthorityExists(resourceName string, certificateAuthority *acmpca.CertificateAuthority) resource.TestCheckFunc { + return func(s *terraform.State) error { + rs, ok := s.RootModule().Resources[resourceName] + if !ok { + return fmt.Errorf("Not found: %s", resourceName) + } + + conn := testAccProvider.Meta().(*AWSClient).acmpcaconn + input := &acmpca.DescribeCertificateAuthorityInput{ + CertificateAuthorityArn: aws.String(rs.Primary.ID), + } + + output, err := conn.DescribeCertificateAuthority(input) + + if err != nil { + return err + } + + if output == nil || output.CertificateAuthority == nil { + return fmt.Errorf("ACMPCA Certificate Authority %q does not exist", rs.Primary.ID) + } + + *certificateAuthority = *output.CertificateAuthority + + return nil + } +} + +func testAccAwsAcmpcaCertificateAuthorityConfig_Enabled(enabled bool) string { + return fmt.Sprintf(` +resource "aws_acmpca_certificate_authority" "test" { + enabled = %t + + certificate_authority_configuration { + key_algorithm = "RSA_4096" + signing_algorithm = "SHA512WITHRSA" + + subject { + common_name = "terraformtesting.com" + } + } +} +`, enabled) +} + +const testAccAwsAcmpcaCertificateAuthorityConfig_Required = ` +resource "aws_acmpca_certificate_authority" "test" { + certificate_authority_configuration { + key_algorithm = "RSA_4096" + signing_algorithm = "SHA512WITHRSA" + + subject { + common_name = "terraformtesting.com" + } + } +} +` + +func testAccAwsAcmpcaCertificateAuthorityConfig_RevocationConfiguration_CrlConfiguration_CustomCname(rName, customCname string) string { + return fmt.Sprintf(` +%s + +resource "aws_acmpca_certificate_authority" "test" { + certificate_authority_configuration { + key_algorithm = "RSA_4096" + signing_algorithm = "SHA512WITHRSA" + + subject { + common_name = "terraformtesting.com" + } + } + + revocation_configuration { + crl_configuration { + custom_cname = "%s" + enabled = true + expiration_in_days = 1 + s3_bucket_name = "${aws_s3_bucket.test.id}" + } + } + + depends_on = ["aws_s3_bucket_policy.test"] +} +`, testAccAwsAcmpcaCertificateAuthorityConfig_S3Bucket(rName), customCname) +} + +func testAccAwsAcmpcaCertificateAuthorityConfig_RevocationConfiguration_CrlConfiguration_Enabled(rName string, enabled bool) string { + return fmt.Sprintf(` +%s + +resource "aws_acmpca_certificate_authority" "test" { + certificate_authority_configuration { + key_algorithm = "RSA_4096" + signing_algorithm = "SHA512WITHRSA" + + subject { + common_name = "terraformtesting.com" + } + } + + revocation_configuration { + crl_configuration { + enabled = %t + expiration_in_days = 1 + s3_bucket_name = "${aws_s3_bucket.test.id}" + } + } +} +`, testAccAwsAcmpcaCertificateAuthorityConfig_S3Bucket(rName), enabled) +} + +func testAccAwsAcmpcaCertificateAuthorityConfig_RevocationConfiguration_CrlConfiguration_ExpirationInDays(rName string, expirationInDays int) string { + return fmt.Sprintf(` +%s + +resource "aws_acmpca_certificate_authority" "test" { + certificate_authority_configuration { + key_algorithm = "RSA_4096" + signing_algorithm = "SHA512WITHRSA" + + subject { + common_name = "terraformtesting.com" + } + } + + revocation_configuration { + crl_configuration { + enabled = true + expiration_in_days = %d + s3_bucket_name = "${aws_s3_bucket.test.id}" + } + } +} +`, testAccAwsAcmpcaCertificateAuthorityConfig_S3Bucket(rName), expirationInDays) +} + +func testAccAwsAcmpcaCertificateAuthorityConfig_S3Bucket(rName string) string { + return fmt.Sprintf(` +resource "aws_s3_bucket" "test" { + bucket = "%s" + force_destroy = true +} + +data "aws_iam_policy_document" "acmpca_bucket_access" { + statement { + actions = [ + "s3:GetBucketAcl", + "s3:GetBucketLocation", + "s3:PutObject", + "s3:PutObjectAcl", + ] + + resources = [ + "${aws_s3_bucket.test.arn}", + "${aws_s3_bucket.test.arn}/*", + ] + + principals { + identifiers = ["acm-pca.amazonaws.com"] + type = "Service" + } + } +} + +resource "aws_s3_bucket_policy" "test" { + bucket = "${aws_s3_bucket.test.id}" + policy = "${data.aws_iam_policy_document.acmpca_bucket_access.json}" +} +`, rName) +} + +const testAccAwsAcmpcaCertificateAuthorityConfig_Tags_Single = ` +resource "aws_acmpca_certificate_authority" "test" { + certificate_authority_configuration { + key_algorithm = "RSA_4096" + signing_algorithm = "SHA512WITHRSA" + + subject { + common_name = "terraformtesting.com" + } + } + + tags = { + tag1 = "tag1value" + } +} +` + +const testAccAwsAcmpcaCertificateAuthorityConfig_Tags_SingleUpdated = ` +resource "aws_acmpca_certificate_authority" "test" { + certificate_authority_configuration { + key_algorithm = "RSA_4096" + signing_algorithm = "SHA512WITHRSA" + + subject { + common_name = "terraformtesting.com" + } + } + + tags = { + tag1 = "tag1value-updated" + } +} +` + +const testAccAwsAcmpcaCertificateAuthorityConfig_Tags_Multiple = ` +resource "aws_acmpca_certificate_authority" "test" { + certificate_authority_configuration { + key_algorithm = "RSA_4096" + signing_algorithm = "SHA512WITHRSA" + + subject { + common_name = "terraformtesting.com" + } + } + + tags = { + tag1 = "tag1value" + tag2 = "tag2value" + } +} +` diff --git a/aws/tagsACMPCA.go b/aws/tagsACMPCA.go new file mode 100644 index 00000000000..3b72390ff6a --- /dev/null +++ b/aws/tagsACMPCA.go @@ -0,0 +1,67 @@ +package aws + +import ( + "log" + "regexp" + + "github.com/aws/aws-sdk-go/aws" + "github.com/aws/aws-sdk-go/service/acmpca" +) + +// 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 diffTagsACMPCA(oldTags, newTags []*acmpca.Tag) ([]*acmpca.Tag, []*acmpca.Tag) { + // First, we're creating everything we have + create := make(map[string]interface{}) + for _, t := range newTags { + create[aws.StringValue(t.Key)] = aws.StringValue(t.Value) + } + + // Build the list of what to remove + var remove []*acmpca.Tag + for _, t := range oldTags { + old, ok := create[aws.StringValue(t.Key)] + if !ok || old != aws.StringValue(t.Value) { + // Delete it! + remove = append(remove, t) + } + } + + return tagsFromMapACMPCA(create), remove +} + +func tagsFromMapACMPCA(m map[string]interface{}) []*acmpca.Tag { + result := []*acmpca.Tag{} + for k, v := range m { + result = append(result, &acmpca.Tag{ + Key: aws.String(k), + Value: aws.String(v.(string)), + }) + } + + return result +} + +func tagsToMapACMPCA(ts []*acmpca.Tag) map[string]string { + result := map[string]string{} + for _, t := range ts { + result[aws.StringValue(t.Key)] = aws.StringValue(t.Value) + } + + return result +} + +// compare a tag against a list of strings and checks if it should +// be ignored or not +func tagIgnoredACMPCA(t *acmpca.Tag) bool { + filter := []string{"^aws:"} + for _, v := range filter { + log.Printf("[DEBUG] Matching %v with %v\n", v, aws.StringValue(t.Key)) + if r, _ := regexp.MatchString(v, aws.StringValue(t.Key)); r == true { + log.Printf("[DEBUG] Found AWS specific tag %s (val: %s), ignoring.\n", aws.StringValue(t.Key), aws.StringValue(t.Value)) + return true + } + } + return false +} diff --git a/aws/tagsACMPCA_test.go b/aws/tagsACMPCA_test.go new file mode 100644 index 00000000000..60daf35457a --- /dev/null +++ b/aws/tagsACMPCA_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/acmpca" +) + +func TestDiffTagsACMPCA(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 := diffTagsACMPCA(tagsFromMapACMPCA(tc.Old), tagsFromMapACMPCA(tc.New)) + cm := tagsToMapACMPCA(c) + rm := tagsToMapACMPCA(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 TestIgnoringTagsACMPCA(t *testing.T) { + var ignoredTags []*acmpca.Tag + ignoredTags = append(ignoredTags, &acmpca.Tag{ + Key: aws.String("aws:cloudformation:logical-id"), + Value: aws.String("foo"), + }) + ignoredTags = append(ignoredTags, &acmpca.Tag{ + Key: aws.String("aws:foo:bar"), + Value: aws.String("baz"), + }) + for _, tag := range ignoredTags { + if !tagIgnoredACMPCA(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 3e08ecf2ac2..80b0226de43 100644 --- a/website/aws.erb +++ b/website/aws.erb @@ -28,6 +28,9 @@