From 75ac23bec963845431c0310512677bc63b4ef039 Mon Sep 17 00:00:00 2001 From: Brian Flad Date: Tue, 2 Feb 2021 14:14:36 -0500 Subject: [PATCH] resource/aws_instance: Apply attribute waiter logic to iam_instance_profile attribute Reference: https://github.com/hashicorp/terraform-provider-aws/issues/16769 This resource attribute has long been the cause of the following flakey acceptance test: ``` === CONT TestAccAWSInstance_instanceProfileChange TestAccAWSInstance_instanceProfileChange: resource_aws_instance_test.go:1134: Step 3/5 error: After applying this test step, the plan was not empty. stdout: An execution plan has been generated and is shown below. Resource actions are indicated with the following symbols: ~ update in-place Terraform will perform the following actions: ~ resource "aws_instance" "test" { + iam_instance_profile = "tf-testacc-instance-1obw9bpsudzr" id = "i-0748e5ca403753317" tags = { "Name" = "tf-testacc-instance-1obw9bpsudzr" } } ``` Adding logic, similar to `SubnetMapCustomerOwnedIpOnLaunchUpdated` in https://github.com/hashicorp/terraform-provider-aws/pull/16676 can be used to ensure the attribute value has flipped correctly after calling the `AssociateIamInstanceProfile`, `DisassociateIamInstanceProfile` or `ReplaceIamInstanceProfile` APIs. This attribute waiter setup will be added to a forthcoming Retries and Waiters section in the Contribution Guide. Output from acceptance testing in AWS Commercial: ``` --- PASS: TestAccAWSInstance_addSecondaryInterface (242.73s) --- PASS: TestAccAWSInstance_addSecurityGroupNetworkInterface (230.14s) --- PASS: TestAccAWSInstance_associatePublic_defaultPrivate (110.25s) --- PASS: TestAccAWSInstance_associatePublic_defaultPublic (110.99s) --- PASS: TestAccAWSInstance_associatePublic_explicitPrivate (100.21s) --- PASS: TestAccAWSInstance_associatePublic_explicitPublic (110.40s) --- PASS: TestAccAWSInstance_associatePublic_overridePrivate (109.74s) --- PASS: TestAccAWSInstance_associatePublic_overridePublic (131.67s) --- PASS: TestAccAWSInstance_associatePublicIPAndPrivateIP (146.51s) --- PASS: TestAccAWSInstance_atLeastOneOtherEbsVolume (193.28s) --- PASS: TestAccAWSInstance_basic (88.80s) --- PASS: TestAccAWSInstance_blockDevices (121.48s) --- PASS: TestAccAWSInstance_blockDeviceTags_ebsAndRoot (210.18s) --- PASS: TestAccAWSInstance_blockDeviceTags_volumeTags (218.14s) --- PASS: TestAccAWSInstance_blockDeviceTags_withAttachedVolume (274.15s) --- PASS: TestAccAWSInstance_changeInstanceType (279.18s) --- PASS: TestAccAWSInstance_CreditSpecification_Empty_NonBurstable (146.18s) --- PASS: TestAccAWSInstance_creditSpecification_isNotAppliedToNonBurstable (121.58s) --- PASS: TestAccAWSInstance_creditSpecification_standardCpuCredits (151.11s) --- PASS: TestAccAWSInstance_creditSpecification_standardCpuCredits_t2Tot3Taint (417.38s) --- PASS: TestAccAWSInstance_creditSpecification_unknownCpuCredits_t2 (108.53s) --- PASS: TestAccAWSInstance_creditSpecification_unknownCpuCredits_t3 (139.87s) --- PASS: TestAccAWSInstance_creditSpecification_unlimitedCpuCredits (142.36s) --- PASS: TestAccAWSInstance_creditSpecification_unlimitedCpuCredits_t2Tot3Taint (209.11s) --- PASS: TestAccAWSInstance_creditSpecification_unspecifiedDefaultsToStandard (111.75s) --- PASS: TestAccAWSInstance_CreditSpecification_UnspecifiedToEmpty_NonBurstable (114.39s) --- PASS: TestAccAWSInstance_creditSpecification_updateCpuCredits (180.11s) --- PASS: TestAccAWSInstance_creditSpecificationT3_standardCpuCredits (136.87s) --- PASS: TestAccAWSInstance_creditSpecificationT3_unlimitedCpuCredits (125.41s) --- PASS: TestAccAWSInstance_creditSpecificationT3_unspecifiedDefaultsToUnlimited (127.50s) --- PASS: TestAccAWSInstance_creditSpecificationT3_updateCpuCredits (180.12s) --- PASS: TestAccAWSInstance_dedicatedInstance (133.35s) --- PASS: TestAccAWSInstance_disableApiTermination (184.77s) --- PASS: TestAccAWSInstance_disappears (78.61s) --- PASS: TestAccAWSInstance_EbsBlockDevice_InvalidIopsForVolumeType (12.67s) --- PASS: TestAccAWSInstance_EbsBlockDevice_InvalidThroughputForVolumeType (10.14s) --- PASS: TestAccAWSInstance_EbsBlockDevice_KmsKeyArn (117.10s) --- PASS: TestAccAWSInstance_EbsRootDevice_basic (193.58s) --- PASS: TestAccAWSInstance_EbsRootDevice_ModifyAll (152.37s) --- PASS: TestAccAWSInstance_EbsRootDevice_ModifyDeleteOnTermination (153.09s) --- PASS: TestAccAWSInstance_EbsRootDevice_ModifyIOPS_Io1 (169.55s) --- PASS: TestAccAWSInstance_EbsRootDevice_ModifyIOPS_Io2 (194.13s) --- PASS: TestAccAWSInstance_EbsRootDevice_ModifySize (165.98s) --- PASS: TestAccAWSInstance_EbsRootDevice_ModifyThroughput_Gp3 (163.26s) --- PASS: TestAccAWSInstance_EbsRootDevice_ModifyType (183.84s) --- PASS: TestAccAWSInstance_EbsRootDevice_MultipleBlockDevices_ModifyDeleteOnTermination (147.87s) --- PASS: TestAccAWSInstance_EbsRootDevice_MultipleBlockDevices_ModifySize (192.81s) --- PASS: TestAccAWSInstance_EbsRootDevice_MultipleDynamicEBSBlockDevices (232.62s) --- PASS: TestAccAWSInstance_Empty_PrivateIP (145.38s) --- PASS: TestAccAWSInstance_enclaveOptions (219.52s) --- PASS: TestAccAWSInstance_forceNewAndTagsDrift (283.16s) --- PASS: TestAccAWSInstance_getPasswordData_falseToTrue (157.98s) --- PASS: TestAccAWSInstance_getPasswordData_trueToFalse (198.56s) --- PASS: TestAccAWSInstance_GP2IopsDevice (99.64s) --- PASS: TestAccAWSInstance_GP2WithIopsValue (43.96s) --- PASS: TestAccAWSInstance_hibernation (236.92s) --- PASS: TestAccAWSInstance_inDefaultVpcBySgId (103.41s) --- PASS: TestAccAWSInstance_inDefaultVpcBySgName (131.23s) --- PASS: TestAccAWSInstance_inEc2Classic (107.37s) --- PASS: TestAccAWSInstance_instanceProfileChange (328.37s) --- PASS: TestAccAWSInstance_ipv6_supportAddressCount (145.65s) --- PASS: TestAccAWSInstance_ipv6_supportAddressCountWithIpv4 (101.01s) --- PASS: TestAccAWSInstance_ipv6AddressCountAndSingleAddressCausesError (19.16s) --- PASS: TestAccAWSInstance_keyPairCheck (131.83s) --- PASS: TestAccAWSInstance_metadataOptions (157.61s) --- PASS: TestAccAWSInstance_NetworkInstanceRemovingAllSecurityGroups (189.12s) --- PASS: TestAccAWSInstance_NetworkInstanceSecurityGroups (162.78s) --- PASS: TestAccAWSInstance_NetworkInstanceVPCSecurityGroupIDs (157.66s) --- PASS: TestAccAWSInstance_NewNetworkInterface_EmptyPrivateIPAndSecondaryPrivateIPs (149.96s) --- PASS: TestAccAWSInstance_NewNetworkInterface_EmptyPrivateIPAndSecondaryPrivateIPsUpdate (220.50s) --- PASS: TestAccAWSInstance_NewNetworkInterface_PrivateIPAndSecondaryPrivateIPs (121.07s) --- PASS: TestAccAWSInstance_NewNetworkInterface_PrivateIPAndSecondaryPrivateIPsUpdate (180.92s) --- PASS: TestAccAWSInstance_NewNetworkInterface_PublicIPAndSecondaryPrivateIPs (297.98s) --- PASS: TestAccAWSInstance_noAMIEphemeralDevices (121.40s) --- PASS: TestAccAWSInstance_placementGroup (121.32s) --- PASS: TestAccAWSInstance_primaryNetworkInterface (157.80s) --- PASS: TestAccAWSInstance_primaryNetworkInterfaceSourceDestCheck (189.25s) --- PASS: TestAccAWSInstance_privateIP (147.33s) --- PASS: TestAccAWSInstance_RootBlockDevice_KmsKeyArn (120.64s) --- PASS: TestAccAWSInstance_rootBlockDeviceMismatch (162.61s) --- PASS: TestAccAWSInstance_rootInstanceStore (101.64s) --- PASS: TestAccAWSInstance_sourceDestCheck (238.40s) --- PASS: TestAccAWSInstance_tags (143.95s) --- PASS: TestAccAWSInstance_UserData_EmptyStringToUnspecified (157.16s) --- PASS: TestAccAWSInstance_UserData_UnspecifiedToEmptyString (136.23s) --- PASS: TestAccAWSInstance_userDataBase64 (156.05s) --- PASS: TestAccAWSInstance_withIamInstanceProfile (127.90s) --- SKIP: TestAccAWSInstance_outpost (1.88s) --- PASS: TestAccAWSInstanceDataSource_AzUserData (106.26s) --- PASS: TestAccAWSInstanceDataSource_basic (110.08s) --- PASS: TestAccAWSInstanceDataSource_blockDevices (117.74s) --- PASS: TestAccAWSInstanceDataSource_blockDeviceTags (121.08s) --- PASS: TestAccAWSInstanceDataSource_creditSpecification (116.29s) --- PASS: TestAccAWSInstanceDataSource_EbsBlockDevice_KmsKeyId (136.51s) --- PASS: TestAccAWSInstanceDataSource_enclaveOptions (87.52s) --- PASS: TestAccAWSInstanceDataSource_getPasswordData_falseToTrue (188.34s) --- PASS: TestAccAWSInstanceDataSource_getPasswordData_trueToFalse (275.27s) --- PASS: TestAccAWSInstanceDataSource_GetUserData (208.51s) --- PASS: TestAccAWSInstanceDataSource_GetUserData_NoUserData (234.58s) --- PASS: TestAccAWSInstanceDataSource_gp2IopsDevice (98.45s) --- PASS: TestAccAWSInstanceDataSource_gp3ThroughputDevice (100.55s) --- PASS: TestAccAWSInstanceDataSource_keyPair (111.47s) --- PASS: TestAccAWSInstanceDataSource_metadataOptions (122.07s) --- PASS: TestAccAWSInstanceDataSource_PlacementGroup (129.96s) --- PASS: TestAccAWSInstanceDataSource_privateIP (113.30s) --- PASS: TestAccAWSInstanceDataSource_RootBlockDevice_KmsKeyId (110.23s) --- PASS: TestAccAWSInstanceDataSource_rootInstanceStore (120.74s) --- PASS: TestAccAWSInstanceDataSource_secondaryPrivateIPs (125.40s) --- PASS: TestAccAWSInstanceDataSource_SecurityGroups (126.16s) --- PASS: TestAccAWSInstanceDataSource_tags (96.07s) --- PASS: TestAccAWSInstanceDataSource_VPC (126.74s) --- PASS: TestAccAWSInstanceDataSource_VPCSecurityGroups (135.88s) ``` Output from acceptance testing in AWS GovCloud (US): ``` --- FAIL: TestAccAWSInstance_dedicatedInstance (31.08s) # https://github.com/hashicorp/terraform-provider-aws/issues/17412 --- FAIL: TestAccAWSInstance_EbsRootDevice_ModifyIOPS_Io2 (12.77s) # https://github.com/hashicorp/terraform-provider-aws/issues/17412 --- FAIL: TestAccAWSInstance_enclaveOptions (31.10s) # https://github.com/hashicorp/terraform-provider-aws/issues/17412 --- PASS: TestAccAWSInstance_addSecondaryInterface (201.36s) --- PASS: TestAccAWSInstance_addSecurityGroupNetworkInterface (169.84s) --- PASS: TestAccAWSInstance_associatePublic_defaultPrivate (100.80s) --- PASS: TestAccAWSInstance_associatePublic_defaultPublic (112.34s) --- PASS: TestAccAWSInstance_associatePublic_explicitPrivate (94.40s) --- PASS: TestAccAWSInstance_associatePublic_explicitPublic (112.61s) --- PASS: TestAccAWSInstance_associatePublic_overridePrivate (93.90s) --- PASS: TestAccAWSInstance_associatePublic_overridePublic (83.17s) --- PASS: TestAccAWSInstance_associatePublicIPAndPrivateIP (98.45s) --- PASS: TestAccAWSInstance_atLeastOneOtherEbsVolume (174.93s) --- PASS: TestAccAWSInstance_basic (90.77s) --- PASS: TestAccAWSInstance_blockDevices (124.40s) --- PASS: TestAccAWSInstance_blockDeviceTags_ebsAndRoot (144.71s) --- PASS: TestAccAWSInstance_blockDeviceTags_volumeTags (189.71s) --- PASS: TestAccAWSInstance_blockDeviceTags_withAttachedVolume (184.30s) --- PASS: TestAccAWSInstance_changeInstanceType (178.33s) --- PASS: TestAccAWSInstance_CreditSpecification_Empty_NonBurstable (140.47s) --- PASS: TestAccAWSInstance_creditSpecification_isNotAppliedToNonBurstable (111.53s) --- PASS: TestAccAWSInstance_creditSpecification_standardCpuCredits (120.43s) --- PASS: TestAccAWSInstance_creditSpecification_standardCpuCredits_t2Tot3Taint (223.58s) --- PASS: TestAccAWSInstance_creditSpecification_unknownCpuCredits_t2 (98.06s) --- PASS: TestAccAWSInstance_creditSpecification_unknownCpuCredits_t3 (90.11s) --- PASS: TestAccAWSInstance_creditSpecification_unlimitedCpuCredits (131.18s) --- PASS: TestAccAWSInstance_creditSpecification_unlimitedCpuCredits_t2Tot3Taint (196.64s) --- PASS: TestAccAWSInstance_creditSpecification_unspecifiedDefaultsToStandard (101.42s) --- PASS: TestAccAWSInstance_CreditSpecification_UnspecifiedToEmpty_NonBurstable (119.74s) --- PASS: TestAccAWSInstance_creditSpecification_updateCpuCredits (184.44s) --- PASS: TestAccAWSInstance_creditSpecificationT3_standardCpuCredits (135.21s) --- PASS: TestAccAWSInstance_creditSpecificationT3_unlimitedCpuCredits (167.30s) --- PASS: TestAccAWSInstance_creditSpecificationT3_unspecifiedDefaultsToUnlimited (100.56s) --- PASS: TestAccAWSInstance_creditSpecificationT3_updateCpuCredits (183.55s) --- PASS: TestAccAWSInstance_disableApiTermination (181.92s) --- PASS: TestAccAWSInstance_disappears (194.95s) --- PASS: TestAccAWSInstance_EbsBlockDevice_InvalidIopsForVolumeType (11.73s) --- PASS: TestAccAWSInstance_EbsBlockDevice_InvalidThroughputForVolumeType (10.91s) --- PASS: TestAccAWSInstance_EbsBlockDevice_KmsKeyArn (109.41s) --- PASS: TestAccAWSInstance_EbsRootDevice_basic (109.65s) --- PASS: TestAccAWSInstance_EbsRootDevice_ModifyAll (133.86s) --- PASS: TestAccAWSInstance_EbsRootDevice_ModifyDeleteOnTermination (147.04s) --- PASS: TestAccAWSInstance_EbsRootDevice_ModifyIOPS_Io1 (166.57s) --- PASS: TestAccAWSInstance_EbsRootDevice_ModifySize (156.04s) --- PASS: TestAccAWSInstance_EbsRootDevice_ModifyThroughput_Gp3 (134.16s) --- PASS: TestAccAWSInstance_EbsRootDevice_ModifyType (123.90s) --- PASS: TestAccAWSInstance_EbsRootDevice_MultipleBlockDevices_ModifyDeleteOnTermination (134.58s) --- PASS: TestAccAWSInstance_EbsRootDevice_MultipleBlockDevices_ModifySize (141.10s) --- PASS: TestAccAWSInstance_EbsRootDevice_MultipleDynamicEBSBlockDevices (202.53s) --- PASS: TestAccAWSInstance_Empty_PrivateIP (116.78s) --- PASS: TestAccAWSInstance_forceNewAndTagsDrift (173.61s) --- PASS: TestAccAWSInstance_getPasswordData_falseToTrue (146.02s) --- PASS: TestAccAWSInstance_getPasswordData_trueToFalse (191.54s) --- PASS: TestAccAWSInstance_GP2IopsDevice (86.02s) --- PASS: TestAccAWSInstance_GP2WithIopsValue (11.25s) --- PASS: TestAccAWSInstance_hibernation (256.05s) --- PASS: TestAccAWSInstance_inDefaultVpcBySgId (93.32s) --- PASS: TestAccAWSInstance_inDefaultVpcBySgName (106.90s) --- PASS: TestAccAWSInstance_instanceProfileChange (248.34s) --- PASS: TestAccAWSInstance_ipv6_supportAddressCount (112.67s) --- PASS: TestAccAWSInstance_ipv6_supportAddressCountWithIpv4 (114.70s) --- PASS: TestAccAWSInstance_ipv6AddressCountAndSingleAddressCausesError (29.25s) --- PASS: TestAccAWSInstance_keyPairCheck (98.40s) --- PASS: TestAccAWSInstance_metadataOptions (185.29s) --- PASS: TestAccAWSInstance_NetworkInstanceRemovingAllSecurityGroups (148.16s) --- PASS: TestAccAWSInstance_NetworkInstanceSecurityGroups (137.85s) --- PASS: TestAccAWSInstance_NetworkInstanceVPCSecurityGroupIDs (123.37s) --- PASS: TestAccAWSInstance_NewNetworkInterface_EmptyPrivateIPAndSecondaryPrivateIPs (135.65s) --- PASS: TestAccAWSInstance_NewNetworkInterface_EmptyPrivateIPAndSecondaryPrivateIPsUpdate (182.43s) --- PASS: TestAccAWSInstance_NewNetworkInterface_PrivateIPAndSecondaryPrivateIPs (101.76s) --- PASS: TestAccAWSInstance_NewNetworkInterface_PrivateIPAndSecondaryPrivateIPsUpdate (187.35s) --- PASS: TestAccAWSInstance_NewNetworkInterface_PublicIPAndSecondaryPrivateIPs (197.08s) --- PASS: TestAccAWSInstance_noAMIEphemeralDevices (92.96s) --- PASS: TestAccAWSInstance_placementGroup (126.79s) --- PASS: TestAccAWSInstance_primaryNetworkInterface (156.42s) --- PASS: TestAccAWSInstance_primaryNetworkInterfaceSourceDestCheck (125.43s) --- PASS: TestAccAWSInstance_privateIP (106.48s) --- PASS: TestAccAWSInstance_RootBlockDevice_KmsKeyArn (80.05s) --- PASS: TestAccAWSInstance_rootInstanceStore (111.78s) --- PASS: TestAccAWSInstance_sourceDestCheck (213.01s) --- PASS: TestAccAWSInstance_tags (128.83s) --- PASS: TestAccAWSInstance_UserData_EmptyStringToUnspecified (148.75s) --- PASS: TestAccAWSInstance_UserData_UnspecifiedToEmptyString (158.81s) --- PASS: TestAccAWSInstance_userDataBase64 (117.21s) --- PASS: TestAccAWSInstance_withIamInstanceProfile (123.11s) --- SKIP: TestAccAWSInstance_inEc2Classic (1.40s) --- SKIP: TestAccAWSInstance_outpost (0.70s) --- SKIP: TestAccAWSInstance_rootBlockDeviceMismatch (0.00s) --- FAIL: TestAccAWSInstanceDataSource_enclaveOptions (33.30s) # https://github.com/hashicorp/terraform-provider-aws/issues/17412 --- PASS: TestAccAWSInstanceDataSource_AzUserData (86.49s) --- PASS: TestAccAWSInstanceDataSource_basic (119.95s) --- PASS: TestAccAWSInstanceDataSource_blockDevices (128.42s) --- PASS: TestAccAWSInstanceDataSource_blockDeviceTags (130.34s) --- PASS: TestAccAWSInstanceDataSource_creditSpecification (119.83s) --- PASS: TestAccAWSInstanceDataSource_EbsBlockDevice_KmsKeyId (101.52s) --- PASS: TestAccAWSInstanceDataSource_getPasswordData_falseToTrue (175.43s) --- PASS: TestAccAWSInstanceDataSource_getPasswordData_trueToFalse (225.22s) --- PASS: TestAccAWSInstanceDataSource_GetUserData (240.00s) --- PASS: TestAccAWSInstanceDataSource_GetUserData_NoUserData (226.72s) --- PASS: TestAccAWSInstanceDataSource_gp2IopsDevice (99.37s) --- PASS: TestAccAWSInstanceDataSource_gp3ThroughputDevice (98.23s) --- PASS: TestAccAWSInstanceDataSource_keyPair (120.55s) --- PASS: TestAccAWSInstanceDataSource_metadataOptions (131.12s) --- PASS: TestAccAWSInstanceDataSource_PlacementGroup (150.17s) --- PASS: TestAccAWSInstanceDataSource_privateIP (112.23s) --- PASS: TestAccAWSInstanceDataSource_RootBlockDevice_KmsKeyId (159.42s) --- PASS: TestAccAWSInstanceDataSource_rootInstanceStore (128.18s) --- PASS: TestAccAWSInstanceDataSource_secondaryPrivateIPs (117.31s) --- PASS: TestAccAWSInstanceDataSource_SecurityGroups (114.26s) --- PASS: TestAccAWSInstanceDataSource_tags (117.87s) --- PASS: TestAccAWSInstanceDataSource_VPC (121.68s) --- PASS: TestAccAWSInstanceDataSource_VPCSecurityGroups (144.39s) ``` --- aws/data_source_aws_instance.go | 14 ++++- aws/internal/service/ec2/errors.go | 4 ++ aws/internal/service/ec2/finder/finder.go | 19 ++++++ aws/internal/service/ec2/waiter/status.go | 35 ++++++++++++ aws/internal/service/ec2/waiter/waiter.go | 18 ++++++ aws/internal/service/iam/arn.go | 40 +++++++++++++ aws/internal/service/iam/arn_test.go | 70 +++++++++++++++++++++++ aws/resource_aws_instance.go | 26 ++++++--- 8 files changed, 216 insertions(+), 10 deletions(-) create mode 100644 aws/internal/service/iam/arn.go create mode 100644 aws/internal/service/iam/arn_test.go diff --git a/aws/data_source_aws_instance.go b/aws/data_source_aws_instance.go index 3eb17e5c359..6bc9b89e899 100644 --- a/aws/data_source_aws_instance.go +++ b/aws/data_source_aws_instance.go @@ -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 { @@ -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 { diff --git a/aws/internal/service/ec2/errors.go b/aws/internal/service/ec2/errors.go index 9162be33aed..ec18186d301 100644 --- a/aws/internal/service/ec2/errors.go +++ b/aws/internal/service/ec2/errors.go @@ -19,6 +19,10 @@ const ( ErrCodeClientVpnRouteNotFound = "InvalidClientVpnRouteNotFound" ) +const ( + ErrCodeInvalidInstanceIDNotFound = "InvalidInstanceID.NotFound" +) + const ( InvalidSecurityGroupIDNotFound = "InvalidSecurityGroupID.NotFound" InvalidGroupNotFound = "InvalidGroup.NotFound" diff --git a/aws/internal/service/ec2/finder/finder.go b/aws/internal/service/ec2/finder/finder.go index 10e42d1faca..4a8966d6048 100644 --- a/aws/internal/service/ec2/finder/finder.go +++ b/aws/internal/service/ec2/finder/finder.go @@ -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{ diff --git a/aws/internal/service/ec2/waiter/status.go b/aws/internal/service/ec2/waiter/status.go index 47a78775504..b563f3819e4 100644 --- a/aws/internal/service/ec2/waiter/status.go +++ b/aws/internal/service/ec2/waiter/status.go @@ -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 ( @@ -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" diff --git a/aws/internal/service/ec2/waiter/waiter.go b/aws/internal/service/ec2/waiter/waiter.go index c8c516294ef..d7a5d99e213 100644 --- a/aws/internal/service/ec2/waiter/waiter.go +++ b/aws/internal/service/ec2/waiter/waiter.go @@ -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}, diff --git a/aws/internal/service/iam/arn.go b/aws/internal/service/iam/arn.go new file mode 100644 index 00000000000..c254c8228d3 --- /dev/null +++ b/aws/internal/service/iam/arn.go @@ -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 +} diff --git a/aws/internal/service/iam/arn_test.go b/aws/internal/service/iam/arn_test.go new file mode 100644 index 00000000000..2adc841f00b --- /dev/null +++ b/aws/internal/service/iam/arn_test.go @@ -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) + } + }) + } +} diff --git a/aws/resource_aws_instance.go b/aws/resource_aws_instance.go index 7e69c85f6c3..f071e1d86fb 100644 --- a/aws/resource_aws_instance.go +++ b/aws/resource_aws_instance.go @@ -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" ) @@ -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 @@ -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. @@ -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