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

iam/role: Fix spurious diffs with inline_policy ordering #22099

Merged
merged 5 commits into from
Dec 8, 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/22099.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
```release-note:bug
resource/aws_iam_role: Fix order-related diffs in `policy`
```
75 changes: 70 additions & 5 deletions internal/service/iam/role.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
awspolicy "github.com/hashicorp/awspolicyequivalence"
"github.com/hashicorp/go-multierror"
"github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource"
"github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema"
Expand Down Expand Up @@ -126,20 +127,27 @@ func ResourceRole() *schema.Resource {
Schema: map[string]*schema.Schema{
"name": {
Type: schema.TypeString,
Optional: true,
Optional: true, // semantically required but syntactically optional to allow empty inline_policy
ValidateFunc: validation.All(
validation.StringIsNotEmpty,
validRolePolicyName,
),
},
"policy": {
Type: schema.TypeString,
Optional: true,
Optional: true, // semantically required but syntactically optional to allow empty inline_policy
ValidateFunc: verify.ValidIAMPolicyJSON,
DiffSuppressFunc: verify.SuppressEquivalentPolicyDiffs,
},
},
},
DiffSuppressFunc: func(k, _, _ string, d *schema.ResourceData) bool {
if d.Id() == "" {
return false
}

return !inlinePoliciesActualDiff(d)
},
},

"managed_policy_arns": {
Expand Down Expand Up @@ -292,8 +300,16 @@ func resourceRoleRead(d *schema.ResourceData, meta interface{}) error {
if err != nil {
return fmt.Errorf("reading inline policies for IAM role %s, error: %s", d.Id(), err)
}
if err := d.Set("inline_policy", flattenRoleInlinePolicies(inlinePolicies)); err != nil {
return fmt.Errorf("error setting inline_policy: %w", err)

var configPoliciesList []*iam.PutRolePolicyInput
if v := d.Get("inline_policy").(*schema.Set); v.Len() > 0 {
configPoliciesList = expandRoleInlinePolicies(aws.StringValue(role.RoleName), v.List())
}

if !inlinePoliciesEquivalent(inlinePolicies, configPoliciesList) {
if err := d.Set("inline_policy", flattenRoleInlinePolicies(inlinePolicies)); err != nil {
return fmt.Errorf("error setting inline_policy: %w", err)
}
}

managedPolicies, err := readRolePolicyAttachments(conn, aws.StringValue(role.RoleName))
Expand Down Expand Up @@ -393,18 +409,22 @@ func resourceRoleUpdate(d *schema.ResourceData, meta interface{}) error {
}
}

if d.HasChange("inline_policy") {
if d.HasChange("inline_policy") && inlinePoliciesActualDiff(d) {
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()

Expand Down Expand Up @@ -793,3 +813,48 @@ func readRoleInlinePolicies(roleName string, meta interface{}) ([]*iam.PutRolePo

return apiObjects, nil
}

func inlinePoliciesActualDiff(d *schema.ResourceData) bool {
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)

osPolicies := expandRoleInlinePolicies(roleName, os.List())
nsPolicies := expandRoleInlinePolicies(roleName, ns.List())

return !inlinePoliciesEquivalent(osPolicies, nsPolicies)
}

func inlinePoliciesEquivalent(one, two []*iam.PutRolePolicyInput) bool {
if one == nil && two == nil {
return true
}

if len(one) != len(two) {
return false
}

matches := 0

for _, policyOne := range one {
for _, policyTwo := range two {
if aws.StringValue(policyOne.PolicyName) == aws.StringValue(policyTwo.PolicyName) {
matches++
if equivalent, err := awspolicy.PoliciesAreEquivalent(aws.StringValue(policyOne.PolicyDocument), aws.StringValue(policyTwo.PolicyDocument)); err != nil || !equivalent {
return false
}
break
}
}
}

return matches == len(one)
}
48 changes: 48 additions & 0 deletions internal/service/iam/role_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -472,10 +472,19 @@ func TestAccIAMRole_InlinePolicy_ignoreOrder(t *testing.T) {
resource.TestCheckResourceAttr(resourceName, "managed_policy_arns.#", "0"),
),
},
{
Config: testAccRolePolicyInlineActionOrderConfig(rName),
PlanOnly: true,
},
{
Config: testAccRolePolicyInlineActionNewOrderConfig(rName),
PlanOnly: true,
},
{
Config: testAccRolePolicyInlineActionOrderActualDiffConfig(rName),
PlanOnly: true,
ExpectNonEmptyPlan: true,
},
},
})
}
Expand Down Expand Up @@ -1698,6 +1707,45 @@ resource "aws_iam_role" "test" {
`, roleName)
}

func testAccRolePolicyInlineActionOrderActualDiffConfig(roleName string) string {
return fmt.Sprintf(`
data "aws_partition" "current" {}

resource "aws_iam_role" "test" {
name = %[1]q

assume_role_policy = jsonencode({
Version = "2012-10-17"
Statement = [{
Action = "sts:AssumeRole",
Principal = {
Service = "ec2.${data.aws_partition.current.dns_suffix}",
}
Effect = "Allow"
Sid = ""
}]
})

inline_policy {
name = %[1]q

policy = jsonencode({
Version = "2012-10-17"
Statement = [{
Action = [
"ec2:DescribeScheduledInstances",
"ec2:DescribeElasticGpus",
"ec2:DescribeScheduledInstanceAvailability",
]
Effect = "Allow"
Resource = "*"
}]
})
}
}
`, roleName)
}

func testAccRolePolicyInlineActionNewOrderConfig(roleName string) string {
return fmt.Sprintf(`
data "aws_partition" "current" {}
Expand Down
4 changes: 3 additions & 1 deletion website/docs/r/iam_role.html.markdown
Original file line number Diff line number Diff line change
Expand Up @@ -183,7 +183,7 @@ 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.
* `inline_policy` - (Optional) Configuration block defining an exclusive set of IAM inline policies associated with the IAM role. See below. If no blocks are configured, Terraform will not manage any inline policies in this resource. Configuring one empty block (i.e., `inline_policy {}`) will cause Terraform to remove _all_ inline policies added out of band on `apply`.
* `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.
Expand All @@ -196,6 +196,8 @@ The following arguments are optional:

This configuration block supports the following:

~> **NOTE:** Since one empty block (i.e., `inline_policy {}`) is valid syntactically to remove out of band policies on `apply`, `name` and `policy` are technically _optional_. However, they are both _required_ in order to manage actual inline policies. Not including one or the other may not result in Terraform errors but will result in unpredictable and incorrect behavior.

* `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://learn.hashicorp.com/tutorials/terraform/aws-iam-policy).

Expand Down