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