Skip to content

Commit 1d3b96a

Browse files
committed
chore: moving helper functions out to dedicated file
1 parent c489c82 commit 1d3b96a

File tree

3 files changed

+356
-344
lines changed

3 files changed

+356
-344
lines changed

provider/rediscloud_active_active_database_enable_default_user_acceptance_test.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@ func TestAccResourceRedisCloudActiveActiveDatabase_enableDefaultUser(t *testing.
2525
ProviderFactories: providerFactories,
2626
CheckDestroy: testAccCheckActiveActiveSubscriptionDestroy,
2727
Steps: []resource.TestStep{
28-
// Step 1: Create with global=true, regions inherit (THE BUG SCENARIO)
28+
// Step 1: Create with global=true, regions inherit
2929
{
3030
Config: fmt.Sprintf(
3131
utils.GetTestConfig(t, "./activeactive/testdata/enable_default_user_global_true_inherit.tf"),
@@ -68,7 +68,7 @@ func TestAccResourceRedisCloudActiveActiveDatabase_enableDefaultUser(t *testing.
6868
),
6969
},
7070

71-
// Step 3: Global false, specific regions enable (CRITICAL USE CASE)
71+
// Step 3: Global false, specific regions enable
7272
{
7373
Config: fmt.Sprintf(
7474
utils.GetTestConfig(t, "./activeactive/testdata/enable_default_user_global_false_region_true.tf"),

provider/resource_rediscloud_active_active_database.go

Lines changed: 0 additions & 342 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,6 @@ import (
1212
"github.com/RedisLabs/terraform-provider-rediscloud/provider/client"
1313
"github.com/RedisLabs/terraform-provider-rediscloud/provider/pro"
1414
"github.com/RedisLabs/terraform-provider-rediscloud/provider/utils"
15-
"github.com/hashicorp/go-cty/cty"
1615
"github.com/hashicorp/terraform-plugin-log/tflog"
1716
"github.com/hashicorp/terraform-plugin-sdk/v2/diag"
1817
"github.com/hashicorp/terraform-plugin-sdk/v2/helper/retry"
@@ -982,344 +981,3 @@ func flattenModulesToNames(modules []*databases.Module) []string {
982981
}
983982
return moduleNames
984983
}
985-
986-
// findRegionFieldInCtyValue navigates through a cty.Value structure to find a field
987-
// in the override_region Set for a specific region.
988-
//
989-
// This generic helper is used to check both raw config (GetRawConfig) and raw state (GetRawState)
990-
// without triggering SDK v2's TypeSet materialization that adds all schema fields with zero-values.
991-
//
992-
// Parameters:
993-
// - ctyVal: The cty.Value to search (from GetRawConfig or GetRawState)
994-
// - regionName: The name of the region to find (e.g., "us-east-1")
995-
// - fieldName: The field name to check within the region (e.g., "enable_default_user")
996-
//
997-
// Returns:
998-
// - fieldValue: The cty.Value of the field if found
999-
// - exists: true if the field was found and is not null, false otherwise
1000-
func findRegionFieldInCtyValue(ctyVal cty.Value, regionName string, fieldName string) (cty.Value, bool) {
1001-
if ctyVal.IsNull() {
1002-
return cty.NilVal, false
1003-
}
1004-
1005-
if !ctyVal.Type().HasAttribute("override_region") {
1006-
return cty.NilVal, false
1007-
}
1008-
1009-
overrideRegionAttr := ctyVal.GetAttr("override_region")
1010-
if overrideRegionAttr.IsNull() {
1011-
return cty.NilVal, false
1012-
}
1013-
1014-
if !overrideRegionAttr.Type().IsSetType() {
1015-
return cty.NilVal, false
1016-
}
1017-
1018-
iter := overrideRegionAttr.ElementIterator()
1019-
for iter.Next() {
1020-
_, regionVal := iter.Element()
1021-
1022-
// Check if this is the region we're looking for
1023-
if regionVal.Type().HasAttribute("name") {
1024-
nameAttr := regionVal.GetAttr("name")
1025-
if !nameAttr.IsNull() && nameAttr.AsString() == regionName {
1026-
// Found matching region, check for field
1027-
if regionVal.Type().HasAttribute(fieldName) {
1028-
fieldAttr := regionVal.GetAttr(fieldName)
1029-
if !fieldAttr.IsNull() {
1030-
return fieldAttr, true
1031-
}
1032-
}
1033-
return cty.NilVal, false
1034-
}
1035-
}
1036-
}
1037-
1038-
return cty.NilVal, false
1039-
}
1040-
1041-
// isEnableDefaultUserExplicitlySetInConfig checks if enable_default_user was explicitly
1042-
// set in the Terraform configuration for a specific region in the override_region block.
1043-
//
1044-
// This is used by the Update function to determine whether to send the field to the API.
1045-
// We only need this for Update operations where GetRawConfig() is available.
1046-
func isEnableDefaultUserExplicitlySetInConfig(d *schema.ResourceData, regionName string) bool {
1047-
_, exists := findRegionFieldInCtyValue(d.GetRawConfig(), regionName, "enable_default_user")
1048-
return exists
1049-
}
1050-
1051-
// isEnableDefaultUserInActualPersistedState checks if enable_default_user was in the ACTUAL
1052-
// persisted Terraform state (not the materialized Go map) for a specific region.
1053-
// Uses GetRawState to bypass TypeSet materialization that adds all fields with zero-values.
1054-
func isEnableDefaultUserInActualPersistedState(d *schema.ResourceData, regionName string) bool {
1055-
_, exists := findRegionFieldInCtyValue(d.GetRawState(), regionName, "enable_default_user")
1056-
return exists
1057-
}
1058-
1059-
// enableDefaultUserDecision encapsulates the decision result for whether to include
1060-
// enable_default_user in the region config.
1061-
type enableDefaultUserDecision struct {
1062-
shouldInclude bool
1063-
reason string
1064-
}
1065-
1066-
// decideEnableDefaultUserInclusion determines whether to include enable_default_user
1067-
// in the region state based on config/state context and API values.
1068-
//
1069-
// This implements the hybrid GetRawConfig/GetRawState strategy:
1070-
// - During Apply/Update (when GetRawConfig available): Check if explicitly set in config
1071-
// - During Refresh (when GetRawConfig unavailable): Check if was in persisted state
1072-
//
1073-
// Parameters:
1074-
// - d: ResourceData containing config and state
1075-
// - region: The region name (e.g., "us-east-1")
1076-
// - regionValue: The enable_default_user value from the API for this region
1077-
// - globalValue: The global_enable_default_user value from the API
1078-
//
1079-
// Returns:
1080-
// - Decision indicating whether to include the field and why
1081-
func decideEnableDefaultUserInclusion(
1082-
d *schema.ResourceData,
1083-
region string,
1084-
regionValue bool,
1085-
globalValue bool,
1086-
) enableDefaultUserDecision {
1087-
rawConfig := d.GetRawConfig()
1088-
valuesDiffer := regionValue != globalValue
1089-
1090-
// Try config-based detection first (available during Apply/Update)
1091-
if !rawConfig.IsNull() {
1092-
if isEnableDefaultUserExplicitlySetInConfig(d, region) {
1093-
return enableDefaultUserDecision{
1094-
shouldInclude: true,
1095-
reason: "explicitly set in config",
1096-
}
1097-
}
1098-
if valuesDiffer {
1099-
return enableDefaultUserDecision{
1100-
shouldInclude: true,
1101-
reason: "differs from global (API override)",
1102-
}
1103-
}
1104-
return enableDefaultUserDecision{
1105-
shouldInclude: false,
1106-
reason: "matches global (inherited)",
1107-
}
1108-
}
1109-
1110-
// Fall back to state-based detection (during Refresh)
1111-
wasInState := isEnableDefaultUserInActualPersistedState(d, region)
1112-
1113-
if wasInState {
1114-
reason := "was in state, preserving (user explicit)"
1115-
if valuesDiffer {
1116-
reason = "was in state, differs from global"
1117-
}
1118-
return enableDefaultUserDecision{
1119-
shouldInclude: true,
1120-
reason: reason,
1121-
}
1122-
}
1123-
1124-
if valuesDiffer {
1125-
return enableDefaultUserDecision{
1126-
shouldInclude: true,
1127-
reason: "not in state, but differs from global",
1128-
}
1129-
}
1130-
1131-
return enableDefaultUserDecision{
1132-
shouldInclude: false,
1133-
reason: "not in state, matches global (inherited)",
1134-
}
1135-
}
1136-
1137-
// filterDefaultSourceIPs removes default source IP values that should not be in state.
1138-
// Returns empty slice if IPs are default values (private ranges or 0.0.0.0/0).
1139-
//
1140-
// The API returns different defaults based on public_endpoint_access:
1141-
// - When public access disabled: Returns private IP ranges
1142-
// - When public access enabled: Returns ["0.0.0.0/0"]
1143-
// - When explicitly set by user: Returns user's values
1144-
//
1145-
// This filtering prevents drift from API-generated defaults.
1146-
func filterDefaultSourceIPs(apiSourceIPs []*string) []string {
1147-
privateIPRanges := []string{"10.0.0.0/8", "172.16.0.0/12", "192.168.0.0/16", "100.64.0.0/10"}
1148-
1149-
// Check for default public access ["0.0.0.0/0"]
1150-
if len(apiSourceIPs) == 1 && redis.StringValue(apiSourceIPs[0]) == "0.0.0.0/0" {
1151-
return []string{}
1152-
}
1153-
1154-
// Check for default private IP ranges
1155-
if len(apiSourceIPs) == len(privateIPRanges) {
1156-
isPrivateDefault := true
1157-
for i, ip := range apiSourceIPs {
1158-
if redis.StringValue(ip) != privateIPRanges[i] {
1159-
isPrivateDefault = false
1160-
break
1161-
}
1162-
}
1163-
if isPrivateDefault {
1164-
return []string{}
1165-
}
1166-
}
1167-
1168-
return redis.StringSliceValue(apiSourceIPs...)
1169-
}
1170-
1171-
// addSourceIPsIfOverridden adds override_global_source_ips to region config if it differs from global.
1172-
func addSourceIPsIfOverridden(regionDbConfig map[string]interface{}, d *schema.ResourceData, regionDb *databases.CrdbDatabase) {
1173-
sourceIPs := filterDefaultSourceIPs(regionDb.Security.SourceIPs)
1174-
if len(sourceIPs) == 0 {
1175-
return
1176-
}
1177-
1178-
globalSourceIPsPtrs := utils.SetToStringSlice(d.Get("global_source_ips").(*schema.Set))
1179-
globalSourceIPs := redis.StringSliceValue(globalSourceIPsPtrs...)
1180-
1181-
if !stringSlicesEqual(sourceIPs, globalSourceIPs) {
1182-
regionDbConfig["override_global_source_ips"] = sourceIPs
1183-
}
1184-
}
1185-
1186-
// addDataPersistenceIfOverridden adds override_global_data_persistence to region config if it differs from global.
1187-
func addDataPersistenceIfOverridden(
1188-
regionDbConfig map[string]interface{},
1189-
db *databases.ActiveActiveDatabase,
1190-
regionDb *databases.CrdbDatabase,
1191-
) {
1192-
if regionDb.DataPersistence != nil && db.GlobalDataPersistence != nil {
1193-
if redis.StringValue(regionDb.DataPersistence) != redis.StringValue(db.GlobalDataPersistence) {
1194-
regionDbConfig["override_global_data_persistence"] = regionDb.DataPersistence
1195-
}
1196-
}
1197-
}
1198-
1199-
// addPasswordIfOverridden adds override_global_password to region config if it differs from global.
1200-
func addPasswordIfOverridden(
1201-
regionDbConfig map[string]interface{},
1202-
db *databases.ActiveActiveDatabase,
1203-
regionDb *databases.CrdbDatabase,
1204-
) {
1205-
if regionDb.Security.Password != nil && db.GlobalPassword != nil {
1206-
if *regionDb.Security.Password != redis.StringValue(db.GlobalPassword) {
1207-
regionDbConfig["override_global_password"] = redis.StringValue(regionDb.Security.Password)
1208-
}
1209-
}
1210-
}
1211-
1212-
// addAlertsIfOverridden adds override_global_alert to region config if count differs from global.
1213-
// Note: Active-Active API doesn't return global alerts separately, so we compare counts.
1214-
func addAlertsIfOverridden(
1215-
regionDbConfig map[string]interface{},
1216-
d *schema.ResourceData,
1217-
regionDb *databases.CrdbDatabase,
1218-
) {
1219-
globalAlerts := d.Get("global_alert").(*schema.Set).List()
1220-
regionAlerts := pro.FlattenAlerts(regionDb.Alerts)
1221-
1222-
if len(globalAlerts) != len(regionAlerts) {
1223-
regionDbConfig["override_global_alert"] = regionAlerts
1224-
}
1225-
}
1226-
1227-
// addRemoteBackupIfConfigured adds remote_backup to region config if it exists in both API and state.
1228-
func addRemoteBackupIfConfigured(
1229-
regionDbConfig map[string]interface{},
1230-
regionDb *databases.CrdbDatabase,
1231-
stateOverrideRegion map[string]interface{},
1232-
) {
1233-
if regionDb.Backup == nil {
1234-
return
1235-
}
1236-
1237-
stateRemoteBackup := stateOverrideRegion["remote_backup"]
1238-
if stateRemoteBackup == nil {
1239-
return
1240-
}
1241-
1242-
stateRemoteBackupList := stateRemoteBackup.([]interface{})
1243-
if len(stateRemoteBackupList) > 0 {
1244-
regionDbConfig["remote_backup"] = pro.FlattenBackupPlan(regionDb.Backup, stateRemoteBackupList, "")
1245-
}
1246-
}
1247-
1248-
// addEnableDefaultUserIfNeeded applies hybrid GetRawConfig/GetRawState logic
1249-
// to determine if enable_default_user should be in state.
1250-
func addEnableDefaultUserIfNeeded(
1251-
ctx context.Context,
1252-
regionDbConfig map[string]interface{},
1253-
d *schema.ResourceData,
1254-
db *databases.ActiveActiveDatabase,
1255-
region string,
1256-
regionDb *databases.CrdbDatabase,
1257-
) {
1258-
if regionDb.Security.EnableDefaultUser == nil || db.GlobalEnableDefaultUser == nil {
1259-
return
1260-
}
1261-
1262-
regionEnableDefaultUser := redis.BoolValue(regionDb.Security.EnableDefaultUser)
1263-
globalEnableDefaultUser := redis.BoolValue(db.GlobalEnableDefaultUser)
1264-
1265-
decision := decideEnableDefaultUserInclusion(d, region, regionEnableDefaultUser, globalEnableDefaultUser)
1266-
1267-
if decision.shouldInclude {
1268-
regionDbConfig["enable_default_user"] = regionEnableDefaultUser
1269-
}
1270-
1271-
tflog.Debug(ctx, "Read: enable_default_user decision", map[string]interface{}{
1272-
"region": region,
1273-
"getRawConfigAvailable": !d.GetRawConfig().IsNull(),
1274-
"shouldInclude": decision.shouldInclude,
1275-
"value": regionEnableDefaultUser,
1276-
"reason": decision.reason,
1277-
})
1278-
}
1279-
1280-
// logRegionConfigBuilt logs the final region config for debugging.
1281-
func logRegionConfigBuilt(ctx context.Context, region string, regionDbConfig map[string]interface{}) {
1282-
tflog.Debug(ctx, "Read: Completed region config", map[string]interface{}{
1283-
"region": region,
1284-
"hasEnableDefaultUser": regionDbConfig["enable_default_user"] != nil,
1285-
"enableDefaultUserValue": regionDbConfig["enable_default_user"],
1286-
"hasOverrideGlobalSourceIps": regionDbConfig["override_global_source_ips"] != nil,
1287-
"hasOverrideGlobalDataPersistence": regionDbConfig["override_global_data_persistence"] != nil,
1288-
"hasOverrideGlobalPassword": regionDbConfig["override_global_password"] != nil,
1289-
"hasOverrideGlobalAlert": regionDbConfig["override_global_alert"] != nil,
1290-
"hasRemoteBackup": regionDbConfig["remote_backup"] != nil,
1291-
})
1292-
}
1293-
1294-
// buildRegionConfigFromAPIAndState orchestrates building region config from API and state.
1295-
// Each override field is handled by a dedicated helper function for clarity and maintainability.
1296-
func buildRegionConfigFromAPIAndState(ctx context.Context, d *schema.ResourceData, db *databases.ActiveActiveDatabase, region string, regionDb *databases.CrdbDatabase, stateOverrideRegion map[string]interface{}) map[string]interface{} {
1297-
regionDbConfig := map[string]interface{}{
1298-
"name": region,
1299-
}
1300-
1301-
// Handle each override field using dedicated helper functions
1302-
addSourceIPsIfOverridden(regionDbConfig, d, regionDb)
1303-
addDataPersistenceIfOverridden(regionDbConfig, db, regionDb)
1304-
addPasswordIfOverridden(regionDbConfig, db, regionDb)
1305-
addAlertsIfOverridden(regionDbConfig, d, regionDb)
1306-
addRemoteBackupIfConfigured(regionDbConfig, regionDb, stateOverrideRegion)
1307-
addEnableDefaultUserIfNeeded(ctx, regionDbConfig, d, db, region, regionDb)
1308-
1309-
logRegionConfigBuilt(ctx, region, regionDbConfig)
1310-
1311-
return regionDbConfig
1312-
}
1313-
1314-
// stringSlicesEqual compares two string slices for equality (order matters)
1315-
func stringSlicesEqual(a, b []string) bool {
1316-
if len(a) != len(b) {
1317-
return false
1318-
}
1319-
for i := range a {
1320-
if a[i] != b[i] {
1321-
return false
1322-
}
1323-
}
1324-
return true
1325-
}

0 commit comments

Comments
 (0)