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

service/iam: Handle read-after-create eventual consistency in IAM Group resources #18459

Merged
merged 4 commits into from
Apr 2, 2021
Merged
Show file tree
Hide file tree
Changes from 2 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
15 changes: 15 additions & 0 deletions .changelog/18459.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
```release-note:bug
resource/aws_iam_group: Handle read-after-create eventual consistency
```

```release-note:bug
resource/aws_iam_group_membership: Handle read-after-create eventual consistency
```

```release-note:bug
resource/aws_iam_group_policy: Handle read-after-create eventual consistency
```

```release-note:bug
resource/aws_iam_group_policy_attachment: Handle read-after-create eventual consistency
```
40 changes: 40 additions & 0 deletions aws/internal/service/iam/finder/finder.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
package finder

import (
"github.com/aws/aws-sdk-go/aws"
"github.com/aws/aws-sdk-go/service/iam"
)

// GroupAttachedPolicy returns the AttachedPolicy corresponding to the specified group and policy ARN.
func GroupAttachedPolicy(conn *iam.IAM, groupName string, policyARN string) (*iam.AttachedPolicy, error) {
input := &iam.ListAttachedGroupPoliciesInput{
GroupName: aws.String(groupName),
}

var result *iam.AttachedPolicy

err := conn.ListAttachedGroupPoliciesPages(input, func(page *iam.ListAttachedGroupPoliciesOutput, lastPage bool) bool {
if page == nil {
return !lastPage
}

for _, attachedPolicy := range page.AttachedPolicies {
if attachedPolicy == nil {
continue
}

if aws.StringValue(attachedPolicy.PolicyArn) == policyARN {
result = attachedPolicy
return false
}
}

return !lastPage
})

if err != nil {
return nil, err
}

return result, nil
}
52 changes: 41 additions & 11 deletions aws/resource_aws_iam_group.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,17 @@ package aws

import (
"fmt"
"log"
"regexp"

"github.com/aws/aws-sdk-go/aws"
"github.com/aws/aws-sdk-go/aws/awserr"
"github.com/aws/aws-sdk-go/service/iam"
"github.com/hashicorp/aws-sdk-go-base/tfawserr"
"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"
"github.com/terraform-providers/terraform-provider-aws/aws/internal/service/iam/waiter"
"github.com/terraform-providers/terraform-provider-aws/aws/internal/tfresource"
)

func resourceAwsIamGroup() *schema.Resource {
Expand Down Expand Up @@ -63,7 +67,7 @@ func resourceAwsIamGroupCreate(d *schema.ResourceData, meta interface{}) error {
}
d.SetId(aws.StringValue(createResp.Group.GroupName))

return resourceAwsIamGroupReadResult(d, createResp.Group)
return resourceAwsIamGroupRead(d, meta)
}

