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