diff --git a/.changelog/3917.txt b/.changelog/3917.txt new file mode 100644 index 00000000000..b043c478b41 --- /dev/null +++ b/.changelog/3917.txt @@ -0,0 +1,3 @@ +```release-note:enhancement +dns: added `force_destroy` option to `google_dns_managed_zone` to delete records created outside of Terraform +``` diff --git a/google/resource_dns_managed_zone.go b/google/resource_dns_managed_zone.go index 895cf060e38..3960108c4e4 100644 --- a/google/resource_dns_managed_zone.go +++ b/google/resource_dns_managed_zone.go @@ -26,6 +26,7 @@ import ( "github.com/hashicorp/terraform-plugin-sdk/helper/hashcode" "github.com/hashicorp/terraform-plugin-sdk/helper/schema" "github.com/hashicorp/terraform-plugin-sdk/helper/validation" + "google.golang.org/api/dns/v1" ) func resourceDNSManagedZone() *schema.Resource { @@ -256,6 +257,11 @@ defined by the server`, Type: schema.TypeString, }, }, + "force_destroy": { + Type: schema.TypeBool, + Optional: true, + Default: false, + }, "project": { Type: schema.TypeString, Optional: true, @@ -422,6 +428,10 @@ func resourceDNSManagedZoneRead(d *schema.ResourceData, meta interface{}) error return handleNotFoundError(err, d, fmt.Sprintf("DNSManagedZone %q", d.Id())) } + // Explicitly set virtual fields to default values if unset + if _, ok := d.GetOk("force_destroy"); !ok { + d.Set("force_destroy", false) + } if err := d.Set("project", project); err != nil { return fmt.Errorf("Error reading ManagedZone: %s", err) } @@ -549,6 +559,76 @@ func resourceDNSManagedZoneDelete(d *schema.ResourceData, meta interface{}) erro } var obj map[string]interface{} + if d.Get("force_destroy").(bool) { + zone := d.Get("name").(string) + token := "" + for paginate := true; paginate; { + var resp *dns.ResourceRecordSetsListResponse + if token == "" { + resp, err = config.clientDns.ResourceRecordSets.List(project, zone).Do() + if err != nil { + return fmt.Errorf("Error reading ResourceRecordSets: %s", err) + } + } else { + resp, err = config.clientDns.ResourceRecordSets.List(project, zone).PageToken(token).Do() + if err != nil { + return fmt.Errorf("Error reading ResourceRecordSets: %s", err) + } + } + + for _, rr := range resp.Rrsets { + // Build the change + chg := &dns.Change{ + Deletions: []*dns.ResourceRecordSet{ + { + Name: rr.Name, + Type: rr.Type, + Ttl: rr.Ttl, + Rrdatas: rr.Rrdatas, + }, + }, + } + + if rr.Type == "NS" { + mz, err := config.clientDns.ManagedZones.Get(project, zone).Do() + if err != nil { + return fmt.Errorf("Error retrieving managed zone %q from %q: %s", zone, project, err) + } + domain := mz.DnsName + + if domain == rr.Name { + log.Println("[DEBUG] NS records can't be deleted due to API restrictions, so they're being left in place. See https://www.terraform.io/docs/providers/google/r/dns_record_set.html for more information.") + continue + } + } + + if rr.Type == "SOA" { + log.Println("[DEBUG] SOA records can't be deleted due to API restrictions, so they're being left in place.") + continue + } + + log.Printf("[DEBUG] DNS Record delete request via MZ: %#v", chg) + chg, err = config.clientDns.Changes.Create(project, zone, chg).Do() + if err != nil { + return fmt.Errorf("Unable to delete ResourceRecordSets: %s", err) + } + + w := &DnsChangeWaiter{ + Service: config.clientDns, + Change: chg, + Project: project, + ManagedZone: zone, + } + _, err = w.Conf().WaitForState() + if err != nil { + return fmt.Errorf("Error waiting for Google DNS change: %s", err) + } + } + + token = resp.NextPageToken + paginate = token != "" + } + } log.Printf("[DEBUG] Deleting ManagedZone %q", d.Id()) // err == nil indicates that the billing_project value was found @@ -582,6 +662,9 @@ func resourceDNSManagedZoneImport(d *schema.ResourceData, meta interface{}) ([]* } d.SetId(id) + // Explicitly set virtual fields to default values on import + d.Set("force_destroy", false) + return []*schema.ResourceData{d}, nil } diff --git a/google/resource_dns_managed_zone_sweeper_test.go b/google/resource_dns_managed_zone_sweeper_test.go deleted file mode 100644 index 3a0e475dcac..00000000000 --- a/google/resource_dns_managed_zone_sweeper_test.go +++ /dev/null @@ -1,124 +0,0 @@ -// ---------------------------------------------------------------------------- -// -// *** AUTO GENERATED CODE *** AUTO GENERATED CODE *** -// -// ---------------------------------------------------------------------------- -// -// This file is automatically generated by Magic Modules and manual -// changes will be clobbered when the file is regenerated. -// -// Please read more about how to change this file in -// .github/CONTRIBUTING.md. -// -// ---------------------------------------------------------------------------- - -package google - -import ( - "context" - "log" - "strings" - "testing" - - "github.com/hashicorp/terraform-plugin-sdk/helper/resource" -) - -func init() { - resource.AddTestSweepers("DNSManagedZone", &resource.Sweeper{ - Name: "DNSManagedZone", - F: testSweepDNSManagedZone, - }) -} - -// At the time of writing, the CI only passes us-central1 as the region -func testSweepDNSManagedZone(region string) error { - resourceName := "DNSManagedZone" - 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://dns.googleapis.com/dns/v1/projects/{{project}}/managedZones", "?")[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, nil) - if err != nil { - log.Printf("[INFO][SWEEPER_LOG] Error in response from request %s: %s", listUrl, err) - return nil - } - - resourceList, ok := res["managedZones"] - 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://dns.googleapis.com/dns/v1/projects/{{project}}/managedZones/{{name}}" - 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 - - // Don't wait on operations as we may have a lot to delete - _, err = sendRequest(config, "DELETE", config.Project, deleteUrl, nil) - 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 -} diff --git a/google/resource_dns_managed_zone_test.go b/google/resource_dns_managed_zone_test.go index 8f6b19d931f..0d3c6332672 100644 --- a/google/resource_dns_managed_zone_test.go +++ b/google/resource_dns_managed_zone_test.go @@ -5,6 +5,8 @@ import ( "testing" "github.com/hashicorp/terraform-plugin-sdk/helper/resource" + "github.com/hashicorp/terraform-plugin-sdk/terraform" + "google.golang.org/api/dns/v1" ) func TestAccDNSManagedZone_update(t *testing.T) { @@ -149,6 +151,89 @@ func TestAccDNSManagedZone_privateForwardingUpdate(t *testing.T) { }) } +func TestAccDNSManagedZone_forceDestroy(t *testing.T) { + //t.Parallel() + + zoneSuffix := randString(t, 10) + project := getTestProjectFromEnv() + + vcrTest(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + Providers: testAccProviders, + CheckDestroy: testAccCheckDNSManagedZoneDestroyProducer(t), + Steps: []resource.TestStep{ + { + Config: testAccDNSManagedZone_forceDestroy(zoneSuffix), + Check: resource.ComposeTestCheckFunc( + testAccCheckManagedZoneCreateRRs(t, zoneSuffix, project), + ), + }, + }, + }) +} + +func testAccCheckManagedZoneCreateRRs(t *testing.T, zoneSuffix string, project string) resource.TestCheckFunc { + return func(s *terraform.State) error { + config := googleProviderConfig(t) + zone := fmt.Sprintf("mzone-test-%s", zoneSuffix) + // Build the change + chg := &dns.Change{ + Additions: []*dns.ResourceRecordSet{ + { + Name: fmt.Sprintf("cname.%s.hashicorptest.com.", zoneSuffix), + Type: "CNAME", + Ttl: 300, + Rrdatas: []string{"foo.example.com."}, + }, + { + Name: fmt.Sprintf("a.%s.hashicorptest.com.", zoneSuffix), + Type: "A", + Ttl: 300, + Rrdatas: []string{"1.1.1.1"}, + }, + { + Name: fmt.Sprintf("nested.%s.hashicorptest.com.", zoneSuffix), + Type: "NS", + Ttl: 300, + Rrdatas: []string{"ns.hashicorp.services.", "ns2.hashicorp.services."}, + }, + }, + } + + chg, err := config.clientDns.Changes.Create(project, zone, chg).Do() + if err != nil { + return fmt.Errorf("Error creating DNS RecordSet: %s", err) + } + + w := &DnsChangeWaiter{ + Service: config.clientDns, + Change: chg, + Project: project, + ManagedZone: zone, + } + _, err = w.Conf().WaitForState() + if err != nil { + return fmt.Errorf("Error waiting for Google DNS change: %s", err) + } + + return nil + } +} + +func testAccDNSManagedZone_forceDestroy(suffix string) string { + return fmt.Sprintf(` +resource "google_dns_managed_zone" "foobar" { + name = "mzone-test-%s" + dns_name = "%s.hashicorptest.com." + labels = { + foo = "bar" + } + force_destroy = true + visibility = "public" +} +`, suffix, suffix) +} + func testAccDnsManagedZone_basic(suffix, description string) string { return fmt.Sprintf(` resource "google_dns_managed_zone" "foobar" { diff --git a/website/docs/r/dns_managed_zone.html.markdown b/website/docs/r/dns_managed_zone.html.markdown index 24832e5d9d6..0fc163f696a 100644 --- a/website/docs/r/dns_managed_zone.html.markdown +++ b/website/docs/r/dns_managed_zone.html.markdown @@ -277,7 +277,7 @@ The following arguments are supported: * `project` - (Optional) The ID of the project in which the resource belongs. If it is not provided, the provider project is used. - +* `force_destroy` - (Optional) Set this true to delete all records in the zone. The `dnssec_config` block supports: * `kind` -