Skip to content

Commit

Permalink
Support deleting Firestore databases (#9450) (#16576)
Browse files Browse the repository at this point in the history
Due to backwards compatibility concerns, the default behavior remains to
abandon the database upon destroy rather than to actually delete it.

To actually delete the database, you must set deletion_policy to DELETE,
and apply if necessary, before running `terraform destroy`.

This also cleans up some related deletion-related docs and bugs:

* Updates the delete protection docs
* delete_protection_state being enabled with deletion_policy = DELETE fails the destroy

Fixes #16488
Fixes #16404
Fixes #16325
[upstream:4829cc4a4f604db5d6e1d09a7c85df6250ebc19a]

Signed-off-by: Modular Magician <magic-modules@google.com>
  • Loading branch information
modular-magician authored Nov 21, 2023
1 parent 53571ee commit 2e84b0e
Show file tree
Hide file tree
Showing 4 changed files with 174 additions and 167 deletions.
9 changes: 9 additions & 0 deletions .changelog/9450.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
```release-note:enhancement
firestore: enabled database deletion upon destroy for `google_firestore_database`
```
```release-note:enhancement
firestore: added virtual field `deletion_policy` to `google_firestore_database`
```
```release-note:bug
firestore: prevent destruction if both `deletion_policy` is `DELETE` and `delete_protection_state` is `DELETE_PROTECTION_ENABLED`
```
77 changes: 72 additions & 5 deletions google/services/firestore/resource_firestore_database.go
Original file line number Diff line number Diff line change
Expand Up @@ -99,7 +99,10 @@ for information about how to choose. Possible values: ["FIRESTORE_NATIVE", "DATA
Computed: true,
Optional: true,
ValidateFunc: verify.ValidateEnum([]string{"DELETE_PROTECTION_STATE_UNSPECIFIED", "DELETE_PROTECTION_ENABLED", "DELETE_PROTECTION_DISABLED", ""}),
Description: `State of delete protection for the database. Possible values: ["DELETE_PROTECTION_STATE_UNSPECIFIED", "DELETE_PROTECTION_ENABLED", "DELETE_PROTECTION_DISABLED"]`,
Description: `State of delete protection for the database.
When delete protection is enabled, this database cannot be deleted.
The default value is 'DELETE_PROTECTION_STATE_UNSPECIFIED', which is currently equivalent to 'DELETE_PROTECTION_DISABLED'.
**Note:** Additionally, to delete this database using 'terraform destroy', 'deletion_policy' must be set to 'DELETE'. Possible values: ["DELETE_PROTECTION_STATE_UNSPECIFIED", "DELETE_PROTECTION_ENABLED", "DELETE_PROTECTION_DISABLED"]`,
},
"point_in_time_recovery_enablement": {
Type: schema.TypeString,
Expand Down Expand Up @@ -156,6 +159,16 @@ This value may be empty in which case the appid to use for URL-encoded keys is t
Any read or query can specify a readTime within this window, and will read the state of the database at that time.
If the PITR feature is enabled, the retention period is 7 days. Otherwise, the retention period is 1 hour.
A duration in seconds with up to nine fractional digits, ending with 's'. Example: "3.5s".`,
},
"deletion_policy": {
Type: schema.TypeString,
Optional: true,
Default: "ABANDON",
Description: `Deletion behavior for this database.
If the deletion policy is 'ABANDON', the database will be removed from Terraform state but not deleted from Google Cloud upon destruction.
If the deletion policy is 'DELETE', the database will both be removed from Terraform state and deleted from Google Cloud upon destruction.
The default value is 'ABANDON'.
See also 'delete_protection'.`,
},
"project": {
Type: schema.TypeString,
Expand Down Expand Up @@ -329,6 +342,12 @@ func resourceFirestoreDatabaseRead(d *schema.ResourceData, meta interface{}) err
return transport_tpg.HandleNotFoundError(err, d, fmt.Sprintf("FirestoreDatabase %q", d.Id()))
}

// Explicitly set virtual fields to default values if unset
if _, ok := d.GetOkExists("deletion_policy"); !ok {
if err := d.Set("deletion_policy", "ABANDON"); err != nil {
return fmt.Errorf("Error setting deletion_policy: %s", err)
}
}
if err := d.Set("project", project); err != nil {
return fmt.Errorf("Error reading Database: %s", err)
}
Expand Down Expand Up @@ -506,11 +525,54 @@ func resourceFirestoreDatabaseUpdate(d *schema.ResourceData, meta interface{}) e
}

func resourceFirestoreDatabaseDelete(d *schema.ResourceData, meta interface{}) error {
log.Printf("[WARNING] Firestore Database resources"+
" cannot be deleted from Google Cloud. The resource %s will be removed from Terraform"+
" state, but will still be present on Google Cloud.", d.Id())
d.SetId("")
config := meta.(*transport_tpg.Config)
userAgent, err := tpgresource.GenerateUserAgentString(d, config.UserAgent)
if err != nil {
return err
}

billingProject := ""

project, err := tpgresource.GetProject(d, config)
if err != nil {
return fmt.Errorf("Error fetching project for Database: %s", err)
}
billingProject = project

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

var obj map[string]interface{}
if deletionPolicy := d.Get("deletion_policy"); deletionPolicy != "DELETE" {
log.Printf("[WARN] Firestore database %q deletion_policy is not set to 'DELETE', skipping deletion", d.Get("name").(string))
return nil
}
if deleteProtection := d.Get("delete_protection_state"); deleteProtection == "DELETE_PROTECTION_ENABLED" {
return fmt.Errorf("Cannot delete Firestore database %s: Delete Protection is enabled. Set delete_protection_state to DELETE_PROTECTION_DISABLED for this resource and run \"terraform apply\" before attempting to delete it.", d.Get("name").(string))
}
log.Printf("[DEBUG] Deleting Database %q", d.Id())

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

res, err := transport_tpg.SendRequest(transport_tpg.SendRequestOptions{
Config: config,
Method: "DELETE",
Project: billingProject,
RawURL: url,
UserAgent: userAgent,
Body: obj,
Timeout: d.Timeout(schema.TimeoutDelete),
})
if err != nil {
return transport_tpg.HandleNotFoundError(err, d, "Database")
}

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

Expand All @@ -531,6 +593,11 @@ func resourceFirestoreDatabaseImport(d *schema.ResourceData, meta interface{}) (
}
d.SetId(id)

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

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

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,20 +18,26 @@
package firestore_test

import (
"fmt"
"strings"
"testing"

"github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource"
"github.com/hashicorp/terraform-plugin-sdk/v2/terraform"

"github.com/hashicorp/terraform-provider-google/google/acctest"
"github.com/hashicorp/terraform-provider-google/google/envvar"
"github.com/hashicorp/terraform-provider-google/google/tpgresource"
transport_tpg "github.com/hashicorp/terraform-provider-google/google/transport"
)

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

context := map[string]interface{}{
"org_id": envvar.GetTestOrgFromEnv(t),
"random_suffix": acctest.RandString(t, 10),
"project_id": envvar.GetTestProjectFromEnv(),
"delete_protection_state": "DELETE_PROTECTION_DISABLED",
"random_suffix": acctest.RandString(t, 10),
}

acctest.VcrTest(t, resource.TestCase{
Expand All @@ -41,6 +47,7 @@ func TestAccFirestoreDatabase_firestoreDefaultDatabaseExample(t *testing.T) {
"random": {},
"time": {},
},
CheckDestroy: testAccCheckFirestoreDatabaseDestroyProducer(t),
Steps: []resource.TestStep{
{
Config: testAccFirestoreDatabase_firestoreDefaultDatabaseExample(context),
Expand All @@ -49,40 +56,21 @@ func TestAccFirestoreDatabase_firestoreDefaultDatabaseExample(t *testing.T) {
ResourceName: "google_firestore_database.database",
ImportState: true,
ImportStateVerify: true,
ImportStateVerifyIgnore: []string{"project", "etag"},
ImportStateVerifyIgnore: []string{"project", "etag", "deletion_policy"},
},
},
})
}

func testAccFirestoreDatabase_firestoreDefaultDatabaseExample(context map[string]interface{}) string {
return acctest.Nprintf(`
resource "google_project" "project" {
project_id = "tf-test-my-project%{random_suffix}"
name = "tf-test-my-project%{random_suffix}"
org_id = "%{org_id}"
}
resource "time_sleep" "wait_60_seconds" {
depends_on = [google_project.project]
create_duration = "60s"
}
resource "google_project_service" "firestore" {
project = google_project.project.project_id
service = "firestore.googleapis.com"
# Needed for CI tests for permissions to propagate, should not be needed for actual usage
depends_on = [time_sleep.wait_60_seconds]
}
resource "google_firestore_database" "database" {
project = google_project.project.project_id
name = "(default)"
location_id = "nam5"
type = "FIRESTORE_NATIVE"
depends_on = [google_project_service.firestore]
project = "%{project_id}"
name = "(default)"
location_id = "nam5"
type = "FIRESTORE_NATIVE"
delete_protection_state = "%{delete_protection_state}"
deletion_policy = "DELETE"
}
`, context)
}
Expand All @@ -91,13 +79,15 @@ func TestAccFirestoreDatabase_firestoreDatabaseExample(t *testing.T) {
t.Parallel()

context := map[string]interface{}{
"project_id": envvar.GetTestProjectFromEnv(),
"random_suffix": acctest.RandString(t, 10),
"project_id": envvar.GetTestProjectFromEnv(),
"delete_protection_state": "DELETE_PROTECTION_DISABLED",
"random_suffix": acctest.RandString(t, 10),
}

acctest.VcrTest(t, resource.TestCase{
PreCheck: func() { acctest.AccTestPreCheck(t) },
ProtoV5ProviderFactories: acctest.ProtoV5ProviderFactories(t),
CheckDestroy: testAccCheckFirestoreDatabaseDestroyProducer(t),
Steps: []resource.TestStep{
{
Config: testAccFirestoreDatabase_firestoreDatabaseExample(context),
Expand All @@ -106,7 +96,7 @@ func TestAccFirestoreDatabase_firestoreDatabaseExample(t *testing.T) {
ResourceName: "google_firestore_database.database",
ImportState: true,
ImportStateVerify: true,
ImportStateVerifyIgnore: []string{"project", "etag"},
ImportStateVerifyIgnore: []string{"project", "etag", "deletion_policy"},
},
},
})
Expand All @@ -122,68 +112,8 @@ resource "google_firestore_database" "database" {
concurrency_mode = "OPTIMISTIC"
app_engine_integration_mode = "DISABLED"
point_in_time_recovery_enablement = "POINT_IN_TIME_RECOVERY_ENABLED"
}
`, context)
}

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

