github.com/armen/terraform@v0.5.2-0.20150529052519-caa8117a08f1/builtin/providers/aws/resource_aws_route53_record.go (about)

     1  package aws
     2  
     3  import (
     4  	"bytes"
     5  	"fmt"
     6  	"log"
     7  	"strings"
     8  	"time"
     9  
    10  	"github.com/hashicorp/terraform/helper/hashcode"
    11  	"github.com/hashicorp/terraform/helper/resource"
    12  	"github.com/hashicorp/terraform/helper/schema"
    13  
    14  	"github.com/awslabs/aws-sdk-go/aws"
    15  	"github.com/awslabs/aws-sdk-go/aws/awserr"
    16  	"github.com/awslabs/aws-sdk-go/service/route53"
    17  )
    18  
    19  func resourceAwsRoute53Record() *schema.Resource {
    20  	return &schema.Resource{
    21  		Create: resourceAwsRoute53RecordCreate,
    22  		Read:   resourceAwsRoute53RecordRead,
    23  		Update: resourceAwsRoute53RecordUpdate,
    24  		Delete: resourceAwsRoute53RecordDelete,
    25  
    26  		Schema: map[string]*schema.Schema{
    27  			"name": &schema.Schema{
    28  				Type:     schema.TypeString,
    29  				Required: true,
    30  				ForceNew: true,
    31  			},
    32  
    33  			"fqdn": &schema.Schema{
    34  				Type:     schema.TypeString,
    35  				Computed: true,
    36  			},
    37  
    38  			"type": &schema.Schema{
    39  				Type:     schema.TypeString,
    40  				Required: true,
    41  				ForceNew: true,
    42  			},
    43  
    44  			"zone_id": &schema.Schema{
    45  				Type:     schema.TypeString,
    46  				Required: true,
    47  				ForceNew: true,
    48  			},
    49  
    50  			"ttl": &schema.Schema{
    51  				Type:          schema.TypeInt,
    52  				Optional:      true,
    53  				ConflictsWith: []string{"alias"},
    54  			},
    55  
    56  			"weight": &schema.Schema{
    57  				Type:     schema.TypeInt,
    58  				Optional: true,
    59  			},
    60  
    61  			"set_identifier": &schema.Schema{
    62  				Type:     schema.TypeString,
    63  				Optional: true,
    64  				ForceNew: true,
    65  			},
    66  
    67  			"alias": &schema.Schema{
    68  				Type:          schema.TypeSet,
    69  				Optional:      true,
    70  				ConflictsWith: []string{"records", "ttl"},
    71  				Elem: &schema.Resource{
    72  					Schema: map[string]*schema.Schema{
    73  						"zone_id": &schema.Schema{
    74  							Type:     schema.TypeString,
    75  							Required: true,
    76  						},
    77  
    78  						"name": &schema.Schema{
    79  							Type:     schema.TypeString,
    80  							Required: true,
    81  						},
    82  
    83  						"evaluate_target_health": &schema.Schema{
    84  							Type:     schema.TypeBool,
    85  							Required: true,
    86  						},
    87  					},
    88  				},
    89  				Set: resourceAwsRoute53AliasRecordHash,
    90  			},
    91  
    92  			"records": &schema.Schema{
    93  				Type:          schema.TypeSet,
    94  				ConflictsWith: []string{"alias"},
    95  				Elem:          &schema.Schema{Type: schema.TypeString},
    96  				Optional:      true,
    97  				Set:           schema.HashString,
    98  			},
    99  		},
   100  	}
   101  }
   102  
   103  func resourceAwsRoute53RecordUpdate(d *schema.ResourceData, meta interface{}) error {
   104  	// Route 53 supports CREATE, DELETE, and UPSERT actions. We use UPSERT, and
   105  	// AWS dynamically determines if a record should be created or updated.
   106  	// Amazon Route 53 can update an existing resource record set only when all
   107  	// of the following values match: Name, Type
   108  	// (and SetIdentifier, which we don't use yet).
   109  	// See http://docs.aws.amazon.com/Route53/latest/APIReference/API_ChangeResourceRecordSets_Requests.html#change-rrsets-request-action
   110  	//
   111  	// Because we use UPSERT, for resouce update here we simply fall through to
   112  	// our resource create function.
   113  	return resourceAwsRoute53RecordCreate(d, meta)
   114  }
   115  
   116  func resourceAwsRoute53RecordCreate(d *schema.ResourceData, meta interface{}) error {
   117  	conn := meta.(*AWSClient).r53conn
   118  	zone := cleanZoneID(d.Get("zone_id").(string))
   119  
   120  	var err error
   121  	zoneRecord, err := conn.GetHostedZone(&route53.GetHostedZoneInput{ID: aws.String(zone)})
   122  	if err != nil {
   123  		return err
   124  	}
   125  
   126  	// Get the record
   127  	rec, err := resourceAwsRoute53RecordBuildSet(d, *zoneRecord.HostedZone.Name)
   128  	if err != nil {
   129  		return err
   130  	}
   131  
   132  	// Create the new records. We abuse StateChangeConf for this to
   133  	// retry for us since Route53 sometimes returns errors about another
   134  	// operation happening at the same time.
   135  	changeBatch := &route53.ChangeBatch{
   136  		Comment: aws.String("Managed by Terraform"),
   137  		Changes: []*route53.Change{
   138  			&route53.Change{
   139  				Action:            aws.String("UPSERT"),
   140  				ResourceRecordSet: rec,
   141  			},
   142  		},
   143  	}
   144  
   145  	req := &route53.ChangeResourceRecordSetsInput{
   146  		HostedZoneID: aws.String(cleanZoneID(*zoneRecord.HostedZone.ID)),
   147  		ChangeBatch:  changeBatch,
   148  	}
   149  
   150  	log.Printf("[DEBUG] Creating resource records for zone: %s, name: %s",
   151  		zone, *rec.Name)
   152  
   153  	wait := resource.StateChangeConf{
   154  		Pending:    []string{"rejected"},
   155  		Target:     "accepted",
   156  		Timeout:    5 * time.Minute,
   157  		MinTimeout: 1 * time.Second,
   158  		Refresh: func() (interface{}, string, error) {
   159  			resp, err := conn.ChangeResourceRecordSets(req)
   160  			if err != nil {
   161  				if r53err, ok := err.(awserr.Error); ok {
   162  					if r53err.Code() == "PriorRequestNotComplete" {
   163  						// There is some pending operation, so just retry
   164  						// in a bit.
   165  						return nil, "rejected", nil
   166  					}
   167  				}
   168  
   169  				return nil, "failure", err
   170  			}
   171  
   172  			return resp, "accepted", nil
   173  		},
   174  	}
   175  
   176  	respRaw, err := wait.WaitForState()
   177  	if err != nil {
   178  		return err
   179  	}
   180  	changeInfo := respRaw.(*route53.ChangeResourceRecordSetsOutput).ChangeInfo
   181  
   182  	// Generate an ID
   183  	vars := []string{
   184  		zone,
   185  		d.Get("name").(string),
   186  		d.Get("type").(string),
   187  	}
   188  	if v, ok := d.GetOk("set_identifier"); ok {
   189  		vars = append(vars, v.(string))
   190  	}
   191  	d.SetId(strings.Join(vars, "_"))
   192  
   193  	// Wait until we are done
   194  	wait = resource.StateChangeConf{
   195  		Delay:      30 * time.Second,
   196  		Pending:    []string{"PENDING"},
   197  		Target:     "INSYNC",
   198  		Timeout:    30 * time.Minute,
   199  		MinTimeout: 5 * time.Second,
   200  		Refresh: func() (result interface{}, state string, err error) {
   201  			changeRequest := &route53.GetChangeInput{
   202  				ID: aws.String(cleanChangeID(*changeInfo.ID)),
   203  			}
   204  			return resourceAwsGoRoute53Wait(conn, changeRequest)
   205  		},
   206  	}
   207  	_, err = wait.WaitForState()
   208  	if err != nil {
   209  		return err
   210  	}
   211  
   212  	return nil
   213  }
   214  
   215  func resourceAwsRoute53RecordRead(d *schema.ResourceData, meta interface{}) error {
   216  	conn := meta.(*AWSClient).r53conn
   217  
   218  	zone := cleanZoneID(d.Get("zone_id").(string))
   219  
   220  	// get expanded name
   221  	zoneRecord, err := conn.GetHostedZone(&route53.GetHostedZoneInput{ID: aws.String(zone)})
   222  	if err != nil {
   223  		return err
   224  	}
   225  	en := expandRecordName(d.Get("name").(string), *zoneRecord.HostedZone.Name)
   226  	log.Printf("[DEBUG] Expanded record name: %s", en)
   227  	d.Set("fqdn", en)
   228  
   229  	lopts := &route53.ListResourceRecordSetsInput{
   230  		HostedZoneID:    aws.String(cleanZoneID(zone)),
   231  		StartRecordName: aws.String(en),
   232  		StartRecordType: aws.String(d.Get("type").(string)),
   233  	}
   234  
   235  	if v, ok := d.GetOk("set_identifier"); ok {
   236  		lopts.StartRecordIdentifier = aws.String(v.(string))
   237  	}
   238  
   239  	resp, err := conn.ListResourceRecordSets(lopts)
   240  	if err != nil {
   241  		return err
   242  	}
   243  
   244  	// Scan for a matching record
   245  	found := false
   246  	for _, record := range resp.ResourceRecordSets {
   247  		name := cleanRecordName(*record.Name)
   248  		if FQDN(name) != FQDN(*lopts.StartRecordName) {
   249  			continue
   250  		}
   251  		if strings.ToUpper(*record.Type) != strings.ToUpper(*lopts.StartRecordType) {
   252  			continue
   253  		}
   254  
   255  		if lopts.StartRecordIdentifier != nil && *record.SetIdentifier != *lopts.StartRecordIdentifier {
   256  			continue
   257  		}
   258  
   259  		found = true
   260  
   261  		err := d.Set("records", flattenResourceRecords(record.ResourceRecords))
   262  		if err != nil {
   263  			return fmt.Errorf("[DEBUG] Error setting records for: %s, error: %#v", en, err)
   264  		}
   265  		d.Set("ttl", record.TTL)
   266  		d.Set("weight", record.Weight)
   267  		d.Set("set_identifier", record.SetIdentifier)
   268  
   269  		break
   270  	}
   271  
   272  	if !found {
   273  		d.SetId("")
   274  	}
   275  
   276  	return nil
   277  }
   278  
   279  func resourceAwsRoute53RecordDelete(d *schema.ResourceData, meta interface{}) error {
   280  	conn := meta.(*AWSClient).r53conn
   281  
   282  	zone := cleanZoneID(d.Get("zone_id").(string))
   283  	log.Printf("[DEBUG] Deleting resource records for zone: %s, name: %s",
   284  		zone, d.Get("name").(string))
   285  	var err error
   286  	zoneRecord, err := conn.GetHostedZone(&route53.GetHostedZoneInput{ID: aws.String(zone)})
   287  	if err != nil {
   288  		return err
   289  	}
   290  	// Get the records
   291  	rec, err := resourceAwsRoute53RecordBuildSet(d, *zoneRecord.HostedZone.Name)
   292  	if err != nil {
   293  		return err
   294  	}
   295  
   296  	// Create the new records
   297  	changeBatch := &route53.ChangeBatch{
   298  		Comment: aws.String("Deleted by Terraform"),
   299  		Changes: []*route53.Change{
   300  			&route53.Change{
   301  				Action:            aws.String("DELETE"),
   302  				ResourceRecordSet: rec,
   303  			},
   304  		},
   305  	}
   306  
   307  	req := &route53.ChangeResourceRecordSetsInput{
   308  		HostedZoneID: aws.String(cleanZoneID(zone)),
   309  		ChangeBatch:  changeBatch,
   310  	}
   311  
   312  	wait := resource.StateChangeConf{
   313  		Pending:    []string{"rejected"},
   314  		Target:     "accepted",
   315  		Timeout:    5 * time.Minute,
   316  		MinTimeout: 1 * time.Second,
   317  		Refresh: func() (interface{}, string, error) {
   318  			_, err := conn.ChangeResourceRecordSets(req)
   319  			if err != nil {
   320  				if r53err, ok := err.(awserr.Error); ok {
   321  					if r53err.Code() == "PriorRequestNotComplete" {
   322  						// There is some pending operation, so just retry
   323  						// in a bit.
   324  						return 42, "rejected", nil
   325  					}
   326  
   327  					if r53err.Code() == "InvalidChangeBatch" {
   328  						// This means that the record is already gone.
   329  						return 42, "accepted", nil
   330  					}
   331  				}
   332  
   333  				return 42, "failure", err
   334  			}
   335  
   336  			return 42, "accepted", nil
   337  		},
   338  	}
   339  
   340  	if _, err := wait.WaitForState(); err != nil {
   341  		return err
   342  	}
   343  
   344  	return nil
   345  }
   346  
   347  func resourceAwsRoute53RecordBuildSet(d *schema.ResourceData, zoneName string) (*route53.ResourceRecordSet, error) {
   348  	// get expanded name
   349  	en := expandRecordName(d.Get("name").(string), zoneName)
   350  
   351  	// Create the RecordSet request with the fully expanded name, e.g.
   352  	// sub.domain.com. Route 53 requires a fully qualified domain name, but does
   353  	// not require the trailing ".", which it will itself, so we don't call FQDN
   354  	// here.
   355  	rec := &route53.ResourceRecordSet{
   356  		Name: aws.String(en),
   357  		Type: aws.String(d.Get("type").(string)),
   358  	}
   359  
   360  	if v, ok := d.GetOk("ttl"); ok {
   361  		rec.TTL = aws.Long(int64(v.(int)))
   362  	}
   363  
   364  	// Resource records
   365  	if v, ok := d.GetOk("records"); ok {
   366  		recs := v.(*schema.Set).List()
   367  		rec.ResourceRecords = expandResourceRecords(recs, d.Get("type").(string))
   368  	}
   369  
   370  	// Alias record
   371  	if v, ok := d.GetOk("alias"); ok {
   372  		aliases := v.(*schema.Set).List()
   373  		if len(aliases) > 1 {
   374  			return nil, fmt.Errorf("You can only define a single alias target per record")
   375  		}
   376  		alias := aliases[0].(map[string]interface{})
   377  		rec.AliasTarget = &route53.AliasTarget{
   378  			DNSName:              aws.String(alias["name"].(string)),
   379  			EvaluateTargetHealth: aws.Boolean(alias["evaluate_target_health"].(bool)),
   380  			HostedZoneID:         aws.String(alias["zone_id"].(string)),
   381  		}
   382  		log.Printf("[DEBUG] Creating alias: %#v", alias)
   383  	} else {
   384  		if _, ok := d.GetOk("ttl"); !ok {
   385  			return nil, fmt.Errorf(`provider.aws: aws_route53_record: %s: "ttl": required field is not set`, d.Get("name").(string))
   386  		}
   387  
   388  		if _, ok := d.GetOk("records"); !ok {
   389  			return nil, fmt.Errorf(`provider.aws: aws_route53_record: %s: "records": required field is not set`, d.Get("name").(string))
   390  		}
   391  	}
   392  
   393  	if v, ok := d.GetOk("weight"); ok {
   394  		rec.Weight = aws.Long(int64(v.(int)))
   395  	}
   396  
   397  	if v, ok := d.GetOk("set_identifier"); ok {
   398  		rec.SetIdentifier = aws.String(v.(string))
   399  	}
   400  
   401  	return rec, nil
   402  }
   403  
   404  func FQDN(name string) string {
   405  	n := len(name)
   406  	if n == 0 || name[n-1] == '.' {
   407  		return name
   408  	} else {
   409  		return name + "."
   410  	}
   411  }
   412  
   413  // Route 53 stores the "*" wildcard indicator as ASCII 42 and returns the
   414  // octal equivalent, "\\052". Here we look for that, and convert back to "*"
   415  // as needed.
   416  func cleanRecordName(name string) string {
   417  	str := name
   418  	if strings.HasPrefix(name, "\\052") {
   419  		str = strings.Replace(name, "\\052", "*", 1)
   420  		log.Printf("[DEBUG] Replacing octal \\052 for * in: %s", name)
   421  	}
   422  	return str
   423  }
   424  
   425  // Check if the current record name contains the zone suffix.
   426  // If it does not, add the zone name to form a fully qualified name
   427  // and keep AWS happy.
   428  func expandRecordName(name, zone string) string {
   429  	rn := strings.TrimSuffix(name, ".")
   430  	zone = strings.TrimSuffix(zone, ".")
   431  	if !strings.HasSuffix(rn, zone) {
   432  		rn = strings.Join([]string{name, zone}, ".")
   433  	}
   434  	return rn
   435  }
   436  
   437  func resourceAwsRoute53AliasRecordHash(v interface{}) int {
   438  	var buf bytes.Buffer
   439  	m := v.(map[string]interface{})
   440  	buf.WriteString(fmt.Sprintf("%s-", m["name"].(string)))
   441  	buf.WriteString(fmt.Sprintf("%s-", m["zone_id"].(string)))
   442  	buf.WriteString(fmt.Sprintf("%t-", m["evaluate_target_health"].(bool)))
   443  
   444  	return hashcode.String(buf.String())
   445  }