diff --git a/.changelog/28689.txt b/.changelog/28689.txt new file mode 100644 index 00000000000..eef0b458763 --- /dev/null +++ b/.changelog/28689.txt @@ -0,0 +1,3 @@ +```release-note:bug +resource/aws_elasticache_user_group_association: Retry on `InvalidUserGroupState` errors to handle concurrent updates +``` diff --git a/internal/service/elasticache/user_group_association.go b/internal/service/elasticache/user_group_association.go index 35c8e706ac4..abed573a7e6 100644 --- a/internal/service/elasticache/user_group_association.go +++ b/internal/service/elasticache/user_group_association.go @@ -49,9 +49,11 @@ func resourceUserGroupAssociationCreate(d *schema.ResourceData, meta interface{} id := userGroupAssociationID(d.Get("user_group_id").(string), d.Get("user_id").(string)) - _, err := tfresource.RetryWhenNotFound(30*time.Second, func() (interface{}, error) { - return conn.ModifyUserGroup(input) - }) + _, err := tfresource.RetryWhenAWSErrCodeEquals(10*time.Minute, func() (interface{}, error) { + return tfresource.RetryWhenNotFound(30*time.Second, func() (interface{}, error) { + return conn.ModifyUserGroup(input) + }) + }, elasticache.ErrCodeInvalidUserGroupStateFault) if err != nil { return fmt.Errorf("creating ElastiCache User Group Association (%q): %w", id, err) @@ -129,7 +131,10 @@ func resourceUserGroupAssociationDelete(d *schema.ResourceData, meta interface{} UserIdsToRemove: aws.StringSlice([]string{d.Get("user_id").(string)}), } - _, err := conn.ModifyUserGroup(input) + _, err := tfresource.RetryWhenAWSErrCodeEquals(10*time.Minute, func() (interface{}, error) { + return conn.ModifyUserGroup(input) + }, elasticache.ErrCodeInvalidUserGroupStateFault) + if err != nil && !tfawserr.ErrMessageContains(err, elasticache.ErrCodeInvalidParameterValueException, "not a member") { return fmt.Errorf("deleting ElastiCache User Group Association (%q): %w", d.Id(), err) } diff --git a/internal/service/elasticache/user_group_association_test.go b/internal/service/elasticache/user_group_association_test.go index db1c1c622f5..75ae385d12a 100644 --- a/internal/service/elasticache/user_group_association_test.go +++ b/internal/service/elasticache/user_group_association_test.go @@ -103,6 +103,35 @@ func TestAccElastiCacheUserGroupAssociation_disappears(t *testing.T) { }) } +func TestAccElastiCacheUserGroupAssociation_multiple(t *testing.T) { + if testing.Short() { + t.Skip("skipping long-running test in short mode") + } + + rName := sdkacctest.RandomWithPrefix(acctest.ResourcePrefix) + resourceName1 := "aws_elasticache_user_group_association.test1" + resourceName2 := "aws_elasticache_user_group_association.test2" + + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { acctest.PreCheck(t) }, + ErrorCheck: acctest.ErrorCheck(t, elasticache.EndpointsID), + ProtoV5ProviderFactories: acctest.ProtoV5ProviderFactories, + CheckDestroy: testAccCheckUserGroupAssociationDestroy, + Steps: []resource.TestStep{ + { + Config: testAccUserGroupAssociationConfig_preMultiple(rName), + }, + { + Config: testAccUserGroupAssociationConfig_multiple(rName), + Check: resource.ComposeTestCheckFunc( + testAccCheckUserGroupAssociationExists(resourceName1), + testAccCheckUserGroupAssociationExists(resourceName2), + ), + }, + }, + }) +} + func testAccCheckUserGroupAssociationDestroy(s *terraform.State) error { return testAccCheckUserGroupAssociationDestroyWithProvider(s, acctest.Provider) } @@ -268,3 +297,41 @@ resource "aws_elasticache_user_group_association" "test" { } `, rName)) } + +func testAccUserGroupAssociationConfig_preMultiple(rName string) string { + return acctest.ConfigCompose( + testAccUserGroupAssociationBaseConfig(rName), + fmt.Sprintf(` +resource "aws_elasticache_user" "test3" { + user_id = "%[1]s-3" + user_name = "username2" + access_string = "on ~app::* -@all +@read +@hash +@bitmap +@geo -setbit -bitfield -hset -hsetnx -hmset -hincrby -hincrbyfloat -hdel -bitop -geoadd -georadius -georadiusbymember" + engine = "REDIS" + passwords = ["password123456789"] +} +`, rName)) +} + +func testAccUserGroupAssociationConfig_multiple(rName string) string { + return acctest.ConfigCompose( + testAccUserGroupAssociationBaseConfig(rName), + fmt.Sprintf(` +resource "aws_elasticache_user" "test3" { + user_id = "%[1]s-3" + user_name = "username2" + access_string = "on ~app::* -@all +@read +@hash +@bitmap +@geo -setbit -bitfield -hset -hsetnx -hmset -hincrby -hincrbyfloat -hdel -bitop -geoadd -georadius -georadiusbymember" + engine = "REDIS" + passwords = ["password123456789"] +} + +resource "aws_elasticache_user_group_association" "test1" { + user_group_id = aws_elasticache_user_group.test.user_group_id + user_id = aws_elasticache_user.test2.user_id +} + +resource "aws_elasticache_user_group_association" "test2" { + user_group_id = aws_elasticache_user_group.test.user_group_id + user_id = aws_elasticache_user.test3.user_id +} +`, rName)) +}