diff --git a/.changelog/22544.txt b/.changelog/22544.txt new file mode 100644 index 00000000000..892084a0088 --- /dev/null +++ b/.changelog/22544.txt @@ -0,0 +1,15 @@ +```release-note:enhancement +resource/aws_iam_role: Attempt `tags`-on-create, fallback to tag after create, and allow some `tags` errors to be non-fatal to support non-standard AWS partitions (i.e., ISO) +``` + +```release-note:enhancement +data-source/aws_iam_role: Allow some `tags` errors to be non-fatal to support non-standard AWS partitions (i.e., ISO) +``` + +```release-note:enhancement +resource/aws_iam_user: Attempt `tags`-on-create, fallback to tag after create, and allow some `tags` errors to be non-fatal to support non-standard AWS partitions (i.e., ISO) +``` + +```release-note:enhancement +data-source/aws_iam_user: Allow some `tags` errors to be non-fatal to support non-standard AWS partitions (i.e., ISO) +``` \ No newline at end of file diff --git a/internal/service/iam/consts.go b/internal/service/iam/consts.go new file mode 100644 index 00000000000..b6a409b420c --- /dev/null +++ b/internal/service/iam/consts.go @@ -0,0 +1,5 @@ +package iam + +const ( + ErrCodeAccessDenied = "AccessDenied" +) diff --git a/internal/service/iam/role.go b/internal/service/iam/role.go index bb866b5cc04..b73fd681f8c 100644 --- a/internal/service/iam/role.go +++ b/internal/service/iam/role.go @@ -8,6 +8,7 @@ import ( "time" "github.com/aws/aws-sdk-go/aws" + "github.com/aws/aws-sdk-go/aws/endpoints" "github.com/aws/aws-sdk-go/service/iam" "github.com/hashicorp/aws-sdk-go-base/tfawserr" awspolicy "github.com/hashicorp/awspolicyequivalence" @@ -199,25 +200,21 @@ func resourceRoleCreate(d *schema.ResourceData, meta interface{}) error { request.Tags = Tags(tags.IgnoreAWS()) } - outputRaw, err := tfresource.RetryWhen( - PropagationTimeout, - func() (interface{}, error) { - return conn.CreateRole(request) - }, - func(err error) (bool, error) { - if tfawserr.ErrMessageContains(err, iam.ErrCodeMalformedPolicyDocumentException, "Invalid principal in policy") { - return true, err - } + output, err := retryCreateRole(conn, request) - return false, err - }, - ) + // Some partitions (i.e., ISO) may not support tag-on-create + if request.Tags != nil && meta.(*conns.AWSClient).Partition != endpoints.AwsPartitionID && (tfawserr.ErrCodeContains(err, ErrCodeAccessDenied) || tfawserr.ErrCodeContains(err, iam.ErrCodeInvalidInputException) || tfawserr.ErrCodeContains(err, iam.ErrCodeServiceFailureException)) { + log.Printf("[WARN] IAM Role (%s) create failed (%s) with tags. Trying create without tags.", d.Id(), err) + request.Tags = nil + + output, err = retryCreateRole(conn, request) + } if err != nil { return fmt.Errorf("error creating IAM Role (%s): %w", name, err) } - roleName := aws.StringValue(outputRaw.(*iam.CreateRoleOutput).Role.RoleName) + roleName := aws.StringValue(output.Role.RoleName) if v, ok := d.GetOk("inline_policy"); ok && v.(*schema.Set).Len() > 0 { policies := expandRoleInlinePolicies(roleName, v.(*schema.Set).List()) @@ -234,6 +231,22 @@ func resourceRoleCreate(d *schema.ResourceData, meta interface{}) error { } d.SetId(roleName) + + // Some partitions (i.e., ISO) may not support tag-on-create, attempt tag after create + if request.Tags == nil && len(tags) > 0 && meta.(*conns.AWSClient).Partition != endpoints.AwsPartitionID { + err := roleUpdateTags(conn, d.Id(), nil, tags) + + // If default tags only, log and continue. Otherwise, error. + if v, ok := d.GetOk("tags"); (!ok || len(v.(map[string]interface{})) == 0) && (tfawserr.ErrCodeContains(err, ErrCodeAccessDenied) || tfawserr.ErrCodeContains(err, iam.ErrCodeInvalidInputException) || tfawserr.ErrCodeContains(err, iam.ErrCodeServiceFailureException)) { + log.Printf("[WARN] error adding tags after create for IAM Role (%s): %s", d.Id(), err) + return resourceRoleRead(d, meta) + } + + if err != nil { + return fmt.Errorf("error creating IAM Role (%s) tags: %w", d.Id(), err) + } + } + return resourceRoleRead(d, meta) } @@ -277,17 +290,6 @@ func resourceRoleRead(d *schema.ResourceData, meta interface{}) error { } d.Set("unique_id", role.RoleId) - tags := KeyValueTags(role.Tags).IgnoreAWS().IgnoreConfig(ignoreTagsConfig) - - //lintignore:AWSR002 - if err := d.Set("tags", tags.RemoveDefaultConfig(defaultTagsConfig).Map()); err != nil { - return fmt.Errorf("error setting tags: %w", err) - } - - if err := d.Set("tags_all", tags.Map()); err != nil { - return fmt.Errorf("error setting tags_all: %w", err) - } - assumeRolePolicy, err := url.QueryUnescape(*role.AssumeRolePolicyDocument) if err != nil { return err @@ -318,6 +320,23 @@ func resourceRoleRead(d *schema.ResourceData, meta interface{}) error { } d.Set("managed_policy_arns", managedPolicies) + tags := KeyValueTags(role.Tags).IgnoreAWS().IgnoreConfig(ignoreTagsConfig) + + // Some partitions (i.e., ISO) may not support tagging, giving error + if meta.(*conns.AWSClient).Partition != endpoints.AwsPartitionID && (tfawserr.ErrCodeContains(err, ErrCodeAccessDenied) || tfawserr.ErrCodeContains(err, iam.ErrCodeInvalidInputException) || tfawserr.ErrCodeContains(err, iam.ErrCodeServiceFailureException)) { + log.Printf("[WARN] Unable to list tags for IAM Role %s: %s", d.Id(), err) + return nil + } + + //lintignore:AWSR002 + if err := d.Set("tags", tags.RemoveDefaultConfig(defaultTagsConfig).Map()); err != nil { + return fmt.Errorf("error setting tags: %w", err) + } + + if err := d.Set("tags_all", tags.Map()); err != nil { + return fmt.Errorf("error setting tags_all: %w", err) + } + return nil } @@ -401,14 +420,6 @@ func resourceRoleUpdate(d *schema.ResourceData, meta interface{}) error { } } - if d.HasChange("tags_all") { - o, n := d.GetChange("tags_all") - - if err := roleUpdateTags(conn, d.Id(), o, n); err != nil { - return fmt.Errorf("error updating IAM Role (%s) tags: %s", d.Id(), err) - } - } - if d.HasChange("inline_policy") && inlinePoliciesActualDiff(d) { roleName := d.Get("name").(string) @@ -475,6 +486,22 @@ func resourceRoleUpdate(d *schema.ResourceData, meta interface{}) error { } } + if d.HasChange("tags_all") { + o, n := d.GetChange("tags_all") + + err := roleUpdateTags(conn, d.Id(), o, n) + + // Some partitions may not support tagging, giving error + if meta.(*conns.AWSClient).Partition != endpoints.AwsPartitionID && (tfawserr.ErrCodeContains(err, ErrCodeAccessDenied) || tfawserr.ErrCodeContains(err, iam.ErrCodeInvalidInputException) || tfawserr.ErrCodeContains(err, iam.ErrCodeServiceFailureException)) { + log.Printf("[WARN] Unable to update tags for IAM Role %s: %s", d.Id(), err) + return resourceRoleRead(d, meta) + } + + if err != nil { + return fmt.Errorf("error updating IAM Role (%s) tags: %w", d.Id(), err) + } + } + return resourceRoleRead(d, meta) } @@ -582,6 +609,33 @@ func deleteRoleInstanceProfiles(conn *iam.IAM, roleName string) error { return nil } +func retryCreateRole(conn *iam.IAM, input *iam.CreateRoleInput) (*iam.CreateRoleOutput, error) { + outputRaw, err := tfresource.RetryWhen( + PropagationTimeout, + func() (interface{}, error) { + return conn.CreateRole(input) + }, + func(err error) (bool, error) { + if tfawserr.ErrMessageContains(err, iam.ErrCodeMalformedPolicyDocumentException, "Invalid principal in policy") { + return true, err + } + + return false, err + }, + ) + + if err != nil { + return nil, err + } + + output, ok := outputRaw.(*iam.CreateRoleOutput) + if !ok || output == nil || aws.StringValue(output.Role.RoleName) == "" { + return nil, fmt.Errorf("create IAM role (%s) returned an empty result", aws.StringValue(input.RoleName)) + } + + return output, err +} + func readRolePolicyAttachments(conn *iam.IAM, roleName string) ([]*string, error) { managedPolicies := make([]*string, 0) input := &iam.ListAttachedRolePoliciesInput{ diff --git a/internal/service/iam/role_data_source.go b/internal/service/iam/role_data_source.go index 206fef40b65..711166adb58 100644 --- a/internal/service/iam/role_data_source.go +++ b/internal/service/iam/role_data_source.go @@ -2,11 +2,14 @@ package iam import ( "fmt" + "log" "net/url" "time" "github.com/aws/aws-sdk-go/aws" + "github.com/aws/aws-sdk-go/aws/endpoints" "github.com/aws/aws-sdk-go/service/iam" + "github.com/hashicorp/aws-sdk-go-base/tfawserr" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" "github.com/hashicorp/terraform-provider-aws/internal/conns" tftags "github.com/hashicorp/terraform-provider-aws/internal/tags" @@ -86,7 +89,6 @@ func dataSourceRoleRead(d *schema.ResourceData, meta interface{}) error { d.Set("permissions_boundary", output.Role.PermissionsBoundary.PermissionsBoundaryArn) } d.Set("unique_id", output.Role.RoleId) - d.Set("tags", KeyValueTags(output.Role.Tags).IgnoreAWS().IgnoreConfig(ignoreTagsConfig).Map()) assumRolePolicy, err := url.QueryUnescape(aws.StringValue(output.Role.AssumeRolePolicyDocument)) if err != nil { @@ -96,6 +98,19 @@ func dataSourceRoleRead(d *schema.ResourceData, meta interface{}) error { return fmt.Errorf("error setting assume_role_policy: %w", err) } + tags := KeyValueTags(output.Role.Tags).IgnoreAWS().IgnoreConfig(ignoreTagsConfig) + + // Some partitions (i.e., ISO) may not support tagging, giving error + if meta.(*conns.AWSClient).Partition != endpoints.AwsPartitionID && (tfawserr.ErrCodeContains(err, ErrCodeAccessDenied) || tfawserr.ErrCodeContains(err, iam.ErrCodeInvalidInputException) || tfawserr.ErrCodeContains(err, iam.ErrCodeServiceFailureException)) { + log.Printf("[WARN] Unable to list tags for IAM Role %s: %s", d.Id(), err) + return nil + } + + //lintignore:AWSR002 + if err := d.Set("tags", tags.Map()); err != nil { + return fmt.Errorf("error setting tags: %w", err) + } + d.SetId(name) return nil diff --git a/internal/service/iam/user.go b/internal/service/iam/user.go index 581ea751571..4b33063a7cc 100644 --- a/internal/service/iam/user.go +++ b/internal/service/iam/user.go @@ -6,6 +6,7 @@ import ( "regexp" "github.com/aws/aws-sdk-go/aws" + "github.com/aws/aws-sdk-go/aws/endpoints" "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" @@ -98,12 +99,36 @@ func resourceUserCreate(d *schema.ResourceData, meta interface{}) error { log.Println("[DEBUG] Create IAM User request:", request) createResp, err := conn.CreateUser(request) + + // Some partitions (i.e., ISO) may not support tag-on-create + if request.Tags != nil && meta.(*conns.AWSClient).Partition != endpoints.AwsPartitionID && (tfawserr.ErrCodeContains(err, ErrCodeAccessDenied) || tfawserr.ErrCodeContains(err, iam.ErrCodeInvalidInputException) || tfawserr.ErrCodeContains(err, iam.ErrCodeServiceFailureException)) { + log.Printf("[WARN] IAM User (%s) create failed (%s) with tags. Trying create without tags.", d.Get("name").(string), err) + request.Tags = nil + + createResp, err = conn.CreateUser(request) + } + if err != nil { return fmt.Errorf("Error creating IAM User %s: %s", name, err) } d.SetId(aws.StringValue(createResp.User.UserName)) + // Some partitions (i.e., ISO) may not support tag-on-create, attempt tag after create + if request.Tags == nil && len(tags) > 0 && meta.(*conns.AWSClient).Partition != endpoints.AwsPartitionID { + err := userUpdateTags(conn, d.Id(), nil, tags) + + // If default tags only, log and continue. Otherwise, error. + if v, ok := d.GetOk("tags"); (!ok || len(v.(map[string]interface{})) == 0) && (tfawserr.ErrCodeContains(err, ErrCodeAccessDenied) || tfawserr.ErrCodeContains(err, iam.ErrCodeInvalidInputException) || tfawserr.ErrCodeContains(err, iam.ErrCodeServiceFailureException)) { + log.Printf("[WARN] error adding tags after create for IAM User (%s): %s", d.Id(), err) + return resourceUserRead(d, meta) + } + + if err != nil { + return fmt.Errorf("error creating IAM User (%s) tags: %w", d.Id(), err) + } + } + return resourceUserRead(d, meta) } @@ -162,6 +187,12 @@ func resourceUserRead(d *schema.ResourceData, meta interface{}) error { tags := KeyValueTags(output.User.Tags).IgnoreAWS().IgnoreConfig(ignoreTagsConfig) + // Some partitions (i.e., ISO) may not support tagging, giving error + if meta.(*conns.AWSClient).Partition != endpoints.AwsPartitionID && (tfawserr.ErrCodeContains(err, ErrCodeAccessDenied) || tfawserr.ErrCodeContains(err, iam.ErrCodeInvalidInputException) || tfawserr.ErrCodeContains(err, iam.ErrCodeServiceFailureException)) { + log.Printf("[WARN] Unable to list tags for IAM User %s: %s", d.Id(), err) + return nil + } + //lintignore:AWSR002 if err := d.Set("tags", tags.RemoveDefaultConfig(defaultTagsConfig).Map()); err != nil { return fmt.Errorf("error setting tags: %w", err) @@ -226,8 +257,16 @@ func resourceUserUpdate(d *schema.ResourceData, meta interface{}) error { if d.HasChange("tags_all") { o, n := d.GetChange("tags_all") - if err := userUpdateTags(conn, d.Id(), o, n); err != nil { - return fmt.Errorf("error updating IAM User (%s) tags: %s", d.Id(), err) + err := userUpdateTags(conn, d.Id(), o, n) + + // Some partitions may not support tagging, giving error + if meta.(*conns.AWSClient).Partition != endpoints.AwsPartitionID && (tfawserr.ErrCodeContains(err, ErrCodeAccessDenied) || tfawserr.ErrCodeContains(err, iam.ErrCodeInvalidInputException) || tfawserr.ErrCodeContains(err, iam.ErrCodeServiceFailureException)) { + log.Printf("[WARN] Unable to update tags for IAM User %s: %s", d.Id(), err) + return resourceUserRead(d, meta) + } + + if err != nil { + return fmt.Errorf("error updating IAM User (%s) tags: %w", d.Id(), err) } } diff --git a/internal/service/iam/user_data_source.go b/internal/service/iam/user_data_source.go index aa0713b5765..89b432256cc 100644 --- a/internal/service/iam/user_data_source.go +++ b/internal/service/iam/user_data_source.go @@ -5,7 +5,9 @@ import ( "log" "github.com/aws/aws-sdk-go/aws" + "github.com/aws/aws-sdk-go/aws/endpoints" "github.com/aws/aws-sdk-go/service/iam" + "github.com/hashicorp/aws-sdk-go-base/tfawserr" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" "github.com/hashicorp/terraform-provider-aws/internal/conns" tftags "github.com/hashicorp/terraform-provider-aws/internal/tags" @@ -65,7 +67,17 @@ func dataSourceUserRead(d *schema.ResourceData, meta interface{}) error { d.Set("permissions_boundary", user.PermissionsBoundary.PermissionsBoundaryArn) } d.Set("user_id", user.UserId) - if err := d.Set("tags", KeyValueTags(user.Tags).IgnoreAWS().IgnoreConfig(ignoreTagsConfig).Map()); err != nil { + + tags := KeyValueTags(user.Tags).IgnoreAWS().IgnoreConfig(ignoreTagsConfig) + + // Some partitions (i.e., ISO) may not support tagging, giving error + if meta.(*conns.AWSClient).Partition != endpoints.AwsPartitionID && (tfawserr.ErrCodeContains(err, ErrCodeAccessDenied) || tfawserr.ErrCodeContains(err, iam.ErrCodeInvalidInputException) || tfawserr.ErrCodeContains(err, iam.ErrCodeServiceFailureException)) { + log.Printf("[WARN] Unable to list tags for IAM User %s: %s", d.Id(), err) + return nil + } + + //lintignore:AWSR002 + if err := d.Set("tags", tags.Map()); err != nil { return fmt.Errorf("error setting tags: %w", err) }