Skip to content

Commit

Permalink
Merge pull request #41186 from acwwat/b-aws_iam_server_certificate-al…
Browse files Browse the repository at this point in the history
…low_arg_updates

fix: Allow name, name_prefix_and path update for aws_iam_server_certificate
  • Loading branch information
ewbankkit authored Feb 3, 2025
2 parents a5dd2b0 + e8df824 commit 0bb20e1
Show file tree
Hide file tree
Showing 4 changed files with 142 additions and 23 deletions.
3 changes: 3 additions & 0 deletions .changelog/41186.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
```release-note:bug
resource/aws_iam_server_certificate: Allow update of `name`, `name_prefix`, and `path` without forcing new resource
```
47 changes: 43 additions & 4 deletions internal/service/iam/server_certificate.go
Original file line number Diff line number Diff line change
Expand Up @@ -74,23 +74,20 @@ func resourceServerCertificate() *schema.Resource {
Type: schema.TypeString,
Optional: true,
Computed: true,
ForceNew: true,
ConflictsWith: []string{names.AttrNamePrefix},
ValidateFunc: validation.StringLenBetween(0, 128),
},
names.AttrNamePrefix: {
Type: schema.TypeString,
Optional: true,
Computed: true,
ForceNew: true,
ConflictsWith: []string{names.AttrName},
ValidateFunc: validation.StringLenBetween(0, 128-id.UniqueIDSuffixLength),
},
names.AttrPath: {
Type: schema.TypeString,
Optional: true,
Default: "/",
ForceNew: true,
},
names.AttrPrivateKey: {
Type: schema.TypeString,
Expand Down Expand Up @@ -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)...)
}
Expand Down
101 changes: 90 additions & 11 deletions internal/service/iam/server_certificate_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand All @@ -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")

Expand All @@ -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"),
Expand All @@ -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)),
),
},
},
})
}
Expand Down Expand Up @@ -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) },
Expand All @@ -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),
),
},
},
Expand Down Expand Up @@ -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) },
Expand All @@ -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),
),
},
{
Expand All @@ -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),
),
},
},
})
}
Expand Down Expand Up @@ -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" {
Expand Down
14 changes: 6 additions & 8 deletions website/docs/r/iam_server_certificate.html.markdown
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand All @@ -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.

Expand Down

0 comments on commit 0bb20e1

Please sign in to comment.