diff --git a/.changelog/41186.txt b/.changelog/41186.txt new file mode 100644 index 00000000000..f15dd8f7c03 --- /dev/null +++ b/.changelog/41186.txt @@ -0,0 +1,3 @@ +```release-note:bug +resource/aws_iam_server_certificate: Allow update of `name`, `name_prefix`, and `path` without forcing new resource +``` \ No newline at end of file diff --git a/internal/service/iam/server_certificate.go b/internal/service/iam/server_certificate.go index b3d9999e037..010c44c944e 100644 --- a/internal/service/iam/server_certificate.go +++ b/internal/service/iam/server_certificate.go @@ -74,7 +74,6 @@ func resourceServerCertificate() *schema.Resource { Type: schema.TypeString, Optional: true, Computed: true, - ForceNew: true, ConflictsWith: []string{names.AttrNamePrefix}, ValidateFunc: validation.StringLenBetween(0, 128), }, @@ -82,7 +81,6 @@ func resourceServerCertificate() *schema.Resource { Type: schema.TypeString, Optional: true, Computed: true, - ForceNew: true, ConflictsWith: []string{names.AttrName}, ValidateFunc: validation.StringLenBetween(0, 128-id.UniqueIDSuffixLength), }, @@ -90,7 +88,6 @@ func resourceServerCertificate() *schema.Resource { Type: schema.TypeString, Optional: true, Default: "/", - ForceNew: true, }, names.AttrPrivateKey: { Type: schema.TypeString, @@ -209,7 +206,49 @@ func resourceServerCertificateRead(ctx context.Context, d *schema.ResourceData, func resourceServerCertificateUpdate(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics { var diags diag.Diagnostics - // Tags only. + conn := meta.(*conns.AWSClient).IAMClient(ctx) + + if d.HasChanges(names.AttrName, names.AttrNamePrefix, names.AttrPath) { + input := &iam.UpdateServerCertificateInput{} + + if d.HasChange(names.AttrName) { + oldName, newName := d.GetChange(names.AttrName) + + // Handle both a name change and a switch to using a name prefix + newSSLCertName := create.Name(newName.(string), d.Get(names.AttrNamePrefix).(string)) + + input.ServerCertificateName = aws.String(oldName.(string)) + input.NewServerCertificateName = aws.String(newSSLCertName) + } else if d.HasChange(names.AttrNamePrefix) { + oldName := d.Get(names.AttrName).(string) + + // Handle only a name prefix change using an empty string as name (as it hasn't been changed) + newSSLCertName := create.Name("", d.Get(names.AttrNamePrefix).(string)) + + input.ServerCertificateName = aws.String(oldName) + input.NewServerCertificateName = aws.String(newSSLCertName) + } + nameChanged := input.NewServerCertificateName != nil + + if d.HasChange(names.AttrPath) { + if !nameChanged { + name := d.Get(names.AttrName).(string) + input.ServerCertificateName = aws.String(name) + } + input.NewPath = aws.String(d.Get(names.AttrPath).(string)) + } + + _, err := conn.UpdateServerCertificate(ctx, input) + + if err != nil { + return sdkdiag.AppendErrorf(diags, "updating IAM Server Certificate (%s): %s", d.Id(), err) + } + + // If the name was changed, the new name must be set in the state for tag update that precedes resource read + if nameChanged { + d.Set(names.AttrName, input.NewServerCertificateName) + } + } return append(diags, resourceServerCertificateRead(ctx, d, meta)...) } diff --git a/internal/service/iam/server_certificate_test.go b/internal/service/iam/server_certificate_test.go index ce93fe7b61b..9adb4414b51 100644 --- a/internal/service/iam/server_certificate_test.go +++ b/internal/service/iam/server_certificate_test.go @@ -9,6 +9,7 @@ import ( "strings" "testing" + "github.com/aws/aws-sdk-go-v2/aws" awstypes "github.com/aws/aws-sdk-go-v2/service/iam/types" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/id" sdkacctest "github.com/hashicorp/terraform-plugin-testing/helper/acctest" @@ -23,9 +24,10 @@ import ( func TestAccIAMServerCertificate_basic(t *testing.T) { ctx := acctest.Context(t) - var cert awstypes.ServerCertificate + var v1, v2 awstypes.ServerCertificate resourceName := "aws_iam_server_certificate.test" rName := sdkacctest.RandomWithPrefix(acctest.ResourcePrefix) + rNameUpdated := sdkacctest.RandomWithPrefix(acctest.ResourcePrefix) key := acctest.TLSRSAPrivateKeyPEM(t, 2048) certificate := acctest.TLSRSAX509SelfSignedCertificatePEM(t, key, "example.com") @@ -38,7 +40,7 @@ func TestAccIAMServerCertificate_basic(t *testing.T) { { Config: testAccServerCertificateConfig_basic(rName, key, certificate), Check: resource.ComposeTestCheckFunc( - testAccCheckServerCertificateExists(ctx, resourceName, &cert), + testAccCheckServerCertificateExists(ctx, resourceName, &v1), acctest.CheckResourceAttrGlobalARN(ctx, resourceName, names.AttrARN, "iam", fmt.Sprintf("server-certificate/%s", rName)), acctest.CheckResourceAttrRFC3339(resourceName, "expiration"), acctest.CheckResourceAttrRFC3339(resourceName, "upload_date"), @@ -56,6 +58,21 @@ func TestAccIAMServerCertificate_basic(t *testing.T) { ImportStateId: rName, ImportStateVerifyIgnore: []string{names.AttrPrivateKey}, }, + { + Config: testAccServerCertificateConfig_basic(rNameUpdated, key, certificate), + Check: resource.ComposeTestCheckFunc( + testAccCheckServerCertificateExists(ctx, resourceName, &v2), + testAccCheckServerCertficateNotRecreated(&v1, &v2), + acctest.CheckResourceAttrGlobalARN(ctx, resourceName, names.AttrARN, "iam", fmt.Sprintf("server-certificate/%s", rNameUpdated)), + acctest.CheckResourceAttrRFC3339(resourceName, "expiration"), + acctest.CheckResourceAttrRFC3339(resourceName, "upload_date"), + resource.TestCheckResourceAttr(resourceName, acctest.CtTagsPercent, "0"), + resource.TestCheckResourceAttr(resourceName, names.AttrName, rNameUpdated), + resource.TestCheckResourceAttr(resourceName, names.AttrNamePrefix, ""), + resource.TestCheckResourceAttr(resourceName, names.AttrPath, "/"), + resource.TestCheckResourceAttr(resourceName, "certificate_body", strings.TrimSpace(certificate)), + ), + }, }, }) } @@ -87,10 +104,13 @@ func TestAccIAMServerCertificate_nameGenerated(t *testing.T) { func TestAccIAMServerCertificate_namePrefix(t *testing.T) { ctx := acctest.Context(t) - var cert awstypes.ServerCertificate + var v1, v2, v3, v4 awstypes.ServerCertificate resourceName := "aws_iam_server_certificate.test" key := acctest.TLSRSAPrivateKeyPEM(t, 2048) certificate := acctest.TLSRSAX509SelfSignedCertificatePEM(t, key, "example.com") + namePrefix := "tf-acc-test-prefix-" + namePrefixUpdated := "tf-acc-test-prefix-updated-" + rName := sdkacctest.RandomWithPrefix(acctest.ResourcePrefix) resource.ParallelTest(t, resource.TestCase{ PreCheck: func() { acctest.PreCheck(ctx, t) }, @@ -99,11 +119,40 @@ func TestAccIAMServerCertificate_namePrefix(t *testing.T) { CheckDestroy: testAccCheckServerCertificateDestroy(ctx), Steps: []resource.TestStep{ { - Config: testAccServerCertificateConfig_namePrefix("tf-acc-test-prefix-", key, certificate), + Config: testAccServerCertificateConfig_namePrefix(namePrefix, key, certificate), Check: resource.ComposeTestCheckFunc( - testAccCheckServerCertificateExists(ctx, resourceName, &cert), - acctest.CheckResourceAttrNameFromPrefix(resourceName, names.AttrName, "tf-acc-test-prefix-"), - resource.TestCheckResourceAttr(resourceName, names.AttrNamePrefix, "tf-acc-test-prefix-"), + testAccCheckServerCertificateExists(ctx, resourceName, &v1), + acctest.CheckResourceAttrNameFromPrefix(resourceName, names.AttrName, namePrefix), + resource.TestCheckResourceAttr(resourceName, names.AttrNamePrefix, namePrefix), + ), + }, + { + Config: testAccServerCertificateConfig_namePrefix(namePrefixUpdated, key, certificate), + Check: resource.ComposeTestCheckFunc( + testAccCheckServerCertificateExists(ctx, resourceName, &v2), + testAccCheckServerCertficateNotRecreated(&v1, &v2), + acctest.CheckResourceAttrNameFromPrefix(resourceName, names.AttrName, namePrefixUpdated), + resource.TestCheckResourceAttr(resourceName, names.AttrNamePrefix, namePrefixUpdated), + ), + }, + // Change from name prefix to name + { + Config: testAccServerCertificateConfig_basic(rName, key, certificate), + Check: resource.ComposeTestCheckFunc( + testAccCheckServerCertificateExists(ctx, resourceName, &v3), + testAccCheckServerCertficateNotRecreated(&v2, &v3), + resource.TestCheckResourceAttr(resourceName, names.AttrName, rName), + resource.TestCheckResourceAttr(resourceName, names.AttrNamePrefix, ""), + ), + }, + // Change back from name to name prefix + { + Config: testAccServerCertificateConfig_namePrefix(namePrefix, key, certificate), + Check: resource.ComposeTestCheckFunc( + testAccCheckServerCertificateExists(ctx, resourceName, &v4), + testAccCheckServerCertficateNotRecreated(&v4, &v4), + acctest.CheckResourceAttrNameFromPrefix(resourceName, names.AttrName, namePrefix), + resource.TestCheckResourceAttr(resourceName, names.AttrNamePrefix, namePrefix), ), }, }, @@ -175,11 +224,14 @@ func TestAccIAMServerCertificate_file(t *testing.T) { func TestAccIAMServerCertificate_path(t *testing.T) { ctx := acctest.Context(t) - var cert awstypes.ServerCertificate + var v1, v2, v3 awstypes.ServerCertificate resourceName := "aws_iam_server_certificate.test" rName := sdkacctest.RandomWithPrefix(acctest.ResourcePrefix) + rNameUpdated := sdkacctest.RandomWithPrefix(acctest.ResourcePrefix) key := acctest.TLSRSAPrivateKeyPEM(t, 2048) certificate := acctest.TLSRSAX509SelfSignedCertificatePEM(t, key, "example.com") + path := "/test/" + pathUpdated := "/test/updated/" resource.ParallelTest(t, resource.TestCase{ PreCheck: func() { acctest.PreCheck(ctx, t) }, @@ -188,10 +240,10 @@ func TestAccIAMServerCertificate_path(t *testing.T) { CheckDestroy: testAccCheckServerCertificateDestroy(ctx), Steps: []resource.TestStep{ { - Config: testAccServerCertificateConfig_path(rName, "/test/", key, certificate), + Config: testAccServerCertificateConfig_path(rName, path, key, certificate), Check: resource.ComposeTestCheckFunc( - testAccCheckServerCertificateExists(ctx, resourceName, &cert), - resource.TestCheckResourceAttr(resourceName, names.AttrPath, "/test/"), + testAccCheckServerCertificateExists(ctx, resourceName, &v1), + resource.TestCheckResourceAttr(resourceName, names.AttrPath, path), ), }, { @@ -201,6 +253,24 @@ func TestAccIAMServerCertificate_path(t *testing.T) { ImportStateId: rName, ImportStateVerifyIgnore: []string{names.AttrPrivateKey}, }, + { + Config: testAccServerCertificateConfig_path(rName, pathUpdated, key, certificate), + Check: resource.ComposeTestCheckFunc( + testAccCheckServerCertificateExists(ctx, resourceName, &v2), + testAccCheckServerCertficateNotRecreated(&v1, &v2), + resource.TestCheckResourceAttr(resourceName, names.AttrPath, pathUpdated), + ), + }, + // Change both name and path + { + Config: testAccServerCertificateConfig_path(rNameUpdated, path, key, certificate), + Check: resource.ComposeTestCheckFunc( + testAccCheckServerCertificateExists(ctx, resourceName, &v3), + testAccCheckServerCertficateNotRecreated(&v2, &v3), + resource.TestCheckResourceAttr(resourceName, names.AttrName, rNameUpdated), + resource.TestCheckResourceAttr(resourceName, names.AttrPath, path), + ), + }, }, }) } @@ -256,6 +326,15 @@ func testAccCheckServerCertificateDestroy(ctx context.Context) resource.TestChec } } +func testAccCheckServerCertficateNotRecreated(v1, v2 *awstypes.ServerCertificate) resource.TestCheckFunc { + return func(s *terraform.State) error { + if aws.ToString(v1.ServerCertificateMetadata.ServerCertificateId) != aws.ToString(v2.ServerCertificateMetadata.ServerCertificateId) { + return fmt.Errorf("IAM Server Certificate recreated") + } + return nil + } +} + func testAccServerCertificateConfig_basic(rName, key, certificate string) string { return fmt.Sprintf(` resource "aws_iam_server_certificate" "test" { diff --git a/website/docs/r/iam_server_certificate.html.markdown b/website/docs/r/iam_server_certificate.html.markdown index 0e8994fa094..74f891a7626 100644 --- a/website/docs/r/iam_server_certificate.html.markdown +++ b/website/docs/r/iam_server_certificate.html.markdown @@ -93,20 +93,19 @@ resource "aws_elb" "ourapp" { This resource supports the following arguments: -* `name` - (Optional) The name of the Server Certificate. Do not include the - path in this value. If omitted, Terraform will assign a random, unique name. -* `name_prefix` - (Optional) Creates a unique name beginning with the specified - prefix. Conflicts with `name`. -* `certificate_body` – (Required) The contents of the public key certificate in +* `certificate_body` – (Required, Forces new resource) The contents of the public key certificate in PEM-encoded format. -* `certificate_chain` – (Optional) The contents of the certificate chain. +* `certificate_chain` – (Optional, Forces new resource) The contents of the certificate chain. This is typically a concatenation of the PEM-encoded public key certificates of the chain. -* `private_key` – (Required) The contents of the private key in PEM-encoded format. +* `name` - (Optional) The name of the Server Certificate. Do not include the path in this value. If omitted, Terraform will assign a random, unique name. +* `name_prefix` - (Optional) Creates a unique name beginning with the specified + prefix. Conflicts with `name`. * `path` - (Optional) The IAM path for the server certificate. If it is not included, it defaults to a slash (/). If this certificate is for use with AWS CloudFront, the path must be in format `/cloudfront/your_path_here`. See [IAM Identifiers][1] for more details on IAM Paths. +* `private_key` – (Required, Forces new resource) The contents of the private key in PEM-encoded format. * `tags` - (Optional) Map of resource tags for the server certificate. If configured with a provider [`default_tags` configuration block](https://registry.terraform.io/providers/hashicorp/aws/latest/docs#default_tags-configuration-block) present, tags with matching keys will overwrite those defined at the provider-level. ~> **NOTE:** AWS performs behind-the-scenes modifications to some certificate files if they do not adhere to a specific format. These modifications will result in terraform forever believing that it needs to update the resources since the local and AWS file contents will not match after theses modifications occur. In order to prevent this from happening you must ensure that all your PEM-encoded files use UNIX line-breaks and that `certificate_body` contains only one certificate. All other certificates should go in `certificate_chain`. It is common for some Certificate Authorities to issue certificate files that have DOS line-breaks and that are actually multiple certificates concatenated together in order to form a full certificate chain. @@ -118,7 +117,6 @@ This resource exports the following attributes in addition to the arguments abov * `arn` - The Amazon Resource Name (ARN) specifying the server certificate. * `expiration` - Date and time in [RFC3339 format](https://tools.ietf.org/html/rfc3339#section-5.8) on which the certificate is set to expire. * `id` - The unique Server Certificate name -* `name` - The name of the Server Certificate * `tags_all` - A map of tags assigned to the resource, including those inherited from the provider [`default_tags` configuration block](https://registry.terraform.io/providers/hashicorp/aws/latest/docs#default_tags-configuration-block). * `upload_date` - Date and time in [RFC3339 format](https://tools.ietf.org/html/rfc3339#section-5.8) when the server certificate was uploaded.