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

Support deleting Firestore databases #16576

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
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