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

r/iam_instance_profile - detach role when role doesn't exist + remove when deleted from state #16188

Merged
merged 7 commits into from
Feb 11, 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
3 changes: 3 additions & 0 deletions .changelog/16188.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
```release-note:bug
resource/aws_iam_instance_profile: Detach role when role doesn't exist + remove when deleted from state.
```
96 changes: 56 additions & 40 deletions aws/resource_aws_iam_instance_profile.go
Original file line number Diff line number Diff line change
Expand Up @@ -28,17 +28,10 @@ func resourceAwsIamInstanceProfile() *schema.Resource {
Type: schema.TypeString,
Computed: true,
},

"create_date": {
Type: schema.TypeString,
Computed: true,
},

"unique_id": {
Type: schema.TypeString,
Computed: true,
},

"name": {
Type: schema.TypeString,
Optional: true,
Expand All @@ -50,7 +43,6 @@ func resourceAwsIamInstanceProfile() *schema.Resource {
validation.StringMatch(regexp.MustCompile(`^[\w+=,.@-]*$`), "must match [\\w+=,.@-]"),
),
},

"name_prefix": {
Type: schema.TypeString,
Optional: true,
Expand All @@ -61,24 +53,26 @@ func resourceAwsIamInstanceProfile() *schema.Resource {
validation.StringMatch(regexp.MustCompile(`^[\w+=,.@-]*$`), "must match [\\w+=,.@-]"),
),
},

"path": {
Type: schema.TypeString,
Optional: true,
Default: "/",
ForceNew: true,
},

"role": {
Type: schema.TypeString,
Optional: true,
},
"unique_id": {
Type: schema.TypeString,
Computed: true,
},
},
}
}

