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

Add delete support and handwriten sweeper for Firebase Web App #6652

Merged
14 changes: 13 additions & 1 deletion mmv1/products/firebase/api.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -107,6 +107,8 @@ objects:
self_link: '{{name}}'
update_verb: :PATCH
update_mask: true
delete_verb: :POST
delete_url: '{{name}}:remove'
description: |
A Google Cloud Firebase web application instance
references: !ruby/object:Api::Resource::ReferenceLinks
Expand All @@ -115,7 +117,7 @@ objects:
'https://firebase.google.com/'
api: 'https://firebase.google.com/docs/projects/api/reference/rest/v1beta1/projects.webApps'
async: !ruby/object:Api::OpAsync
actions: ["create"]
actions: ["create", "delete"]
operation: !ruby/object:Api::OpAsync::Operation
path: 'name'
base_url: '{{op_id}}'
Expand All @@ -132,6 +134,16 @@ objects:
error: !ruby/object:Api::OpAsync::Error
path: 'error'
message: 'message'
parameters:
- !ruby/object:Api::Type::String
name: 'deletion_policy'
description: |
(Optional) Set to `ABANDON` to allow the WebApp to be untracked from terraform state
rather than deleted upon `terraform destroy`. This is useful becaue the WebApp may be
serving traffic. Set to `DELETE` to delete the WebApp. Default to `ABANDON`
input: true
url_param_only: true
default_value: ABANDON
properties:
- !ruby/object:Api::Type::String
name: name
Expand Down
4 changes: 2 additions & 2 deletions mmv1/products/firebase/terraform.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -41,8 +41,7 @@ overrides: !ruby/object:Overrides::ResourceOverrides
WebApp: !ruby/object:Overrides::Terraform::ResourceOverride
import_format: ['{{project}} {{name}}']
autogen_async: true
skip_delete: true #currently only able to delete a webapp through the Firebase Admin console
skip_sweeper: true
skip_sweeper: true # Skip sweeper generation and use hard-delete in hand-written sweeper
examples:
- !ruby/object:Provider::Terraform::Examples
name: "firebase_web_app_basic"
Expand All @@ -57,6 +56,7 @@ overrides: !ruby/object:Overrides::ResourceOverrides
- project
custom_code: !ruby/object:Provider::Terraform::CustomCode
custom_import: templates/terraform/custom_import/self_link_as_name.erb
custom_delete: templates/terraform/custom_delete/firebase_app_deletion_policy.erb
# This is for copying files over
files: !ruby/object:Provider::Config::Files
# These files have templating (ERB) code that will be run.
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
// Handwritten
obj := make(map[string]interface{})
if d.Get("deletion_policy") == "DELETE" {
obj["immediate"] = true
} else {
fmt.Printf("Skip deleting App %q due to deletion_policy: %q\n", d.Id(), d.Get("deletion_policy"))
return nil
}
// End of Handwritten
billingProject := ""

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

url, err := replaceVars(d, config, "{{FirebaseBasePath}}{{name}}:remove")
if err != nil {
return err
}

log.Printf("[DEBUG] Deleting App %q", d.Id())

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

res, err := sendRequestWithTimeout(config, "POST", billingProject, url, userAgent, obj, d.Timeout(schema.TimeoutDelete))
if err != nil {
return handleNotFoundError(err, d, "App")
}

err = firebaseOperationWaitTime(
config, res, project, "Deleting App", userAgent,
d.Timeout(schema.TimeoutDelete))

if err != nil {
return err
}

