diff --git a/mmv1/third_party/terraform/resources/resource_sql_database_instance.go.erb b/mmv1/third_party/terraform/resources/resource_sql_database_instance.go.erb index 7007df19d3c2..39df21521ae3 100644 --- a/mmv1/third_party/terraform/resources/resource_sql_database_instance.go.erb +++ b/mmv1/third_party/terraform/resources/resource_sql_database_instance.go.erb @@ -9,6 +9,7 @@ import ( "strings" "time" + "github.com/hashicorp/go-cty/cty" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/customdiff" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" @@ -111,6 +112,8 @@ func resourceSqlDatabaseInstance() *schema.Resource { CustomizeDiff: customdiff.All( customdiff.ForceNewIfChange("settings.0.disk_size", isDiskShrinkage), + customdiff.ForceNewIfChange("master_instance_name", isMasterInstanceNameSet), + customdiff.IfValueChange("instance_type", isReplicaPromoteRequested, checkPromoteConfigurationsAndUpdateDiff), privateNetworkCustomizeDiff, pitrSupportDbCustomizeDiff, ), @@ -671,7 +674,6 @@ is set to true. Defaults to ZONAL.`, Type: schema.TypeString, Optional: true, Computed: true, - ForceNew: true, Description: `The name of the instance that will act as the master in the replication setup. Note, this requires the master to have binary_log_enabled set, as well as existing backups.`, }, @@ -686,6 +688,7 @@ is set to true. Defaults to ZONAL.`, "instance_type": { Type: schema.TypeString, Computed: true, + Optional: true, Description: `The type of the instance. The valid values are:- 'SQL_INSTANCE_TYPE_UNSPECIFIED', 'CLOUD_SQL_INSTANCE', 'ON_PREMISES_INSTANCE' and 'READ_REPLICA_INSTANCE'.`, }, @@ -1506,6 +1509,20 @@ func resourceSqlDatabaseInstanceUpdate(d *schema.ResourceData, meta interface{}) maintenance_version = v.(string) } + promoteReadReplicaRequired := false + if d.HasChange("instance_type") { + oldInstanceType, newInstanceType := d.GetChange("instance_type") + + if isReplicaPromoteRequested(nil, oldInstanceType, newInstanceType, nil) { + err = checkPromoteConfigurations(d) + if err != nil { + return err + } + + promoteReadReplicaRequired = true + } + } + desiredSetting := d.Get("settings") var op *sqladmin.Operation var instance *sqladmin.DatabaseInstance @@ -1613,6 +1630,24 @@ func resourceSqlDatabaseInstanceUpdate(d *schema.ResourceData, meta interface{}) } } + if promoteReadReplicaRequired { + err = retryTimeDuration(func() (rerr error) { + op, rerr = config.NewSqlAdminClient(userAgent).Instances.PromoteReplica(project, d.Get("name").(string)).Do() + return rerr + }, d.Timeout(schema.TimeoutUpdate), isSqlOperationInProgressError) + if err != nil { + return fmt.Errorf("Error, failed to promote read replica instance as primary stand-alone %s: %s", instance.Name, err) + } + err = sqlAdminOperationWaitTime(config, op, project, "Promote Instance", userAgent, d.Timeout(schema.TimeoutUpdate)) + if err != nil { + return err + } + err = resourceSqlDatabaseInstanceRead(d, meta) + if err != nil { + return err + } + } + s := d.Get("settings") instance = &sqladmin.DatabaseInstance{ Settings: expandSqlDatabaseInstanceSettings(desiredSetting.([]interface{})), @@ -2110,3 +2145,62 @@ func caseDiffDashSuppress(_, old, new string, _ *schema.ResourceData) bool { postReplaceNew := strings.Replace(new, "-", "_", -1) return strings.ToUpper(postReplaceNew) == strings.ToUpper(old) } + +func isMasterInstanceNameSet(_ context.Context, oldMasterInstanceName interface{}, newMasterInstanceName interface{}, _ interface{}) bool { + new := newMasterInstanceName.(string) + if new == "" { + return false + } + + return true +} + +func isReplicaPromoteRequested(_ context.Context, oldInstanceType interface{}, newInstanceType interface{}, _ interface{}) bool { + oldInstanceType = oldInstanceType.(string) + newInstanceType = newInstanceType.(string) + + if newInstanceType == "CLOUD_SQL_INSTANCE" && oldInstanceType == "READ_REPLICA_INSTANCE" { + return true + } + + return false +} + +func checkPromoteConfigurations(d *schema.ResourceData) error { + masterInstanceName := d.GetRawConfig().GetAttr("master_instance_name") + replicaConfiguration := d.GetRawConfig().GetAttr("replica_configuration").AsValueSlice() + + return validatePromoteConfigurations(masterInstanceName, replicaConfiguration) +} + +func checkPromoteConfigurationsAndUpdateDiff(_ context.Context, diff *schema.ResourceDiff, _ interface{}) error { + masterInstanceName := diff.GetRawConfig().GetAttr("master_instance_name") + replicaConfiguration := diff.GetRawConfig().GetAttr("replica_configuration").AsValueSlice() + + err := validatePromoteConfigurations(masterInstanceName, replicaConfiguration) + if (err != nil) { + return err + } + + err = diff.SetNew("master_instance_name", nil) + if err != nil { + return err + } + + err = diff.SetNew("replica_configuration", nil) + if err != nil { + return err + } + return nil +} + +func validatePromoteConfigurations(masterInstanceName cty.Value, replicaConfigurations []cty.Value) error { + if !masterInstanceName.IsNull() { + return fmt.Errorf("Replica promote configuration check failed. Please remove master_instance_name and try again.") + } + + if len(replicaConfigurations) != 0 { + return fmt.Errorf("Replica promote configuration check failed. Please remove replica_configuration and try again.") + } + return nil +} diff --git a/mmv1/third_party/terraform/tests/resource_sql_database_instance_test.go.erb b/mmv1/third_party/terraform/tests/resource_sql_database_instance_test.go.erb index 2279785e680b..f705bc118249 100644 --- a/mmv1/third_party/terraform/tests/resource_sql_database_instance_test.go.erb +++ b/mmv1/third_party/terraform/tests/resource_sql_database_instance_test.go.erb @@ -1530,7 +1530,231 @@ func TestAccSqlDatabaseInstance_rootPasswordShouldBeUpdatable(t *testing.T) { }) } +func TestAccSqlDatabaseInstance_ReplicaPromoteSuccessful(t *testing.T) { + t.Parallel() + + databaseName := "sql-instance-test-" + randString(t, 10) + failoverName := "sql-instance-test-failover-" + randString(t, 10) + vcrTest(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + Providers: testAccProviders, + CheckDestroy: testAccSqlDatabaseInstanceDestroyProducer(t), + Steps: []resource.TestStep{ + { + Config: testGoogleSqlDatabaseInstanceConfig_withReplica(databaseName, failoverName), + }, + { + ResourceName: "google_sql_database_instance.instance", + ImportState: true, + ImportStateVerify: true, + ImportStateVerifyIgnore: []string{"deletion_protection"}, + }, + { + ResourceName: "google_sql_database_instance.instance-failover", + ImportState: true, + ImportStateVerify: true, + ImportStateVerifyIgnore: []string{"deletion_protection"}, + }, + { + Config: googleSqlDatabaseInstance_replicaPromote(databaseName, failoverName), + Check: resource.ComposeTestCheckFunc(checkPromoteReplicaConfigurations("google_sql_database_instance.instance-failover")), + }, + { + ResourceName: "google_sql_database_instance.instance", + ImportState: true, + ImportStateVerify: true, + ImportStateVerifyIgnore: []string{"deletion_protection"}, + }, + { + ResourceName: "google_sql_database_instance.instance-failover", + ImportState: true, + ImportStateVerify: true, + ImportStateVerifyIgnore: []string{"deletion_protection"}, + }, + }, + }) +} + +func TestAccSqlDatabaseInstance_ReplicaPromoteFailedWithMasterInstanceNamePresent(t *testing.T) { + t.Parallel() + databaseName := "sql-instance-test-" + randString(t, 10) + failoverName := "sql-instance-test-failover-" + randString(t, 10) + vcrTest(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + Providers: testAccProviders, + CheckDestroy: testAccSqlDatabaseInstanceDestroyProducer(t), + Steps: []resource.TestStep{ + { + Config: testGoogleSqlDatabaseInstanceConfig_withReplica(databaseName, failoverName), + }, + { + ResourceName: "google_sql_database_instance.instance", + ImportState: true, + ImportStateVerify: true, + ImportStateVerifyIgnore: []string{"deletion_protection"}, + }, + { + ResourceName: "google_sql_database_instance.instance-failover", + ImportState: true, + ImportStateVerify: true, + ImportStateVerifyIgnore: []string{"deletion_protection"}, + }, + { + Config: googleSqlDatabaseInstance_replicaPromoteWithMasterInstanceName(databaseName, failoverName), + ExpectError: regexp.MustCompile("Replica promote configuration check failed. Please remove master_instance_name and try again."), + Check: resource.ComposeTestCheckFunc(checkPromoteReplicaSkipConfigurations("google_sql_database_instance.instance-failover")), + }, + { + ResourceName: "google_sql_database_instance.instance", + ImportState: true, + ImportStateVerify: true, + ImportStateVerifyIgnore: []string{"deletion_protection"}, + }, + { + ResourceName: "google_sql_database_instance.instance-failover", + ImportState: true, + ImportStateVerify: true, + ImportStateVerifyIgnore: []string{"deletion_protection"}, + }, + }, + }) +} + +func TestAccSqlDatabaseInstance_ReplicaPromoteFailedWithReplicaConfigurationPresent(t *testing.T) { + t.Parallel() + databaseName := "sql-instance-test-" + randString(t, 10) + failoverName := "sql-instance-test-failover-" + randString(t, 10) + vcrTest(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + Providers: testAccProviders, + CheckDestroy: testAccSqlDatabaseInstanceDestroyProducer(t), + Steps: []resource.TestStep{ + { + Config: testGoogleSqlDatabaseInstanceConfig_withReplica(databaseName, failoverName), + }, + { + ResourceName: "google_sql_database_instance.instance", + ImportState: true, + ImportStateVerify: true, + ImportStateVerifyIgnore: []string{"deletion_protection"}, + }, + { + ResourceName: "google_sql_database_instance.instance-failover", + ImportState: true, + ImportStateVerify: true, + ImportStateVerifyIgnore: []string{"deletion_protection"}, + }, + { + Config: googleSqlDatabaseInstance_replicaPromoteWithReplicaConfiguration(databaseName, failoverName), + ExpectError: regexp.MustCompile("Replica promote configuration check failed. Please remove replica_configuration and try again."), + Check: resource.ComposeTestCheckFunc(checkPromoteReplicaSkipConfigurations("google_sql_database_instance.instance-failover")), + }, + { + ResourceName: "google_sql_database_instance.instance", + ImportState: true, + ImportStateVerify: true, + ImportStateVerifyIgnore: []string{"deletion_protection"}, + }, + { + ResourceName: "google_sql_database_instance.instance-failover", + ImportState: true, + ImportStateVerify: true, + ImportStateVerifyIgnore: []string{"deletion_protection"}, + }, + }, + }) +} + +func TestAccSqlDatabaseInstance_ReplicaPromoteFailedWithMasterInstanceNameAndReplicaConfigurationPresent(t *testing.T) { + t.Parallel() + + databaseName := "sql-instance-test-" + randString(t, 10) + failoverName := "sql-instance-test-failover-" + randString(t, 10) + vcrTest(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + Providers: testAccProviders, + CheckDestroy: testAccSqlDatabaseInstanceDestroyProducer(t), + Steps: []resource.TestStep{ + { + Config: testGoogleSqlDatabaseInstanceConfig_withReplica(databaseName, failoverName), + }, + { + ResourceName: "google_sql_database_instance.instance", + ImportState: true, + ImportStateVerify: true, + ImportStateVerifyIgnore: []string{"deletion_protection"}, + }, + { + ResourceName: "google_sql_database_instance.instance-failover", + ImportState: true, + ImportStateVerify: true, + ImportStateVerifyIgnore: []string{"deletion_protection"}, + }, + { + Config: googleSqlDatabaseInstance_replicaPromoteWithMasterInstanceNameAndReplicaConfiguration(databaseName, failoverName), + ExpectError: regexp.MustCompile("Replica promote configuration check failed. Please remove master_instance_name and try again."), + }, + { + ResourceName: "google_sql_database_instance.instance", + ImportState: true, + ImportStateVerify: true, + ImportStateVerifyIgnore: []string{"deletion_protection"}, + }, + { + ResourceName: "google_sql_database_instance.instance-failover", + ImportState: true, + ImportStateVerify: true, + ImportStateVerifyIgnore: []string{"deletion_protection"}, + }, + }, + }) +} + +func TestAccSqlDatabaseInstance_ReplicaPromoteSkippedWithNoMasterInstanceNameAndNoReplicaConfigurationPresent(t *testing.T) { + t.Parallel() + + databaseName := "sql-instance-test-" + randString(t, 10) + failoverName := "sql-instance-test-failover-" + randString(t, 10) + vcrTest(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + Providers: testAccProviders, + CheckDestroy: testAccSqlDatabaseInstanceDestroyProducer(t), + Steps: []resource.TestStep{ + { + Config: testGoogleSqlDatabaseInstanceConfig_withReplica(databaseName, failoverName), + }, + { + ResourceName: "google_sql_database_instance.instance", + ImportState: true, + ImportStateVerify: true, + ImportStateVerifyIgnore: []string{"deletion_protection"}, + }, + { + ResourceName: "google_sql_database_instance.instance-failover", + ImportState: true, + ImportStateVerify: true, + ImportStateVerifyIgnore: []string{"deletion_protection"}, + }, + { + Config: googleSqlDatabaseInstance_replicaPromoteSkippedWithNoMasterInstanceNameAndNoReplicaConfiguration(databaseName, failoverName), + Check: resource.ComposeTestCheckFunc(checkPromoteReplicaSkipConfigurations("google_sql_database_instance.instance-failover")), + }, + { + ResourceName: "google_sql_database_instance.instance", + ImportState: true, + ImportStateVerify: true, + ImportStateVerifyIgnore: []string{"deletion_protection"}, + }, + { + ResourceName: "google_sql_database_instance.instance-failover", + ImportState: true, + ImportStateVerify: true, + ImportStateVerifyIgnore: []string{"deletion_protection"}, + }, + }, + }) +} func testAccSqlDatabaseInstance_sqlMysqlInstancePvpExample(context map[string]interface{}) string { return Nprintf(` @@ -1843,6 +2067,179 @@ resource "google_sql_database_instance" "instance-failover" { `, instanceName, failoverName) } +func googleSqlDatabaseInstance_replicaPromote(instanceName, failoverName string) string { +return fmt.Sprintf(` +resource "google_sql_database_instance" "instance" { + name = "%s" + region = "us-central1" + database_version = "MYSQL_5_7" + deletion_protection = false + + settings { + tier = "db-n1-standard-1" + + backup_configuration { + binary_log_enabled = "true" + enabled = "true" + start_time = "18:00" + } + } +} + +resource "google_sql_database_instance" "instance-failover" { + name = "%s" + region = "us-central1" + database_version = "MYSQL_5_7" + deletion_protection = false + + instance_type = "CLOUD_SQL_INSTANCE" + settings { + tier = "db-n1-standard-1" + } +} +`, instanceName, failoverName) +} + +func googleSqlDatabaseInstance_replicaPromoteWithMasterInstanceName(instanceName, failoverName string) string { + return fmt.Sprintf(` +resource "google_sql_database_instance" "instance" { + name = "%s" + region = "us-central1" + database_version = "MYSQL_5_7" + deletion_protection = false + + settings { + tier = "db-n1-standard-1" + + backup_configuration { + binary_log_enabled = "true" + enabled = "true" + start_time = "18:00" + } + } +} + +resource "google_sql_database_instance" "instance-failover" { + name = "%s" + region = "us-central1" + master_instance_name = google_sql_database_instance.instance.name + database_version = "MYSQL_5_7" + deletion_protection = false + instance_type = "CLOUD_SQL_INSTANCE" + settings { + tier = "db-n1-standard-1" + } +} +`, instanceName, failoverName) +} + +func googleSqlDatabaseInstance_replicaPromoteWithMasterInstanceNameAndReplicaConfiguration(instanceName string, failoverName string) string { + return fmt.Sprintf(` +resource "google_sql_database_instance" "instance" { + name = "%s" + region = "us-central1" + database_version = "MYSQL_5_7" + deletion_protection = false + + settings { + tier = "db-n1-standard-1" + + backup_configuration { + binary_log_enabled = "true" + enabled = "true" + start_time = "18:00" + } + } +} + +resource "google_sql_database_instance" "instance-failover" { + name = "%s" + region = "us-central1" + master_instance_name = google_sql_database_instance.instance.name + database_version = "MYSQL_5_7" + deletion_protection = false + + replica_configuration { + failover_target = "true" + } + + instance_type = "CLOUD_SQL_INSTANCE" + settings { + tier = "db-n1-standard-1" + } +} +`, instanceName, failoverName) +} + +func googleSqlDatabaseInstance_replicaPromoteSkippedWithNoMasterInstanceNameAndNoReplicaConfiguration(instanceName string, failoverName string) string { + return fmt.Sprintf(` +resource "google_sql_database_instance" "instance" { + name = "%s" + region = "us-central1" + database_version = "MYSQL_5_7" + deletion_protection = false + + settings { + tier = "db-n1-standard-1" + + backup_configuration { + binary_log_enabled = "true" + enabled = "true" + start_time = "18:00" + } + } +} + +resource "google_sql_database_instance" "instance-failover" { + name = "%s" + region = "us-central1" + database_version = "MYSQL_5_7" + deletion_protection = false + settings { + tier = "db-n1-standard-1" + } + depends_on = [google_sql_database_instance.instance] +} +`, instanceName, failoverName) +} + +func googleSqlDatabaseInstance_replicaPromoteWithReplicaConfiguration(instanceName string, failoverName string) string { + return fmt.Sprintf(` +resource "google_sql_database_instance" "instance" { + name = "%s" + region = "us-central1" + database_version = "MYSQL_5_7" + deletion_protection = false + + settings { + tier = "db-n1-standard-1" + + backup_configuration { + binary_log_enabled = "true" + enabled = "true" + start_time = "18:00" + } + } +} + +resource "google_sql_database_instance" "instance-failover" { + name = "%s" + region = "us-central1" + database_version = "MYSQL_5_7" + deletion_protection = false + + replica_configuration { + failover_target = "true" + } + + instance_type = "CLOUD_SQL_INSTANCE" + settings { + tier = "db-n1-standard-1" + } +} +`, instanceName, failoverName) +} + func testAccSqlDatabaseInstance_withPrivateNetwork_withoutAllocatedIpRange(databaseName, networkName, addressRangeName string, specifyPrivatePathOption bool, enablePrivatePath bool) string { privatePathOption := "" if specifyPrivatePathOption { @@ -2811,6 +3208,61 @@ data "google_sql_backup_run" "backup" { `, context) } +func checkPromoteReplicaSkipConfigurations(resourceName string) func(*terraform.State) error { + return func(s *terraform.State) error { + resource, ok := s.RootModule().Resources[resourceName] + if !ok { + return fmt.Errorf("Can't find %s in state", resourceName) + } + + resourceAttributes := resource.Primary.Attributes + instanceType, ok := resourceAttributes["instance_type"] + if !ok { + return fmt.Errorf("Instance type is not present in state for %s", resourceName) + } + if instanceType != "READ_REPLICA_INSTANCE" { + return fmt.Errorf("instance_type is %s, it should be READ_REPLICA_INSTANCE.", instanceType) + } + + masterInstanceName, ok := resourceAttributes["master_instance_name"] + if !ok && masterInstanceName != "" { + return fmt.Errorf("master_instance_name should be present in %s state.", resourceName) + } + + return nil + } +} + +func checkPromoteReplicaConfigurations(resourceName string) func(*terraform.State) error { + return func(s *terraform.State) error { + resource, ok := s.RootModule().Resources[resourceName] + if !ok { + return fmt.Errorf("Can't find %s in state", resourceName) + } + + resourceAttributes := resource.Primary.Attributes + instanceType, ok := resourceAttributes["instance_type"] + if !ok { + return fmt.Errorf("Instance type is not present in state for %s", resourceName) + } + if instanceType != "CLOUD_SQL_INSTANCE" { + return fmt.Errorf("Error in replica promotion. instance_type is %s, it should be CLOUD_SQL_INSTANCE.", instanceType) + } + + masterInstanceName, ok := resourceAttributes["master_instance_name"] + if ok && masterInstanceName != "" { + return fmt.Errorf("Error in replica promotion. master_instance_name should not be present in %s state.", resourceName) + } + + replicaConfiguration, ok := resourceAttributes["replica_configuration"] + if ok && replicaConfiguration != "" { + return fmt.Errorf("Error in replica promotion. replica_configuration should not be present in %s state.", resourceName) + } + + return nil + } +} + func checkInstanceTypeIsPresent(resourceName string) func(*terraform.State) error { return func(s *terraform.State) error { rs, ok := s.RootModule().Resources[resourceName] diff --git a/mmv1/third_party/terraform/website/docs/r/sql_database_instance.html.markdown b/mmv1/third_party/terraform/website/docs/r/sql_database_instance.html.markdown index 99330c41ca35..9af08c650a52 100644 --- a/mmv1/third_party/terraform/website/docs/r/sql_database_instance.html.markdown +++ b/mmv1/third_party/terraform/website/docs/r/sql_database_instance.html.markdown @@ -485,6 +485,8 @@ performing filtering in a Terraform config. * `instance_type` - The type of the instance. The supported values are `SQL_INSTANCE_TYPE_UNSPECIFIED`, `CLOUD_SQL_INSTANCE`, `ON_PREMISES_INSTANCE` and `READ_REPLICA_INSTANCE`. +~> **NOTE:** Users can upgrade a read replica instance to a stand-alone Cloud SQL instance with the help of `instance_type`. To promote, users have to set the `instance_type` property as `CLOUD_SQL_INSTANCE` and remove/unset `master_instance_name` and `replica_configuration` from instance configuration. This operation might cause your instance to restart. + * `settings.version` - Used to make sure changes to the `settings` block are atomic.