Skip to content

Commit e8a9b97

Browse files
Philip-JonanyPhilip Jonany
andauthored
Support SQL Server Switchover (after resolving email rebase conflicts) (GoogleCloudPlatform#12241)
Co-authored-by: Philip Jonany <[email protected]>
1 parent e4f5b0e commit e8a9b97

File tree

4 files changed

+493
-17
lines changed

4 files changed

+493
-17
lines changed

mmv1/third_party/terraform/services/sql/resource_sql_database_instance.go.tmpl

Lines changed: 107 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import (
66
"fmt"
77
"log"
88
"reflect"
9+
"slices"
910
"strings"
1011
"time"
1112

@@ -89,6 +90,7 @@ var (
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+
17031739
func 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+
25302597
func 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+
25412636
func 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

Comments
 (0)