github.com/alexissmirnov/terraform@v0.4.3-0.20150423153700-1ef9731a2f14/builtin/providers/aws/resource_aws_route53_record.go (about)

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