Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions .changelog/46870.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
```release-note:bug
resource/aws_key_pair: Fix import drift by persisting and normalizing `public_key`
```
20 changes: 19 additions & 1 deletion internal/service/ec2/ec2_key_pair.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 ""
}
Expand Down Expand Up @@ -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)

Expand Down Expand Up @@ -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)
}
75 changes: 58 additions & 17 deletions internal/service/ec2/ec2_key_pair_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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,
},
},
})
Expand Down Expand Up @@ -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),
Expand Down Expand Up @@ -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,
},
},
})
Expand Down Expand Up @@ -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,
},
},
})
Expand Down
3 changes: 2 additions & 1 deletion internal/service/ec2/find.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
4 changes: 1 addition & 3 deletions website/docs/cdktf/python/r/key_pair.html.markdown
Original file line number Diff line number Diff line change
Expand Up @@ -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.

<!-- cache-key: cdktf-0.20.8 input-2d5b9a919919188a55815230f7ef8313f298eb6827fc618c8a8bece269766d53 -->
<!-- cache-key: cdktf-0.20.8 input-2d5b9a919919188a55815230f7ef8313f298eb6827fc618c8a8bece269766d53 -->
4 changes: 1 addition & 3 deletions website/docs/cdktf/typescript/r/key_pair.html.markdown
Original file line number Diff line number Diff line change
Expand Up @@ -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.

<!-- cache-key: cdktf-0.20.8 input-2d5b9a919919188a55815230f7ef8313f298eb6827fc618c8a8bece269766d53 -->
<!-- cache-key: cdktf-0.20.8 input-2d5b9a919919188a55815230f7ef8313f298eb6827fc618c8a8bece269766d53 -->
2 changes: 0 additions & 2 deletions website/docs/r/key_pair.html.markdown
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Loading