diff --git a/.changelog/46870.txt b/.changelog/46870.txt new file mode 100644 index 000000000000..c166a5e1f640 --- /dev/null +++ b/.changelog/46870.txt @@ -0,0 +1,3 @@ +```release-note:bug +resource/aws_key_pair: Fix import drift by persisting and normalizing `public_key` +``` diff --git a/internal/service/ec2/ec2_key_pair.go b/internal/service/ec2/ec2_key_pair.go index 5eac7d280628..40fa13dfb6e0 100644 --- a/internal/service/ec2/ec2_key_pair.go +++ b/internal/service/ec2/ec2_key_pair.go @@ -85,7 +85,7 @@ func resourceKeyPair() *schema.Resource { StateFunc: func(v any) string { switch v := v.(type) { case string: - return strings.TrimSpace(v) + return normalizeOpenSSHPublicKey(v) default: return "" } @@ -142,6 +142,7 @@ func resourceKeyPairRead(ctx context.Context, d *schema.ResourceData, meta any) d.Set("key_name_prefix", create.NamePrefixFromName(aws.ToString(keyPair.KeyName))) d.Set("key_pair_id", keyPair.KeyPairId) d.Set("key_type", keyPair.KeyType) + d.Set(names.AttrPublicKey, normalizeOpenSSHPublicKey(aws.ToString(keyPair.PublicKey))) setTagsOut(ctx, keyPair.Tags) @@ -190,6 +191,23 @@ func openSSHPublicKeysEqual(v1, v2 string) bool { return key1.Type() == key2.Type() && bytes.Equal(key1.Marshal(), key2.Marshal()) } + +// normalizeOpenSSHPublicKey canonicalizes an OpenSSH public key so provider state +// does not drift when AWS rewrites the trailing comment or formatting. +// For example, AWS may return: +// "ssh-rsa AAAA... tf-acc-test-123\n" +// for config that was originally: +// "ssh-rsa AAAA... no-reply@hashicorp.com". +func normalizeOpenSSHPublicKey(v string) string { + key, _, _, _, err := ssh.ParseAuthorizedKey([]byte(v)) + + if err != nil { + return strings.TrimSpace(v) + } + + return strings.TrimSpace(string(ssh.MarshalAuthorizedKey(key))) +} + func keyPairARN(ctx context.Context, c *conns.AWSClient, keyName string) string { return c.RegionalARN(ctx, names.EC2, "key-pair/"+keyName) } diff --git a/internal/service/ec2/ec2_key_pair_test.go b/internal/service/ec2/ec2_key_pair_test.go index 9553a75bb1d2..459c6fedba09 100644 --- a/internal/service/ec2/ec2_key_pair_test.go +++ b/internal/service/ec2/ec2_key_pair_test.go @@ -44,14 +44,19 @@ func TestAccEC2KeyPair_basic(t *testing.T) { resource.TestMatchResourceAttr(resourceName, "fingerprint", regexache.MustCompile(`[0-9a-f]{2}(:[0-9a-f]{2}){15}`)), resource.TestCheckResourceAttr(resourceName, "key_name", rName), resource.TestCheckResourceAttr(resourceName, "key_name_prefix", ""), - resource.TestCheckResourceAttr(resourceName, names.AttrPublicKey, publicKey), + resource.TestCheckResourceAttrWith(resourceName, names.AttrPublicKey, func(v string) error { + if !tfec2.OpenSSHPublicKeysEqual(v, publicKey) { + return fmt.Errorf("Attribute 'public_key' expected %q, not equal to %q", publicKey, v) + } + + return nil + }), ), }, { - ResourceName: resourceName, - ImportState: true, - ImportStateVerify: true, - ImportStateVerifyIgnore: []string{names.AttrPublicKey}, + ResourceName: resourceName, + ImportState: true, + ImportStateVerify: true, }, }, }) @@ -83,10 +88,9 @@ func TestAccEC2KeyPair_tags(t *testing.T) { ), }, { - ResourceName: resourceName, - ImportState: true, - ImportStateVerify: true, - ImportStateVerifyIgnore: []string{names.AttrPublicKey}, + ResourceName: resourceName, + ImportState: true, + ImportStateVerify: true, }, { Config: testAccKeyPairConfig_tags2(rName, publicKey, acctest.CtKey1, acctest.CtValue1Updated, acctest.CtKey2, acctest.CtValue2), @@ -134,10 +138,9 @@ func TestAccEC2KeyPair_nameGenerated(t *testing.T) { ), }, { - ResourceName: resourceName, - ImportState: true, - ImportStateVerify: true, - ImportStateVerifyIgnore: []string{names.AttrPublicKey}, + ResourceName: resourceName, + ImportState: true, + ImportStateVerify: true, }, }, }) @@ -168,10 +171,48 @@ func TestAccEC2KeyPair_namePrefix(t *testing.T) { ), }, { - ResourceName: resourceName, - ImportState: true, - ImportStateVerify: true, - ImportStateVerifyIgnore: []string{names.AttrPublicKey}, + ResourceName: resourceName, + ImportState: true, + ImportStateVerify: true, + }, + }, + }) +} + +func TestAccEC2KeyPair_publicKey(t *testing.T) { + ctx := acctest.Context(t) + var keyPair awstypes.KeyPairInfo + resourceName := "aws_key_pair.test" + rName := acctest.RandomWithPrefix(t, acctest.ResourcePrefix) + + publicKey, _, err := sdkacctest.RandSSHKeyPair(acctest.DefaultEmailAddress) + if err != nil { + t.Fatalf("error generating random SSH key: %s", err) + } + + acctest.ParallelTest(ctx, t, resource.TestCase{ + PreCheck: func() { acctest.PreCheck(ctx, t) }, + ErrorCheck: acctest.ErrorCheck(t, names.EC2ServiceID), + ProtoV5ProviderFactories: acctest.ProtoV5ProviderFactories, + CheckDestroy: testAccCheckKeyPairDestroy(ctx, t), + Steps: []resource.TestStep{ + { + Config: testAccKeyPairConfig_basic(rName, publicKey), + Check: resource.ComposeAggregateTestCheckFunc( + testAccCheckKeyPairExists(ctx, t, resourceName, &keyPair), + resource.TestCheckResourceAttrWith(resourceName, names.AttrPublicKey, func(v string) error { + if !tfec2.OpenSSHPublicKeysEqual(v, publicKey) { + return fmt.Errorf("Attribute 'public_key' expected %q, not equal to %q", publicKey, v) + } + + return nil + }), + ), + }, + { + ResourceName: resourceName, + ImportState: true, + ImportStateVerify: true, }, }, }) diff --git a/internal/service/ec2/find.go b/internal/service/ec2/find.go index 91100b3636a2..98f6845248a5 100644 --- a/internal/service/ec2/find.go +++ b/internal/service/ec2/find.go @@ -5726,7 +5726,8 @@ func findKeyPairs(ctx context.Context, conn *ec2.Client, input *ec2.DescribeKeyP func findKeyPairByName(ctx context.Context, conn *ec2.Client, name string) (*awstypes.KeyPairInfo, error) { input := ec2.DescribeKeyPairsInput{ - KeyNames: []string{name}, + KeyNames: []string{name}, + IncludePublicKey: aws.Bool(true), } output, err := findKeyPair(ctx, conn, &input) diff --git a/website/docs/cdktf/python/r/key_pair.html.markdown b/website/docs/cdktf/python/r/key_pair.html.markdown index 03b518abcd56..64ee990400b3 100644 --- a/website/docs/cdktf/python/r/key_pair.html.markdown +++ b/website/docs/cdktf/python/r/key_pair.html.markdown @@ -87,6 +87,4 @@ Using `terraform import`, import Key Pairs using the `key_name`. For example: % terraform import aws_key_pair.deployer deployer-key ``` -~> **NOTE:** The AWS API does not include the public key in the response, so `terraform apply` will attempt to replace the key pair. There is currently no supported workaround for this limitation. - - \ No newline at end of file + diff --git a/website/docs/cdktf/typescript/r/key_pair.html.markdown b/website/docs/cdktf/typescript/r/key_pair.html.markdown index 6923683233f4..f941875e32ba 100644 --- a/website/docs/cdktf/typescript/r/key_pair.html.markdown +++ b/website/docs/cdktf/typescript/r/key_pair.html.markdown @@ -94,6 +94,4 @@ Using `terraform import`, import Key Pairs using the `keyName`. For example: % terraform import aws_key_pair.deployer deployer-key ``` -~> **NOTE:** The AWS API does not include the public key in the response, so `terraform apply` will attempt to replace the key pair. There is currently no supported workaround for this limitation. - - \ No newline at end of file + diff --git a/website/docs/r/key_pair.html.markdown b/website/docs/r/key_pair.html.markdown index bd40e0af88fc..c5ea4c2071d3 100644 --- a/website/docs/r/key_pair.html.markdown +++ b/website/docs/r/key_pair.html.markdown @@ -65,5 +65,3 @@ Using `terraform import`, import Key Pairs using the `key_name`. For example: ```console % terraform import aws_key_pair.deployer deployer-key ``` - -~> **NOTE:** The AWS API does not include the public key in the response, so `terraform apply` will attempt to replace the key pair. There is currently no supported workaround for this limitation.