Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

resource/aws_instance: Apply attribute waiter logic to iam_instance_profile attribute #17414

Merged
merged 1 commit into from
Feb 17, 2021
Merged
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
14 changes: 13 additions & 1 deletion aws/data_source_aws_instance.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import (
"github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema"
"github.com/terraform-providers/terraform-provider-aws/aws/internal/hashcode"
"github.com/terraform-providers/terraform-provider-aws/aws/internal/keyvaluetags"
tfiam "github.com/terraform-providers/terraform-provider-aws/aws/internal/service/iam"
)

func dataSourceAwsInstance() *schema.Resource {
Expand Down Expand Up @@ -458,7 +459,18 @@ func instanceDescriptionAttributes(d *schema.ResourceData, instance *ec2.Instanc
d.Set("private_dns", instance.PrivateDnsName)
d.Set("private_ip", instance.PrivateIpAddress)
d.Set("outpost_arn", instance.OutpostArn)
d.Set("iam_instance_profile", iamInstanceProfileArnToName(instance.IamInstanceProfile))

if instance.IamInstanceProfile != nil && instance.IamInstanceProfile.Arn != nil {
name, err := tfiam.InstanceProfileARNToName(aws.StringValue(instance.IamInstanceProfile.Arn))

if err != nil {
return fmt.Errorf("error setting iam_instance_profile: %w", err)
}

d.Set("iam_instance_profile", name)
} else {
d.Set("iam_instance_profile", nil)
}

// iterate through network interfaces, and set subnet, network_interface, public_addr
if len(instance.NetworkInterfaces) > 0 {
Expand Down
4 changes: 4 additions & 0 deletions aws/internal/service/ec2/errors.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,10 @@ const (
ErrCodeClientVpnRouteNotFound = "InvalidClientVpnRouteNotFound"
)

const (
ErrCodeInvalidInstanceIDNotFound = "InvalidInstanceID.NotFound"
)

const (
InvalidSecurityGroupIDNotFound = "InvalidSecurityGroupID.NotFound"
InvalidGroupNotFound = "InvalidGroup.NotFound"
Expand Down
19 changes: 19 additions & 0 deletions aws/internal/service/ec2/finder/finder.go
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,25 @@ func ClientVpnRouteByID(conn *ec2.EC2, routeID string) (*ec2.DescribeClientVpnRo
return ClientVpnRoute(conn, endpointID, targetSubnetID, destinationCidr)
}

// InstanceByID looks up a Instance by ID. When not found, returns nil and potentially an API error.
func InstanceByID(conn *ec2.EC2, id string) (*ec2.Instance, error) {
input := &ec2.DescribeInstancesInput{
InstanceIds: aws.StringSlice([]string{id}),
}

output, err := conn.DescribeInstances(input)

if err != nil {
return nil, err
}

if output == nil || len(output.Reservations) == 0 || output.Reservations[0] == nil || len(output.Reservations[0].Instances) == 0 || output.Reservations[0].Instances[0] == nil {
return nil, nil
}

return output.Reservations[0].Instances[0], nil
}

// SecurityGroupByID looks up a security group by ID. When not found, returns nil and potentially an API error.
func SecurityGroupByID(conn *ec2.EC2, id string) (*ec2.SecurityGroup, error) {
req := &ec2.DescribeSecurityGroupsInput{
Expand Down
35 changes: 35 additions & 0 deletions aws/internal/service/ec2/waiter/status.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import (
"github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource"
tfec2 "github.com/terraform-providers/terraform-provider-aws/aws/internal/service/ec2"
"github.com/terraform-providers/terraform-provider-aws/aws/internal/service/ec2/finder"
tfiam "github.com/terraform-providers/terraform-provider-aws/aws/internal/service/iam"
)

const (
Expand Down Expand Up @@ -210,6 +211,40 @@ func ClientVpnRouteStatus(conn *ec2.EC2, routeID string) resource.StateRefreshFu
}
}

// InstanceIamInstanceProfile fetches the Instance and its IamInstanceProfile
//
// The EC2 API accepts a name and always returns an ARN, so it is converted
// back to the name to prevent unexpected differences.
func InstanceIamInstanceProfile(conn *ec2.EC2, id string) resource.StateRefreshFunc {
return func() (interface{}, string, error) {
instance, err := finder.InstanceByID(conn, id)

if tfawserr.ErrCodeEquals(err, tfec2.ErrCodeInvalidInstanceIDNotFound) {
return nil, "", nil
}

if err != nil {
return nil, "", err
}

if instance == nil {
return nil, "", nil
}

if instance.IamInstanceProfile == nil || instance.IamInstanceProfile.Arn == nil {
return instance, "", nil
}

name, err := tfiam.InstanceProfileARNToName(aws.StringValue(instance.IamInstanceProfile.Arn))

if err != nil {
return instance, "", err
}

return instance, name, nil
}
}

const (
SecurityGroupStatusCreated = "Created"

Expand Down
18 changes: 18 additions & 0 deletions aws/internal/service/ec2/waiter/waiter.go
Original file line number Diff line number Diff line change
Expand Up @@ -233,6 +233,24 @@ func ClientVpnRouteDeleted(conn *ec2.EC2, routeID string) (*ec2.ClientVpnRoute,
return nil, err
}

func InstanceIamInstanceProfileUpdated(conn *ec2.EC2, instanceID string, expectedValue string) (*ec2.Instance, error) {
stateConf := &resource.StateChangeConf{
Target: []string{expectedValue},
Refresh: InstanceIamInstanceProfile(conn, instanceID),
Timeout: InstanceAttributePropagationTimeout,
Delay: 10 * time.Second,
MinTimeout: 3 * time.Second,
}

outputRaw, err := stateConf.WaitForState()

if output, ok := outputRaw.(*ec2.Instance); ok {
return output, err
}

return nil, err
}

func SecurityGroupCreated(conn *ec2.EC2, id string, timeout time.Duration) (*ec2.SecurityGroup, error) {
stateConf := &resource.StateChangeConf{
Pending: []string{SecurityGroupStatusNotFound},
Expand Down
40 changes: 40 additions & 0 deletions aws/internal/service/iam/arn.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
package iam

import (
"fmt"
"strings"

"github.com/aws/aws-sdk-go/aws/arn"
)

const (
ARNSeparator = "/"
ARNService = "iam"

InstanceProfileResourcePrefix = "instance-profile"
)

// InstanceProfileARNToName converts Amazon Resource Name (ARN) to Name.
func InstanceProfileARNToName(inputARN string) (string, error) {
parsedARN, err := arn.Parse(inputARN)

if err != nil {
return "", fmt.Errorf("error parsing ARN (%s): %w", inputARN, err)
}

if actual, expected := parsedARN.Service, ARNService; actual != expected {
return "", fmt.Errorf("expected service %s in ARN (%s), got: %s", expected, inputARN, actual)
}

resourceParts := strings.Split(parsedARN.Resource, ARNSeparator)

if actual, expected := len(resourceParts), 2; actual != expected {
return "", fmt.Errorf("expected %d resource parts in ARN (%s), got: %d", expected, inputARN, actual)
}

if actual, expected := resourceParts[0], InstanceProfileResourcePrefix; actual != expected {
return "", fmt.Errorf("expected resource prefix %s in ARN (%s), got: %s", expected, inputARN, actual)
}

return resourceParts[1], nil
}
70 changes: 70 additions & 0 deletions aws/internal/service/iam/arn_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
package iam_test

import (
"regexp"
"testing"

"github.com/terraform-providers/terraform-provider-aws/aws/internal/service/iam"
)

func TestInstanceProfileARNToName(t *testing.T) {
testCases := []struct {
TestName string
InputARN string
ExpectedError *regexp.Regexp
ExpectedName string
}{
{
TestName: "empty ARN",
InputARN: "",
ExpectedError: regexp.MustCompile(`error parsing ARN`),
},
{
TestName: "unparsable ARN",
InputARN: "test",
ExpectedError: regexp.MustCompile(`error parsing ARN`),
},
{
TestName: "invalid ARN service",
InputARN: "arn:aws:ec2:us-east-1:123456789012:instance/i-12345678",
ExpectedError: regexp.MustCompile(`expected service iam`),
},
{
TestName: "invalid ARN resource parts",
InputARN: "arn:aws:iam:us-east-1:123456789012:instance-profile/test/name",
ExpectedError: regexp.MustCompile(`expected 2 resource parts`),
},
{
TestName: "invalid ARN resource prefix",
InputARN: "arn:aws:iam:us-east-1:123456789012:role/name",
ExpectedError: regexp.MustCompile(`expected resource prefix instance-profile`),
},
{
TestName: "valid ARN",
InputARN: "arn:aws:iam:us-east-1:123456789012:instance-profile/name",
ExpectedName: "name",
},
}

for _, testCase := range testCases {
t.Run(testCase.TestName, func(t *testing.T) {
got, err := iam.InstanceProfileARNToName(testCase.InputARN)

if err == nil && testCase.ExpectedError != nil {
t.Fatalf("expected error %s, got no error", testCase.ExpectedError.String())
}

if err != nil && testCase.ExpectedError == nil {
t.Fatalf("got unexpected error: %s", err)
}

if err != nil && !testCase.ExpectedError.MatchString(err.Error()) {
t.Fatalf("expected error %s, got: %s", testCase.ExpectedError.String(), err)
}

if got != testCase.ExpectedName {
t.Errorf("got %s, expected %s", got, testCase.ExpectedName)
}
})
}
}
26 changes: 17 additions & 9 deletions aws/resource_aws_instance.go
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ import (
"github.com/terraform-providers/terraform-provider-aws/aws/internal/keyvaluetags"
tfec2 "github.com/terraform-providers/terraform-provider-aws/aws/internal/service/ec2"
"github.com/terraform-providers/terraform-provider-aws/aws/internal/service/ec2/waiter"
tfiam "github.com/terraform-providers/terraform-provider-aws/aws/internal/service/iam"
"github.com/terraform-providers/terraform-provider-aws/aws/internal/tfresource"
)

Expand Down Expand Up @@ -813,7 +814,18 @@ func resourceAwsInstanceRead(d *schema.ResourceData, meta interface{}) error {
d.Set("private_dns", instance.PrivateDnsName)
d.Set("private_ip", instance.PrivateIpAddress)
d.Set("outpost_arn", instance.OutpostArn)
d.Set("iam_instance_profile", iamInstanceProfileArnToName(instance.IamInstanceProfile))

if instance.IamInstanceProfile != nil && instance.IamInstanceProfile.Arn != nil {
name, err := tfiam.InstanceProfileARNToName(aws.StringValue(instance.IamInstanceProfile.Arn))

if err != nil {
return fmt.Errorf("error setting iam_instance_profile: %w", err)
}

d.Set("iam_instance_profile", name)
} else {
d.Set("iam_instance_profile", nil)
}

// Set configured Network Interface Device Index Slice
// We only want to read, and populate state for the configured network_interface attachments. Otherwise, other
Expand Down Expand Up @@ -1109,6 +1121,10 @@ func resourceAwsInstanceUpdate(d *schema.ResourceData, meta interface{}) error {
}
}
}

if _, err := waiter.InstanceIamInstanceProfileUpdated(conn, d.Id(), d.Get("iam_instance_profile").(string)); err != nil {
return fmt.Errorf("error waiting for EC2 Instance (%s) IAM Instance Profile update: %w", d.Id(), err)
}
}

// SourceDestCheck can only be modified on an instance without manually specified network interfaces.
Expand Down Expand Up @@ -2466,14 +2482,6 @@ func waitForInstanceDeletion(conn *ec2.EC2, id string, timeout time.Duration) er
return nil
}

func iamInstanceProfileArnToName(ip *ec2.IamInstanceProfile) string {
if ip == nil || ip.Arn == nil {
return ""
}
parts := strings.Split(aws.StringValue(ip.Arn), "/")
return parts[len(parts)-1]
}

func userDataHashSum(user_data string) string {
// Check whether the user_data is not Base64 encoded.
// Always calculate hash of base64 decoded value since we
Expand Down