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 User resources #18458

Merged
merged 3 commits into from
Apr 2, 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
23 changes: 23 additions & 0 deletions .changelog/18458.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
```release-note:bug
resource/aws_iam_user: Handle read-after-create eventual consistency
```

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

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

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

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

```release-note:bug
resource/aws_iam_user_ssh_key: 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"
)

// UserAttachedPolicy returns the AttachedPolicy corresponding to the specified user and policy ARN.
func UserAttachedPolicy(conn *iam.IAM, userName string, policyARN string) (*iam.AttachedPolicy, error) {
input := &iam.ListAttachedUserPoliciesInput{
UserName: aws.String(userName),
}

var result *iam.AttachedPolicy

err := conn.ListAttachedUserPoliciesPages(input, func(page *iam.ListAttachedUserPoliciesOutput, 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
}
40 changes: 31 additions & 9 deletions aws/resource_aws_iam_user.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,11 +7,13 @@ 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/hashicorp/terraform-plugin-sdk/v2/helper/validation"
"github.com/terraform-providers/terraform-provider-aws/aws/internal/keyvaluetags"
"github.com/terraform-providers/terraform-provider-aws/aws/internal/service/iam/waiter"
"github.com/terraform-providers/terraform-provider-aws/aws/internal/tfresource"
)

func resourceAwsIamUser() *schema.Resource {
Expand Down Expand Up @@ -107,22 +109,42 @@ func resourceAwsIamUserRead(d *schema.ResourceData, meta interface{}) error {
UserName: aws.String(d.Id()),
}

output, err := iamconn.GetUser(request)
if err != nil {
if isAWSErr(err, iam.ErrCodeNoSuchEntityException, "") {
log.Printf("[WARN] No IAM user by name (%s) found, removing from state", d.Id())
d.SetId("")
return nil
var output *iam.GetUserOutput

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

output, err = iamconn.GetUser(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 User %s: %s", d.Id(), err)

return nil
})

if tfresource.TimedOut(err) {
output, err = iamconn.GetUser(request)
}

if output == nil || output.User == nil {
log.Printf("[WARN] No IAM user by name (%s) found, removing from state", d.Id())
if !d.IsNewResource() && tfawserr.ErrCodeEquals(err, iam.ErrCodeNoSuchEntityException) {
log.Printf("[WARN] IAM User (%s) not found, removing from state", d.Id())
d.SetId("")
return nil
}

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

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

d.Set("arn", output.User.Arn)
d.Set("name", output.User.UserName)
d.Set("path", output.User.Path)
Expand Down
73 changes: 52 additions & 21 deletions aws/resource_aws_iam_user_group_membership.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,13 @@ import (
"log"
"strings"

"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 resourceAwsIamUserGroupMembership() *schema.Resource {
Expand Down Expand Up @@ -57,36 +61,63 @@ func resourceAwsIamUserGroupMembershipRead(d *schema.ResourceData, meta interfac

user := d.Get("user").(string)
groups := d.Get("groups").(*schema.Set)

input := &iam.ListGroupsForUserInput{
UserName: aws.String(user),
}

var gl []string
var marker *string

for {
resp, err := conn.ListGroupsForUser(&iam.ListGroupsForUserInput{
UserName: &user,
Marker: marker,
})
if err != nil {
if isAWSErr(err, iam.ErrCodeNoSuchEntityException, "") {
// no such user
log.Printf("[WARN] Groups not found for user (%s), removing from state", user)
d.SetId("")
return nil
err := resource.Retry(waiter.PropagationTimeout, func() *resource.RetryError {
err := conn.ListGroupsForUserPages(input, func(page *iam.ListGroupsForUserOutput, lastPage bool) bool {
if page == nil {
return !lastPage
}
return err
}

for _, g := range resp.Groups {
// only read in the groups we care about
if groups.Contains(*g.GroupName) {
gl = append(gl, *g.GroupName)
for _, group := range page.Groups {
if groups.Contains(aws.StringValue(group.GroupName)) {
gl = append(gl, aws.StringValue(group.GroupName))
}
}

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.ListGroupsForUserPages(input, func(page *iam.ListGroupsForUserOutput, lastPage bool) bool {
if page == nil {
return !lastPage
}

for _, group := range page.Groups {
if groups.Contains(aws.StringValue(group.GroupName)) {
gl = append(gl, aws.StringValue(group.GroupName))
}
}

return !lastPage
})
}

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

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

if err := d.Set("groups", gl); err != nil {
Expand Down
31 changes: 26 additions & 5 deletions aws/resource_aws_iam_user_login_profile.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,11 +11,13 @@ 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/hashicorp/terraform-plugin-sdk/v2/helper/validation"
"github.com/terraform-providers/terraform-provider-aws/aws/internal/encryption"
"github.com/terraform-providers/terraform-provider-aws/aws/internal/service/ec2/waiter"
"github.com/terraform-providers/terraform-provider-aws/aws/internal/tfresource"
)

func resourceAwsIamUserLoginProfile() *schema.Resource {
Expand Down Expand Up @@ -168,21 +170,40 @@ func resourceAwsIamUserLoginProfileRead(d *schema.ResourceData, meta interface{}
UserName: aws.String(d.Id()),
}

log.Printf("[DEBUG] Getting IAM User Login Profile (%s): %s", d.Id(), input)
output, err := conn.GetLoginProfile(input)
var output *iam.GetLoginProfileOutput

if isAWSErr(err, iam.ErrCodeNoSuchEntityException, "") {
err := resource.Retry(waiter.PropagationTimeout, func() *resource.RetryError {
var err error

output, err = conn.GetLoginProfile(input)

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

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

return nil
})

if tfresource.TimedOut(err) {
output, err = conn.GetLoginProfile(input)
}

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

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

if output == nil || output.LoginProfile == nil {
return fmt.Errorf("error getting IAM User Login Profile (%s): empty response", d.Id())
return fmt.Errorf("error reading IAM User Login Profile (%s): empty response", d.Id())
}

d.Set("user", aws.StringValue(output.LoginProfile.UserName))
Expand Down
43 changes: 34 additions & 9 deletions aws/resource_aws_iam_user_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 resourceAwsIamUserPolicy() *schema.Resource {
Expand Down Expand Up @@ -98,18 +101,40 @@ func resourceAwsIamUserPolicyRead(d *schema.ResourceData, meta interface{}) erro
UserName: aws.String(user),
}

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

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

getResp, err = iamconn.GetUserPolicy(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 user %s: %s", name, user, err)

return nil
})

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

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

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

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

policy, err := url.QueryUnescape(*getResp.PolicyDocument)
Expand Down
Loading