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