diff --git a/.changelog/9450.txt b/.changelog/9450.txt new file mode 100644 index 0000000000..fad36204bd --- /dev/null +++ b/.changelog/9450.txt @@ -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` +``` diff --git a/google-beta/services/firestore/resource_firestore_database.go b/google-beta/services/firestore/resource_firestore_database.go index 448b466871..aa188fdba0 100644 --- a/google-beta/services/firestore/resource_firestore_database.go +++ b/google-beta/services/firestore/resource_firestore_database.go @@ -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, @@ -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, @@ -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) } @@ -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 } @@ -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 } diff --git a/google-beta/services/firestore/resource_firestore_database_generated_test.go b/google-beta/services/firestore/resource_firestore_database_generated_test.go index c75a1e289b..4691b7a65e 100644 --- a/google-beta/services/firestore/resource_firestore_database_generated_test.go +++ b/google-beta/services/firestore/resource_firestore_database_generated_test.go @@ -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-beta/google-beta/acctest" "github.com/hashicorp/terraform-provider-google-beta/google-beta/envvar" + "github.com/hashicorp/terraform-provider-google-beta/google-beta/tpgresource" + transport_tpg "github.com/hashicorp/terraform-provider-google-beta/google-beta/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{ @@ -41,6 +47,7 @@ func TestAccFirestoreDatabase_firestoreDefaultDatabaseExample(t *testing.T) { "random": {}, "time": {}, }, + CheckDestroy: testAccCheckFirestoreDatabaseDestroyProducer(t), Steps: []resource.TestStep{ { Config: testAccFirestoreDatabase_firestoreDefaultDatabaseExample(context), @@ -49,7 +56,7 @@ 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"}, }, }, }) @@ -57,32 +64,13 @@ func TestAccFirestoreDatabase_firestoreDefaultDatabaseExample(t *testing.T) { 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) } @@ -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), @@ -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"}, }, }, }) @@ -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) } @@ -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), @@ -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"}, }, }, }) @@ -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 + } +} diff --git a/website/docs/r/firestore_database.html.markdown b/website/docs/r/firestore_database.html.markdown index 17c66e5946..66ca7f102d 100644 --- a/website/docs/r/firestore_database.html.markdown +++ b/website/docs/r/firestore_database.html.markdown @@ -38,32 +38,13 @@ To get more information about Database, see: ```hcl -resource "google_project" "project" { - project_id = "my-project" - name = "my-project" - org_id = "123456789" -} - -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 = "my-project-name" + name = "(default)" + location_id = "nam5" + type = "FIRESTORE_NATIVE" + delete_protection_state = "DELETE_PROTECTION_ENABLED" + deletion_policy = "DELETE" } ``` ## Example Usage - Firestore Database @@ -78,39 +59,21 @@ resource "google_firestore_database" "database" { concurrency_mode = "OPTIMISTIC" app_engine_integration_mode = "DISABLED" point_in_time_recovery_enablement = "POINT_IN_TIME_RECOVERY_ENABLED" + delete_protection_state = "DELETE_PROTECTION_ENABLED" + deletion_policy = "DELETE" } ``` ## Example Usage - Firestore Default Database In Datastore Mode ```hcl -resource "google_project" "project" { - project_id = "tf-test%{random_suffix}" - name = "tf-test%{random_suffix}" - org_id = "123456789" -} - -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] + project = "my-project-name" + name = "(default)" + location_id = "nam5" + type = "DATASTORE_MODE" + delete_protection_state = "DELETE_PROTECTION_ENABLED" + deletion_policy = "DELETE" } ``` ## Example Usage - Firestore Database In Datastore Mode @@ -125,22 +88,8 @@ 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" -} -``` -## Example Usage - Firestore Database With Delete Protection - - -```hcl -resource "google_firestore_database" "database" { - project = "my-project-name" - name = "example-database-id" - location_id = "nam5" - type = "FIRESTORE_NATIVE" - - # Prevents accidental deletion of the database. - # To delete the database, first set this field to `DELETE_PROTECTION_DISABLED`, apply the changes. - # Then delete the database resource and apply the changes again. delete_protection_state = "DELETE_PROTECTION_ENABLED" + deletion_policy = "DELETE" } ``` @@ -197,11 +146,20 @@ The following arguments are supported: * `delete_protection_state` - (Optional) 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 are: `DELETE_PROTECTION_STATE_UNSPECIFIED`, `DELETE_PROTECTION_ENABLED`, `DELETE_PROTECTION_DISABLED`. * `project` - (Optional) The ID of the project in which the resource belongs. If it is not provided, the provider project is used. +* `deletion_policy` - (Optional) 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`. + ## Attributes Reference