github.com/armen/terraform@v0.5.2-0.20150529052519-caa8117a08f1/builtin/providers/aws/resource_aws_route53_record.go (about) 1 package aws 2 3 import ( 4 "bytes" 5 "fmt" 6 "log" 7 "strings" 8 "time" 9 10 "github.com/hashicorp/terraform/helper/hashcode" 11 "github.com/hashicorp/terraform/helper/resource" 12 "github.com/hashicorp/terraform/helper/schema" 13 14 "github.com/awslabs/aws-sdk-go/aws" 15 "github.com/awslabs/aws-sdk-go/aws/awserr" 16 "github.com/awslabs/aws-sdk-go/service/route53" 17 ) 18 19 func resourceAwsRoute53Record() *schema.Resource { 20 return &schema.Resource{ 21 Create: resourceAwsRoute53RecordCreate, 22 Read: resourceAwsRoute53RecordRead, 23 Update: resourceAwsRoute53RecordUpdate, 24 Delete: resourceAwsRoute53RecordDelete, 25 26 Schema: map[string]*schema.Schema{ 27 "name": &schema.Schema{ 28 Type: schema.TypeString, 29 Required: true, 30 ForceNew: true, 31 }, 32 33 "fqdn": &schema.Schema{ 34 Type: schema.TypeString, 35 Computed: true, 36 }, 37 38 "type": &schema.Schema{ 39 Type: schema.TypeString, 40 Required: true, 41 ForceNew: true, 42 }, 43 44 "zone_id": &schema.Schema{ 45 Type: schema.TypeString, 46 Required: true, 47 ForceNew: true, 48 }, 49 50 "ttl": &schema.Schema{ 51 Type: schema.TypeInt, 52 Optional: true, 53 ConflictsWith: []string{"alias"}, 54 }, 55 56 "weight": &schema.Schema{ 57 Type: schema.TypeInt, 58 Optional: true, 59 }, 60 61 "set_identifier": &schema.Schema{ 62 Type: schema.TypeString, 63 Optional: true, 64 ForceNew: true, 65 }, 66 67 "alias": &schema.Schema{ 68 Type: schema.TypeSet, 69 Optional: true, 70 ConflictsWith: []string{"records", "ttl"}, 71 Elem: &schema.Resource{ 72 Schema: map[string]*schema.Schema{ 73 "zone_id": &schema.Schema{ 74 Type: schema.TypeString, 75 Required: true, 76 }, 77 78 "name": &schema.Schema{ 79 Type: schema.TypeString, 80 Required: true, 81 }, 82 83 "evaluate_target_health": &schema.Schema{ 84 Type: schema.TypeBool, 85 Required: true, 86 }, 87 }, 88 }, 89 Set: resourceAwsRoute53AliasRecordHash, 90 }, 91 92 "records": &schema.Schema{ 93 Type: schema.TypeSet, 94 ConflictsWith: []string{"alias"}, 95 Elem: &schema.Schema{Type: schema.TypeString}, 96 Optional: true, 97 Set: schema.HashString, 98 }, 99 }, 100 } 101 } 102 103 func resourceAwsRoute53RecordUpdate(d *schema.ResourceData, meta interface{}) error { 104 // Route 53 supports CREATE, DELETE, and UPSERT actions. We use UPSERT, and 105 // AWS dynamically determines if a record should be created or updated. 106 // Amazon Route 53 can update an existing resource record set only when all 107 // of the following values match: Name, Type 108 // (and SetIdentifier, which we don't use yet). 109 // See http://docs.aws.amazon.com/Route53/latest/APIReference/API_ChangeResourceRecordSets_Requests.html#change-rrsets-request-action 110 // 111 // Because we use UPSERT, for resouce update here we simply fall through to 112 // our resource create function. 113 return resourceAwsRoute53RecordCreate(d, meta) 114 } 115 116 func resourceAwsRoute53RecordCreate(d *schema.ResourceData, meta interface{}) error { 117 conn := meta.(*AWSClient).r53conn 118 zone := cleanZoneID(d.Get("zone_id").(string)) 119 120 var err error 121 zoneRecord, err := conn.GetHostedZone(&route53.GetHostedZoneInput{ID: aws.String(zone)}) 122 if err != nil { 123 return err 124 } 125 126 // Get the record 127 rec, err := resourceAwsRoute53RecordBuildSet(d, *zoneRecord.HostedZone.Name) 128 if err != nil { 129 return err 130 } 131 132 // Create the new records. We abuse StateChangeConf for this to 133 // retry for us since Route53 sometimes returns errors about another 134 // operation happening at the same time. 135 changeBatch := &route53.ChangeBatch{ 136 Comment: aws.String("Managed by Terraform"), 137 Changes: []*route53.Change{ 138 &route53.Change{ 139 Action: aws.String("UPSERT"), 140 ResourceRecordSet: rec, 141 }, 142 }, 143 } 144 145 req := &route53.ChangeResourceRecordSetsInput{ 146 HostedZoneID: aws.String(cleanZoneID(*zoneRecord.HostedZone.ID)), 147 ChangeBatch: changeBatch, 148 } 149 150 log.Printf("[DEBUG] Creating resource records for zone: %s, name: %s", 151 zone, *rec.Name) 152 153 wait := resource.StateChangeConf{ 154 Pending: []string{"rejected"}, 155 Target: "accepted", 156 Timeout: 5 * time.Minute, 157 MinTimeout: 1 * time.Second, 158 Refresh: func() (interface{}, string, error) { 159 resp, err := conn.ChangeResourceRecordSets(req) 160 if err != nil { 161 if r53err, ok := err.(awserr.Error); ok { 162 if r53err.Code() == "PriorRequestNotComplete" { 163 // There is some pending operation, so just retry 164 // in a bit. 165 return nil, "rejected", nil 166 } 167 } 168 169 return nil, "failure", err 170 } 171 172 return resp, "accepted", nil 173 }, 174 } 175 176 respRaw, err := wait.WaitForState() 177 if err != nil { 178 return err 179 } 180 changeInfo := respRaw.(*route53.ChangeResourceRecordSetsOutput).ChangeInfo 181 182 // Generate an ID 183 vars := []string{ 184 zone, 185 d.Get("name").(string), 186 d.Get("type").(string), 187 } 188 if v, ok := d.GetOk("set_identifier"); ok { 189 vars = append(vars, v.(string)) 190 } 191 d.SetId(strings.Join(vars, "_")) 192 193 // Wait until we are done 194 wait = resource.StateChangeConf{ 195 Delay: 30 * time.Second, 196 Pending: []string{"PENDING"}, 197 Target: "INSYNC", 198 Timeout: 30 * time.Minute, 199 MinTimeout: 5 * time.Second, 200 Refresh: func() (result interface{}, state string, err error) { 201 changeRequest := &route53.GetChangeInput{ 202 ID: aws.String(cleanChangeID(*changeInfo.ID)), 203 } 204 return resourceAwsGoRoute53Wait(conn, changeRequest) 205 }, 206 } 207 _, err = wait.WaitForState() 208 if err != nil { 209 return err 210 } 211 212 return nil 213 } 214 215 func resourceAwsRoute53RecordRead(d *schema.ResourceData, meta interface{}) error { 216 conn := meta.(*AWSClient).r53conn 217 218 zone := cleanZoneID(d.Get("zone_id").(string)) 219 220 // get expanded name 221 zoneRecord, err := conn.GetHostedZone(&route53.GetHostedZoneInput{ID: aws.String(zone)}) 222 if err != nil { 223 return err 224 } 225 en := expandRecordName(d.Get("name").(string), *zoneRecord.HostedZone.Name) 226 log.Printf("[DEBUG] Expanded record name: %s", en) 227 d.Set("fqdn", en) 228 229 lopts := &route53.ListResourceRecordSetsInput{ 230 HostedZoneID: aws.String(cleanZoneID(zone)), 231 StartRecordName: aws.String(en), 232 StartRecordType: aws.String(d.Get("type").(string)), 233 } 234 235 if v, ok := d.GetOk("set_identifier"); ok { 236 lopts.StartRecordIdentifier = aws.String(v.(string)) 237 } 238 239 resp, err := conn.ListResourceRecordSets(lopts) 240 if err != nil { 241 return err 242 } 243 244 // Scan for a matching record 245 found := false 246 for _, record := range resp.ResourceRecordSets { 247 name := cleanRecordName(*record.Name) 248 if FQDN(name) != FQDN(*lopts.StartRecordName) { 249 continue 250 } 251 if strings.ToUpper(*record.Type) != strings.ToUpper(*lopts.StartRecordType) { 252 continue 253 } 254 255 if lopts.StartRecordIdentifier != nil && *record.SetIdentifier != *lopts.StartRecordIdentifier { 256 continue 257 } 258 259 found = true 260 261 err := d.Set("records", flattenResourceRecords(record.ResourceRecords)) 262 if err != nil { 263 return fmt.Errorf("[DEBUG] Error setting records for: %s, error: %#v", en, err) 264 } 265 d.Set("ttl", record.TTL) 266 d.Set("weight", record.Weight) 267 d.Set("set_identifier", record.SetIdentifier) 268 269 break 270 } 271 272 if !found { 273 d.SetId("") 274 } 275 276 return nil 277 } 278 279 func resourceAwsRoute53RecordDelete(d *schema.ResourceData, meta interface{}) error { 280 conn := meta.(*AWSClient).r53conn 281 282 zone := cleanZoneID(d.Get("zone_id").(string)) 283 log.Printf("[DEBUG] Deleting resource records for zone: %s, name: %s", 284 zone, d.Get("name").(string)) 285 var err error 286 zoneRecord, err := conn.GetHostedZone(&route53.GetHostedZoneInput{ID: aws.String(zone)}) 287 if err != nil { 288 return err 289 } 290 // Get the records 291 rec, err := resourceAwsRoute53RecordBuildSet(d, *zoneRecord.HostedZone.Name) 292 if err != nil { 293 return err 294 } 295 296 // Create the new records 297 changeBatch := &route53.ChangeBatch{ 298 Comment: aws.String("Deleted by Terraform"), 299 Changes: []*route53.Change{ 300 &route53.Change{ 301 Action: aws.String("DELETE"), 302 ResourceRecordSet: rec, 303 }, 304 }, 305 } 306 307 req := &route53.ChangeResourceRecordSetsInput{ 308 HostedZoneID: aws.String(cleanZoneID(zone)), 309 ChangeBatch: changeBatch, 310 } 311 312 wait := resource.StateChangeConf{ 313 Pending: []string{"rejected"}, 314 Target: "accepted", 315 Timeout: 5 * time.Minute, 316 MinTimeout: 1 * time.Second, 317 Refresh: func() (interface{}, string, error) { 318 _, err := conn.ChangeResourceRecordSets(req) 319 if err != nil { 320 if r53err, ok := err.(awserr.Error); ok { 321 if r53err.Code() == "PriorRequestNotComplete" { 322 // There is some pending operation, so just retry 323 // in a bit. 324 return 42, "rejected", nil 325 } 326 327 if r53err.Code() == "InvalidChangeBatch" { 328 // This means that the record is already gone. 329 return 42, "accepted", nil 330 } 331 } 332 333 return 42, "failure", err 334 } 335 336 return 42, "accepted", nil 337 }, 338 } 339 340 if _, err := wait.WaitForState(); err != nil { 341 return err 342 } 343 344 return nil 345 } 346 347 func resourceAwsRoute53RecordBuildSet(d *schema.ResourceData, zoneName string) (*route53.ResourceRecordSet, error) { 348 // get expanded name 349 en := expandRecordName(d.Get("name").(string), zoneName) 350 351 // Create the RecordSet request with the fully expanded name, e.g. 352 // sub.domain.com. Route 53 requires a fully qualified domain name, but does 353 // not require the trailing ".", which it will itself, so we don't call FQDN 354 // here. 355 rec := &route53.ResourceRecordSet{ 356 Name: aws.String(en), 357 Type: aws.String(d.Get("type").(string)), 358 } 359 360 if v, ok := d.GetOk("ttl"); ok { 361 rec.TTL = aws.Long(int64(v.(int))) 362 } 363 364 // Resource records 365 if v, ok := d.GetOk("records"); ok { 366 recs := v.(*schema.Set).List() 367 rec.ResourceRecords = expandResourceRecords(recs, d.Get("type").(string)) 368 } 369 370 // Alias record 371 if v, ok := d.GetOk("alias"); ok { 372 aliases := v.(*schema.Set).List() 373 if len(aliases) > 1 { 374 return nil, fmt.Errorf("You can only define a single alias target per record") 375 } 376 alias := aliases[0].(map[string]interface{}) 377 rec.AliasTarget = &route53.AliasTarget{ 378 DNSName: aws.String(alias["name"].(string)), 379 EvaluateTargetHealth: aws.Boolean(alias["evaluate_target_health"].(bool)), 380 HostedZoneID: aws.String(alias["zone_id"].(string)), 381 } 382 log.Printf("[DEBUG] Creating alias: %#v", alias) 383 } else { 384 if _, ok := d.GetOk("ttl"); !ok { 385 return nil, fmt.Errorf(`provider.aws: aws_route53_record: %s: "ttl": required field is not set`, d.Get("name").(string)) 386 } 387 388 if _, ok := d.GetOk("records"); !ok { 389 return nil, fmt.Errorf(`provider.aws: aws_route53_record: %s: "records": required field is not set`, d.Get("name").(string)) 390 } 391 } 392 393 if v, ok := d.GetOk("weight"); ok { 394 rec.Weight = aws.Long(int64(v.(int))) 395 } 396 397 if v, ok := d.GetOk("set_identifier"); ok { 398 rec.SetIdentifier = aws.String(v.(string)) 399 } 400 401 return rec, nil 402 } 403 404 func FQDN(name string) string { 405 n := len(name) 406 if n == 0 || name[n-1] == '.' { 407 return name 408 } else { 409 return name + "." 410 } 411 } 412 413 // Route 53 stores the "*" wildcard indicator as ASCII 42 and returns the 414 // octal equivalent, "\\052". Here we look for that, and convert back to "*" 415 // as needed. 416 func cleanRecordName(name string) string { 417 str := name 418 if strings.HasPrefix(name, "\\052") { 419 str = strings.Replace(name, "\\052", "*", 1) 420 log.Printf("[DEBUG] Replacing octal \\052 for * in: %s", name) 421 } 422 return str 423 } 424 425 // Check if the current record name contains the zone suffix. 426 // If it does not, add the zone name to form a fully qualified name 427 // and keep AWS happy. 428 func expandRecordName(name, zone string) string { 429 rn := strings.TrimSuffix(name, ".") 430 zone = strings.TrimSuffix(zone, ".") 431 if !strings.HasSuffix(rn, zone) { 432 rn = strings.Join([]string{name, zone}, ".") 433 } 434 return rn 435 } 436 437 func resourceAwsRoute53AliasRecordHash(v interface{}) int { 438 var buf bytes.Buffer 439 m := v.(map[string]interface{}) 440 buf.WriteString(fmt.Sprintf("%s-", m["name"].(string))) 441 buf.WriteString(fmt.Sprintf("%s-", m["zone_id"].(string))) 442 buf.WriteString(fmt.Sprintf("%t-", m["evaluate_target_health"].(bool))) 443 444 return hashcode.String(buf.String()) 445 }