func resourceAwsIamGroupRead(d *schema.ResourceData, meta interface{}) error {
Expand All @@ -73,18 +77,44 @@ func resourceAwsIamGroupRead(d *schema.ResourceData, meta interface{}) error {
GroupName: aws.String(d.Id()),
}

getResp, err := iamconn.GetGroup(request)
if err != nil {
if iamerr, ok := err.(awserr.Error); ok && iamerr.Code() == "NoSuchEntity" {
d.SetId("")
return nil
var getResp *iam.GetGroupOutput

err := resource.Retry(waiter.PropagationTimeout, func() *resource.RetryError {
var err error

getResp, err = iamconn.GetGroup(request)

if d.IsNewResource() && tfawserr.ErrCodeEquals(err, iam.ErrCodeNoSuchEntityException) {
return resource.RetryableError(err)
}
return fmt.Errorf("Error reading IAM Group %s: %s", d.Id(), err)

if err != nil {
return resource.NonRetryableError(err)
}

return nil
})

if tfresource.TimedOut(err) {
getResp, err = iamconn.GetGroup(request)
}
return resourceAwsIamGroupReadResult(d, getResp.Group)
}

func resourceAwsIamGroupReadResult(d *schema.ResourceData, group *iam.Group) error {
if !d.IsNewResource() && tfawserr.ErrCodeEquals(err, iam.ErrCodeNoSuchEntityException) {
log.Printf("[WARN] IAM Group (%s) not found, removing from state", d.Id())
d.SetId("")
return nil
}

if err != nil {
return fmt.Errorf("error reading IAM Group (%s): %w", d.Id(), err)
}

if getResp == nil || getResp.Group == nil {
return fmt.Errorf("error reading IAM Group (%s): empty response", d.Id())
}

group := getResp.Group

if err := d.Set("name", group.GroupName); err != nil {
return err
}
Expand Down
71 changes: 50 additions & 21 deletions aws/resource_aws_iam_group_membership.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,16 @@ package aws

import (
"fmt"
"log"

"github.com/aws/aws-sdk-go/aws"
"github.com/aws/aws-sdk-go/aws/awserr"
"github.com/aws/aws-sdk-go/service/iam"
"github.com/hashicorp/aws-sdk-go-base/tfawserr"
"github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource"
"github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema"
"github.com/terraform-providers/terraform-provider-aws/aws/internal/service/iam/waiter"
"github.com/terraform-providers/terraform-provider-aws/aws/internal/tfresource"
)

func resourceAwsIamGroupMembership() *schema.Resource {
Expand Down Expand Up @@ -57,34 +62,58 @@ func resourceAwsIamGroupMembershipRead(d *schema.ResourceData, meta interface{})
conn := meta.(*AWSClient).iamconn
group := d.Get("group").(string)

input := &iam.GetGroupInput{
GroupName: aws.String(group),
}

var ul []string
var marker *string
for {
resp, err := conn.GetGroup(&iam.GetGroupInput{
GroupName: aws.String(group),
Marker: marker,
})

if err != nil {
if awsErr, ok := err.(awserr.Error); ok {
// aws specific error
if awsErr.Code() == "NoSuchEntity" {
// group not found
d.SetId("")
return nil
}
err := resource.Retry(waiter.PropagationTimeout, func() *resource.RetryError {
err := conn.GetGroupPages(input, func(page *iam.GetGroupOutput, lastPage bool) bool {
if page == nil {
return !lastPage
}

for _, user := range page.Users {
ul = append(ul, aws.StringValue(user.UserName))
}
return err
}

for _, u := range resp.Users {
ul = append(ul, *u.UserName)
return !lastPage
})

if d.IsNewResource() && tfawserr.ErrCodeEquals(err, iam.ErrCodeNoSuchEntityException) {
return resource.RetryableError(err)
}

if !*resp.IsTruncated {
break
if err != nil {
return resource.NonRetryableError(err)
}
marker = resp.Marker

return nil
})

if tfresource.TimedOut(err) {
err = conn.GetGroupPages(input, func(page *iam.GetGroupOutput, lastPage bool) bool {
if page == nil {
return !lastPage
}

for _, user := range page.Users {
ul = append(ul, aws.StringValue(user.UserName))
}

return !lastPage
})
}

if !d.IsNewResource() && tfawserr.ErrCodeEquals(err, iam.ErrCodeNoSuchEntityException) {
log.Printf("[WARN] IAM Group Membership (%s) not found, removing from state", group)
d.SetId("")
return nil
}

if err != nil {
return fmt.Errorf("error reading IAM Group Membership (%s): %w", group, err)
}

if err := d.Set("users", ul); err != nil {
Expand Down
43 changes: 34 additions & 9 deletions aws/resource_aws_iam_group_policy.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,11 @@ 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/terraform-plugin-sdk/v2/helper/resource"
"github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema"
"github.com/terraform-providers/terraform-provider-aws/aws/internal/service/iam/waiter"
"github.com/terraform-providers/terraform-provider-aws/aws/internal/tfresource"
)

func resourceAwsIamGroupPolicy() *schema.Resource {
Expand Down Expand Up @@ -93,18 +96,40 @@ func resourceAwsIamGroupPolicyRead(d *schema.ResourceData, meta interface{}) err
GroupName: aws.String(group),
}

getResp, err := iamconn.GetGroupPolicy(request)
if err != nil {
if isAWSErr(err, iam.ErrCodeNoSuchEntityException, "") {
log.Printf("[WARN] IAM Group Policy (%s) for %s not found, removing from state", name, group)
d.SetId("")
return nil
var getResp *iam.GetGroupPolicyOutput

err = resource.Retry(waiter.PropagationTimeout, func() *resource.RetryError {
var err error

getResp, err = iamconn.GetGroupPolicy(request)

if d.IsNewResource() && tfawserr.ErrCodeEquals(err, iam.ErrCodeNoSuchEntityException) {
return resource.RetryableError(err)
}

if err != nil {
return resource.NonRetryableError(err)
}
return fmt.Errorf("Error reading IAM policy %s from group %s: %s", name, group, err)

return nil
})

if tfresource.TimedOut(err) {
getResp, err = iamconn.GetGroupPolicy(request)
}

if !d.IsNewResource() && tfawserr.ErrCodeEquals(err, iam.ErrCodeNoSuchEntityException) {
log.Printf("[WARN] IAM Group Policy (%s) not found, removing from state", d.Id())
d.SetId("")
return nil
}

if err != nil {
return fmt.Errorf("error reading IAM Group Policy (%s): %w", d.Id(), err)
}

if getResp.PolicyDocument == nil {
return fmt.Errorf("GetGroupPolicy returned a nil policy document")
if getResp == nil || getResp.PolicyDocument == nil {
return fmt.Errorf("error reading IAM Group Policy (%s): empty response", d.Id())
}

policy, err := url.QueryUnescape(*getResp.PolicyDocument)
Expand Down
65 changes: 42 additions & 23 deletions aws/resource_aws_iam_group_policy_attachment.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,10 +6,13 @@ import (
"strings"

"github.com/aws/aws-sdk-go/aws"
"github.com/aws/aws-sdk-go/aws/awserr"
"github.com/aws/aws-sdk-go/service/iam"
"github.com/hashicorp/aws-sdk-go-base/tfawserr"
"github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource"
"github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema"
"github.com/terraform-providers/terraform-provider-aws/aws/internal/service/iam/finder"
"github.com/terraform-providers/terraform-provider-aws/aws/internal/service/iam/waiter"
"github.com/terraform-providers/terraform-provider-aws/aws/internal/tfresource"
)

func resourceAwsIamGroupPolicyAttachment() *schema.Resource {
Expand Down Expand Up @@ -57,39 +60,55 @@ func resourceAwsIamGroupPolicyAttachmentRead(d *schema.ResourceData, meta interf
conn := meta.(*AWSClient).iamconn
group := d.Get("group").(string)
arn := d.Get("policy_arn").(string)
// Human friendly ID for error messages since d.Id() is non-descriptive
id := fmt.Sprintf("%s:%s", group, arn)

_, err := conn.GetGroup(&iam.GetGroupInput{
GroupName: aws.String(group),
})
var attachedPolicy *iam.AttachedPolicy

if err != nil {
if awsErr, ok := err.(awserr.Error); ok {
if awsErr.Code() == "NoSuchEntity" {
log.Printf("[WARN] No such entity found for Policy Attachment (%s)", group)
d.SetId("")
return nil
}
err := resource.Retry(waiter.PropagationTimeout, func() *resource.RetryError {
var err error

attachedPolicy, err = finder.GroupAttachedPolicy(conn, group, arn)

if d.IsNewResource() && tfawserr.ErrCodeEquals(err, iam.ErrCodeNoSuchEntityException) {
return resource.RetryableError(err)
}
return err
}

attachedPolicies, err := conn.ListAttachedGroupPolicies(&iam.ListAttachedGroupPoliciesInput{
GroupName: aws.String(group),
if err != nil {
return resource.NonRetryableError(err)
}

if d.IsNewResource() && attachedPolicy == nil {
return resource.RetryableError(&resource.NotFoundError{
LastError: fmt.Errorf("IAM Group Managed Policy Attachment (%s) not found", id),
})
}

return nil
})

if tfresource.TimedOut(err) {
attachedPolicy, err = finder.GroupAttachedPolicy(conn, group, arn)
}

if !d.IsNewResource() && tfawserr.ErrCodeEquals(err, iam.ErrCodeNoSuchEntityException) {
log.Printf("[WARN] IAM User Managed Policy Attachment (%s) not found, removing from state", id)
d.SetId("")
return nil
}

if err != nil {
return err
return fmt.Errorf("error reading IAM Group Managed Policy Attachment (%s): %w", id, err)
}

var policy string
for _, p := range attachedPolicies.AttachedPolicies {
if *p.PolicyArn == arn {
policy = *p.PolicyArn
if attachedPolicy == nil {
if d.IsNewResource() {
return fmt.Errorf("error reading IAM User Managed Policy Attachment (%s): not found after creation", id)
}
}

if policy == "" {
log.Printf("[WARN] No such policy found for Group Policy Attachment (%s)", group)
log.Printf("[WARN] IAM Group Managed Policy Attachment (%s) not found, removing from state", id)
d.SetId("")
return nil
}

return nil
Expand Down