diff --git a/.changelog/16188.txt b/.changelog/16188.txt new file mode 100644 index 00000000000..23f2b79e8b0 --- /dev/null +++ b/.changelog/16188.txt @@ -0,0 +1,3 @@ +```release-note:bug +resource/aws_iam_instance_profile: Detach role when role doesn't exist + remove when deleted from state. +``` diff --git a/aws/resource_aws_iam_instance_profile.go b/aws/resource_aws_iam_instance_profile.go index ca4a422181e..33e15b2a8bc 100644 --- a/aws/resource_aws_iam_instance_profile.go +++ b/aws/resource_aws_iam_instance_profile.go @@ -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, @@ -50,7 +43,6 @@ func resourceAwsIamInstanceProfile() *schema.Resource { validation.StringMatch(regexp.MustCompile(`^[\w+=,.@-]*$`), "must match [\\w+=,.@-]"), ), }, - "name_prefix": { Type: schema.TypeString, Optional: true, @@ -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 { @@ -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{ @@ -109,15 +103,15 @@ 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), @@ -125,11 +119,11 @@ func instanceProfileAddRole(iamconn *iam.IAM, profileName, roleName string) erro 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 { @@ -138,33 +132,33 @@ 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) } } @@ -172,22 +166,22 @@ func instanceProfileRemoveAllRoles(d *schema.ResourceData, iamconn *iam.IAM) err } 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) } } } @@ -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 diff --git a/aws/resource_aws_iam_instance_profile_test.go b/aws/resource_aws_iam_instance_profile_test.go index 2ec2fc4be5e..0bc63b918c5 100644 --- a/aws/resource_aws_iam_instance_profile_test.go +++ b/aws/resource_aws_iam_instance_profile_test.go @@ -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)), ), }, { @@ -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] @@ -110,7 +157,7 @@ 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" { @@ -118,14 +165,14 @@ func testAccCheckAWSInstanceProfileDestroy(s *terraform.State) error { } // 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 } @@ -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 { diff --git a/website/docs/r/iam_instance_profile.html.markdown b/website/docs/r/iam_instance_profile.html.markdown index b1fc14ef3cf..5a97590858b 100644 --- a/website/docs/r/iam_instance_profile.html.markdown +++ b/website/docs/r/iam_instance_profile.html.markdown @@ -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