github.com/recobe182/terraform@v0.8.5-0.20170117231232-49ab22a935b7/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/errwrap" 12 "github.com/hashicorp/terraform/helper/hashcode" 13 "github.com/hashicorp/terraform/helper/resource" 14 "github.com/hashicorp/terraform/helper/schema" 15 16 "github.com/aws/aws-sdk-go/aws" 17 "github.com/aws/aws-sdk-go/aws/awserr" 18 "github.com/aws/aws-sdk-go/service/route53" 19 ) 20 21 var r53NoRecordsFound = errors.New("No matching Hosted Zone found") 22 var r53NoHostedZoneFound = errors.New("No matching records found") 23 24 func resourceAwsRoute53Record() *schema.Resource { 25 return &schema.Resource{ 26 Create: resourceAwsRoute53RecordCreate, 27 Read: resourceAwsRoute53RecordRead, 28 Update: resourceAwsRoute53RecordUpdate, 29 Delete: resourceAwsRoute53RecordDelete, 30 Importer: &schema.ResourceImporter{ 31 State: schema.ImportStatePassthrough, 32 }, 33 SchemaVersion: 2, 34 MigrateState: resourceAwsRoute53RecordMigrateState, 35 Schema: map[string]*schema.Schema{ 36 "name": &schema.Schema{ 37 Type: schema.TypeString, 38 Required: true, 39 ForceNew: true, 40 StateFunc: func(v interface{}) string { 41 value := strings.TrimSuffix(v.(string), ".") 42 return strings.ToLower(value) 43 }, 44 }, 45 46 "fqdn": &schema.Schema{ 47 Type: schema.TypeString, 48 Computed: true, 49 }, 50 51 "type": &schema.Schema{ 52 Type: schema.TypeString, 53 Required: true, 54 ValidateFunc: validateRoute53RecordType, 55 }, 56 57 "zone_id": &schema.Schema{ 58 Type: schema.TypeString, 59 Required: true, 60 ForceNew: true, 61 ValidateFunc: func(v interface{}, k string) (ws []string, es []error) { 62 value := v.(string) 63 if value == "" { 64 es = append(es, fmt.Errorf("Cannot have empty zone_id")) 65 } 66 return 67 }, 68 }, 69 70 "ttl": &schema.Schema{ 71 Type: schema.TypeInt, 72 Optional: true, 73 ConflictsWith: []string{"alias"}, 74 }, 75 76 "weight": &schema.Schema{ 77 Type: schema.TypeInt, 78 Optional: true, 79 Removed: "Now implemented as weighted_routing_policy; Please see https://www.terraform.io/docs/providers/aws/r/route53_record.html", 80 }, 81 82 "set_identifier": &schema.Schema{ 83 Type: schema.TypeString, 84 Optional: true, 85 }, 86 87 "alias": &schema.Schema{ 88 Type: schema.TypeSet, 89 Optional: true, 90 ConflictsWith: []string{"records", "ttl"}, 91 Elem: &schema.Resource{ 92 Schema: map[string]*schema.Schema{ 93 "zone_id": &schema.Schema{ 94 Type: schema.TypeString, 95 Required: true, 96 }, 97 98 "name": &schema.Schema{ 99 Type: schema.TypeString, 100 Required: true, 101 StateFunc: normalizeAwsAliasName, 102 }, 103 104 "evaluate_target_health": &schema.Schema{ 105 Type: schema.TypeBool, 106 Required: true, 107 }, 108 }, 109 }, 110 Set: resourceAwsRoute53AliasRecordHash, 111 }, 112 113 "failover": &schema.Schema{ // PRIMARY | SECONDARY 114 Type: schema.TypeString, 115 Optional: true, 116 Removed: "Now implemented as failover_routing_policy; see docs", 117 }, 118 119 "failover_routing_policy": &schema.Schema{ 120 Type: schema.TypeList, 121 Optional: true, 122 ConflictsWith: []string{ 123 "geolocation_routing_policy", 124 "latency_routing_policy", 125 "weighted_routing_policy", 126 }, 127 Elem: &schema.Resource{ 128 Schema: map[string]*schema.Schema{ 129 "type": &schema.Schema{ 130 Type: schema.TypeString, 131 Required: true, 132 ValidateFunc: func(v interface{}, k string) (ws []string, es []error) { 133 value := v.(string) 134 if value != "PRIMARY" && value != "SECONDARY" { 135 es = append(es, fmt.Errorf("Failover policy type must be PRIMARY or SECONDARY")) 136 } 137 return 138 }, 139 }, 140 }, 141 }, 142 }, 143 144 "latency_routing_policy": &schema.Schema{ 145 Type: schema.TypeList, 146 Optional: true, 147 ConflictsWith: []string{ 148 "failover_routing_policy", 149 "geolocation_routing_policy", 150 "weighted_routing_policy", 151 }, 152 Elem: &schema.Resource{ 153 Schema: map[string]*schema.Schema{ 154 "region": &schema.Schema{ 155 Type: schema.TypeString, 156 Required: true, 157 }, 158 }, 159 }, 160 }, 161 162 "geolocation_routing_policy": &schema.Schema{ // AWS Geolocation 163 Type: schema.TypeList, 164 Optional: true, 165 ConflictsWith: []string{ 166 "failover_routing_policy", 167 "latency_routing_policy", 168 "weighted_routing_policy", 169 }, 170 Elem: &schema.Resource{ 171 Schema: map[string]*schema.Schema{ 172 "continent": &schema.Schema{ 173 Type: schema.TypeString, 174 Optional: true, 175 }, 176 "country": &schema.Schema{ 177 Type: schema.TypeString, 178 Optional: true, 179 }, 180 "subdivision": &schema.Schema{ 181 Type: schema.TypeString, 182 Optional: true, 183 }, 184 }, 185 }, 186 }, 187 188 "weighted_routing_policy": &schema.Schema{ 189 Type: schema.TypeList, 190 Optional: true, 191 ConflictsWith: []string{ 192 "failover_routing_policy", 193 "geolocation_routing_policy", 194 "latency_routing_policy", 195 }, 196 Elem: &schema.Resource{ 197 Schema: map[string]*schema.Schema{ 198 "weight": &schema.Schema{ 199 Type: schema.TypeInt, 200 Required: true, 201 }, 202 }, 203 }, 204 }, 205 206 "health_check_id": &schema.Schema{ // ID of health check 207 Type: schema.TypeString, 208 Optional: true, 209 }, 210 211 "records": &schema.Schema{ 212 Type: schema.TypeSet, 213 ConflictsWith: []string{"alias"}, 214 Elem: &schema.Schema{Type: schema.TypeString}, 215 Optional: true, 216 Set: schema.HashString, 217 }, 218 }, 219 } 220 } 221 222 func resourceAwsRoute53RecordUpdate(d *schema.ResourceData, meta interface{}) error { 223 // Route 53 supports CREATE, DELETE, and UPSERT actions. We use UPSERT, and 224 // AWS dynamically determines if a record should be created or updated. 225 // Amazon Route 53 can update an existing resource record set only when all 226 // of the following values match: Name, Type and SetIdentifier 227 // See http://docs.aws.amazon.com/Route53/latest/APIReference/API_ChangeResourceRecordSets.html 228 229 if !d.HasChange("type") && !d.HasChange("set_identifier") { 230 // If neither type nor set_identifier changed we use UPSERT, 231 // for resouce update here we simply fall through to 232 // our resource create function. 233 return resourceAwsRoute53RecordCreate(d, meta) 234 } 235 236 // Otherwise we delete the existing record and create a new record within 237 // a transactional change 238 conn := meta.(*AWSClient).r53conn 239 zone := cleanZoneID(d.Get("zone_id").(string)) 240 241 var err error 242 zoneRecord, err := conn.GetHostedZone(&route53.GetHostedZoneInput{Id: aws.String(zone)}) 243 if err != nil { 244 return err 245 } 246 if zoneRecord.HostedZone == nil { 247 return fmt.Errorf("[WARN] No Route53 Zone found for id (%s)", zone) 248 } 249 250 // Build the to be deleted record 251 en := expandRecordName(d.Get("name").(string), *zoneRecord.HostedZone.Name) 252 typeo, _ := d.GetChange("type") 253 254 oldRec := &route53.ResourceRecordSet{ 255 Name: aws.String(en), 256 Type: aws.String(typeo.(string)), 257 } 258 259 if v, _ := d.GetChange("ttl"); v.(int) != 0 { 260 oldRec.TTL = aws.Int64(int64(v.(int))) 261 } 262 263 // Resource records 264 if v, _ := d.GetChange("records"); v != nil { 265 recs := v.(*schema.Set).List() 266 if len(recs) > 0 { 267 oldRec.ResourceRecords = expandResourceRecords(recs, typeo.(string)) 268 } 269 } 270 271 // Alias record 272 if v, _ := d.GetChange("alias"); v != nil { 273 aliases := v.(*schema.Set).List() 274 if len(aliases) == 1 { 275 alias := aliases[0].(map[string]interface{}) 276 oldRec.AliasTarget = &route53.AliasTarget{ 277 DNSName: aws.String(alias["name"].(string)), 278 EvaluateTargetHealth: aws.Bool(alias["evaluate_target_health"].(bool)), 279 HostedZoneId: aws.String(alias["zone_id"].(string)), 280 } 281 } 282 } 283 284 if v, _ := d.GetChange("set_identifier"); v.(string) != "" { 285 oldRec.SetIdentifier = aws.String(v.(string)) 286 } 287 288 // Build the to be created record 289 rec, err := resourceAwsRoute53RecordBuildSet(d, *zoneRecord.HostedZone.Name) 290 if err != nil { 291 return err 292 } 293 294 // Delete the old and create the new records in a single batch. We abuse 295 // StateChangeConf for this to retry for us since Route53 sometimes returns 296 // errors about another operation happening at the same time. 297 changeBatch := &route53.ChangeBatch{ 298 Comment: aws.String("Managed by Terraform"), 299 Changes: []*route53.Change{ 300 &route53.Change{ 301 Action: aws.String("DELETE"), 302 ResourceRecordSet: oldRec, 303 }, 304 &route53.Change{ 305 Action: aws.String("CREATE"), 306 ResourceRecordSet: rec, 307 }, 308 }, 309 } 310 311 req := &route53.ChangeResourceRecordSetsInput{ 312 HostedZoneId: aws.String(cleanZoneID(*zoneRecord.HostedZone.Id)), 313 ChangeBatch: changeBatch, 314 } 315 316 log.Printf("[DEBUG] Updating resource records for zone: %s, name: %s\n\n%s", 317 zone, *rec.Name, req) 318 319 respRaw, err := changeRoute53RecordSet(conn, req) 320 if err != nil { 321 return errwrap.Wrapf("[ERR]: Error building changeset: {{err}}", err) 322 } 323 324 changeInfo := respRaw.(*route53.ChangeResourceRecordSetsOutput).ChangeInfo 325 326 // Generate an ID 327 vars := []string{ 328 zone, 329 strings.ToLower(d.Get("name").(string)), 330 d.Get("type").(string), 331 } 332 if v, ok := d.GetOk("set_identifier"); ok { 333 vars = append(vars, v.(string)) 334 } 335 336 d.SetId(strings.Join(vars, "_")) 337 338 err = waitForRoute53RecordSetToSync(conn, cleanChangeID(*changeInfo.Id)) 339 if err != nil { 340 return err 341 } 342 343 return resourceAwsRoute53RecordRead(d, meta) 344 } 345 346 func resourceAwsRoute53RecordCreate(d *schema.ResourceData, meta interface{}) error { 347 conn := meta.(*AWSClient).r53conn 348 zone := cleanZoneID(d.Get("zone_id").(string)) 349 350 var err error 351 zoneRecord, err := conn.GetHostedZone(&route53.GetHostedZoneInput{Id: aws.String(zone)}) 352 if err != nil { 353 return err 354 } 355 if zoneRecord.HostedZone == nil { 356 return fmt.Errorf("[WARN] No Route53 Zone found for id (%s)", zone) 357 } 358 359 // Build the record 360 rec, err := resourceAwsRoute53RecordBuildSet(d, *zoneRecord.HostedZone.Name) 361 if err != nil { 362 return err 363 } 364 365 // Create the new records. We abuse StateChangeConf for this to 366 // retry for us since Route53 sometimes returns errors about another 367 // operation happening at the same time. 368 changeBatch := &route53.ChangeBatch{ 369 Comment: aws.String("Managed by Terraform"), 370 Changes: []*route53.Change{ 371 &route53.Change{ 372 Action: aws.String("UPSERT"), 373 ResourceRecordSet: rec, 374 }, 375 }, 376 } 377 378 req := &route53.ChangeResourceRecordSetsInput{ 379 HostedZoneId: aws.String(cleanZoneID(*zoneRecord.HostedZone.Id)), 380 ChangeBatch: changeBatch, 381 } 382 383 log.Printf("[DEBUG] Creating resource records for zone: %s, name: %s\n\n%s", 384 zone, *rec.Name, req) 385 386 respRaw, err := changeRoute53RecordSet(conn, req) 387 if err != nil { 388 return errwrap.Wrapf("[ERR]: Error building changeset: {{err}}", err) 389 } 390 391 changeInfo := respRaw.(*route53.ChangeResourceRecordSetsOutput).ChangeInfo 392 393 // Generate an ID 394 vars := []string{ 395 zone, 396 strings.ToLower(d.Get("name").(string)), 397 d.Get("type").(string), 398 } 399 if v, ok := d.GetOk("set_identifier"); ok { 400 vars = append(vars, v.(string)) 401 } 402 403 d.SetId(strings.Join(vars, "_")) 404 405 err = waitForRoute53RecordSetToSync(conn, cleanChangeID(*changeInfo.Id)) 406 if err != nil { 407 return err 408 } 409 410 return resourceAwsRoute53RecordRead(d, meta) 411 } 412 413 func changeRoute53RecordSet(conn *route53.Route53, input *route53.ChangeResourceRecordSetsInput) (interface{}, error) { 414 wait := resource.StateChangeConf{ 415 Pending: []string{"rejected"}, 416 Target: []string{"accepted"}, 417 Timeout: 5 * time.Minute, 418 MinTimeout: 1 * time.Second, 419 Refresh: func() (interface{}, string, error) { 420 resp, err := conn.ChangeResourceRecordSets(input) 421 if err != nil { 422 if r53err, ok := err.(awserr.Error); ok { 423 if r53err.Code() == "PriorRequestNotComplete" { 424 // There is some pending operation, so just retry 425 // in a bit. 426 return nil, "rejected", nil 427 } 428 } 429 430 return nil, "failure", err 431 } 432 433 return resp, "accepted", nil 434 }, 435 } 436 437 return wait.WaitForState() 438 } 439 440 func waitForRoute53RecordSetToSync(conn *route53.Route53, requestId string) error { 441 wait := resource.StateChangeConf{ 442 Delay: 30 * time.Second, 443 Pending: []string{"PENDING"}, 444 Target: []string{"INSYNC"}, 445 Timeout: 30 * time.Minute, 446 MinTimeout: 5 * time.Second, 447 Refresh: func() (result interface{}, state string, err error) { 448 changeRequest := &route53.GetChangeInput{ 449 Id: aws.String(requestId), 450 } 451 return resourceAwsGoRoute53Wait(conn, changeRequest) 452 }, 453 } 454 _, err := wait.WaitForState() 455 return err 456 } 457 458 func resourceAwsRoute53RecordRead(d *schema.ResourceData, meta interface{}) error { 459 // If we don't have a zone ID we're doing an import. Parse it from the ID. 460 if _, ok := d.GetOk("zone_id"); !ok { 461 parts := strings.Split(d.Id(), "_") 462 d.Set("zone_id", parts[0]) 463 d.Set("name", parts[1]) 464 d.Set("type", parts[2]) 465 if len(parts) > 3 { 466 d.Set("set_identifier", parts[3]) 467 } 468 469 d.Set("weight", -1) 470 } 471 472 record, err := findRecord(d, meta) 473 if err != nil { 474 switch err { 475 case r53NoHostedZoneFound, r53NoRecordsFound: 476 log.Printf("[DEBUG] %s for: %s, removing from state file", err, d.Id()) 477 d.SetId("") 478 return nil 479 default: 480 return err 481 } 482 } 483 484 err = d.Set("records", flattenResourceRecords(record.ResourceRecords)) 485 if err != nil { 486 return fmt.Errorf("[DEBUG] Error setting records for: %s, error: %#v", d.Id(), err) 487 } 488 489 if alias := record.AliasTarget; alias != nil { 490 name := normalizeAwsAliasName(*alias.DNSName) 491 d.Set("alias", []interface{}{ 492 map[string]interface{}{ 493 "zone_id": *alias.HostedZoneId, 494 "name": name, 495 "evaluate_target_health": *alias.EvaluateTargetHealth, 496 }, 497 }) 498 } 499 500 d.Set("ttl", record.TTL) 501 502 if record.Failover != nil { 503 v := []map[string]interface{}{{ 504 "type": aws.StringValue(record.Failover), 505 }} 506 if err := d.Set("failover_routing_policy", v); err != nil { 507 return fmt.Errorf("[DEBUG] Error setting failover records for: %s, error: %#v", d.Id(), err) 508 } 509 } 510 511 if record.GeoLocation != nil { 512 v := []map[string]interface{}{{ 513 "continent": aws.StringValue(record.GeoLocation.ContinentCode), 514 "country": aws.StringValue(record.GeoLocation.CountryCode), 515 "subdivision": aws.StringValue(record.GeoLocation.SubdivisionCode), 516 }} 517 if err := d.Set("geolocation_routing_policy", v); err != nil { 518 return fmt.Errorf("[DEBUG] Error setting gelocation records for: %s, error: %#v", d.Id(), err) 519 } 520 } 521 522 if record.Region != nil { 523 v := []map[string]interface{}{{ 524 "region": aws.StringValue(record.Region), 525 }} 526 if err := d.Set("latency_routing_policy", v); err != nil { 527 return fmt.Errorf("[DEBUG] Error setting latency records for: %s, error: %#v", d.Id(), err) 528 } 529 } 530 531 if record.Weight != nil { 532 v := []map[string]interface{}{{ 533 "weight": aws.Int64Value((record.Weight)), 534 }} 535 if err := d.Set("weighted_routing_policy", v); err != nil { 536 return fmt.Errorf("[DEBUG] Error setting weighted records for: %s, error: %#v", d.Id(), err) 537 } 538 } 539 540 d.Set("set_identifier", record.SetIdentifier) 541 d.Set("health_check_id", record.HealthCheckId) 542 543 return nil 544 } 545 546 // findRecord takes a ResourceData struct for aws_resource_route53_record. It 547 // uses the referenced zone_id to query Route53 and find information on it's 548 // records. 549 // 550 // If records are found, it returns the matching 551 // route53.ResourceRecordSet and nil for the error. 552 // 553 // If no hosted zone is found, it returns a nil recordset and r53NoHostedZoneFound 554 // error. 555 // 556 // If no matching recordset is found, it returns nil and a r53NoRecordsFound 557 // error 558 // 559 // If there are other errors, it returns nil a nil recordset and passes on the 560 // error. 561 func findRecord(d *schema.ResourceData, meta interface{}) (*route53.ResourceRecordSet, error) { 562 conn := meta.(*AWSClient).r53conn 563 // Scan for a 564 zone := cleanZoneID(d.Get("zone_id").(string)) 565 566 // get expanded name 567 zoneRecord, err := conn.GetHostedZone(&route53.GetHostedZoneInput{Id: aws.String(zone)}) 568 if err != nil { 569 if r53err, ok := err.(awserr.Error); ok && r53err.Code() == "NoSuchHostedZone" { 570 return nil, r53NoHostedZoneFound 571 } 572 return nil, err 573 } 574 575 en := expandRecordName(d.Get("name").(string), *zoneRecord.HostedZone.Name) 576 log.Printf("[DEBUG] Expanded record name: %s", en) 577 d.Set("fqdn", en) 578 579 lopts := &route53.ListResourceRecordSetsInput{ 580 HostedZoneId: aws.String(cleanZoneID(zone)), 581 StartRecordName: aws.String(en), 582 StartRecordType: aws.String(d.Get("type").(string)), 583 } 584 585 log.Printf("[DEBUG] List resource records sets for zone: %s, opts: %s", 586 zone, lopts) 587 resp, err := conn.ListResourceRecordSets(lopts) 588 if err != nil { 589 return nil, err 590 } 591 592 for _, record := range resp.ResourceRecordSets { 593 name := cleanRecordName(*record.Name) 594 if FQDN(strings.ToLower(name)) != FQDN(strings.ToLower(*lopts.StartRecordName)) { 595 continue 596 } 597 if strings.ToUpper(*record.Type) != strings.ToUpper(*lopts.StartRecordType) { 598 continue 599 } 600 601 if record.SetIdentifier != nil && *record.SetIdentifier != d.Get("set_identifier") { 602 continue 603 } 604 // The only safe return where a record is found 605 return record, nil 606 } 607 return nil, r53NoRecordsFound 608 } 609 610 func resourceAwsRoute53RecordDelete(d *schema.ResourceData, meta interface{}) error { 611 conn := meta.(*AWSClient).r53conn 612 // Get the records 613 rec, err := findRecord(d, meta) 614 if err != nil { 615 switch err { 616 case r53NoHostedZoneFound, r53NoRecordsFound: 617 log.Printf("[DEBUG] %s for: %s, removing from state file", err, d.Id()) 618 d.SetId("") 619 return nil 620 default: 621 return err 622 } 623 } 624 625 // Change batch for deleting 626 changeBatch := &route53.ChangeBatch{ 627 Comment: aws.String("Deleted by Terraform"), 628 Changes: []*route53.Change{ 629 &route53.Change{ 630 Action: aws.String("DELETE"), 631 ResourceRecordSet: rec, 632 }, 633 }, 634 } 635 636 zone := cleanZoneID(d.Get("zone_id").(string)) 637 638 req := &route53.ChangeResourceRecordSetsInput{ 639 HostedZoneId: aws.String(cleanZoneID(zone)), 640 ChangeBatch: changeBatch, 641 } 642 643 respRaw, err := deleteRoute53RecordSet(conn, req) 644 if err != nil { 645 return errwrap.Wrapf("[ERR]: Error building changeset: {{err}}", err) 646 } 647 648 changeInfo := respRaw.(*route53.ChangeResourceRecordSetsOutput).ChangeInfo 649 if changeInfo == nil { 650 log.Printf("[INFO] No ChangeInfo Found. Waiting for Sync not required") 651 return nil 652 } 653 654 err = waitForRoute53RecordSetToSync(conn, cleanChangeID(*changeInfo.Id)) 655 if err != nil { 656 return err 657 } 658 659 return err 660 } 661 662 func deleteRoute53RecordSet(conn *route53.Route53, input *route53.ChangeResourceRecordSetsInput) (interface{}, error) { 663 wait := resource.StateChangeConf{ 664 Pending: []string{"rejected"}, 665 Target: []string{"accepted"}, 666 Timeout: 5 * time.Minute, 667 MinTimeout: 1 * time.Second, 668 Refresh: func() (interface{}, string, error) { 669 resp, err := conn.ChangeResourceRecordSets(input) 670 if err != nil { 671 if r53err, ok := err.(awserr.Error); ok { 672 if r53err.Code() == "PriorRequestNotComplete" { 673 // There is some pending operation, so just retry 674 // in a bit. 675 return 42, "rejected", nil 676 } 677 678 if r53err.Code() == "InvalidChangeBatch" { 679 // This means that the record is already gone. 680 return resp, "accepted", nil 681 } 682 } 683 684 return 42, "failure", err 685 } 686 687 return resp, "accepted", nil 688 }, 689 } 690 691 return wait.WaitForState() 692 } 693 694 func resourceAwsRoute53RecordBuildSet(d *schema.ResourceData, zoneName string) (*route53.ResourceRecordSet, error) { 695 // get expanded name 696 en := expandRecordName(d.Get("name").(string), zoneName) 697 698 // Create the RecordSet request with the fully expanded name, e.g. 699 // sub.domain.com. Route 53 requires a fully qualified domain name, but does 700 // not require the trailing ".", which it will itself, so we don't call FQDN 701 // here. 702 rec := &route53.ResourceRecordSet{ 703 Name: aws.String(en), 704 Type: aws.String(d.Get("type").(string)), 705 } 706 707 if v, ok := d.GetOk("ttl"); ok { 708 rec.TTL = aws.Int64(int64(v.(int))) 709 } 710 711 // Resource records 712 if v, ok := d.GetOk("records"); ok { 713 recs := v.(*schema.Set).List() 714 rec.ResourceRecords = expandResourceRecords(recs, d.Get("type").(string)) 715 } 716 717 // Alias record 718 if v, ok := d.GetOk("alias"); ok { 719 aliases := v.(*schema.Set).List() 720 if len(aliases) > 1 { 721 return nil, fmt.Errorf("You can only define a single alias target per record") 722 } 723 alias := aliases[0].(map[string]interface{}) 724 rec.AliasTarget = &route53.AliasTarget{ 725 DNSName: aws.String(alias["name"].(string)), 726 EvaluateTargetHealth: aws.Bool(alias["evaluate_target_health"].(bool)), 727 HostedZoneId: aws.String(alias["zone_id"].(string)), 728 } 729 log.Printf("[DEBUG] Creating alias: %#v", alias) 730 } else { 731 if _, ok := d.GetOk("ttl"); !ok { 732 return nil, fmt.Errorf(`provider.aws: aws_route53_record: %s: "ttl": required field is not set`, d.Get("name").(string)) 733 } 734 735 if _, ok := d.GetOk("records"); !ok { 736 return nil, fmt.Errorf(`provider.aws: aws_route53_record: %s: "records": required field is not set`, d.Get("name").(string)) 737 } 738 } 739 740 if v, ok := d.GetOk("failover_routing_policy"); ok { 741 if _, ok := d.GetOk("set_identifier"); !ok { 742 return nil, fmt.Errorf(`provider.aws: aws_route53_record: %s: "set_identifier": required field is not set when "failover_routing_policy" is set`, d.Get("name").(string)) 743 } 744 records := v.([]interface{}) 745 if len(records) > 1 { 746 return nil, fmt.Errorf("You can only define a single failover_routing_policy per record") 747 } 748 failover := records[0].(map[string]interface{}) 749 750 rec.Failover = aws.String(failover["type"].(string)) 751 } 752 753 if v, ok := d.GetOk("health_check_id"); ok { 754 rec.HealthCheckId = aws.String(v.(string)) 755 } 756 757 if v, ok := d.GetOk("weighted_routing_policy"); ok { 758 if _, ok := d.GetOk("set_identifier"); !ok { 759 return nil, fmt.Errorf(`provider.aws: aws_route53_record: %s: "set_identifier": required field is not set when "weight_routing_policy" is set`, d.Get("name").(string)) 760 } 761 records := v.([]interface{}) 762 if len(records) > 1 { 763 return nil, fmt.Errorf("You can only define a single weighed_routing_policy per record") 764 } 765 weight := records[0].(map[string]interface{}) 766 767 rec.Weight = aws.Int64(int64(weight["weight"].(int))) 768 } 769 770 if v, ok := d.GetOk("set_identifier"); ok { 771 rec.SetIdentifier = aws.String(v.(string)) 772 } 773 774 if v, ok := d.GetOk("latency_routing_policy"); ok { 775 if _, ok := d.GetOk("set_identifier"); !ok { 776 return nil, fmt.Errorf(`provider.aws: aws_route53_record: %s: "set_identifier": required field is not set when "latency_routing_policy" is set`, d.Get("name").(string)) 777 } 778 records := v.([]interface{}) 779 if len(records) > 1 { 780 return nil, fmt.Errorf("You can only define a single latency_routing_policy per record") 781 } 782 latency := records[0].(map[string]interface{}) 783 784 rec.Region = aws.String(latency["region"].(string)) 785 } 786 787 if v, ok := d.GetOk("geolocation_routing_policy"); ok { 788 if _, ok := d.GetOk("set_identifier"); !ok { 789 return nil, fmt.Errorf(`provider.aws: aws_route53_record: %s: "set_identifier": required field is not set when "geolocation_routing_policy" is set`, d.Get("name").(string)) 790 } 791 geolocations := v.([]interface{}) 792 if len(geolocations) > 1 { 793 return nil, fmt.Errorf("You can only define a single geolocation_routing_policy per record") 794 } 795 geolocation := geolocations[0].(map[string]interface{}) 796 797 rec.GeoLocation = &route53.GeoLocation{ 798 ContinentCode: nilString(geolocation["continent"].(string)), 799 CountryCode: nilString(geolocation["country"].(string)), 800 SubdivisionCode: nilString(geolocation["subdivision"].(string)), 801 } 802 log.Printf("[DEBUG] Creating geolocation: %#v", geolocation) 803 } 804 805 return rec, nil 806 } 807 808 func FQDN(name string) string { 809 n := len(name) 810 if n == 0 || name[n-1] == '.' { 811 return name 812 } else { 813 return name + "." 814 } 815 } 816 817 // Route 53 stores the "*" wildcard indicator as ASCII 42 and returns the 818 // octal equivalent, "\\052". Here we look for that, and convert back to "*" 819 // as needed. 820 func cleanRecordName(name string) string { 821 str := name 822 if strings.HasPrefix(name, "\\052") { 823 str = strings.Replace(name, "\\052", "*", 1) 824 log.Printf("[DEBUG] Replacing octal \\052 for * in: %s", name) 825 } 826 return str 827 } 828 829 // Check if the current record name contains the zone suffix. 830 // If it does not, add the zone name to form a fully qualified name 831 // and keep AWS happy. 832 func expandRecordName(name, zone string) string { 833 rn := strings.ToLower(strings.TrimSuffix(name, ".")) 834 zone = strings.TrimSuffix(zone, ".") 835 if !strings.HasSuffix(rn, zone) { 836 if len(name) == 0 { 837 rn = zone 838 } else { 839 rn = strings.Join([]string{name, zone}, ".") 840 } 841 } 842 return rn 843 } 844 845 func resourceAwsRoute53AliasRecordHash(v interface{}) int { 846 var buf bytes.Buffer 847 m := v.(map[string]interface{}) 848 buf.WriteString(fmt.Sprintf("%s-", normalizeAwsAliasName(m["name"].(string)))) 849 buf.WriteString(fmt.Sprintf("%s-", m["zone_id"].(string))) 850 buf.WriteString(fmt.Sprintf("%t-", m["evaluate_target_health"].(bool))) 851 852 return hashcode.String(buf.String()) 853 } 854 855 // nilString takes a string as an argument and returns a string 856 // pointer. The returned pointer is nil if the string argument is 857 // empty, otherwise it is a pointer to a copy of the string. 858 func nilString(s string) *string { 859 if s == "" { 860 return nil 861 } 862 return aws.String(s) 863 } 864 865 func normalizeAwsAliasName(alias interface{}) string { 866 input := alias.(string) 867 if strings.HasPrefix(input, "dualstack.") { 868 return strings.Replace(input, "dualstack.", "", -1) 869 } 870 871 return strings.TrimRight(input, ".") 872 }