Skip to content

Commit

Permalink
Add enableDropProtection support to spanner database (#8011) (#15283)
Browse files Browse the repository at this point in the history
Signed-off-by: Modular Magician <[email protected]>
  • Loading branch information
modular-magician authored Jul 25, 2023
1 parent ea2ca70 commit f423e7c
Show file tree
Hide file tree
Showing 4 changed files with 267 additions and 30 deletions.
3 changes: 3 additions & 0 deletions .changelog/8011.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
```release-note:enhancement
spanner: added `enable_drop_protection` to `google_spanner_database`
```
76 changes: 76 additions & 0 deletions google/resource_spanner_database_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -391,6 +391,82 @@ resource "google_spanner_database" "basic" {
`, instanceName, instanceName, databaseName, databaseName, databaseName)
}

func TestAccSpannerDatabase_enableDropProtection(t *testing.T) {
t.Parallel()

rnd := RandString(t, 10)
instanceName := fmt.Sprintf("tf-test-%s", rnd)
databaseName := fmt.Sprintf("tfgen_%s", rnd)

VcrTest(t, resource.TestCase{
PreCheck: func() { acctest.AccTestPreCheck(t) },
ProtoV5ProviderFactories: ProtoV5ProviderFactories(t),
CheckDestroy: testAccCheckSpannerDatabaseDestroyProducer(t),
Steps: []resource.TestStep{
{
Config: testAccSpannerDatabase_enableDropProtection(instanceName, databaseName),
},
{
ResourceName: "google_spanner_database.basic",
ImportState: true,
ImportStateVerify: true,
ImportStateVerifyIgnore: []string{"ddl", "deletion_protection"},
},
{
Config: testAccSpannerDatabase_enableDropProtectionUpdate(instanceName, databaseName),
},
{
ResourceName: "google_spanner_database.basic",
ImportState: true,
ImportStateVerify: true,
ImportStateVerifyIgnore: []string{"ddl", "deletion_protection"},
},
},
})
}

func testAccSpannerDatabase_enableDropProtection(instanceName, databaseName string) string {
return fmt.Sprintf(`
resource "google_spanner_instance" "basic" {
name = "%s"
config = "regional-us-central1"
display_name = "%s-display"
num_nodes = 1
}
resource "google_spanner_database" "basic" {
instance = google_spanner_instance.basic.name
name = "%s"
enable_drop_protection = true
deletion_protection = false
ddl = [
"CREATE TABLE t1 (t1 INT64 NOT NULL,) PRIMARY KEY(t1)",
]
}
`, instanceName, instanceName, databaseName)
}

func testAccSpannerDatabase_enableDropProtectionUpdate(instanceName, databaseName string) string {
return fmt.Sprintf(`
resource "google_spanner_instance" "basic" {
name = "%s"
config = "regional-us-central1"
display_name = "%s-display"
num_nodes = 1
}
resource "google_spanner_database" "basic" {
instance = google_spanner_instance.basic.name
name = "%s"
enable_drop_protection = false
deletion_protection = false
ddl = [
"CREATE TABLE t1 (t1 INT64 NOT NULL,) PRIMARY KEY(t1)",
]
}
`, instanceName, instanceName, databaseName)
}

// Unit Tests for validation of retention period argument
func TestValidateDatabaseRetentionPeriod(t *testing.T) {
t.Parallel()
Expand Down
204 changes: 176 additions & 28 deletions google/services/spanner/resource_spanner_database.go
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ import (
"reflect"
"regexp"
"strconv"
"strings"
"time"

"github.com/hashicorp/terraform-plugin-sdk/v2/helper/customdiff"
Expand Down Expand Up @@ -177,6 +178,18 @@ error in any statement, the database is not created.`,
Type: schema.TypeString,
},
},
"enable_drop_protection": {
Type: schema.TypeBool,
Optional: true,
Description: `Whether drop protection is enabled for this database. Defaults to false.
Drop protection is different from
the "deletion_protection" attribute in the following ways:
(1) "deletion_protection" only protects the database from deletions in Terraform.
whereas setting “enableDropProtection” to true protects the database from deletions in all interfaces.
(2) Setting "enableDropProtection" to true also prevents the deletion of the parent instance containing the database.
"deletion_protection" attribute does not provide protection against the deletion of the parent instance.`,
Default: false,
},
"encryption_config": {
Type: schema.TypeList,
Optional: true,
Expand Down Expand Up @@ -215,8 +228,8 @@ update the database's version_retention_period.`,
Type: schema.TypeBool,
Optional: true,
Default: true,
Description: `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' that would delete the instance will fail.`,
Description: `Whether or not to allow Terraform to destroy the database. Defaults to true. Unless this field is set to false
in Terraform state, a 'terraform destroy' or 'terraform apply' that would delete the database will fail.`,
},
"project": {
Type: schema.TypeString,
Expand Down Expand Up @@ -267,6 +280,12 @@ func resourceSpannerDatabaseCreate(d *schema.ResourceData, meta interface{}) err
} else if v, ok := d.GetOkExists("database_dialect"); !tpgresource.IsEmptyValue(reflect.ValueOf(databaseDialectProp)) && (ok || !reflect.DeepEqual(v, databaseDialectProp)) {
obj["databaseDialect"] = databaseDialectProp
}
enableDropProtectionProp, err := expandSpannerDatabaseEnableDropProtection(d.Get("enable_drop_protection"), d, config)
if err != nil {
return err
} else if v, ok := d.GetOkExists("enable_drop_protection"); !tpgresource.IsEmptyValue(reflect.ValueOf(enableDropProtectionProp)) && (ok || !reflect.DeepEqual(v, enableDropProtectionProp)) {
obj["enableDropProtection"] = enableDropProtectionProp
}
instanceProp, err := expandSpannerDatabaseInstance(d.Get("instance"), d, config)
if err != nil {
return err
Expand Down Expand Up @@ -426,6 +445,37 @@ func resourceSpannerDatabaseCreate(d *schema.ResourceData, meta interface{}) err
}
}

