Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Terraform 7456 spanner database deletion protection #7557

Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions .changelog/4117.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
```release-note:enhancement
spanner: added `deletion_protection` field to `google_spanner_database` to make deleting them require an explicit intent.
```
```release-note:breaking-change
spanner: `google_spanner_database` resources now cannot be destroyed unless `deletion_protection = false` is set in state for the resource.
```
4 changes: 2 additions & 2 deletions google/resource_app_engine_flexible_app_version.go
Original file line number Diff line number Diff line change
Expand Up @@ -1097,12 +1097,12 @@ func resourceAppEngineFlexibleAppVersionRead(d *schema.ResourceData, meta interf
}

// Explicitly set virtual fields to default values if unset
if _, ok := d.GetOk("noop_on_destroy"); !ok {
if _, ok := d.GetOkExists("noop_on_destroy"); !ok {
if err := d.Set("noop_on_destroy", false); err != nil {
return fmt.Errorf("Error setting noop_on_destroy: %s", err)
}
}
if _, ok := d.GetOk("delete_service_on_destroy"); !ok {
if _, ok := d.GetOkExists("delete_service_on_destroy"); !ok {
if err := d.Set("delete_service_on_destroy", false); err != nil {
return fmt.Errorf("Error setting delete_service_on_destroy: %s", err)
}
Expand Down
4 changes: 2 additions & 2 deletions google/resource_app_engine_standard_app_version.go
Original file line number Diff line number Diff line change
Expand Up @@ -629,12 +629,12 @@ func resourceAppEngineStandardAppVersionRead(d *schema.ResourceData, meta interf
}

// Explicitly set virtual fields to default values if unset
if _, ok := d.GetOk("noop_on_destroy"); !ok {
if _, ok := d.GetOkExists("noop_on_destroy"); !ok {
if err := d.Set("noop_on_destroy", false); err != nil {
return fmt.Errorf("Error setting noop_on_destroy: %s", err)
}
}
if _, ok := d.GetOk("delete_service_on_destroy"); !ok {
if _, ok := d.GetOkExists("delete_service_on_destroy"); !ok {
if err := d.Set("delete_service_on_destroy", false); err != nil {
return fmt.Errorf("Error setting delete_service_on_destroy: %s", err)
}
Expand Down
2 changes: 1 addition & 1 deletion google/resource_big_query_dataset.go
Original file line number Diff line number Diff line change
Expand Up @@ -425,7 +425,7 @@ func resourceBigQueryDatasetRead(d *schema.ResourceData, meta interface{}) error
}

// Explicitly set virtual fields to default values if unset
if _, ok := d.GetOk("delete_contents_on_destroy"); !ok {
if _, ok := d.GetOkExists("delete_contents_on_destroy"); !ok {
if err := d.Set("delete_contents_on_destroy", false); err != nil {
return fmt.Errorf("Error setting delete_contents_on_destroy: %s", err)
}
Expand Down
2 changes: 1 addition & 1 deletion google/resource_cloud_run_service.go
Original file line number Diff line number Diff line change
Expand Up @@ -783,7 +783,7 @@ func resourceCloudRunServiceRead(d *schema.ResourceData, meta interface{}) error
}

// Explicitly set virtual fields to default values if unset
if _, ok := d.GetOk("autogenerate_revision_name"); !ok {
if _, ok := d.GetOkExists("autogenerate_revision_name"); !ok {
if err := d.Set("autogenerate_revision_name", false); err != nil {
return fmt.Errorf("Error setting autogenerate_revision_name: %s", err)
}
Expand Down
2 changes: 1 addition & 1 deletion google/resource_compute_network.go
Original file line number Diff line number Diff line change
Expand Up @@ -253,7 +253,7 @@ func resourceComputeNetworkRead(d *schema.ResourceData, meta interface{}) error
}

// Explicitly set virtual fields to default values if unset
if _, ok := d.GetOk("delete_default_routes_on_create"); !ok {
if _, ok := d.GetOkExists("delete_default_routes_on_create"); !ok {
if err := d.Set("delete_default_routes_on_create", false); err != nil {
return fmt.Errorf("Error setting delete_default_routes_on_create: %s", err)
}
Expand Down
6 changes: 3 additions & 3 deletions google/resource_compute_per_instance_config.go
Original file line number Diff line number Diff line change
Expand Up @@ -268,17 +268,17 @@ func resourceComputePerInstanceConfigRead(d *schema.ResourceData, meta interface
}

// Explicitly set virtual fields to default values if unset
if _, ok := d.GetOk("minimal_action"); !ok {
if _, ok := d.GetOkExists("minimal_action"); !ok {
if err := d.Set("minimal_action", "NONE"); err != nil {
return fmt.Errorf("Error setting minimal_action: %s", err)
}
}
if _, ok := d.GetOk("most_disruptive_allowed_action"); !ok {
if _, ok := d.GetOkExists("most_disruptive_allowed_action"); !ok {
if err := d.Set("most_disruptive_allowed_action", "REPLACE"); err != nil {
return fmt.Errorf("Error setting most_disruptive_allowed_action: %s", err)
}
}
if _, ok := d.GetOk("remove_instance_state_on_destroy"); !ok {
if _, ok := d.GetOkExists("remove_instance_state_on_destroy"); !ok {
if err := d.Set("remove_instance_state_on_destroy", false); err != nil {
return fmt.Errorf("Error setting remove_instance_state_on_destroy: %s", err)
}
Expand Down
6 changes: 3 additions & 3 deletions google/resource_compute_region_per_instance_config.go
Original file line number Diff line number Diff line change
Expand Up @@ -268,17 +268,17 @@ func resourceComputeRegionPerInstanceConfigRead(d *schema.ResourceData, meta int
}

// Explicitly set virtual fields to default values if unset
if _, ok := d.GetOk("minimal_action"); !ok {
if _, ok := d.GetOkExists("minimal_action"); !ok {
if err := d.Set("minimal_action", "NONE"); err != nil {
return fmt.Errorf("Error setting minimal_action: %s", err)
}
}
if _, ok := d.GetOk("most_disruptive_allowed_action"); !ok {
if _, ok := d.GetOkExists("most_disruptive_allowed_action"); !ok {
if err := d.Set("most_disruptive_allowed_action", "REPLACE"); err != nil {
return fmt.Errorf("Error setting most_disruptive_allowed_action: %s", err)
}
}
if _, ok := d.GetOk("remove_instance_state_on_destroy"); !ok {
if _, ok := d.GetOkExists("remove_instance_state_on_destroy"); !ok {
if err := d.Set("remove_instance_state_on_destroy", false); err != nil {
return fmt.Errorf("Error setting remove_instance_state_on_destroy: %s", err)
}
Expand Down
2 changes: 1 addition & 1 deletion google/resource_dns_managed_zone.go
Original file line number Diff line number Diff line change
Expand Up @@ -436,7 +436,7 @@ func resourceDNSManagedZoneRead(d *schema.ResourceData, meta interface{}) error
}

// Explicitly set virtual fields to default values if unset
if _, ok := d.GetOk("force_destroy"); !ok {
if _, ok := d.GetOkExists("force_destroy"); !ok {
if err := d.Set("force_destroy", false); err != nil {
return fmt.Errorf("Error setting force_destroy: %s", err)
}
Expand Down
19 changes: 19 additions & 0 deletions google/resource_spanner_database.go
Original file line number Diff line number Diff line change
Expand Up @@ -108,6 +108,11 @@ error in any statement, the database is not created.`,
Computed: true,
Description: `An explanation of the status of the database.`,
},
"deletion_protection": {
Type: schema.TypeBool,
Optional: true,
Default: true,
},
"project": {
Type: schema.TypeString,
Optional: true,
Expand Down Expand Up @@ -259,6 +264,12 @@ func resourceSpannerDatabaseRead(d *schema.ResourceData, meta interface{}) error
return nil
}

// Explicitly set virtual fields to default values if unset
if _, ok := d.GetOkExists("deletion_protection"); !ok {
if err := d.Set("deletion_protection", true); err != nil {
return fmt.Errorf("Error setting deletion_protection: %s", err)
}
}
if err := d.Set("project", project); err != nil {
return fmt.Errorf("Error reading Database: %s", err)
}
Expand Down Expand Up @@ -361,6 +372,9 @@ func resourceSpannerDatabaseDelete(d *schema.ResourceData, meta interface{}) err
}

var obj map[string]interface{}
if d.Get("deletion_protection").(bool) {
return fmt.Errorf("cannot destroy instance without setting deletion_protection=false and running `terraform apply`")
}
log.Printf("[DEBUG] Deleting Database %q", d.Id())

// err == nil indicates that the billing_project value was found
Expand Down Expand Up @@ -403,6 +417,11 @@ func resourceSpannerDatabaseImport(d *schema.ResourceData, meta interface{}) ([]
}
d.SetId(id)

// Explicitly set virtual fields to default values on import
if err := d.Set("deletion_protection", true); err != nil {
return nil, fmt.Errorf("Error setting deletion_protection: %s", err)
}

return []*schema.ResourceData{d}, nil
}

Expand Down
3 changes: 2 additions & 1 deletion google/resource_spanner_database_generated_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@ func TestAccSpannerDatabase_spannerDatabaseBasicExample(t *testing.T) {
ResourceName: "google_spanner_database.database",
ImportState: true,
ImportStateVerify: true,
ImportStateVerifyIgnore: []string{"ddl", "instance"},
ImportStateVerifyIgnore: []string{"ddl", "instance", "deletion_protection"},
},
},
})
Expand All @@ -66,6 +66,7 @@ resource "google_spanner_database" "database" {
"CREATE TABLE t1 (t1 INT64 NOT NULL,) PRIMARY KEY(t1)",
"CREATE TABLE t2 (t2 INT64 NOT NULL,) PRIMARY KEY(t2)",
]
deletion_protection = false
}
`, context)
}
Expand Down
4 changes: 4 additions & 0 deletions google/resource_spanner_database_iam_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -130,6 +130,7 @@ resource "google_spanner_instance" "instance" {
resource "google_spanner_database" "database" {
instance = google_spanner_instance.instance.name
name = "%s"
deletion_protection = false
}

resource "google_spanner_database_iam_binding" "foo" {
Expand Down Expand Up @@ -164,6 +165,7 @@ resource "google_spanner_instance" "instance" {
resource "google_spanner_database" "database" {
instance = google_spanner_instance.instance.name
name = "%s"
deletion_protection = false
}

resource "google_spanner_database_iam_binding" "foo" {
Expand Down Expand Up @@ -196,6 +198,7 @@ resource "google_spanner_instance" "instance" {
resource "google_spanner_database" "database" {
instance = google_spanner_instance.instance.name
name = "%s"
deletion_protection = false
}

resource "google_spanner_database_iam_member" "foo" {
Expand Down Expand Up @@ -225,6 +228,7 @@ resource "google_spanner_instance" "instance" {
resource "google_spanner_database" "database" {
instance = google_spanner_instance.instance.name
name = "%s"
deletion_protection = false
}

data "google_iam_policy" "foo" {
Expand Down
68 changes: 63 additions & 5 deletions google/resource_spanner_database_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package google

import (
"fmt"
"regexp"
"testing"

"github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource"
Expand Down Expand Up @@ -31,7 +32,7 @@ func TestAccSpannerDatabase_basic(t *testing.T) {
ResourceName: "google_spanner_database.basic",
ImportState: true,
ImportStateVerify: true,
ImportStateVerifyIgnore: []string{"ddl"},
ImportStateVerifyIgnore: []string{"ddl", "deletion_protection"},
},
{
Config: testAccSpannerDatabase_basicUpdate(instanceName, databaseName),
Expand All @@ -44,28 +45,28 @@ func TestAccSpannerDatabase_basic(t *testing.T) {
ResourceName: "google_spanner_database.basic",
ImportState: true,
ImportStateVerify: true,
ImportStateVerifyIgnore: []string{"ddl"},
ImportStateVerifyIgnore: []string{"ddl", "deletion_protection"},
},
{
ResourceName: "google_spanner_database.basic",
ImportStateId: fmt.Sprintf("projects/%s/instances/%s/databases/%s", project, instanceName, databaseName),
ImportState: true,
ImportStateVerify: true,
ImportStateVerifyIgnore: []string{"ddl"},
ImportStateVerifyIgnore: []string{"ddl", "deletion_protection"},
},
{
ResourceName: "google_spanner_database.basic",
ImportStateId: fmt.Sprintf("instances/%s/databases/%s", instanceName, databaseName),
ImportState: true,
ImportStateVerify: true,
ImportStateVerifyIgnore: []string{"ddl"},
ImportStateVerifyIgnore: []string{"ddl", "deletion_protection"},
},
{
ResourceName: "google_spanner_database.basic",
ImportStateId: fmt.Sprintf("%s/%s", instanceName, databaseName),
ImportState: true,
ImportStateVerify: true,
ImportStateVerifyIgnore: []string{"ddl"},
ImportStateVerifyIgnore: []string{"ddl", "deletion_protection"},
},
},
})
Expand All @@ -87,6 +88,7 @@ resource "google_spanner_database" "basic" {
"CREATE TABLE t1 (t1 INT64 NOT NULL,) PRIMARY KEY(t1)",
"CREATE TABLE t2 (t2 INT64 NOT NULL,) PRIMARY KEY(t2)",
]
deletion_protection = false
}
`, instanceName, instanceName, databaseName)
}
Expand All @@ -108,6 +110,7 @@ resource "google_spanner_database" "basic" {
"CREATE TABLE t2 (t2 INT64 NOT NULL,) PRIMARY KEY(t2)",
"CREATE TABLE t3 (t3 INT64 NOT NULL,) PRIMARY KEY(t3)",
]
deletion_protection = false
}
`, instanceName, instanceName, databaseName)
}
Expand Down Expand Up @@ -201,3 +204,58 @@ func TestSpannerDatabase_resourceSpannerDBDdlCustomDiffFuncForceNew(t *testing.T
}
}
}

func TestAccSpannerDatabase_deletionProtection(t *testing.T) {
skipIfVcr(t)
t.Parallel()

context := map[string]interface{}{
"random_suffix": randString(t, 10),
}

vcrTest(t, resource.TestCase{
PreCheck: func() { testAccPreCheck(t) },
Providers: testAccProviders,
ExternalProviders: map[string]resource.ExternalProvider{
"random": {},
},
CheckDestroy: testAccCheckSpannerDatabaseDestroyProducer(t),
Steps: []resource.TestStep{
{
Config: testAccSpannerDatabase_deletionProtection(context),
},
{
ResourceName: "google_spanner_database.database",
ImportState: true,
ImportStateVerify: true,
ImportStateVerifyIgnore: []string{"ddl", "instance", "deletion_protection"},
},
{
Config: testAccSpannerDatabase_deletionProtection(context),
Destroy: true,
ExpectError: regexp.MustCompile("deletion_protection"),
},
{
Config: testAccSpannerDatabase_spannerDatabaseBasicExample(context),
},
},
})
}

func testAccSpannerDatabase_deletionProtection(context map[string]interface{}) string {
return Nprintf(`
resource "google_spanner_instance" "main" {
config = "regional-europe-west1"
display_name = "main-instance"
}

resource "google_spanner_database" "database" {
instance = google_spanner_instance.main.name
name = "tf-test-my-database%{random_suffix}"
ddl = [
"CREATE TABLE t1 (t1 INT64 NOT NULL,) PRIMARY KEY(t1)",
"CREATE TABLE t2 (t2 INT64 NOT NULL,) PRIMARY KEY(t2)",
]
}
`, context)
}
10 changes: 9 additions & 1 deletion website/docs/r/spanner_database.html.markdown
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,12 @@ To get more information about Database, see:
* How-to Guides
* [Official Documentation](https://cloud.google.com/spanner/)

~> **Warning:** It is strongly recommended to set `lifecycle { prevent_destroy = true }` on databases in order to prevent accidental data loss. See [Terraform docs](https://www.terraform.io/docs/configuration/resources.html#prevent_destroy) for more information on lifecycle parameters.
~> **Warning:** On newer versions of the provider, you must explicitly set `deletion_protection=false`
(and run `terraform apply` to write the field to state) in order to destroy an instance.
It is recommended to not set this field (or set it to true) until you're ready to destroy.
On older versions, it is strongly recommended to set `lifecycle { prevent_destroy = true }`
on databases in order to prevent accidental data loss. See [Terraform docs](https://www.terraform.io/docs/configuration/resources.html#prevent_destroy)
for more information on lifecycle parameters.

<div class = "oics-button" style="float: right; margin: 0 0 -15px">
<a href="https://console.cloud.google.com/cloudshell/open?cloudshell_git_repo=https%3A%2F%2Fgit.luolix.top%2Fterraform-google-modules%2Fdocs-examples.git&cloudshell_working_dir=spanner_database_basic&cloudshell_image=gcr.io%2Fgraphite-cloud-shell-images%2Fterraform%3Alatest&open_in_editor=main.tf&cloudshell_print=.%2Fmotd&cloudshell_tutorial=.%2Ftutorial.md" target="_blank">
Expand All @@ -54,6 +59,7 @@ resource "google_spanner_database" "database" {
"CREATE TABLE t1 (t1 INT64 NOT NULL,) PRIMARY KEY(t1)",
"CREATE TABLE t2 (t2 INT64 NOT NULL,) PRIMARY KEY(t2)",
]
deletion_protection = false
}
```

Expand Down Expand Up @@ -85,6 +91,8 @@ The following arguments are supported:
* `project` - (Optional) The ID of the project in which the resource belongs.
If it is not provided, the provider project is used.

* `deletion_protection` - (Optional) 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.

## Attributes Reference

Expand Down