Skip to content

Commit db89926

Browse files
Support SQL Server Switchover (after resolving email rebase conflicts) (#12241) (#8637)
[upstream:e8a9b97048f37ad92d777ec5ab705471fa4664ba] Signed-off-by: Modular Magician <[email protected]>
1 parent c23583e commit db89926

File tree

5 files changed

+497
-18
lines changed

5 files changed

+497
-18
lines changed

.changelog/12241.txt

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
```release-note:enhancement
2+
compute: added `replica_names` field to `sql_database_instance` resource
3+
```

google-beta/services/sql/resource_sql_database_instance.go

Lines changed: 108 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import (
88
"fmt"
99
"log"
1010
"reflect"
11+
"slices"
1112
"strings"
1213
"time"
1314

@@ -91,6 +92,7 @@ var (
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+
17051741
func 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+
25322599
func 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+
25432638
func checkPromoteConfigurations(d *schema.ResourceData) error {
25442639
masterInstanceName := d.GetRawConfig().GetAttr("master_instance_name")
25452640
replicaConfiguration := d.GetRawConfig().GetAttr("replica_configuration").AsValueSlice()

0 commit comments

Comments
 (0)