github.com/vtorhonen/terraform@v0.9.0-beta2.0.20170307220345-5d894e4ffda7/builtin/providers/aws/resource_aws_route53_zone.go (about)

     1  package aws
     2  
     3  import (
     4  	"fmt"
     5  	"log"
     6  	"sort"
     7  	"strings"
     8  	"time"
     9  
    10  	"github.com/hashicorp/errwrap"
    11  	"github.com/hashicorp/terraform/helper/resource"
    12  	"github.com/hashicorp/terraform/helper/schema"
    13  
    14  	"github.com/aws/aws-sdk-go/aws"
    15  	"github.com/aws/aws-sdk-go/aws/awserr"
    16  	"github.com/aws/aws-sdk-go/service/route53"
    17  )
    18  
    19  func resourceAwsRoute53Zone() *schema.Resource {
    20  	return &schema.Resource{
    21  		Create: resourceAwsRoute53ZoneCreate,
    22  		Read:   resourceAwsRoute53ZoneRead,
    23  		Update: resourceAwsRoute53ZoneUpdate,
    24  		Delete: resourceAwsRoute53ZoneDelete,
    25  		Importer: &schema.ResourceImporter{
    26  			State: schema.ImportStatePassthrough,
    27  		},
    28  
    29  		Schema: map[string]*schema.Schema{
    30  			"name": &schema.Schema{
    31  				Type:     schema.TypeString,
    32  				Required: true,
    33  				ForceNew: true,
    34  			},
    35  
    36  			"comment": &schema.Schema{
    37  				Type:     schema.TypeString,
    38  				Optional: true,
    39  				Default:  "Managed by Terraform",
    40  			},
    41  
    42  			"vpc_id": &schema.Schema{
    43  				Type:          schema.TypeString,
    44  				Optional:      true,
    45  				ForceNew:      true,
    46  				ConflictsWith: []string{"delegation_set_id"},
    47  			},
    48  
    49  			"vpc_region": &schema.Schema{
    50  				Type:     schema.TypeString,
    51  				Optional: true,
    52  				ForceNew: true,
    53  				Computed: true,
    54  			},
    55  
    56  			"zone_id": &schema.Schema{
    57  				Type:     schema.TypeString,
    58  				Computed: true,
    59  			},
    60  
    61  			"delegation_set_id": &schema.Schema{
    62  				Type:          schema.TypeString,
    63  				Optional:      true,
    64  				ForceNew:      true,
    65  				ConflictsWith: []string{"vpc_id"},
    66  			},
    67  
    68  			"name_servers": &schema.Schema{
    69  				Type:     schema.TypeList,
    70  				Elem:     &schema.Schema{Type: schema.TypeString},
    71  				Computed: true,
    72  			},
    73  
    74  			"tags": tagsSchema(),
    75  
    76  			"force_destroy": &schema.Schema{
    77  				Type:     schema.TypeBool,
    78  				Optional: true,
    79  				Default:  false,
    80  			},
    81  		},
    82  	}
    83  }
    84  
    85  func resourceAwsRoute53ZoneCreate(d *schema.ResourceData, meta interface{}) error {
    86  	r53 := meta.(*AWSClient).r53conn
    87  
    88  	req := &route53.CreateHostedZoneInput{
    89  		Name:             aws.String(d.Get("name").(string)),
    90  		HostedZoneConfig: &route53.HostedZoneConfig{Comment: aws.String(d.Get("comment").(string))},
    91  		CallerReference:  aws.String(time.Now().Format(time.RFC3339Nano)),
    92  	}
    93  	if v := d.Get("vpc_id"); v != "" {
    94  		req.VPC = &route53.VPC{
    95  			VPCId:     aws.String(v.(string)),
    96  			VPCRegion: aws.String(meta.(*AWSClient).region),
    97  		}
    98  		if w := d.Get("vpc_region"); w != "" {
    99  			req.VPC.VPCRegion = aws.String(w.(string))
   100  		}
   101  		d.Set("vpc_region", req.VPC.VPCRegion)
   102  	}
   103  
   104  	if v, ok := d.GetOk("delegation_set_id"); ok {
   105  		req.DelegationSetId = aws.String(v.(string))
   106  	}
   107  
   108  	log.Printf("[DEBUG] Creating Route53 hosted zone: %s", *req.Name)
   109  	var err error
   110  	resp, err := r53.CreateHostedZone(req)
   111  	if err != nil {
   112  		return err
   113  	}
   114  
   115  	// Store the zone_id
   116  	zone := cleanZoneID(*resp.HostedZone.Id)
   117  	d.Set("zone_id", zone)
   118  	d.SetId(zone)
   119  
   120  	// Wait until we are done initializing
   121  	wait := resource.StateChangeConf{
   122  		Delay:      30 * time.Second,
   123  		Pending:    []string{"PENDING"},
   124  		Target:     []string{"INSYNC"},
   125  		Timeout:    10 * time.Minute,
   126  		MinTimeout: 2 * time.Second,
   127  		Refresh: func() (result interface{}, state string, err error) {
   128  			changeRequest := &route53.GetChangeInput{
   129  				Id: aws.String(cleanChangeID(*resp.ChangeInfo.Id)),
   130  			}
   131  			return resourceAwsGoRoute53Wait(r53, changeRequest)
   132  		},
   133  	}
   134  	_, err = wait.WaitForState()
   135  	if err != nil {
   136  		return err
   137  	}
   138  	return resourceAwsRoute53ZoneUpdate(d, meta)
   139  }
   140  
   141  func resourceAwsRoute53ZoneRead(d *schema.ResourceData, meta interface{}) error {
   142  	r53 := meta.(*AWSClient).r53conn
   143  	zone, err := r53.GetHostedZone(&route53.GetHostedZoneInput{Id: aws.String(d.Id())})
   144  	if err != nil {
   145  		// Handle a deleted zone
   146  		if r53err, ok := err.(awserr.Error); ok && r53err.Code() == "NoSuchHostedZone" {
   147  			d.SetId("")
   148  			return nil
   149  		}
   150  		return err
   151  	}
   152  
   153  	// In the import case this will be empty
   154  	if _, ok := d.GetOk("zone_id"); !ok {
   155  		d.Set("zone_id", d.Id())
   156  	}
   157  	if _, ok := d.GetOk("name"); !ok {
   158  		d.Set("name", zone.HostedZone.Name)
   159  	}
   160  
   161  	if !*zone.HostedZone.Config.PrivateZone {
   162  		ns := make([]string, len(zone.DelegationSet.NameServers))
   163  		for i := range zone.DelegationSet.NameServers {
   164  			ns[i] = *zone.DelegationSet.NameServers[i]
   165  		}
   166  		sort.Strings(ns)
   167  		if err := d.Set("name_servers", ns); err != nil {
   168  			return fmt.Errorf("[DEBUG] Error setting name servers for: %s, error: %#v", d.Id(), err)
   169  		}
   170  	} else {
   171  		ns, err := getNameServers(d.Id(), d.Get("name").(string), r53)
   172  		if err != nil {
   173  			return err
   174  		}
   175  		if err := d.Set("name_servers", ns); err != nil {
   176  			return fmt.Errorf("[DEBUG] Error setting name servers for: %s, error: %#v", d.Id(), err)
   177  		}
   178  
   179  		// In the import case we just associate it with the first VPC
   180  		if _, ok := d.GetOk("vpc_id"); !ok {
   181  			if len(zone.VPCs) > 1 {
   182  				return fmt.Errorf(
   183  					"Can't import a route53_zone with more than one VPC attachment")
   184  			}
   185  
   186  			if len(zone.VPCs) > 0 {
   187  				d.Set("vpc_id", zone.VPCs[0].VPCId)
   188  				d.Set("vpc_region", zone.VPCs[0].VPCRegion)
   189  			}
   190  		}
   191  
   192  		var associatedVPC *route53.VPC
   193  		for _, vpc := range zone.VPCs {
   194  			if *vpc.VPCId == d.Get("vpc_id") {
   195  				associatedVPC = vpc
   196  				break
   197  			}
   198  		}
   199  		if associatedVPC == nil {
   200  			return fmt.Errorf("[DEBUG] VPC: %v is not associated with Zone: %v", d.Get("vpc_id"), d.Id())
   201  		}
   202  	}
   203  
   204  	if zone.DelegationSet != nil && zone.DelegationSet.Id != nil {
   205  		d.Set("delegation_set_id", cleanDelegationSetId(*zone.DelegationSet.Id))
   206  	}
   207  
   208  	if zone.HostedZone != nil && zone.HostedZone.Config != nil && zone.HostedZone.Config.Comment != nil {
   209  		d.Set("comment", zone.HostedZone.Config.Comment)
   210  	}
   211  
   212  	// get tags
   213  	req := &route53.ListTagsForResourceInput{
   214  		ResourceId:   aws.String(d.Id()),
   215  		ResourceType: aws.String("hostedzone"),
   216  	}
   217  
   218  	resp, err := r53.ListTagsForResource(req)
   219  	if err != nil {
   220  		return err
   221  	}
   222  
   223  	var tags []*route53.Tag
   224  	if resp.ResourceTagSet != nil {
   225  		tags = resp.ResourceTagSet.Tags
   226  	}
   227  
   228  	if err := d.Set("tags", tagsToMapR53(tags)); err != nil {
   229  		return err
   230  	}
   231  
   232  	return nil
   233  }
   234  
   235  func resourceAwsRoute53ZoneUpdate(d *schema.ResourceData, meta interface{}) error {
   236  	conn := meta.(*AWSClient).r53conn
   237  
   238  	d.Partial(true)
   239  
   240  	if d.HasChange("comment") {
   241  		zoneInput := route53.UpdateHostedZoneCommentInput{
   242  			Id:      aws.String(d.Id()),
   243  			Comment: aws.String(d.Get("comment").(string)),
   244  		}
   245  
   246  		_, err := conn.UpdateHostedZoneComment(&zoneInput)
   247  		if err != nil {
   248  			return err
   249  		} else {
   250  			d.SetPartial("comment")
   251  		}
   252  	}
   253  
   254  	if err := setTagsR53(conn, d, "hostedzone"); err != nil {
   255  		return err
   256  	} else {
   257  		d.SetPartial("tags")
   258  	}
   259  
   260  	d.Partial(false)
   261  
   262  	return resourceAwsRoute53ZoneRead(d, meta)
   263  }
   264  
   265  func resourceAwsRoute53ZoneDelete(d *schema.ResourceData, meta interface{}) error {
   266  	r53 := meta.(*AWSClient).r53conn
   267  
   268  	if d.Get("force_destroy").(bool) {
   269  		if err := deleteAllRecordsInHostedZoneId(d.Id(), d.Get("name").(string), r53); err != nil {
   270  			return errwrap.Wrapf("{{err}}", err)
   271  		}
   272  	}
   273  
   274  	log.Printf("[DEBUG] Deleting Route53 hosted zone: %s (ID: %s)",
   275  		d.Get("name").(string), d.Id())
   276  	_, err := r53.DeleteHostedZone(&route53.DeleteHostedZoneInput{Id: aws.String(d.Id())})
   277  	if err != nil {
   278  		if r53err, ok := err.(awserr.Error); ok && r53err.Code() == "NoSuchHostedZone" {
   279  			log.Printf("[DEBUG] No matching Route 53 Zone found for: %s, removing from state file", d.Id())
   280  			d.SetId("")
   281  			return nil
   282  		}
   283  		return err
   284  	}
   285  
   286  	return nil
   287  }
   288  
   289  func deleteAllRecordsInHostedZoneId(hostedZoneId, hostedZoneName string, conn *route53.Route53) error {
   290  	input := &route53.ListResourceRecordSetsInput{
   291  		HostedZoneId: aws.String(hostedZoneId),
   292  	}
   293  
   294  	var lastDeleteErr, lastErrorFromWaiter error
   295  	var pageNum = 0
   296  	err := conn.ListResourceRecordSetsPages(input, func(page *route53.ListResourceRecordSetsOutput, isLastPage bool) bool {
   297  		sets := page.ResourceRecordSets
   298  		pageNum += 1
   299  
   300  		changes := make([]*route53.Change, 0)
   301  		// 100 items per page returned by default
   302  		for _, set := range sets {
   303  			if *set.Name == hostedZoneName+"." && (*set.Type == "NS" || *set.Type == "SOA") {
   304  				// Zone NS & SOA records cannot be deleted
   305  				continue
   306  			}
   307  			changes = append(changes, &route53.Change{
   308  				Action:            aws.String("DELETE"),
   309  				ResourceRecordSet: set,
   310  			})
   311  		}
   312  		log.Printf("[DEBUG] Deleting %d records (page %d) from %s",
   313  			len(changes), pageNum, hostedZoneId)
   314  
   315  		req := &route53.ChangeResourceRecordSetsInput{
   316  			HostedZoneId: aws.String(hostedZoneId),
   317  			ChangeBatch: &route53.ChangeBatch{
   318  				Comment: aws.String("Deleted by Terraform"),
   319  				Changes: changes,
   320  			},
   321  		}
   322  
   323  		var resp interface{}
   324  		resp, lastDeleteErr = deleteRoute53RecordSet(conn, req)
   325  		if out, ok := resp.(*route53.ChangeResourceRecordSetsOutput); ok {
   326  			log.Printf("[DEBUG] Waiting for change batch to become INSYNC: %#v", out)
   327  			if out.ChangeInfo != nil && out.ChangeInfo.Id != nil {
   328  				lastErrorFromWaiter = waitForRoute53RecordSetToSync(conn, cleanChangeID(*out.ChangeInfo.Id))
   329  			} else {
   330  				log.Printf("[DEBUG] Change info was empty")
   331  			}
   332  		} else {
   333  			log.Printf("[DEBUG] Unable to wait for change batch because of an error: %s", lastDeleteErr)
   334  		}
   335  
   336  		return !isLastPage
   337  	})
   338  	if err != nil {
   339  		return fmt.Errorf("Failed listing/deleting record sets: %s\nLast error from deletion: %s\nLast error from waiter: %s",
   340  			err, lastDeleteErr, lastErrorFromWaiter)
   341  	}
   342  
   343  	return nil
   344  }
   345  
   346  func resourceAwsGoRoute53Wait(r53 *route53.Route53, ref *route53.GetChangeInput) (result interface{}, state string, err error) {
   347  
   348  	status, err := r53.GetChange(ref)
   349  	if err != nil {
   350  		return nil, "UNKNOWN", err
   351  	}
   352  	return true, *status.ChangeInfo.Status, nil
   353  }
   354  
   355  // cleanChangeID is used to remove the leading /change/
   356  func cleanChangeID(ID string) string {
   357  	return cleanPrefix(ID, "/change/")
   358  }
   359  
   360  // cleanZoneID is used to remove the leading /hostedzone/
   361  func cleanZoneID(ID string) string {
   362  	return cleanPrefix(ID, "/hostedzone/")
   363  }
   364  
   365  // cleanPrefix removes a string prefix from an ID
   366  func cleanPrefix(ID, prefix string) string {
   367  	if strings.HasPrefix(ID, prefix) {
   368  		ID = strings.TrimPrefix(ID, prefix)
   369  	}
   370  	return ID
   371  }
   372  
   373  func getNameServers(zoneId string, zoneName string, r53 *route53.Route53) ([]string, error) {
   374  	resp, err := r53.ListResourceRecordSets(&route53.ListResourceRecordSetsInput{
   375  		HostedZoneId:    aws.String(zoneId),
   376  		StartRecordName: aws.String(zoneName),
   377  		StartRecordType: aws.String("NS"),
   378  	})
   379  	if err != nil {
   380  		return nil, err
   381  	}
   382  	if len(resp.ResourceRecordSets) == 0 {
   383  		return nil, nil
   384  	}
   385  	ns := make([]string, len(resp.ResourceRecordSets[0].ResourceRecords))
   386  	for i := range resp.ResourceRecordSets[0].ResourceRecords {
   387  		ns[i] = *resp.ResourceRecordSets[0].ResourceRecords[i].Value
   388  	}
   389  	sort.Strings(ns)
   390  	return ns, nil
   391  }