Skip to content

Commit 2e84b0e

Browse files
Support deleting Firestore databases (#9450) (#16576)
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 <[email protected]>
1 parent 53571ee commit 2e84b0e

File tree

4 files changed

+174
-167
lines changed

4 files changed

+174
-167
lines changed

.changelog/9450.txt

+9
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
```release-note:enhancement
2+
firestore: enabled database deletion upon destroy for `google_firestore_database`
3+
```
4+
```release-note:enhancement
5+
firestore: added virtual field `deletion_policy` to `google_firestore_database`
6+
```
7+
```release-note:bug
8+
firestore: prevent destruction if both `deletion_policy` is `DELETE` and `delete_protection_state` is `DELETE_PROTECTION_ENABLED`
9+
```

google/services/firestore/resource_firestore_database.go

+72-5
Original file line numberDiff line numberDiff line change
@@ -99,7 +99,10 @@ for information about how to choose. Possible values: ["FIRESTORE_NATIVE", "DATA
9999
Computed: true,
100100
Optional: true,
101101
ValidateFunc: verify.ValidateEnum([]string{"DELETE_PROTECTION_STATE_UNSPECIFIED", "DELETE_PROTECTION_ENABLED", "DELETE_PROTECTION_DISABLED", ""}),
102-
Description: `State of delete protection for the database. Possible values: ["DELETE_PROTECTION_STATE_UNSPECIFIED", "DELETE_PROTECTION_ENABLED", "DELETE_PROTECTION_DISABLED"]`,
102+
Description: `State of delete protection for the database.
103+
When delete protection is enabled, this database cannot be deleted.
104+
The default value is 'DELETE_PROTECTION_STATE_UNSPECIFIED', which is currently equivalent to 'DELETE_PROTECTION_DISABLED'.
105+
**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"]`,
103106
},
104107
"point_in_time_recovery_enablement": {
105108
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
156159
Any read or query can specify a readTime within this window, and will read the state of the database at that time.
157160
If the PITR feature is enabled, the retention period is 7 days. Otherwise, the retention period is 1 hour.
158161
A duration in seconds with up to nine fractional digits, ending with 's'. Example: "3.5s".`,
162+
},
163+
"deletion_policy": {
164+
Type: schema.TypeString,
165+
Optional: true,
166+
Default: "ABANDON",
167+
Description: `Deletion behavior for this database.
168+
If the deletion policy is 'ABANDON', the database will be removed from Terraform state but not deleted from Google Cloud upon destruction.
169+
If the deletion policy is 'DELETE', the database will both be removed from Terraform state and deleted from Google Cloud upon destruction.
170+
The default value is 'ABANDON'.
171+
See also 'delete_protection'.`,
159172
},
160173
"project": {
161174
Type: schema.TypeString,
@@ -329,6 +342,12 @@ func resourceFirestoreDatabaseRead(d *schema.ResourceData, meta interface{}) err
329342
return transport_tpg.HandleNotFoundError(err, d, fmt.Sprintf("FirestoreDatabase %q", d.Id()))
330343
}
331344

345+
// Explicitly set virtual fields to default values if unset
346+
if _, ok := d.GetOkExists("deletion_policy"); !ok {
347+
if err := d.Set("deletion_policy", "ABANDON"); err != nil {
348+
return fmt.Errorf("Error setting deletion_policy: %s", err)
349+
}
350+
}
332351
if err := d.Set("project", project); err != nil {
333352
return fmt.Errorf("Error reading Database: %s", err)
334353
}
@@ -506,11 +525,54 @@ func resourceFirestoreDatabaseUpdate(d *schema.ResourceData, meta interface{}) e
506525
}
507526

508527
func resourceFirestoreDatabaseDelete(d *schema.ResourceData, meta interface{}) error {
509-
log.Printf("[WARNING] Firestore Database resources"+
510-
" cannot be deleted from Google Cloud. The resource %s will be removed from Terraform"+
511-
" state, but will still be present on Google Cloud.", d.Id())
512-
d.SetId("")
528+
config := meta.(*transport_tpg.Config)
529+
userAgent, err := tpgresource.GenerateUserAgentString(d, config.UserAgent)
530+
if err != nil {
531+
return err
532+
}
533+
534+
billingProject := ""
513535

536+
project, err := tpgresource.GetProject(d, config)
537+
if err != nil {
538+
return fmt.Errorf("Error fetching project for Database: %s", err)
539+
}
540+
billingProject = project
541+
542+
url, err := tpgresource.ReplaceVars(d, config, "{{FirestoreBasePath}}projects/{{project}}/databases/{{name}}")
543+
if err != nil {
544+
return err
545+
}
546+
547+
var obj map[string]interface{}
548+
if deletionPolicy := d.Get("deletion_policy"); deletionPolicy != "DELETE" {
549+
log.Printf("[WARN] Firestore database %q deletion_policy is not set to 'DELETE', skipping deletion", d.Get("name").(string))
550+
return nil
551+
}
552+
if deleteProtection := d.Get("delete_protection_state"); deleteProtection == "DELETE_PROTECTION_ENABLED" {
553+
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))
554+
}
555+
log.Printf("[DEBUG] Deleting Database %q", d.Id())
556+
557+
// err == nil indicates that the billing_project value was found
558+
if bp, err := tpgresource.GetBillingProject(d, config); err == nil {
559+
billingProject = bp
560+
}
561+
562+
res, err := transport_tpg.SendRequest(transport_tpg.SendRequestOptions{
563+
Config: config,
564+
Method: "DELETE",
565+
Project: billingProject,
566+
RawURL: url,
567+
UserAgent: userAgent,
568+
Body: obj,
569+
Timeout: d.Timeout(schema.TimeoutDelete),
570+
})
571+
if err != nil {
572+
return transport_tpg.HandleNotFoundError(err, d, "Database")
573+
}
574+
575+
log.Printf("[DEBUG] Finished deleting Database %q: %#v", d.Id(), res)
514576
return nil
515577
}
516578

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

596+
// Explicitly set virtual fields to default values on import
597+
if err := d.Set("deletion_policy", "ABANDON"); err != nil {
598+
return nil, fmt.Errorf("Error setting deletion_policy: %s", err)
599+
}
600+
534601
return []*schema.ResourceData{d}, nil
535602
}
536603

google/services/firestore/resource_firestore_database_generated_test.go

+69-96
Original file line numberDiff line numberDiff line change
@@ -18,20 +18,26 @@
1818
package firestore_test
1919

2020
import (
21+
"fmt"
22+
"strings"
2123
"testing"
2224

2325
"github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource"
26+
"github.com/hashicorp/terraform-plugin-sdk/v2/terraform"
2427

2528
"github.com/hashicorp/terraform-provider-google/google/acctest"
2629
"github.com/hashicorp/terraform-provider-google/google/envvar"
30+
"github.com/hashicorp/terraform-provider-google/google/tpgresource"
31+
transport_tpg "github.com/hashicorp/terraform-provider-google/google/transport"
2732
)
2833

2934
func TestAccFirestoreDatabase_firestoreDefaultDatabaseExample(t *testing.T) {
3035
t.Parallel()
3136

3237
context := map[string]interface{}{
33-
"org_id": envvar.GetTestOrgFromEnv(t),
34-
"random_suffix": acctest.RandString(t, 10),
38+
"project_id": envvar.GetTestProjectFromEnv(),
39+
"delete_protection_state": "DELETE_PROTECTION_DISABLED",
40+
"random_suffix": acctest.RandString(t, 10),
3541
}
3642

3743
acctest.VcrTest(t, resource.TestCase{
@@ -41,6 +47,7 @@ func TestAccFirestoreDatabase_firestoreDefaultDatabaseExample(t *testing.T) {
4147
"random": {},
4248
"time": {},
4349
},
50+
CheckDestroy: testAccCheckFirestoreDatabaseDestroyProducer(t),
4451
Steps: []resource.TestStep{
4552
{
4653
Config: testAccFirestoreDatabase_firestoreDefaultDatabaseExample(context),
@@ -49,40 +56,21 @@ func TestAccFirestoreDatabase_firestoreDefaultDatabaseExample(t *testing.T) {
4956
ResourceName: "google_firestore_database.database",
5057
ImportState: true,
5158
ImportStateVerify: true,
52-
ImportStateVerifyIgnore: []string{"project", "etag"},
59+
ImportStateVerifyIgnore: []string{"project", "etag", "deletion_policy"},
5360
},
5461
},
5562
})
5663
}
5764

5865
func testAccFirestoreDatabase_firestoreDefaultDatabaseExample(context map[string]interface{}) string {
5966
return acctest.Nprintf(`
60-
resource "google_project" "project" {
61-
project_id = "tf-test-my-project%{random_suffix}"
62-
name = "tf-test-my-project%{random_suffix}"
63-
org_id = "%{org_id}"
64-
}
65-
66-
resource "time_sleep" "wait_60_seconds" {
67-
depends_on = [google_project.project]
68-
69-
create_duration = "60s"
70-
}
71-
72-
resource "google_project_service" "firestore" {
73-
project = google_project.project.project_id
74-
service = "firestore.googleapis.com"
75-
# Needed for CI tests for permissions to propagate, should not be needed for actual usage
76-
depends_on = [time_sleep.wait_60_seconds]
77-
}
78-
7967
resource "google_firestore_database" "database" {
80-
project = google_project.project.project_id
81-
name = "(default)"
82-
location_id = "nam5"
83-
type = "FIRESTORE_NATIVE"
84-
85-
depends_on = [google_project_service.firestore]
68+
project = "%{project_id}"
69+
name = "(default)"
70+
location_id = "nam5"
71+
type = "FIRESTORE_NATIVE"
72+
delete_protection_state = "%{delete_protection_state}"
73+
deletion_policy = "DELETE"
8674
}
8775
`, context)
8876
}
@@ -91,13 +79,15 @@ func TestAccFirestoreDatabase_firestoreDatabaseExample(t *testing.T) {
9179
t.Parallel()
9280

9381
context := map[string]interface{}{
94-
"project_id": envvar.GetTestProjectFromEnv(),
95-
"random_suffix": acctest.RandString(t, 10),
82+
"project_id": envvar.GetTestProjectFromEnv(),
83+
"delete_protection_state": "DELETE_PROTECTION_DISABLED",
84+
"random_suffix": acctest.RandString(t, 10),
9685
}
9786

9887
acctest.VcrTest(t, resource.TestCase{
9988
PreCheck: func() { acctest.AccTestPreCheck(t) },
10089
ProtoV5ProviderFactories: acctest.ProtoV5ProviderFactories(t),
90+
CheckDestroy: testAccCheckFirestoreDatabaseDestroyProducer(t),
10191
Steps: []resource.TestStep{
10292
{
10393
Config: testAccFirestoreDatabase_firestoreDatabaseExample(context),
@@ -106,7 +96,7 @@ func TestAccFirestoreDatabase_firestoreDatabaseExample(t *testing.T) {
10696
ResourceName: "google_firestore_database.database",
10797
ImportState: true,
10898
ImportStateVerify: true,
109-
ImportStateVerifyIgnore: []string{"project", "etag"},
99+
ImportStateVerifyIgnore: []string{"project", "etag", "deletion_policy"},
110100
},
111101
},
112102
})
@@ -122,68 +112,8 @@ resource "google_firestore_database" "database" {
122112
concurrency_mode = "OPTIMISTIC"
123113
app_engine_integration_mode = "DISABLED"
124114
point_in_time_recovery_enablement = "POINT_IN_TIME_RECOVERY_ENABLED"
125-
}
126-
`, context)
127-
}
128-
129-
func TestAccFirestoreDatabase_firestoreDefaultDatabaseInDatastoreModeExample(t *testing.T) {
130-
t.Parallel()
131-
132-
context := map[string]interface{}{
133-
"org_id": envvar.GetTestOrgFromEnv(t),
134-
"random_suffix": acctest.RandString(t, 10),
135-
}
136-
137-
acctest.VcrTest(t, resource.TestCase{
138-
PreCheck: func() { acctest.AccTestPreCheck(t) },
139-
ProtoV5ProviderFactories: acctest.ProtoV5ProviderFactories(t),
140-
ExternalProviders: map[string]resource.ExternalProvider{
141-
"random": {},
142-
"time": {},
143-
},
144-
Steps: []resource.TestStep{
145-
{
146-
Config: testAccFirestoreDatabase_firestoreDefaultDatabaseInDatastoreModeExample(context),
147-
},
148-
{
149-
ResourceName: "google_firestore_database.datastore_mode_database",
150-
ImportState: true,
151-
ImportStateVerify: true,
152-
ImportStateVerifyIgnore: []string{"project", "etag"},
153-
},
154-
},
155-
})
156-
}
157-
158-
func testAccFirestoreDatabase_firestoreDefaultDatabaseInDatastoreModeExample(context map[string]interface{}) string {
159-
return acctest.Nprintf(`
160-
resource "google_project" "project" {
161-
project_id = "tf-test%{random_suffix}"
162-
name = "tf-test%{random_suffix}"
163-
org_id = "%{org_id}"
164-
}
165-
166-
resource "time_sleep" "wait_60_seconds" {
167-
depends_on = [google_project.project]
168-
create_duration = "60s"
169-
}
170-
171-
resource "google_project_service" "firestore" {
172-
project = google_project.project.project_id
173-
service = "firestore.googleapis.com"
174-
# Needed for CI tests for permissions to propagate, should not be needed for actual usage
175-
depends_on = [time_sleep.wait_60_seconds]
176-
}
177-
178-
resource "google_firestore_database" "datastore_mode_database" {
179-
project = google_project.project.project_id
180-
181-
name = "(default)"
182-
183-
location_id = "nam5"
184-
type = "DATASTORE_MODE"
185-
186-
depends_on = [google_project_service.firestore]
115+
delete_protection_state = "%{delete_protection_state}"
116+
deletion_policy = "DELETE"
187117
}
188118
`, context)
189119
}
@@ -192,13 +122,15 @@ func TestAccFirestoreDatabase_firestoreDatabaseInDatastoreModeExample(t *testing
192122
t.Parallel()
193123

194124
context := map[string]interface{}{
195-
"project_id": envvar.GetTestProjectFromEnv(),
196-
"random_suffix": acctest.RandString(t, 10),
125+
"project_id": envvar.GetTestProjectFromEnv(),
126+
"delete_protection_state": "DELETE_PROTECTION_DISABLED",
127+
"random_suffix": acctest.RandString(t, 10),
197128
}
198129

199130
acctest.VcrTest(t, resource.TestCase{
200131
PreCheck: func() { acctest.AccTestPreCheck(t) },
201132
ProtoV5ProviderFactories: acctest.ProtoV5ProviderFactories(t),
133+
CheckDestroy: testAccCheckFirestoreDatabaseDestroyProducer(t),
202134
Steps: []resource.TestStep{
203135
{
204136
Config: testAccFirestoreDatabase_firestoreDatabaseInDatastoreModeExample(context),
@@ -207,7 +139,7 @@ func TestAccFirestoreDatabase_firestoreDatabaseInDatastoreModeExample(t *testing
207139
ResourceName: "google_firestore_database.datastore_mode_database",
208140
ImportState: true,
209141
ImportStateVerify: true,
210-
ImportStateVerifyIgnore: []string{"project", "etag"},
142+
ImportStateVerifyIgnore: []string{"project", "etag", "deletion_policy"},
211143
},
212144
},
213145
})
@@ -223,6 +155,47 @@ resource "google_firestore_database" "datastore_mode_database" {
223155
concurrency_mode = "OPTIMISTIC"
224156
app_engine_integration_mode = "DISABLED"
225157
point_in_time_recovery_enablement = "POINT_IN_TIME_RECOVERY_ENABLED"
158+
delete_protection_state = "%{delete_protection_state}"
159+
deletion_policy = "DELETE"
226160
}
227161
`, context)
228162
}
163+
164+
func testAccCheckFirestoreDatabaseDestroyProducer(t *testing.T) func(s *terraform.State) error {
165+
return func(s *terraform.State) error {
166+
for name, rs := range s.RootModule().Resources {
167+
if rs.Type != "google_firestore_database" {
168+
continue
169+
}
170+
if strings.HasPrefix(name, "data.") {
171+
continue
172+
}
173+
174+
config := acctest.GoogleProviderConfig(t)
175+
176+
url, err := tpgresource.ReplaceVarsForTest(config, rs, "{{FirestoreBasePath}}projects/{{project}}/databases/{{name}}")
177+
if err != nil {
178+
return err
179+
}
180+
181+
billingProject := ""
182+
183+
if config.BillingProject != "" {
184+
billingProject = config.BillingProject
185+
}
186+
187+
_, err = transport_tpg.SendRequest(transport_tpg.SendRequestOptions{
188+
Config: config,
189+
Method: "GET",
190+
Project: billingProject,
191+
RawURL: url,
192+
UserAgent: config.UserAgent,
193+
})
194+
if err == nil {
195+
return fmt.Errorf("FirestoreDatabase still exists at %s", url)
196+
}
197+
}
198+
199+
return nil
200+
}
201+
}

0 commit comments

Comments
 (0)