diff --git a/.changelog/5904.txt b/.changelog/5904.txt new file mode 100644 index 00000000000..cc516468999 --- /dev/null +++ b/.changelog/5904.txt @@ -0,0 +1,3 @@ +```release-note:enhancement +resource/aws_iam_role: Add support for exclusive policy management `inline_policy` and `managed_policy_arns` arguments +``` \ No newline at end of file diff --git a/aws/resource_aws_iam_role.go b/aws/resource_aws_iam_role.go index 2c25f6a8357..b2451459085 100644 --- a/aws/resource_aws_iam_role.go +++ b/aws/resource_aws_iam_role.go @@ -10,6 +10,7 @@ import ( "github.com/aws/aws-sdk-go/aws" "github.com/aws/aws-sdk-go/service/iam" "github.com/hashicorp/aws-sdk-go-base/tfawserr" + "github.com/hashicorp/go-multierror" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/validation" @@ -26,7 +27,6 @@ func resourceAwsIamRole() *schema.Resource { Importer: &schema.ResourceImporter{ State: resourceAwsIamRoleImport, }, - Schema: map[string]*schema.Schema{ "arn": { Type: schema.TypeString, @@ -110,6 +110,38 @@ func resourceAwsIamRole() *schema.Resource { }, "tags": tagsSchema(), + + "inline_policy": { + Type: schema.TypeSet, + Optional: true, + Computed: true, + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "name": { + Type: schema.TypeString, + Optional: true, + ValidateFunc: validation.All( + validation.StringIsNotEmpty, + validateIamRolePolicyName, + ), + }, + "policy": { + Type: schema.TypeString, + Optional: true, + ValidateFunc: validateIAMPolicyJson, + DiffSuppressFunc: suppressEquivalentAwsPolicyDiffs, + }, + }, + }, + }, + + "managed_policy_arns": { + Type: schema.TypeSet, + Optional: true, + Computed: true, + Elem: &schema.Schema{Type: schema.TypeString}, + Set: schema.HashString, + }, }, } } @@ -174,7 +206,24 @@ func resourceAwsIamRoleCreate(d *schema.ResourceData, meta interface{}) error { if err != nil { return fmt.Errorf("Error creating IAM Role %s: %s", name, err) } - d.SetId(aws.StringValue(createResp.Role.RoleName)) + + roleName := aws.StringValue(createResp.Role.RoleName) + + if v, ok := d.GetOk("inline_policy"); ok && v.(*schema.Set).Len() > 0 { + policies := expandIamInlinePolicies(roleName, v.(*schema.Set).List()) + if err := addIamInlinePolicies(policies, meta); err != nil { + return err + } + } + + if v, ok := d.GetOk("managed_policy_arns"); ok && v.(*schema.Set).Len() > 0 { + managedPolicies := expandStringSet(v.(*schema.Set)) + if err := addIamManagedPolicies(roleName, managedPolicies, meta); err != nil { + return err + } + } + + d.SetId(roleName) return resourceAwsIamRoleRead(d, meta) } @@ -221,13 +270,28 @@ func resourceAwsIamRoleRead(d *schema.ResourceData, meta interface{}) error { return fmt.Errorf("error setting tags: %s", err) } - assumRolePolicy, err := url.QueryUnescape(*role.AssumeRolePolicyDocument) + assumeRolePolicy, err := url.QueryUnescape(*role.AssumeRolePolicyDocument) if err != nil { return err } - if err := d.Set("assume_role_policy", assumRolePolicy); err != nil { + if err := d.Set("assume_role_policy", assumeRolePolicy); err != nil { return err } + + inlinePolicies, err := readIamInlinePolicies(aws.StringValue(role.RoleName), meta) + if err != nil { + return fmt.Errorf("reading inline policies for IAM role %s, error: %s", d.Id(), err) + } + if err := d.Set("inline_policy", flattenIamInlinePolicies(inlinePolicies)); err != nil { + return fmt.Errorf("error setting inline_policy: %w", err) + } + + managedPolicies, err := readIamRolePolicyAttachments(iamconn, aws.StringValue(role.RoleName)) + if err != nil { + return fmt.Errorf("reading managed policies for IAM role %s, error: %s", d.Id(), err) + } + d.Set("managed_policy_arns", managedPolicies) + return nil } @@ -309,13 +373,85 @@ func resourceAwsIamRoleUpdate(d *schema.ResourceData, meta interface{}) error { } } + if d.HasChange("inline_policy") { + roleName := d.Get("name").(string) + o, n := d.GetChange("inline_policy") + if o == nil { + o = new(schema.Set) + } + if n == nil { + n = new(schema.Set) + } + + os := o.(*schema.Set) + ns := n.(*schema.Set) + remove := os.Difference(ns).List() + add := ns.Difference(os).List() + + var policyNames []*string + for _, policy := range remove { + tfMap, ok := policy.(map[string]interface{}) + + if !ok { + continue + } + + if v, ok := tfMap["name"].(string); ok && v != "" { + policyNames = append(policyNames, aws.String(tfMap["name"].(string))) + } + } + if err := deleteIamRolePolicies(iamconn, roleName, policyNames); err != nil { + return fmt.Errorf("unable to delete inline policies: %w", err) + } + + policies := expandIamInlinePolicies(roleName, add) + if err := addIamInlinePolicies(policies, meta); err != nil { + return err + } + } + + if d.HasChange("managed_policy_arns") { + roleName := d.Get("name").(string) + + o, n := d.GetChange("managed_policy_arns") + if o == nil { + o = new(schema.Set) + } + if n == nil { + n = new(schema.Set) + } + + os := o.(*schema.Set) + ns := n.(*schema.Set) + remove := expandStringSet(os.Difference(ns)) + add := expandStringSet(ns.Difference(os)) + + if err := deleteIamRolePolicyAttachments(iamconn, roleName, remove); err != nil { + return fmt.Errorf("unable to detach policies: %w", err) + } + + if err := addIamManagedPolicies(roleName, add, meta); err != nil { + return err + } + } + return resourceAwsIamRoleRead(d, meta) } func resourceAwsIamRoleDelete(d *schema.ResourceData, meta interface{}) error { conn := meta.(*AWSClient).iamconn - err := deleteAwsIamRole(conn, d.Id(), d.Get("force_detach_policies").(bool)) + hasInline := false + if v, ok := d.GetOk("inline_policy"); ok && v.(*schema.Set).Len() > 0 { + hasInline = true + } + + hasManaged := false + if v, ok := d.GetOk("managed_policy_arns"); ok && v.(*schema.Set).Len() > 0 { + hasManaged = true + } + + err := deleteIamRole(conn, d.Id(), d.Get("force_detach_policies").(bool), hasInline, hasManaged) if tfawserr.ErrCodeEquals(err, iam.ErrCodeNoSuchEntityException) { return nil } @@ -326,23 +462,35 @@ func resourceAwsIamRoleDelete(d *schema.ResourceData, meta interface{}) error { return nil } -func deleteAwsIamRole(conn *iam.IAM, rolename string, forceDetach bool) error { - if err := deleteAwsIamRoleInstanceProfiles(conn, rolename); err != nil { +func deleteIamRole(conn *iam.IAM, roleName string, forceDetach, hasInline, hasManaged bool) error { + if err := deleteIamRoleInstanceProfiles(conn, roleName); err != nil { return fmt.Errorf("unable to detach instance profiles: %w", err) } - if forceDetach { - if err := deleteAwsIamRolePolicyAttachments(conn, rolename); err != nil { + if forceDetach || hasManaged { + managedPolicies, err := readIamRolePolicyAttachments(conn, roleName) + if err != nil { + return err + } + + if err := deleteIamRolePolicyAttachments(conn, roleName, managedPolicies); err != nil { return fmt.Errorf("unable to detach policies: %w", err) } + } + + if forceDetach || hasInline { + inlinePolicies, err := readIamRolePolicyNames(conn, roleName) + if err != nil { + return err + } - if err := deleteAwsIamRolePolicies(conn, rolename); err != nil { + if err := deleteIamRolePolicies(conn, roleName, inlinePolicies); err != nil { return fmt.Errorf("unable to delete inline policies: %w", err) } } deleteRoleInput := &iam.DeleteRoleInput{ - RoleName: aws.String(rolename), + RoleName: aws.String(roleName), } err := resource.Retry(waiter.PropagationTimeout, func() *resource.RetryError { _, err := conn.DeleteRole(deleteRoleInput) @@ -362,9 +510,9 @@ func deleteAwsIamRole(conn *iam.IAM, rolename string, forceDetach bool) error { return err } -func deleteAwsIamRoleInstanceProfiles(conn *iam.IAM, rolename string) error { +func deleteIamRoleInstanceProfiles(conn *iam.IAM, roleName string) error { resp, err := conn.ListInstanceProfilesForRole(&iam.ListInstanceProfilesForRoleInput{ - RoleName: aws.String(rolename), + RoleName: aws.String(roleName), }) if tfawserr.ErrCodeEquals(err, iam.ErrCodeNoSuchEntityException) { return nil @@ -377,7 +525,7 @@ func deleteAwsIamRoleInstanceProfiles(conn *iam.IAM, rolename string) error { for _, i := range resp.InstanceProfiles { input := &iam.RemoveRoleFromInstanceProfileInput{ InstanceProfileName: i.InstanceProfileName, - RoleName: aws.String(rolename), + RoleName: aws.String(roleName), } _, err := conn.RemoveRoleFromInstanceProfile(input) @@ -392,10 +540,10 @@ func deleteAwsIamRoleInstanceProfiles(conn *iam.IAM, rolename string) error { return nil } -func deleteAwsIamRolePolicyAttachments(conn *iam.IAM, rolename string) error { +func readIamRolePolicyAttachments(conn *iam.IAM, roleName string) ([]*string, error) { managedPolicies := make([]*string, 0) input := &iam.ListAttachedRolePoliciesInput{ - RoleName: aws.String(rolename), + RoleName: aws.String(roleName), } err := conn.ListAttachedRolePoliciesPages(input, func(page *iam.ListAttachedRolePoliciesOutput, lastPage bool) bool { @@ -404,20 +552,21 @@ func deleteAwsIamRolePolicyAttachments(conn *iam.IAM, rolename string) error { } return !lastPage }) - if tfawserr.ErrCodeEquals(err, iam.ErrCodeNoSuchEntityException) { - return nil - } - if err != nil { - return err + if err != nil && !tfawserr.ErrCodeEquals(err, iam.ErrCodeNoSuchEntityException) { + return nil, err } + return managedPolicies, nil +} + +func deleteIamRolePolicyAttachments(conn *iam.IAM, roleName string, managedPolicies []*string) error { for _, parn := range managedPolicies { input := &iam.DetachRolePolicyInput{ PolicyArn: parn, - RoleName: aws.String(rolename), + RoleName: aws.String(roleName), } - _, err = conn.DetachRolePolicy(input) + _, err := conn.DetachRolePolicy(input) if tfawserr.ErrCodeEquals(err, iam.ErrCodeNoSuchEntityException) { continue } @@ -429,27 +578,33 @@ func deleteAwsIamRolePolicyAttachments(conn *iam.IAM, rolename string) error { return nil } -func deleteAwsIamRolePolicies(conn *iam.IAM, rolename string) error { +func readIamRolePolicyNames(conn *iam.IAM, roleName string) ([]*string, error) { inlinePolicies := make([]*string, 0) input := &iam.ListRolePoliciesInput{ - RoleName: aws.String(rolename), + RoleName: aws.String(roleName), } err := conn.ListRolePoliciesPages(input, func(page *iam.ListRolePoliciesOutput, lastPage bool) bool { inlinePolicies = append(inlinePolicies, page.PolicyNames...) return !lastPage }) - if tfawserr.ErrCodeEquals(err, iam.ErrCodeNoSuchEntityException) { - return nil - } - if err != nil { - return err + + if err != nil && !tfawserr.ErrCodeEquals(err, iam.ErrCodeNoSuchEntityException) { + return nil, err } - for _, pname := range inlinePolicies { + return inlinePolicies, nil +} + +func deleteIamRolePolicies(conn *iam.IAM, roleName string, policyNames []*string) error { + for _, name := range policyNames { + if len(aws.StringValue(name)) == 0 { + continue + } + input := &iam.DeleteRolePolicyInput{ - PolicyName: pname, - RoleName: aws.String(rolename), + PolicyName: name, + RoleName: aws.String(roleName), } _, err := conn.DeleteRolePolicy(input) @@ -463,3 +618,156 @@ func deleteAwsIamRolePolicies(conn *iam.IAM, rolename string) error { return nil } + +func flattenIamInlinePolicy(apiObject *iam.PutRolePolicyInput) map[string]interface{} { + if apiObject == nil { + return nil + } + + tfMap := map[string]interface{}{} + + tfMap["name"] = aws.StringValue(apiObject.PolicyName) + tfMap["policy"] = aws.StringValue(apiObject.PolicyDocument) + + return tfMap +} + +func flattenIamInlinePolicies(apiObjects []*iam.PutRolePolicyInput) []interface{} { + if len(apiObjects) == 0 { + return nil + } + + var tfList []interface{} + + for _, apiObject := range apiObjects { + if apiObject == nil { + continue + } + + tfList = append(tfList, flattenIamInlinePolicy(apiObject)) + } + + return tfList +} + +func expandIamInlinePolicy(roleName string, tfMap map[string]interface{}) *iam.PutRolePolicyInput { + if tfMap == nil { + return nil + } + + apiObject := &iam.PutRolePolicyInput{ + RoleName: aws.String(roleName), + } + + if v, ok := tfMap["name"].(string); ok && v != "" { + apiObject.PolicyName = aws.String(v) + } + + if v, ok := tfMap["policy"].(string); ok && v != "" { + apiObject.PolicyDocument = aws.String(v) + } + + return apiObject +} + +func expandIamInlinePolicies(roleName string, tfList []interface{}) []*iam.PutRolePolicyInput { + if len(tfList) == 0 { + return nil + } + + var apiObjects []*iam.PutRolePolicyInput + + for _, tfMapRaw := range tfList { + tfMap, ok := tfMapRaw.(map[string]interface{}) + + if !ok { + continue + } + + apiObject := expandIamInlinePolicy(roleName, tfMap) + + if apiObject == nil { + continue + } + + apiObjects = append(apiObjects, apiObject) + } + + return apiObjects +} + +func addIamInlinePolicies(policies []*iam.PutRolePolicyInput, meta interface{}) error { + conn := meta.(*AWSClient).iamconn + + var errs *multierror.Error + for _, policy := range policies { + if len(aws.StringValue(policy.PolicyName)) == 0 || len(aws.StringValue(policy.PolicyDocument)) == 0 { + continue + } + + if _, err := conn.PutRolePolicy(policy); err != nil { + newErr := fmt.Errorf("creating inline policy (%s): %w", aws.StringValue(policy.PolicyName), err) + log.Printf("[ERROR] %s", newErr) + errs = multierror.Append(errs, newErr) + } + } + + return errs.ErrorOrNil() +} + +func addIamManagedPolicies(roleName string, policies []*string, meta interface{}) error { + conn := meta.(*AWSClient).iamconn + + var errs *multierror.Error + for _, arn := range policies { + if err := attachPolicyToRole(conn, roleName, aws.StringValue(arn)); err != nil { + newErr := fmt.Errorf("attaching managed policy (%s): %w", aws.StringValue(arn), err) + log.Printf("[ERROR] %s", newErr) + errs = multierror.Append(errs, newErr) + } + } + + return errs.ErrorOrNil() +} + +func readIamInlinePolicies(roleName string, meta interface{}) ([]*iam.PutRolePolicyInput, error) { + conn := meta.(*AWSClient).iamconn + + policyNames, err := readIamRolePolicyNames(conn, roleName) + if err != nil { + return nil, err + } + + var apiObjects []*iam.PutRolePolicyInput + for _, policyName := range policyNames { + policyResp, err := conn.GetRolePolicy(&iam.GetRolePolicyInput{ + RoleName: aws.String(roleName), + PolicyName: policyName, + }) + if err != nil { + return nil, err + } + + policy, err := url.QueryUnescape(*policyResp.PolicyDocument) + if err != nil { + return nil, err + } + + apiObject := &iam.PutRolePolicyInput{ + RoleName: aws.String(roleName), + PolicyDocument: aws.String(policy), + PolicyName: policyName, + } + + apiObjects = append(apiObjects, apiObject) + } + + if len(apiObjects) == 0 { + apiObjects = append(apiObjects, &iam.PutRolePolicyInput{ + PolicyDocument: aws.String(""), + PolicyName: aws.String(""), + }) + } + + return apiObjects, nil +} diff --git a/aws/resource_aws_iam_role_test.go b/aws/resource_aws_iam_role_test.go index 8a4be8ec92f..3549c937777 100644 --- a/aws/resource_aws_iam_role_test.go +++ b/aws/resource_aws_iam_role_test.go @@ -120,19 +120,19 @@ func testSweepIamRoles(region string) error { var sweeperErrs *multierror.Error for _, role := range roles { - rolename := aws.StringValue(role.RoleName) - log.Printf("[DEBUG] Deleting IAM Role (%s)", rolename) + roleName := aws.StringValue(role.RoleName) + log.Printf("[DEBUG] Deleting IAM Role (%s)", roleName) - err := deleteAwsIamRole(conn, rolename, true) + err := deleteIamRole(conn, roleName, true, true, true) if tfawserr.ErrCodeEquals(err, iam.ErrCodeNoSuchEntityException) { continue } if testSweepSkipResourceError(err) { - log.Printf("[WARN] Skipping IAM Role (%s): %s", rolename, err) + log.Printf("[WARN] Skipping IAM Role (%s): %s", roleName, err) continue } if err != nil { - sweeperErr := fmt.Errorf("error deleting IAM Role (%s): %w", rolename, err) + sweeperErr := fmt.Errorf("error deleting IAM Role (%s): %w", roleName, err) log.Printf("[ERROR] %s", sweeperErr) sweeperErrs = multierror.Append(sweeperErrs, sweeperErr) continue @@ -419,10 +419,12 @@ func TestAccAWSIAMRole_PermissionsBoundary(t *testing.T) { }, // Test import { - ResourceName: resourceName, - ImportState: true, - ImportStateVerify: true, - ImportStateVerifyIgnore: []string{"force_destroy"}, + ResourceName: resourceName, + ImportState: true, + ImportStateVerify: true, + ImportStateVerifyIgnore: []string{ + "force_destroy", + }, }, // Test removal { @@ -491,109 +493,484 @@ func TestAccAWSIAMRole_tags(t *testing.T) { }) } -func testAccCheckAWSRoleDestroy(s *terraform.State) error { - iamconn := testAccProvider.Meta().(*AWSClient).iamconn - - for _, rs := range s.RootModule().Resources { - if rs.Type != "aws_iam_role" { - continue - } - - // Try to get role - _, err := iamconn.GetRole(&iam.GetRoleInput{ - RoleName: aws.String(rs.Primary.ID), - }) - if err == nil { - return fmt.Errorf("still exist.") - } - - // Verify the error is what we want - ec2err, ok := err.(awserr.Error) - if !ok { - return err - } - if ec2err.Code() != "NoSuchEntity" { - return err - } - } +func TestAccAWSIAMRole_policyBasicInline(t *testing.T) { + var role iam.GetRoleOutput + rName := acctest.RandomWithPrefix("tf-acc-test") + policyName1 := acctest.RandomWithPrefix("tf-acc-test") + policyName2 := acctest.RandomWithPrefix("tf-acc-test") + policyName3 := acctest.RandomWithPrefix("tf-acc-test") + resourceName := "aws_iam_role.test" - return nil + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + Providers: testAccProviders, + CheckDestroy: testAccCheckAWSRoleDestroy, + Steps: []resource.TestStep{ + { + Config: testAccAWSRolePolicyInlineConfig(rName, policyName1), + Check: resource.ComposeTestCheckFunc( + testAccCheckAWSRoleExists(resourceName, &role), + resource.TestCheckResourceAttr(resourceName, "inline_policy.#", "1"), + resource.TestCheckResourceAttr(resourceName, "name", rName), + resource.TestCheckResourceAttr(resourceName, "managed_policy_arns.#", "0"), + ), + }, + { + Config: testAccAWSRolePolicyInlineConfigUpdate(rName, policyName2, policyName3), + Check: resource.ComposeTestCheckFunc( + testAccCheckAWSRoleExists(resourceName, &role), + resource.TestCheckResourceAttr(resourceName, "inline_policy.#", "2"), + resource.TestCheckResourceAttr(resourceName, "managed_policy_arns.#", "0"), + ), + }, + { + Config: testAccAWSRolePolicyInlineConfigUpdateDown(rName, policyName3), + Check: resource.ComposeTestCheckFunc( + testAccCheckAWSRoleExists(resourceName, &role), + resource.TestCheckResourceAttr(resourceName, "inline_policy.#", "1"), + resource.TestCheckResourceAttr(resourceName, "managed_policy_arns.#", "0"), + ), + }, + { + ResourceName: resourceName, + ImportState: true, + ImportStateVerify: true, + }, + }, + }) } -func testAccCheckAWSRoleExists(n string, res *iam.GetRoleOutput) resource.TestCheckFunc { - return func(s *terraform.State) error { - rs, ok := s.RootModule().Resources[n] - if !ok { - return fmt.Errorf("Not found: %s", n) - } +func TestAccAWSIAMRole_policyBasicInlineEmpty(t *testing.T) { + var role iam.GetRoleOutput + rName := acctest.RandomWithPrefix("tf-acc-test") + resourceName := "aws_iam_role.test" - if rs.Primary.ID == "" { - return fmt.Errorf("No Role name is set") - } + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + Providers: testAccProviders, + CheckDestroy: testAccCheckAWSRoleDestroy, + Steps: []resource.TestStep{ + { + Config: testAccAWSRolePolicyEmptyInlineConfig(rName), + Check: resource.ComposeTestCheckFunc( + testAccCheckAWSRoleExists(resourceName, &role), + ), + }, + }, + }) +} - iamconn := testAccProvider.Meta().(*AWSClient).iamconn +func TestAccAWSIAMRole_policyBasicManaged(t *testing.T) { + var role iam.GetRoleOutput + rName := acctest.RandomWithPrefix("tf-acc-test") + policyName1 := acctest.RandomWithPrefix("tf-acc-test") + policyName2 := acctest.RandomWithPrefix("tf-acc-test") + policyName3 := acctest.RandomWithPrefix("tf-acc-test") + resourceName := "aws_iam_role.test" - resp, err := iamconn.GetRole(&iam.GetRoleInput{ - RoleName: aws.String(rs.Primary.ID), - }) - if err != nil { - return err - } + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + Providers: testAccProviders, + CheckDestroy: testAccCheckAWSRoleDestroy, + Steps: []resource.TestStep{ + { + Config: testAccAWSRolePolicyManagedConfig(rName, policyName1), + Check: resource.ComposeTestCheckFunc( + testAccCheckAWSRoleExists(resourceName, &role), + resource.TestCheckResourceAttr(resourceName, "name", rName), + resource.TestCheckResourceAttr(resourceName, "managed_policy_arns.#", "1"), + ), + }, + { + Config: testAccAWSRolePolicyManagedConfigUpdate(rName, policyName1, policyName2, policyName3), + Check: resource.ComposeTestCheckFunc( + testAccCheckAWSRoleExists(resourceName, &role), + resource.TestCheckResourceAttr(resourceName, "managed_policy_arns.#", "2"), + ), + }, + { + Config: testAccAWSRolePolicyManagedConfigUpdateDown(rName, policyName1, policyName2, policyName3), + Check: resource.ComposeTestCheckFunc( + testAccCheckAWSRoleExists(resourceName, &role), + resource.TestCheckResourceAttr(resourceName, "managed_policy_arns.#", "1"), + ), + }, + { + ResourceName: resourceName, + ImportState: true, + ImportStateVerify: true, + }, + }, + }) +} - *res = *resp +// TestAccAWSIAMRole_policyOutOfBandRemovalAddedBack_managedNonEmpty: if a policy is detached +// out of band, it should be reattached. +func TestAccAWSIAMRole_policyOutOfBandRemovalAddedBack_managedNonEmpty(t *testing.T) { + var role iam.GetRoleOutput + rName := acctest.RandomWithPrefix("tf-acc-test") + policyName := acctest.RandomWithPrefix("tf-acc-test") + resourceName := "aws_iam_role.test" - return nil - } + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + Providers: testAccProviders, + CheckDestroy: testAccCheckAWSRoleDestroy, + Steps: []resource.TestStep{ + { + Config: testAccAWSRolePolicyManagedConfig(rName, policyName), + Check: resource.ComposeTestCheckFunc( + testAccCheckAWSRoleExists(resourceName, &role), + testAccCheckAWSRolePolicyDetachManagedPolicy(&role, policyName), + ), + ExpectNonEmptyPlan: true, + }, + { + Config: testAccAWSRolePolicyManagedConfig(rName, policyName), + Check: resource.ComposeTestCheckFunc( + testAccCheckAWSRoleExists(resourceName, &role), + resource.TestCheckResourceAttr(resourceName, "managed_policy_arns.#", "1"), + ), + }, + }, + }) } -func testAccCheckAWSRoleDisappears(getRoleOutput *iam.GetRoleOutput) resource.TestCheckFunc { - return func(s *terraform.State) error { - iamconn := testAccProvider.Meta().(*AWSClient).iamconn +// TestAccAWSIAMRole_policyOutOfBandRemovalAddedBack_inlineNonEmpty: if a policy is removed +// out of band, it should be recreated. +func TestAccAWSIAMRole_policyOutOfBandRemovalAddedBack_inlineNonEmpty(t *testing.T) { + var role iam.GetRoleOutput + rName := acctest.RandomWithPrefix("tf-acc-test") + policyName := acctest.RandomWithPrefix("tf-acc-test") + resourceName := "aws_iam_role.test" - roleName := aws.StringValue(getRoleOutput.Role.RoleName) + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + Providers: testAccProviders, + CheckDestroy: testAccCheckAWSRoleDestroy, + Steps: []resource.TestStep{ + { + Config: testAccAWSRolePolicyInlineConfig(rName, policyName), + Check: resource.ComposeTestCheckFunc( + testAccCheckAWSRoleExists(resourceName, &role), + testAccCheckAWSRolePolicyRemoveInlinePolicy(&role, policyName), + ), + ExpectNonEmptyPlan: true, + }, + { + Config: testAccAWSRolePolicyInlineConfig(rName, policyName), + Check: resource.ComposeTestCheckFunc( + testAccCheckAWSRoleExists(resourceName, &role), + resource.TestCheckResourceAttr(resourceName, "inline_policy.#", "1"), + ), + }, + }, + }) +} - _, err := iamconn.DeleteRole(&iam.DeleteRoleInput{ - RoleName: aws.String(roleName), - }) - if err != nil { - return fmt.Errorf("error deleting role %q: %s", roleName, err) - } +// TestAccAWSIAMRole_policyOutOfBandAdditionRemoved_managedNonEmpty: if managed_policies arg +// exists and is non-empty, policy attached out of band should be removed +func TestAccAWSIAMRole_policyOutOfBandAdditionRemoved_managedNonEmpty(t *testing.T) { + var role iam.GetRoleOutput + rName := acctest.RandomWithPrefix("tf-acc-test") + policyName1 := acctest.RandomWithPrefix("tf-acc-test") + policyName2 := acctest.RandomWithPrefix("tf-acc-test") + resourceName := "aws_iam_role.test" - return nil - } + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + Providers: testAccProviders, + CheckDestroy: testAccCheckAWSRoleDestroy, + Steps: []resource.TestStep{ + { + Config: testAccAWSRolePolicyExtraManagedConfig(rName, policyName1, policyName2), + Check: resource.ComposeTestCheckFunc( + testAccCheckAWSRoleExists(resourceName, &role), + testAccCheckAWSRolePolicyAttachManagedPolicy(&role, policyName2), + ), + ExpectNonEmptyPlan: true, + }, + { + Config: testAccAWSRolePolicyExtraManagedConfig(rName, policyName1, policyName2), + Check: resource.ComposeTestCheckFunc( + testAccCheckAWSRoleExists(resourceName, &role), + resource.TestCheckResourceAttr(resourceName, "managed_policy_arns.#", "1"), + ), + }, + }, + }) } -func testAccCheckAWSRoleGeneratedNamePrefix(resource, prefix string) resource.TestCheckFunc { - return func(s *terraform.State) error { - r, ok := s.RootModule().Resources[resource] - if !ok { - return fmt.Errorf("Resource not found") - } - name, ok := r.Primary.Attributes["name"] - if !ok { - return fmt.Errorf("Name attr not found: %#v", r.Primary.Attributes) - } - if !strings.HasPrefix(name, prefix) { - return fmt.Errorf("Name: %q, does not have prefix: %q", name, prefix) - } - return nil - } -} +// TestAccAWSIAMRole_policyOutOfBandAdditionRemoved_inlineNonEmpty: if inline_policy arg +// exists and is non-empty, policy added out of band should be removed +func TestAccAWSIAMRole_policyOutOfBandAdditionRemoved_inlineNonEmpty(t *testing.T) { + var role iam.GetRoleOutput + rName := acctest.RandomWithPrefix("tf-acc-test") + policyName1 := acctest.RandomWithPrefix("tf-acc-test") + policyName2 := acctest.RandomWithPrefix("tf-acc-test") + resourceName := "aws_iam_role.test" -// Attach inline policy outside of terraform CRUD. -func testAccAddAwsIAMRolePolicy(n string) resource.TestCheckFunc { - return func(s *terraform.State) error { - rs, ok := s.RootModule().Resources[n] - if !ok { - return fmt.Errorf("Resource not found") - } - if rs.Primary.ID == "" { - return fmt.Errorf("No Role name is set") - } + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + Providers: testAccProviders, + CheckDestroy: testAccCheckAWSRoleDestroy, + Steps: []resource.TestStep{ + { + Config: testAccAWSRolePolicyInlineConfig(rName, policyName1), + Check: resource.ComposeTestCheckFunc( + testAccCheckAWSRoleExists(resourceName, &role), + testAccCheckAWSRolePolicyAddInlinePolicy(&role, policyName2), + ), + ExpectNonEmptyPlan: true, + }, + { + Config: testAccAWSRolePolicyInlineConfig(rName, policyName1), + Check: resource.ComposeTestCheckFunc( + testAccCheckAWSRoleExists(resourceName, &role), + resource.TestCheckResourceAttr(resourceName, "inline_policy.#", "1"), + resource.TestCheckResourceAttr(resourceName, "managed_policy_arns.#", "0"), + ), + }, + }, + }) +} - iamconn := testAccProvider.Meta().(*AWSClient).iamconn +// TestAccAWSIAMRole_policyOutOfBandAdditionIgnored_inlineNonExistent: if there is no +// inline_policy attribute, out of band changes should be ignored. +func TestAccAWSIAMRole_policyOutOfBandAdditionIgnored_inlineNonExistent(t *testing.T) { + var role iam.GetRoleOutput + rName := acctest.RandomWithPrefix("tf-acc-test") + policyName1 := acctest.RandomWithPrefix("tf-acc-test") + policyName2 := acctest.RandomWithPrefix("tf-acc-test") + resourceName := "aws_iam_role.test" - input := &iam.PutRolePolicyInput{ + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + Providers: testAccProviders, + CheckDestroy: testAccCheckAWSRoleDestroy, + Steps: []resource.TestStep{ + { + Config: testAccAWSRolePolicyNoInlineConfig(rName), + Check: resource.ComposeTestCheckFunc( + testAccCheckAWSRoleExists(resourceName, &role), + testAccCheckAWSRolePolicyAddInlinePolicy(&role, policyName1), + ), + }, + { + Config: testAccAWSRolePolicyNoInlineConfig(rName), + Check: resource.ComposeTestCheckFunc( + testAccCheckAWSRoleExists(resourceName, &role), + testAccCheckAWSRolePolicyAddInlinePolicy(&role, policyName2), + ), + }, + { + Config: testAccAWSRolePolicyNoInlineConfig(rName), + Check: resource.ComposeTestCheckFunc( + testAccCheckAWSRoleExists(resourceName, &role), + testAccCheckAWSRolePolicyRemoveInlinePolicy(&role, policyName1), + testAccCheckAWSRolePolicyRemoveInlinePolicy(&role, policyName2), + ), + }, + }, + }) +} + +// TestAccAWSIAMRole_policyOutOfBandAdditionIgnored_managedNonExistent: if there is no +// managed_policies attribute, out of band changes should be ignored. +func TestAccAWSIAMRole_policyOutOfBandAdditionIgnored_managedNonExistent(t *testing.T) { + var role iam.GetRoleOutput + rName := acctest.RandomWithPrefix("tf-acc-test") + policyName := acctest.RandomWithPrefix("tf-acc-test") + resourceName := "aws_iam_role.test" + + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + Providers: testAccProviders, + CheckDestroy: testAccCheckAWSRoleDestroy, + Steps: []resource.TestStep{ + { + Config: testAccAWSRolePolicyNoManagedConfig(rName, policyName), + Check: resource.ComposeTestCheckFunc( + testAccCheckAWSRoleExists(resourceName, &role), + testAccCheckAWSRolePolicyAttachManagedPolicy(&role, policyName), + ), + }, + { + Config: testAccAWSRolePolicyNoManagedConfig(rName, policyName), + Check: resource.ComposeTestCheckFunc( + testAccCheckAWSRoleExists(resourceName, &role), + testAccCheckAWSRolePolicyDetachManagedPolicy(&role, policyName), + ), + }, + }, + }) +} + +// TestAccAWSIAMRole_policyOutOfBandAdditionRemoved_inlineEmpty: if inline is added +// out of band with empty inline arg, should be removed +func TestAccAWSIAMRole_policyOutOfBandAdditionRemoved_inlineEmpty(t *testing.T) { + var role iam.GetRoleOutput + rName := acctest.RandomWithPrefix("tf-acc-test") + policyName := acctest.RandomWithPrefix("tf-acc-test") + resourceName := "aws_iam_role.test" + + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + Providers: testAccProviders, + CheckDestroy: testAccCheckAWSRoleDestroy, + Steps: []resource.TestStep{ + { + Config: testAccAWSRolePolicyEmptyInlineConfig(rName), + Check: resource.ComposeTestCheckFunc( + testAccCheckAWSRoleExists(resourceName, &role), + testAccCheckAWSRolePolicyAddInlinePolicy(&role, policyName), + ), + ExpectNonEmptyPlan: true, + }, + { + Config: testAccAWSRolePolicyEmptyInlineConfig(rName), + Check: resource.ComposeTestCheckFunc( + testAccCheckAWSRoleExists(resourceName, &role), + ), + }, + }, + }) +} + +// TestAccAWSIAMRole_policyOutOfBandAdditionRemoved_managedEmpty: if managed is attached +// out of band with empty managed arg, should be detached +func TestAccAWSIAMRole_policyOutOfBandAdditionRemoved_managedEmpty(t *testing.T) { + var role iam.GetRoleOutput + rName := acctest.RandomWithPrefix("tf-acc-test") + policyName := acctest.RandomWithPrefix("tf-acc-test") + resourceName := "aws_iam_role.test" + + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + Providers: testAccProviders, + CheckDestroy: testAccCheckAWSRoleDestroy, + Steps: []resource.TestStep{ + { + Config: testAccAWSRolePolicyEmptyManagedConfig(rName, policyName), + Check: resource.ComposeTestCheckFunc( + testAccCheckAWSRoleExists(resourceName, &role), + testAccCheckAWSRolePolicyAttachManagedPolicy(&role, policyName), + ), + ExpectNonEmptyPlan: true, + }, + { + Config: testAccAWSRolePolicyEmptyManagedConfig(rName, policyName), + Check: resource.ComposeTestCheckFunc( + testAccCheckAWSRoleExists(resourceName, &role), + ), + }, + }, + }) +} + +func testAccCheckAWSRoleDestroy(s *terraform.State) error { + iamconn := testAccProvider.Meta().(*AWSClient).iamconn + + for _, rs := range s.RootModule().Resources { + if rs.Type != "aws_iam_role" { + continue + } + + // Try to get role + _, err := iamconn.GetRole(&iam.GetRoleInput{ + RoleName: aws.String(rs.Primary.ID), + }) + if err == nil { + return fmt.Errorf("still exist.") + } + + // Verify the error is what we want + ec2err, ok := err.(awserr.Error) + if !ok { + return err + } + if ec2err.Code() != "NoSuchEntity" { + return err + } + } + + return nil +} + +func testAccCheckAWSRoleExists(n string, res *iam.GetRoleOutput) resource.TestCheckFunc { + return func(s *terraform.State) error { + rs, ok := s.RootModule().Resources[n] + if !ok { + return fmt.Errorf("Not found: %s", n) + } + + if rs.Primary.ID == "" { + return fmt.Errorf("No Role name is set") + } + + iamconn := testAccProvider.Meta().(*AWSClient).iamconn + + resp, err := iamconn.GetRole(&iam.GetRoleInput{ + RoleName: aws.String(rs.Primary.ID), + }) + if err != nil { + return err + } + + *res = *resp + + return nil + } +} + +func testAccCheckAWSRoleDisappears(getRoleOutput *iam.GetRoleOutput) resource.TestCheckFunc { + return func(s *terraform.State) error { + iamconn := testAccProvider.Meta().(*AWSClient).iamconn + + roleName := aws.StringValue(getRoleOutput.Role.RoleName) + + _, err := iamconn.DeleteRole(&iam.DeleteRoleInput{ + RoleName: aws.String(roleName), + }) + if err != nil { + return fmt.Errorf("error deleting role %q: %s", roleName, err) + } + + return nil + } +} + +func testAccCheckAWSRoleGeneratedNamePrefix(resource, prefix string) resource.TestCheckFunc { + return func(s *terraform.State) error { + r, ok := s.RootModule().Resources[resource] + if !ok { + return fmt.Errorf("Resource not found") + } + name, ok := r.Primary.Attributes["name"] + if !ok { + return fmt.Errorf("Name attr not found: %#v", r.Primary.Attributes) + } + if !strings.HasPrefix(name, prefix) { + return fmt.Errorf("Name: %q, does not have prefix: %q", name, prefix) + } + return nil + } +} + +// Attach inline policy out of band (outside of terraform) +func testAccAddAwsIAMRolePolicy(n string) resource.TestCheckFunc { + return func(s *terraform.State) error { + rs, ok := s.RootModule().Resources[n] + if !ok { + return fmt.Errorf("Resource not found") + } + if rs.Primary.ID == "" { + return fmt.Errorf("No Role name is set") + } + + iamconn := testAccProvider.Meta().(*AWSClient).iamconn + + input := &iam.PutRolePolicyInput{ RoleName: aws.String(rs.Primary.ID), PolicyDocument: aws.String(`{ "Version": "2012-10-17", @@ -627,185 +1004,922 @@ func testAccCheckAWSRolePermissionsBoundary(getRoleOutput *iam.GetRoleOutput, ex } } -func testAccCheckIAMRoleConfig_MaxSessionDuration(rName string, maxSessionDuration int) string { - return fmt.Sprintf(` -resource "aws_iam_role" "test" { - name = "test-role-%s" - path = "/" - max_session_duration = %d +func testAccCheckAWSRolePolicyDetachManagedPolicy(role *iam.GetRoleOutput, policyName string) resource.TestCheckFunc { + return func(s *terraform.State) error { + conn := testAccProvider.Meta().(*AWSClient).iamconn - assume_role_policy = < **NOTE:** The usage of this resource conflicts with the `aws_iam_group_policy_attachment`, `aws_iam_role_policy_attachment`, and `aws_iam_user_policy_attachment` resources and will permanently show a difference if both are defined. +~> **NOTE:** For a given role, this resource is incompatible with using the [`aws_iam_role` resource](/docs/providers/aws/r/iam_role.html) `managed_policy_arns` argument. When using that argument and this resource, both will attempt to manage the role's managed policy attachments and Terraform will show a permanent difference. + ## Example Usage ```hcl diff --git a/website/docs/r/iam_role.html.markdown b/website/docs/r/iam_role.html.markdown index dada55200f6..9ac2eea8719 100644 --- a/website/docs/r/iam_role.html.markdown +++ b/website/docs/r/iam_role.html.markdown @@ -10,10 +10,14 @@ description: |- Provides an IAM role. -~> *NOTE:* If policies are attached to the role via the [`aws_iam_policy_attachment` resource](/docs/providers/aws/r/iam_policy_attachment.html) and you are modifying the role `name` or `path`, the `force_detach_policies` argument must be set to `true` and applied before attempting the operation otherwise you will encounter a `DeleteConflict` error. The [`aws_iam_role_policy_attachment` resource (recommended)](/docs/providers/aws/r/iam_role_policy_attachment.html) does not have this requirement. +~> **NOTE:** If policies are attached to the role via the [`aws_iam_policy_attachment` resource](/docs/providers/aws/r/iam_policy_attachment.html) and you are modifying the role `name` or `path`, the `force_detach_policies` argument must be set to `true` and applied before attempting the operation otherwise you will encounter a `DeleteConflict` error. The [`aws_iam_role_policy_attachment` resource (recommended)](/docs/providers/aws/r/iam_role_policy_attachment.html) does not have this requirement. + +~> **NOTE:** If you use this resource's `managed_policy_arns` argument or `inline_policy` configuration blocks, this resource will take over exclusive management of the role's respective policy types (e.g., both policy types if both arguments are used). These arguments are incompatible with other ways of managing a role's policies, such as [`aws_iam_policy_attachment`](/docs/providers/aws/r/iam_policy_attachment.html), [`aws_iam_role_policy_attachment`](/docs/providers/aws/r/iam_role_policy_attachment.html), and [`aws_iam_role_policy`](/docs/providers/aws/r/iam_role_policy.html). If you attempt to manage a role's policies by multiple means, you will get resource cycling and/or errors. ## Example Usage +### Basic Example + ```hcl resource "aws_iam_role" "test_role" { name = "test_role" @@ -40,37 +44,7 @@ resource "aws_iam_role" "test_role" { } ``` -## Argument Reference - -The following arguments are supported: - -* `name` - (Optional, Forces new resource) The name of the role. If omitted, Terraform will assign a random, unique name. -* `name_prefix` - (Optional, Forces new resource) Creates a unique name beginning with the specified prefix. Conflicts with `name`. -* `assume_role_policy` - (Required) The policy that grants an entity permission to assume the role. - -~> **NOTE:** This `assume_role_policy` is very similar but slightly different than just a standard IAM policy and cannot use an `aws_iam_policy` resource. It _can_ however, use an `aws_iam_policy_document` [data source](/docs/providers/aws/d/iam_policy_document.html), see example below for how this could work. - -* `force_detach_policies` - (Optional) Specifies to force detaching any policies the role has before destroying it. Defaults to `false`. -* `path` - (Optional) The path to the role. - See [IAM Identifiers](https://docs.aws.amazon.com/IAM/latest/UserGuide/Using_Identifiers.html) for more information. -* `description` - (Optional) The description of the role. - -* `max_session_duration` - (Optional) The maximum session duration (in seconds) that you want to set for the specified role. If you do not specify a value for this setting, the default maximum of one hour is applied. This setting can have a value from 1 hour to 12 hours. -* `permissions_boundary` - (Optional) The ARN of the policy that is used to set the permissions boundary for the role. -* `tags` - Key-value map of tags for the IAM role - -## Attributes Reference - -In addition to all arguments above, the following attributes are exported: - -* `arn` - The Amazon Resource Name (ARN) specifying the role. -* `create_date` - The creation date of the IAM role. -* `description` - The description of the role. -* `id` - The name of the role. -* `name` - The name of the role. -* `unique_id` - The stable and unique string identifying the role. - -## Example of Using Data Source for Assume Role Policy +### Example of Using Data Source for Assume Role Policy ```hcl data "aws_iam_policy_document" "instance-assume-role-policy" { @@ -91,6 +65,150 @@ resource "aws_iam_role" "instance" { } ``` +### Example of Exclusive Inline Policies + +This example creates an IAM role with two inline IAM policies. If someone adds another inline policy out-of-band, on the next apply, Terraform will remove that policy. If someone deletes these policies out-of-band, Terraform will recreate them. + +```hcl +resource "aws_iam_role" "example" { + name = "yak_role" + assume_role_policy = data.aws_iam_policy_document.instance_assume_role_policy.json # (not shown) + + inline_policy { + name = "my_inline_policy" + + policy = jsonencode({ + Version = "2012-10-17" + Statement = [ + { + Action = ["ec2:Describe*"] + Effect = "Allow" + Resource = "*" + }, + ] + }) + } + + inline_policy { + name = "policy-8675309" + policy = data.aws_iam_policy_document.inline_policy.json + } +} + +data "aws_iam_policy_document" "inline_policy" { + statement { + actions = ["ec2:DescribeAccountAttributes"] + resources = ["*"] + } +} +``` + +### Example of Removing Inline Policies + +This example creates an IAM role with what appears to be empty IAM `inline_policy` argument instead of using `inline_policy` as a configuration block. The result is that if someone were to add an inline policy out-of-band, on the next apply, Terraform will remove that policy. + + +```hcl +resource "aws_iam_role" "example" { + name = "yak_role" + assume_role_policy = data.aws_iam_policy_document.instance_assume_role_policy.json # (not shown) + + inline_policy {} +} +``` + +### Example of Exclusive Managed Policies + +This example creates an IAM role and attaches two managed IAM policies. If someone attaches another managed policy out-of-band, on the next apply, Terraform will detach that policy. If someone detaches these policies out-of-band, Terraform will attach them again. + +```hcl +resource "aws_iam_role" "example" { + name = "yak_role" + assume_role_policy = data.aws_iam_policy_document.instance_assume_role_policy.json # (not shown) + managed_policy_arns = [aws_iam_policy.policy_one.arn, aws_iam_policy.policy_two.arn] +} + +resource "aws_iam_policy" "policy_one" { + name = "policy-618033" + + policy = jsonencode({ + Version = "2012-10-17" + Statement = [ + { + Action = ["ec2:Describe*"] + Effect = "Allow" + Resource = "*" + }, + ] + }) +} + +resource "aws_iam_policy" "policy_two" { + name = "policy-381966" + + policy = jsonencode({ + Version = "2012-10-17" + Statement = [ + { + Action = ["s3:ListAllMyBuckets", "s3:ListBucket", "s3:HeadBucket"] + Effect = "Allow" + Resource = "*" + }, + ] + }) +} +``` + +### Example of Removing Managed Policies + +This example creates an IAM role with an empty `managed_policy_arns` argument. If someone attaches a policy out-of-band, on the next apply, Terraform will detach that policy. + +```hcl +resource "aws_iam_role" "example" { + name = "yak_role" + assume_role_policy = data.aws_iam_policy_document.instance_assume_role_policy.json # (not shown) + managed_policy_arns = [] +} +``` + +## Argument Reference + +The following argument is required: + +* `assume_role_policy` - (Required) Policy that grants an entity permission to assume the role. + +~> **NOTE:** The `assume_role_policy` is very similar to but slightly different than a standard IAM policy and cannot use an `aws_iam_policy` resource. However, it _can_ use an `aws_iam_policy_document` [data source](/docs/providers/aws/d/iam_policy_document.html). See the example above of how this works. + +The following arguments are optional: + +* `description` - (Optional) Description of the role. +* `force_detach_policies` - (Optional) Whether to force detaching any policies the role has before destroying it. Defaults to `false`. +* `inline_policy` - (Optional) Configuration block defining an exclusive set of IAM inline policies associated with the IAM role. Defined below. If no blocks are configured, Terraform will ignore any managing any inline policies in this resource. Configuring one empty block (i.e., `inline_policy {}`) will cause Terraform to remove _all_ inline policies. +* `managed_policy_arns` - (Optional) Set of exclusive IAM managed policy ARNs to attach to the IAM role. If this attribute is not configured, Terraform will ignore policy attachments to this resource. When configured, Terraform will align the role's managed policy attachments with this set by attaching or detaching managed policies. Configuring an empty set (i.e., `managed_policy_arns = []`) will cause Terraform to remove _all_ managed policy attachments. +* `max_session_duration` - (Optional) Maximum session duration (in seconds) that you want to set for the specified role. If you do not specify a value for this setting, the default maximum of one hour is applied. This setting can have a value from 1 hour to 12 hours. +* `name` - (Optional, Forces new resource) Friendly name of the role. If omitted, Terraform will assign a random, unique name. See [IAM Identifiers](https://docs.aws.amazon.com/IAM/latest/UserGuide/Using_Identifiers.html) for more information. +* `name_prefix` - (Optional, Forces new resource) Creates a unique friendly name beginning with the specified prefix. Conflicts with `name`. +* `path` - (Optional) Path to the role. See [IAM Identifiers](https://docs.aws.amazon.com/IAM/latest/UserGuide/Using_Identifiers.html) for more information. +* `permissions_boundary` - (Optional) ARN of the policy that is used to set the permissions boundary for the role. +* `tags` - Key-value mapping of tags for the IAM role + +### inline_policy + +This configuration block supports the following: + +* `name` - (Required) Name of the role policy. +* `policy` - (Required) Policy document as a JSON formatted string. For more information about building IAM policy documents with Terraform, see the [AWS IAM Policy Document Guide](https://www.terraform.io/docs/providers/aws/guides/iam-policy-documents.html). + +## Attributes Reference + +In addition to the arguments above, the following attributes are exported: + +* `arn` - Amazon Resource Name (ARN) specifying the role. +* `create_date` - Creation date of the IAM role. +* `id` - Name of the role. +* `name` - Name of the role. +* `unique_id` - Stable and unique string identifying the role. + ## Import IAM Roles can be imported using the `name`, e.g. diff --git a/website/docs/r/iam_role_policy.html.markdown b/website/docs/r/iam_role_policy.html.markdown index dc081aa826a..1bec734beba 100644 --- a/website/docs/r/iam_role_policy.html.markdown +++ b/website/docs/r/iam_role_policy.html.markdown @@ -10,6 +10,8 @@ description: |- Provides an IAM role inline policy. +~> **NOTE:** For a given role, this resource is incompatible with using the [`aws_iam_role` resource](/docs/providers/aws/r/iam_role.html) `inline_policy` argument. When using that argument and this resource, both will attempt to manage the role's inline policies and Terraform will show a permanent difference. + ## Example Usage ```hcl diff --git a/website/docs/r/iam_role_policy_attachment.markdown b/website/docs/r/iam_role_policy_attachment.markdown index 4c3cd7845de..b714008e1ac 100644 --- a/website/docs/r/iam_role_policy_attachment.markdown +++ b/website/docs/r/iam_role_policy_attachment.markdown @@ -12,6 +12,8 @@ Attaches a Managed IAM Policy to an IAM role ~> **NOTE:** The usage of this resource conflicts with the `aws_iam_policy_attachment` resource and will permanently show a difference if both are defined. +~> **NOTE:** For a given role, this resource is incompatible with using the [`aws_iam_role` resource](/docs/providers/aws/r/iam_role.html) `managed_policy_arns` argument. When using that argument and this resource, both will attempt to manage the role's managed policy attachments and Terraform will show a permanent difference. + ## Example Usage ```hcl