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_record.go b/builtin/providers/aws/resource_aws_route53_record.go index 6a646335f..a576749f1 100644 --- a/builtin/providers/aws/resource_aws_route53_record.go +++ b/builtin/providers/aws/resource_aws_route53_record.go @@ -269,13 +269,38 @@ func resourceAwsRoute53RecordCreate(d *schema.ResourceData, meta interface{}) er log.Printf("[DEBUG] Creating resource records for zone: %s, name: %s\n\n%s", zone, *rec.Name, req) + respRaw, err := changeRoute53RecordSet(conn, req) + + changeInfo := respRaw.(*route53.ChangeResourceRecordSetsOutput).ChangeInfo + + // Generate an ID + vars := []string{ + zone, + strings.ToLower(d.Get("name").(string)), + d.Get("type").(string), + } + if v, ok := d.GetOk("set_identifier"); ok { + vars = append(vars, v.(string)) + } + + d.SetId(strings.Join(vars, "_")) + + err = waitForRoute53RecordSetToSync(conn, cleanChangeID(*changeInfo.Id)) + if err != nil { + return err + } + + return resourceAwsRoute53RecordRead(d, meta) +} + +func changeRoute53RecordSet(conn *route53.Route53, input *route53.ChangeResourceRecordSetsInput) (interface{}, error) { wait := resource.StateChangeConf{ Pending: []string{"rejected"}, Target: []string{"accepted"}, Timeout: 5 * time.Minute, MinTimeout: 1 * time.Second, Refresh: func() (interface{}, string, error) { - resp, err := conn.ChangeResourceRecordSets(req) + resp, err := conn.ChangeResourceRecordSets(input) if err != nil { if r53err, ok := err.(awserr.Error); ok { if r53err.Code() == "PriorRequestNotComplete" { @@ -292,26 +317,11 @@ func resourceAwsRoute53RecordCreate(d *schema.ResourceData, meta interface{}) er }, } - respRaw, err := wait.WaitForState() - if err != nil { - return err - } - changeInfo := respRaw.(*route53.ChangeResourceRecordSetsOutput).ChangeInfo + return wait.WaitForState() +} - // Generate an ID - vars := []string{ - zone, - strings.ToLower(d.Get("name").(string)), - d.Get("type").(string), - } - if v, ok := d.GetOk("set_identifier"); ok { - vars = append(vars, v.(string)) - } - - d.SetId(strings.Join(vars, "_")) - - // Wait until we are done - wait = resource.StateChangeConf{ +func waitForRoute53RecordSetToSync(conn *route53.Route53, requestId string) error { + wait := resource.StateChangeConf{ Delay: 30 * time.Second, Pending: []string{"PENDING"}, Target: []string{"INSYNC"}, @@ -319,17 +329,13 @@ func resourceAwsRoute53RecordCreate(d *schema.ResourceData, meta interface{}) er MinTimeout: 5 * time.Second, Refresh: func() (result interface{}, state string, err error) { changeRequest := &route53.GetChangeInput{ - Id: aws.String(cleanChangeID(*changeInfo.Id)), + Id: aws.String(requestId), } return resourceAwsGoRoute53Wait(conn, changeRequest) }, } - _, err = wait.WaitForState() - if err != nil { - return err - } - - return resourceAwsRoute53RecordRead(d, meta) + _, err := wait.WaitForState() + return err } func resourceAwsRoute53RecordRead(d *schema.ResourceData, meta interface{}) error { @@ -518,13 +524,18 @@ func resourceAwsRoute53RecordDelete(d *schema.ResourceData, meta interface{}) er ChangeBatch: changeBatch, } + _, err = deleteRoute53RecordSet(conn, req) + return err +} + +func deleteRoute53RecordSet(conn *route53.Route53, input *route53.ChangeResourceRecordSetsInput) (interface{}, error) { wait := resource.StateChangeConf{ Pending: []string{"rejected"}, Target: []string{"accepted"}, Timeout: 5 * time.Minute, MinTimeout: 1 * time.Second, Refresh: func() (interface{}, string, error) { - _, err := conn.ChangeResourceRecordSets(req) + resp, err := conn.ChangeResourceRecordSets(input) if err != nil { if r53err, ok := err.(awserr.Error); ok { if r53err.Code() == "PriorRequestNotComplete" { @@ -535,22 +546,18 @@ func resourceAwsRoute53RecordDelete(d *schema.ResourceData, meta interface{}) er if r53err.Code() == "InvalidChangeBatch" { // This means that the record is already gone. - return 42, "accepted", nil + return resp, "accepted", nil } } return 42, "failure", err } - return 42, "accepted", nil + return resp, "accepted", nil }, } - if _, err := wait.WaitForState(); err != nil { - return err - } - - return nil + return wait.WaitForState() } func resourceAwsRoute53RecordBuildSet(d *schema.ResourceData, zoneName string) (*route53.ResourceRecordSet, error) { 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