enableDropProtection, enableDropProtectionOk := d.GetOk("enable_drop_protection")
dropProtection := enableDropProtection.(bool)
if enableDropProtectionOk && dropProtection {
updateMask := []string{"enableDropProtection"}
url, err := tpgresource.ReplaceVars(d, config, "{{SpannerBasePath}}projects/{{project}}/instances/{{instance}}/databases/{{name}}")
if err != nil {
return err
}
// updateMask is a URL parameter but not present in the schema, so ReplaceVars
// won't set it
url, err = transport_tpg.AddQueryParams(url, map[string]string{"updateMask": strings.Join(updateMask, ",")})
if err != nil {
return err
}
obj := map[string]interface{}{"enableDropProtection": dropProtection}
res, err := transport_tpg.SendRequest(transport_tpg.SendRequestOptions{
Config: config,
Method: "PATCH",
Project: billingProject,
RawURL: url,
UserAgent: userAgent,
Body: obj,
Timeout: d.Timeout(schema.TimeoutUpdate),
})
if err != nil {
return fmt.Errorf("Error updating enableDropDatabaseProtection on Database: %s", err)
} else {
log.Printf("[DEBUG] Finished updating enableDropDatabaseProtection %q: %#v", d.Id(), res)
}
}

log.Printf("[DEBUG] Finished creating Database %q: %#v", d.Id(), res)

return resourceSpannerDatabaseRead(d, meta)
Expand Down Expand Up @@ -504,6 +554,9 @@ func resourceSpannerDatabaseRead(d *schema.ResourceData, meta interface{}) error
if err := d.Set("database_dialect", flattenSpannerDatabaseDatabaseDialect(res["databaseDialect"], d, config)); err != nil {
return fmt.Errorf("Error reading Database: %s", err)
}
if err := d.Set("enable_drop_protection", flattenSpannerDatabaseEnableDropProtection(res["enableDropProtection"], d, config)); err != nil {
return fmt.Errorf("Error reading Database: %s", err)
}
if err := d.Set("instance", flattenSpannerDatabaseInstance(res["instance"], d, config)); err != nil {
return fmt.Errorf("Error reading Database: %s", err)
}
Expand All @@ -526,6 +579,86 @@ func resourceSpannerDatabaseUpdate(d *schema.ResourceData, meta interface{}) err
}
billingProject = project

