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