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