log.Printf("[DEBUG] Finished deleting App %q: %#v", d.Id(), res)
return nil
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ resource "google_firebase_web_app" "<%= ctx[:primary_resource_id] %>" {
provider = google-beta
project = google_project.default.project_id
display_name = "<%= ctx[:vars]['display_name'] %>"
deletion_policy = "DELETE"

depends_on = [google_firebase_project.default]
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,119 @@
// ----------------------------------------------------------------------------
//
// *** HANDWRITTEN CODE *** Type: MMv1 ***
//
// ----------------------------------------------------------------------------

package google

import (
"context"
"log"
"strings"
"testing"

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

func init() {
resource.AddTestSweepers("FirebaseWebApp", &resource.Sweeper{
Name: "FirebaseWebApp",
F: testSweepFirebaseWebApp,
})
}

// At the time of writing, the CI only passes us-central1 as the region
func testSweepFirebaseWebApp(region string) error {
resourceName := "FirebaseWebApp"
log.Printf("[INFO][SWEEPER_LOG] Starting sweeper for %s", resourceName)

config, err := sharedConfigForRegion(region)
if err != nil {
log.Printf("[INFO][SWEEPER_LOG] error getting shared config for region: %s", err)
return err
}

err = config.LoadAndValidate(context.Background())
if err != nil {
log.Printf("[INFO][SWEEPER_LOG] error loading: %s", err)
return err
}

t := &testing.T{}
billingId := getTestBillingAccountFromEnv(t)

// Setup variables to replace in list template
d := &ResourceDataMock{
FieldsInSchema: map[string]interface{}{
"project": config.Project,
"region": region,
"location": region,
"zone": "-",
"billing_account": billingId,
},
}

listTemplate := strings.Split("https://firebase.googleapis.com/v1beta1/projects/{{project}}/webApps", "?")[0]
listUrl, err := replaceVars(d, config, listTemplate)
if err != nil {
log.Printf("[INFO][SWEEPER_LOG] error preparing sweeper list url: %s", err)
return nil
}

res, err := sendRequest(config, "GET", config.Project, listUrl, config.userAgent, nil)
if err != nil {
log.Printf("[INFO][SWEEPER_LOG] Error in response from request %s: %s", listUrl, err)
return nil
}

resourceList, ok := res["webApps"]
if !ok {
log.Printf("[INFO][SWEEPER_LOG] Nothing found in response.")
return nil
}

rl := resourceList.([]interface{})

log.Printf("[INFO][SWEEPER_LOG] Found %d items in %s list response.", len(rl), resourceName)
// Keep count of items that aren't sweepable for logging.
nonPrefixCount := 0
for _, ri := range rl {
obj := ri.(map[string]interface{})
if obj["name"] == nil {
log.Printf("[INFO][SWEEPER_LOG] %s resource name was nil", resourceName)
return nil
}

name := GetResourceNameFromSelfLink(obj["name"].(string))
// Skip resources that shouldn't be sweeped
if !isSweepableTestResource(name) {
nonPrefixCount++
continue
}

deleteTemplate := "https://firebase.googleapis.com/v1beta1/{{name}}:remove"
deleteUrl, err := replaceVars(d, config, deleteTemplate)
if err != nil {
log.Printf("[INFO][SWEEPER_LOG] error preparing delete url: %s", err)
return nil
}
deleteUrl = deleteUrl + name

body := make(map[string]interface{})
body["immediate"] = true

// Don't wait on operations as we may have a lot to delete
_, err = sendRequest(config, "POST", config.Project, deleteUrl, config.userAgent, body)
if err != nil {
log.Printf("[INFO][SWEEPER_LOG] Error deleting for url %s : %s", deleteUrl, err)
} else {
log.Printf("[INFO][SWEEPER_LOG] Sent delete request for %s resource: %s", resourceName, name)
}
}

if nonPrefixCount > 0 {
log.Printf("[INFO][SWEEPER_LOG] %d items were non-sweepable and skipped.", nonPrefixCount)
}

return nil
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,12 @@ package google
<% unless version == 'ga' -%>

import (
"fmt"
"strings"
"testing"

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

func TestAccFirebaseWebApp_firebaseWebAppFull(t *testing.T) {
Expand Down Expand Up @@ -66,4 +69,75 @@ data "google_firebase_web_app_config" "default" {
}
`, context)
}

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

context := map[string]interface{}{
"project_id": getTestProjectFromEnv(),
"random_suffix": randString(t, 10),
"display_name": "Display Name N",
}

vcrTest(t, resource.TestCase{
PreCheck: func() { testAccPreCheck(t) },
Providers: testAccProvidersOiCS,
CheckDestroy: testAccCheckFirebaseWebAppNotDestroyedProducer(t),
Steps: []resource.TestStep{
{
Config: testAccFirebaseWebApp_firebaseWebAppSkipDelete(context, ""),
},
{
ResourceName: "google_firebase_web_app.skip_delete",
ImportState: true,
ImportStateVerify: true,
ImportStateVerifyIgnore: []string{"deletion_policy", "project"},
},
},
})
}

func testAccFirebaseWebApp_firebaseWebAppSkipDelete(context map[string]interface{}, update string) string {
return Nprintf(`
resource "google_firebase_web_app" "skip_delete" {
provider = google-beta
project = "%{project_id}"
display_name = "%{display_name} %{random_suffix}"
}
`, context)
}

func testAccCheckFirebaseWebAppNotDestroyedProducer(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_firebase_web_app" {
continue
}
if strings.HasPrefix(name, "data.") {
continue
}

config := googleProviderConfig(t)

url, err := replaceVarsForTest(config, rs, "{{FirebaseBasePath}}{{name}}")
if err != nil {
return err
}

billingProject := ""

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

_, err = sendRequest(config, "GET", billingProject, url, config.userAgent, nil)
if err != nil {
return fmt.Errorf("FirebaseWebApp doesn't exists at %s", url)
}
}

return nil
}
}

<% end -%>