diff --git a/.changelog/20073.txt b/.changelog/20073.txt new file mode 100644 index 000000000000..f4e6c9021684 --- /dev/null +++ b/.changelog/20073.txt @@ -0,0 +1,7 @@ +```release-note:bug +resource/aws_acm_certificate: Correctly handle SAN entries that match `domain_name` +``` + +```release-note:enhancement +resource/aws_acm_certificate_validation: Increase default resource Create (certificate issuance) timeout to 75 minutes +``` diff --git a/internal/service/acm/certificate.go b/internal/service/acm/certificate.go index 1e31986c1444..65ff3826f80e 100644 --- a/internal/service/acm/certificate.go +++ b/internal/service/acm/certificate.go @@ -8,6 +8,7 @@ import ( "fmt" "log" "regexp" + "strconv" "strings" "time" @@ -33,6 +34,8 @@ const ( // Maximum amount of time for ACM Certificate asynchronous DNS validation record assignment. // This timeout is unrelated to any creation or validation of those assigned DNS records. AcmCertificateDnsValidationAssignmentTimeout = 5 * time.Minute + + certificateValidationMethodNone = "NONE" ) func ResourceCertificate() *schema.Resource { @@ -41,27 +44,33 @@ func ResourceCertificate() *schema.Resource { Read: resourceCertificateRead, Update: resourceCertificateUpdate, Delete: resourceCertificateDelete, + Importer: &schema.ResourceImporter{ State: schema.ImportStatePassthrough, }, + Schema: map[string]*schema.Schema{ - "certificate_body": { + "arn": { Type: schema.TypeString, - Optional: true, + Computed: true, }, - "certificate_chain": { - Type: schema.TypeString, - Optional: true, + "certificate_authority_arn": { + Type: schema.TypeString, + Optional: true, + ForceNew: true, + ValidateFunc: verify.ValidARN, + ConflictsWith: []string{"certificate_body", "private_key", "validation_method"}, }, - "private_key": { - Type: schema.TypeString, - Optional: true, - Sensitive: true, + "certificate_body": { + Type: schema.TypeString, + Optional: true, + RequiredWith: []string{"private_key"}, + ConflictsWith: []string{"certificate_authority_arn", "domain_name", "validation_method"}, }, - "certificate_authority_arn": { - Type: schema.TypeString, - Optional: true, - ForceNew: true, + "certificate_chain": { + Type: schema.TypeString, + Optional: true, + ConflictsWith: []string{"certificate_authority_arn", "domain_name", "validation_method"}, }, "domain_name": { // AWS Provider 3.0.0 aws_route53_zone references no longer contain a @@ -71,37 +80,9 @@ func ResourceCertificate() *schema.Resource { Optional: true, Computed: true, ForceNew: true, - ConflictsWith: []string{"private_key", "certificate_body", "certificate_chain"}, ValidateFunc: validation.StringDoesNotMatch(regexp.MustCompile(`\.$`), "cannot end with a period"), - }, - "subject_alternative_names": { - Type: schema.TypeSet, - Optional: true, - Computed: true, - ForceNew: true, - Elem: &schema.Schema{ - // AWS Provider 3.0.0 aws_route53_zone references no longer contain a - // trailing period, no longer requiring a custom StateFunc - // to prevent ACM API error - Type: schema.TypeString, - ValidateFunc: validation.All( - validation.StringLenBetween(1, 253), - validation.StringDoesNotMatch(regexp.MustCompile(`\.$`), "cannot end with a period"), - ), - }, - Set: schema.HashString, - ConflictsWith: []string{"private_key", "certificate_body", "certificate_chain"}, - }, - "validation_method": { - Type: schema.TypeString, - Optional: true, - Computed: true, - ForceNew: true, - ConflictsWith: []string{"private_key", "certificate_body", "certificate_chain", "certificate_authority_arn"}, - }, - "arn": { - Type: schema.TypeString, - Computed: true, + ExactlyOneOf: []string{"domain_name", "private_key"}, + ConflictsWith: []string{"certificate_body", "certificate_chain", "private_key"}, }, "domain_validation_options": { Type: schema.TypeSet, @@ -128,51 +109,80 @@ func ResourceCertificate() *schema.Resource { }, Set: acmDomainValidationOptionsHash, }, - "validation_emails": { - Type: schema.TypeList, - Computed: true, - Elem: &schema.Schema{Type: schema.TypeString}, - }, "options": { Type: schema.TypeList, Optional: true, MaxItems: 1, - DiffSuppressFunc: func(k, old, new string, d *schema.ResourceData) bool { - if _, ok := d.GetOk("private_key"); ok { - // ignore diffs for imported certs; they have a different logging preference - // default to requested certs which can't be changed by the ImportCertificate API - return true - } - // behave just like verify.SuppressMissingOptionalConfigurationBlock() for requested certs - return old == "1" && new == "0" - }, Elem: &schema.Resource{ Schema: map[string]*schema.Schema{ "certificate_transparency_logging_preference": { Type: schema.TypeString, Optional: true, - Default: acm.CertificateTransparencyLoggingPreferenceEnabled, ForceNew: true, - ConflictsWith: []string{"private_key", "certificate_body", "certificate_chain"}, - ValidateFunc: validation.StringInSlice([]string{ - acm.CertificateTransparencyLoggingPreferenceEnabled, - acm.CertificateTransparencyLoggingPreferenceDisabled, - }, false), + Default: acm.CertificateTransparencyLoggingPreferenceEnabled, + ValidateFunc: validation.StringInSlice(acm.CertificateTransparencyLoggingPreference_Values(), false), + ConflictsWith: []string{"certificate_body", "certificate_chain", "private_key"}, }, }, }, + DiffSuppressFunc: func(k, old, new string, d *schema.ResourceData) bool { + if _, ok := d.GetOk("private_key"); ok { + // ignore diffs for imported certs; they have a different logging preference + // default to requested certs which can't be changed by the ImportCertificate API + return true + } + // behave just like verify.SuppressMissingOptionalConfigurationBlock() for requested certs + return old == "1" && new == "0" + }, + }, + "private_key": { + Type: schema.TypeString, + Optional: true, + Sensitive: true, + ExactlyOneOf: []string{"domain_name", "private_key"}, }, "status": { Type: schema.TypeString, Computed: true, }, + "subject_alternative_names": { + Type: schema.TypeSet, + Optional: true, + Computed: true, + ForceNew: true, + Elem: &schema.Schema{ + // AWS Provider 3.0.0 aws_route53_zone references no longer contain a + // trailing period, no longer requiring a custom StateFunc + // to prevent ACM API error + Type: schema.TypeString, + ValidateFunc: validation.All( + validation.StringLenBetween(1, 253), + validation.StringDoesNotMatch(regexp.MustCompile(`\.$`), "cannot end with a period"), + ), + }, + ConflictsWith: []string{"certificate_body", "certificate_chain", "private_key"}, + }, "tags": tftags.TagsSchema(), "tags_all": tftags.TagsSchemaComputed(), + "validation_emails": { + Type: schema.TypeList, + Computed: true, + Elem: &schema.Schema{Type: schema.TypeString}, + }, + "validation_method": { + Type: schema.TypeString, + Optional: true, + Computed: true, + ForceNew: true, + ValidateFunc: validation.StringInSlice(append(acm.ValidationMethod_Values(), certificateValidationMethodNone), false), + ConflictsWith: []string{"certificate_authority_arn", "certificate_body", "certificate_chain", "private_key"}, + }, }, + CustomizeDiff: customdiff.Sequence( func(_ context.Context, diff *schema.ResourceDiff, v interface{}) error { // Attempt to calculate the domain validation options based on domains present in domain_name and subject_alternative_names - if diff.Get("validation_method").(string) == "DNS" && (diff.HasChange("domain_name") || diff.HasChange("subject_alternative_names")) { + if diff.Get("validation_method").(string) == acm.ValidationMethodDns && (diff.HasChange("domain_name") || diff.HasChange("subject_alternative_names")) { domainValidationOptionsList := []interface{}{map[string]interface{}{ // AWS Provider 3.0 -- plan-time validation prevents "domain_name" // argument to accept a string with trailing period; thus, trim of trailing period @@ -204,6 +214,19 @@ func ResourceCertificate() *schema.Resource { } } + // ACM automatically adds the domain_name value to the list of SANs. Mimic ACM's behavior + // so that the user doesn't need to explicitly set it themselves. + if diff.HasChange("domain_name") || diff.HasChange("subject_alternative_names") { + domain_name := diff.Get("domain_name").(string) + + if sanSet, ok := diff.Get("subject_alternative_names").(*schema.Set); ok { + sanSet.Add(domain_name) + if err := diff.SetNew("subject_alternative_names", sanSet); err != nil { + return fmt.Errorf("error setting new subject_alternative_names diff: %w", err) + } + } + } + return nil }, verify.SetTagsDiff, @@ -212,92 +235,80 @@ func ResourceCertificate() *schema.Resource { } func resourceCertificateCreate(d *schema.ResourceData, meta interface{}) error { - if _, ok := d.GetOk("domain_name"); ok { - if _, ok := d.GetOk("certificate_authority_arn"); ok { - return resourceCertificateCreateRequested(d, meta) - } - - if _, ok := d.GetOk("validation_method"); !ok { - return errors.New("validation_method must be set when creating a certificate") - } - return resourceCertificateCreateRequested(d, meta) - } else if _, ok := d.GetOk("private_key"); ok { - if _, ok := d.GetOk("certificate_body"); !ok { - return errors.New("certificate_body must be set when importing a certificate with private_key") - } - return resourceCertificateCreateImported(d, meta) - } - return errors.New("certificate must be imported (private_key) or created (domain_name)") -} - -func resourceCertificateCreateImported(d *schema.ResourceData, meta interface{}) error { conn := meta.(*conns.AWSClient).ACMConn defaultTagsConfig := meta.(*conns.AWSClient).DefaultTagsConfig tags := defaultTagsConfig.MergeTags(tftags.New(d.Get("tags").(map[string]interface{}))) - input := &acm.ImportCertificateInput{ - Certificate: []byte(d.Get("certificate_body").(string)), - PrivateKey: []byte(d.Get("private_key").(string)), - } + if _, ok := d.GetOk("domain_name"); ok { + _, v1 := d.GetOk("certificate_authority_arn") + _, v2 := d.GetOk("validation_method") - if v, ok := d.GetOk("certificate_chain"); ok { - input.CertificateChain = []byte(v.(string)) - } + if !v1 && !v2 { + return errors.New("`certificate_authority_arn` or `validation_method` must be set when creating an ACM certificate") + } - if len(tags) > 0 { - input.Tags = Tags(tags.IgnoreAWS()) - } + domainName := d.Get("domain_name").(string) + input := &acm.RequestCertificateInput{ + DomainName: aws.String(domainName), + IdempotencyToken: aws.String(resource.PrefixedUniqueId("tf")), // 32 character limit + } - output, err := conn.ImportCertificate(input) + if v, ok := d.GetOk("certificate_authority_arn"); ok { + input.CertificateAuthorityArn = aws.String(v.(string)) + } - if err != nil { - return fmt.Errorf("error importing ACM Certificate: %w", err) - } + if v, ok := d.GetOk("options"); ok && len(v.([]interface{})) > 0 && v.([]interface{})[0] != nil { + input.Options = expandCertificateOptions(v.([]interface{})[0].(map[string]interface{})) + } - d.SetId(aws.StringValue(output.CertificateArn)) + if v, ok := d.GetOk("subject_alternative_names"); ok { + for _, v := range v.(*schema.Set).List() { + input.SubjectAlternativeNames = append(input.SubjectAlternativeNames, aws.String(v.(string))) + } + } - return resourceCertificateRead(d, meta) -} + if v, ok := d.GetOk("validation_method"); ok { + input.ValidationMethod = aws.String(v.(string)) + } -func resourceCertificateCreateRequested(d *schema.ResourceData, meta interface{}) error { - conn := meta.(*conns.AWSClient).ACMConn - defaultTagsConfig := meta.(*conns.AWSClient).DefaultTagsConfig - tags := defaultTagsConfig.MergeTags(tftags.New(d.Get("tags").(map[string]interface{}))) + if len(tags) > 0 { + input.Tags = Tags(tags.IgnoreAWS()) + } - params := &acm.RequestCertificateInput{ - DomainName: aws.String(d.Get("domain_name").(string)), - IdempotencyToken: aws.String(resource.PrefixedUniqueId("tf")), // 32 character limit - Options: expandAcmCertificateOptions(d.Get("options").([]interface{})), - } + log.Printf("[DEBUG] Requesting ACM Certificate: %s", input) + output, err := conn.RequestCertificate(input) - if len(tags) > 0 { - params.Tags = Tags(tags.IgnoreAWS()) - } + if err != nil { + return fmt.Errorf("requesting ACM Certificate (%s): %w", domainName, err) + } - if caARN, ok := d.GetOk("certificate_authority_arn"); ok { - params.CertificateAuthorityArn = aws.String(caARN.(string)) - } + d.SetId(aws.StringValue(output.CertificateArn)) + } else { + input := &acm.ImportCertificateInput{ + Certificate: []byte(d.Get("certificate_body").(string)), + PrivateKey: []byte(d.Get("private_key").(string)), + } - if sans, ok := d.GetOk("subject_alternative_names"); ok { - subjectAlternativeNames := make([]*string, len(sans.(*schema.Set).List())) - for i, sanRaw := range sans.(*schema.Set).List() { - subjectAlternativeNames[i] = aws.String(sanRaw.(string)) + if v, ok := d.GetOk("certificate_chain"); ok { + input.CertificateChain = []byte(v.(string)) } - params.SubjectAlternativeNames = subjectAlternativeNames - } - if v, ok := d.GetOk("validation_method"); ok { - params.ValidationMethod = aws.String(v.(string)) - } + if len(tags) > 0 { + input.Tags = Tags(tags.IgnoreAWS()) + } - log.Printf("[DEBUG] ACM Certificate Request: %#v", params) - resp, err := conn.RequestCertificate(params) + output, err := conn.ImportCertificate(input) - if err != nil { - return fmt.Errorf("Error requesting certificate: %s", err) + if err != nil { + return fmt.Errorf("importing ACM Certificate: %w", err) + } + + d.SetId(aws.StringValue(output.CertificateArn)) } - d.SetId(aws.StringValue(resp.CertificateArn)) + if _, err := waitCertificateDomainValidationsAvailable(conn, d.Id(), AcmCertificateDnsValidationAssignmentTimeout); err != nil { + return fmt.Errorf("waiting for ACM Certificate (%s) to be issued: %w", d.Id(), err) + } return resourceCertificateRead(d, meta) } @@ -307,93 +318,56 @@ func resourceCertificateRead(d *schema.ResourceData, meta interface{}) error { defaultTagsConfig := meta.(*conns.AWSClient).DefaultTagsConfig ignoreTagsConfig := meta.(*conns.AWSClient).IgnoreTagsConfig - params := &acm.DescribeCertificateInput{ - CertificateArn: aws.String(d.Id()), - } - - return resource.Retry(AcmCertificateDnsValidationAssignmentTimeout, func() *resource.RetryError { - resp, err := conn.DescribeCertificate(params) - - if !d.IsNewResource() && tfawserr.ErrCodeEquals(err, acm.ErrCodeResourceNotFoundException) { - log.Printf("[WARN] ACM Certificate (%s) not found, removing from state", d.Id()) - d.SetId("") - return nil - } - - if err != nil { - return resource.NonRetryableError(fmt.Errorf("error reading ACM Certificate (%s): %w", d.Id(), err)) - } - - if resp == nil || resp.Certificate == nil { - return resource.NonRetryableError(fmt.Errorf("error reading ACM Certificate (%s): empty response", d.Id())) - } - - if !d.IsNewResource() && aws.StringValue(resp.Certificate.Status) == acm.CertificateStatusValidationTimedOut { - log.Printf("[WARN] ACM Certificate (%s) validation timed out, removing from state", d.Id()) - d.SetId("") - return nil - } - - d.Set("domain_name", resp.Certificate.DomainName) - d.Set("arn", resp.Certificate.CertificateArn) - d.Set("certificate_authority_arn", resp.Certificate.CertificateAuthorityArn) - - 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) - } + certificate, err := FindCertificateByARN(conn, d.Id()) - d.Set("validation_method", resourceCertificateValidationMethod(resp.Certificate)) - - if err := d.Set("options", flattenAcmCertificateOptions(resp.Certificate.Options)); err != nil { - return resource.NonRetryableError(fmt.Errorf("error setting certificate options: %s", err)) - } + if !d.IsNewResource() && tfresource.NotFound(err) { + log.Printf("[WARN] ACM Certificate %s not found, removing from state", d.Id()) + d.SetId("") + return nil + } - d.Set("status", resp.Certificate.Status) + if err != nil { + return fmt.Errorf("reading ACM Certificate (%s): %w", d.Id(), err) + } - tags, err := ListTags(conn, d.Id()) + domainValidationOptions, validationEmails := flattenDomainValidations(certificate.DomainValidationOptions) - if err != nil { - return resource.NonRetryableError(fmt.Errorf("error listing tags for ACM Certificate (%s): %s", d.Id(), err)) + d.Set("arn", certificate.CertificateArn) + d.Set("certificate_authority_arn", certificate.CertificateAuthorityArn) + d.Set("domain_name", certificate.DomainName) + if err := d.Set("domain_validation_options", domainValidationOptions); err != nil { + return fmt.Errorf("error setting domain_validation_options: %w", err) + } + if certificate.Options != nil { + if err := d.Set("options", []interface{}{flattenCertificateOptions(certificate.Options)}); err != nil { + return fmt.Errorf("error setting options: %w", err) } + } else { + d.Set("options", nil) + } + d.Set("status", certificate.Status) + d.Set("subject_alternative_names", aws.StringValueSlice(certificate.SubjectAlternativeNames)) + d.Set("validation_emails", validationEmails) + d.Set("validation_method", certificateValidationMethod(certificate)) - tags = tags.IgnoreAWS().IgnoreConfig(ignoreTagsConfig) + tags, err := ListTags(conn, d.Id()) - //lintignore:AWSR002 - if err := d.Set("tags", tags.RemoveDefaultConfig(defaultTagsConfig).Map()); err != nil { - return resource.NonRetryableError(fmt.Errorf("error setting tags: %w", err)) - } + if err != nil { + return fmt.Errorf("listing tags for ACM Certificate (%s): %w", d.Id(), err) + } - if err := d.Set("tags_all", tags.Map()); err != nil { - return resource.NonRetryableError(fmt.Errorf("error setting tags_all: %w", err)) - } + tags = tags.IgnoreAWS().IgnoreConfig(ignoreTagsConfig) - return nil - }) -} + //lintignore:AWSR002 + if err := d.Set("tags", tags.RemoveDefaultConfig(defaultTagsConfig).Map()); err != nil { + return fmt.Errorf("error setting tags: %w", err) + } -func resourceCertificateValidationMethod(certificate *acm.CertificateDetail) string { - if aws.StringValue(certificate.Type) == acm.CertificateTypeAmazonIssued { - for _, domainValidation := range certificate.DomainValidationOptions { - if domainValidation.ValidationMethod != nil { - return aws.StringValue(domainValidation.ValidationMethod) - } - } + if err := d.Set("tags_all", tags.Map()); err != nil { + return fmt.Errorf("error setting tags_all: %w", err) } - return "NONE" + return nil } func resourceCertificateUpdate(d *schema.ResourceData, meta interface{}) error { @@ -420,108 +394,56 @@ func resourceCertificateUpdate(d *schema.ResourceData, meta interface{}) error { _, err := conn.ImportCertificate(input) if err != nil { - return fmt.Errorf("error re-importing ACM Certificate (%s): %w", d.Id(), err) + return fmt.Errorf("re-importing ACM Certificate (%s): %w", d.Id(), err) } } } if d.HasChange("tags_all") { o, n := d.GetChange("tags_all") - if err := UpdateTags(conn, d.Id(), o, n); err != nil { - return fmt.Errorf("error updating tags: %s", err) - } - } - return resourceCertificateRead(d, meta) -} - -func cleanUpSubjectAlternativeNames(cert *acm.CertificateDetail) []string { - sans := cert.SubjectAlternativeNames - vs := make([]string, 0) - for _, v := range sans { - if aws.StringValue(v) != aws.StringValue(cert.DomainName) { - vs = append(vs, aws.StringValue(v)) - } - } - return vs -} -func convertValidationOptions(certificate *acm.CertificateDetail) ([]map[string]interface{}, []string, error) { - var domainValidationResult []map[string]interface{} - var emailValidationResult []string - - switch aws.StringValue(certificate.Type) { - case acm.CertificateTypeAmazonIssued: - if len(certificate.DomainValidationOptions) == 0 && aws.StringValue(certificate.Status) == acm.DomainStatusPendingValidation { - log.Printf("[DEBUG] No validation options need to retry.") - return nil, nil, fmt.Errorf("No validation options need to retry.") - } - for _, o := range certificate.DomainValidationOptions { - if o.ResourceRecord != nil { - validationOption := map[string]interface{}{ - "domain_name": aws.StringValue(o.DomainName), - "resource_record_name": aws.StringValue(o.ResourceRecord.Name), - "resource_record_type": aws.StringValue(o.ResourceRecord.Type), - "resource_record_value": aws.StringValue(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 if o.ValidationStatus == nil || aws.StringValue(o.ValidationStatus) == acm.DomainStatusPendingValidation { - log.Printf("[DEBUG] Asynchronous ACM service domain validation assignment not complete, need to retry: %#v", o) - return nil, nil, fmt.Errorf("asynchronous ACM service domain validation assignment not complete, need to retry: %#v", o) - } - } - case acm.CertificateTypePrivate: - // While ACM PRIVATE certificates do not need to be validated, there is a slight delay for - // the API to fill in all certificate details, which is during the PENDING_VALIDATION status. - if aws.StringValue(certificate.Status) == acm.DomainStatusPendingValidation { - return nil, nil, fmt.Errorf("certificate still pending issuance") + if err := UpdateTags(conn, d.Id(), o, n); err != nil { + return fmt.Errorf("error updating tags: %w", err) } } - return domainValidationResult, emailValidationResult, nil + return resourceCertificateRead(d, meta) } func resourceCertificateDelete(d *schema.ResourceData, meta interface{}) error { conn := meta.(*conns.AWSClient).ACMConn log.Printf("[INFO] Deleting ACM Certificate: %s", d.Id()) - - params := &acm.DeleteCertificateInput{ - CertificateArn: aws.String(d.Id()), - } - - err := resource.Retry(AcmCertificateCrossServicePropagationTimeout, func() *resource.RetryError { - _, err := conn.DeleteCertificate(params) - - if tfawserr.ErrCodeEquals(err, acm.ErrCodeResourceInUseException) { - return resource.RetryableError(err) - } - - if err != nil { - return resource.NonRetryableError(err) - } - - return nil - }) - - if tfresource.TimedOut(err) { - _, err = conn.DeleteCertificate(params) - } + _, err := tfresource.RetryWhenAWSErrCodeEquals(AcmCertificateCrossServicePropagationTimeout, + func() (interface{}, error) { + return conn.DeleteCertificate(&acm.DeleteCertificateInput{ + CertificateArn: aws.String(d.Id()), + }) + }, acm.ErrCodeResourceInUseException) if tfawserr.ErrCodeEquals(err, acm.ErrCodeResourceNotFoundException) { return nil } if err != nil { - return fmt.Errorf("error deleting ACM Certificate (%s): %w", d.Id(), err) + return fmt.Errorf("deleting ACM Certificate (%s): %w", d.Id(), err) } return nil } +func certificateValidationMethod(certificate *acm.CertificateDetail) string { + if aws.StringValue(certificate.Type) == acm.CertificateTypeAmazonIssued { + for _, v := range certificate.DomainValidationOptions { + if v.ValidationMethod != nil { + return aws.StringValue(v.ValidationMethod) + } + } + } + + return certificateValidationMethodNone +} + func acmDomainValidationOptionsHash(v interface{}) int { m, ok := v.(map[string]interface{}) @@ -536,28 +458,89 @@ func acmDomainValidationOptionsHash(v interface{}) int { return 0 } -func expandAcmCertificateOptions(l []interface{}) *acm.CertificateOptions { - if len(l) == 0 || l[0] == nil { +func expandCertificateOptions(tfMap map[string]interface{}) *acm.CertificateOptions { + if tfMap == nil { + return nil + } + + apiObject := &acm.CertificateOptions{} + + if v, ok := tfMap["certificate_transparency_logging_preference"].(string); ok && v != "" { + apiObject.CertificateTransparencyLoggingPreference = aws.String(v) + } + + return apiObject +} + +func flattenCertificateOptions(apiObject *acm.CertificateOptions) map[string]interface{} { + if apiObject == nil { return nil } - m := l[0].(map[string]interface{}) + tfMap := map[string]interface{}{} + + if v := apiObject.CertificateTransparencyLoggingPreference; v != nil { + tfMap["certificate_transparency_logging_preference"] = aws.StringValue(v) + } + + return tfMap +} + +func flattenDomainValidation(apiObject *acm.DomainValidation) (map[string]interface{}, []string) { + if apiObject == nil { + return nil, nil + } + + tfMap := map[string]interface{}{} + var tfStrings []string + + if v := apiObject.ResourceRecord; v != nil { + if v := apiObject.DomainName; v != nil { + tfMap["domain_name"] = aws.StringValue(v) + } - options := &acm.CertificateOptions{} + if v := v.Name; v != nil { + tfMap["resource_record_name"] = aws.StringValue(v) + } - if v, ok := m["certificate_transparency_logging_preference"]; ok { - options.CertificateTransparencyLoggingPreference = aws.String(v.(string)) + if v := v.Type; v != nil { + tfMap["resource_record_type"] = aws.StringValue(v) + } + + if v := v.Value; v != nil { + tfMap["resource_record_value"] = aws.StringValue(v) + } } - return options + tfStrings = aws.StringValueSlice(apiObject.ValidationEmails) + + return tfMap, tfStrings } -func flattenAcmCertificateOptions(co *acm.CertificateOptions) []interface{} { - m := map[string]interface{}{ - "certificate_transparency_logging_preference": aws.StringValue(co.CertificateTransparencyLoggingPreference), +func flattenDomainValidations(apiObjects []*acm.DomainValidation) ([]interface{}, []string) { + if len(apiObjects) == 0 { + return nil, nil + } + + var tfList []interface{} + var tfStrings []string + + for _, apiObject := range apiObjects { + if apiObject == nil { + continue + } + + v1, v2 := flattenDomainValidation(apiObject) + + if len(v1) > 0 { + tfList = append(tfList, v1) + } + if len(v2) > 0 { + tfStrings = append(tfStrings, v2...) + } } - return []interface{}{m} + return tfList, tfStrings } func isChangeNormalizeCertRemoval(oldRaw, newRaw interface{}) bool { @@ -573,20 +556,116 @@ func isChangeNormalizeCertRemoval(oldRaw, newRaw interface{}) bool { return false } + // strip CRs from raw literals. Lifted from go/scanner/scanner.go + // See https://github.com/golang/go/blob/release-branch.go1.6/src/go/scanner/scanner.go#L479 + stripCR := func(b []byte) []byte { + c := make([]byte, len(b)) + i := 0 + for _, ch := range b { + if ch != '\r' { + c[i] = ch + i++ + } + } + return c[:i] + } + newCleanVal := sha1.Sum(stripCR([]byte(strings.TrimSpace(new)))) return hex.EncodeToString(newCleanVal[:]) == old } -// strip CRs from raw literals. Lifted from go/scanner/scanner.go -// See https://github.com/golang/go/blob/release-branch.go1.6/src/go/scanner/scanner.go#L479 -func stripCR(b []byte) []byte { - c := make([]byte, len(b)) - i := 0 - for _, ch := range b { - if ch != '\r' { - c[i] = ch - i++ +func findCertificate(conn *acm.ACM, input *acm.DescribeCertificateInput) (*acm.CertificateDetail, error) { + output, err := conn.DescribeCertificate(input) + + if tfawserr.ErrCodeEquals(err, acm.ErrCodeResourceNotFoundException) { + return nil, &resource.NotFoundError{ + LastError: err, + LastRequest: input, } } - return c[:i] + + if err != nil { + return nil, err + } + + if output == nil || output.Certificate == nil { + return nil, tfresource.NewEmptyResultError(input) + } + + return output.Certificate, nil +} + +func FindCertificateByARN(conn *acm.ACM, arn string) (*acm.CertificateDetail, error) { + input := &acm.DescribeCertificateInput{ + CertificateArn: aws.String(arn), + } + + output, err := findCertificate(conn, input) + + if err != nil { + return nil, err + } + + if status := aws.StringValue(output.Status); status == acm.CertificateStatusValidationTimedOut { + return nil, &resource.NotFoundError{ + Message: status, + LastRequest: input, + } + } + + return output, nil +} + +func statusCertificateDomainValidationsAvailable(conn *acm.ACM, arn string) resource.StateRefreshFunc { + return func() (interface{}, string, error) { + certificate, err := FindCertificateByARN(conn, arn) + + if tfresource.NotFound(err) { + return nil, "", nil + } + + if err != nil { + return nil, "", err + } + + domainValidationsAvailable := true + + switch aws.StringValue(certificate.Type) { + case acm.CertificateTypeAmazonIssued: + domainValidationsAvailable = false + + for _, v := range certificate.DomainValidationOptions { + if v.ResourceRecord != nil || len(v.ValidationEmails) > 0 || (aws.StringValue(v.ValidationStatus) == acm.DomainStatusSuccess) { + domainValidationsAvailable = true + + break + } + } + + case acm.CertificateTypePrivate: + // While ACM PRIVATE certificates do not need to be validated, there is a slight delay for + // the API to fill in all certificate details, which is during the PENDING_VALIDATION status. + if aws.StringValue(certificate.Status) == acm.DomainStatusPendingValidation { + domainValidationsAvailable = false + } + } + + return certificate, strconv.FormatBool(domainValidationsAvailable), nil + } +} + +func waitCertificateDomainValidationsAvailable(conn *acm.ACM, arn string, timeout time.Duration) (*acm.CertificateDetail, error) { + stateConf := &resource.StateChangeConf{ + Target: []string{strconv.FormatBool(true)}, + Refresh: statusCertificateDomainValidationsAvailable(conn, arn), + Timeout: timeout, + } + + outputRaw, err := stateConf.WaitForState() + + if output, ok := outputRaw.(*acm.CertificateDetail); ok { + return output, err + } + + return nil, err } diff --git a/internal/service/acm/certificate_data_source_test.go b/internal/service/acm/certificate_data_source_test.go index 63544ea9a848..f60aa5f8dc0d 100644 --- a/internal/service/acm/certificate_data_source_test.go +++ b/internal/service/acm/certificate_data_source_test.go @@ -197,7 +197,7 @@ func TestAccACMCertificateDataSource_keyTypes(t *testing.T) { resourceName := "aws_acm_certificate.test" dataSourceName := "data.aws_acm_certificate.test" key := acctest.TLSRSAPrivateKeyPEM(4096) - certificate := acctest.TLSRSAX509SelfSignedCertificatePEM(key, "example.com") + certificate := acctest.TLSRSAX509SelfSignedCertificatePEM(key, acctest.RandomDomain().String()) rName := sdkacctest.RandomWithPrefix(acctest.ResourcePrefix) resource.ParallelTest(t, resource.TestCase{ diff --git a/internal/service/acm/certificate_test.go b/internal/service/acm/certificate_test.go index 9b830f51db33..d78e2317511c 100644 --- a/internal/service/acm/certificate_test.go +++ b/internal/service/acm/certificate_test.go @@ -7,19 +7,20 @@ import ( "strings" "testing" - "github.com/aws/aws-sdk-go/aws" "github.com/aws/aws-sdk-go/service/acm" - "github.com/hashicorp/aws-sdk-go-base/v2/awsv1shim/v2/tfawserr" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource" "github.com/hashicorp/terraform-plugin-sdk/v2/terraform" "github.com/hashicorp/terraform-provider-aws/internal/acctest" "github.com/hashicorp/terraform-provider-aws/internal/conns" + tfacm "github.com/hashicorp/terraform-provider-aws/internal/service/acm" + "github.com/hashicorp/terraform-provider-aws/internal/tfresource" ) func TestAccACMCertificate_emailValidation(t *testing.T) { - resourceName := "aws_acm_certificate.cert" + resourceName := "aws_acm_certificate.test" rootDomain := acctest.ACMCertificateDomainFromEnv(t) domain := acctest.ACMCertificateRandomSubDomain(rootDomain) + var v acm.CertificateDetail resource.ParallelTest(t, resource.TestCase{ PreCheck: func() { acctest.PreCheck(t) }, @@ -29,12 +30,15 @@ func TestAccACMCertificate_emailValidation(t *testing.T) { Steps: []resource.TestStep{ { Config: testAccAcmCertificateConfig(domain, acm.ValidationMethodEmail), - Check: resource.ComposeTestCheckFunc( + Check: resource.ComposeAggregateTestCheckFunc( + testAccCheckAcmCertificateExists(resourceName, &v), acctest.MatchResourceAttrRegionalARN(resourceName, "arn", "acm", regexp.MustCompile("certificate/.+$")), resource.TestCheckResourceAttr(resourceName, "domain_name", domain), resource.TestCheckResourceAttr(resourceName, "domain_validation_options.#", "0"), resource.TestCheckResourceAttr(resourceName, "status", acm.CertificateStatusPendingValidation), - resource.TestCheckResourceAttr(resourceName, "subject_alternative_names.#", "0"), + resource.TestCheckResourceAttr(resourceName, "subject_alternative_names.#", "1"), + resource.TestCheckTypeSetElemAttr(resourceName, "subject_alternative_names.*", domain), + acctest.CheckResourceAttrGreaterThanValue(resourceName, "validation_emails.#", "0"), resource.TestMatchResourceAttr(resourceName, "validation_emails.0", regexp.MustCompile(`^[^@]+@.+$`)), resource.TestCheckResourceAttr(resourceName, "validation_method", acm.ValidationMethodEmail), ), @@ -49,9 +53,10 @@ func TestAccACMCertificate_emailValidation(t *testing.T) { } func TestAccACMCertificate_dnsValidation(t *testing.T) { - resourceName := "aws_acm_certificate.cert" + resourceName := "aws_acm_certificate.test" rootDomain := acctest.ACMCertificateDomainFromEnv(t) domain := acctest.ACMCertificateRandomSubDomain(rootDomain) + var v acm.CertificateDetail resource.ParallelTest(t, resource.TestCase{ PreCheck: func() { acctest.PreCheck(t) }, @@ -62,6 +67,7 @@ func TestAccACMCertificate_dnsValidation(t *testing.T) { { Config: testAccAcmCertificateConfig(domain, acm.ValidationMethodDns), Check: resource.ComposeTestCheckFunc( + testAccCheckAcmCertificateExists(resourceName, &v), acctest.MatchResourceAttrRegionalARN(resourceName, "arn", "acm", regexp.MustCompile("certificate/.+$")), resource.TestCheckResourceAttr(resourceName, "domain_name", domain), resource.TestCheckResourceAttr(resourceName, "domain_validation_options.#", "1"), @@ -70,7 +76,8 @@ func TestAccACMCertificate_dnsValidation(t *testing.T) { "resource_record_type": "CNAME", }), resource.TestCheckResourceAttr(resourceName, "status", acm.CertificateStatusPendingValidation), - resource.TestCheckResourceAttr(resourceName, "subject_alternative_names.#", "0"), + resource.TestCheckResourceAttr(resourceName, "subject_alternative_names.#", "1"), + resource.TestCheckTypeSetElemAttr(resourceName, "subject_alternative_names.*", domain), resource.TestCheckResourceAttr(resourceName, "validation_emails.#", "0"), resource.TestCheckResourceAttr(resourceName, "validation_method", acm.ValidationMethodDns), ), @@ -85,8 +92,9 @@ func TestAccACMCertificate_dnsValidation(t *testing.T) { } func TestAccACMCertificate_root(t *testing.T) { - resourceName := "aws_acm_certificate.cert" + resourceName := "aws_acm_certificate.test" rootDomain := acctest.ACMCertificateDomainFromEnv(t) + var v acm.CertificateDetail resource.ParallelTest(t, resource.TestCase{ PreCheck: func() { acctest.PreCheck(t) }, @@ -97,6 +105,7 @@ func TestAccACMCertificate_root(t *testing.T) { { Config: testAccAcmCertificateConfig(rootDomain, acm.ValidationMethodDns), Check: resource.ComposeTestCheckFunc( + testAccCheckAcmCertificateExists(resourceName, &v), acctest.MatchResourceAttrRegionalARN(resourceName, "arn", "acm", regexp.MustCompile("certificate/.+$")), resource.TestCheckResourceAttr(resourceName, "domain_name", rootDomain), resource.TestCheckResourceAttr(resourceName, "domain_validation_options.#", "1"), @@ -105,7 +114,8 @@ func TestAccACMCertificate_root(t *testing.T) { "resource_record_type": "CNAME", }), resource.TestCheckResourceAttr(resourceName, "status", acm.CertificateStatusPendingValidation), - resource.TestCheckResourceAttr(resourceName, "subject_alternative_names.#", "0"), + resource.TestCheckResourceAttr(resourceName, "subject_alternative_names.#", "1"), + resource.TestCheckTypeSetElemAttr(resourceName, "subject_alternative_names.*", rootDomain), resource.TestCheckResourceAttr(resourceName, "validation_emails.#", "0"), resource.TestCheckResourceAttr(resourceName, "validation_method", acm.ValidationMethodDns), ), @@ -121,10 +131,10 @@ func TestAccACMCertificate_root(t *testing.T) { func TestAccACMCertificate_privateCert(t *testing.T) { certificateAuthorityResourceName := "aws_acmpca_certificate_authority.test" - resourceName := "aws_acm_certificate.cert" - + resourceName := "aws_acm_certificate.test" commonName := acctest.RandomDomain() certificateDomainName := commonName.RandomSubdomain().String() + var v acm.CertificateDetail resource.ParallelTest(t, resource.TestCase{ PreCheck: func() { acctest.PreCheck(t) }, @@ -133,13 +143,15 @@ func TestAccACMCertificate_privateCert(t *testing.T) { CheckDestroy: testAccCheckAcmCertificateDestroy, Steps: []resource.TestStep{ { - Config: testAccAcmCertificateConfig_privateCert(commonName.String(), certificateDomainName), + Config: testAccAcmCertificatePrivateCertConfig(commonName.String(), certificateDomainName), Check: resource.ComposeTestCheckFunc( + testAccCheckAcmCertificateExists(resourceName, &v), acctest.MatchResourceAttrRegionalARN(resourceName, "arn", "acm", regexp.MustCompile("certificate/.+$")), resource.TestCheckResourceAttr(resourceName, "domain_name", certificateDomainName), resource.TestCheckResourceAttr(resourceName, "domain_validation_options.#", "0"), resource.TestCheckResourceAttr(resourceName, "status", acm.CertificateStatusFailed), // FailureReason: PCA_INVALID_STATE (PCA State: PENDING_CERTIFICATE) - resource.TestCheckResourceAttr(resourceName, "subject_alternative_names.#", "0"), + resource.TestCheckResourceAttr(resourceName, "subject_alternative_names.#", "1"), + resource.TestCheckTypeSetElemAttr(resourceName, "subject_alternative_names.*", certificateDomainName), resource.TestCheckResourceAttr(resourceName, "validation_emails.#", "0"), resource.TestCheckResourceAttr(resourceName, "validation_method", "NONE"), resource.TestCheckResourceAttrPair(resourceName, "certificate_authority_arn", certificateAuthorityResourceName, "arn"), @@ -175,9 +187,10 @@ func TestAccACMCertificate_Root_trailingPeriod(t *testing.T) { } func TestAccACMCertificate_rootAndWildcardSan(t *testing.T) { - resourceName := "aws_acm_certificate.cert" + resourceName := "aws_acm_certificate.test" rootDomain := acctest.ACMCertificateDomainFromEnv(t) wildcardDomain := fmt.Sprintf("*.%s", rootDomain) + var v acm.CertificateDetail resource.ParallelTest(t, resource.TestCase{ PreCheck: func() { acctest.PreCheck(t) }, @@ -186,8 +199,9 @@ func TestAccACMCertificate_rootAndWildcardSan(t *testing.T) { CheckDestroy: testAccCheckAcmCertificateDestroy, Steps: []resource.TestStep{ { - Config: testAccAcmCertificateConfig_subjectAlternativeNames(rootDomain, strconv.Quote(wildcardDomain), acm.ValidationMethodDns), + Config: testAccAcmCertificateSubjectAlternativeNamesConfig(rootDomain, strconv.Quote(wildcardDomain), acm.ValidationMethodDns), Check: resource.ComposeTestCheckFunc( + testAccCheckAcmCertificateExists(resourceName, &v), acctest.MatchResourceAttrRegionalARN(resourceName, "arn", "acm", regexp.MustCompile("certificate/.+$")), resource.TestCheckResourceAttr(resourceName, "domain_name", rootDomain), resource.TestCheckResourceAttr(resourceName, "domain_validation_options.#", "2"), @@ -200,7 +214,8 @@ func TestAccACMCertificate_rootAndWildcardSan(t *testing.T) { "resource_record_type": "CNAME", }), resource.TestCheckResourceAttr(resourceName, "status", acm.CertificateStatusPendingValidation), - resource.TestCheckResourceAttr(resourceName, "subject_alternative_names.#", "1"), + resource.TestCheckResourceAttr(resourceName, "subject_alternative_names.#", "2"), + resource.TestCheckTypeSetElemAttr(resourceName, "subject_alternative_names.*", rootDomain), resource.TestCheckTypeSetElemAttr(resourceName, "subject_alternative_names.*", wildcardDomain), resource.TestCheckResourceAttr(resourceName, "validation_emails.#", "0"), resource.TestCheckResourceAttr(resourceName, "validation_method", acm.ValidationMethodDns), @@ -226,7 +241,7 @@ func TestAccACMCertificate_SubjectAlternativeNames_emptyString(t *testing.T) { CheckDestroy: testAccCheckAcmCertificateDestroy, Steps: []resource.TestStep{ { - Config: testAccAcmCertificateConfig_subjectAlternativeNames(domain, strconv.Quote(""), acm.ValidationMethodDns), + Config: testAccAcmCertificateSubjectAlternativeNamesConfig(domain, strconv.Quote(""), acm.ValidationMethodDns), ExpectError: regexp.MustCompile(`expected length`), }, }, @@ -234,10 +249,11 @@ func TestAccACMCertificate_SubjectAlternativeNames_emptyString(t *testing.T) { } func TestAccACMCertificate_San_single(t *testing.T) { - resourceName := "aws_acm_certificate.cert" + resourceName := "aws_acm_certificate.test" rootDomain := acctest.ACMCertificateDomainFromEnv(t) domain := acctest.ACMCertificateRandomSubDomain(rootDomain) sanDomain := acctest.ACMCertificateRandomSubDomain(rootDomain) + var v acm.CertificateDetail resource.ParallelTest(t, resource.TestCase{ PreCheck: func() { acctest.PreCheck(t) }, @@ -246,8 +262,9 @@ func TestAccACMCertificate_San_single(t *testing.T) { CheckDestroy: testAccCheckAcmCertificateDestroy, Steps: []resource.TestStep{ { - Config: testAccAcmCertificateConfig_subjectAlternativeNames(domain, strconv.Quote(sanDomain), acm.ValidationMethodDns), + Config: testAccAcmCertificateSubjectAlternativeNamesConfig(domain, strconv.Quote(sanDomain), acm.ValidationMethodDns), Check: resource.ComposeTestCheckFunc( + testAccCheckAcmCertificateExists(resourceName, &v), acctest.MatchResourceAttrRegionalARN(resourceName, "arn", "acm", regexp.MustCompile("certificate/.+$")), resource.TestCheckResourceAttr(resourceName, "domain_name", domain), resource.TestCheckResourceAttr(resourceName, "domain_validation_options.#", "2"), @@ -260,7 +277,8 @@ func TestAccACMCertificate_San_single(t *testing.T) { "resource_record_type": "CNAME", }), resource.TestCheckResourceAttr(resourceName, "status", acm.CertificateStatusPendingValidation), - resource.TestCheckResourceAttr(resourceName, "subject_alternative_names.#", "1"), + resource.TestCheckResourceAttr(resourceName, "subject_alternative_names.#", "2"), + resource.TestCheckTypeSetElemAttr(resourceName, "subject_alternative_names.*", domain), resource.TestCheckTypeSetElemAttr(resourceName, "subject_alternative_names.*", sanDomain), resource.TestCheckResourceAttr(resourceName, "validation_emails.#", "0"), resource.TestCheckResourceAttr(resourceName, "validation_method", acm.ValidationMethodDns), @@ -276,11 +294,12 @@ func TestAccACMCertificate_San_single(t *testing.T) { } func TestAccACMCertificate_San_multiple(t *testing.T) { - resourceName := "aws_acm_certificate.cert" + resourceName := "aws_acm_certificate.test" rootDomain := acctest.ACMCertificateDomainFromEnv(t) domain := acctest.ACMCertificateRandomSubDomain(rootDomain) sanDomain1 := acctest.ACMCertificateRandomSubDomain(rootDomain) sanDomain2 := acctest.ACMCertificateRandomSubDomain(rootDomain) + var v acm.CertificateDetail resource.ParallelTest(t, resource.TestCase{ PreCheck: func() { acctest.PreCheck(t) }, @@ -289,8 +308,9 @@ func TestAccACMCertificate_San_multiple(t *testing.T) { CheckDestroy: testAccCheckAcmCertificateDestroy, Steps: []resource.TestStep{ { - Config: testAccAcmCertificateConfig_subjectAlternativeNames(domain, fmt.Sprintf("%q, %q", sanDomain1, sanDomain2), acm.ValidationMethodDns), + Config: testAccAcmCertificateSubjectAlternativeNamesConfig(domain, fmt.Sprintf("%q, %q", sanDomain1, sanDomain2), acm.ValidationMethodDns), Check: resource.ComposeTestCheckFunc( + testAccCheckAcmCertificateExists(resourceName, &v), acctest.MatchResourceAttrRegionalARN(resourceName, "arn", "acm", regexp.MustCompile("certificate/.+$")), resource.TestCheckResourceAttr(resourceName, "domain_name", domain), resource.TestCheckResourceAttr(resourceName, "domain_validation_options.#", "3"), @@ -307,7 +327,8 @@ func TestAccACMCertificate_San_multiple(t *testing.T) { "resource_record_type": "CNAME", }), resource.TestCheckResourceAttr(resourceName, "status", acm.CertificateStatusPendingValidation), - resource.TestCheckResourceAttr(resourceName, "subject_alternative_names.#", "2"), + resource.TestCheckResourceAttr(resourceName, "subject_alternative_names.#", "3"), + resource.TestCheckTypeSetElemAttr(resourceName, "subject_alternative_names.*", domain), resource.TestCheckTypeSetElemAttr(resourceName, "subject_alternative_names.*", sanDomain1), resource.TestCheckTypeSetElemAttr(resourceName, "subject_alternative_names.*", sanDomain2), resource.TestCheckResourceAttr(resourceName, "validation_emails.#", "0"), @@ -327,7 +348,8 @@ func TestAccACMCertificate_San_trailingPeriod(t *testing.T) { rootDomain := acctest.ACMCertificateDomainFromEnv(t) domain := acctest.ACMCertificateRandomSubDomain(rootDomain) sanDomain := acctest.ACMCertificateRandomSubDomain(rootDomain) - resourceName := "aws_acm_certificate.cert" + resourceName := "aws_acm_certificate.test" + var v acm.CertificateDetail resource.ParallelTest(t, resource.TestCase{ PreCheck: func() { acctest.PreCheck(t) }, @@ -336,8 +358,9 @@ func TestAccACMCertificate_San_trailingPeriod(t *testing.T) { CheckDestroy: testAccCheckAcmCertificateDestroy, Steps: []resource.TestStep{ { - Config: testAccAcmCertificateConfig_subjectAlternativeNames(domain, strconv.Quote(sanDomain), acm.ValidationMethodDns), + Config: testAccAcmCertificateSubjectAlternativeNamesConfig(domain, strconv.Quote(sanDomain), acm.ValidationMethodDns), Check: resource.ComposeTestCheckFunc( + testAccCheckAcmCertificateExists(resourceName, &v), acctest.MatchResourceAttrRegionalARN(resourceName, "arn", "acm", regexp.MustCompile(`certificate/.+`)), resource.TestCheckResourceAttr(resourceName, "domain_name", domain), resource.TestCheckResourceAttr(resourceName, "domain_validation_options.#", "2"), @@ -350,7 +373,8 @@ func TestAccACMCertificate_San_trailingPeriod(t *testing.T) { "resource_record_type": "CNAME", }), resource.TestCheckResourceAttr(resourceName, "status", acm.CertificateStatusPendingValidation), - resource.TestCheckResourceAttr(resourceName, "subject_alternative_names.#", "1"), + resource.TestCheckResourceAttr(resourceName, "subject_alternative_names.#", "2"), + resource.TestCheckTypeSetElemAttr(resourceName, "subject_alternative_names.*", domain), resource.TestCheckTypeSetElemAttr(resourceName, "subject_alternative_names.*", strings.TrimSuffix(sanDomain, ".")), resource.TestCheckResourceAttr(resourceName, "validation_emails.#", "0"), resource.TestCheckResourceAttr(resourceName, "validation_method", acm.ValidationMethodDns), @@ -365,10 +389,12 @@ func TestAccACMCertificate_San_trailingPeriod(t *testing.T) { }) } -func TestAccACMCertificate_wildcard(t *testing.T) { - resourceName := "aws_acm_certificate.cert" +func TestAccACMCertificate_San_matches_domain(t *testing.T) { + resourceName := "aws_acm_certificate.test" rootDomain := acctest.ACMCertificateDomainFromEnv(t) - wildcardDomain := fmt.Sprintf("*.%s", rootDomain) + domain := acctest.ACMCertificateRandomSubDomain(rootDomain) + sanDomain := rootDomain + var v acm.CertificateDetail resource.ParallelTest(t, resource.TestCase{ PreCheck: func() { acctest.PreCheck(t) }, @@ -377,17 +403,24 @@ func TestAccACMCertificate_wildcard(t *testing.T) { CheckDestroy: testAccCheckAcmCertificateDestroy, Steps: []resource.TestStep{ { - Config: testAccAcmCertificateConfig(wildcardDomain, acm.ValidationMethodDns), - Check: resource.ComposeTestCheckFunc( - acctest.MatchResourceAttrRegionalARN(resourceName, "arn", "acm", regexp.MustCompile("certificate/.+$")), - resource.TestCheckResourceAttr(resourceName, "domain_name", wildcardDomain), - resource.TestCheckResourceAttr(resourceName, "domain_validation_options.#", "1"), + Config: testAccAcmCertificateSubjectAlternativeNamesConfig(domain, strconv.Quote(sanDomain), acm.ValidationMethodDns), + Check: resource.ComposeAggregateTestCheckFunc( + testAccCheckAcmCertificateExists(resourceName, &v), + acctest.MatchResourceAttrRegionalARN(resourceName, "arn", "acm", regexp.MustCompile(`certificate/.+`)), + resource.TestCheckResourceAttr(resourceName, "domain_name", domain), + resource.TestCheckResourceAttr(resourceName, "domain_validation_options.#", "2"), resource.TestCheckTypeSetElemNestedAttrs(resourceName, "domain_validation_options.*", map[string]string{ - "domain_name": wildcardDomain, + "domain_name": domain, + "resource_record_type": "CNAME", + }), + resource.TestCheckTypeSetElemNestedAttrs(resourceName, "domain_validation_options.*", map[string]string{ + "domain_name": sanDomain, "resource_record_type": "CNAME", }), resource.TestCheckResourceAttr(resourceName, "status", acm.CertificateStatusPendingValidation), - resource.TestCheckResourceAttr(resourceName, "subject_alternative_names.#", "0"), + resource.TestCheckResourceAttr(resourceName, "subject_alternative_names.#", "2"), + resource.TestCheckTypeSetElemAttr(resourceName, "subject_alternative_names.*", domain), + resource.TestCheckTypeSetElemAttr(resourceName, "subject_alternative_names.*", sanDomain), resource.TestCheckResourceAttr(resourceName, "validation_emails.#", "0"), resource.TestCheckResourceAttr(resourceName, "validation_method", acm.ValidationMethodDns), ), @@ -401,10 +434,11 @@ func TestAccACMCertificate_wildcard(t *testing.T) { }) } -func TestAccACMCertificate_wildcardAndRootSan(t *testing.T) { - resourceName := "aws_acm_certificate.cert" +func TestAccACMCertificate_wildcard(t *testing.T) { + resourceName := "aws_acm_certificate.test" rootDomain := acctest.ACMCertificateDomainFromEnv(t) wildcardDomain := fmt.Sprintf("*.%s", rootDomain) + var v acm.CertificateDetail resource.ParallelTest(t, resource.TestCase{ PreCheck: func() { acctest.PreCheck(t) }, @@ -413,22 +447,19 @@ func TestAccACMCertificate_wildcardAndRootSan(t *testing.T) { CheckDestroy: testAccCheckAcmCertificateDestroy, Steps: []resource.TestStep{ { - Config: testAccAcmCertificateConfig_subjectAlternativeNames(wildcardDomain, strconv.Quote(rootDomain), acm.ValidationMethodDns), + Config: testAccAcmCertificateConfig(wildcardDomain, acm.ValidationMethodDns), Check: resource.ComposeTestCheckFunc( + testAccCheckAcmCertificateExists(resourceName, &v), acctest.MatchResourceAttrRegionalARN(resourceName, "arn", "acm", regexp.MustCompile("certificate/.+$")), resource.TestCheckResourceAttr(resourceName, "domain_name", wildcardDomain), - resource.TestCheckResourceAttr(resourceName, "domain_validation_options.#", "2"), - resource.TestCheckTypeSetElemNestedAttrs(resourceName, "domain_validation_options.*", map[string]string{ - "domain_name": rootDomain, - "resource_record_type": "CNAME", - }), + resource.TestCheckResourceAttr(resourceName, "domain_validation_options.#", "1"), resource.TestCheckTypeSetElemNestedAttrs(resourceName, "domain_validation_options.*", map[string]string{ "domain_name": wildcardDomain, "resource_record_type": "CNAME", }), resource.TestCheckResourceAttr(resourceName, "status", acm.CertificateStatusPendingValidation), resource.TestCheckResourceAttr(resourceName, "subject_alternative_names.#", "1"), - resource.TestCheckTypeSetElemAttr(resourceName, "subject_alternative_names.*", rootDomain), + resource.TestCheckTypeSetElemAttr(resourceName, "subject_alternative_names.*", wildcardDomain), resource.TestCheckResourceAttr(resourceName, "validation_emails.#", "0"), resource.TestCheckResourceAttr(resourceName, "validation_method", acm.ValidationMethodDns), ), @@ -442,9 +473,11 @@ func TestAccACMCertificate_wildcardAndRootSan(t *testing.T) { }) } -func TestAccACMCertificate_disableCTLogging(t *testing.T) { - resourceName := "aws_acm_certificate.cert" +func TestAccACMCertificate_wildcardAndRootSan(t *testing.T) { + resourceName := "aws_acm_certificate.test" rootDomain := acctest.ACMCertificateDomainFromEnv(t) + wildcardDomain := fmt.Sprintf("*.%s", rootDomain) + var v acm.CertificateDetail resource.ParallelTest(t, resource.TestCase{ PreCheck: func() { acctest.PreCheck(t) }, @@ -453,21 +486,26 @@ func TestAccACMCertificate_disableCTLogging(t *testing.T) { CheckDestroy: testAccCheckAcmCertificateDestroy, Steps: []resource.TestStep{ { - Config: testAccAcmCertificateConfig_disableCTLogging(rootDomain, acm.ValidationMethodDns), + Config: testAccAcmCertificateSubjectAlternativeNamesConfig(wildcardDomain, strconv.Quote(rootDomain), acm.ValidationMethodDns), Check: resource.ComposeTestCheckFunc( + testAccCheckAcmCertificateExists(resourceName, &v), acctest.MatchResourceAttrRegionalARN(resourceName, "arn", "acm", regexp.MustCompile("certificate/.+$")), - resource.TestCheckResourceAttr(resourceName, "domain_name", rootDomain), - resource.TestCheckResourceAttr(resourceName, "domain_validation_options.#", "1"), + resource.TestCheckResourceAttr(resourceName, "domain_name", wildcardDomain), + resource.TestCheckResourceAttr(resourceName, "domain_validation_options.#", "2"), resource.TestCheckTypeSetElemNestedAttrs(resourceName, "domain_validation_options.*", map[string]string{ "domain_name": rootDomain, "resource_record_type": "CNAME", }), - resource.TestCheckResourceAttr(resourceName, "subject_alternative_names.#", "0"), + resource.TestCheckTypeSetElemNestedAttrs(resourceName, "domain_validation_options.*", map[string]string{ + "domain_name": wildcardDomain, + "resource_record_type": "CNAME", + }), resource.TestCheckResourceAttr(resourceName, "status", acm.CertificateStatusPendingValidation), + resource.TestCheckResourceAttr(resourceName, "subject_alternative_names.#", "2"), + resource.TestCheckTypeSetElemAttr(resourceName, "subject_alternative_names.*", rootDomain), + resource.TestCheckTypeSetElemAttr(resourceName, "subject_alternative_names.*", wildcardDomain), resource.TestCheckResourceAttr(resourceName, "validation_emails.#", "0"), resource.TestCheckResourceAttr(resourceName, "validation_method", acm.ValidationMethodDns), - resource.TestCheckResourceAttr(resourceName, "options.#", "1"), - resource.TestCheckResourceAttr(resourceName, "options.0.certificate_transparency_logging_preference", acm.CertificateTransparencyLoggingPreferenceDisabled), ), }, { @@ -479,10 +517,10 @@ func TestAccACMCertificate_disableCTLogging(t *testing.T) { }) } -func TestAccACMCertificate_tags(t *testing.T) { - resourceName := "aws_acm_certificate.cert" +func TestAccACMCertificate_disableCTLogging(t *testing.T) { + resourceName := "aws_acm_certificate.test" rootDomain := acctest.ACMCertificateDomainFromEnv(t) - domain := acctest.ACMCertificateRandomSubDomain(rootDomain) + var v acm.CertificateDetail resource.ParallelTest(t, resource.TestCase{ PreCheck: func() { acctest.PreCheck(t) }, @@ -491,32 +529,23 @@ func TestAccACMCertificate_tags(t *testing.T) { CheckDestroy: testAccCheckAcmCertificateDestroy, Steps: []resource.TestStep{ { - Config: testAccAcmCertificateConfig(domain, acm.ValidationMethodDns), - Check: resource.ComposeTestCheckFunc( - resource.TestCheckResourceAttr(resourceName, "tags.%", "0"), - ), - }, - { - Config: testAccAcmCertificateConfig_twoTags(domain, acm.ValidationMethodDns, "Hello", "World", "Foo", "Bar"), - Check: resource.ComposeTestCheckFunc( - resource.TestCheckResourceAttr(resourceName, "tags.%", "2"), - resource.TestCheckResourceAttr(resourceName, "tags.Hello", "World"), - resource.TestCheckResourceAttr(resourceName, "tags.Foo", "Bar"), - ), - }, - { - Config: testAccAcmCertificateConfig_twoTags(domain, acm.ValidationMethodDns, "Hello", "World", "Foo", "Baz"), - Check: resource.ComposeTestCheckFunc( - resource.TestCheckResourceAttr(resourceName, "tags.%", "2"), - resource.TestCheckResourceAttr(resourceName, "tags.Hello", "World"), - resource.TestCheckResourceAttr(resourceName, "tags.Foo", "Baz"), - ), - }, - { - Config: testAccAcmCertificateConfig_oneTag(domain, acm.ValidationMethodDns, "Environment", "Test"), + Config: testAccAcmCertificateDisableCTLoggingConfig(rootDomain, acm.ValidationMethodDns), Check: resource.ComposeTestCheckFunc( - resource.TestCheckResourceAttr(resourceName, "tags.%", "1"), - resource.TestCheckResourceAttr(resourceName, "tags.Environment", "Test"), + testAccCheckAcmCertificateExists(resourceName, &v), + acctest.MatchResourceAttrRegionalARN(resourceName, "arn", "acm", regexp.MustCompile("certificate/.+$")), + resource.TestCheckResourceAttr(resourceName, "domain_name", rootDomain), + resource.TestCheckResourceAttr(resourceName, "domain_validation_options.#", "1"), + resource.TestCheckTypeSetElemNestedAttrs(resourceName, "domain_validation_options.*", map[string]string{ + "domain_name": rootDomain, + "resource_record_type": "CNAME", + }), + resource.TestCheckResourceAttr(resourceName, "subject_alternative_names.#", "1"), + resource.TestCheckTypeSetElemAttr(resourceName, "subject_alternative_names.*", rootDomain), + resource.TestCheckResourceAttr(resourceName, "status", acm.CertificateStatusPendingValidation), + resource.TestCheckResourceAttr(resourceName, "validation_emails.#", "0"), + resource.TestCheckResourceAttr(resourceName, "validation_method", acm.ValidationMethodDns), + resource.TestCheckResourceAttr(resourceName, "options.#", "1"), + resource.TestCheckResourceAttr(resourceName, "options.0.certificate_transparency_logging_preference", acm.CertificateTransparencyLoggingPreferenceDisabled), ), }, { @@ -531,18 +560,16 @@ func TestAccACMCertificate_tags(t *testing.T) { //lintignore:AT002 func TestAccACMCertificate_Imported_domainName(t *testing.T) { resourceName := "aws_acm_certificate.test" - commonName := "example.com" caKey := acctest.TLSRSAPrivateKeyPEM(2048) caCertificate := acctest.TLSRSAX509SelfSignedCACertificatePEM(caKey) key := acctest.TLSRSAPrivateKeyPEM(2048) certificate := acctest.TLSRSAX509LocallySignedCertificatePEM(caKey, caCertificate, key, commonName) - newCaKey := acctest.TLSRSAPrivateKeyPEM(2048) newCaCertificate := acctest.TLSRSAX509SelfSignedCACertificatePEM(newCaKey) newCertificate := acctest.TLSRSAX509LocallySignedCertificatePEM(newCaKey, newCaCertificate, key, commonName) - withoutChainDomain := acctest.RandomDomainName() + var v acm.CertificateDetail resource.ParallelTest(t, resource.TestCase{ PreCheck: func() { acctest.PreCheck(t) }, @@ -551,14 +578,15 @@ func TestAccACMCertificate_Imported_domainName(t *testing.T) { CheckDestroy: testAccCheckAcmCertificateDestroy, Steps: []resource.TestStep{ { - Config: testAccAcmCertificateConfigPrivateKey(certificate, key, caCertificate), + Config: testAccAcmCertificatePrivateKeyConfig(certificate, key, caCertificate), Check: resource.ComposeTestCheckFunc( + testAccCheckAcmCertificateExists(resourceName, &v), resource.TestCheckResourceAttr(resourceName, "tags.%", "0"), resource.TestCheckResourceAttr(resourceName, "domain_name", commonName), ), }, { - Config: testAccAcmCertificateConfigPrivateKey(newCertificate, key, newCaCertificate), + Config: testAccAcmCertificatePrivateKeyConfig(newCertificate, key, newCaCertificate), Check: resource.ComposeTestCheckFunc( resource.TestCheckResourceAttr(resourceName, "status", acm.CertificateStatusIssued), resource.TestCheckResourceAttr(resourceName, "tags.%", "0"), @@ -566,7 +594,7 @@ func TestAccACMCertificate_Imported_domainName(t *testing.T) { ), }, { - Config: testAccAcmCertificateConfigPrivateKeyWithoutChain(withoutChainDomain), + Config: testAccAcmCertificatePrivateKeyWithoutChainConfig(withoutChainDomain), Check: resource.ComposeTestCheckFunc( resource.TestCheckResourceAttr(resourceName, "status", acm.CertificateStatusIssued), resource.TestCheckResourceAttr(resourceName, "tags.%", "0"), @@ -587,6 +615,7 @@ func TestAccACMCertificate_Imported_domainName(t *testing.T) { //lintignore:AT002 func TestAccACMCertificate_Imported_ipAddress(t *testing.T) { // Reference: https://github.com/hashicorp/terraform-provider-aws/issues/7103 resourceName := "aws_acm_certificate.test" + var v acm.CertificateDetail resource.ParallelTest(t, resource.TestCase{ PreCheck: func() { acctest.PreCheck(t) }, @@ -595,8 +624,9 @@ func TestAccACMCertificate_Imported_ipAddress(t *testing.T) { // Reference: http CheckDestroy: testAccCheckAcmCertificateDestroy, Steps: []resource.TestStep{ { - Config: testAccAcmCertificateConfigPrivateKeyWithoutChain("1.2.3.4"), + Config: testAccAcmCertificatePrivateKeyWithoutChainConfig("1.2.3.4"), Check: resource.ComposeTestCheckFunc( + testAccCheckAcmCertificateExists(resourceName, &v), resource.TestCheckResourceAttr(resourceName, "domain_name", ""), resource.TestCheckResourceAttr(resourceName, "status", acm.CertificateStatusIssued), resource.TestCheckResourceAttr(resourceName, "subject_alternative_names.#", "0"), @@ -616,6 +646,11 @@ func TestAccACMCertificate_Imported_ipAddress(t *testing.T) { // Reference: http // Reference: https://github.com/hashicorp/terraform-provider-aws/issues/15055 func TestAccACMCertificate_PrivateKey_tags(t *testing.T) { resourceName := "aws_acm_certificate.test" + key1 := acctest.TLSRSAPrivateKeyPEM(2048) + certificate1 := acctest.TLSRSAX509SelfSignedCertificatePEM(key1, "1.2.3.4") + key2 := acctest.TLSRSAPrivateKeyPEM(2048) + certificate2 := acctest.TLSRSAX509SelfSignedCertificatePEM(key2, "5.6.7.8") + var v acm.CertificateDetail resource.ParallelTest(t, resource.TestCase{ PreCheck: func() { acctest.PreCheck(t) }, @@ -624,9 +659,11 @@ func TestAccACMCertificate_PrivateKey_tags(t *testing.T) { CheckDestroy: testAccCheckAcmCertificateDestroy, Steps: []resource.TestStep{ { - Config: testAccAcmCertificateConfigPrivateKeyTags("1.2.3.4"), + Config: testAccAcmCertificateConfigTags1(certificate1, key1, "key1", "value1"), Check: resource.ComposeTestCheckFunc( + testAccCheckAcmCertificateExists(resourceName, &v), resource.TestCheckResourceAttr(resourceName, "tags.%", "1"), + resource.TestCheckResourceAttr(resourceName, "tags.key1", "value1"), ), }, { @@ -636,25 +673,90 @@ func TestAccACMCertificate_PrivateKey_tags(t *testing.T) { ImportStateVerifyIgnore: []string{"private_key", "certificate_body"}, }, { - Config: testAccAcmCertificateConfigPrivateKeyTags("5.6.7.8"), + Config: testAccAcmCertificateConfigTags2(certificate1, key1, "key1", "value1updated", "key2", "value2"), + Check: resource.ComposeTestCheckFunc( + resource.TestCheckResourceAttr(resourceName, "tags.%", "2"), + resource.TestCheckResourceAttr(resourceName, "tags.key1", "value1updated"), + resource.TestCheckResourceAttr(resourceName, "tags.key2", "value2"), + ), + }, + { + Config: testAccAcmCertificateConfigTags1(certificate1, key1, "key2", "value2"), Check: resource.ComposeTestCheckFunc( resource.TestCheckResourceAttr(resourceName, "tags.%", "1"), + resource.TestCheckResourceAttr(resourceName, "tags.key2", "value2"), + ), + }, + { + Config: testAccAcmCertificateConfigTags1(certificate2, key2, "key1", "value1"), + Check: resource.ComposeTestCheckFunc( + resource.TestCheckResourceAttr(resourceName, "tags.%", "1"), + resource.TestCheckResourceAttr(resourceName, "tags.key1", "value1"), ), }, }, }) } +func testAccCheckAcmCertificateExists(n string, v *acm.CertificateDetail) 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 ACM Certificate ID is set") + } + + conn := acctest.Provider.Meta().(*conns.AWSClient).ACMConn + + output, err := tfacm.FindCertificateByARN(conn, rs.Primary.ID) + + if err != nil { + return err + } + + *v = *output + + return nil + } +} + +func testAccCheckAcmCertificateDestroy(s *terraform.State) error { + conn := acctest.Provider.Meta().(*conns.AWSClient).ACMConn + + for _, rs := range s.RootModule().Resources { + if rs.Type != "aws_acm_certificate" { + continue + } + + _, err := tfacm.FindCertificateByARN(conn, rs.Primary.ID) + + if tfresource.NotFound(err) { + continue + } + + if err != nil { + return err + } + + return fmt.Errorf("ACM Certificate %s still exists", rs.Primary.ID) + } + + return nil +} + func testAccAcmCertificateConfig(domainName, validationMethod string) string { return fmt.Sprintf(` -resource "aws_acm_certificate" "cert" { +resource "aws_acm_certificate" "test" { domain_name = "%s" validation_method = "%s" } `, domainName, validationMethod) } -func testAccAcmCertificateConfig_privateCert(commonName, certificateDomainName string) string { +func testAccAcmCertificatePrivateCertConfig(commonName, certificateDomainName string) string { return fmt.Sprintf(` resource "aws_acmpca_certificate_authority" "test" { permanent_deletion_time_in_days = 7 @@ -670,51 +772,24 @@ resource "aws_acmpca_certificate_authority" "test" { } } -resource "aws_acm_certificate" "cert" { +resource "aws_acm_certificate" "test" { domain_name = %[2]q certificate_authority_arn = aws_acmpca_certificate_authority.test.arn } `, commonName, certificateDomainName) } -func testAccAcmCertificateConfig_subjectAlternativeNames(domainName, subjectAlternativeNames, validationMethod string) string { +func testAccAcmCertificateSubjectAlternativeNamesConfig(domainName, subjectAlternativeNames, validationMethod string) string { return fmt.Sprintf(` -resource "aws_acm_certificate" "cert" { - domain_name = "%s" - subject_alternative_names = [%s] - validation_method = "%s" +resource "aws_acm_certificate" "test" { + domain_name = %[1]q + subject_alternative_names = [%[2]s] + validation_method = %[3]q } `, domainName, subjectAlternativeNames, validationMethod) } -func testAccAcmCertificateConfig_oneTag(domainName, validationMethod, tag1Key, tag1Value string) string { - return fmt.Sprintf(` -resource "aws_acm_certificate" "cert" { - domain_name = "%s" - validation_method = "%s" - - tags = { - "%s" = "%s" - } -} -`, domainName, validationMethod, tag1Key, tag1Value) -} - -func testAccAcmCertificateConfig_twoTags(domainName, validationMethod, tag1Key, tag1Value, tag2Key, tag2Value string) string { - return fmt.Sprintf(` -resource "aws_acm_certificate" "cert" { - domain_name = "%s" - validation_method = "%s" - - tags = { - "%s" = "%s" - "%s" = "%s" - } -} -`, domainName, validationMethod, tag1Key, tag1Value, tag2Key, tag2Value) -} - -func testAccAcmCertificateConfigPrivateKeyWithoutChain(commonName string) string { +func testAccAcmCertificatePrivateKeyWithoutChainConfig(commonName string) string { key := acctest.TLSRSAPrivateKeyPEM(2048) certificate := acctest.TLSRSAX509SelfSignedCertificatePEM(key, commonName) @@ -726,23 +801,34 @@ resource "aws_acm_certificate" "test" { `, acctest.TLSPEMEscapeNewlines(certificate), acctest.TLSPEMEscapeNewlines(key)) } -func testAccAcmCertificateConfigPrivateKeyTags(commonName string) string { - key := acctest.TLSRSAPrivateKeyPEM(2048) - certificate := acctest.TLSRSAX509SelfSignedCertificatePEM(key, commonName) +func testAccAcmCertificateConfigTags1(certificate, key, tagKey1, tagValue1 string) string { + return fmt.Sprintf(` +resource "aws_acm_certificate" "test" { + certificate_body = "%[1]s" + private_key = "%[2]s" + tags = { + %[3]q = %[4]q + } +} +`, acctest.TLSPEMEscapeNewlines(certificate), acctest.TLSPEMEscapeNewlines(key), tagKey1, tagValue1) +} + +func testAccAcmCertificateConfigTags2(certificate, key, tagKey1, tagValue1, tagKey2, tagValue2 string) string { return fmt.Sprintf(` resource "aws_acm_certificate" "test" { certificate_body = "%[1]s" private_key = "%[2]s" tags = { - key1 = "value1" + %[3]q = %[4]q + %[5]q = %[6]q } } -`, acctest.TLSPEMEscapeNewlines(certificate), acctest.TLSPEMEscapeNewlines(key)) +`, acctest.TLSPEMEscapeNewlines(certificate), acctest.TLSPEMEscapeNewlines(key), tagKey1, tagValue1, tagKey2, tagValue2) } -func testAccAcmCertificateConfigPrivateKey(certificate, privateKey, chain string) string { +func testAccAcmCertificatePrivateKeyConfig(certificate, privateKey, chain string) string { return fmt.Sprintf(` resource "aws_acm_certificate" "test" { certificate_body = "%[1]s" @@ -752,38 +838,15 @@ resource "aws_acm_certificate" "test" { `, acctest.TLSPEMEscapeNewlines(certificate), acctest.TLSPEMEscapeNewlines(privateKey), acctest.TLSPEMEscapeNewlines(chain)) } -func testAccAcmCertificateConfig_disableCTLogging(domainName, validationMethod string) string { +func testAccAcmCertificateDisableCTLoggingConfig(domainName, validationMethod string) string { return fmt.Sprintf(` -resource "aws_acm_certificate" "cert" { - domain_name = "%s" - validation_method = "%s" +resource "aws_acm_certificate" "test" { + domain_name = %[1]q + validation_method = %[2]q + options { certificate_transparency_logging_preference = "DISABLED" } } `, domainName, validationMethod) } - -func testAccCheckAcmCertificateDestroy(s *terraform.State) error { - conn := acctest.Provider.Meta().(*conns.AWSClient).ACMConn - - for _, rs := range s.RootModule().Resources { - if rs.Type != "aws_acm_certificate" { - continue - } - _, err := conn.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 !tfawserr.ErrCodeEquals(err, acm.ErrCodeResourceNotFoundException) { - return err - } - } - - return nil -} diff --git a/internal/service/acm/certificate_validation.go b/internal/service/acm/certificate_validation.go index e1c9c738e780..c0d772a15e55 100644 --- a/internal/service/acm/certificate_validation.go +++ b/internal/service/acm/certificate_validation.go @@ -1,6 +1,7 @@ package acm import ( + "errors" "fmt" "log" "strings" @@ -8,7 +9,6 @@ import ( "github.com/aws/aws-sdk-go/aws" "github.com/aws/aws-sdk-go/service/acm" - "github.com/hashicorp/aws-sdk-go-base/v2/awsv1shim/v2/tfawserr" multierror "github.com/hashicorp/go-multierror" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" @@ -20,7 +20,11 @@ func ResourceCertificateValidation() *schema.Resource { return &schema.Resource{ Create: resourceCertificateValidationCreate, Read: resourceCertificateValidationRead, - Delete: resourceCertificateValidationDelete, + Delete: schema.Noop, + + Timeouts: &schema.ResourceTimeout{ + Create: schema.DefaultTimeout(75 * time.Minute), + }, Schema: map[string]*schema.Schema{ "certificate_arn": { @@ -33,183 +37,142 @@ func ResourceCertificateValidation() *schema.Resource { Optional: true, ForceNew: true, Elem: &schema.Schema{Type: schema.TypeString}, - Set: schema.HashString, }, }, - Timeouts: &schema.ResourceTimeout{ - Create: schema.DefaultTimeout(45 * time.Minute), - }, } } func resourceCertificateValidationCreate(d *schema.ResourceData, meta interface{}) error { - certificate_arn := d.Get("certificate_arn").(string) - conn := meta.(*conns.AWSClient).ACMConn - params := &acm.DescribeCertificateInput{ - CertificateArn: aws.String(certificate_arn), - } - resp, err := conn.DescribeCertificate(params) + arn := d.Get("certificate_arn").(string) + certificate, err := FindCertificateByARN(conn, arn) if err != nil { - return fmt.Errorf("Error describing certificate: %w", err) - } - - if resp == nil || resp.Certificate == nil { - return fmt.Errorf("Error describing certificate: empty output") + return fmt.Errorf("reading ACM Certificate (%s): %w", arn, err) } - if aws.StringValue(resp.Certificate.Type) != acm.CertificateTypeAmazonIssued { - return fmt.Errorf("Certificate %s has type %s, no validation necessary", aws.StringValue(resp.Certificate.CertificateArn), aws.StringValue(resp.Certificate.Status)) + if v := aws.StringValue(certificate.Type); v != acm.CertificateTypeAmazonIssued { + return fmt.Errorf("ACM Certificate (%s) has type %s, no validation necessary", arn, v) } - if validation_record_fqdns, ok := d.GetOk("validation_record_fqdns"); ok { - err := resourceCertificateCheckValidationRecords(validation_record_fqdns.(*schema.Set).List(), resp.Certificate, conn) - if err != nil { - return err - } - } else { - log.Printf("[INFO] No validation_record_fqdns set, skipping check") - } + if v, ok := d.GetOk("validation_record_fqdns"); ok && v.(*schema.Set).Len() > 0 { + fqdns := make(map[string]*acm.DomainValidation) - err = resource.Retry(d.Timeout(schema.TimeoutCreate), func() *resource.RetryError { - resp, err := conn.DescribeCertificate(params) + for _, domainValidation := range certificate.DomainValidationOptions { + if v := aws.StringValue(domainValidation.ValidationMethod); v != acm.ValidationMethodDns { + return fmt.Errorf("validation_record_fqdns is not valid for %s validation", v) + } - if err != nil { - return resource.NonRetryableError(fmt.Errorf("Error describing certificate: %w", err)) + if v := domainValidation.ResourceRecord; v != nil { + if v := aws.StringValue(v.Name); v != "" { + fqdns[strings.TrimSuffix(v, ".")] = domainValidation + } + } } - if aws.StringValue(resp.Certificate.Status) != acm.CertificateStatusIssued { - return resource.RetryableError(fmt.Errorf("Expected certificate to be issued but was in state %s", aws.StringValue(resp.Certificate.Status))) + for _, v := range v.(*schema.Set).List() { + delete(fqdns, strings.TrimSuffix(v.(string), ".")) } - log.Printf("[INFO] ACM Certificate validation for %s done, certificate was issued", certificate_arn) - if err := resourceCertificateValidationRead(d, meta); err != nil { - return resource.NonRetryableError(err) - } - return nil - }) - if tfresource.TimedOut(err) { - resp, err = conn.DescribeCertificate(params) - if aws.StringValue(resp.Certificate.Status) != acm.CertificateStatusIssued { - return fmt.Errorf("Expected certificate to be issued but was in state %s", aws.StringValue(resp.Certificate.Status)) - } - } - if err != nil { - return fmt.Errorf("Error describing created certificate: %w", err) - } - return nil -} + if len(fqdns) > 0 { + var errs *multierror.Error -func resourceCertificateCheckValidationRecords(validationRecordFqdns []interface{}, cert *acm.CertificateDetail, conn *acm.ACM) error { - expectedFqdns := make(map[string]*acm.DomainValidation) - - if len(cert.DomainValidationOptions) == 0 { - input := &acm.DescribeCertificateInput{ - CertificateArn: cert.CertificateArn, - } - var err error - var output *acm.DescribeCertificateOutput - err = resource.Retry(1*time.Minute, func() *resource.RetryError { - log.Printf("[DEBUG] Certificate domain validation options empty for %s, retrying", aws.StringValue(cert.CertificateArn)) - output, err = conn.DescribeCertificate(input) - if err != nil { - return resource.NonRetryableError(err) - } - if len(output.Certificate.DomainValidationOptions) == 0 { - return resource.RetryableError(fmt.Errorf("Certificate domain validation options empty for %s", aws.StringValue(cert.CertificateArn))) + for fqdn, domainValidation := range fqdns { + errs = multierror.Append(errs, fmt.Errorf("missing %s DNS validation record: %s", aws.StringValue(domainValidation.DomainName), fqdn)) } - cert = output.Certificate - return nil - }) - if tfresource.TimedOut(err) { - output, err = conn.DescribeCertificate(input) - if err != nil { - return fmt.Errorf("Error describing ACM certificate: %w", err) - } - if len(output.Certificate.DomainValidationOptions) == 0 { - return fmt.Errorf("Certificate domain validation options empty for %s", aws.StringValue(cert.CertificateArn)) - } - } - if err != nil { - return fmt.Errorf("Error checking certificate domain validation options: %w", err) - } - if output == nil || output.Certificate == nil { - return fmt.Errorf("Error checking certificate domain validation options: empty output") - } - cert = output.Certificate - } - for _, v := range cert.DomainValidationOptions { - if v.ValidationMethod != nil { - if aws.StringValue(v.ValidationMethod) != acm.ValidationMethodDns { - return fmt.Errorf("validation_record_fqdns is only valid for DNS validation") - } - if v.ResourceRecord != nil && aws.StringValue(v.ResourceRecord.Name) != "" { - newExpectedFqdn := strings.TrimSuffix(aws.StringValue(v.ResourceRecord.Name), ".") - expectedFqdns[newExpectedFqdn] = v - } - } else if len(v.ValidationEmails) > 0 { - // ACM API sometimes is not sending ValidationMethod for EMAIL validation - return fmt.Errorf("validation_record_fqdns is only valid for DNS validation") + return errs } } - for _, v := range validationRecordFqdns { - delete(expectedFqdns, strings.TrimSuffix(v.(string), ".")) + if _, err := waitCertificateIssued(conn, arn, d.Timeout(schema.TimeoutCreate)); err != nil { + return fmt.Errorf("waiting for ACM Certificate (%s) to be issued: %w", arn, err) } - if len(expectedFqdns) > 0 { - var errors error - for expectedFqdn, domainValidation := range expectedFqdns { - errors = multierror.Append(errors, fmt.Errorf("missing %s DNS validation record: %s", aws.StringValue(domainValidation.DomainName), expectedFqdn)) - } - return errors - } + d.SetId(arn) - return nil + return resourceCertificateValidationRead(d, meta) } func resourceCertificateValidationRead(d *schema.ResourceData, meta interface{}) error { conn := meta.(*conns.AWSClient).ACMConn - params := &acm.DescribeCertificateInput{ - CertificateArn: aws.String(d.Get("certificate_arn").(string)), - } + certificate, err := FindCertificateValidationByARN(conn, d.Id()) - resp, err := conn.DescribeCertificate(params) - - if !d.IsNewResource() && tfawserr.ErrCodeEquals(err, acm.ErrCodeResourceNotFoundException) { - log.Printf("[WARN] ACM Certificate (%s) not found, removing from state", d.Id()) + if !d.IsNewResource() && tfresource.NotFound(err) { + log.Printf("[WARN] ACM Certificate %s not found, removing from state", d.Id()) d.SetId("") return nil } if err != nil { - return fmt.Errorf("error describing ACM Certificate (%s): %w", d.Id(), err) + return fmt.Errorf("reading ACM Certificate (%s): %w", d.Id(), err) } - if resp == nil || resp.Certificate == nil { - return fmt.Errorf("error describing ACM Certificate (%s): empty response", d.Id()) + d.Set("certificate_arn", certificate.CertificateArn) + + return nil +} + +func FindCertificateValidationByARN(conn *acm.ACM, arn string) (*acm.CertificateDetail, error) { + output, err := FindCertificateByARN(conn, arn) + + if err != nil { + return nil, err } - if status := aws.StringValue(resp.Certificate.Status); status != acm.CertificateStatusIssued { - if d.IsNewResource() { - return fmt.Errorf("ACM Certificate (%s) status not issued: %s", d.Id(), status) + if status := aws.StringValue(output.Status); status != acm.CertificateStatusIssued { + return nil, &resource.NotFoundError{ + Message: status, + LastRequest: arn, } - - log.Printf("[WARN] ACM Certificate (%s) status not issued (%s), removing from state", d.Id(), status) - d.SetId("") - return nil } - d.SetId(aws.TimeValue(resp.Certificate.IssuedAt).String()) + return output, nil +} - return nil +func statusCertificate(conn *acm.ACM, arn string) resource.StateRefreshFunc { + return func() (interface{}, string, error) { + // Don't call FindCertificateByARN as it maps useful status codes to NotFoundError. + input := &acm.DescribeCertificateInput{ + CertificateArn: aws.String(arn), + } + + output, err := findCertificate(conn, input) + + if tfresource.NotFound(err) { + return nil, "", nil + } + + if err != nil { + return nil, "", err + } + + return output, aws.StringValue(output.Status), nil + } } -func resourceCertificateValidationDelete(d *schema.ResourceData, meta interface{}) error { - // No need to do anything, certificate will be deleted when acm_certificate is deleted - return nil +func waitCertificateIssued(conn *acm.ACM, arn string, timeout time.Duration) (*acm.CertificateDetail, error) { + stateConf := &resource.StateChangeConf{ + Pending: []string{acm.CertificateStatusPendingValidation}, + Target: []string{acm.CertificateStatusIssued}, + Refresh: statusCertificate(conn, arn), + Timeout: timeout, + } + + outputRaw, err := stateConf.WaitForState() + + if output, ok := outputRaw.(*acm.CertificateDetail); ok { + switch aws.StringValue(output.Status) { + case acm.CertificateStatusFailed: + tfresource.SetLastError(err, errors.New(aws.StringValue(output.FailureReason))) + case acm.CertificateStatusRevoked: + tfresource.SetLastError(err, errors.New(aws.StringValue(output.RevocationReason))) + } + + return output, err + } + + return nil, err } diff --git a/internal/service/acm/certificate_validation_test.go b/internal/service/acm/certificate_validation_test.go index 4a0aa0fe5002..bbac5bf94fc0 100644 --- a/internal/service/acm/certificate_validation_test.go +++ b/internal/service/acm/certificate_validation_test.go @@ -8,7 +8,10 @@ import ( "github.com/aws/aws-sdk-go/service/acm" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource" + "github.com/hashicorp/terraform-plugin-sdk/v2/terraform" "github.com/hashicorp/terraform-provider-aws/internal/acctest" + "github.com/hashicorp/terraform-provider-aws/internal/conns" + tfacm "github.com/hashicorp/terraform-provider-aws/internal/service/acm" ) func TestAccACMCertificateValidation_basic(t *testing.T) { @@ -25,8 +28,9 @@ func TestAccACMCertificateValidation_basic(t *testing.T) { Steps: []resource.TestStep{ // Test that validation succeeds { - Config: testAccAcmCertificateValidation_basic(rootDomain, domain), + Config: testAccAcmCertificateValidationConfig(rootDomain, domain), Check: resource.ComposeTestCheckFunc( + testAccCheckAcmCertificateValidationExists(resourceName), resource.TestCheckResourceAttrPair(resourceName, "certificate_arn", certificateResourceName, "arn"), ), }, @@ -45,8 +49,8 @@ func TestAccACMCertificateValidation_timeout(t *testing.T) { CheckDestroy: testAccCheckAcmCertificateDestroy, Steps: []resource.TestStep{ { - Config: testAccAcmCertificateValidation_timeout(domain), - ExpectError: regexp.MustCompile("Expected certificate to be issued but was in state PENDING_VALIDATION"), + Config: testAccAcmCertificateValidationTimeoutConfig(domain), + ExpectError: regexp.MustCompile(`timeout while waiting for state to become 'ISSUED' \(last state: 'PENDING_VALIDATION'`), }, }, }) @@ -66,13 +70,14 @@ func TestAccACMCertificateValidation_validationRecordFQDNS(t *testing.T) { Steps: []resource.TestStep{ // Test that validation fails if given validation_fqdns don't match { - Config: testAccAcmCertificateValidation_validationRecordFqdnsWrongFqdn(domain), + Config: testAccAcmCertificateValidationValidationRecordFQDNsWrongFQDNConfig(domain), ExpectError: regexp.MustCompile("missing .+ DNS validation record: .+"), }, // Test that validation succeeds with validation { - Config: testAccAcmCertificateValidation_validationRecordFqdnsOneRoute53Record(rootDomain, domain), + Config: testAccAcmCertificateValidationValidationRecordFQDNsOneRoute53RecordConfig(rootDomain, domain), Check: resource.ComposeTestCheckFunc( + testAccCheckAcmCertificateValidationExists(resourceName), resource.TestCheckResourceAttrPair(resourceName, "certificate_arn", certificateResourceName, "arn"), ), }, @@ -91,8 +96,8 @@ func TestAccACMCertificateValidation_validationRecordFQDNSEmail(t *testing.T) { CheckDestroy: testAccCheckAcmCertificateDestroy, Steps: []resource.TestStep{ { - Config: testAccAcmCertificateValidation_validationRecordFqdnsEmailValidation(domain), - ExpectError: regexp.MustCompile("validation_record_fqdns is only valid for DNS validation"), + Config: testAccAcmCertificateValidationValidationRecordFQDNsEmailValidationConfig(domain), + ExpectError: regexp.MustCompile("validation_record_fqdns is not valid for EMAIL validation"), }, }, }) @@ -110,8 +115,9 @@ func TestAccACMCertificateValidation_validationRecordFQDNSRoot(t *testing.T) { CheckDestroy: testAccCheckAcmCertificateDestroy, Steps: []resource.TestStep{ { - Config: testAccAcmCertificateValidation_validationRecordFqdnsOneRoute53Record(rootDomain, rootDomain), + Config: testAccAcmCertificateValidationValidationRecordFQDNsOneRoute53RecordConfig(rootDomain, rootDomain), Check: resource.ComposeTestCheckFunc( + testAccCheckAcmCertificateValidationExists(resourceName), resource.TestCheckResourceAttrPair(resourceName, "certificate_arn", certificateResourceName, "arn"), ), }, @@ -132,8 +138,9 @@ func TestAccACMCertificateValidation_validationRecordFQDNSRootAndWildcard(t *tes CheckDestroy: testAccCheckAcmCertificateDestroy, Steps: []resource.TestStep{ { - Config: testAccAcmCertificateValidation_validationRecordFqdnsTwoRoute53Records(rootDomain, rootDomain, strconv.Quote(wildcardDomain)), + Config: testAccAcmCertificateValidationValidationRecordFQDNsTwoRoute53RecordsConfig(rootDomain, rootDomain, strconv.Quote(wildcardDomain)), Check: resource.ComposeTestCheckFunc( + testAccCheckAcmCertificateValidationExists(resourceName), resource.TestCheckResourceAttrPair(resourceName, "certificate_arn", certificateResourceName, "arn"), ), }, @@ -155,8 +162,9 @@ func TestAccACMCertificateValidation_validationRecordFQDNSSan(t *testing.T) { CheckDestroy: testAccCheckAcmCertificateDestroy, Steps: []resource.TestStep{ { - Config: testAccAcmCertificateValidation_validationRecordFqdnsTwoRoute53Records(rootDomain, domain, strconv.Quote(sanDomain)), + Config: testAccAcmCertificateValidationValidationRecordFQDNsTwoRoute53RecordsConfig(rootDomain, domain, strconv.Quote(sanDomain)), Check: resource.ComposeTestCheckFunc( + testAccCheckAcmCertificateValidationExists(resourceName), resource.TestCheckResourceAttrPair(resourceName, "certificate_arn", certificateResourceName, "arn"), ), }, @@ -177,10 +185,12 @@ func TestAccACMCertificateValidation_validationRecordFQDNSWildcard(t *testing.T) CheckDestroy: testAccCheckAcmCertificateDestroy, Steps: []resource.TestStep{ { - Config: testAccAcmCertificateValidation_validationRecordFqdnsOneRoute53Record(rootDomain, wildcardDomain), + Config: testAccAcmCertificateValidationValidationRecordFQDNsOneRoute53RecordConfig(rootDomain, wildcardDomain), Check: resource.ComposeTestCheckFunc( + testAccCheckAcmCertificateValidationExists(resourceName), resource.TestCheckResourceAttrPair(resourceName, "certificate_arn", certificateResourceName, "arn"), ), + // ExpectNonEmptyPlan: true, // https://github.com/hashicorp/terraform-provider-aws/issues/16913 }, }, }) @@ -199,16 +209,41 @@ func TestAccACMCertificateValidation_validationRecordFQDNSWildcardAndRoot(t *tes CheckDestroy: testAccCheckAcmCertificateDestroy, Steps: []resource.TestStep{ { - Config: testAccAcmCertificateValidation_validationRecordFqdnsTwoRoute53Records(rootDomain, wildcardDomain, strconv.Quote(rootDomain)), + Config: testAccAcmCertificateValidationValidationRecordFQDNsTwoRoute53RecordsConfig(rootDomain, wildcardDomain, strconv.Quote(rootDomain)), Check: resource.ComposeTestCheckFunc( + testAccCheckAcmCertificateValidationExists(resourceName), resource.TestCheckResourceAttrPair(resourceName, "certificate_arn", certificateResourceName, "arn"), ), + // ExpectNonEmptyPlan: true, // https://github.com/hashicorp/terraform-provider-aws/issues/16913 }, }, }) } -func testAccAcmCertificateValidation_basic(rootZoneDomain, domainName string) string { +func testAccCheckAcmCertificateValidationExists(n string) 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 ACM Certificate Validation ID is set") + } + + conn := acctest.Provider.Meta().(*conns.AWSClient).ACMConn + + _, err := tfacm.FindCertificateValidationByARN(conn, rs.Primary.ID) + + if err != nil { + return err + } + + return nil + } +} + +func testAccAcmCertificateValidationConfig(rootZoneDomain, domainName string) string { return fmt.Sprintf(` resource "aws_acm_certificate" "test" { domain_name = %[1]q @@ -257,7 +292,7 @@ resource "aws_acm_certificate_validation" "test" { `, domainName, rootZoneDomain) } -func testAccAcmCertificateValidation_timeout(domainName string) string { +func testAccAcmCertificateValidationTimeoutConfig(domainName string) string { return fmt.Sprintf(` resource "aws_acm_certificate" "test" { domain_name = %[1]q @@ -274,7 +309,7 @@ resource "aws_acm_certificate_validation" "test" { `, domainName) } -func testAccAcmCertificateValidation_validationRecordFqdnsEmailValidation(domainName string) string { +func testAccAcmCertificateValidationValidationRecordFQDNsEmailValidationConfig(domainName string) string { return fmt.Sprintf(` resource "aws_acm_certificate" "test" { domain_name = %[1]q @@ -288,7 +323,7 @@ resource "aws_acm_certificate_validation" "test" { `, domainName) } -func testAccAcmCertificateValidation_validationRecordFqdnsOneRoute53Record(rootZoneDomain, domainName string) string { +func testAccAcmCertificateValidationValidationRecordFQDNsOneRoute53RecordConfig(rootZoneDomain, domainName string) string { return fmt.Sprintf(` resource "aws_acm_certificate" "test" { domain_name = %[1]q @@ -341,7 +376,7 @@ resource "aws_acm_certificate_validation" "test" { `, domainName, rootZoneDomain) } -func testAccAcmCertificateValidation_validationRecordFqdnsTwoRoute53Records(rootZoneDomain, domainName, subjectAlternativeNames string) string { +func testAccAcmCertificateValidationValidationRecordFQDNsTwoRoute53RecordsConfig(rootZoneDomain, domainName, subjectAlternativeNames string) string { return fmt.Sprintf(` resource "aws_acm_certificate" "test" { domain_name = %[1]q @@ -404,7 +439,7 @@ resource "aws_acm_certificate_validation" "test" { `, domainName, subjectAlternativeNames, rootZoneDomain) } -func testAccAcmCertificateValidation_validationRecordFqdnsWrongFqdn(domainName string) string { +func testAccAcmCertificateValidationValidationRecordFQDNsWrongFQDNConfig(domainName string) string { return fmt.Sprintf(` resource "aws_acm_certificate" "test" { domain_name = %[1]q diff --git a/website/docs/r/acm_certificate_validation.html.markdown b/website/docs/r/acm_certificate_validation.html.markdown index 5d68e0a8e34a..d9602205d6b0 100644 --- a/website/docs/r/acm_certificate_validation.html.markdown +++ b/website/docs/r/acm_certificate_validation.html.markdown @@ -144,4 +144,4 @@ In addition to all arguments above, the following attributes are exported: `acm_certificate_validation` provides the following [Timeouts](https://www.terraform.io/docs/configuration/blocks/resources/syntax.html#operation-timeouts) configuration options: -- `create` - (Default `45m`) How long to wait for a certificate to be issued. +- `create` - (Default `75m`) How long to wait for a certificate to be issued.