obj := make(map[string]interface{})
enableDropProtectionProp, err := expandSpannerDatabaseEnableDropProtection(d.Get("enable_drop_protection"), d, config)
if err != nil {
return err
} else if v, ok := d.GetOkExists("enable_drop_protection"); !tpgresource.IsEmptyValue(reflect.ValueOf(v)) && (ok || !reflect.DeepEqual(v, enableDropProtectionProp)) {
obj["enableDropProtection"] = enableDropProtectionProp
}

obj, err = resourceSpannerDatabaseUpdateEncoder(d, meta, obj)
if err != nil {
return err
}

url, err := tpgresource.ReplaceVars(d, config, "{{SpannerBasePath}}projects/{{project}}/instances/{{instance}}/databases/{{name}}")
if err != nil {
return err
}

log.Printf("[DEBUG] Updating Database %q: %#v", d.Id(), obj)
updateMask := []string{}

if d.HasChange("enable_drop_protection") {
updateMask = append(updateMask, "enableDropProtection")
}
// updateMask is a URL parameter but not present in the schema, so ReplaceVars
// won't set it
url, err = transport_tpg.AddQueryParams(url, map[string]string{"updateMask": strings.Join(updateMask, ",")})
if err != nil {
return err
}

if obj["statements"] != nil {
if len(obj["statements"].([]string)) == 0 {
// Return early to avoid making an API call that errors,
// due to containing no DDL SQL statements
d.Partial(false)
return resourceSpannerDatabaseRead(d, meta)
}
}

if resourceSpannerDBVirtualUpdate(d, ResourceSpannerDatabase().Schema) {
if d.Get("deletion_protection") != nil {
if err := d.Set("deletion_protection", d.Get("deletion_protection")); err != nil {
return fmt.Errorf("Error reading Instance: %s", err)
}
}
return nil
}

// err == nil indicates that the billing_project value was found
if bp, err := tpgresource.GetBillingProject(d, config); err == nil {
billingProject = bp
}

// if updateMask is empty we are not updating anything so skip the post
if len(updateMask) > 0 {
res, err := transport_tpg.SendRequest(transport_tpg.SendRequestOptions{
Config: config,
Method: "PATCH",
Project: billingProject,
RawURL: url,
UserAgent: userAgent,
Body: obj,
Timeout: d.Timeout(schema.TimeoutUpdate),
})

if err != nil {
return fmt.Errorf("Error updating Database %q: %s", d.Id(), err)
} else {
log.Printf("[DEBUG] Finished updating Database %q: %#v", d.Id(), res)
}

err = SpannerOperationWaitTime(
config, res, project, "Updating Database", userAgent,
d.Timeout(schema.TimeoutUpdate))

if err != nil {
return err
}
}
d.Partial(true)

if d.HasChange("version_retention_period") || d.HasChange("ddl") {
Expand Down Expand Up @@ -554,10 +687,13 @@ func resourceSpannerDatabaseUpdate(d *schema.ResourceData, meta interface{}) err
return err
}