context := map[string]interface{}{
"org_id": envvar.GetTestOrgFromEnv(t),
"random_suffix": acctest.RandString(t, 10),
}

acctest.VcrTest(t, resource.TestCase{
PreCheck: func() { acctest.AccTestPreCheck(t) },
ProtoV5ProviderFactories: acctest.ProtoV5ProviderFactories(t),
ExternalProviders: map[string]resource.ExternalProvider{
"random": {},
"time": {},
},
Steps: []resource.TestStep{
{
Config: testAccFirestoreDatabase_firestoreDefaultDatabaseInDatastoreModeExample(context),
},
{
ResourceName: "google_firestore_database.datastore_mode_database",
ImportState: true,
ImportStateVerify: true,
ImportStateVerifyIgnore: []string{"project", "etag"},
},
},
})
}

func testAccFirestoreDatabase_firestoreDefaultDatabaseInDatastoreModeExample(context map[string]interface{}) string {
return acctest.Nprintf(`
resource "google_project" "project" {
project_id = "tf-test%{random_suffix}"
name = "tf-test%{random_suffix}"
org_id = "%{org_id}"
}
resource "time_sleep" "wait_60_seconds" {
depends_on = [google_project.project]
create_duration = "60s"
}
resource "google_project_service" "firestore" {
project = google_project.project.project_id
service = "firestore.googleapis.com"
# Needed for CI tests for permissions to propagate, should not be needed for actual usage
depends_on = [time_sleep.wait_60_seconds]
}
resource "google_firestore_database" "datastore_mode_database" {
project = google_project.project.project_id
name = "(default)"
location_id = "nam5"
type = "DATASTORE_MODE"
depends_on = [google_project_service.firestore]
delete_protection_state = "%{delete_protection_state}"
deletion_policy = "DELETE"
}
`, context)
}
Expand All @@ -192,13 +122,15 @@ func TestAccFirestoreDatabase_firestoreDatabaseInDatastoreModeExample(t *testing
t.Parallel()

context := map[string]interface{}{
"project_id": envvar.GetTestProjectFromEnv(),
"random_suffix": acctest.RandString(t, 10),
"project_id": envvar.GetTestProjectFromEnv(),
"delete_protection_state": "DELETE_PROTECTION_DISABLED",
"random_suffix": acctest.RandString(t, 10),
}

acctest.VcrTest(t, resource.TestCase{
PreCheck: func() { acctest.AccTestPreCheck(t) },
ProtoV5ProviderFactories: acctest.ProtoV5ProviderFactories(t),
CheckDestroy: testAccCheckFirestoreDatabaseDestroyProducer(t),
Steps: []resource.TestStep{
{
Config: testAccFirestoreDatabase_firestoreDatabaseInDatastoreModeExample(context),
Expand All @@ -207,7 +139,7 @@ func TestAccFirestoreDatabase_firestoreDatabaseInDatastoreModeExample(t *testing
ResourceName: "google_firestore_database.datastore_mode_database",
ImportState: true,
ImportStateVerify: true,
ImportStateVerifyIgnore: []string{"project", "etag"},
ImportStateVerifyIgnore: []string{"project", "etag", "deletion_policy"},
},
},
})
Expand All @@ -223,6 +155,47 @@ resource "google_firestore_database" "datastore_mode_database" {
concurrency_mode = "OPTIMISTIC"
app_engine_integration_mode = "DISABLED"
point_in_time_recovery_enablement = "POINT_IN_TIME_RECOVERY_ENABLED"
delete_protection_state = "%{delete_protection_state}"
deletion_policy = "DELETE"
}
`, context)
}

func testAccCheckFirestoreDatabaseDestroyProducer(t *testing.T) func(s *terraform.State) error {
return func(s *terraform.State) error {
for name, rs := range s.RootModule().Resources {
if rs.Type != "google_firestore_database" {
continue
}
if strings.HasPrefix(name, "data.") {
continue
}

config := acctest.GoogleProviderConfig(t)

url, err := tpgresource.ReplaceVarsForTest(config, rs, "{{FirestoreBasePath}}projects/{{project}}/databases/{{name}}")
if err != nil {
return err
}

billingProject := ""

if config.BillingProject != "" {
billingProject = config.BillingProject
}

_, err = transport_tpg.SendRequest(transport_tpg.SendRequestOptions{
Config: config,
Method: "GET",
Project: billingProject,
RawURL: url,
UserAgent: config.UserAgent,
})
if err == nil {
return fmt.Errorf("FirestoreDatabase still exists at %s", url)
}
}

return nil
}
}
Loading

0 comments on commit 2e84b0e

Please sign in to comment.