diff --git a/.github/workflows/terraform_provider_pr.yml b/.github/workflows/terraform_provider_pr.yml index 1a41b313..8fadb84e 100644 --- a/.github/workflows/terraform_provider_pr.yml +++ b/.github/workflows/terraform_provider_pr.yml @@ -126,6 +126,7 @@ jobs: go_test_smoke_essentials_db: + if: false # Temporarily disabled - waiting on client fixes name: go test smoke essentials db needs: go_test_smoke_essentials_sub runs-on: ubuntu-latest @@ -193,6 +194,17 @@ jobs: go-version-file: go.mod - run: EXECUTE_TESTS=true make testacc TESTARGS='-run="TestAcc(RedisCloudProDatabaseBlockPublicEndpoints|ActiveActiveSubscriptionDatabaseBlockPublicEndpoints)"' + go_test_smoke_qpf: + name: go test smoke query performance factor + needs: [ go_build ] + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 + - uses: actions/setup-go@44694675825211faa026b3c33043df3e48a5fa00 # v6.0.0 + with: + go-version-file: go.mod + - run: EXECUTE_TESTS=true make testacc TESTARGS='-run="TestAccResourceRedisCloudProDatabase_qpf"' + go_unit_test: name: go unit test needs: [go_build] diff --git a/CHANGELOG.md b/CHANGELOG.md index bbc777cc..d71914aa 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,6 +3,17 @@ All notable changes to this project will be documented in this file. See updating [Changelog example here](https://keepachangelog.com/en/1.0.0/) + +# 2.7.1 (27th October 2025) + +## Fixed +- rediscloud_subscription_database: The query_performance_factor attribute can now be updated in-place without recreating the database. Previously, any changes to this attribute would force resource replacement. +- rediscloud_subscription_database (Redis 8.0+): Fixed drift detection issues where explicitly configured modules would incorrectly show as changes requiring resource replacement after upgrading to Redis 8.0 or higher. Modules are bundled + by default in Redis 8.0+, so configuration differences are now properly suppressed. +- rediscloud_subscription_database (Redis 8.0+): The warning for modules has been made more prominent. +- Support for a new pending status for subscription and database updates. +- Test Suite: Fixed incorrect file path references in acceptance tests. + # 2.7.0 (22nd October 2025) ## Added: diff --git a/provider/pro/resource_rediscloud_pro_database.go b/provider/pro/resource_rediscloud_pro_database.go index 5e72be5d..9e04c16d 100644 --- a/provider/pro/resource_rediscloud_pro_database.go +++ b/provider/pro/resource_rediscloud_pro_database.go @@ -230,7 +230,6 @@ func ResourceRedisCloudProDatabase() *schema.Resource { Type: schema.TypeString, Optional: true, Computed: true, - ForceNew: true, ValidateFunc: func(val interface{}, key string) (warns []string, errs []error) { v := val.(string) matched, err := regexp.MatchString(`^([2468])x$`, v) @@ -251,8 +250,9 @@ func ResourceRedisCloudProDatabase() *schema.Resource { ConfigMode: schema.SchemaConfigModeAttr, Optional: true, // The API doesn't allow updating/delete modules. Unless we recreate the database. - ForceNew: true, - MinItems: 1, + ForceNew: true, + MinItems: 1, + DiffSuppressFunc: modulesDiffSuppressFunc, Elem: &schema.Resource{ Schema: map[string]*schema.Schema{ "name": { @@ -418,6 +418,17 @@ func resourceRedisCloudProDatabaseCreate(ctx context.Context, d *schema.Resource createDatabase.RedisVersion = s }) + // Warn if modules are explicitly configured for Redis 8.0+ + var diags diag.Diagnostics + redisVersion := d.Get("redis_version").(string) + if shouldWarnRedis8Modules(redisVersion, len(createModules) > 0) { + diags = append(diags, diag.Diagnostic{ + Severity: diag.Warning, + Summary: "Modules are bundled by default in Redis 8.0+", + Detail: fmt.Sprintf("The 'modules' block is deprecated for Redis %s and later versions, as modules (RediSearch, RedisJSON, RedisBloom, RedisTimeSeries) are bundled by default. You should remove the 'modules' block from your configuration.", redisVersion), + }) + } + utils.SetStringIfNotEmpty(d, "password", func(s *string) { createDatabase.Password = s }) @@ -449,13 +460,13 @@ func resourceRedisCloudProDatabaseCreate(ctx context.Context, d *schema.Resource // Confirm sub is ready to accept a db request if err := utils.WaitForSubscriptionToBeActive(ctx, subId, api); err != nil { utils.SubscriptionMutex.Unlock(subId) - return diag.FromErr(err) + return append(diags, diag.FromErr(err)...) } dbId, err := api.Client.Database.Create(ctx, subId, createDatabase) if err != nil { utils.SubscriptionMutex.Unlock(subId) - return diag.FromErr(err) + return append(diags, diag.FromErr(err)...) } d.SetId(utils.BuildResourceId(subId, dbId)) @@ -463,17 +474,18 @@ func resourceRedisCloudProDatabaseCreate(ctx context.Context, d *schema.Resource // Confirm db + sub active status if err := utils.WaitForDatabaseToBeActive(ctx, subId, dbId, api); err != nil { utils.SubscriptionMutex.Unlock(subId) - return diag.FromErr(err) + return append(diags, diag.FromErr(err)...) } if err := utils.WaitForSubscriptionToBeActive(ctx, subId, api); err != nil { utils.SubscriptionMutex.Unlock(subId) - return diag.FromErr(err) + return append(diags, diag.FromErr(err)...) } // Some attributes on a database are not accessible by the subscription creation API. // Run the subscription update function to apply any additional changes to the databases, such as password, enableDefaultUser and so on. utils.SubscriptionMutex.Unlock(subId) - return resourceRedisCloudProDatabaseUpdate(ctx, d, meta) + updateDiags := resourceRedisCloudProDatabaseUpdate(ctx, d, meta) + return append(diags, updateDiags...) } func resourceRedisCloudProDatabaseRead(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics { @@ -559,7 +571,7 @@ func resourceRedisCloudProDatabaseRead(ctx context.Context, d *schema.ResourceDa // For Redis 8.0+, modules are bundled by default and returned by the API // Only set modules in state if they were explicitly defined in the config redisVersion := redis.StringValue(db.RedisVersion) - if redisVersion >= "8.0" { + if shouldSuppressModuleDiffsForRedis8(redisVersion) { // Only set modules if they were explicitly configured by the user if _, ok := d.GetOk("modules"); ok { if err := d.Set("modules", FlattenModules(db.Modules)); err != nil { @@ -714,6 +726,7 @@ func resourceRedisCloudProDatabaseDelete(ctx context.Context, d *schema.Resource func resourceRedisCloudProDatabaseUpdate(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics { api := meta.(*client.ApiClient) + var diags diag.Diagnostics _, dbId, err := ToDatabaseId(d.Id()) if err != nil { @@ -821,7 +834,7 @@ func resourceRedisCloudProDatabaseUpdate(ctx context.Context, d *schema.Resource update.ClientSSLCertificate = redis.String(clientSSLCertificate) } else if len(clientTLSCertificates) > 0 { utils.SubscriptionMutex.Unlock(subId) - return diag.Errorf("TLS certificates may not be provided while enable_tls is false") + return append(diags, diag.Errorf("TLS certificates may not be provided while enable_tls is false")...) } else { // Default: enable_tls=false, client_ssl_certificate="" update.EnableTls = redis.Bool(enableTLS) @@ -845,14 +858,25 @@ func resourceRedisCloudProDatabaseUpdate(ctx context.Context, d *schema.Resource update.RespVersion = redis.String(respVersion) } + // Warn if modules are explicitly configured for Redis 8.0+ + redisVersion := d.Get("redis_version").(string) + modules := d.Get("modules").(*schema.Set) + if shouldWarnRedis8Modules(redisVersion, modules.Len() > 0) { + diags = append(diags, diag.Diagnostic{ + Severity: diag.Warning, + Summary: "Modules are bundled by default in Redis 8.0+", + Detail: fmt.Sprintf("The 'modules' block is deprecated for Redis %s and later versions, as modules (RediSearch, RedisJSON, RedisBloom, RedisTimeSeries) are bundled by default. You should remove the 'modules' block from your configuration.", redisVersion), + }) + } + // Confirm sub + db are ready to accept a db request if err := utils.WaitForSubscriptionToBeActive(ctx, subId, api); err != nil { utils.SubscriptionMutex.Unlock(subId) - return diag.FromErr(err) + return append(diags, diag.FromErr(err)...) } if err := utils.WaitForDatabaseToBeActive(ctx, subId, dbId, api); err != nil { utils.SubscriptionMutex.Unlock(subId) - return diag.FromErr(err) + return append(diags, diag.FromErr(err)...) } // if redis_version has changed, then upgrade first @@ -863,11 +887,11 @@ func resourceRedisCloudProDatabaseUpdate(ctx context.Context, d *schema.Resource // if either version is blank, it could attempt to upgrade unnecessarily. // only upgrade when a known version goes to another known version if originalVersion.(string) != "" && newVersion.(string) != "" { - if diags, unlocked := upgradeRedisVersion(ctx, api, subId, dbId, newVersion.(string)); diags != nil { + if upgradeDiags, unlocked := upgradeRedisVersion(ctx, api, subId, dbId, newVersion.(string)); upgradeDiags != nil { if !unlocked { utils.SubscriptionMutex.Unlock(subId) } - return diags + return append(diags, upgradeDiags...) } } } @@ -876,26 +900,27 @@ func resourceRedisCloudProDatabaseUpdate(ctx context.Context, d *schema.Resource if err := api.Client.Database.Update(ctx, subId, dbId, update); err != nil { utils.SubscriptionMutex.Unlock(subId) - return diag.FromErr(err) + return append(diags, diag.FromErr(err)...) } // Confirm db + sub active status if err := utils.WaitForDatabaseToBeActive(ctx, subId, dbId, api); err != nil { utils.SubscriptionMutex.Unlock(subId) - return diag.FromErr(err) + return append(diags, diag.FromErr(err)...) } if err := utils.WaitForSubscriptionToBeActive(ctx, subId, api); err != nil { utils.SubscriptionMutex.Unlock(subId) - return diag.FromErr(err) + return append(diags, diag.FromErr(err)...) } // The Tags API is synchronous so we shouldn't have to wait for anything if err := WriteTags(ctx, api, subId, dbId, d); err != nil { - return diag.FromErr(err) + return append(diags, diag.FromErr(err)...) } utils.SubscriptionMutex.Unlock(subId) - return resourceRedisCloudProDatabaseRead(ctx, d, meta) + readDiags := resourceRedisCloudProDatabaseRead(ctx, d, meta) + return append(diags, readDiags...) } func upgradeRedisVersion(ctx context.Context, api *client.ApiClient, subId int, dbId int, newVersion string) (diag.Diagnostics, bool) { @@ -1068,19 +1093,49 @@ func shouldWarnRedis8Modules(version string, hasModules bool) bool { return false } +// shouldSuppressModuleDiffsForRedis8 checks if module diffs should be suppressed for Redis 8.0 or higher +// In Redis 8.0+, modules are bundled by default, so we should ignore changes to explicitly configured modules +func shouldSuppressModuleDiffsForRedis8(version string) bool { + if len(version) == 0 { + return false + } + majorVersionStr := strings.Split(version, ".")[0] + if majorVersion, err := strconv.Atoi(majorVersionStr); err == nil { + return majorVersion >= 8 + } + return false +} + +// modulesDiffSuppressFunc returns a DiffSuppressFunc that suppresses module diffs for Redis 8.0+ +// This prevents Terraform from showing module changes as "forces replacement" when upgrading to Redis 8.0+ +func modulesDiffSuppressFunc(k, oldValue, newValue string, d *schema.ResourceData) bool { + redisVersion, ok := d.GetOk("redis_version") + if !ok { + return false + } + version := redisVersion.(string) + return shouldSuppressModuleDiffsForRedis8(version) +} + func validateModulesForRedis8() schema.CustomizeDiffFunc { return func(ctx context.Context, diff *schema.ResourceDiff, meta interface{}) error { - redisVersion, versionExists := diff.GetOk("redis_version") - modules, modulesExists := diff.GetOkExists("modules") + // Check if modules are configured for Redis 8.0+ + redisVersionRaw, ok := diff.GetOk("redis_version") + if !ok { + return nil + } + redisVersion := redisVersionRaw.(string) - if versionExists && modulesExists { - version := redisVersion.(string) - moduleSet := modules.(*schema.Set) + modulesRaw, ok := diff.GetOk("modules") + if !ok { + return nil + } + modules := modulesRaw.(*schema.Set) - if shouldWarnRedis8Modules(version, moduleSet.Len() > 0) { - log.Printf("[WARN] Modules are bundled by default in Redis %s. You should remove the modules block as it is deprecated for this version.", version) - } + if shouldWarnRedis8Modules(redisVersion, modules.Len() > 0) { + log.Printf("[WARN] Modules are bundled by default in Redis %s and later versions. The 'modules' block is deprecated for Redis 8.0+ as modules (RediSearch, RedisJSON, RedisBloom, RedisTimeSeries) are bundled by default. You should remove the 'modules' block from your configuration.", redisVersion) } + return nil } } diff --git a/provider/pro/resource_rediscloud_pro_database_validation_test.go b/provider/pro/resource_rediscloud_pro_database_validation_test.go index a1a9c6a4..de32f786 100644 --- a/provider/pro/resource_rediscloud_pro_database_validation_test.go +++ b/provider/pro/resource_rediscloud_pro_database_validation_test.go @@ -65,3 +65,33 @@ func TestUnitShouldWarnRedis8Modules_Redis10WithModules(t *testing.T) { result := shouldWarnRedis8Modules("10.0.0", true) assert.True(t, result, "should warn for Redis 10.0.0 with modules (modules bundled in 8.0+)") } + +// TestUnitShouldSuppressModuleDiffsForRedis8_Redis8 tests that module diffs are suppressed for Redis 8.0 +func TestUnitShouldSuppressModuleDiffsForRedis8_Redis8(t *testing.T) { + result := shouldSuppressModuleDiffsForRedis8("8.0") + assert.True(t, result, "should suppress module diffs for Redis 8.0") +} + +// TestUnitShouldSuppressModuleDiffsForRedis8_Redis82 tests that module diffs are suppressed for Redis 8.2 +func TestUnitShouldSuppressModuleDiffsForRedis8_Redis82(t *testing.T) { + result := shouldSuppressModuleDiffsForRedis8("8.2") + assert.True(t, result, "should suppress module diffs for Redis 8.2") +} + +// TestUnitShouldSuppressModuleDiffsForRedis8_Redis9 tests that module diffs are suppressed for Redis 9.0 +func TestUnitShouldSuppressModuleDiffsForRedis8_Redis9(t *testing.T) { + result := shouldSuppressModuleDiffsForRedis8("9.0") + assert.True(t, result, "should suppress module diffs for Redis 9.0") +} + +// TestUnitShouldSuppressModuleDiffsForRedis8_Redis7 tests that module diffs are NOT suppressed for Redis 7.x +func TestUnitShouldSuppressModuleDiffsForRedis8_Redis7(t *testing.T) { + result := shouldSuppressModuleDiffsForRedis8("7.4") + assert.False(t, result, "should not suppress module diffs for Redis 7.4") +} + +// TestUnitShouldSuppressModuleDiffsForRedis8_Redis6 tests that module diffs are NOT suppressed for Redis 6.x +func TestUnitShouldSuppressModuleDiffsForRedis8_Redis6(t *testing.T) { + result := shouldSuppressModuleDiffsForRedis8("6.2") + assert.False(t, result, "should not suppress module diffs for Redis 6.2") +} diff --git a/provider/pro/resource_rediscloud_pro_subscription.go b/provider/pro/resource_rediscloud_pro_subscription.go index ff3a008a..91c7a4ac 100644 --- a/provider/pro/resource_rediscloud_pro_subscription.go +++ b/provider/pro/resource_rediscloud_pro_subscription.go @@ -371,12 +371,18 @@ func ResourceRedisCloudProSubscription() *schema.Resource { return false } - if old != new { - // The user is requesting a change - return false + // Suppress diff if user removes the deprecated attribute + if new == "" { + return true } - return true + // Suppress diff if no actual change + if old == new { + return true + } + + // User is requesting a version change - don't suppress + return false }, }, "maintenance_windows": { diff --git a/provider/rediscloud_active_active_private_link_test.go b/provider/rediscloud_active_active_private_link_test.go index 93620660..60675500 100644 --- a/provider/rediscloud_active_active_private_link_test.go +++ b/provider/rediscloud_active_active_private_link_test.go @@ -10,7 +10,7 @@ import ( "github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource" ) -const testActiveActivePrivateLinkConfigFile = "../privatelink/testdata/active_active_private_link.tf" +const testActiveActivePrivateLinkConfigFile = "./privatelink/testdata/active_active_private_link.tf" func TestAccResourceRedisCloudActiveActivePrivateLink_CRUDI(t *testing.T) { diff --git a/provider/rediscloud_active_active_subscription_test.go b/provider/rediscloud_active_active_subscription_test.go index 7617f586..907b1aeb 100644 --- a/provider/rediscloud_active_active_subscription_test.go +++ b/provider/rediscloud_active_active_subscription_test.go @@ -486,21 +486,21 @@ resource "rediscloud_active_active_subscription" "example" { ` func testAccResourceRedisCloudActiveActiveSubscription(t *testing.T, subscriptionName string) string { - content := utils.GetTestConfig(t, "./activeactive/testdata/testAccResourceRedisCloudActiveActiveSubscription.tf") + content := utils.GetTestConfig(t, "./activeactive/testdata/active_active_sub.tf") return fmt.Sprintf(content, subscriptionName) } func testAccResourceRedisCloudActiveActiveSubscriptionUpdate(t *testing.T, subscriptionName string, cloudProvider string) string { - content := utils.GetTestConfig(t, "./activeactive/testdata/testAccResourceRedisCloudActiveActiveSubscriptionUpdate.tf") + content := utils.GetTestConfig(t, "./activeactive/testdata/subscription_update.tf") return fmt.Sprintf(content, subscriptionName, cloudProvider) } func testAccResourceRedisCloudActiveActiveSubscriptionPublicEndpointDisabled(t *testing.T, subscriptionName string) string { - content := utils.GetTestConfig(t, "./activeactive/testdata/testAccResourceRedisCloudActiveActiveSubscription_PublicEndpointDisabled.tf") + content := utils.GetTestConfig(t, "./activeactive/testdata/public_endpoint_disabled.tf") return fmt.Sprintf(content, subscriptionName) } func testAccResourceRedisCloudActiveActiveSubscriptionPublicEndpointEnabled(t *testing.T, subscriptionName string) string { - content := utils.GetTestConfig(t, "./activeactive/testdata/testAccResourceRedisCloudActiveActiveSubscription_PublicEndpointEnabled.tf") + content := utils.GetTestConfig(t, "./activeactive/testdata/public_endpoint_enabled.tf") return fmt.Sprintf(content, subscriptionName) } diff --git a/provider/resource_rediscloud_pro_database_qpf_test.go b/provider/resource_rediscloud_pro_database_qpf_test.go index 4faeb814..b553fd58 100644 --- a/provider/resource_rediscloud_pro_database_qpf_test.go +++ b/provider/resource_rediscloud_pro_database_qpf_test.go @@ -130,12 +130,11 @@ func TestAccResourceRedisCloudProDatabase_qpf(t *testing.T) { ), }, - // Test plan to ensure query_performance_factor change forces a new resource + // Test that query_performance_factor can be updated without forcing replacement { - Config: formatDatabaseConfig(name, testCloudAccountName, password, "2x", `modules = [{ name = "RediSearch" }]`), - PlanOnly: true, // Runs terraform plan without applying - ExpectNonEmptyPlan: true, // Ensures that a change is detected - Check: resource.ComposeTestCheckFunc( + Config: formatDatabaseConfig(name, testCloudAccountName, password, "2x", `modules = [{ name = "RediSearch" }]`), + Check: resource.ComposeAggregateTestCheckFunc( + resource.TestCheckResourceAttr("rediscloud_subscription_database.example", "name", "example"), resource.TestCheckResourceAttr("rediscloud_subscription_database.example", "query_performance_factor", "2x"), ), }, diff --git a/provider/utils/wait.go b/provider/utils/wait.go index 92244de8..a183104e 100644 --- a/provider/utils/wait.go +++ b/provider/utils/wait.go @@ -52,6 +52,8 @@ func WaitForDatabaseToBeActive(ctx context.Context, subId, id int, api *client.A databases.StatusProxyPolicyChangeDraft, databases.StatusDynamicEndpointsCreationPending, databases.StatusActiveUpgradePending, + "bdb-update-pending", // Database update in progress. + // TODO replace with api model string in next release }, Target: []string{databases.StatusActive}, Timeout: SafetyTimeout,