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 }