From 079e0c5b86374af2036c026f5c372fa8c63b2a37 Mon Sep 17 00:00:00 2001 From: Radek Simko Date: Tue, 16 Aug 2016 19:34:58 +0100 Subject: [PATCH] provider/aws: Allow force_destroying records in R53 hosted zone --- .../aws/import_aws_route53_zone_test.go | 7 +- .../aws/resource_aws_route53_zone.go | 63 ++++++++++++ .../aws/resource_aws_route53_zone_test.go | 95 +++++++++++++++++++ .../aws/r/route53_zone.html.markdown | 2 + 4 files changed, 164 insertions(+), 3 deletions(-) diff --git a/builtin/providers/aws/import_aws_route53_zone_test.go b/builtin/providers/aws/import_aws_route53_zone_test.go index f2ad82f3f..cc4b0afef 100644 --- a/builtin/providers/aws/import_aws_route53_zone_test.go +++ b/builtin/providers/aws/import_aws_route53_zone_test.go @@ -19,9 +19,10 @@ func TestAccAWSRoute53Zone_importBasic(t *testing.T) { }, resource.TestStep{ - ResourceName: resourceName, - ImportState: true, - ImportStateVerify: true, + ResourceName: resourceName, + ImportState: true, + ImportStateVerify: true, + ImportStateVerifyIgnore: []string{"force_destroy"}, }, }, }) diff --git a/builtin/providers/aws/resource_aws_route53_zone.go b/builtin/providers/aws/resource_aws_route53_zone.go index 0702b8903..c97e9a4b3 100644 --- a/builtin/providers/aws/resource_aws_route53_zone.go +++ b/builtin/providers/aws/resource_aws_route53_zone.go @@ -71,6 +71,12 @@ func resourceAwsRoute53Zone() *schema.Resource { }, "tags": tagsSchema(), + + "force_destroy": &schema.Schema{ + Type: schema.TypeBool, + Optional: true, + Default: false, + }, }, } } @@ -258,6 +264,10 @@ func resourceAwsRoute53ZoneUpdate(d *schema.ResourceData, meta interface{}) erro func resourceAwsRoute53ZoneDelete(d *schema.ResourceData, meta interface{}) error { r53 := meta.(*AWSClient).r53conn + if d.Get("force_destroy").(bool) { + deleteAllRecordsInHostedZoneId(d.Id(), d.Get("name").(string), r53) + } + log.Printf("[DEBUG] Deleting Route53 hosted zone: %s (ID: %s)", d.Get("name").(string), d.Id()) _, err := r53.DeleteHostedZone(&route53.DeleteHostedZoneInput{Id: aws.String(d.Id())}) @@ -273,6 +283,59 @@ func resourceAwsRoute53ZoneDelete(d *schema.ResourceData, meta interface{}) erro return nil } +func deleteAllRecordsInHostedZoneId(hostedZoneId, hostedZoneName string, conn *route53.Route53) error { + input := &route53.ListResourceRecordSetsInput{ + HostedZoneId: aws.String(hostedZoneId), + } + + var lastDeleteErr, lastErrorFromWaiter error + var pageNum = 0 + err := conn.ListResourceRecordSetsPages(input, func(page *route53.ListResourceRecordSetsOutput, isLastPage bool) bool { + sets := page.ResourceRecordSets + pageNum += 1 + + changes := make([]*route53.Change, 0) + // 100 items per page returned by default + for _, set := range sets { + if *set.Name == hostedZoneName+"." && (*set.Type == "NS" || *set.Type == "SOA") { + // Zone NS & SOA records cannot be deleted + continue + } + changes = append(changes, &route53.Change{ + Action: aws.String("DELETE"), + ResourceRecordSet: set, + }) + } + log.Printf("[DEBUG] Deleting %d records (page %d) from %s", + len(changes), pageNum, hostedZoneId) + + req := &route53.ChangeResourceRecordSetsInput{ + HostedZoneId: aws.String(hostedZoneId), + ChangeBatch: &route53.ChangeBatch{ + Comment: aws.String("Deleted by Terraform"), + Changes: changes, + }, + } + + var resp interface{} + resp, lastDeleteErr = deleteRoute53RecordSet(conn, req) + if out, ok := resp.(*route53.ChangeResourceRecordSetsOutput); ok { + log.Printf("[DEBUG] Waiting for change batch to become INSYNC: %#v", out) + lastErrorFromWaiter = waitForRoute53RecordSetToSync(conn, cleanChangeID(*out.ChangeInfo.Id)) + } else { + log.Printf("[DEBUG] Unable to wait for change batch because of an error: %s", lastDeleteErr) + } + + return !isLastPage + }) + if err != nil { + return fmt.Errorf("Failed listing/deleting record sets: %s\nLast error from deletion: %s\nLast error from waiter: %s", + err, lastDeleteErr, lastErrorFromWaiter) + } + + return nil +} + func resourceAwsGoRoute53Wait(r53 *route53.Route53, ref *route53.GetChangeInput) (result interface{}, state string, err error) { status, err := r53.GetChange(ref) diff --git a/builtin/providers/aws/resource_aws_route53_zone_test.go b/builtin/providers/aws/resource_aws_route53_zone_test.go index bea1d9320..6679ea72d 100644 --- a/builtin/providers/aws/resource_aws_route53_zone_test.go +++ b/builtin/providers/aws/resource_aws_route53_zone_test.go @@ -2,9 +2,11 @@ package aws import ( "fmt" + "log" "sort" "testing" + "github.com/hashicorp/terraform/helper/acctest" "github.com/hashicorp/terraform/helper/resource" "github.com/hashicorp/terraform/helper/schema" "github.com/hashicorp/terraform/terraform" @@ -86,6 +88,39 @@ func TestAccAWSRoute53Zone_basic(t *testing.T) { }) } +func TestAccAWSRoute53Zone_forceDestroy(t *testing.T) { + var zone route53.GetHostedZoneOutput + + // record the initialized providers so that we can use them to + // check for the instances in each region + var providers []*schema.Provider + providerFactories := map[string]terraform.ResourceProviderFactory{ + "aws": func() (terraform.ResourceProvider, error) { + p := Provider() + providers = append(providers, p.(*schema.Provider)) + return p, nil + }, + } + + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + IDRefreshName: "aws_route53_zone.destroyable", + ProviderFactories: providerFactories, + CheckDestroy: testAccCheckRoute53ZoneDestroyWithProviders(&providers), + Steps: []resource.TestStep{ + resource.TestStep{ + Config: testAccRoute53ZoneConfig_forceDestroy, + Check: resource.ComposeTestCheckFunc( + testAccCheckRoute53ZoneExistsWithProviders("aws_route53_zone.destroyable", &zone, &providers), + // Add >100 records to verify pagination works ok + testAccCreateRandomRoute53RecordsInZoneIdWithProviders(&providers, &zone, 100), + testAccCreateRandomRoute53RecordsInZoneIdWithProviders(&providers, &zone, 5), + ), + }, + }, + }) +} + func TestAccAWSRoute53Zone_updateComment(t *testing.T) { var zone route53.GetHostedZoneOutput var td route53.ResourceTagSet @@ -204,6 +239,59 @@ func testAccCheckRoute53ZoneDestroyWithProvider(s *terraform.State, provider *sc return nil } +func testAccCreateRandomRoute53RecordsInZoneIdWithProviders(providers *[]*schema.Provider, + zone *route53.GetHostedZoneOutput, recordsCount int) resource.TestCheckFunc { + return func(s *terraform.State) error { + for _, provider := range *providers { + if provider.Meta() == nil { + continue + } + if err := testAccCreateRandomRoute53RecordsInZoneId(provider, zone, recordsCount); err != nil { + return err + } + } + return nil + } +} + +func testAccCreateRandomRoute53RecordsInZoneId(provider *schema.Provider, zone *route53.GetHostedZoneOutput, recordsCount int) error { + conn := provider.Meta().(*AWSClient).r53conn + + var changes []*route53.Change + if recordsCount > 100 { + return fmt.Errorf("Route53 API only allows 100 record sets in a single batch") + } + for i := 0; i < recordsCount; i++ { + changes = append(changes, &route53.Change{ + Action: aws.String("UPSERT"), + ResourceRecordSet: &route53.ResourceRecordSet{ + Name: aws.String(fmt.Sprintf("%d-tf-acc-random.%s", acctest.RandInt(), *zone.HostedZone.Name)), + Type: aws.String("CNAME"), + ResourceRecords: []*route53.ResourceRecord{ + &route53.ResourceRecord{Value: aws.String(fmt.Sprintf("random.%s", *zone.HostedZone.Name))}, + }, + TTL: aws.Int64(int64(30)), + }, + }) + } + + req := &route53.ChangeResourceRecordSetsInput{ + HostedZoneId: zone.HostedZone.Id, + ChangeBatch: &route53.ChangeBatch{ + Comment: aws.String("Generated by Terraform"), + Changes: changes, + }, + } + log.Printf("[DEBUG] Change set: %s\n", *req) + resp, err := changeRoute53RecordSet(conn, req) + if err != nil { + return err + } + changeInfo := resp.(*route53.ChangeResourceRecordSetsOutput).ChangeInfo + err = waitForRoute53RecordSetToSync(conn, cleanChangeID(*changeInfo.Id)) + return err +} + func testAccCheckRoute53ZoneExists(n string, zone *route53.GetHostedZoneOutput) resource.TestCheckFunc { return func(s *terraform.State) error { return testAccCheckRoute53ZoneExistsWithProvider(s, n, zone, testAccProvider) @@ -324,6 +412,13 @@ resource "aws_route53_zone" "main" { } ` +const testAccRoute53ZoneConfig_forceDestroy = ` +resource "aws_route53_zone" "destroyable" { + name = "terraform.io" + force_destroy = true +} +` + const testAccRoute53ZoneConfigUpdateComment = ` resource "aws_route53_zone" "main" { name = "hashicorp.com." diff --git a/website/source/docs/providers/aws/r/route53_zone.html.markdown b/website/source/docs/providers/aws/r/route53_zone.html.markdown index 061946a9c..105449796 100644 --- a/website/source/docs/providers/aws/r/route53_zone.html.markdown +++ b/website/source/docs/providers/aws/r/route53_zone.html.markdown @@ -61,6 +61,8 @@ The following arguments are supported: * `vpc_region` - (Optional) The VPC's region. Defaults to the region of the AWS provider. * `delegation_set_id` - (Optional) The ID of the reusable delgation set whose NS records you want to assign to the hosted zone. Conflicts w/ `vpc_id` as delegation sets can only be used for public zones. +* `force_destroy` - (Optional) Whether to destroy all records (possibly managed outside of Terraform) + in the zone when destroying the zone. ## Attributes Reference