func resourceAwsIamInstanceProfileCreate(d *schema.ResourceData, meta interface{}) error {
iamconn := meta.(*AWSClient).iamconn
conn := meta.(*AWSClient).iamconn

var name string
if v, ok := d.GetOk("name"); ok {
Expand All @@ -95,12 +89,12 @@ func resourceAwsIamInstanceProfileCreate(d *schema.ResourceData, meta interface{
}

var err error
response, err := iamconn.CreateInstanceProfile(request)
response, err := conn.CreateInstanceProfile(request)
if err == nil {
err = instanceProfileReadResult(d, response.InstanceProfile)
}
if err != nil {
return fmt.Errorf("Error creating IAM instance profile %s: %s", name, err)
return fmt.Errorf("creating IAM instance profile %s: %w", name, err)
}

waiterRequest := &iam.GetInstanceProfileInput{
Expand All @@ -109,27 +103,27 @@ func resourceAwsIamInstanceProfileCreate(d *schema.ResourceData, meta interface{
// don't return until the IAM service reports that the instance profile is ready.
// this ensures that terraform resources which rely on the instance profile will 'see'
// that the instance profile exists.
err = iamconn.WaitUntilInstanceProfileExists(waiterRequest)
err = conn.WaitUntilInstanceProfileExists(waiterRequest)
if err != nil {
return fmt.Errorf("Timed out while waiting for instance profile %s: %s", name, err)
return fmt.Errorf("timed out while waiting for instance profile %s: %w", name, err)
}

return resourceAwsIamInstanceProfileUpdate(d, meta)
}

func instanceProfileAddRole(iamconn *iam.IAM, profileName, roleName string) error {
func instanceProfileAddRole(conn *iam.IAM, profileName, roleName string) error {
request := &iam.AddRoleToInstanceProfileInput{
InstanceProfileName: aws.String(profileName),
RoleName: aws.String(roleName),
}

err := resource.Retry(30*time.Second, func() *resource.RetryError {
var err error
_, err = iamconn.AddRoleToInstanceProfile(request)
_, err = conn.AddRoleToInstanceProfile(request)
// IAM unfortunately does not provide a better error code or message for eventual consistency
// InvalidParameterValue: Value (XXX) for parameter iamInstanceProfile.name is invalid. Invalid IAM Instance Profile name
// NoSuchEntity: The request was rejected because it referenced an entity that does not exist. The error message describes the entity. HTTP Status Code: 404
if isAWSErr(err, "InvalidParameterValue", "Invalid IAM Instance Profile name") || isAWSErr(err, "NoSuchEntity", "The role with name") {
if isAWSErr(err, "InvalidParameterValue", "Invalid IAM Instance Profile name") || isAWSErr(err, iam.ErrCodeNoSuchEntityException, "The role with name") {
return resource.RetryableError(err)
}
if err != nil {
Expand All @@ -138,56 +132,56 @@ func instanceProfileAddRole(iamconn *iam.IAM, profileName, roleName string) erro
return nil
})
if isResourceTimeoutError(err) {
_, err = iamconn.AddRoleToInstanceProfile(request)
_, err = conn.AddRoleToInstanceProfile(request)
}
if err != nil {
return fmt.Errorf("Error adding IAM Role %s to Instance Profile %s: %s", roleName, profileName, err)
return fmt.Errorf("adding IAM Role %s to Instance Profile %s: %w", roleName, profileName, err)
}

return err
}

func instanceProfileRemoveRole(iamconn *iam.IAM, profileName, roleName string) error {
func instanceProfileRemoveRole(conn *iam.IAM, profileName, roleName string) error {
request := &iam.RemoveRoleFromInstanceProfileInput{
InstanceProfileName: aws.String(profileName),
RoleName: aws.String(roleName),
}

_, err := iamconn.RemoveRoleFromInstanceProfile(request)
if isAWSErr(err, "NoSuchEntity", "") {
_, err := conn.RemoveRoleFromInstanceProfile(request)
if isAWSErr(err, iam.ErrCodeNoSuchEntityException, "") {
return nil
}
return err
}

func instanceProfileRemoveAllRoles(d *schema.ResourceData, iamconn *iam.IAM) error {
func instanceProfileRemoveAllRoles(d *schema.ResourceData, conn *iam.IAM) error {
if role, ok := d.GetOk("role"); ok {
err := instanceProfileRemoveRole(iamconn, d.Id(), role.(string))
err := instanceProfileRemoveRole(conn, d.Id(), role.(string))
if err != nil {
return fmt.Errorf("Error removing role %s from IAM instance profile %s: %s", role, d.Id(), err)
return fmt.Errorf("removing role %s from IAM instance profile %s: %w", role, d.Id(), err)
}
}

return nil
}

func resourceAwsIamInstanceProfileUpdate(d *schema.ResourceData, meta interface{}) error {
iamconn := meta.(*AWSClient).iamconn
conn := meta.(*AWSClient).iamconn

if d.HasChange("role") {
oldRole, newRole := d.GetChange("role")

if oldRole.(string) != "" {
err := instanceProfileRemoveRole(iamconn, d.Id(), oldRole.(string))
err := instanceProfileRemoveRole(conn, d.Id(), oldRole.(string))
if err != nil {
return fmt.Errorf("Error adding role %s to IAM instance profile %s: %s", oldRole.(string), d.Id(), err)
return fmt.Errorf("removing role %s to IAM instance profile %s: %w", oldRole.(string), d.Id(), err)
}
}

if newRole.(string) != "" {
err := instanceProfileAddRole(iamconn, d.Id(), newRole.(string))
err := instanceProfileAddRole(conn, d.Id(), newRole.(string))
if err != nil {
return fmt.Errorf("Error adding role %s to IAM instance profile %s: %s", newRole.(string), d.Id(), err)
return fmt.Errorf("adding role %s to IAM instance profile %s: %w", newRole.(string), d.Id(), err)
}
}
}
Expand All @@ -196,38 +190,60 @@ func resourceAwsIamInstanceProfileUpdate(d *schema.ResourceData, meta interface{
}

func resourceAwsIamInstanceProfileRead(d *schema.ResourceData, meta interface{}) error {
iamconn := meta.(*AWSClient).iamconn
conn := meta.(*AWSClient).iamconn

request := &iam.GetInstanceProfileInput{
InstanceProfileName: aws.String(d.Id()),
}

result, err := iamconn.GetInstanceProfile(request)
if isAWSErr(err, "NoSuchEntity", "") {
result, err := conn.GetInstanceProfile(request)
if isAWSErr(err, iam.ErrCodeNoSuchEntityException, "") {
log.Printf("[WARN] IAM Instance Profile %s is already gone", d.Id())
d.SetId("")
return nil
}
if err != nil {
return fmt.Errorf("Error reading IAM instance profile %s: %s", d.Id(), err)
return fmt.Errorf("reading IAM instance profile %s: %w", d.Id(), err)
}

return instanceProfileReadResult(d, result.InstanceProfile)
instanceProfile := result.InstanceProfile
if instanceProfile.Roles != nil && len(instanceProfile.Roles) > 0 {
roleName := aws.StringValue(instanceProfile.Roles[0].RoleName)
input := &iam.GetRoleInput{
RoleName: aws.String(roleName),
}

_, err := conn.GetRole(input)
if err != nil {
if isAWSErr(err, iam.ErrCodeNoSuchEntityException, "") {
err := instanceProfileRemoveRole(conn, d.Id(), roleName)
if err != nil {
return fmt.Errorf("removing role %s to IAM instance profile %s: %w", roleName, d.Id(), err)
}
}
return fmt.Errorf("reading IAM Role %s attcahed to IAM Instance Profile %s: %w", roleName, d.Id(), err)
}
}

return instanceProfileReadResult(d, instanceProfile)
}

func resourceAwsIamInstanceProfileDelete(d *schema.ResourceData, meta interface{}) error {
iamconn := meta.(*AWSClient).iamconn
conn := meta.(*AWSClient).iamconn

if err := instanceProfileRemoveAllRoles(d, iamconn); err != nil {
if err := instanceProfileRemoveAllRoles(d, conn); err != nil {
return err
}

request := &iam.DeleteInstanceProfileInput{
InstanceProfileName: aws.String(d.Id()),
}
_, err := iamconn.DeleteInstanceProfile(request)
_, err := conn.DeleteInstanceProfile(request)
if err != nil {
return fmt.Errorf("Error deleting IAM instance profile %s: %s", d.Id(), err)
if isAWSErr(err, iam.ErrCodeNoSuchEntityException, "") {
return nil
}
return fmt.Errorf("deleting IAM instance profile %s: %w", d.Id(), err)
}

return nil
Expand Down
57 changes: 52 additions & 5 deletions aws/resource_aws_iam_instance_profile_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,9 @@ func TestAccAWSIAMInstanceProfile_basic(t *testing.T) {
Config: testAccAwsIamInstanceProfileConfig(rName),
Check: resource.ComposeTestCheckFunc(
testAccCheckAWSInstanceProfileExists(resourceName, &conf),
testAccCheckResourceAttrGlobalARN(resourceName, "arn", "iam", fmt.Sprintf("instance-profile/test-%s", rName)),
resource.TestCheckResourceAttrPair(resourceName, "role", "aws_iam_role.test", "name"),
resource.TestCheckResourceAttr(resourceName, "name", fmt.Sprintf("test-%s", rName)),
),
},
{
Expand Down Expand Up @@ -92,6 +95,50 @@ func TestAccAWSIAMInstanceProfile_namePrefix(t *testing.T) {
})
}

func TestAccAWSIAMInstanceProfile_disappears(t *testing.T) {
var conf iam.GetInstanceProfileOutput
resourceName := "aws_iam_instance_profile.test"
rName := acctest.RandString(5)

resource.ParallelTest(t, resource.TestCase{
PreCheck: func() { testAccPreCheck(t) },
Providers: testAccProviders,
CheckDestroy: testAccCheckAWSInstanceProfileDestroy,
Steps: []resource.TestStep{
{
Config: testAccAwsIamInstanceProfileConfig(rName),
Check: resource.ComposeTestCheckFunc(
testAccCheckAWSInstanceProfileExists(resourceName, &conf),
testAccCheckResourceDisappears(testAccProvider, resourceAwsIamInstanceProfile(), resourceName),
),
ExpectNonEmptyPlan: true,
},
},
})
}

func TestAccAWSIAMInstanceProfile_disappears_role(t *testing.T) {
var conf iam.GetInstanceProfileOutput
resourceName := "aws_iam_instance_profile.test"
rName := acctest.RandString(5)

resource.ParallelTest(t, resource.TestCase{
PreCheck: func() { testAccPreCheck(t) },
Providers: testAccProviders,
CheckDestroy: testAccCheckAWSInstanceProfileDestroy,
Steps: []resource.TestStep{
{
Config: testAccAwsIamInstanceProfileConfig(rName),
Check: resource.ComposeTestCheckFunc(
testAccCheckAWSInstanceProfileExists(resourceName, &conf),
testAccCheckResourceDisappears(testAccProvider, resourceAwsIamRole(), "aws_iam_role.test"),
),
ExpectNonEmptyPlan: true,
},
},
})
}

func testAccCheckAWSInstanceProfileGeneratedNamePrefix(resource, prefix string) resource.TestCheckFunc {
return func(s *terraform.State) error {
r, ok := s.RootModule().Resources[resource]
Expand All @@ -110,22 +157,22 @@ func testAccCheckAWSInstanceProfileGeneratedNamePrefix(resource, prefix string)
}

func testAccCheckAWSInstanceProfileDestroy(s *terraform.State) error {
iamconn := testAccProvider.Meta().(*AWSClient).iamconn
conn := testAccProvider.Meta().(*AWSClient).iamconn

for _, rs := range s.RootModule().Resources {
if rs.Type != "aws_iam_instance_profile" {
continue
}

// Try to get role
_, err := iamconn.GetInstanceProfile(&iam.GetInstanceProfileInput{
_, err := conn.GetInstanceProfile(&iam.GetInstanceProfileInput{
InstanceProfileName: aws.String(rs.Primary.ID),
})
if err == nil {
return fmt.Errorf("still exist.")
}

if isAWSErr(err, "NoSuchEntity", "") {
if isAWSErr(err, iam.ErrCodeNoSuchEntityException, "") {
continue
}

Expand All @@ -146,9 +193,9 @@ func testAccCheckAWSInstanceProfileExists(n string, res *iam.GetInstanceProfileO
return fmt.Errorf("No Instance Profile name is set")
}

iamconn := testAccProvider.Meta().(*AWSClient).iamconn
conn := testAccProvider.Meta().(*AWSClient).iamconn

resp, err := iamconn.GetInstanceProfile(&iam.GetInstanceProfileInput{
resp, err := conn.GetInstanceProfile(&iam.GetInstanceProfileInput{
InstanceProfileName: aws.String(rs.Primary.ID),
})
if err != nil {
Expand Down
21 changes: 9 additions & 12 deletions website/docs/r/iam_instance_profile.html.markdown
Original file line number Diff line number Diff line change
Expand Up @@ -42,24 +42,21 @@ EOF

## Argument Reference

The following arguments are supported:
The following arguments are optional:

* `name` - (Optional, Forces new resource) The profile's name. If omitted, Terraform will assign a random, unique name.
* `name` - (Optional, Forces new resource) Name of the instance profile. If omitted, Terraform will assign a random, unique name. Conflicts with `name_prefix`. Can be a string of characters consisting of upper and lowercase alphanumeric characters and these special characters: `_`, `+`, `=`, `,`, `.`, `@`, `-`. Spaces are not allowed.
* `name_prefix` - (Optional, Forces new resource) Creates a unique name beginning with the specified prefix. Conflicts with `name`.
* `path` - (Optional, default "/") Path in which to create the profile.
* `role` - (Optional) The role name to include in the profile.
* `path` - (Optional, default "/") Path to the instance profile. For more information about paths, see [IAM Identifiers](https://docs.aws.amazon.com/IAM/latest/UserGuide/Using_Identifiers.html) in the IAM User Guide. Can be a string of characters consisting of either a forward slash (`/`) by itself or a string that must begin and end with forward slashes. Can include any ASCII character from the ! (\u0021) through the DEL character (\u007F), including most punctuation characters, digits, and upper and lowercase letters.
* `role` - (Optional) Name of the role to add to the profile.

## Attributes Reference

In addition to all arguments above, the following attributes are exported:
In addition to the arguments above, the following attributes are exported:

* `id` - The instance profile's ID.
* `arn` - The ARN assigned by AWS to the instance profile.
* `create_date` - The creation timestamp of the instance profile.
* `name` - The instance profile's name.
* `path` - The path of the instance profile in IAM.
* `role` - The role assigned to the instance profile.
* `unique_id` - The [unique ID][1] assigned by AWS.
* `arn` - ARN assigned by AWS to the instance profile.
* `create_date` - Creation timestamp of the instance profile.
* `id` - Instance profile's ID.
* `unique_id` - [Unique ID][1] assigned by AWS.

[1]: https://docs.aws.amazon.com/IAM/latest/UserGuide/Using_Identifiers.html#GUIDs

Expand Down