github.com/rmenn/terraform@v0.3.8-0.20150225065417-fc84b3a78802/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/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: 10 * 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 if FQDN(*record.Name) != FQDN(*lopts.StartRecordName) { 177 continue 178 } 179 if strings.ToUpper(*record.Type) != strings.ToUpper(*lopts.StartRecordType) { 180 continue 181 } 182 183 found = true 184 185 d.Set("records", record.ResourceRecords) 186 d.Set("ttl", record.TTL) 187 188 break 189 } 190 191 if !found { 192 d.SetId("") 193 } 194 195 return nil 196 } 197 198 func resourceAwsRoute53RecordDelete(d *schema.ResourceData, meta interface{}) error { 199 conn := meta.(*AWSClient).r53conn 200 201 zone := d.Get("zone_id").(string) 202 log.Printf("[DEBUG] Deleting resource records for zone: %s, name: %s", 203 zone, d.Get("name").(string)) 204 205 // Get the records 206 rec, err := resourceAwsRoute53RecordBuildSet(d) 207 if err != nil { 208 return err 209 } 210 211 // Create the new records 212 changeBatch := &route53.ChangeBatch{ 213 Comment: aws.String("Deleted by Terraform"), 214 Changes: []route53.Change{ 215 route53.Change{ 216 Action: aws.String("DELETE"), 217 ResourceRecordSet: rec, 218 }, 219 }, 220 } 221 222 req := &route53.ChangeResourceRecordSetsRequest{ 223 HostedZoneID: aws.String(cleanZoneID(zone)), 224 ChangeBatch: changeBatch, 225 } 226 227 wait := resource.StateChangeConf{ 228 Pending: []string{"rejected"}, 229 Target: "accepted", 230 Timeout: 5 * time.Minute, 231 MinTimeout: 1 * time.Second, 232 Refresh: func() (interface{}, string, error) { 233 _, err := conn.ChangeResourceRecordSets(req) 234 if err != nil { 235 if strings.Contains(err.Error(), "PriorRequestNotComplete") { 236 // There is some pending operation, so just retry 237 // in a bit. 238 return 42, "rejected", nil 239 } 240 241 if strings.Contains(err.Error(), "InvalidChangeBatch") { 242 // This means that the record is already gone. 243 return 42, "accepted", nil 244 } 245 246 return 42, "failure", err 247 } 248 249 return 42, "accepted", nil 250 }, 251 } 252 253 if _, err := wait.WaitForState(); err != nil { 254 return err 255 } 256 257 return nil 258 } 259 260 func resourceAwsRoute53RecordBuildSet(d *schema.ResourceData) (*route53.ResourceRecordSet, error) { 261 recs := d.Get("records").(*schema.Set).List() 262 records := make([]route53.ResourceRecord, 0, len(recs)) 263 264 for _, r := range recs { 265 records = append(records, route53.ResourceRecord{Value: aws.String(r.(string))}) 266 } 267 268 rec := &route53.ResourceRecordSet{ 269 Name: aws.String(d.Get("name").(string)), 270 Type: aws.String(d.Get("type").(string)), 271 TTL: aws.Long(int64(d.Get("ttl").(int))), 272 ResourceRecords: records, 273 } 274 return rec, nil 275 } 276 277 func FQDN(name string) string { 278 n := len(name) 279 if n == 0 || name[n-1] == '.' { 280 return name 281 } else { 282 return name + "." 283 } 284 }