Skip to content
Draft
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
70 changes: 52 additions & 18 deletions internal/service/elasticache/replication_group.go
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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
}
Expand All @@ -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") {
Expand Down Expand Up @@ -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()
Expand Down
188 changes: 188 additions & 0 deletions internal/service/elasticache/replication_group_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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() {
Expand Down Expand Up @@ -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),
Expand Down