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