Merge pull request #8239 from TimeIncOSS/f-aws-r53-zone-force-destroy

provider/aws: Add force_destroy option to aws_route53_zone
This commit is contained in:
Radek Simko 2016-08-17 07:10:00 +01:00 committed by GitHub
commit 523627ba24
5 changed files with 206 additions and 38 deletions

View File

@ -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"},
},
},
})

View File

@ -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) {

View File

@ -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)

View File

@ -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."

View File

@ -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