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