github.com/anfernee/terraform@v0.6.16-0.20160430000239-06e5085a92f2/builtin/providers/aws/resource_aws_route53_record.go (about) 1 package aws 2 3 import ( 4 "bytes" 5 "errors" 6 "fmt" 7 "log" 8 "strings" 9 "time" 10 11 "github.com/hashicorp/terraform/helper/hashcode" 12 "github.com/hashicorp/terraform/helper/resource" 13 "github.com/hashicorp/terraform/helper/schema" 14 15 "github.com/aws/aws-sdk-go/aws" 16 "github.com/aws/aws-sdk-go/aws/awserr" 17 "github.com/aws/aws-sdk-go/service/route53" 18 ) 19 20 var r53NoRecordsFound = errors.New("No matching Hosted Zone found") 21 var r53NoHostedZoneFound = errors.New("No matching records found") 22 23 func resourceAwsRoute53Record() *schema.Resource { 24 return &schema.Resource{ 25 Create: resourceAwsRoute53RecordCreate, 26 Read: resourceAwsRoute53RecordRead, 27 Update: resourceAwsRoute53RecordUpdate, 28 Delete: resourceAwsRoute53RecordDelete, 29 30 Schema: map[string]*schema.Schema{ 31 "name": &schema.Schema{ 32 Type: schema.TypeString, 33 Required: true, 34 ForceNew: true, 35 StateFunc: func(v interface{}) string { 36 value := v.(string) 37 return strings.ToLower(value) 38 }, 39 }, 40 41 "fqdn": &schema.Schema{ 42 Type: schema.TypeString, 43 Computed: true, 44 }, 45 46 "type": &schema.Schema{ 47 Type: schema.TypeString, 48 Required: true, 49 ForceNew: true, 50 }, 51 52 "zone_id": &schema.Schema{ 53 Type: schema.TypeString, 54 Required: true, 55 ForceNew: true, 56 ValidateFunc: func(v interface{}, k string) (ws []string, es []error) { 57 value := v.(string) 58 if value == "" { 59 es = append(es, fmt.Errorf("Cannot have empty zone_id")) 60 } 61 return 62 }, 63 }, 64 65 "ttl": &schema.Schema{ 66 Type: schema.TypeInt, 67 Optional: true, 68 ConflictsWith: []string{"alias"}, 69 }, 70 71 // Weight uses a special sentinel value to indicate its presence. 72 // Because 0 is a valid value for Weight, we default to -1 so that any 73 // inclusion of a weight (zero or not) will be a usable value 74 "weight": &schema.Schema{ 75 Type: schema.TypeInt, 76 Optional: true, 77 Default: -1, 78 }, 79 80 "set_identifier": &schema.Schema{ 81 Type: schema.TypeString, 82 Optional: true, 83 ForceNew: true, 84 }, 85 86 "alias": &schema.Schema{ 87 Type: schema.TypeSet, 88 Optional: true, 89 ConflictsWith: []string{"records", "ttl"}, 90 Elem: &schema.Resource{ 91 Schema: map[string]*schema.Schema{ 92 "zone_id": &schema.Schema{ 93 Type: schema.TypeString, 94 Required: true, 95 }, 96 97 "name": &schema.Schema{ 98 Type: schema.TypeString, 99 Required: true, 100 }, 101 102 "evaluate_target_health": &schema.Schema{ 103 Type: schema.TypeBool, 104 Required: true, 105 }, 106 }, 107 }, 108 Set: resourceAwsRoute53AliasRecordHash, 109 }, 110 111 "failover": &schema.Schema{ // PRIMARY | SECONDARY 112 Type: schema.TypeString, 113 Optional: true, 114 }, 115 116 "health_check_id": &schema.Schema{ // ID of health check 117 Type: schema.TypeString, 118 Optional: true, 119 }, 120 121 "records": &schema.Schema{ 122 Type: schema.TypeSet, 123 ConflictsWith: []string{"alias"}, 124 Elem: &schema.Schema{Type: schema.TypeString}, 125 Optional: true, 126 Set: schema.HashString, 127 }, 128 }, 129 } 130 } 131 132 func resourceAwsRoute53RecordUpdate(d *schema.ResourceData, meta interface{}) error { 133 // Route 53 supports CREATE, DELETE, and UPSERT actions. We use UPSERT, and 134 // AWS dynamically determines if a record should be created or updated. 135 // Amazon Route 53 can update an existing resource record set only when all 136 // of the following values match: Name, Type 137 // (and SetIdentifier, which we don't use yet). 138 // See http://docs.aws.amazon.com/Route53/latest/APIReference/API_ChangeResourceRecordSets_Requests.html#change-rrsets-request-action 139 // 140 // Because we use UPSERT, for resouce update here we simply fall through to 141 // our resource create function. 142 return resourceAwsRoute53RecordCreate(d, meta) 143 } 144 145 func resourceAwsRoute53RecordCreate(d *schema.ResourceData, meta interface{}) error { 146 conn := meta.(*AWSClient).r53conn 147 zone := cleanZoneID(d.Get("zone_id").(string)) 148 149 var err error 150 zoneRecord, err := conn.GetHostedZone(&route53.GetHostedZoneInput{Id: aws.String(zone)}) 151 if err != nil { 152 return err 153 } 154 if zoneRecord.HostedZone == nil { 155 return fmt.Errorf("[WARN] No Route53 Zone found for id (%s)", zone) 156 } 157 158 // Build the record 159 rec, err := resourceAwsRoute53RecordBuildSet(d, *zoneRecord.HostedZone.Name) 160 if err != nil { 161 return err 162 } 163 164 // Create the new records. We abuse StateChangeConf for this to 165 // retry for us since Route53 sometimes returns errors about another 166 // operation happening at the same time. 167 changeBatch := &route53.ChangeBatch{ 168 Comment: aws.String("Managed by Terraform"), 169 Changes: []*route53.Change{ 170 &route53.Change{ 171 Action: aws.String("UPSERT"), 172 ResourceRecordSet: rec, 173 }, 174 }, 175 } 176 177 req := &route53.ChangeResourceRecordSetsInput{ 178 HostedZoneId: aws.String(cleanZoneID(*zoneRecord.HostedZone.Id)), 179 ChangeBatch: changeBatch, 180 } 181 182 log.Printf("[DEBUG] Creating resource records for zone: %s, name: %s\n\n%s", 183 zone, *rec.Name, req) 184 185 wait := resource.StateChangeConf{ 186 Pending: []string{"rejected"}, 187 Target: []string{"accepted"}, 188 Timeout: 5 * time.Minute, 189 MinTimeout: 1 * time.Second, 190 Refresh: func() (interface{}, string, error) { 191 resp, err := conn.ChangeResourceRecordSets(req) 192 if err != nil { 193 if r53err, ok := err.(awserr.Error); ok { 194 if r53err.Code() == "PriorRequestNotComplete" { 195 // There is some pending operation, so just retry 196 // in a bit. 197 return nil, "rejected", nil 198 } 199 } 200 201 return nil, "failure", err 202 } 203 204 return resp, "accepted", nil 205 }, 206 } 207 208 respRaw, err := wait.WaitForState() 209 if err != nil { 210 return err 211 } 212 changeInfo := respRaw.(*route53.ChangeResourceRecordSetsOutput).ChangeInfo 213 214 // Generate an ID 215 vars := []string{ 216 zone, 217 strings.ToLower(d.Get("name").(string)), 218 d.Get("type").(string), 219 } 220 if v, ok := d.GetOk("set_identifier"); ok { 221 vars = append(vars, v.(string)) 222 } 223 224 d.SetId(strings.Join(vars, "_")) 225 226 // Wait until we are done 227 wait = resource.StateChangeConf{ 228 Delay: 30 * time.Second, 229 Pending: []string{"PENDING"}, 230 Target: []string{"INSYNC"}, 231 Timeout: 30 * time.Minute, 232 MinTimeout: 5 * time.Second, 233 Refresh: func() (result interface{}, state string, err error) { 234 changeRequest := &route53.GetChangeInput{ 235 Id: aws.String(cleanChangeID(*changeInfo.Id)), 236 } 237 return resourceAwsGoRoute53Wait(conn, changeRequest) 238 }, 239 } 240 _, err = wait.WaitForState() 241 if err != nil { 242 return err 243 } 244 245 return resourceAwsRoute53RecordRead(d, meta) 246 } 247 248 func resourceAwsRoute53RecordRead(d *schema.ResourceData, meta interface{}) error { 249 // If we don't have a zone ID we're doing an import. Parse it from the ID. 250 if _, ok := d.GetOk("zone_id"); !ok { 251 parts := strings.Split(d.Id(), "_") 252 d.Set("zone_id", parts[0]) 253 d.Set("name", parts[1]) 254 d.Set("type", parts[2]) 255 if len(parts) > 3 { 256 d.Set("set_identifier", parts[3]) 257 } 258 259 d.Set("weight", -1) 260 } 261 262 record, err := findRecord(d, meta) 263 if err != nil { 264 switch err { 265 case r53NoHostedZoneFound, r53NoRecordsFound: 266 log.Printf("[DEBUG] %s for: %s, removing from state file", err, d.Id()) 267 d.SetId("") 268 return nil 269 default: 270 return err 271 } 272 } 273 274 err = d.Set("records", flattenResourceRecords(record.ResourceRecords)) 275 if err != nil { 276 return fmt.Errorf("[DEBUG] Error setting records for: %s, error: %#v", d.Id(), err) 277 } 278 279 if alias := record.AliasTarget; alias != nil { 280 if _, ok := d.GetOk("alias"); !ok { 281 d.Set("alias", []interface{}{ 282 map[string]interface{}{ 283 "zone_id": *alias.HostedZoneId, 284 "name": *alias.DNSName, 285 "evaluate_target_health": *alias.EvaluateTargetHealth, 286 }, 287 }) 288 } 289 } 290 291 d.Set("ttl", record.TTL) 292 // Only set the weight if it's non-nil, otherwise we end up with a 0 weight 293 // which has actual contextual meaning with Route 53 records 294 // See http://docs.aws.amazon.com/fr_fr/Route53/latest/APIReference/API_ChangeResourceRecordSets_Examples.html 295 if record.Weight != nil { 296 d.Set("weight", record.Weight) 297 } 298 d.Set("set_identifier", record.SetIdentifier) 299 d.Set("failover", record.Failover) 300 d.Set("health_check_id", record.HealthCheckId) 301 302 return nil 303 } 304 305 // findRecord takes a ResourceData struct for aws_resource_route53_record. It 306 // uses the referenced zone_id to query Route53 and find information on it's 307 // records. 308 // 309 // If records are found, it returns the matching 310 // route53.ResourceRecordSet and nil for the error. 311 // 312 // If no hosted zone is found, it returns a nil recordset and r53NoHostedZoneFound 313 // error. 314 // 315 // If no matching recordset is found, it returns nil and a r53NoRecordsFound 316 // error 317 // 318 // If there are other errors, it returns nil a nil recordset and passes on the 319 // error. 320 func findRecord(d *schema.ResourceData, meta interface{}) (*route53.ResourceRecordSet, error) { 321 conn := meta.(*AWSClient).r53conn 322 // Scan for a 323 zone := cleanZoneID(d.Get("zone_id").(string)) 324 325 // get expanded name 326 zoneRecord, err := conn.GetHostedZone(&route53.GetHostedZoneInput{Id: aws.String(zone)}) 327 if err != nil { 328 if r53err, ok := err.(awserr.Error); ok && r53err.Code() == "NoSuchHostedZone" { 329 return nil, r53NoHostedZoneFound 330 } 331 return nil, err 332 } 333 en := expandRecordName(d.Get("name").(string), *zoneRecord.HostedZone.Name) 334 log.Printf("[DEBUG] Expanded record name: %s", en) 335 d.Set("fqdn", en) 336 337 lopts := &route53.ListResourceRecordSetsInput{ 338 HostedZoneId: aws.String(cleanZoneID(zone)), 339 StartRecordName: aws.String(en), 340 StartRecordType: aws.String(d.Get("type").(string)), 341 } 342 343 log.Printf("[DEBUG] List resource records sets for zone: %s, opts: %s", 344 zone, lopts) 345 resp, err := conn.ListResourceRecordSets(lopts) 346 if err != nil { 347 return nil, err 348 } 349 350 for _, record := range resp.ResourceRecordSets { 351 name := cleanRecordName(*record.Name) 352 if FQDN(strings.ToLower(name)) != FQDN(strings.ToLower(*lopts.StartRecordName)) { 353 continue 354 } 355 if strings.ToUpper(*record.Type) != strings.ToUpper(*lopts.StartRecordType) { 356 continue 357 } 358 359 if record.SetIdentifier != nil && *record.SetIdentifier != d.Get("set_identifier") { 360 continue 361 } 362 // The only safe return where a record is found 363 return record, nil 364 } 365 return nil, r53NoRecordsFound 366 } 367 368 func resourceAwsRoute53RecordDelete(d *schema.ResourceData, meta interface{}) error { 369 conn := meta.(*AWSClient).r53conn 370 // Get the records 371 rec, err := findRecord(d, meta) 372 if err != nil { 373 switch err { 374 case r53NoHostedZoneFound, r53NoRecordsFound: 375 log.Printf("[DEBUG] %s for: %s, removing from state file", err, d.Id()) 376 d.SetId("") 377 return nil 378 default: 379 return err 380 } 381 } 382 383 // Change batch for deleting 384 changeBatch := &route53.ChangeBatch{ 385 Comment: aws.String("Deleted by Terraform"), 386 Changes: []*route53.Change{ 387 &route53.Change{ 388 Action: aws.String("DELETE"), 389 ResourceRecordSet: rec, 390 }, 391 }, 392 } 393 394 zone := cleanZoneID(d.Get("zone_id").(string)) 395 396 req := &route53.ChangeResourceRecordSetsInput{ 397 HostedZoneId: aws.String(cleanZoneID(zone)), 398 ChangeBatch: changeBatch, 399 } 400 401 wait := resource.StateChangeConf{ 402 Pending: []string{"rejected"}, 403 Target: []string{"accepted"}, 404 Timeout: 5 * time.Minute, 405 MinTimeout: 1 * time.Second, 406 Refresh: func() (interface{}, string, error) { 407 _, err := conn.ChangeResourceRecordSets(req) 408 if err != nil { 409 if r53err, ok := err.(awserr.Error); ok { 410 if r53err.Code() == "PriorRequestNotComplete" { 411 // There is some pending operation, so just retry 412 // in a bit. 413 return 42, "rejected", nil 414 } 415 416 if r53err.Code() == "InvalidChangeBatch" { 417 // This means that the record is already gone. 418 return 42, "accepted", nil 419 } 420 } 421 422 return 42, "failure", err 423 } 424 425 return 42, "accepted", nil 426 }, 427 } 428 429 if _, err := wait.WaitForState(); err != nil { 430 return err 431 } 432 433 return nil 434 } 435 436 func resourceAwsRoute53RecordBuildSet(d *schema.ResourceData, zoneName string) (*route53.ResourceRecordSet, error) { 437 // get expanded name 438 en := expandRecordName(d.Get("name").(string), zoneName) 439 440 // Create the RecordSet request with the fully expanded name, e.g. 441 // sub.domain.com. Route 53 requires a fully qualified domain name, but does 442 // not require the trailing ".", which it will itself, so we don't call FQDN 443 // here. 444 rec := &route53.ResourceRecordSet{ 445 Name: aws.String(en), 446 Type: aws.String(d.Get("type").(string)), 447 } 448 449 if v, ok := d.GetOk("ttl"); ok { 450 rec.TTL = aws.Int64(int64(v.(int))) 451 } 452 453 // Resource records 454 if v, ok := d.GetOk("records"); ok { 455 recs := v.(*schema.Set).List() 456 rec.ResourceRecords = expandResourceRecords(recs, d.Get("type").(string)) 457 } 458 459 // Alias record 460 if v, ok := d.GetOk("alias"); ok { 461 aliases := v.(*schema.Set).List() 462 if len(aliases) > 1 { 463 return nil, fmt.Errorf("You can only define a single alias target per record") 464 } 465 alias := aliases[0].(map[string]interface{}) 466 rec.AliasTarget = &route53.AliasTarget{ 467 DNSName: aws.String(alias["name"].(string)), 468 EvaluateTargetHealth: aws.Bool(alias["evaluate_target_health"].(bool)), 469 HostedZoneId: aws.String(alias["zone_id"].(string)), 470 } 471 log.Printf("[DEBUG] Creating alias: %#v", alias) 472 } else { 473 if _, ok := d.GetOk("ttl"); !ok { 474 return nil, fmt.Errorf(`provider.aws: aws_route53_record: %s: "ttl": required field is not set`, d.Get("name").(string)) 475 } 476 477 if _, ok := d.GetOk("records"); !ok { 478 return nil, fmt.Errorf(`provider.aws: aws_route53_record: %s: "records": required field is not set`, d.Get("name").(string)) 479 } 480 } 481 482 if v, ok := d.GetOk("failover"); ok { 483 if _, ok := d.GetOk("set_identifier"); !ok { 484 return nil, fmt.Errorf(`provider.aws: aws_route53_record: %s: "set_identifier": required field is not set when "failover" is set`, d.Get("name").(string)) 485 } 486 rec.Failover = aws.String(v.(string)) 487 } 488 489 if v, ok := d.GetOk("health_check_id"); ok { 490 rec.HealthCheckId = aws.String(v.(string)) 491 } 492 493 if v, ok := d.GetOk("set_identifier"); ok { 494 rec.SetIdentifier = aws.String(v.(string)) 495 } 496 497 w := d.Get("weight").(int) 498 if w > -1 { 499 if _, ok := d.GetOk("set_identifier"); !ok { 500 return nil, fmt.Errorf(`provider.aws: aws_route53_record: %s: "set_identifier": required field is not set when "weight" is set`, d.Get("name").(string)) 501 } 502 rec.Weight = aws.Int64(int64(w)) 503 } 504 505 return rec, nil 506 } 507 508 func FQDN(name string) string { 509 n := len(name) 510 if n == 0 || name[n-1] == '.' { 511 return name 512 } else { 513 return name + "." 514 } 515 } 516 517 // Route 53 stores the "*" wildcard indicator as ASCII 42 and returns the 518 // octal equivalent, "\\052". Here we look for that, and convert back to "*" 519 // as needed. 520 func cleanRecordName(name string) string { 521 str := name 522 if strings.HasPrefix(name, "\\052") { 523 str = strings.Replace(name, "\\052", "*", 1) 524 log.Printf("[DEBUG] Replacing octal \\052 for * in: %s", name) 525 } 526 return str 527 } 528 529 // Check if the current record name contains the zone suffix. 530 // If it does not, add the zone name to form a fully qualified name 531 // and keep AWS happy. 532 func expandRecordName(name, zone string) string { 533 rn := strings.ToLower(strings.TrimSuffix(name, ".")) 534 zone = strings.TrimSuffix(zone, ".") 535 if !strings.HasSuffix(rn, zone) { 536 rn = strings.Join([]string{name, zone}, ".") 537 } 538 return rn 539 } 540 541 func resourceAwsRoute53AliasRecordHash(v interface{}) int { 542 var buf bytes.Buffer 543 m := v.(map[string]interface{}) 544 buf.WriteString(fmt.Sprintf("%s-", m["name"].(string))) 545 buf.WriteString(fmt.Sprintf("%s-", m["zone_id"].(string))) 546 buf.WriteString(fmt.Sprintf("%t-", m["evaluate_target_health"].(bool))) 547 548 return hashcode.String(buf.String()) 549 }