878 lines
25 KiB
Go
878 lines
25 KiB
Go
package aws
|
|
|
|
import (
|
|
"bytes"
|
|
"errors"
|
|
"fmt"
|
|
"log"
|
|
"strings"
|
|
"time"
|
|
|
|
"github.com/hashicorp/errwrap"
|
|
"github.com/hashicorp/terraform/helper/hashcode"
|
|
"github.com/hashicorp/terraform/helper/resource"
|
|
"github.com/hashicorp/terraform/helper/schema"
|
|
|
|
"github.com/aws/aws-sdk-go/aws"
|
|
"github.com/aws/aws-sdk-go/aws/awserr"
|
|
"github.com/aws/aws-sdk-go/service/route53"
|
|
)
|
|
|
|
var r53NoRecordsFound = errors.New("No matching Hosted Zone found")
|
|
var r53NoHostedZoneFound = errors.New("No matching records found")
|
|
|
|
func resourceAwsRoute53Record() *schema.Resource {
|
|
return &schema.Resource{
|
|
Create: resourceAwsRoute53RecordCreate,
|
|
Read: resourceAwsRoute53RecordRead,
|
|
Update: resourceAwsRoute53RecordUpdate,
|
|
Delete: resourceAwsRoute53RecordDelete,
|
|
Importer: &schema.ResourceImporter{
|
|
State: schema.ImportStatePassthrough,
|
|
},
|
|
SchemaVersion: 2,
|
|
MigrateState: resourceAwsRoute53RecordMigrateState,
|
|
Schema: map[string]*schema.Schema{
|
|
"name": &schema.Schema{
|
|
Type: schema.TypeString,
|
|
Required: true,
|
|
ForceNew: true,
|
|
StateFunc: func(v interface{}) string {
|
|
value := strings.TrimSuffix(v.(string), ".")
|
|
return strings.ToLower(value)
|
|
},
|
|
},
|
|
|
|
"fqdn": &schema.Schema{
|
|
Type: schema.TypeString,
|
|
Computed: true,
|
|
},
|
|
|
|
"type": &schema.Schema{
|
|
Type: schema.TypeString,
|
|
Required: true,
|
|
ValidateFunc: validateRoute53RecordType,
|
|
},
|
|
|
|
"zone_id": &schema.Schema{
|
|
Type: schema.TypeString,
|
|
Required: true,
|
|
ForceNew: true,
|
|
ValidateFunc: func(v interface{}, k string) (ws []string, es []error) {
|
|
value := v.(string)
|
|
if value == "" {
|
|
es = append(es, fmt.Errorf("Cannot have empty zone_id"))
|
|
}
|
|
return
|
|
},
|
|
},
|
|
|
|
"ttl": &schema.Schema{
|
|
Type: schema.TypeInt,
|
|
Optional: true,
|
|
ConflictsWith: []string{"alias"},
|
|
},
|
|
|
|
"weight": &schema.Schema{
|
|
Type: schema.TypeInt,
|
|
Optional: true,
|
|
Removed: "Now implemented as weighted_routing_policy; Please see https://www.terraform.io/docs/providers/aws/r/route53_record.html",
|
|
},
|
|
|
|
"set_identifier": &schema.Schema{
|
|
Type: schema.TypeString,
|
|
Optional: true,
|
|
},
|
|
|
|
"alias": &schema.Schema{
|
|
Type: schema.TypeSet,
|
|
Optional: true,
|
|
ConflictsWith: []string{"records", "ttl"},
|
|
Elem: &schema.Resource{
|
|
Schema: map[string]*schema.Schema{
|
|
"zone_id": &schema.Schema{
|
|
Type: schema.TypeString,
|
|
Required: true,
|
|
},
|
|
|
|
"name": &schema.Schema{
|
|
Type: schema.TypeString,
|
|
Required: true,
|
|
StateFunc: normalizeAwsAliasName,
|
|
},
|
|
|
|
"evaluate_target_health": &schema.Schema{
|
|
Type: schema.TypeBool,
|
|
Required: true,
|
|
},
|
|
},
|
|
},
|
|
Set: resourceAwsRoute53AliasRecordHash,
|
|
},
|
|
|
|
"failover": &schema.Schema{ // PRIMARY | SECONDARY
|
|
Type: schema.TypeString,
|
|
Optional: true,
|
|
Removed: "Now implemented as failover_routing_policy; see docs",
|
|
},
|
|
|
|
"failover_routing_policy": &schema.Schema{
|
|
Type: schema.TypeList,
|
|
Optional: true,
|
|
ConflictsWith: []string{
|
|
"geolocation_routing_policy",
|
|
"latency_routing_policy",
|
|
"weighted_routing_policy",
|
|
},
|
|
Elem: &schema.Resource{
|
|
Schema: map[string]*schema.Schema{
|
|
"type": &schema.Schema{
|
|
Type: schema.TypeString,
|
|
Required: true,
|
|
ValidateFunc: func(v interface{}, k string) (ws []string, es []error) {
|
|
value := v.(string)
|
|
if value != "PRIMARY" && value != "SECONDARY" {
|
|
es = append(es, fmt.Errorf("Failover policy type must be PRIMARY or SECONDARY"))
|
|
}
|
|
return
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
|
|
"latency_routing_policy": &schema.Schema{
|
|
Type: schema.TypeList,
|
|
Optional: true,
|
|
ConflictsWith: []string{
|
|
"failover_routing_policy",
|
|
"geolocation_routing_policy",
|
|
"weighted_routing_policy",
|
|
},
|
|
Elem: &schema.Resource{
|
|
Schema: map[string]*schema.Schema{
|
|
"region": &schema.Schema{
|
|
Type: schema.TypeString,
|
|
Required: true,
|
|
},
|
|
},
|
|
},
|
|
},
|
|
|
|
"geolocation_routing_policy": &schema.Schema{ // AWS Geolocation
|
|
Type: schema.TypeList,
|
|
Optional: true,
|
|
ConflictsWith: []string{
|
|
"failover_routing_policy",
|
|
"latency_routing_policy",
|
|
"weighted_routing_policy",
|
|
},
|
|
Elem: &schema.Resource{
|
|
Schema: map[string]*schema.Schema{
|
|
"continent": &schema.Schema{
|
|
Type: schema.TypeString,
|
|
Optional: true,
|
|
},
|
|
"country": &schema.Schema{
|
|
Type: schema.TypeString,
|
|
Optional: true,
|
|
},
|
|
"subdivision": &schema.Schema{
|
|
Type: schema.TypeString,
|
|
Optional: true,
|
|
},
|
|
},
|
|
},
|
|
},
|
|
|
|
"weighted_routing_policy": &schema.Schema{
|
|
Type: schema.TypeList,
|
|
Optional: true,
|
|
ConflictsWith: []string{
|
|
"failover_routing_policy",
|
|
"geolocation_routing_policy",
|
|
"latency_routing_policy",
|
|
},
|
|
Elem: &schema.Resource{
|
|
Schema: map[string]*schema.Schema{
|
|
"weight": &schema.Schema{
|
|
Type: schema.TypeInt,
|
|
Required: true,
|
|
},
|
|
},
|
|
},
|
|
},
|
|
|
|
"health_check_id": &schema.Schema{ // ID of health check
|
|
Type: schema.TypeString,
|
|
Optional: true,
|
|
},
|
|
|
|
"records": &schema.Schema{
|
|
Type: schema.TypeSet,
|
|
ConflictsWith: []string{"alias"},
|
|
Elem: &schema.Schema{Type: schema.TypeString},
|
|
Optional: true,
|
|
Set: schema.HashString,
|
|
},
|
|
},
|
|
}
|
|
}
|
|
|
|
func resourceAwsRoute53RecordUpdate(d *schema.ResourceData, meta interface{}) error {
|
|
// Route 53 supports CREATE, DELETE, and UPSERT actions. We use UPSERT, and
|
|
// AWS dynamically determines if a record should be created or updated.
|
|
// Amazon Route 53 can update an existing resource record set only when all
|
|
// of the following values match: Name, Type and SetIdentifier
|
|
// See http://docs.aws.amazon.com/Route53/latest/APIReference/API_ChangeResourceRecordSets.html
|
|
|
|
if !d.HasChange("type") && !d.HasChange("set_identifier") {
|
|
// If neither type nor set_identifier changed we use UPSERT,
|
|
// for resouce update here we simply fall through to
|
|
// our resource create function.
|
|
return resourceAwsRoute53RecordCreate(d, meta)
|
|
}
|
|
|
|
// Otherwise we delete the existing record and create a new record within
|
|
// a transactional change
|
|
conn := meta.(*AWSClient).r53conn
|
|
zone := cleanZoneID(d.Get("zone_id").(string))
|
|
|
|
var err error
|
|
zoneRecord, err := conn.GetHostedZone(&route53.GetHostedZoneInput{Id: aws.String(zone)})
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if zoneRecord.HostedZone == nil {
|
|
return fmt.Errorf("[WARN] No Route53 Zone found for id (%s)", zone)
|
|
}
|
|
|
|
// Build the to be deleted record
|
|
en := expandRecordName(d.Get("name").(string), *zoneRecord.HostedZone.Name)
|
|
typeo, _ := d.GetChange("type")
|
|
|
|
oldRec := &route53.ResourceRecordSet{
|
|
Name: aws.String(en),
|
|
Type: aws.String(typeo.(string)),
|
|
}
|
|
|
|
if v, _ := d.GetChange("ttl"); v.(int) != 0 {
|
|
oldRec.TTL = aws.Int64(int64(v.(int)))
|
|
}
|
|
|
|
// Resource records
|
|
if v, _ := d.GetChange("records"); v != nil {
|
|
recs := v.(*schema.Set).List()
|
|
if len(recs) > 0 {
|
|
oldRec.ResourceRecords = expandResourceRecords(recs, typeo.(string))
|
|
}
|
|
}
|
|
|
|
// Alias record
|
|
if v, _ := d.GetChange("alias"); v != nil {
|
|
aliases := v.(*schema.Set).List()
|
|
if len(aliases) == 1 {
|
|
alias := aliases[0].(map[string]interface{})
|
|
oldRec.AliasTarget = &route53.AliasTarget{
|
|
DNSName: aws.String(alias["name"].(string)),
|
|
EvaluateTargetHealth: aws.Bool(alias["evaluate_target_health"].(bool)),
|
|
HostedZoneId: aws.String(alias["zone_id"].(string)),
|
|
}
|
|
}
|
|
}
|
|
|
|
if v, _ := d.GetChange("set_identifier"); v.(string) != "" {
|
|
oldRec.SetIdentifier = aws.String(v.(string))
|
|
}
|
|
|
|
// Build the to be created record
|
|
rec, err := resourceAwsRoute53RecordBuildSet(d, *zoneRecord.HostedZone.Name)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
// Delete the old and create the new records in a single batch. We abuse
|
|
// StateChangeConf for this to retry for us since Route53 sometimes returns
|
|
// errors about another operation happening at the same time.
|
|
changeBatch := &route53.ChangeBatch{
|
|
Comment: aws.String("Managed by Terraform"),
|
|
Changes: []*route53.Change{
|
|
&route53.Change{
|
|
Action: aws.String("DELETE"),
|
|
ResourceRecordSet: oldRec,
|
|
},
|
|
&route53.Change{
|
|
Action: aws.String("CREATE"),
|
|
ResourceRecordSet: rec,
|
|
},
|
|
},
|
|
}
|
|
|
|
req := &route53.ChangeResourceRecordSetsInput{
|
|
HostedZoneId: aws.String(cleanZoneID(*zoneRecord.HostedZone.Id)),
|
|
ChangeBatch: changeBatch,
|
|
}
|
|
|
|
log.Printf("[DEBUG] Updating resource records for zone: %s, name: %s\n\n%s",
|
|
zone, *rec.Name, req)
|
|
|
|
respRaw, err := changeRoute53RecordSet(conn, req)
|
|
if err != nil {
|
|
return errwrap.Wrapf("[ERR]: Error building changeset: {{err}}", err)
|
|
}
|
|
|
|
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 resourceAwsRoute53RecordCreate(d *schema.ResourceData, meta interface{}) error {
|
|
conn := meta.(*AWSClient).r53conn
|
|
zone := cleanZoneID(d.Get("zone_id").(string))
|
|
|
|
var err error
|
|
zoneRecord, err := conn.GetHostedZone(&route53.GetHostedZoneInput{Id: aws.String(zone)})
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if zoneRecord.HostedZone == nil {
|
|
return fmt.Errorf("[WARN] No Route53 Zone found for id (%s)", zone)
|
|
}
|
|
|
|
// Build the record
|
|
rec, err := resourceAwsRoute53RecordBuildSet(d, *zoneRecord.HostedZone.Name)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
// Create the new records. We abuse StateChangeConf for this to
|
|
// retry for us since Route53 sometimes returns errors about another
|
|
// operation happening at the same time.
|
|
changeBatch := &route53.ChangeBatch{
|
|
Comment: aws.String("Managed by Terraform"),
|
|
Changes: []*route53.Change{
|
|
&route53.Change{
|
|
Action: aws.String("UPSERT"),
|
|
ResourceRecordSet: rec,
|
|
},
|
|
},
|
|
}
|
|
|
|
req := &route53.ChangeResourceRecordSetsInput{
|
|
HostedZoneId: aws.String(cleanZoneID(*zoneRecord.HostedZone.Id)),
|
|
ChangeBatch: changeBatch,
|
|
}
|
|
|
|
log.Printf("[DEBUG] Creating resource records for zone: %s, name: %s\n\n%s",
|
|
zone, *rec.Name, req)
|
|
|
|
respRaw, err := changeRoute53RecordSet(conn, req)
|
|
if err != nil {
|
|
return errwrap.Wrapf("[ERR]: Error building changeset: {{err}}", err)
|
|
}
|
|
|
|
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(input)
|
|
if err != nil {
|
|
if r53err, ok := err.(awserr.Error); ok {
|
|
if r53err.Code() == "PriorRequestNotComplete" {
|
|
// There is some pending operation, so just retry
|
|
// in a bit.
|
|
return nil, "rejected", nil
|
|
}
|
|
}
|
|
|
|
return nil, "failure", err
|
|
}
|
|
|
|
return resp, "accepted", nil
|
|
},
|
|
}
|
|
|
|
return wait.WaitForState()
|
|
}
|
|
|
|
func waitForRoute53RecordSetToSync(conn *route53.Route53, requestId string) error {
|
|
wait := resource.StateChangeConf{
|
|
Delay: 30 * time.Second,
|
|
Pending: []string{"PENDING"},
|
|
Target: []string{"INSYNC"},
|
|
Timeout: 30 * time.Minute,
|
|
MinTimeout: 5 * time.Second,
|
|
Refresh: func() (result interface{}, state string, err error) {
|
|
changeRequest := &route53.GetChangeInput{
|
|
Id: aws.String(requestId),
|
|
}
|
|
return resourceAwsGoRoute53Wait(conn, changeRequest)
|
|
},
|
|
}
|
|
_, err := wait.WaitForState()
|
|
return err
|
|
}
|
|
|
|
func resourceAwsRoute53RecordRead(d *schema.ResourceData, meta interface{}) error {
|
|
// If we don't have a zone ID we're doing an import. Parse it from the ID.
|
|
if _, ok := d.GetOk("zone_id"); !ok {
|
|
parts := strings.Split(d.Id(), "_")
|
|
|
|
if len(parts) == 1 {
|
|
return fmt.Errorf("Error Importing aws_route_53 record. Please make sure the record ID is in the form ZONEID_RECORDNAME_TYPE (i.e. Z4KAPRWWNC7JR_dev_A")
|
|
}
|
|
|
|
d.Set("zone_id", parts[0])
|
|
d.Set("name", parts[1])
|
|
d.Set("type", parts[2])
|
|
if len(parts) > 3 {
|
|
d.Set("set_identifier", parts[3])
|
|
}
|
|
|
|
d.Set("weight", -1)
|
|
}
|
|
|
|
record, err := findRecord(d, meta)
|
|
if err != nil {
|
|
switch err {
|
|
case r53NoHostedZoneFound, r53NoRecordsFound:
|
|
log.Printf("[DEBUG] %s for: %s, removing from state file", err, d.Id())
|
|
d.SetId("")
|
|
return nil
|
|
default:
|
|
return err
|
|
}
|
|
}
|
|
|
|
err = d.Set("records", flattenResourceRecords(record.ResourceRecords))
|
|
if err != nil {
|
|
return fmt.Errorf("[DEBUG] Error setting records for: %s, error: %#v", d.Id(), err)
|
|
}
|
|
|
|
if alias := record.AliasTarget; alias != nil {
|
|
name := normalizeAwsAliasName(*alias.DNSName)
|
|
d.Set("alias", []interface{}{
|
|
map[string]interface{}{
|
|
"zone_id": *alias.HostedZoneId,
|
|
"name": name,
|
|
"evaluate_target_health": *alias.EvaluateTargetHealth,
|
|
},
|
|
})
|
|
}
|
|
|
|
d.Set("ttl", record.TTL)
|
|
|
|
if record.Failover != nil {
|
|
v := []map[string]interface{}{{
|
|
"type": aws.StringValue(record.Failover),
|
|
}}
|
|
if err := d.Set("failover_routing_policy", v); err != nil {
|
|
return fmt.Errorf("[DEBUG] Error setting failover records for: %s, error: %#v", d.Id(), err)
|
|
}
|
|
}
|
|
|
|
if record.GeoLocation != nil {
|
|
v := []map[string]interface{}{{
|
|
"continent": aws.StringValue(record.GeoLocation.ContinentCode),
|
|
"country": aws.StringValue(record.GeoLocation.CountryCode),
|
|
"subdivision": aws.StringValue(record.GeoLocation.SubdivisionCode),
|
|
}}
|
|
if err := d.Set("geolocation_routing_policy", v); err != nil {
|
|
return fmt.Errorf("[DEBUG] Error setting gelocation records for: %s, error: %#v", d.Id(), err)
|
|
}
|
|
}
|
|
|
|
if record.Region != nil {
|
|
v := []map[string]interface{}{{
|
|
"region": aws.StringValue(record.Region),
|
|
}}
|
|
if err := d.Set("latency_routing_policy", v); err != nil {
|
|
return fmt.Errorf("[DEBUG] Error setting latency records for: %s, error: %#v", d.Id(), err)
|
|
}
|
|
}
|
|
|
|
if record.Weight != nil {
|
|
v := []map[string]interface{}{{
|
|
"weight": aws.Int64Value((record.Weight)),
|
|
}}
|
|
if err := d.Set("weighted_routing_policy", v); err != nil {
|
|
return fmt.Errorf("[DEBUG] Error setting weighted records for: %s, error: %#v", d.Id(), err)
|
|
}
|
|
}
|
|
|
|
d.Set("set_identifier", record.SetIdentifier)
|
|
d.Set("health_check_id", record.HealthCheckId)
|
|
|
|
return nil
|
|
}
|
|
|
|
// findRecord takes a ResourceData struct for aws_resource_route53_record. It
|
|
// uses the referenced zone_id to query Route53 and find information on it's
|
|
// records.
|
|
//
|
|
// If records are found, it returns the matching
|
|
// route53.ResourceRecordSet and nil for the error.
|
|
//
|
|
// If no hosted zone is found, it returns a nil recordset and r53NoHostedZoneFound
|
|
// error.
|
|
//
|
|
// If no matching recordset is found, it returns nil and a r53NoRecordsFound
|
|
// error
|
|
//
|
|
// If there are other errors, it returns nil a nil recordset and passes on the
|
|
// error.
|
|
func findRecord(d *schema.ResourceData, meta interface{}) (*route53.ResourceRecordSet, error) {
|
|
conn := meta.(*AWSClient).r53conn
|
|
// Scan for a
|
|
zone := cleanZoneID(d.Get("zone_id").(string))
|
|
|
|
// get expanded name
|
|
zoneRecord, err := conn.GetHostedZone(&route53.GetHostedZoneInput{Id: aws.String(zone)})
|
|
if err != nil {
|
|
if r53err, ok := err.(awserr.Error); ok && r53err.Code() == "NoSuchHostedZone" {
|
|
return nil, r53NoHostedZoneFound
|
|
}
|
|
return nil, err
|
|
}
|
|
|
|
en := expandRecordName(d.Get("name").(string), *zoneRecord.HostedZone.Name)
|
|
log.Printf("[DEBUG] Expanded record name: %s", en)
|
|
d.Set("fqdn", en)
|
|
|
|
lopts := &route53.ListResourceRecordSetsInput{
|
|
HostedZoneId: aws.String(cleanZoneID(zone)),
|
|
StartRecordName: aws.String(en),
|
|
StartRecordType: aws.String(d.Get("type").(string)),
|
|
}
|
|
|
|
log.Printf("[DEBUG] List resource records sets for zone: %s, opts: %s",
|
|
zone, lopts)
|
|
resp, err := conn.ListResourceRecordSets(lopts)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
for _, record := range resp.ResourceRecordSets {
|
|
name := cleanRecordName(*record.Name)
|
|
if FQDN(strings.ToLower(name)) != FQDN(strings.ToLower(*lopts.StartRecordName)) {
|
|
continue
|
|
}
|
|
if strings.ToUpper(*record.Type) != strings.ToUpper(*lopts.StartRecordType) {
|
|
continue
|
|
}
|
|
|
|
if record.SetIdentifier != nil && *record.SetIdentifier != d.Get("set_identifier") {
|
|
continue
|
|
}
|
|
// The only safe return where a record is found
|
|
return record, nil
|
|
}
|
|
return nil, r53NoRecordsFound
|
|
}
|
|
|
|
func resourceAwsRoute53RecordDelete(d *schema.ResourceData, meta interface{}) error {
|
|
conn := meta.(*AWSClient).r53conn
|
|
// Get the records
|
|
rec, err := findRecord(d, meta)
|
|
if err != nil {
|
|
switch err {
|
|
case r53NoHostedZoneFound, r53NoRecordsFound:
|
|
log.Printf("[DEBUG] %s for: %s, removing from state file", err, d.Id())
|
|
d.SetId("")
|
|
return nil
|
|
default:
|
|
return err
|
|
}
|
|
}
|
|
|
|
// Change batch for deleting
|
|
changeBatch := &route53.ChangeBatch{
|
|
Comment: aws.String("Deleted by Terraform"),
|
|
Changes: []*route53.Change{
|
|
&route53.Change{
|
|
Action: aws.String("DELETE"),
|
|
ResourceRecordSet: rec,
|
|
},
|
|
},
|
|
}
|
|
|
|
zone := cleanZoneID(d.Get("zone_id").(string))
|
|
|
|
req := &route53.ChangeResourceRecordSetsInput{
|
|
HostedZoneId: aws.String(cleanZoneID(zone)),
|
|
ChangeBatch: changeBatch,
|
|
}
|
|
|
|
respRaw, err := deleteRoute53RecordSet(conn, req)
|
|
if err != nil {
|
|
return errwrap.Wrapf("[ERR]: Error building changeset: {{err}}", err)
|
|
}
|
|
|
|
changeInfo := respRaw.(*route53.ChangeResourceRecordSetsOutput).ChangeInfo
|
|
if changeInfo == nil {
|
|
log.Printf("[INFO] No ChangeInfo Found. Waiting for Sync not required")
|
|
return nil
|
|
}
|
|
|
|
err = waitForRoute53RecordSetToSync(conn, cleanChangeID(*changeInfo.Id))
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
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) {
|
|
resp, err := conn.ChangeResourceRecordSets(input)
|
|
if err != nil {
|
|
if r53err, ok := err.(awserr.Error); ok {
|
|
if r53err.Code() == "PriorRequestNotComplete" {
|
|
// There is some pending operation, so just retry
|
|
// in a bit.
|
|
return 42, "rejected", nil
|
|
}
|
|
|
|
if r53err.Code() == "InvalidChangeBatch" {
|
|
// This means that the record is already gone.
|
|
return resp, "accepted", nil
|
|
}
|
|
}
|
|
|
|
return 42, "failure", err
|
|
}
|
|
|
|
return resp, "accepted", nil
|
|
},
|
|
}
|
|
|
|
return wait.WaitForState()
|
|
}
|
|
|
|
func resourceAwsRoute53RecordBuildSet(d *schema.ResourceData, zoneName string) (*route53.ResourceRecordSet, error) {
|
|
// get expanded name
|
|
en := expandRecordName(d.Get("name").(string), zoneName)
|
|
|
|
// Create the RecordSet request with the fully expanded name, e.g.
|
|
// sub.domain.com. Route 53 requires a fully qualified domain name, but does
|
|
// not require the trailing ".", which it will itself, so we don't call FQDN
|
|
// here.
|
|
rec := &route53.ResourceRecordSet{
|
|
Name: aws.String(en),
|
|
Type: aws.String(d.Get("type").(string)),
|
|
}
|
|
|
|
if v, ok := d.GetOk("ttl"); ok {
|
|
rec.TTL = aws.Int64(int64(v.(int)))
|
|
}
|
|
|
|
// Resource records
|
|
if v, ok := d.GetOk("records"); ok {
|
|
recs := v.(*schema.Set).List()
|
|
rec.ResourceRecords = expandResourceRecords(recs, d.Get("type").(string))
|
|
}
|
|
|
|
// Alias record
|
|
if v, ok := d.GetOk("alias"); ok {
|
|
aliases := v.(*schema.Set).List()
|
|
if len(aliases) > 1 {
|
|
return nil, fmt.Errorf("You can only define a single alias target per record")
|
|
}
|
|
alias := aliases[0].(map[string]interface{})
|
|
rec.AliasTarget = &route53.AliasTarget{
|
|
DNSName: aws.String(alias["name"].(string)),
|
|
EvaluateTargetHealth: aws.Bool(alias["evaluate_target_health"].(bool)),
|
|
HostedZoneId: aws.String(alias["zone_id"].(string)),
|
|
}
|
|
log.Printf("[DEBUG] Creating alias: %#v", alias)
|
|
} else {
|
|
if _, ok := d.GetOk("ttl"); !ok {
|
|
return nil, fmt.Errorf(`provider.aws: aws_route53_record: %s: "ttl": required field is not set`, d.Get("name").(string))
|
|
}
|
|
|
|
if _, ok := d.GetOk("records"); !ok {
|
|
return nil, fmt.Errorf(`provider.aws: aws_route53_record: %s: "records": required field is not set`, d.Get("name").(string))
|
|
}
|
|
}
|
|
|
|
if v, ok := d.GetOk("failover_routing_policy"); ok {
|
|
if _, ok := d.GetOk("set_identifier"); !ok {
|
|
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))
|
|
}
|
|
records := v.([]interface{})
|
|
if len(records) > 1 {
|
|
return nil, fmt.Errorf("You can only define a single failover_routing_policy per record")
|
|
}
|
|
failover := records[0].(map[string]interface{})
|
|
|
|
rec.Failover = aws.String(failover["type"].(string))
|
|
}
|
|
|
|
if v, ok := d.GetOk("health_check_id"); ok {
|
|
rec.HealthCheckId = aws.String(v.(string))
|
|
}
|
|
|
|
if v, ok := d.GetOk("weighted_routing_policy"); ok {
|
|
if _, ok := d.GetOk("set_identifier"); !ok {
|
|
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))
|
|
}
|
|
records := v.([]interface{})
|
|
if len(records) > 1 {
|
|
return nil, fmt.Errorf("You can only define a single weighed_routing_policy per record")
|
|
}
|
|
weight := records[0].(map[string]interface{})
|
|
|
|
rec.Weight = aws.Int64(int64(weight["weight"].(int)))
|
|
}
|
|
|
|
if v, ok := d.GetOk("set_identifier"); ok {
|
|
rec.SetIdentifier = aws.String(v.(string))
|
|
}
|
|
|
|
if v, ok := d.GetOk("latency_routing_policy"); ok {
|
|
if _, ok := d.GetOk("set_identifier"); !ok {
|
|
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))
|
|
}
|
|
records := v.([]interface{})
|
|
if len(records) > 1 {
|
|
return nil, fmt.Errorf("You can only define a single latency_routing_policy per record")
|
|
}
|
|
latency := records[0].(map[string]interface{})
|
|
|
|
rec.Region = aws.String(latency["region"].(string))
|
|
}
|
|
|
|
if v, ok := d.GetOk("geolocation_routing_policy"); ok {
|
|
if _, ok := d.GetOk("set_identifier"); !ok {
|
|
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))
|
|
}
|
|
geolocations := v.([]interface{})
|
|
if len(geolocations) > 1 {
|
|
return nil, fmt.Errorf("You can only define a single geolocation_routing_policy per record")
|
|
}
|
|
geolocation := geolocations[0].(map[string]interface{})
|
|
|
|
rec.GeoLocation = &route53.GeoLocation{
|
|
ContinentCode: nilString(geolocation["continent"].(string)),
|
|
CountryCode: nilString(geolocation["country"].(string)),
|
|
SubdivisionCode: nilString(geolocation["subdivision"].(string)),
|
|
}
|
|
log.Printf("[DEBUG] Creating geolocation: %#v", geolocation)
|
|
}
|
|
|
|
return rec, nil
|
|
}
|
|
|
|
func FQDN(name string) string {
|
|
n := len(name)
|
|
if n == 0 || name[n-1] == '.' {
|
|
return name
|
|
} else {
|
|
return name + "."
|
|
}
|
|
}
|
|
|
|
// Route 53 stores the "*" wildcard indicator as ASCII 42 and returns the
|
|
// octal equivalent, "\\052". Here we look for that, and convert back to "*"
|
|
// as needed.
|
|
func cleanRecordName(name string) string {
|
|
str := name
|
|
if strings.HasPrefix(name, "\\052") {
|
|
str = strings.Replace(name, "\\052", "*", 1)
|
|
log.Printf("[DEBUG] Replacing octal \\052 for * in: %s", name)
|
|
}
|
|
return str
|
|
}
|
|
|
|
// Check if the current record name contains the zone suffix.
|
|
// If it does not, add the zone name to form a fully qualified name
|
|
// and keep AWS happy.
|
|
func expandRecordName(name, zone string) string {
|
|
rn := strings.ToLower(strings.TrimSuffix(name, "."))
|
|
zone = strings.TrimSuffix(zone, ".")
|
|
if !strings.HasSuffix(rn, zone) {
|
|
if len(name) == 0 {
|
|
rn = zone
|
|
} else {
|
|
rn = strings.Join([]string{name, zone}, ".")
|
|
}
|
|
}
|
|
return rn
|
|
}
|
|
|
|
func resourceAwsRoute53AliasRecordHash(v interface{}) int {
|
|
var buf bytes.Buffer
|
|
m := v.(map[string]interface{})
|
|
buf.WriteString(fmt.Sprintf("%s-", normalizeAwsAliasName(m["name"].(string))))
|
|
buf.WriteString(fmt.Sprintf("%s-", m["zone_id"].(string)))
|
|
buf.WriteString(fmt.Sprintf("%t-", m["evaluate_target_health"].(bool)))
|
|
|
|
return hashcode.String(buf.String())
|
|
}
|
|
|
|
// nilString takes a string as an argument and returns a string
|
|
// pointer. The returned pointer is nil if the string argument is
|
|
// empty, otherwise it is a pointer to a copy of the string.
|
|
func nilString(s string) *string {
|
|
if s == "" {
|
|
return nil
|
|
}
|
|
return aws.String(s)
|
|
}
|
|
|
|
func normalizeAwsAliasName(alias interface{}) string {
|
|
input := alias.(string)
|
|
if strings.HasPrefix(input, "dualstack.") {
|
|
return strings.Replace(input, "dualstack.", "", -1)
|
|
}
|
|
|
|
return strings.TrimRight(input, ".")
|
|
}
|