From 7be40c2ff8eada721c10e986d75be503893f2b6f Mon Sep 17 00:00:00 2001 From: Modular Magician Date: Thu, 7 Jan 2021 23:16:28 +0000 Subject: [PATCH] Add restoreBackup support for sql db instance (#4336) Signed-off-by: Modular Magician --- .changelog/4336.txt | 3 + google-beta/resource_sql_database_instance.go | 80 +++++++++++++ .../resource_sql_database_instance_test.go | 113 ++++++++++++++++++ .../r/sql_database_instance.html.markdown | 16 +++ 4 files changed, 212 insertions(+) create mode 100644 .changelog/4336.txt diff --git a/.changelog/4336.txt b/.changelog/4336.txt new file mode 100644 index 0000000000..e322bcb15a --- /dev/null +++ b/.changelog/4336.txt @@ -0,0 +1,3 @@ +```release-note:enhancement +sql: added restore from backup support to `google_sql_database_instance` +``` diff --git a/google-beta/resource_sql_database_instance.go b/google-beta/resource_sql_database_instance.go index aa8694ad0e..1cfdeedebc 100644 --- a/google-beta/resource_sql_database_instance.go +++ b/google-beta/resource_sql_database_instance.go @@ -580,6 +580,30 @@ settings.backup_configuration.binary_log_enabled are both set to true.`, Computed: true, Description: `The URI of the created resource.`, }, + "restore_backup_context": { + Type: schema.TypeList, + Optional: true, + MaxItems: 1, + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "backup_run_id": { + Type: schema.TypeInt, + Required: true, + Description: `The ID of the backup run to restore from.`, + }, + "instance_id": { + Type: schema.TypeString, + Optional: true, + Description: `The ID of the instance that the backup was taken from.`, + }, + "project": { + Type: schema.TypeString, + Optional: true, + Description: `The full project ID of the source instance.`, + }, + }, + }, + }, }, UseJSONNumber: true, } @@ -752,6 +776,14 @@ func resourceSqlDatabaseInstanceCreate(d *schema.ResourceData, meta interface{}) } } + // Perform a backup restore if the backup context exists + if r, ok := d.GetOk("restore_backup_context"); ok { + err = sqlDatabaseInstanceRestoreFromBackup(d, config, userAgent, project, name, r) + if err != nil { + return err + } + } + return nil } @@ -1045,6 +1077,16 @@ func resourceSqlDatabaseInstanceUpdate(d *schema.ResourceData, meta interface{}) return err } + // Perform a backup restore if the backup context exists and has changed + if r, ok := d.GetOk("restore_backup_context"); ok { + if d.HasChange("restore_backup_context") { + err = sqlDatabaseInstanceRestoreFromBackup(d, config, userAgent, project, d.Get("name").(string), r) + if err != nil { + return err + } + } + } + return resourceSqlDatabaseInstanceRead(d, meta) } @@ -1337,3 +1379,41 @@ func sqlDatabaseInstanceServiceNetworkPrecheck(d *schema.ResourceData, config *C return nil } + +func expandRestoreBackupContext(configured []interface{}) *sqladmin.RestoreBackupContext { + if len(configured) == 0 || configured[0] == nil { + return nil + } + + _rc := configured[0].(map[string]interface{}) + return &sqladmin.RestoreBackupContext{ + BackupRunId: int64(_rc["backup_run_id"].(int)), + InstanceId: _rc["instance_id"].(string), + Project: _rc["project"].(string), + } +} + +func sqlDatabaseInstanceRestoreFromBackup(d *schema.ResourceData, config *Config, userAgent, project, instanceId string, r interface{}) error { + log.Printf("[DEBUG] Initiating SQL database instance backup restore") + restoreContext := r.([]interface{}) + + backupRequest := &sqladmin.InstancesRestoreBackupRequest{ + RestoreBackupContext: expandRestoreBackupContext(restoreContext), + } + + var op *sqladmin.Operation + err := retryTimeDuration(func() (operr error) { + op, operr = config.NewSqlAdminClient(userAgent).Instances.RestoreBackup(project, instanceId, backupRequest).Do() + return operr + }, d.Timeout(schema.TimeoutUpdate), isSqlOperationInProgressError) + if err != nil { + return fmt.Errorf("Error, failed to restore instance from backup %s: %s", instanceId, err) + } + + err = sqlAdminOperationWaitTime(config, op, project, "Restore Backup", userAgent, d.Timeout(schema.TimeoutUpdate)) + if err != nil { + return err + } + + return nil +} diff --git a/google-beta/resource_sql_database_instance_test.go b/google-beta/resource_sql_database_instance_test.go index 52f3403e57..f078607734 100644 --- a/google-beta/resource_sql_database_instance_test.go +++ b/google-beta/resource_sql_database_instance_test.go @@ -690,6 +690,71 @@ func TestAccSqlDatabaseInstance_withPrivateNetwork(t *testing.T) { }) } +func TestAccSqlDatabaseInstance_createFromBackup(t *testing.T) { + // Sqladmin client + skipIfVcr(t) + t.Parallel() + + context := map[string]interface{}{ + "random_suffix": randString(t, 10), + "original_db_name": BootstrapSharedSQLInstanceBackupRun(t), + } + + vcrTest(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + Providers: testAccProviders, + CheckDestroy: testAccSqlDatabaseInstanceDestroyProducer(t), + Steps: []resource.TestStep{ + { + Config: testAccSqlDatabaseInstance_restoreFromBackup(context), + }, + { + ResourceName: "google_sql_database_instance.instance", + ImportState: true, + ImportStateVerify: true, + ImportStateVerifyIgnore: []string{"deletion_protection", "restore_backup_context"}, + }, + }, + }) +} + +func TestAccSqlDatabaseInstance_backupUpdate(t *testing.T) { + // Sqladmin client + skipIfVcr(t) + t.Parallel() + + context := map[string]interface{}{ + "random_suffix": randString(t, 10), + "original_db_name": BootstrapSharedSQLInstanceBackupRun(t), + } + + vcrTest(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + Providers: testAccProviders, + CheckDestroy: testAccSqlDatabaseInstanceDestroyProducer(t), + Steps: []resource.TestStep{ + { + Config: testAccSqlDatabaseInstance_beforeBackup(context), + }, + { + ResourceName: "google_sql_database_instance.instance", + ImportState: true, + ImportStateVerify: true, + ImportStateVerifyIgnore: []string{"deletion_protection"}, + }, + { + Config: testAccSqlDatabaseInstance_restoreFromBackup(context), + }, + { + ResourceName: "google_sql_database_instance.instance", + ImportState: true, + ImportStateVerify: true, + ImportStateVerifyIgnore: []string{"deletion_protection", "restore_backup_context"}, + }, + }, + }) +} + func testAccSqlDatabaseInstanceDestroyProducer(t *testing.T) func(s *terraform.State) error { return func(s *terraform.State) error { for _, rs := range s.RootModule().Resources { @@ -1214,3 +1279,51 @@ resource "google_sql_database_instance" "instance" { } `, masterID, pointInTimeRecoveryEnabled) } + +func testAccSqlDatabaseInstance_beforeBackup(context map[string]interface{}) string { + return Nprintf(` +resource "google_sql_database_instance" "instance" { + name = "tf-test-%{random_suffix}" + database_version = "POSTGRES_11" + region = "us-central1" + + settings { + tier = "db-f1-micro" + backup_configuration { + enabled = "false" + } + } + + deletion_protection = false +} +`, context) +} + +func testAccSqlDatabaseInstance_restoreFromBackup(context map[string]interface{}) string { + return Nprintf(` +resource "google_sql_database_instance" "instance" { + name = "tf-test-%{random_suffix}" + database_version = "POSTGRES_11" + region = "us-central1" + + settings { + tier = "db-f1-micro" + backup_configuration { + enabled = "false" + } + } + + restore_backup_context { + backup_run_id = data.google_sql_backup_run.backup.backup_id + instance_id = data.google_sql_backup_run.backup.instance + } + + deletion_protection = false +} + +data "google_sql_backup_run" "backup" { + instance = "%{original_db_name}" + most_recent = true +} +`, context) +} diff --git a/website/docs/r/sql_database_instance.html.markdown b/website/docs/r/sql_database_instance.html.markdown index dd399f1ce2..038a4daae6 100644 --- a/website/docs/r/sql_database_instance.html.markdown +++ b/website/docs/r/sql_database_instance.html.markdown @@ -234,6 +234,11 @@ includes an up-to-date reference of supported versions. * `deletion_protection` - (Optional, Default: `true` ) Whether or not to allow Terraform to destroy the instance. Unless this field is set to false in Terraform state, a `terraform destroy` or `terraform apply` command that deletes the instance will fail. +* `restore_backup_context` - (optional) The context needed to restore the database to a backup run. This field will + cause Terraform to trigger the database to restore from the backup run indicated. The configuration is detailed below. + **NOTE:** Restoring from a backup is an imperative action and not recommended via Terraform. Adding or modifying this + block during resource creation/update will trigger the restore action after the resource is created/updated. + The required `settings` block supports: * `tier` - (Required) The machine type to use. See [tiers](https://cloud.google.com/sql/docs/admin-api/v1beta4/tiers) @@ -373,6 +378,17 @@ to work, cannot be updated, and supports: * `verify_server_certificate` - (Optional) True if the master's common name value is checked during the SSL handshake. +The optional `restore_backup_context` block supports: +**NOTE:** Restoring from a backup is an imperative action and not recommended via Terraform. Adding or modifying this +block during resource creation/update will trigger the restore action after the resource is created/updated. + +* `backup_run_id` - (Required) The ID of the backup run to restore from. + +* `instance_id` - (Optional) The ID of the instance that the backup was taken from. If left empty, + this instance's ID will be used. + +* `project` - (Optional) The full project ID of the source instance.` + ## Attributes Reference In addition to the arguments listed above, the following computed attributes are