@@ -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