if len(obj["statements"].([]string)) == 0 {
// Return early to avoid making an API call that errors,
// due to containing no DDL SQL statements
return resourceSpannerDatabaseRead(d, meta)
if obj["statements"] != nil {
if len(obj["statements"].([]string)) == 0 {
// Return early to avoid making an API call that errors,
// due to containing no DDL SQL statements
d.Partial(false)
return resourceSpannerDatabaseRead(d, meta)
}
}

if resourceSpannerDBVirtualUpdate(d, ResourceSpannerDatabase().Schema) {
Expand Down Expand Up @@ -720,6 +856,10 @@ func flattenSpannerDatabaseDatabaseDialect(v interface{}, d *schema.ResourceData
return v
}

func flattenSpannerDatabaseEnableDropProtection(v interface{}, d *schema.ResourceData, config *transport_tpg.Config) interface{} {
return v
}

func flattenSpannerDatabaseInstance(v interface{}, d *schema.ResourceData, config *transport_tpg.Config) interface{} {
if v == nil {
return v
Expand Down Expand Up @@ -766,6 +906,10 @@ func expandSpannerDatabaseDatabaseDialect(v interface{}, d tpgresource.Terraform
return v, nil
}

func expandSpannerDatabaseEnableDropProtection(v interface{}, d tpgresource.TerraformResourceData, config *transport_tpg.Config) (interface{}, error) {
return v, nil
}

func expandSpannerDatabaseInstance(v interface{}, d tpgresource.TerraformResourceData, config *transport_tpg.Config) (interface{}, error) {
f, err := tpgresource.ParseGlobalFieldValue("instances", v.(string), "project", d, config, true)
if err != nil {
Expand All @@ -790,37 +934,41 @@ func resourceSpannerDatabaseEncoder(d *schema.ResourceData, meta interface{}, ob

delete(obj, "versionRetentionPeriod")
delete(obj, "extraStatements")
delete(obj, "enableDropProtection")
return obj, nil
}

func resourceSpannerDatabaseUpdateEncoder(d *schema.ResourceData, meta interface{}, obj map[string]interface{}) (map[string]interface{}, error) {
old, new := d.GetChange("ddl")
oldDdls := old.([]interface{})
newDdls := new.([]interface{})
updateDdls := []string{}

// Only new ddl statments to be add to update call
for i := len(oldDdls); i < len(newDdls); i++ {
if newDdls[i] != nil {
updateDdls = append(updateDdls, newDdls[i].(string))
if obj["versionRetentionPeriod"] != nil || obj["extraStatements"] != nil {
old, new := d.GetChange("ddl")
oldDdls := old.([]interface{})
newDdls := new.([]interface{})
updateDdls := []string{}

//Only new ddl statments to be add to update call
for i := len(oldDdls); i < len(newDdls); i++ {
if newDdls[i] != nil {
updateDdls = append(updateDdls, newDdls[i].(string))
}
}
}

// Add statement to update version_retention_period property, if needed
if d.HasChange("version_retention_period") {
dbName := d.Get("name")
retentionDdl := fmt.Sprintf("ALTER DATABASE `%s` SET OPTIONS (version_retention_period=\"%s\")", dbName, obj["versionRetentionPeriod"])
if dialect, ok := d.GetOk("database_dialect"); ok && dialect == "POSTGRESQL" {
retentionDdl = fmt.Sprintf("ALTER DATABASE \"%s\" SET spanner.version_retention_period TO \"%s\"", dbName, obj["versionRetentionPeriod"])
//Add statement to update version_retention_period property, if needed
if d.HasChange("version_retention_period") {
dbName := d.Get("name")
retentionDdl := fmt.Sprintf("ALTER DATABASE `%s` SET OPTIONS (version_retention_period=\"%s\")", dbName, obj["versionRetentionPeriod"])
if dialect, ok := d.GetOk("database_dialect"); ok && dialect == "POSTGRESQL" {
retentionDdl = fmt.Sprintf("ALTER DATABASE \"%s\" SET spanner.version_retention_period TO \"%s\"", dbName, obj["versionRetentionPeriod"])
}
updateDdls = append(updateDdls, retentionDdl)
}
updateDdls = append(updateDdls, retentionDdl)
}

obj["statements"] = updateDdls
delete(obj, "name")
delete(obj, "versionRetentionPeriod")
delete(obj, "instance")
delete(obj, "extraStatements")
obj["statements"] = updateDdls
delete(obj, "name")
delete(obj, "versionRetentionPeriod")
delete(obj, "instance")
delete(obj, "extraStatements")
}
return obj, nil
}

Expand Down
Loading

0 comments on commit f423e7c

Please sign in to comment.