88 "fmt"
99 "log"
1010 "reflect"
11+ "slices"
1112 "strings"
1213 "time"
1314
9192 }
9293
9394 replicaConfigurationKeys = []string {
95+ "replica_configuration.0.cascadable_replica" ,
9496 "replica_configuration.0.ca_certificate" ,
9597 "replica_configuration.0.client_certificate" ,
9698 "replica_configuration.0.client_key" ,
@@ -138,7 +140,13 @@ func ResourceSqlDatabaseInstance() *schema.Resource {
138140 CustomizeDiff : customdiff .All (
139141 tpgresource .DefaultProviderProject ,
140142 customdiff .ForceNewIfChange ("settings.0.disk_size" , compute .IsDiskShrinkage ),
141- customdiff .ForceNewIfChange ("master_instance_name" , isMasterInstanceNameSet ),
143+ customdiff .ForceNewIf ("master_instance_name" , func (_ context.Context , d * schema.ResourceDiff , meta interface {}) bool {
144+ // If we set master but this is not the new master of a switchover, require replacement and warn user.
145+ return ! isSwitchoverFromOldPrimarySide (d )
146+ }),
147+ customdiff .ForceNewIf ("replica_configuration.0.cascadable_replica" , func (_ context.Context , d * schema.ResourceDiff , meta interface {}) bool {
148+ return ! isSwitchoverFromOldPrimarySide (d )
149+ }),
142150 customdiff .IfValueChange ("instance_type" , isReplicaPromoteRequested , checkPromoteConfigurationsAndUpdateDiff ),
143151 privateNetworkCustomizeDiff ,
144152 pitrSupportDbCustomizeDiff ,
@@ -803,6 +811,12 @@ is set to true. Defaults to ZONAL.`,
803811 AtLeastOneOf : replicaConfigurationKeys ,
804812 Description : `PEM representation of the trusted CA's x509 certificate.` ,
805813 },
814+ "cascadable_replica" : {
815+ Type : schema .TypeBool ,
816+ Optional : true ,
817+ AtLeastOneOf : replicaConfigurationKeys ,
818+ Description : `Specifies if a SQL Server replica is a cascadable replica. A cascadable replica is a SQL Server cross region replica that supports replica(s) under it.` ,
819+ },
806820 "client_certificate" : {
807821 Type : schema .TypeString ,
808822 Optional : true ,
@@ -878,6 +892,15 @@ is set to true. Defaults to ZONAL.`,
878892 },
879893 Description : `The configuration for replication.` ,
880894 },
895+ "replica_names" : {
896+ Type : schema .TypeList ,
897+ Computed : true ,
898+ Optional : true ,
899+ Elem : & schema.Schema {
900+ Type : schema .TypeString ,
901+ },
902+ Description : `The replicas of the instance.` ,
903+ },
881904 "server_ca_cert" : {
882905 Type : schema .TypeList ,
883906 Computed : true ,
@@ -1320,7 +1343,8 @@ func expandReplicaConfiguration(configured []interface{}) *sqladmin.ReplicaConfi
13201343
13211344 _replicaConfiguration := configured [0 ].(map [string ]interface {})
13221345 return & sqladmin.ReplicaConfiguration {
1323- FailoverTarget : _replicaConfiguration ["failover_target" ].(bool ),
1346+ CascadableReplica : _replicaConfiguration ["cascadable_replica" ].(bool ),
1347+ FailoverTarget : _replicaConfiguration ["failover_target" ].(bool ),
13241348
13251349 // MysqlReplicaConfiguration has been flattened in the TF schema, so
13261350 // we'll keep it flat here instead of another expand method.
@@ -1648,6 +1672,10 @@ func resourceSqlDatabaseInstanceRead(d *schema.ResourceData, meta interface{}) e
16481672 if err := d .Set ("replica_configuration" , flattenReplicaConfiguration (instance .ReplicaConfiguration , d )); err != nil {
16491673 log .Printf ("[WARN] Failed to set SQL Database Instance Replica Configuration" )
16501674 }
1675+
1676+ if err := d .Set ("replica_names" , instance .ReplicaNames ); err != nil {
1677+ return fmt .Errorf ("Error setting replica_names: %w" , err )
1678+ }
16511679 ipAddresses := flattenIpAddresses (instance .IpAddresses )
16521680 if err := d .Set ("ip_address" , ipAddresses ); err != nil {
16531681 log .Printf ("[WARN] Failed to set SQL Database Instance IP Addresses" )
@@ -1702,6 +1730,14 @@ func resourceSqlDatabaseInstanceRead(d *schema.ResourceData, meta interface{}) e
17021730 return nil
17031731}
17041732
1733+ type replicaDRKind int
1734+
1735+ const (
1736+ replicaDRNone replicaDRKind = iota
1737+ replicaDRByPromote
1738+ replicaDRBySwitchover
1739+ )
1740+
17051741func resourceSqlDatabaseInstanceUpdate (d * schema.ResourceData , meta interface {}) error {
17061742 config := meta .(* transport_tpg.Config )
17071743 userAgent , err := tpgresource .GenerateUserAgentString (d , config .UserAgent )
@@ -1718,17 +1754,20 @@ func resourceSqlDatabaseInstanceUpdate(d *schema.ResourceData, meta interface{})
17181754 maintenance_version = v .(string )
17191755 }
17201756
1721- promoteReadReplicaRequired := false
1757+ replicaDRKind := replicaDRNone
17221758 if d .HasChange ("instance_type" ) {
17231759 oldInstanceType , newInstanceType := d .GetChange ("instance_type" )
17241760
17251761 if isReplicaPromoteRequested (nil , oldInstanceType , newInstanceType , nil ) {
1726- err = checkPromoteConfigurations (d )
1727- if err != nil {
1728- return err
1762+ if isSwitchoverRequested (d ) {
1763+ replicaDRKind = replicaDRBySwitchover
1764+ } else {
1765+ err = checkPromoteConfigurations (d )
1766+ if err != nil {
1767+ return err
1768+ }
1769+ replicaDRKind = replicaDRByPromote
17291770 }
1730-
1731- promoteReadReplicaRequired = true
17321771 }
17331772 }
17341773
@@ -1876,12 +1915,25 @@ func resourceSqlDatabaseInstanceUpdate(d *schema.ResourceData, meta interface{})
18761915 }
18771916 }
18781917
1879- if promoteReadReplicaRequired {
1880- err = transport_tpg .Retry (transport_tpg.RetryOptions {
1881- RetryFunc : func () (rerr error ) {
1918+ if replicaDRKind != replicaDRNone {
1919+ var retryFunc func () (rerr error )
1920+ switch replicaDRKind {
1921+ case replicaDRByPromote :
1922+ retryFunc = func () (rerr error ) {
18821923 op , rerr = config .NewSqlAdminClient (userAgent ).Instances .PromoteReplica (project , d .Get ("name" ).(string )).Do ()
18831924 return rerr
1884- },
1925+ }
1926+ case replicaDRBySwitchover :
1927+ retryFunc = func () (rerr error ) {
1928+ op , rerr = config .NewSqlAdminClient (userAgent ).Instances .Switchover (project , d .Get ("name" ).(string )).Do ()
1929+ return rerr
1930+ }
1931+ default :
1932+ return fmt .Errorf ("unknown replica DR scenario: %v" , replicaDRKind )
1933+ }
1934+
1935+ err = transport_tpg .Retry (transport_tpg.RetryOptions {
1936+ RetryFunc : retryFunc ,
18851937 Timeout : d .Timeout (schema .TimeoutUpdate ),
18861938 ErrorRetryPredicates : []transport_tpg.RetryErrorPredicateFunc {transport_tpg .IsSqlOperationInProgressError },
18871939 })
@@ -2342,7 +2394,8 @@ func flattenReplicaConfiguration(replicaConfiguration *sqladmin.ReplicaConfigura
23422394
23432395 if replicaConfiguration != nil {
23442396 data := map [string ]interface {}{
2345- "failover_target" : replicaConfiguration .FailoverTarget ,
2397+ "cascadable_replica" : replicaConfiguration .CascadableReplica ,
2398+ "failover_target" : replicaConfiguration .FailoverTarget ,
23462399
23472400 // Don't attempt to assign anything from replicaConfiguration.MysqlReplicaConfiguration,
23482401 // since those fields are set on create and then not stored. See description at
@@ -2529,6 +2582,20 @@ func isMasterInstanceNameSet(_ context.Context, oldMasterInstanceName interface{
25292582 return true
25302583}
25312584
2585+ func isSwitchoverRequested (d * schema.ResourceData ) bool {
2586+ originalPrimaryName , _ := d .GetChange ("master_instance_name" )
2587+ _ , newReplicaNames := d .GetChange ("replica_names" )
2588+ if ! slices .Contains (newReplicaNames .([]interface {}), originalPrimaryName ) {
2589+ return false
2590+ }
2591+ dbVersion := d .Get ("database_version" )
2592+ if ! strings .HasPrefix (dbVersion .(string ), "SQLSERVER" ) {
2593+ log .Printf ("[WARN] Switchover is only supported for SQL Server %q" , dbVersion )
2594+ return false
2595+ }
2596+ return true
2597+ }
2598+
25322599func isReplicaPromoteRequested (_ context.Context , oldInstanceType interface {}, newInstanceType interface {}, _ interface {}) bool {
25332600 oldInstanceType = oldInstanceType .(string )
25342601 newInstanceType = newInstanceType .(string )
@@ -2540,6 +2607,34 @@ func isReplicaPromoteRequested(_ context.Context, oldInstanceType interface{}, n
25402607 return false
25412608}
25422609
2610+ // Check if this resource change is the manual update done on old primary after a switchover. If true, no replacement is needed.
2611+ func isSwitchoverFromOldPrimarySide (d * schema.ResourceDiff ) bool {
2612+ dbVersion := d .Get ("database_version" )
2613+ if ! strings .HasPrefix (dbVersion .(string ), "SQLSERVER" ) {
2614+ log .Printf ("[WARN] Switchover is only supported for SQL Server %q" , dbVersion )
2615+ return false
2616+ }
2617+ oldInstanceType , newInstanceType := d .GetChange ("instance_type" )
2618+ oldReplicaNames , newReplicaNames := d .GetChange ("replica_names" )
2619+ _ , newMasterInstanceName := d .GetChange ("master_instance_name" )
2620+ _ , newReplicaConfiguration := d .GetChange ("replica_configuration" )
2621+ if len (newReplicaConfiguration .([]interface {})) != 1 || newReplicaConfiguration .([]interface {})[0 ] == nil {
2622+ return false
2623+ }
2624+ replicaConfiguration := newReplicaConfiguration .([]interface {})[0 ].(map [string ]interface {})
2625+ cascadableReplica , cascadableReplicaFieldExists := replicaConfiguration ["cascadable_replica" ]
2626+
2627+ instanceTypeChangedFromPrimaryToReplica := oldInstanceType .(string ) == "CLOUD_SQL_INSTANCE" && newInstanceType .(string ) == "READ_REPLICA_INSTANCE"
2628+ newMasterInOldReplicaNames := slices .Contains (oldReplicaNames .([]interface {}), newMasterInstanceName )
2629+ newMasterNotInNewReplicaNames := ! slices .Contains (newReplicaNames .([]interface {}), newMasterInstanceName )
2630+ isCascadableReplica := cascadableReplicaFieldExists && cascadableReplica .(bool )
2631+
2632+ return newMasterInstanceName != nil &&
2633+ instanceTypeChangedFromPrimaryToReplica &&
2634+ newMasterInOldReplicaNames && newMasterNotInNewReplicaNames &&
2635+ isCascadableReplica
2636+ }
2637+
25432638func checkPromoteConfigurations (d * schema.ResourceData ) error {
25442639 masterInstanceName := d .GetRawConfig ().GetAttr ("master_instance_name" )
25452640 replicaConfiguration := d .GetRawConfig ().GetAttr ("replica_configuration" ).AsValueSlice ()
0 commit comments