diff --git a/.changelog/28928.txt b/.changelog/28928.txt new file mode 100644 index 000000000000..470da24180e7 --- /dev/null +++ b/.changelog/28928.txt @@ -0,0 +1,7 @@ +```release-note:enhancement +resource/aws_elasticache_user: Add `authentication_mode` argument +``` + +```release-note:bug +resource/aws_elasticache_user: Change `user_id` to [ForceNew](https://developer.hashicorp.com/terraform/plugin/sdkv2/schemas/schema-behaviors#forcenew) +``` \ No newline at end of file diff --git a/internal/service/elasticache/find.go b/internal/service/elasticache/find.go index 1cbbe6cf2c7f..dba86b440647 100644 --- a/internal/service/elasticache/find.go +++ b/internal/service/elasticache/find.go @@ -183,30 +183,6 @@ func FindGlobalReplicationGroupMemberByID(ctx context.Context, conn *elasticache } } -func FindUserByID(ctx context.Context, conn *elasticache.ElastiCache, userID string) (*elasticache.User, error) { - input := &elasticache.DescribeUsersInput{ - UserId: aws.String(userID), - } - out, err := conn.DescribeUsersWithContext(ctx, input) - - if err != nil { - return nil, err - } - - switch len(out.Users) { - case 0: - return nil, &resource.NotFoundError{ - Message: "empty result", - } - case 1: - return out.Users[0], nil - default: - return nil, &resource.NotFoundError{ - Message: "too many results", - } - } -} - func FindUserGroupByID(ctx context.Context, conn *elasticache.ElastiCache, groupID string) (*elasticache.UserGroup, error) { input := &elasticache.DescribeUserGroupsInput{ UserGroupId: aws.String(groupID), diff --git a/internal/service/elasticache/status.go b/internal/service/elasticache/status.go index 09dd9ac2755d..4169466b9487 100644 --- a/internal/service/elasticache/status.go +++ b/internal/service/elasticache/status.go @@ -16,10 +16,6 @@ const ( ReplicationGroupStatusDeleting = "deleting" ReplicationGroupStatusCreateFailed = "create-failed" ReplicationGroupStatusSnapshotting = "snapshotting" - - UserStatusActive = "active" - UserStatusDeleting = "deleting" - UserStatusModifying = "modifying" ) // StatusReplicationGroup fetches the Replication Group and its Status @@ -130,20 +126,3 @@ func statusGlobalReplicationGroupMember(ctx context.Context, conn *elasticache.E return member, aws.StringValue(member.Status), nil } } - -// StatusUser fetches the ElastiCache user and its Status -func StatusUser(ctx context.Context, conn *elasticache.ElastiCache, userId string) resource.StateRefreshFunc { - return func() (interface{}, string, error) { - user, err := FindUserByID(ctx, conn, userId) - - if tfresource.NotFound(err) { - return nil, "", nil - } - - if err != nil { - return nil, "", err - } - - return user, aws.StringValue(user.Status), nil - } -} diff --git a/internal/service/elasticache/user.go b/internal/service/elasticache/user.go index 4ac1f2fb3910..278b024b5faf 100644 --- a/internal/service/elasticache/user.go +++ b/internal/service/elasticache/user.go @@ -4,11 +4,13 @@ import ( "context" "log" "strings" + "time" "github.com/aws/aws-sdk-go/aws" "github.com/aws/aws-sdk-go/service/elasticache" "github.com/hashicorp/aws-sdk-go-base/v2/awsv1shim/v2/tfawserr" "github.com/hashicorp/terraform-plugin-sdk/v2/diag" + "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/hashicorp/terraform-provider-aws/internal/conns" @@ -38,8 +40,35 @@ func ResourceUser() *schema.Resource { }, "arn": { Type: schema.TypeString, + Computed: true, + }, + "authentication_mode": { + Type: schema.TypeList, Optional: true, Computed: true, + MaxItems: 1, + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "passwords": { + Type: schema.TypeSet, + Optional: true, + MinItems: 1, + Sensitive: true, + Elem: &schema.Schema{ + Type: schema.TypeString, + }, + }, + "password_count": { + Type: schema.TypeInt, + Computed: true, + }, + "type": { + Type: schema.TypeString, + Required: true, + ValidateFunc: validation.StringInSlice(elasticache.InputAuthenticationType_Values(), false), + }, + }, + }, }, "engine": { Type: schema.TypeString, @@ -70,6 +99,7 @@ func ResourceUser() *schema.Resource { "user_id": { Type: schema.TypeString, Required: true, + ForceNew: true, }, "user_name": { Type: schema.TypeString, @@ -86,6 +116,7 @@ func resourceUserCreate(ctx context.Context, d *schema.ResourceData, meta interf defaultTagsConfig := meta.(*conns.AWSClient).DefaultTagsConfig tags := defaultTagsConfig.MergeTags(tftags.New(ctx, d.Get("tags").(map[string]interface{}))) + userID := d.Get("user_id").(string) input := &elasticache.CreateUserInput{ AccessString: aws.String(d.Get("access_string").(string)), Engine: aws.String(d.Get("engine").(string)), @@ -94,7 +125,11 @@ func resourceUserCreate(ctx context.Context, d *schema.ResourceData, meta interf UserName: aws.String(d.Get("user_name").(string)), } - if v, ok := d.GetOk("passwords"); ok { + if v, ok := d.GetOk("authentication_mode"); ok && len(v.([]interface{})) > 0 && v.([]interface{})[0] != nil { + input.AuthenticationMode = expandAuthenticationMode(v.([]interface{})[0].(map[string]interface{})) + } + + if v, ok := d.GetOk("passwords"); ok && v.(*schema.Set).Len() > 0 { input.Passwords = flex.ExpandStringSet(v.(*schema.Set)) } @@ -102,24 +137,24 @@ func resourceUserCreate(ctx context.Context, d *schema.ResourceData, meta interf input.Tags = Tags(tags.IgnoreAWS()) } - out, err := conn.CreateUserWithContext(ctx, input) + output, err := conn.CreateUserWithContext(ctx, input) if input.Tags != nil && verify.ErrorISOUnsupported(conn.PartitionID, err) { log.Printf("[WARN] failed creating ElastiCache User with tags: %s. Trying create without tags.", err) input.Tags = nil - out, err = conn.CreateUserWithContext(ctx, input) + output, err = conn.CreateUserWithContext(ctx, input) } if err != nil { - return sdkdiag.AppendErrorf(diags, "creating ElastiCache User: %s", err) + return sdkdiag.AppendErrorf(diags, "creating ElastiCache User (%s): %s", userID, err) } - d.SetId(aws.StringValue(out.UserId)) + d.SetId(aws.StringValue(output.UserId)) // In some partitions, only post-create tagging supported if input.Tags == nil && len(tags) > 0 { - err := UpdateTags(ctx, conn, aws.StringValue(out.ARN), nil, tags) + err := UpdateTags(ctx, conn, aws.StringValue(output.ARN), nil, tags) if err != nil { if v, ok := d.GetOk("tags"); (ok && len(v.(map[string]interface{})) > 0) || !verify.ErrorISOUnsupported(conn.PartitionID, err) { @@ -140,32 +175,46 @@ func resourceUserRead(ctx context.Context, d *schema.ResourceData, meta interfac defaultTagsConfig := meta.(*conns.AWSClient).DefaultTagsConfig ignoreTagsConfig := meta.(*conns.AWSClient).IgnoreTagsConfig - resp, err := FindUserByID(ctx, conn, d.Id()) - if !d.IsNewResource() && (tfresource.NotFound(err) || tfawserr.ErrCodeEquals(err, elasticache.ErrCodeUserNotFoundFault)) { + user, err := FindUserByID(ctx, conn, d.Id()) + + if !d.IsNewResource() && tfresource.NotFound(err) { log.Printf("[WARN] ElastiCache User (%s) not found, removing from state", d.Id()) d.SetId("") return diags } if err != nil { - return sdkdiag.AppendErrorf(diags, "describing ElastiCache User (%s): %s", d.Id(), err) + return sdkdiag.AppendErrorf(diags, "reading ElastiCache User (%s): %s", d.Id(), err) } - d.Set("access_string", resp.AccessString) - d.Set("engine", resp.Engine) - d.Set("user_id", resp.UserId) - d.Set("user_name", resp.UserName) - d.Set("arn", resp.ARN) + d.Set("access_string", user.AccessString) + d.Set("arn", user.ARN) + if v := user.Authentication; v != nil { + authenticationMode := map[string]interface{}{ + "passwords": d.Get("authentication_mode.0.passwords"), + "password_count": aws.Int64Value(v.PasswordCount), + "type": aws.StringValue(v.Type), + } + + if err := d.Set("authentication_mode", []interface{}{authenticationMode}); err != nil { + return sdkdiag.AppendErrorf(diags, "setting authentication_mode: %s", err) + } + } else { + d.Set("authentication_mode", nil) + } + d.Set("engine", user.Engine) + d.Set("user_id", user.UserId) + d.Set("user_name", user.UserName) - tags, err := ListTags(ctx, conn, aws.StringValue(resp.ARN)) + tags, err := ListTags(ctx, conn, aws.StringValue(user.ARN)) if err != nil && !verify.ErrorISOUnsupported(conn.PartitionID, err) { - return sdkdiag.AppendErrorf(diags, "listing tags for ElastiCache User (%s): %s", aws.StringValue(resp.ARN), err) + return sdkdiag.AppendErrorf(diags, "listing tags for ElastiCache User (%s): %s", aws.StringValue(user.ARN), err) } // tags not supported in all partitions if err != nil { - log.Printf("[WARN] failed listing tags for ElastiCache User (%s): %s", aws.StringValue(resp.ARN), err) + log.Printf("[WARN] failed listing tags for ElastiCache User (%s): %s", aws.StringValue(user.ARN), err) } if tags != nil { @@ -187,37 +236,38 @@ func resourceUserRead(ctx context.Context, d *schema.ResourceData, meta interfac func resourceUserUpdate(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics { var diags diag.Diagnostics conn := meta.(*conns.AWSClient).ElastiCacheConn() - hasChange := false if d.HasChangesExcept("tags", "tags_all") { - req := &elasticache.ModifyUserInput{ + input := &elasticache.ModifyUserInput{ UserId: aws.String(d.Id()), } if d.HasChange("access_string") { - req.AccessString = aws.String(d.Get("access_string").(string)) - hasChange = true + input.AccessString = aws.String(d.Get("access_string").(string)) + } + + if d.HasChange("authentication_mode") { + if v, ok := d.GetOk("authentication_mode"); ok && len(v.([]interface{})) > 0 && v.([]interface{})[0] != nil { + input.AuthenticationMode = expandAuthenticationMode(v.([]interface{})[0].(map[string]interface{})) + } } if d.HasChange("no_password_required") { - req.NoPasswordRequired = aws.Bool(d.Get("no_password_required").(bool)) - hasChange = true + input.NoPasswordRequired = aws.Bool(d.Get("no_password_required").(bool)) } if d.HasChange("passwords") { - req.Passwords = flex.ExpandStringSet(d.Get("passwords").(*schema.Set)) - hasChange = true + input.Passwords = flex.ExpandStringSet(d.Get("passwords").(*schema.Set)) } - if hasChange { - _, err := conn.ModifyUserWithContext(ctx, req) - if err != nil { - return sdkdiag.AppendErrorf(diags, "updating ElastiCache User (%s): %s", d.Id(), err) - } + _, err := conn.ModifyUserWithContext(ctx, input) - if err := WaitUserActive(ctx, conn, d.Id()); err != nil { - return sdkdiag.AppendErrorf(diags, "waiting for ElastiCache User (%s) to be modified: %s", d.Id(), err) - } + if err != nil { + return sdkdiag.AppendErrorf(diags, "updating ElastiCache User (%s): %s", d.Id(), err) + } + + if _, err := waitUserUpdated(ctx, conn, d.Id()); err != nil { + return sdkdiag.AppendErrorf(diags, "waiting for ElastiCache User (%s) update: %s", d.Id(), err) } } @@ -243,11 +293,10 @@ func resourceUserDelete(ctx context.Context, d *schema.ResourceData, meta interf var diags diag.Diagnostics conn := meta.(*conns.AWSClient).ElastiCacheConn() - input := &elasticache.DeleteUserInput{ + log.Printf("[INFO] Deleting ElastiCache User: %s", d.Id()) + _, err := conn.DeleteUserWithContext(ctx, &elasticache.DeleteUserInput{ UserId: aws.String(d.Id()), - } - - _, err := conn.DeleteUserWithContext(ctx, input) + }) if tfawserr.ErrCodeEquals(err, elasticache.ErrCodeUserNotFoundFault) { return diags @@ -257,12 +306,118 @@ func resourceUserDelete(ctx context.Context, d *schema.ResourceData, meta interf return sdkdiag.AppendErrorf(diags, "deleting ElastiCache User (%s): %s", d.Id(), err) } - if err := WaitUserDeleted(ctx, conn, d.Id()); err != nil { - if tfawserr.ErrCodeEquals(err, elasticache.ErrCodeUserNotFoundFault) { - return diags - } - return sdkdiag.AppendErrorf(diags, "waiting for ElastiCache User (%s) to be deleted: %s", d.Id(), err) + if _, err := waitUserDeleted(ctx, conn, d.Id()); err != nil { + return sdkdiag.AppendErrorf(diags, "waiting for ElastiCache User (%s) delete: %s", d.Id(), err) } return diags } + +func FindUserByID(ctx context.Context, conn *elasticache.ElastiCache, userID string) (*elasticache.User, error) { + input := &elasticache.DescribeUsersInput{ + UserId: aws.String(userID), + } + + output, err := conn.DescribeUsersWithContext(ctx, input) + + if tfawserr.ErrCodeEquals(err, elasticache.ErrCodeUserNotFoundFault) { + return nil, &resource.NotFoundError{ + LastError: err, + LastRequest: input, + } + } + + if err != nil { + return nil, err + } + + if output == nil || len(output.Users) == 0 || output.Users[0] == nil { + return nil, tfresource.NewEmptyResultError(input) + } + + if count := len(output.Users); count > 1 { + return nil, tfresource.NewTooManyResultsError(count, input) + } + + return output.Users[0], nil +} + +func statusUser(ctx context.Context, conn *elasticache.ElastiCache, id string) resource.StateRefreshFunc { + return func() (interface{}, string, error) { + user, err := FindUserByID(ctx, conn, id) + + if tfresource.NotFound(err) { + return nil, "", nil + } + + if err != nil { + return nil, "", err + } + + return user, aws.StringValue(user.Status), nil + } +} + +const ( + UserStatusActive = "active" + UserStatusDeleting = "deleting" + UserStatusModifying = "modifying" +) + +func waitUserUpdated(ctx context.Context, conn *elasticache.ElastiCache, id string) (*elasticache.User, error) { + const ( + timeout = 5 * time.Minute + ) + stateConf := &resource.StateChangeConf{ + Pending: []string{UserStatusModifying}, + Target: []string{UserStatusActive}, + Refresh: statusUser(ctx, conn, id), + Timeout: timeout, + } + + outputRaw, err := stateConf.WaitForStateContext(ctx) + + if output, ok := outputRaw.(*elasticache.User); ok { + return output, err + } + + return nil, err +} + +func waitUserDeleted(ctx context.Context, conn *elasticache.ElastiCache, id string) (*elasticache.User, error) { + const ( + timeout = 5 * time.Minute + ) + stateConf := &resource.StateChangeConf{ + Pending: []string{UserStatusDeleting}, + Target: []string{}, + Refresh: statusUser(ctx, conn, id), + Timeout: timeout, + } + + outputRaw, err := stateConf.WaitForStateContext(ctx) + + if output, ok := outputRaw.(*elasticache.User); ok { + return output, err + } + + return nil, err +} + +func expandAuthenticationMode(tfMap map[string]interface{}) *elasticache.AuthenticationMode { + if tfMap == nil { + return nil + } + + apiObject := &elasticache.AuthenticationMode{} + + if v, ok := tfMap["passwords"].(*schema.Set); ok && v.Len() > 0 { + apiObject.Passwords = flex.ExpandStringSet(v) + } + + if v, ok := tfMap["type"].(string); ok && v != "" { + apiObject.Type = aws.String(v) + } + + return apiObject +} diff --git a/internal/service/elasticache/user_data_source.go b/internal/service/elasticache/user_data_source.go index bbb98f8e2c2c..b1624fec5959 100644 --- a/internal/service/elasticache/user_data_source.go +++ b/internal/service/elasticache/user_data_source.go @@ -21,6 +21,22 @@ func DataSourceUser() *schema.Resource { Type: schema.TypeString, Optional: true, }, + "authentication_mode": { + Type: schema.TypeList, + Optional: true, + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "password_count": { + Optional: true, + Type: schema.TypeInt, + }, + "type": { + Optional: true, + Type: schema.TypeString, + }, + }, + }, + }, "engine": { Type: schema.TypeString, Optional: true, @@ -63,6 +79,18 @@ func dataSourceUserRead(ctx context.Context, d *schema.ResourceData, meta interf d.SetId(aws.StringValue(user.UserId)) d.Set("access_string", user.AccessString) + + if v := user.Authentication; v != nil { + authenticationMode := map[string]interface{}{ + "password_count": aws.Int64Value(v.PasswordCount), + "type": aws.StringValue(v.Type), + } + + if err := d.Set("authentication_mode", []interface{}{authenticationMode}); err != nil { + return sdkdiag.AppendErrorf(diags, "setting authentication_mode: %s", err) + } + } + d.Set("engine", user.Engine) d.Set("user_id", user.UserId) d.Set("user_name", user.UserName) diff --git a/internal/service/elasticache/user_test.go b/internal/service/elasticache/user_test.go index d4d47594494a..acf7f17ba152 100644 --- a/internal/service/elasticache/user_test.go +++ b/internal/service/elasticache/user_test.go @@ -6,10 +6,8 @@ import ( "testing" "github.com/aws/aws-sdk-go/service/elasticache" - "github.com/hashicorp/aws-sdk-go-base/v2/awsv1shim/v2/tfawserr" sdkacctest "github.com/hashicorp/terraform-plugin-sdk/v2/helper/acctest" "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/terraform" "github.com/hashicorp/terraform-provider-aws/internal/acctest" "github.com/hashicorp/terraform-provider-aws/internal/conns" @@ -52,6 +50,79 @@ func TestAccElastiCacheUser_basic(t *testing.T) { }) } +func TestAccElastiCacheUser_password_auth_mode(t *testing.T) { + ctx := acctest.Context(t) + var user elasticache.User + rName := sdkacctest.RandomWithPrefix("tf-acc") + resourceName := "aws_elasticache_user.test" + + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { acctest.PreCheck(ctx, t) }, + ErrorCheck: acctest.ErrorCheck(t, elasticache.EndpointsID), + ProtoV5ProviderFactories: acctest.ProtoV5ProviderFactories, + CheckDestroy: testAccCheckUserDestroy(ctx), + Steps: []resource.TestStep{ + { + Config: testAccUserConfigWithPasswordAuthMode_basic(rName), + Check: resource.ComposeTestCheckFunc( + testAccCheckUserExists(ctx, resourceName, &user), + resource.TestCheckResourceAttr(resourceName, "user_id", rName), + resource.TestCheckResourceAttr(resourceName, "user_name", "username1"), + resource.TestCheckResourceAttr(resourceName, "engine", "redis"), + resource.TestCheckResourceAttr(resourceName, "authentication_mode.0.password_count", "1"), + resource.TestCheckResourceAttr(resourceName, "authentication_mode.0.passwords.#", "1"), + resource.TestCheckTypeSetElemAttr(resourceName, "authentication_mode.0.passwords.*", "aaaaaaaaaaaaaaaa"), + resource.TestCheckResourceAttr(resourceName, "authentication_mode.0.type", "password"), + ), + }, + { + ResourceName: resourceName, + ImportState: true, + ImportStateVerify: true, + ImportStateVerifyIgnore: []string{ + "authentication_mode.0.passwords.#", + "authentication_mode.0.passwords.0", + "no_password_required", + }, + }, + }, + }) +} + +func TestAccElastiCacheUser_iam_auth_mode(t *testing.T) { + ctx := acctest.Context(t) + var user elasticache.User + rName := sdkacctest.RandomWithPrefix("tf-acc") + resourceName := "aws_elasticache_user.test" + + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { acctest.PreCheck(ctx, t) }, + ErrorCheck: acctest.ErrorCheck(t, elasticache.EndpointsID), + ProtoV5ProviderFactories: acctest.ProtoV5ProviderFactories, + CheckDestroy: testAccCheckUserDestroy(ctx), + Steps: []resource.TestStep{ + { + Config: testAccUserConfigWithIAMAuthMode_basic(rName), + Check: resource.ComposeTestCheckFunc( + testAccCheckUserExists(ctx, resourceName, &user), + resource.TestCheckResourceAttr(resourceName, "user_id", rName), + resource.TestCheckResourceAttr(resourceName, "user_name", rName), + resource.TestCheckResourceAttr(resourceName, "engine", "redis"), + resource.TestCheckResourceAttr(resourceName, "authentication_mode.0.type", "iam"), + ), + }, + { + ResourceName: resourceName, + ImportState: true, + ImportStateVerify: true, + ImportStateVerifyIgnore: []string{ + "no_password_required", + }, + }, + }, + }) +} + func TestAccElastiCacheUser_update(t *testing.T) { ctx := acctest.Context(t) var user elasticache.User @@ -90,6 +161,70 @@ func TestAccElastiCacheUser_update(t *testing.T) { }) } +func TestAccElastiCacheUser_update_password_auth_mode(t *testing.T) { + ctx := acctest.Context(t) + var user elasticache.User + rName := sdkacctest.RandomWithPrefix("tf-acc") + resourceName := "aws_elasticache_user.test" + + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { acctest.PreCheck(ctx, t) }, + ErrorCheck: acctest.ErrorCheck(t, elasticache.EndpointsID), + ProtoV5ProviderFactories: acctest.ProtoV5ProviderFactories, + CheckDestroy: testAccCheckUserDestroy(ctx), + Steps: []resource.TestStep{ + { + Config: testAccUserConfigWithPasswordAuthMode_twoPasswords(rName, "aaaaaaaaaaaaaaaa", "bbbbbbbbbbbbbbbb"), + Check: resource.ComposeTestCheckFunc( + testAccCheckUserExists(ctx, resourceName, &user), + resource.TestCheckResourceAttr(resourceName, "authentication_mode.0.password_count", "2"), + ), + }, + { + ResourceName: resourceName, + ImportState: true, + ImportStateVerify: true, + ImportStateVerifyIgnore: []string{ + "authentication_mode.0.passwords", + "no_password_required", + }, + }, + { + Config: testAccUserConfigWithPasswordAuthMode_onePassword(rName, "aaaaaaaaaaaaaaaa"), + Check: resource.ComposeTestCheckFunc( + testAccCheckUserExists(ctx, resourceName, &user), + resource.TestCheckResourceAttr(resourceName, "authentication_mode.0.password_count", "1"), + ), + }, + { + ResourceName: resourceName, + ImportState: true, + ImportStateVerify: true, + ImportStateVerifyIgnore: []string{ + "authentication_mode.0.passwords", + "no_password_required", + }, + }, + { + Config: testAccUserConfigWithPasswordAuthMode_twoPasswords(rName, "cccccccccccccccc", "dddddddddddddddd"), + Check: resource.ComposeTestCheckFunc( + testAccCheckUserExists(ctx, resourceName, &user), + resource.TestCheckResourceAttr(resourceName, "authentication_mode.0.password_count", "2"), + ), + }, + { + ResourceName: resourceName, + ImportState: true, + ImportStateVerify: true, + ImportStateVerifyIgnore: []string{ + "authentication_mode.0.passwords", + "no_password_required", + }, + }, + }, + }) +} + func TestAccElastiCacheUser_tags(t *testing.T) { ctx := acctest.Context(t) var user elasticache.User @@ -166,21 +301,17 @@ func TestAccElastiCacheUser_disappears(t *testing.T) { } func testAccCheckUserDestroy(ctx context.Context) resource.TestCheckFunc { - return func(s *terraform.State) error { return testAccCheckUserDestroyWithProvider(ctx)(s, acctest.Provider) } -} - -func testAccCheckUserDestroyWithProvider(ctx context.Context) acctest.TestCheckWithProviderFunc { - return func(s *terraform.State, provider *schema.Provider) error { - conn := provider.Meta().(*conns.AWSClient).ElastiCacheConn() + return func(s *terraform.State) error { + conn := acctest.Provider.Meta().(*conns.AWSClient).ElastiCacheConn() for _, rs := range s.RootModule().Resources { if rs.Type != "aws_elasticache_user" { continue } - user, err := tfelasticache.FindUserByID(ctx, conn, rs.Primary.ID) + _, err := tfelasticache.FindUserByID(ctx, conn, rs.Primary.ID) - if tfawserr.ErrCodeEquals(err, elasticache.ErrCodeUserNotFoundFault) || tfresource.NotFound(err) { + if tfresource.NotFound(err) { continue } @@ -188,9 +319,7 @@ func testAccCheckUserDestroyWithProvider(ctx context.Context) acctest.TestCheckW return err } - if user != nil { - return fmt.Errorf("ElastiCache User (%s) still exists", rs.Primary.ID) - } + return fmt.Errorf("ElastiCache User (%s) still exists", rs.Primary.ID) } return nil @@ -198,10 +327,6 @@ func testAccCheckUserDestroyWithProvider(ctx context.Context) acctest.TestCheckW } func testAccCheckUserExists(ctx context.Context, n string, v *elasticache.User) resource.TestCheckFunc { - return testAccCheckUserExistsWithProvider(ctx, n, v, func() *schema.Provider { return acctest.Provider }) -} - -func testAccCheckUserExistsWithProvider(ctx context.Context, n string, v *elasticache.User, providerF func() *schema.Provider) resource.TestCheckFunc { return func(s *terraform.State) error { rs, ok := s.RootModule().Resources[n] if !ok { @@ -212,21 +337,22 @@ func testAccCheckUserExistsWithProvider(ctx context.Context, n string, v *elasti return fmt.Errorf("No ElastiCache User ID is set") } - provider := providerF() - conn := provider.Meta().(*conns.AWSClient).ElastiCacheConn() - resp, err := tfelasticache.FindUserByID(ctx, conn, rs.Primary.ID) + conn := acctest.Provider.Meta().(*conns.AWSClient).ElastiCacheConn() + + output, err := tfelasticache.FindUserByID(ctx, conn, rs.Primary.ID) + if err != nil { - return fmt.Errorf("ElastiCache User (%s) not found: %w", rs.Primary.ID, err) + return err } - *v = *resp + *v = *output return nil } } func testAccUserConfig_basic(rName string) string { - return acctest.ConfigCompose(acctest.ConfigAvailableAZsNoOptIn(), fmt.Sprintf(` + return fmt.Sprintf(` resource "aws_elasticache_user" "test" { user_id = %[1]q user_name = "username1" @@ -234,11 +360,42 @@ resource "aws_elasticache_user" "test" { engine = "REDIS" passwords = ["password123456789"] } -`, rName)) +`, rName) +} + +func testAccUserConfigWithPasswordAuthMode_basic(rName string) string { + return fmt.Sprintf(` +resource "aws_elasticache_user" "test" { + user_id = %[1]q + user_name = "username1" + access_string = "on ~app::* -@all +@read +@hash +@bitmap +@geo -setbit -bitfield -hset -hsetnx -hmset -hincrby -hincrbyfloat -hdel -bitop -geoadd -georadius -georadiusbymember" + engine = "REDIS" + + authentication_mode { + type = "password" + passwords = ["aaaaaaaaaaaaaaaa"] + } +} +`, rName) +} + +func testAccUserConfigWithIAMAuthMode_basic(rName string) string { + return fmt.Sprintf(` +resource "aws_elasticache_user" "test" { + user_id = %[1]q + user_name = %[1]q + access_string = "on ~app::* -@all +@read +@hash +@bitmap +@geo -setbit -bitfield -hset -hsetnx -hmset -hincrby -hincrbyfloat -hdel -bitop -geoadd -georadius -georadiusbymember" + engine = "REDIS" + + authentication_mode { + type = "iam" + } +} +`, rName) } func testAccUserConfig_update(rName string) string { - return acctest.ConfigCompose(acctest.ConfigAvailableAZsNoOptIn(), fmt.Sprintf(` + return fmt.Sprintf(` resource "aws_elasticache_user" "test" { user_id = %[1]q user_name = "username1" @@ -246,11 +403,43 @@ resource "aws_elasticache_user" "test" { engine = "REDIS" passwords = ["password234567891", "password345678912"] } -`, rName)) +`, rName) +} + +func testAccUserConfigWithPasswordAuthMode_twoPasswords(rName string, password1 string, password2 string) string { + return fmt.Sprintf(` +resource "aws_elasticache_user" "test" { + user_id = %[1]q + user_name = "username1" + access_string = "on ~app::* -@all +@read +@hash +@bitmap +@geo -setbit -bitfield -hset -hsetnx -hmset -hincrby -hincrbyfloat -hdel -bitop -geoadd -georadius -georadiusbymember" + engine = "REDIS" + + authentication_mode { + type = "password" + passwords = [%[2]q, %[3]q] + } +} +`, rName, password1, password2) +} + +func testAccUserConfigWithPasswordAuthMode_onePassword(rName string, password string) string { + return fmt.Sprintf(` +resource "aws_elasticache_user" "test" { + user_id = %[1]q + user_name = "username1" + access_string = "on ~app::* -@all +@read +@hash +@bitmap +@geo -setbit -bitfield -hset -hsetnx -hmset -hincrby -hincrbyfloat -hdel -bitop -geoadd -georadius -georadiusbymember" + engine = "REDIS" + + authentication_mode { + type = "password" + passwords = [%[2]q] + } +} +`, rName, password) } func testAccUserConfig_tags(rName, tagKey, tagValue string) string { - return acctest.ConfigCompose(acctest.ConfigAvailableAZsNoOptIn(), fmt.Sprintf(` + return fmt.Sprintf(` resource "aws_elasticache_user" "test" { user_id = %[1]q user_name = "username1" @@ -262,5 +451,5 @@ resource "aws_elasticache_user" "test" { %[2]s = %[3]q } } -`, rName, tagKey, tagValue)) +`, rName, tagKey, tagValue) } diff --git a/internal/service/elasticache/wait.go b/internal/service/elasticache/wait.go index dee0f218f051..637607dfd90c 100644 --- a/internal/service/elasticache/wait.go +++ b/internal/service/elasticache/wait.go @@ -18,9 +18,6 @@ const ( replicationGroupDeletedMinTimeout = 10 * time.Second replicationGroupDeletedDelay = 30 * time.Second - - UserActiveTimeout = 5 * time.Minute - UserDeletedTimeout = 5 * time.Minute ) // WaitReplicationGroupAvailable waits for a ReplicationGroup to return Available @@ -234,31 +231,3 @@ func waitGlobalReplicationGroupMemberDetached(ctx context.Context, conn *elastic } return nil, err } - -// WaitUserActive waits for an ElastiCache user to reach an active state after modifications -func WaitUserActive(ctx context.Context, conn *elasticache.ElastiCache, userId string) error { - stateConf := &resource.StateChangeConf{ - Pending: []string{UserStatusModifying}, - Target: []string{UserStatusActive}, - Refresh: StatusUser(ctx, conn, userId), - Timeout: UserActiveTimeout, - } - - _, err := stateConf.WaitForStateContext(ctx) - - return err -} - -// WaitUserDeleted waits for an ElastiCache user to be deleted -func WaitUserDeleted(ctx context.Context, conn *elasticache.ElastiCache, userId string) error { - stateConf := &resource.StateChangeConf{ - Pending: []string{UserStatusDeleting}, - Target: []string{}, - Refresh: StatusUser(ctx, conn, userId), - Timeout: UserDeletedTimeout, - } - - _, err := stateConf.WaitForStateContext(ctx) - - return err -} diff --git a/website/docs/r/elasticache_user.html.markdown b/website/docs/r/elasticache_user.html.markdown index bc9efdea0eee..fb3a20cda708 100644 --- a/website/docs/r/elasticache_user.html.markdown +++ b/website/docs/r/elasticache_user.html.markdown @@ -25,6 +25,33 @@ resource "aws_elasticache_user" "test" { } ``` +```terraform +resource "aws_elasticache_user" "test" { + user_id = "testUserId" + user_name = "testUserName" + access_string = "on ~* +@all" + engine = "REDIS" + + authentication_mode { + type = "iam" + } +} +``` + +```terraform +resource "aws_elasticache_user" "test" { + user_id = "testUserId" + user_name = "testUserName" + access_string = "on ~* +@all" + engine = "REDIS" + + authentication_mode { + type = "password" + passwords = ["password1", "password2"] + } +} +``` + ## Argument Reference The following arguments are required: @@ -36,10 +63,16 @@ The following arguments are required: The following arguments are optional: +* `authentication_mode` - (Optional) Denotes the user's authentication properties. Detailed below. * `no_password_required` - (Optional) Indicates a password is not required for this user. * `passwords` - (Optional) Passwords used for this user. You can create up to two passwords for each user. * `tags` - (Optional) A list of tags to be added to this resource. A tag is a key-value pair. +### authentication_mode Configuration Block + +* `passwords` - (Optional) Specifies the passwords to use for authentication if `type` is set to `password`. +* `type` - (Required) Specifies the authentication type. Possible options are: `password`, `no-password-required` or `iam`. + ## Attributes Reference In addition to all arguments above, the following attributes are exported: