diff --git a/internal/service/elasticache/replication_group.go b/internal/service/elasticache/replication_group.go index 8f4dd5d99527..790a06c42abd 100644 --- a/internal/service/elasticache/replication_group.go +++ b/internal/service/elasticache/replication_group.go @@ -87,7 +87,6 @@ func resourceReplicationGroup() *schema.Resource { Type: schema.TypeString, Optional: true, ValidateDiagFunc: enum.Validate[awstypes.AuthTokenUpdateStrategyType](), - RequiredWith: []string{"auth_token"}, }, names.AttrAutoMinorVersionUpgrade: { Type: nullable.TypeNullableBool, @@ -475,6 +474,7 @@ func resourceReplicationGroup() *schema.Resource { CustomizeDiff: customdiff.All( replicationGroupValidateMultiAZAutomaticFailover, customizeDiffEngineVersionForceNewOnDowngrade, + authTokenUpdateStrategyValidate, customizeDiffEngineForceNewOnDowngrade(), customdiff.ForceNewIf("node_group_configuration", func(ctx context.Context, d *schema.ResourceDiff, meta any) bool { // Only force new if user explicitly configured node_group_configuration and made meaningful changes @@ -1075,6 +1075,10 @@ func resourceReplicationGroupUpdate(ctx context.Context, d *schema.ResourceData, add, del := ns.Difference(os), os.Difference(ns) if add.Len() > 0 { + if d.HasChanges("auth_token", "auth_token_update_strategy") && awstypes.AuthTokenUpdateStrategyType(d.Get("auth_token_update_strategy").(string)) == awstypes.AuthTokenUpdateStrategyTypeDelete { + // transitioning to RBAC + input.AuthTokenUpdateStrategy = awstypes.AuthTokenUpdateStrategyType(d.Get("auth_token_update_strategy").(string)) + } input.UserGroupIdsToAdd = flex.ExpandStringValueSet(add) requestUpdate = true } @@ -1101,25 +1105,28 @@ func resourceReplicationGroupUpdate(ctx context.Context, d *schema.ResourceData, } if d.HasChanges("auth_token", "auth_token_update_strategy") { - authInput := elasticache.ModifyReplicationGroupInput{ - ApplyImmediately: aws.Bool(true), - AuthToken: aws.String(d.Get("auth_token").(string)), - AuthTokenUpdateStrategy: awstypes.AuthTokenUpdateStrategyType(d.Get("auth_token_update_strategy").(string)), - ReplicationGroupId: aws.String(d.Id()), - } - - updateFuncs = append(updateFuncs, func() error { - _, err := conn.ModifyReplicationGroup(ctx, &authInput) - // modifying to match out of band operations may result in this error - if errs.IsAErrorMessageContains[*awstypes.InvalidParameterCombinationException](err, "No modifications were requested") { - return nil + //AuthTokenUpdateStrategyTypeDelete only supported while transitioning to RBAC + if awstypes.AuthTokenUpdateStrategyType(d.Get("auth_token_update_strategy").(string)) != awstypes.AuthTokenUpdateStrategyTypeDelete { + authInput := elasticache.ModifyReplicationGroupInput{ + ApplyImmediately: aws.Bool(true), + AuthToken: aws.String(d.Get("auth_token").(string)), + AuthTokenUpdateStrategy: awstypes.AuthTokenUpdateStrategyType(d.Get("auth_token_update_strategy").(string)), + ReplicationGroupId: aws.String(d.Id()), } - if err != nil { - return fmt.Errorf("modifying ElastiCache Replication Group (%s) authentication: %w", d.Id(), err) - } - return nil - }) + updateFuncs = append(updateFuncs, func() error { + _, err := conn.ModifyReplicationGroup(ctx, &authInput) + // modifying to match out of band operations may result in this error + if errs.IsAErrorMessageContains[*awstypes.InvalidParameterCombinationException](err, "No modifications were requested") { + return nil + } + + if err != nil { + return fmt.Errorf("modifying ElastiCache Replication Group (%s) authentication: %w", d.Id(), err) + } + return nil + }) + } } if d.HasChange("num_cache_clusters") { @@ -1594,6 +1601,33 @@ func replicationGroupValidateAutomaticFailoverNumCacheClusters(_ context.Context return errors.New(`"num_cache_clusters": must be at least 2 if automatic_failover_enabled is true`) } +func authTokenUpdateStrategyValidate(_ context.Context, diff *schema.ResourceDiff, _ any) error { + strategy, strategyOk := diff.GetOk("auth_token_update_strategy") + _, tokenOk := diff.GetOk("auth_token") + + if strategyOk { + if awstypes.AuthTokenUpdateStrategyType(strategy.(string)) == awstypes.AuthTokenUpdateStrategyTypeDelete { + return nil + } else { + if tokenOk { + return nil + } + } + } + + if !tokenOk && !strategyOk { + return nil + } + + if tokenOk && !strategyOk { + return nil + } + + //AuthTokenUpdateStrategyTypeDelete can only be there while migrating to RBAC + //auth_token should not be provided in this case + return errors.New(`"auth_token_update_strategy": all of "auth_token,auth_token_update_strategy" must be specified. Except when "auth_token_update_strategy" is DELETE, allowed only when transitioning to RBAC`) +} + func suppressDiffIfBelongsToGlobalReplicationGroup(k, old, new string, d *schema.ResourceData) bool { _, has_global_replication_group := d.GetOk("global_replication_group_id") return has_global_replication_group && !d.IsNewResource() diff --git a/internal/service/elasticache/replication_group_test.go b/internal/service/elasticache/replication_group_test.go index 1bc5a4098f5b..e15a6bc24a6d 100644 --- a/internal/service/elasticache/replication_group_test.go +++ b/internal/service/elasticache/replication_group_test.go @@ -611,6 +611,64 @@ func TestAccElastiCacheReplicationGroup_updateUserGroups(t *testing.T) { }) } +func TestAccElastiCacheReplicationGroup_authToRbacMigration(t *testing.T) { + ctx := acctest.Context(t) + if testing.Short() { + t.Skip("skipping long-running test in short mode") + } + + var rg awstypes.ReplicationGroup + rName := acctest.RandomWithPrefix(t, acctest.ResourcePrefix) + resourceName := "aws_elasticache_replication_group.test" + token1 := sdkacctest.RandString(16) + userGroup := acctest.RandomWithPrefix(t, acctest.ResourcePrefix) + userId := acctest.RandomWithPrefix(t, acctest.ResourcePrefix) + + acctest.ParallelTest(ctx, t, resource.TestCase{ + PreCheck: func() { acctest.PreCheck(ctx, t) }, + ErrorCheck: acctest.ErrorCheck(t, names.ElastiCacheServiceID), + ProtoV5ProviderFactories: acctest.ProtoV5ProviderFactories, + CheckDestroy: testAccCheckReplicationGroupDestroy(ctx, t), + Steps: []resource.TestStep{ + { + Config: testAccReplicationGroupConfig_authTokenMigrationBase(rName), + Check: resource.ComposeAggregateTestCheckFunc( + testAccCheckReplicationGroupExists(ctx, t, resourceName, &rg), + ), + }, + { + ResourceName: resourceName, + ImportState: true, + ImportStateVerify: true, + ImportStateVerifyIgnore: []string{names.AttrApplyImmediately, "auth_token_update_strategy"}, + }, + { + // When adding an auth_token to a previously passwordless replication + // group, the SET strategy can be used. + Config: testAccReplicationGroupConfig_authTokenUpdateStrategyMigration(rName, token1, string(awstypes.AuthTokenUpdateStrategyTypeSet)), + Check: resource.ComposeAggregateTestCheckFunc( + testAccCheckReplicationGroupExists(ctx, t, resourceName, &rg), + resource.TestCheckResourceAttr(resourceName, "transit_encryption_enabled", acctest.CtTrue), + resource.TestCheckResourceAttr(resourceName, "auth_token", token1), + resource.TestCheckResourceAttr(resourceName, "auth_token_update_strategy", string(awstypes.AuthTokenUpdateStrategyTypeSet)), + ), + }, + { + // To migrate from AUTH to RBAC, modify request should not include the auth_token and + // need to keep DELETE auth_token_update_strategy + // Ref: https://docs.aws.amazon.com/AmazonElastiCache/latest/dg/Clusters.RBAC.html#Migrate-From-RBAC-to-Auth + Config: testAccReplicationGroupConfig_userGroupMigration(rName, userId, userGroup), + Check: resource.ComposeAggregateTestCheckFunc( + testAccCheckReplicationGroupExists(ctx, t, resourceName, &rg), + testAccCheckReplicationGroupUserGroup(ctx, t, resourceName, userGroup), + resource.TestCheckTypeSetElemAttr(resourceName, "user_group_ids.*", userGroup), + resource.TestCheckResourceAttr(resourceName, "auth_token_update_strategy", string(awstypes.AuthTokenUpdateStrategyTypeDelete)), + ), + }, + }, + }) +} + func TestAccElastiCacheReplicationGroup_updateNodeSize(t *testing.T) { ctx := acctest.Context(t) if testing.Short() { @@ -4667,6 +4725,136 @@ resource "aws_security_group" "test" { `, rName, authToken, updateStrategy)) } +func testAccReplicationGroupConfig_authTokenMigrationBase(rName string) string { + return acctest.ConfigCompose( + acctest.ConfigVPCWithSubnets(rName, 1), + fmt.Sprintf(` +resource "aws_elasticache_replication_group" "test" { + replication_group_id = %[1]q + description = "test description" + node_type = "cache.t2.micro" + num_cache_clusters = "1" + port = 6379 + subnet_group_name = aws_elasticache_subnet_group.test.name + security_group_ids = [aws_security_group.test.id] + engine_version = "6.2" + parameter_group_name = "default.redis6.x" +} + +resource "aws_elasticache_subnet_group" "test" { + name = %[1]q + subnet_ids = aws_subnet.test[*].id +} + +resource "aws_security_group" "test" { + name = %[1]q + description = "tf-test-security-group-descr" + vpc_id = aws_vpc.test.id + + ingress { + from_port = -1 + to_port = -1 + protocol = "icmp" + cidr_blocks = ["0.0.0.0/0"] + } +} +`, rName)) +} + +func testAccReplicationGroupConfig_authTokenUpdateStrategyMigration(rName string, authToken string, updateStrategy string) string { + return acctest.ConfigCompose( + acctest.ConfigVPCWithSubnets(rName, 1), + fmt.Sprintf(` +resource "aws_elasticache_replication_group" "test" { + replication_group_id = %[1]q + description = "test description" + node_type = "cache.t2.micro" + num_cache_clusters = "1" + port = 6379 + subnet_group_name = aws_elasticache_subnet_group.test.name + security_group_ids = [aws_security_group.test.id] + engine_version = "6.2" + parameter_group_name = "default.redis6.x" + transit_encryption_enabled = true + auth_token = %[2]q + auth_token_update_strategy = %[3]q + apply_immediately = true +} + +resource "aws_elasticache_subnet_group" "test" { + name = %[1]q + subnet_ids = aws_subnet.test[*].id +} + +resource "aws_security_group" "test" { + name = %[1]q + description = "tf-test-security-group-descr" + vpc_id = aws_vpc.test.id + + ingress { + from_port = -1 + to_port = -1 + protocol = "icmp" + cidr_blocks = ["0.0.0.0/0"] + } +} +`, rName, authToken, updateStrategy)) +} + +func testAccReplicationGroupConfig_userGroupMigration(rName string, userId string, userGroup string) string { + return acctest.ConfigCompose( + acctest.ConfigVPCWithSubnets(rName, 1), + fmt.Sprintf(` +resource "aws_elasticache_user" "test" { + user_id = %[2]q + user_name = "default" + 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" "test" { + user_group_id = %[3]q + engine = "redis" + user_ids = [aws_elasticache_user.test.user_id] +} + +resource "aws_elasticache_replication_group" "test" { + replication_group_id = %[1]q + description = "test description" + node_type = "cache.t2.micro" + num_cache_clusters = "1" + port = 6379 + subnet_group_name = aws_elasticache_subnet_group.test.name + security_group_ids = [aws_security_group.test.id] + engine_version = "6.2" + parameter_group_name = "default.redis6.x" + transit_encryption_enabled = true + auth_token_update_strategy = "DELETE" + user_group_ids = [aws_elasticache_user_group.test.id] + apply_immediately = true +} + +resource "aws_elasticache_subnet_group" "test" { + name = %[1]q + subnet_ids = aws_subnet.test[*].id +} + +resource "aws_security_group" "test" { + name = %[1]q + description = "tf-test-security-group-descr" + vpc_id = aws_vpc.test.id + + ingress { + from_port = -1 + to_port = -1 + protocol = "icmp" + cidr_blocks = ["0.0.0.0/0"] + } +} +`, rName, userId, userGroup)) +} + func testAccReplicationGroupConfig_numberCacheClusters(rName string, numberCacheClusters int) string { return acctest.ConfigCompose( acctest.ConfigVPCWithSubnets(rName, 2),