66 "fmt"
77 "log"
88 "reflect"
9+ "slices"
910 "strings"
1011 "time"
1112
8990 }
9091
9192 replicaConfigurationKeys = []string{
93+ "replica_configuration.0.cascadable_replica",
9294 "replica_configuration.0.ca_certificate",
9395 "replica_configuration.0.client_certificate",
9496 "replica_configuration.0.client_key",
@@ -136,7 +138,13 @@ func ResourceSqlDatabaseInstance() *schema.Resource {
136138 CustomizeDiff: customdiff.All(
137139 tpgresource.DefaultProviderProject,
138140 customdiff.ForceNewIfChange("settings.0.disk_size", compute.IsDiskShrinkage),
139- customdiff.ForceNewIfChange("master_instance_name", isMasterInstanceNameSet),
141+ customdiff.ForceNewIf("master_instance_name", func(_ context.Context, d *schema.ResourceDiff, meta interface{}) bool {
142+ // If we set master but this is not the new master of a switchover, require replacement and warn user.
143+ return !isSwitchoverFromOldPrimarySide(d)
144+ }),
145+ customdiff.ForceNewIf("replica_configuration.0.cascadable_replica", func(_ context.Context, d *schema.ResourceDiff, meta interface{}) bool {
146+ return !isSwitchoverFromOldPrimarySide(d)
147+ }),
140148 customdiff.IfValueChange("instance_type", isReplicaPromoteRequested, checkPromoteConfigurationsAndUpdateDiff),
141149 privateNetworkCustomizeDiff,
142150 pitrSupportDbCustomizeDiff,
@@ -801,6 +809,12 @@ is set to true. Defaults to ZONAL.`,
801809 AtLeastOneOf: replicaConfigurationKeys,
802810 Description: `PEM representation of the trusted CA's x509 certificate.`,
803811 },
812+ "cascadable_replica": {
813+ Type: schema.TypeBool,
814+ Optional: true,
815+ AtLeastOneOf: replicaConfigurationKeys,
816+ 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.`,
817+ },
804818 "client_certificate": {
805819 Type: schema.TypeString,
806820 Optional: true,
@@ -876,6 +890,15 @@ is set to true. Defaults to ZONAL.`,
876890 },
877891 Description: `The configuration for replication.`,
878892 },
893+ "replica_names": {
894+ Type: schema.TypeList,
895+ Computed: true,
896+ Optional: true,
897+ Elem: &schema.Schema{
898+ Type: schema.TypeString,
899+ },
900+ Description: `The replicas of the instance.`,
901+ },
879902 "server_ca_cert": {
880903 Type: schema.TypeList,
881904 Computed: true,
@@ -1318,6 +1341,7 @@ func expandReplicaConfiguration(configured []interface{}) *sqladmin.ReplicaConfi
13181341
13191342 _replicaConfiguration := configured[0].(map[string]interface{})
13201343 return &sqladmin.ReplicaConfiguration{
1344+ CascadableReplica: _replicaConfiguration["cascadable_replica"].(bool),
13211345 FailoverTarget: _replicaConfiguration["failover_target"].(bool),
13221346
13231347 // MysqlReplicaConfiguration has been flattened in the TF schema, so
@@ -1646,6 +1670,10 @@ func resourceSqlDatabaseInstanceRead(d *schema.ResourceData, meta interface{}) e
16461670 if err := d.Set("replica_configuration", flattenReplicaConfiguration(instance.ReplicaConfiguration, d)); err != nil {
16471671 log.Printf("[WARN] Failed to set SQL Database Instance Replica Configuration")
16481672 }
1673+
1674+ if err := d.Set("replica_names", instance.ReplicaNames); err != nil {
1675+ return fmt.Errorf("Error setting replica_names: %w", err)
1676+ }
16491677 ipAddresses := flattenIpAddresses(instance.IpAddresses)
16501678 if err := d.Set("ip_address", ipAddresses); err != nil {
16511679 log.Printf("[WARN] Failed to set SQL Database Instance IP Addresses")
@@ -1700,6 +1728,14 @@ func resourceSqlDatabaseInstanceRead(d *schema.ResourceData, meta interface{}) e
17001728 return nil
17011729}
17021730
1731+ type replicaDRKind int
1732+
1733+ const (
1734+ replicaDRNone replicaDRKind = iota
1735+ replicaDRByPromote
1736+ replicaDRBySwitchover
1737+ )
1738+
17031739func resourceSqlDatabaseInstanceUpdate(d *schema.ResourceData, meta interface{}) error {
17041740 config := meta.(*transport_tpg.Config)
17051741 userAgent, err := tpgresource.GenerateUserAgentString(d, config.UserAgent)
@@ -1716,17 +1752,20 @@ func resourceSqlDatabaseInstanceUpdate(d *schema.ResourceData, meta interface{})
17161752 maintenance_version = v.(string)
17171753 }
17181754
1719- promoteReadReplicaRequired := false
1755+ replicaDRKind := replicaDRNone
17201756 if d.HasChange("instance_type") {
17211757 oldInstanceType, newInstanceType := d.GetChange("instance_type")
17221758
17231759 if isReplicaPromoteRequested(nil, oldInstanceType, newInstanceType, nil) {
1724- err = checkPromoteConfigurations(d)
1725- if err != nil {
1726- return err
1760+ if isSwitchoverRequested(d) {
1761+ replicaDRKind = replicaDRBySwitchover
1762+ } else {
1763+ err = checkPromoteConfigurations(d)
1764+ if err != nil {
1765+ return err
1766+ }
1767+ replicaDRKind = replicaDRByPromote
17271768 }
1728-
1729- promoteReadReplicaRequired = true
17301769 }
17311770 }
17321771
@@ -1874,12 +1913,25 @@ func resourceSqlDatabaseInstanceUpdate(d *schema.ResourceData, meta interface{})
18741913 }
18751914 }
18761915
1877- if promoteReadReplicaRequired {
1878- err = transport_tpg.Retry(transport_tpg.RetryOptions{
1879- RetryFunc: func() (rerr error) {
1916+ if replicaDRKind != replicaDRNone {
1917+ var retryFunc func() (rerr error)
1918+ switch replicaDRKind {
1919+ case replicaDRByPromote:
1920+ retryFunc = func() (rerr error) {
18801921 op, rerr = config.NewSqlAdminClient(userAgent).Instances.PromoteReplica(project, d.Get("name").(string)).Do()
18811922 return rerr
1882- },
1923+ }
1924+ case replicaDRBySwitchover:
1925+ retryFunc = func() (rerr error) {
1926+ op, rerr = config.NewSqlAdminClient(userAgent).Instances.Switchover(project, d.Get("name").(string)).Do()
1927+ return rerr
1928+ }
1929+ default:
1930+ return fmt.Errorf("unknown replica DR scenario: %v", replicaDRKind)
1931+ }
1932+
1933+ err = transport_tpg.Retry(transport_tpg.RetryOptions{
1934+ RetryFunc: retryFunc,
18831935 Timeout: d.Timeout(schema.TimeoutUpdate),
18841936 ErrorRetryPredicates: []transport_tpg.RetryErrorPredicateFunc{transport_tpg.IsSqlOperationInProgressError},
18851937 })
@@ -2340,6 +2392,7 @@ func flattenReplicaConfiguration(replicaConfiguration *sqladmin.ReplicaConfigura
23402392
23412393 if replicaConfiguration != nil {
23422394 data := map[string]interface{}{
2395+ "cascadable_replica": replicaConfiguration.CascadableReplica,
23432396 "failover_target": replicaConfiguration.FailoverTarget,
23442397
23452398 // Don't attempt to assign anything from replicaConfiguration.MysqlReplicaConfiguration,
@@ -2527,6 +2580,20 @@ func isMasterInstanceNameSet(_ context.Context, oldMasterInstanceName interface{
25272580 return true
25282581}
25292582
2583+ func isSwitchoverRequested(d *schema.ResourceData) bool {
2584+ originalPrimaryName, _ := d.GetChange("master_instance_name")
2585+ _, newReplicaNames := d.GetChange("replica_names")
2586+ if !slices.Contains(newReplicaNames.([]interface{}), originalPrimaryName) {
2587+ return false
2588+ }
2589+ dbVersion := d.Get("database_version")
2590+ if !strings.HasPrefix(dbVersion.(string), "SQLSERVER") {
2591+ log.Printf("[WARN] Switchover is only supported for SQL Server %q", dbVersion)
2592+ return false
2593+ }
2594+ return true
2595+ }
2596+
25302597func isReplicaPromoteRequested(_ context.Context, oldInstanceType interface{}, newInstanceType interface{}, _ interface{}) bool {
25312598 oldInstanceType = oldInstanceType.(string)
25322599 newInstanceType = newInstanceType.(string)
@@ -2538,6 +2605,34 @@ func isReplicaPromoteRequested(_ context.Context, oldInstanceType interface{}, n
25382605 return false
25392606}
25402607
2608+ // Check if this resource change is the manual update done on old primary after a switchover. If true, no replacement is needed.
2609+ func isSwitchoverFromOldPrimarySide(d *schema.ResourceDiff) bool {
2610+ dbVersion := d.Get("database_version")
2611+ if !strings.HasPrefix(dbVersion.(string), "SQLSERVER") {
2612+ log.Printf("[WARN] Switchover is only supported for SQL Server %q", dbVersion)
2613+ return false
2614+ }
2615+ oldInstanceType, newInstanceType := d.GetChange("instance_type")
2616+ oldReplicaNames, newReplicaNames := d.GetChange("replica_names")
2617+ _, newMasterInstanceName := d.GetChange("master_instance_name")
2618+ _, newReplicaConfiguration := d.GetChange("replica_configuration")
2619+ if len(newReplicaConfiguration.([]interface{})) != 1 || newReplicaConfiguration.([]interface{})[0] == nil{
2620+ return false;
2621+ }
2622+ replicaConfiguration := newReplicaConfiguration.([]interface{})[0].(map[string]interface{})
2623+ cascadableReplica, cascadableReplicaFieldExists := replicaConfiguration["cascadable_replica"]
2624+
2625+ instanceTypeChangedFromPrimaryToReplica := oldInstanceType.(string) == "CLOUD_SQL_INSTANCE" && newInstanceType.(string) == "READ_REPLICA_INSTANCE"
2626+ newMasterInOldReplicaNames := slices.Contains(oldReplicaNames.([]interface{}), newMasterInstanceName)
2627+ newMasterNotInNewReplicaNames := !slices.Contains(newReplicaNames.([]interface{}), newMasterInstanceName)
2628+ isCascadableReplica := cascadableReplicaFieldExists && cascadableReplica.(bool)
2629+
2630+ return newMasterInstanceName != nil &&
2631+ instanceTypeChangedFromPrimaryToReplica &&
2632+ newMasterInOldReplicaNames && newMasterNotInNewReplicaNames &&
2633+ isCascadableReplica
2634+ }
2635+
25412636func checkPromoteConfigurations(d *schema.ResourceData) error {
25422637 masterInstanceName := d.GetRawConfig().GetAttr("master_instance_name")
25432638 replicaConfiguration := d.GetRawConfig().GetAttr("replica_configuration").AsValueSlice()
@@ -2575,4 +2670,4 @@ func validatePromoteConfigurations(masterInstanceName cty.Value, replicaConfigur
25752670 return fmt.Errorf("Replica promote configuration check failed. Please remove replica_configuration and try again.")
25762671 }
25772672 return nil
2578- }
2673+ }
0 commit comments