Skip to content
Merged
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
12 changes: 12 additions & 0 deletions .github/workflows/terraform_provider_pr.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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]
Expand Down
11 changes: 11 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down
109 changes: 82 additions & 27 deletions provider/pro/resource_rediscloud_pro_database.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand All @@ -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": {
Expand Down Expand Up @@ -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
})
Expand Down Expand Up @@ -449,31 +460,32 @@ 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))

// 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 {
Expand Down Expand Up @@ -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 {
Expand Down Expand Up @@ -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 {
Expand Down Expand Up @@ -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)
Expand All @@ -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
Expand All @@ -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...)
}
}
}
Expand All @@ -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) {
Expand Down Expand Up @@ -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
}
}
Expand Down
30 changes: 30 additions & 0 deletions provider/pro/resource_rediscloud_pro_database_validation_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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")
}
14 changes: 10 additions & 4 deletions provider/pro/resource_rediscloud_pro_subscription.go
Original file line number Diff line number Diff line change
Expand Up @@ -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": {
Expand Down
2 changes: 1 addition & 1 deletion provider/rediscloud_active_active_private_link_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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) {

Expand Down
8 changes: 4 additions & 4 deletions provider/rediscloud_active_active_subscription_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}
9 changes: 4 additions & 5 deletions provider/resource_rediscloud_pro_database_qpf_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"),
),
},
Expand Down
2 changes: 2 additions & 0 deletions provider/utils/wait.go
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down