Skip to content

Commit 41f6b54

Browse files
Add point-in-time-recovery to sql database instance (#4431) (#2923)
Signed-off-by: Modular Magician <[email protected]>
1 parent a0b3dc6 commit 41f6b54

File tree

5 files changed

+233
-10
lines changed

5 files changed

+233
-10
lines changed

.changelog/4431.txt

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
```release-note:enhancement
2+
sql: added support for point-in-time-recovery to `google_sql_database_instance`
3+
```

google-beta/bootstrap_utils_test.go

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -356,8 +356,14 @@ func BootstrapSharedSQLInstanceBackupRun(t *testing.T) string {
356356
if err != nil {
357357
if isGoogleApiErrorWithCode(err, 404) {
358358
log.Printf("[DEBUG] SQL Instance %q not found, bootstrapping", SharedTestSQLInstanceName)
359+
360+
backupConfig := &sqladmin.BackupConfiguration{
361+
Enabled: true,
362+
PointInTimeRecoveryEnabled: true,
363+
}
359364
settings := &sqladmin.Settings{
360-
Tier: "db-f1-micro",
365+
Tier: "db-f1-micro",
366+
BackupConfiguration: backupConfig,
361367
}
362368
instance = &sqladmin.DatabaseInstance{
363369
Name: SharedTestSQLInstanceName,

google-beta/resource_sql_database_instance.go

Lines changed: 90 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -110,9 +110,11 @@ func resourceSqlDatabaseInstance() *schema.Resource {
110110
Description: `Used to block Terraform from deleting a SQL Instance.`,
111111
},
112112
"settings": {
113-
Type: schema.TypeList,
114-
Required: true,
115-
MaxItems: 1,
113+
Type: schema.TypeList,
114+
Optional: true,
115+
Computed: true,
116+
AtLeastOneOf: []string{"settings", "clone"},
117+
MaxItems: 1,
116118
Elem: &schema.Resource{
117119
Schema: map[string]*schema.Schema{
118120
"version": {
@@ -135,6 +137,7 @@ func resourceSqlDatabaseInstance() *schema.Resource {
135137
"authorized_gae_applications": {
136138
Type: schema.TypeList,
137139
Optional: true,
140+
Computed: true,
138141
Elem: &schema.Schema{Type: schema.TypeString},
139142
Deprecated: "This property is only applicable to First Generation instances, and First Generation instances are now deprecated.",
140143
Description: `This property is only applicable to First Generation instances. First Generation instances are now deprecated, see https://cloud.google.com/sql/docs/mysql/deprecation-notice for information on how to upgrade to Second Generation instances. A list of Google App Engine project names that are allowed to access this instance.`,
@@ -346,6 +349,7 @@ settings.backup_configuration.binary_log_enabled are both set to true.`,
346349
"user_labels": {
347350
Type: schema.TypeMap,
348351
Optional: true,
352+
Computed: true,
349353
Elem: &schema.Schema{Type: schema.TypeString},
350354
Description: `A set of key/value user label pairs to assign to the instance.`,
351355
},
@@ -604,6 +608,29 @@ settings.backup_configuration.binary_log_enabled are both set to true.`,
604608
},
605609
},
606610
},
611+
"clone": {
612+
Type: schema.TypeList,
613+
Optional: true,
614+
Computed: false,
615+
AtLeastOneOf: []string{"settings", "clone"},
616+
Description: `Configuration for creating a new instance as a clone of another instance.`,
617+
MaxItems: 1,
618+
Elem: &schema.Resource{
619+
Schema: map[string]*schema.Schema{
620+
"source_instance_name": {
621+
Type: schema.TypeString,
622+
Required: true,
623+
Description: `The name of the instance from which the point in time should be restored.`,
624+
},
625+
"point_in_time": {
626+
Type: schema.TypeString,
627+
Required: true,
628+
DiffSuppressFunc: timestampDiffSuppress(time.RFC3339Nano),
629+
Description: `The timestamp of the point in time that should be restored.`,
630+
},
631+
},
632+
},
633+
},
607634
},
608635
UseJSONNumber: true,
609636
}
@@ -623,6 +650,9 @@ func suppressFirstGen(k, old, new string, d *schema.ResourceData) bool {
623650
// Detects whether a database is 1st Generation by inspecting the tier name
624651
func isFirstGen(d *schema.ResourceData) bool {
625652
settingsList := d.Get("settings").([]interface{})
653+
if len(settingsList) == 0 {
654+
return false
655+
}
626656
settings := settingsList[0].(map[string]interface{})
627657
tier := settings["tier"].(string)
628658

@@ -699,12 +729,19 @@ func resourceSqlDatabaseInstanceCreate(d *schema.ResourceData, meta interface{})
699729
instance := &sqladmin.DatabaseInstance{
700730
Name: name,
701731
Region: region,
702-
Settings: expandSqlDatabaseInstanceSettings(d.Get("settings").([]interface{}), !isFirstGen(d)),
703732
DatabaseVersion: d.Get("database_version").(string),
704733
MasterInstanceName: d.Get("master_instance_name").(string),
705734
ReplicaConfiguration: expandReplicaConfiguration(d.Get("replica_configuration").([]interface{})),
706735
}
707736

737+
cloneContext, cloneSource := expandCloneContext(d.Get("clone").([]interface{}))
738+
739+
s, ok := d.GetOk("settings")
740+
desiredSettings := expandSqlDatabaseInstanceSettings(s.([]interface{}), !isFirstGen(d))
741+
if ok {
742+
instance.Settings = desiredSettings
743+
}
744+
708745
// MSSQL Server require rootPassword to be set
709746
if strings.Contains(instance.DatabaseVersion, "SQLSERVER") {
710747
instance.RootPassword = d.Get("root_password").(string)
@@ -725,7 +762,13 @@ func resourceSqlDatabaseInstanceCreate(d *schema.ResourceData, meta interface{})
725762

726763
var op *sqladmin.Operation
727764
err = retryTimeDuration(func() (operr error) {
728-
op, operr = config.NewSqlAdminClient(userAgent).Instances.Insert(project, instance).Do()
765+
if cloneContext != nil {
766+
cloneContext.DestinationInstanceName = name
767+
clodeReq := sqladmin.InstancesCloneRequest{CloneContext: cloneContext}
768+
op, operr = config.NewSqlAdminClient(userAgent).Instances.Clone(project, cloneSource, &clodeReq).Do()
769+
} else {
770+
op, operr = config.NewSqlAdminClient(userAgent).Instances.Insert(project, instance).Do()
771+
}
729772
return operr
730773
}, d.Timeout(schema.TimeoutCreate), isSqlOperationInProgressError)
731774
if err != nil {
@@ -749,6 +792,36 @@ func resourceSqlDatabaseInstanceCreate(d *schema.ResourceData, meta interface{})
749792
return err
750793
}
751794

795+
// Refresh settings from read as they may have defaulted from the API
796+
s = d.Get("settings")
797+
// If we've created an instance as a clone, we need to update it to set the correct settings
798+
if len(s.([]interface{})) != 0 && cloneContext != nil {
799+
instanceUpdate := &sqladmin.DatabaseInstance{
800+
Settings: desiredSettings,
801+
}
802+
_settings := s.([]interface{})[0].(map[string]interface{})
803+
instanceUpdate.Settings.SettingsVersion = int64(_settings["version"].(int))
804+
var op *sqladmin.Operation
805+
err = retryTimeDuration(func() (rerr error) {
806+
op, rerr = config.NewSqlAdminClient(userAgent).Instances.Update(project, name, instanceUpdate).Do()
807+
return rerr
808+
}, d.Timeout(schema.TimeoutUpdate), isSqlOperationInProgressError)
809+
if err != nil {
810+
return fmt.Errorf("Error, failed to update instance settings for %s: %s", instance.Name, err)
811+
}
812+
813+
err = sqlAdminOperationWaitTime(config, op, project, "Update Instance", userAgent, d.Timeout(schema.TimeoutUpdate))
814+
if err != nil {
815+
return err
816+
}
817+
818+
// Refresh the state of the instance after updating the settings
819+
err = resourceSqlDatabaseInstanceRead(d, meta)
820+
if err != nil {
821+
return err
822+
}
823+
}
824+
752825
// If a default root user was created with a wildcard ('%') hostname, delete it.
753826
// Users in a replica instance are inherited from the master instance and should be left alone.
754827
if sqlDatabaseIsMaster(d) {
@@ -850,6 +923,18 @@ func expandReplicaConfiguration(configured []interface{}) *sqladmin.ReplicaConfi
850923
}
851924
}
852925

926+
func expandCloneContext(configured []interface{}) (*sqladmin.CloneContext, string) {
927+
if len(configured) == 0 || configured[0] == nil {
928+
return nil, ""
929+
}
930+
931+
_cloneConfiguration := configured[0].(map[string]interface{})
932+
933+
return &sqladmin.CloneContext{
934+
PointInTime: _cloneConfiguration["point_in_time"].(string),
935+
}, _cloneConfiguration["source_instance_name"].(string)
936+
}
937+
853938
func expandMaintenanceWindow(configured []interface{}) *sqladmin.MaintenanceWindow {
854939
if len(configured) == 0 || configured[0] == nil {
855940
return nil

google-beta/resource_sql_database_instance_test.go

Lines changed: 117 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -755,6 +755,62 @@ func TestAccSqlDatabaseInstance_backupUpdate(t *testing.T) {
755755
})
756756
}
757757

758+
func TestAccSqlDatabaseInstance_basicClone(t *testing.T) {
759+
// Sqladmin client
760+
skipIfVcr(t)
761+
t.Parallel()
762+
763+
context := map[string]interface{}{
764+
"random_suffix": randString(t, 10),
765+
"original_db_name": BootstrapSharedSQLInstanceBackupRun(t),
766+
}
767+
768+
vcrTest(t, resource.TestCase{
769+
PreCheck: func() { testAccPreCheck(t) },
770+
Providers: testAccProviders,
771+
CheckDestroy: testAccSqlDatabaseInstanceDestroyProducer(t),
772+
Steps: []resource.TestStep{
773+
{
774+
Config: testAccSqlDatabaseInstance_basicClone(context),
775+
},
776+
{
777+
ResourceName: "google_sql_database_instance.instance",
778+
ImportState: true,
779+
ImportStateVerify: true,
780+
ImportStateVerifyIgnore: []string{"deletion_protection", "clone"},
781+
},
782+
},
783+
})
784+
}
785+
786+
func TestAccSqlDatabaseInstance_cloneWithSettings(t *testing.T) {
787+
// Sqladmin client
788+
skipIfVcr(t)
789+
t.Parallel()
790+
791+
context := map[string]interface{}{
792+
"random_suffix": randString(t, 10),
793+
"original_db_name": BootstrapSharedSQLInstanceBackupRun(t),
794+
}
795+
796+
vcrTest(t, resource.TestCase{
797+
PreCheck: func() { testAccPreCheck(t) },
798+
Providers: testAccProviders,
799+
CheckDestroy: testAccSqlDatabaseInstanceDestroyProducer(t),
800+
Steps: []resource.TestStep{
801+
{
802+
Config: testAccSqlDatabaseInstance_cloneWithSettings(context),
803+
},
804+
{
805+
ResourceName: "google_sql_database_instance.instance",
806+
ImportState: true,
807+
ImportStateVerify: true,
808+
ImportStateVerifyIgnore: []string{"deletion_protection", "clone"},
809+
},
810+
},
811+
})
812+
}
813+
758814
func testAccSqlDatabaseInstanceDestroyProducer(t *testing.T) func(s *terraform.State) error {
759815
return func(s *terraform.State) error {
760816
for _, rs := range s.RootModule().Resources {
@@ -1327,3 +1383,64 @@ data "google_sql_backup_run" "backup" {
13271383
}
13281384
`, context)
13291385
}
1386+
1387+
func testAccSqlDatabaseInstance_basicClone(context map[string]interface{}) string {
1388+
return Nprintf(`
1389+
resource "google_sql_database_instance" "instance" {
1390+
name = "tf-test-%{random_suffix}"
1391+
database_version = "POSTGRES_11"
1392+
region = "us-central1"
1393+
1394+
clone {
1395+
source_instance_name = data.google_sql_backup_run.backup.instance
1396+
point_in_time = data.google_sql_backup_run.backup.start_time
1397+
}
1398+
1399+
deletion_protection = false
1400+
1401+
// Ignore changes, since the most recent backup may change during the test
1402+
lifecycle{
1403+
ignore_changes = [clone[0].point_in_time]
1404+
}
1405+
}
1406+
1407+
data "google_sql_backup_run" "backup" {
1408+
instance = "%{original_db_name}"
1409+
most_recent = true
1410+
}
1411+
`, context)
1412+
}
1413+
1414+
func testAccSqlDatabaseInstance_cloneWithSettings(context map[string]interface{}) string {
1415+
return Nprintf(`
1416+
resource "google_sql_database_instance" "instance" {
1417+
name = "tf-test-%{random_suffix}"
1418+
database_version = "POSTGRES_11"
1419+
region = "us-central1"
1420+
1421+
settings {
1422+
tier = "db-f1-micro"
1423+
backup_configuration {
1424+
enabled = false
1425+
}
1426+
}
1427+
1428+
clone {
1429+
source_instance_name = data.google_sql_backup_run.backup.instance
1430+
point_in_time = data.google_sql_backup_run.backup.start_time
1431+
}
1432+
1433+
deletion_protection = false
1434+
1435+
// Ignore changes, since the most recent backup may change during the test
1436+
lifecycle{
1437+
ignore_changes = [clone[0].point_in_time]
1438+
}
1439+
}
1440+
1441+
data "google_sql_backup_run" "backup" {
1442+
instance = "%{original_db_name}"
1443+
most_recent = true
1444+
}
1445+
`, context)
1446+
}

website/docs/r/sql_database_instance.html.markdown

Lines changed: 16 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -191,11 +191,11 @@ The following arguments are supported:
191191
region is not supported with Cloud SQL. If you choose not to provide the `region` argument for this resource,
192192
make sure you understand this.
193193

194-
* `settings` - (Required) The settings to use for the database. The
195-
configuration is detailed below.
196-
197194
- - -
198195

196+
* `settings` - (Optional) The settings to use for the database. The
197+
configuration is detailed below. Required if `clone` is not set.
198+
199199
* `database_version` - (Optional, Default: `MYSQL_5_6`) The MySQL, PostgreSQL or
200200
SQL Server (beta) version to use. Supported values include `MYSQL_5_6`,
201201
`MYSQL_5_7`, `MYSQL_8_0`, `POSTGRES_9_6`,`POSTGRES_10`, `POSTGRES_11`,
@@ -239,7 +239,11 @@ in Terraform state, a `terraform destroy` or `terraform apply` command that dele
239239
**NOTE:** Restoring from a backup is an imperative action and not recommended via Terraform. Adding or modifying this
240240
block during resource creation/update will trigger the restore action after the resource is created/updated.
241241

242-
The required `settings` block supports:
242+
* `clone` - (Optional) The context needed to create this instance as a clone of another instance. When this field is set during
243+
resource creation, Terraform will attempt to clone another instance as indicated in the context. The
244+
configuration is detailed below.
245+
246+
The `settings` block supports:
243247

244248
* `tier` - (Required) The machine type to use. See [tiers](https://cloud.google.com/sql/docs/admin-api/v1beta4/tiers)
245249
for more details and supported versions. Postgres supports only shared-core machine types such as `db-f1-micro`,
@@ -378,6 +382,14 @@ to work, cannot be updated, and supports:
378382
* `verify_server_certificate` - (Optional) True if the master's common name
379383
value is checked during the SSL handshake.
380384

385+
The optional `clone` block supports:
386+
387+
* `source_instance_name` - (Required) Name of the source instance which will be cloned.
388+
389+
* `point_in_time` - (Optional) The timestamp of the point in time that should be restored.
390+
391+
A timestamp in RFC3339 UTC "Zulu" format, with nanosecond resolution and up to nine fractional digits. Examples: "2014-10-02T15:01:23Z" and "2014-10-02T15:01:23.045123456Z".
392+
381393
The optional `restore_backup_context` block supports:
382394
**NOTE:** Restoring from a backup is an imperative action and not recommended via Terraform. Adding or modifying this
383395
block during resource creation/update will trigger the restore action after the resource is created/updated.

0 commit comments

Comments
 (0)