github.com/gabrielperezs/terraform@v0.7.0-rc2.0.20160715084931-f7da2612946f/builtin/providers/aws/resource_aws_route53_record.go (about)

     1  package aws
     2  
     3  import (
     4  	"bytes"
     5  	"errors"
     6  	"fmt"
     7  	"log"
     8  	"strings"
     9  	"time"
    10  
    11  	"github.com/hashicorp/terraform/helper/hashcode"
    12  	"github.com/hashicorp/terraform/helper/resource"
    13  	"github.com/hashicorp/terraform/helper/schema"
    14  
    15  	"github.com/aws/aws-sdk-go/aws"
    16  	"github.com/aws/aws-sdk-go/aws/awserr"
    17  	"github.com/aws/aws-sdk-go/service/route53"
    18  )
    19  
    20  var r53NoRecordsFound = errors.New("No matching Hosted Zone found")
    21  var r53NoHostedZoneFound = errors.New("No matching records found")
    22  
    23  func resourceAwsRoute53Record() *schema.Resource {
    24  	return &schema.Resource{
    25  		Create: resourceAwsRoute53RecordCreate,
    26  		Read:   resourceAwsRoute53RecordRead,
    27  		Update: resourceAwsRoute53RecordUpdate,
    28  		Delete: resourceAwsRoute53RecordDelete,
    29  
    30  		SchemaVersion: 2,
    31  		MigrateState:  resourceAwsRoute53RecordMigrateState,
    32  		Schema: map[string]*schema.Schema{
    33  			"name": &schema.Schema{
    34  				Type:     schema.TypeString,
    35  				Required: true,
    36  				ForceNew: true,
    37  				StateFunc: func(v interface{}) string {
    38  					value := strings.TrimSuffix(v.(string), ".")
    39  					return strings.ToLower(value)
    40  				},
    41  			},
    42  
    43  			"fqdn": &schema.Schema{
    44  				Type:     schema.TypeString,
    45  				Computed: true,
    46  			},
    47  
    48  			"type": &schema.Schema{
    49  				Type:     schema.TypeString,
    50  				Required: true,
    51  				ForceNew: true,
    52  			},
    53  
    54  			"zone_id": &schema.Schema{
    55  				Type:     schema.TypeString,
    56  				Required: true,
    57  				ForceNew: true,
    58  				ValidateFunc: func(v interface{}, k string) (ws []string, es []error) {
    59  					value := v.(string)
    60  					if value == "" {
    61  						es = append(es, fmt.Errorf("Cannot have empty zone_id"))
    62  					}
    63  					return
    64  				},
    65  			},
    66  
    67  			"ttl": &schema.Schema{
    68  				Type:          schema.TypeInt,
    69  				Optional:      true,
    70  				ConflictsWith: []string{"alias"},
    71  			},
    72  
    73  			"weight": &schema.Schema{
    74  				Type:     schema.TypeInt,
    75  				Optional: true,
    76  				Removed:  "Now implemented as weighted_routing_policy; Please see https://www.terraform.io/docs/providers/aws/r/route53_record.html",
    77  			},
    78  
    79  			"set_identifier": &schema.Schema{
    80  				Type:     schema.TypeString,
    81  				Optional: true,
    82  				ForceNew: true,
    83  			},
    84  
    85  			"alias": &schema.Schema{
    86  				Type:          schema.TypeSet,
    87  				Optional:      true,
    88  				ConflictsWith: []string{"records", "ttl"},
    89  				Elem: &schema.Resource{
    90  					Schema: map[string]*schema.Schema{
    91  						"zone_id": &schema.Schema{
    92  							Type:     schema.TypeString,
    93  							Required: true,
    94  						},
    95  
    96  						"name": &schema.Schema{
    97  							Type:     schema.TypeString,
    98  							Required: true,
    99  						},
   100  
   101  						"evaluate_target_health": &schema.Schema{
   102  							Type:     schema.TypeBool,
   103  							Required: true,
   104  						},
   105  					},
   106  				},
   107  				Set: resourceAwsRoute53AliasRecordHash,
   108  			},
   109  
   110  			"failover": &schema.Schema{ // PRIMARY | SECONDARY
   111  				Type:     schema.TypeString,
   112  				Optional: true,
   113  				Removed:  "Now implemented as failover_routing_policy; see docs",
   114  			},
   115  
   116  			"failover_routing_policy": &schema.Schema{
   117  				Type:     schema.TypeList,
   118  				Optional: true,
   119  				ConflictsWith: []string{
   120  					"geolocation_routing_policy",
   121  					"latency_routing_policy",
   122  					"weighted_routing_policy",
   123  				},
   124  				Elem: &schema.Resource{
   125  					Schema: map[string]*schema.Schema{
   126  						"type": &schema.Schema{
   127  							Type:     schema.TypeString,
   128  							Required: true,
   129  							ValidateFunc: func(v interface{}, k string) (ws []string, es []error) {
   130  								value := v.(string)
   131  								if value != "PRIMARY" && value != "SECONDARY" {
   132  									es = append(es, fmt.Errorf("Failover policy type must be PRIMARY or SECONDARY"))
   133  								}
   134  								return
   135  							},
   136  						},
   137  					},
   138  				},
   139  			},
   140  
   141  			"latency_routing_policy": &schema.Schema{
   142  				Type:     schema.TypeList,
   143  				Optional: true,
   144  				ConflictsWith: []string{
   145  					"failover_routing_policy",
   146  					"geolocation_routing_policy",
   147  					"weighted_routing_policy",
   148  				},
   149  				Elem: &schema.Resource{
   150  					Schema: map[string]*schema.Schema{
   151  						"region": &schema.Schema{
   152  							Type:     schema.TypeString,
   153  							Required: true,
   154  						},
   155  					},
   156  				},
   157  			},
   158  
   159  			"geolocation_routing_policy": &schema.Schema{ // AWS Geolocation
   160  				Type:     schema.TypeList,
   161  				Optional: true,
   162  				ConflictsWith: []string{
   163  					"failover_routing_policy",
   164  					"latency_routing_policy",
   165  					"weighted_routing_policy",
   166  				},
   167  				Elem: &schema.Resource{
   168  					Schema: map[string]*schema.Schema{
   169  						"continent": &schema.Schema{
   170  							Type:     schema.TypeString,
   171  							Optional: true,
   172  						},
   173  						"country": &schema.Schema{
   174  							Type:     schema.TypeString,
   175  							Optional: true,
   176  						},
   177  						"subdivision": &schema.Schema{
   178  							Type:     schema.TypeString,
   179  							Optional: true,
   180  						},
   181  					},
   182  				},
   183  			},
   184  
   185  			"weighted_routing_policy": &schema.Schema{
   186  				Type:     schema.TypeList,
   187  				Optional: true,
   188  				ConflictsWith: []string{
   189  					"failover_routing_policy",
   190  					"geolocation_routing_policy",
   191  					"latency_routing_policy",
   192  				},
   193  				Elem: &schema.Resource{
   194  					Schema: map[string]*schema.Schema{
   195  						"weight": &schema.Schema{
   196  							Type:     schema.TypeInt,
   197  							Required: true,
   198  						},
   199  					},
   200  				},
   201  			},
   202  
   203  			"health_check_id": &schema.Schema{ // ID of health check
   204  				Type:     schema.TypeString,
   205  				Optional: true,
   206  			},
   207  
   208  			"records": &schema.Schema{
   209  				Type:          schema.TypeSet,
   210  				ConflictsWith: []string{"alias"},
   211  				Elem:          &schema.Schema{Type: schema.TypeString},
   212  				Optional:      true,
   213  				Set:           schema.HashString,
   214  			},
   215  		},
   216  	}
   217  }
   218  
   219  func resourceAwsRoute53RecordUpdate(d *schema.ResourceData, meta interface{}) error {
   220  	// Route 53 supports CREATE, DELETE, and UPSERT actions. We use UPSERT, and
   221  	// AWS dynamically determines if a record should be created or updated.
   222  	// Amazon Route 53 can update an existing resource record set only when all
   223  	// of the following values match: Name, Type
   224  	// (and SetIdentifier, which we don't use yet).
   225  	// See http://docs.aws.amazon.com/Route53/latest/APIReference/API_ChangeResourceRecordSets_Requests.html#change-rrsets-request-action
   226  	//
   227  	// Because we use UPSERT, for resouce update here we simply fall through to
   228  	// our resource create function.
   229  	return resourceAwsRoute53RecordCreate(d, meta)
   230  }
   231  
   232  func resourceAwsRoute53RecordCreate(d *schema.ResourceData, meta interface{}) error {
   233  	conn := meta.(*AWSClient).r53conn
   234  	zone := cleanZoneID(d.Get("zone_id").(string))
   235  
   236  	var err error
   237  	zoneRecord, err := conn.GetHostedZone(&route53.GetHostedZoneInput{Id: aws.String(zone)})
   238  	if err != nil {
   239  		return err
   240  	}
   241  	if zoneRecord.HostedZone == nil {
   242  		return fmt.Errorf("[WARN] No Route53 Zone found for id (%s)", zone)
   243  	}
   244  
   245  	// Build the record
   246  	rec, err := resourceAwsRoute53RecordBuildSet(d, *zoneRecord.HostedZone.Name)
   247  	if err != nil {
   248  		return err
   249  	}
   250  
   251  	// Create the new records. We abuse StateChangeConf for this to
   252  	// retry for us since Route53 sometimes returns errors about another
   253  	// operation happening at the same time.
   254  	changeBatch := &route53.ChangeBatch{
   255  		Comment: aws.String("Managed by Terraform"),
   256  		Changes: []*route53.Change{
   257  			&route53.Change{
   258  				Action:            aws.String("UPSERT"),
   259  				ResourceRecordSet: rec,
   260  			},
   261  		},
   262  	}
   263  
   264  	req := &route53.ChangeResourceRecordSetsInput{
   265  		HostedZoneId: aws.String(cleanZoneID(*zoneRecord.HostedZone.Id)),
   266  		ChangeBatch:  changeBatch,
   267  	}
   268  
   269  	log.Printf("[DEBUG] Creating resource records for zone: %s, name: %s\n\n%s",
   270  		zone, *rec.Name, req)
   271  
   272  	wait := resource.StateChangeConf{
   273  		Pending:    []string{"rejected"},
   274  		Target:     []string{"accepted"},
   275  		Timeout:    5 * time.Minute,
   276  		MinTimeout: 1 * time.Second,
   277  		Refresh: func() (interface{}, string, error) {
   278  			resp, err := conn.ChangeResourceRecordSets(req)
   279  			if err != nil {
   280  				if r53err, ok := err.(awserr.Error); ok {
   281  					if r53err.Code() == "PriorRequestNotComplete" {
   282  						// There is some pending operation, so just retry
   283  						// in a bit.
   284  						return nil, "rejected", nil
   285  					}
   286  				}
   287  
   288  				return nil, "failure", err
   289  			}
   290  
   291  			return resp, "accepted", nil
   292  		},
   293  	}
   294  
   295  	respRaw, err := wait.WaitForState()
   296  	if err != nil {
   297  		return err
   298  	}
   299  	changeInfo := respRaw.(*route53.ChangeResourceRecordSetsOutput).ChangeInfo
   300  
   301  	// Generate an ID
   302  	vars := []string{
   303  		zone,
   304  		strings.ToLower(d.Get("name").(string)),
   305  		d.Get("type").(string),
   306  	}
   307  	if v, ok := d.GetOk("set_identifier"); ok {
   308  		vars = append(vars, v.(string))
   309  	}
   310  
   311  	d.SetId(strings.Join(vars, "_"))
   312  
   313  	// Wait until we are done
   314  	wait = resource.StateChangeConf{
   315  		Delay:      30 * time.Second,
   316  		Pending:    []string{"PENDING"},
   317  		Target:     []string{"INSYNC"},
   318  		Timeout:    30 * time.Minute,
   319  		MinTimeout: 5 * time.Second,
   320  		Refresh: func() (result interface{}, state string, err error) {
   321  			changeRequest := &route53.GetChangeInput{
   322  				Id: aws.String(cleanChangeID(*changeInfo.Id)),
   323  			}
   324  			return resourceAwsGoRoute53Wait(conn, changeRequest)
   325  		},
   326  	}
   327  	_, err = wait.WaitForState()
   328  	if err != nil {
   329  		return err
   330  	}
   331  
   332  	return resourceAwsRoute53RecordRead(d, meta)
   333  }
   334  
   335  func resourceAwsRoute53RecordRead(d *schema.ResourceData, meta interface{}) error {
   336  	// If we don't have a zone ID we're doing an import. Parse it from the ID.
   337  	if _, ok := d.GetOk("zone_id"); !ok {
   338  		parts := strings.Split(d.Id(), "_")
   339  		d.Set("zone_id", parts[0])
   340  		d.Set("name", parts[1])
   341  		d.Set("type", parts[2])
   342  		if len(parts) > 3 {
   343  			d.Set("set_identifier", parts[3])
   344  		}
   345  
   346  		d.Set("weight", -1)
   347  	}
   348  
   349  	record, err := findRecord(d, meta)
   350  	if err != nil {
   351  		switch err {
   352  		case r53NoHostedZoneFound, r53NoRecordsFound:
   353  			log.Printf("[DEBUG] %s for: %s, removing from state file", err, d.Id())
   354  			d.SetId("")
   355  			return nil
   356  		default:
   357  			return err
   358  		}
   359  	}
   360  
   361  	err = d.Set("records", flattenResourceRecords(record.ResourceRecords))
   362  	if err != nil {
   363  		return fmt.Errorf("[DEBUG] Error setting records for: %s, error: %#v", d.Id(), err)
   364  	}
   365  
   366  	if alias := record.AliasTarget; alias != nil {
   367  		if _, ok := d.GetOk("alias"); !ok {
   368  			d.Set("alias", []interface{}{
   369  				map[string]interface{}{
   370  					"zone_id": *alias.HostedZoneId,
   371  					"name":    *alias.DNSName,
   372  					"evaluate_target_health": *alias.EvaluateTargetHealth,
   373  				},
   374  			})
   375  		}
   376  	}
   377  
   378  	d.Set("ttl", record.TTL)
   379  
   380  	if record.Failover != nil {
   381  		v := []map[string]interface{}{{
   382  			"type": aws.StringValue(record.Failover),
   383  		}}
   384  		if err := d.Set("failover_routing_policy", v); err != nil {
   385  			return fmt.Errorf("[DEBUG] Error setting failover records for: %s, error: %#v", d.Id(), err)
   386  		}
   387  	}
   388  
   389  	if record.GeoLocation != nil {
   390  		v := []map[string]interface{}{{
   391  			"continent":   aws.StringValue(record.GeoLocation.ContinentCode),
   392  			"country":     aws.StringValue(record.GeoLocation.CountryCode),
   393  			"subdivision": aws.StringValue(record.GeoLocation.SubdivisionCode),
   394  		}}
   395  		if err := d.Set("geolocation_routing_policy", v); err != nil {
   396  			return fmt.Errorf("[DEBUG] Error setting gelocation records for: %s, error: %#v", d.Id(), err)
   397  		}
   398  	}
   399  
   400  	if record.Region != nil {
   401  		v := []map[string]interface{}{{
   402  			"region": aws.StringValue(record.Region),
   403  		}}
   404  		if err := d.Set("latency_routing_policy", v); err != nil {
   405  			return fmt.Errorf("[DEBUG] Error setting latency records for: %s, error: %#v", d.Id(), err)
   406  		}
   407  	}
   408  
   409  	if record.Weight != nil {
   410  		v := []map[string]interface{}{{
   411  			"weight": aws.Int64Value((record.Weight)),
   412  		}}
   413  		if err := d.Set("weighted_routing_policy", v); err != nil {
   414  			return fmt.Errorf("[DEBUG] Error setting weighted records for: %s, error: %#v", d.Id(), err)
   415  		}
   416  	}
   417  
   418  	d.Set("set_identifier", record.SetIdentifier)
   419  	d.Set("health_check_id", record.HealthCheckId)
   420  
   421  	return nil
   422  }
   423  
   424  // findRecord takes a ResourceData struct for aws_resource_route53_record. It
   425  // uses the referenced zone_id to query Route53 and find information on it's
   426  // records.
   427  //
   428  // If records are found, it returns the matching
   429  // route53.ResourceRecordSet and nil for the error.
   430  //
   431  // If no hosted zone is found, it returns a nil recordset and r53NoHostedZoneFound
   432  // error.
   433  //
   434  // If no matching recordset is found, it returns nil and a r53NoRecordsFound
   435  // error
   436  //
   437  // If there are other errors, it returns nil a nil recordset and passes on the
   438  // error.
   439  func findRecord(d *schema.ResourceData, meta interface{}) (*route53.ResourceRecordSet, error) {
   440  	conn := meta.(*AWSClient).r53conn
   441  	// Scan for a
   442  	zone := cleanZoneID(d.Get("zone_id").(string))
   443  
   444  	// get expanded name
   445  	zoneRecord, err := conn.GetHostedZone(&route53.GetHostedZoneInput{Id: aws.String(zone)})
   446  	if err != nil {
   447  		if r53err, ok := err.(awserr.Error); ok && r53err.Code() == "NoSuchHostedZone" {
   448  			return nil, r53NoHostedZoneFound
   449  		}
   450  		return nil, err
   451  	}
   452  
   453  	en := expandRecordName(d.Get("name").(string), *zoneRecord.HostedZone.Name)
   454  	log.Printf("[DEBUG] Expanded record name: %s", en)
   455  	d.Set("fqdn", en)
   456  
   457  	lopts := &route53.ListResourceRecordSetsInput{
   458  		HostedZoneId:    aws.String(cleanZoneID(zone)),
   459  		StartRecordName: aws.String(en),
   460  		StartRecordType: aws.String(d.Get("type").(string)),
   461  	}
   462  
   463  	log.Printf("[DEBUG] List resource records sets for zone: %s, opts: %s",
   464  		zone, lopts)
   465  	resp, err := conn.ListResourceRecordSets(lopts)
   466  	if err != nil {
   467  		return nil, err
   468  	}
   469  
   470  	for _, record := range resp.ResourceRecordSets {
   471  		name := cleanRecordName(*record.Name)
   472  		if FQDN(strings.ToLower(name)) != FQDN(strings.ToLower(*lopts.StartRecordName)) {
   473  			continue
   474  		}
   475  		if strings.ToUpper(*record.Type) != strings.ToUpper(*lopts.StartRecordType) {
   476  			continue
   477  		}
   478  
   479  		if record.SetIdentifier != nil && *record.SetIdentifier != d.Get("set_identifier") {
   480  			continue
   481  		}
   482  		// The only safe return where a record is found
   483  		return record, nil
   484  	}
   485  	return nil, r53NoRecordsFound
   486  }
   487  
   488  func resourceAwsRoute53RecordDelete(d *schema.ResourceData, meta interface{}) error {
   489  	conn := meta.(*AWSClient).r53conn
   490  	// Get the records
   491  	rec, err := findRecord(d, meta)
   492  	if err != nil {
   493  		switch err {
   494  		case r53NoHostedZoneFound, r53NoRecordsFound:
   495  			log.Printf("[DEBUG] %s for: %s, removing from state file", err, d.Id())
   496  			d.SetId("")
   497  			return nil
   498  		default:
   499  			return err
   500  		}
   501  	}
   502  
   503  	// Change batch for deleting
   504  	changeBatch := &route53.ChangeBatch{
   505  		Comment: aws.String("Deleted by Terraform"),
   506  		Changes: []*route53.Change{
   507  			&route53.Change{
   508  				Action:            aws.String("DELETE"),
   509  				ResourceRecordSet: rec,
   510  			},
   511  		},
   512  	}
   513  
   514  	zone := cleanZoneID(d.Get("zone_id").(string))
   515  
   516  	req := &route53.ChangeResourceRecordSetsInput{
   517  		HostedZoneId: aws.String(cleanZoneID(zone)),
   518  		ChangeBatch:  changeBatch,
   519  	}
   520  
   521  	wait := resource.StateChangeConf{
   522  		Pending:    []string{"rejected"},
   523  		Target:     []string{"accepted"},
   524  		Timeout:    5 * time.Minute,
   525  		MinTimeout: 1 * time.Second,
   526  		Refresh: func() (interface{}, string, error) {
   527  			_, err := conn.ChangeResourceRecordSets(req)
   528  			if err != nil {
   529  				if r53err, ok := err.(awserr.Error); ok {
   530  					if r53err.Code() == "PriorRequestNotComplete" {
   531  						// There is some pending operation, so just retry
   532  						// in a bit.
   533  						return 42, "rejected", nil
   534  					}
   535  
   536  					if r53err.Code() == "InvalidChangeBatch" {
   537  						// This means that the record is already gone.
   538  						return 42, "accepted", nil
   539  					}
   540  				}
   541  
   542  				return 42, "failure", err
   543  			}
   544  
   545  			return 42, "accepted", nil
   546  		},
   547  	}
   548  
   549  	if _, err := wait.WaitForState(); err != nil {
   550  		return err
   551  	}
   552  
   553  	return nil
   554  }
   555  
   556  func resourceAwsRoute53RecordBuildSet(d *schema.ResourceData, zoneName string) (*route53.ResourceRecordSet, error) {
   557  	// get expanded name
   558  	en := expandRecordName(d.Get("name").(string), zoneName)
   559  
   560  	// Create the RecordSet request with the fully expanded name, e.g.
   561  	// sub.domain.com. Route 53 requires a fully qualified domain name, but does
   562  	// not require the trailing ".", which it will itself, so we don't call FQDN
   563  	// here.
   564  	rec := &route53.ResourceRecordSet{
   565  		Name: aws.String(en),
   566  		Type: aws.String(d.Get("type").(string)),
   567  	}
   568  
   569  	if v, ok := d.GetOk("ttl"); ok {
   570  		rec.TTL = aws.Int64(int64(v.(int)))
   571  	}
   572  
   573  	// Resource records
   574  	if v, ok := d.GetOk("records"); ok {
   575  		recs := v.(*schema.Set).List()
   576  		rec.ResourceRecords = expandResourceRecords(recs, d.Get("type").(string))
   577  	}
   578  
   579  	// Alias record
   580  	if v, ok := d.GetOk("alias"); ok {
   581  		aliases := v.(*schema.Set).List()
   582  		if len(aliases) > 1 {
   583  			return nil, fmt.Errorf("You can only define a single alias target per record")
   584  		}
   585  		alias := aliases[0].(map[string]interface{})
   586  		rec.AliasTarget = &route53.AliasTarget{
   587  			DNSName:              aws.String(alias["name"].(string)),
   588  			EvaluateTargetHealth: aws.Bool(alias["evaluate_target_health"].(bool)),
   589  			HostedZoneId:         aws.String(alias["zone_id"].(string)),
   590  		}
   591  		log.Printf("[DEBUG] Creating alias: %#v", alias)
   592  	} else {
   593  		if _, ok := d.GetOk("ttl"); !ok {
   594  			return nil, fmt.Errorf(`provider.aws: aws_route53_record: %s: "ttl": required field is not set`, d.Get("name").(string))
   595  		}
   596  
   597  		if _, ok := d.GetOk("records"); !ok {
   598  			return nil, fmt.Errorf(`provider.aws: aws_route53_record: %s: "records": required field is not set`, d.Get("name").(string))
   599  		}
   600  	}
   601  
   602  	if v, ok := d.GetOk("failover_routing_policy"); ok {
   603  		if _, ok := d.GetOk("set_identifier"); !ok {
   604  			return nil, fmt.Errorf(`provider.aws: aws_route53_record: %s: "set_identifier": required field is not set when "failover_routing_policy" is set`, d.Get("name").(string))
   605  		}
   606  		records := v.([]interface{})
   607  		if len(records) > 1 {
   608  			return nil, fmt.Errorf("You can only define a single failover_routing_policy per record")
   609  		}
   610  		failover := records[0].(map[string]interface{})
   611  
   612  		rec.Failover = aws.String(failover["type"].(string))
   613  	}
   614  
   615  	if v, ok := d.GetOk("health_check_id"); ok {
   616  		rec.HealthCheckId = aws.String(v.(string))
   617  	}
   618  
   619  	if v, ok := d.GetOk("weighted_routing_policy"); ok {
   620  		if _, ok := d.GetOk("set_identifier"); !ok {
   621  			return nil, fmt.Errorf(`provider.aws: aws_route53_record: %s: "set_identifier": required field is not set when "weight_routing_policy" is set`, d.Get("name").(string))
   622  		}
   623  		records := v.([]interface{})
   624  		if len(records) > 1 {
   625  			return nil, fmt.Errorf("You can only define a single weighed_routing_policy per record")
   626  		}
   627  		weight := records[0].(map[string]interface{})
   628  
   629  		rec.Weight = aws.Int64(int64(weight["weight"].(int)))
   630  	}
   631  
   632  	if v, ok := d.GetOk("set_identifier"); ok {
   633  		rec.SetIdentifier = aws.String(v.(string))
   634  	}
   635  
   636  	if v, ok := d.GetOk("latency_routing_policy"); ok {
   637  		if _, ok := d.GetOk("set_identifier"); !ok {
   638  			return nil, fmt.Errorf(`provider.aws: aws_route53_record: %s: "set_identifier": required field is not set when "latency_routing_policy" is set`, d.Get("name").(string))
   639  		}
   640  		records := v.([]interface{})
   641  		if len(records) > 1 {
   642  			return nil, fmt.Errorf("You can only define a single latency_routing_policy per record")
   643  		}
   644  		latency := records[0].(map[string]interface{})
   645  
   646  		rec.Region = aws.String(latency["region"].(string))
   647  	}
   648  
   649  	if v, ok := d.GetOk("geolocation_routing_policy"); ok {
   650  		if _, ok := d.GetOk("set_identifier"); !ok {
   651  			return nil, fmt.Errorf(`provider.aws: aws_route53_record: %s: "set_identifier": required field is not set when "geolocation_routing_policy" is set`, d.Get("name").(string))
   652  		}
   653  		geolocations := v.([]interface{})
   654  		if len(geolocations) > 1 {
   655  			return nil, fmt.Errorf("You can only define a single geolocation_routing_policy per record")
   656  		}
   657  		geolocation := geolocations[0].(map[string]interface{})
   658  
   659  		rec.GeoLocation = &route53.GeoLocation{
   660  			ContinentCode:   nilString(geolocation["continent"].(string)),
   661  			CountryCode:     nilString(geolocation["country"].(string)),
   662  			SubdivisionCode: nilString(geolocation["subdivision"].(string)),
   663  		}
   664  		log.Printf("[DEBUG] Creating geolocation: %#v", geolocation)
   665  	}
   666  
   667  	return rec, nil
   668  }
   669  
   670  func FQDN(name string) string {
   671  	n := len(name)
   672  	if n == 0 || name[n-1] == '.' {
   673  		return name
   674  	} else {
   675  		return name + "."
   676  	}
   677  }
   678  
   679  // Route 53 stores the "*" wildcard indicator as ASCII 42 and returns the
   680  // octal equivalent, "\\052". Here we look for that, and convert back to "*"
   681  // as needed.
   682  func cleanRecordName(name string) string {
   683  	str := name
   684  	if strings.HasPrefix(name, "\\052") {
   685  		str = strings.Replace(name, "\\052", "*", 1)
   686  		log.Printf("[DEBUG] Replacing octal \\052 for * in: %s", name)
   687  	}
   688  	return str
   689  }
   690  
   691  // Check if the current record name contains the zone suffix.
   692  // If it does not, add the zone name to form a fully qualified name
   693  // and keep AWS happy.
   694  func expandRecordName(name, zone string) string {
   695  	rn := strings.ToLower(strings.TrimSuffix(name, "."))
   696  	zone = strings.TrimSuffix(zone, ".")
   697  	if !strings.HasSuffix(rn, zone) {
   698  		rn = strings.Join([]string{name, zone}, ".")
   699  	}
   700  	return rn
   701  }
   702  
   703  func resourceAwsRoute53AliasRecordHash(v interface{}) int {
   704  	var buf bytes.Buffer
   705  	m := v.(map[string]interface{})
   706  	buf.WriteString(fmt.Sprintf("%s-", m["name"].(string)))
   707  	buf.WriteString(fmt.Sprintf("%s-", m["zone_id"].(string)))
   708  	buf.WriteString(fmt.Sprintf("%t-", m["evaluate_target_health"].(bool)))
   709  
   710  	return hashcode.String(buf.String())
   711  }
   712  
   713  // nilString takes a string as an argument and returns a string
   714  // pointer. The returned pointer is nil if the string argument is
   715  // empty, otherwise it is a pointer to a copy of the string.
   716  func nilString(s string) *string {
   717  	if s == "" {
   718  		return nil
   719  	}
   720  	return aws.String(s)
   721  }