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