diff --git a/internal/service/elasticache/serverless_cache.go b/internal/service/elasticache/serverless_cache.go index b44e3b90efef..42d037ee8b96 100644 --- a/internal/service/elasticache/serverless_cache.go +++ b/internal/service/elasticache/serverless_cache.go @@ -6,6 +6,7 @@ package elasticache import ( "context" "fmt" + "strconv" "time" "github.com/aws/aws-sdk-go-v2/aws" @@ -15,6 +16,7 @@ import ( "github.com/hashicorp/terraform-plugin-framework-timetypes/timetypes" "github.com/hashicorp/terraform-plugin-framework-validators/int64validator" "github.com/hashicorp/terraform-plugin-framework-validators/listvalidator" + "github.com/hashicorp/terraform-plugin-framework/path" "github.com/hashicorp/terraform-plugin-framework/resource" "github.com/hashicorp/terraform-plugin-framework/resource/schema" "github.com/hashicorp/terraform-plugin-framework/resource/schema/int64planmodifier" @@ -90,18 +92,23 @@ func (r *serverlessCacheResource) Schema(ctx context.Context, request resource.S names.AttrEngine: schema.StringAttribute{ Required: true, PlanModifiers: []planmodifier.String{ + stringplanmodifier.UseStateForUnknown(), stringplanmodifier.RequiresReplaceIf( func(ctx context.Context, req planmodifier.StringRequest, resp *stringplanmodifier.RequiresReplaceIfFuncResponse) { - // In-place updates are only supported for redis -> valkey + // In-place update support for redis -> valkey if req.StateValue.Equal(types.StringValue(engineRedis)) && req.PlanValue.Equal(types.StringValue(engineValkey)) { return } + // In-place updates support for valkey -> redis + if req.StateValue.Equal(types.StringValue(engineValkey)) && req.PlanValue.Equal(types.StringValue(engineRedis)) { + return + } // Any other change will force a replacement resp.RequiresReplace = true }, - "Engine modifications other than redis to valkey require a replacement", - "Engine modifications other than redis to valkey require a replacement", + "Engine modifications other than redis to valkey or valkey to redis require a replacement", + "Engine modifications other than redis to valkey or valkey to redis require a replacement", ), }, }, @@ -120,7 +127,33 @@ func (r *serverlessCacheResource) Schema(ctx context.Context, request resource.S Computed: true, PlanModifiers: []planmodifier.String{ stringplanmodifier.UseStateForUnknown(), - stringplanmodifier.RequiresReplace(), + stringplanmodifier.RequiresReplaceIf( + func(ctx context.Context, req planmodifier.StringRequest, resp *stringplanmodifier.RequiresReplaceIfFuncResponse) { + var engineVal types.String + req.Config.GetAttribute(ctx, path.Root(names.AttrEngine), &engineVal) + + stateFloatVal, err := strconv.ParseFloat(req.StateValue.ValueString(), 64) + if err != nil { + response.Diagnostics.AddError("incorrect major_engine_version format", err.Error()) + return + } + + planFloatVal, err := strconv.ParseFloat(req.PlanValue.ValueString(), 64) + if err != nil { + response.Diagnostics.AddError("incorrect major_engine_version format", err.Error()) + return + } + + if stateFloatVal < planFloatVal && engineVal.Equal(types.StringValue(engineValkey)) { + return + } + + // Any other change will force a replacement + resp.RequiresReplace = true + }, + "major_engine_version downgrade is not supported for valkey", + "major_engine_version downgrade is not supported for valkey", + ), }, }, names.AttrName: schema.StringAttribute{ diff --git a/internal/service/elasticache/serverless_cache_test.go b/internal/service/elasticache/serverless_cache_test.go index db67cd11b6c9..4f56978572ae 100644 --- a/internal/service/elasticache/serverless_cache_test.go +++ b/internal/service/elasticache/serverless_cache_test.go @@ -514,6 +514,67 @@ func TestAccElastiCacheServerlessCache_engine(t *testing.T) { testAccCheckServerlessCacheExists(ctx, t, resourceName, &v), resource.TestCheckResourceAttr(resourceName, names.AttrEngine, tfelasticache.EngineRedis), ), + ConfigPlanChecks: resource.ConfigPlanChecks{ + PreApply: []plancheck.PlanCheck{ + plancheck.ExpectResourceAction(resourceName, plancheck.ResourceActionUpdate), + }, + }, + }, + }, + }) +} + +func TestAccElastiCacheServerlessCache_valkeyMajorEngineVersion(t *testing.T) { + ctx := acctest.Context(t) + if testing.Short() { + t.Skip("skipping long-running test in short mode") + } + + rName := acctest.RandomWithPrefix(t, acctest.ResourcePrefix) + resourceName := "aws_elasticache_serverless_cache.test" + var v awstypes.ServerlessCache + + acctest.ParallelTest(ctx, t, resource.TestCase{ + PreCheck: func() { acctest.PreCheck(ctx, t) }, + ErrorCheck: acctest.ErrorCheck(t, names.ElastiCacheServiceID), + ProtoV5ProviderFactories: acctest.ProtoV5ProviderFactories, + CheckDestroy: resource.ComposeAggregateTestCheckFunc( + testAccCheckServerlessCacheDestroy(ctx, t), + ), + Steps: []resource.TestStep{ + { + Config: testAccServerlessCacheConfig_majorEngineVersion(rName, tfelasticache.EngineValkey, "7"), + Check: resource.ComposeAggregateTestCheckFunc( + testAccCheckServerlessCacheExists(ctx, t, resourceName, &v), + resource.TestCheckResourceAttr(resourceName, names.AttrEngine, tfelasticache.EngineValkey), + resource.TestCheckResourceAttr(resourceName, "major_engine_version", "7"), + ), + ConfigPlanChecks: resource.ConfigPlanChecks{ + PreApply: []plancheck.PlanCheck{ + plancheck.ExpectResourceAction(resourceName, plancheck.ResourceActionCreate), + }, + }, + }, + { + Config: testAccServerlessCacheConfig_majorEngineVersion(rName, tfelasticache.EngineValkey, "8"), + Check: resource.ComposeAggregateTestCheckFunc( + testAccCheckServerlessCacheExists(ctx, t, resourceName, &v), + resource.TestCheckResourceAttr(resourceName, names.AttrEngine, tfelasticache.EngineValkey), + resource.TestCheckResourceAttr(resourceName, "major_engine_version", "8"), + ), + ConfigPlanChecks: resource.ConfigPlanChecks{ + PreApply: []plancheck.PlanCheck{ + plancheck.ExpectResourceAction(resourceName, plancheck.ResourceActionUpdate), + }, + }, + }, + { + Config: testAccServerlessCacheConfig_majorEngineVersion(rName, tfelasticache.EngineValkey, "7"), + Check: resource.ComposeAggregateTestCheckFunc( + testAccCheckServerlessCacheExists(ctx, t, resourceName, &v), + resource.TestCheckResourceAttr(resourceName, names.AttrEngine, tfelasticache.EngineValkey), + resource.TestCheckResourceAttr(resourceName, "major_engine_version", "7"), + ), ConfigPlanChecks: resource.ConfigPlanChecks{ PreApply: []plancheck.PlanCheck{ plancheck.ExpectResourceAction(resourceName, plancheck.ResourceActionDestroyBeforeCreate), @@ -702,6 +763,16 @@ resource "aws_elasticache_serverless_cache" "test" { `, rName, engine) } +func testAccServerlessCacheConfig_majorEngineVersion(rName, engine, major_engine_version string) string { + return fmt.Sprintf(` +resource "aws_elasticache_serverless_cache" "test" { + name = %[1]q + engine = %[2]q + major_engine_version = %[3]q +} +`, rName, engine, major_engine_version) +} + func testAccServerlessCacheConfig_full(rName string) string { return acctest.ConfigCompose(acctest.ConfigVPCWithSubnets(rName, 2), fmt.Sprintf(` resource "aws_elasticache_serverless_cache" "test" {