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