github.com/nathanielks/terraform@v0.6.1-0.20170509030759-13e1a62319dc/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": { 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": { 47 Type: schema.TypeString, 48 Computed: true, 49 }, 50 51 "type": { 52 Type: schema.TypeString, 53 Required: true, 54 ValidateFunc: validateRoute53RecordType, 55 }, 56 57 "zone_id": { 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": { 71 Type: schema.TypeInt, 72 Optional: true, 73 ConflictsWith: []string{"alias"}, 74 }, 75 76 "weight": { 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": { 83 Type: schema.TypeString, 84 Optional: true, 85 }, 86 87 "alias": { 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": { 94 Type: schema.TypeString, 95 Required: true, 96 }, 97 98 "name": { 99 Type: schema.TypeString, 100 Required: true, 101 StateFunc: normalizeAwsAliasName, 102 }, 103 104 "evaluate_target_health": { 105 Type: schema.TypeBool, 106 Required: true, 107 }, 108 }, 109 }, 110 Set: resourceAwsRoute53AliasRecordHash, 111 }, 112 113 "failover": { // 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": { 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": { 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": { 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": { 155 Type: schema.TypeString, 156 Required: true, 157 }, 158 }, 159 }, 160 }, 161 162 "geolocation_routing_policy": { // 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": { 173 Type: schema.TypeString, 174 Optional: true, 175 }, 176 "country": { 177 Type: schema.TypeString, 178 Optional: true, 179 }, 180 "subdivision": { 181 Type: schema.TypeString, 182 Optional: true, 183 }, 184 }, 185 }, 186 }, 187 188 "weighted_routing_policy": { 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": { 199 Type: schema.TypeInt, 200 Required: true, 201 }, 202 }, 203 }, 204 }, 205 206 "health_check_id": { // ID of health check 207 Type: schema.TypeString, 208 Optional: true, 209 }, 210 211 "records": { 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 { 301 Action: aws.String("DELETE"), 302 ResourceRecordSet: oldRec, 303 }, 304 { 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 { 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 463 //we check that we have parsed the id into the correct number of segments 464 //we need at least 3 segments! 465 if len(parts) == 1 || len(parts) < 3 { 466 return fmt.Errorf("Error Importing aws_route_53 record. Please make sure the record ID is in the form ZONEID_RECORDNAME_TYPE (i.e. Z4KAPRWWNC7JR_dev_A") 467 } 468 469 d.Set("zone_id", parts[0]) 470 d.Set("name", parts[1]) 471 d.Set("type", parts[2]) 472 if len(parts) > 3 { 473 d.Set("set_identifier", parts[3]) 474 } 475 } 476 477 record, err := findRecord(d, meta) 478 if err != nil { 479 switch err { 480 case r53NoHostedZoneFound, r53NoRecordsFound: 481 log.Printf("[DEBUG] %s for: %s, removing from state file", err, d.Id()) 482 d.SetId("") 483 return nil 484 default: 485 return err 486 } 487 } 488 489 err = d.Set("records", flattenResourceRecords(record.ResourceRecords, *record.Type)) 490 if err != nil { 491 return fmt.Errorf("[DEBUG] Error setting records for: %s, error: %#v", d.Id(), err) 492 } 493 494 if alias := record.AliasTarget; alias != nil { 495 name := normalizeAwsAliasName(*alias.DNSName) 496 d.Set("alias", []interface{}{ 497 map[string]interface{}{ 498 "zone_id": *alias.HostedZoneId, 499 "name": name, 500 "evaluate_target_health": *alias.EvaluateTargetHealth, 501 }, 502 }) 503 } 504 505 d.Set("ttl", record.TTL) 506 507 if record.Failover != nil { 508 v := []map[string]interface{}{{ 509 "type": aws.StringValue(record.Failover), 510 }} 511 if err := d.Set("failover_routing_policy", v); err != nil { 512 return fmt.Errorf("[DEBUG] Error setting failover records for: %s, error: %#v", d.Id(), err) 513 } 514 } 515 516 if record.GeoLocation != nil { 517 v := []map[string]interface{}{{ 518 "continent": aws.StringValue(record.GeoLocation.ContinentCode), 519 "country": aws.StringValue(record.GeoLocation.CountryCode), 520 "subdivision": aws.StringValue(record.GeoLocation.SubdivisionCode), 521 }} 522 if err := d.Set("geolocation_routing_policy", v); err != nil { 523 return fmt.Errorf("[DEBUG] Error setting gelocation records for: %s, error: %#v", d.Id(), err) 524 } 525 } 526 527 if record.Region != nil { 528 v := []map[string]interface{}{{ 529 "region": aws.StringValue(record.Region), 530 }} 531 if err := d.Set("latency_routing_policy", v); err != nil { 532 return fmt.Errorf("[DEBUG] Error setting latency records for: %s, error: %#v", d.Id(), err) 533 } 534 } 535 536 if record.Weight != nil { 537 v := []map[string]interface{}{{ 538 "weight": aws.Int64Value((record.Weight)), 539 }} 540 if err := d.Set("weighted_routing_policy", v); err != nil { 541 return fmt.Errorf("[DEBUG] Error setting weighted records for: %s, error: %#v", d.Id(), err) 542 } 543 } 544 545 d.Set("set_identifier", record.SetIdentifier) 546 d.Set("health_check_id", record.HealthCheckId) 547 548 return nil 549 } 550 551 // findRecord takes a ResourceData struct for aws_resource_route53_record. It 552 // uses the referenced zone_id to query Route53 and find information on it's 553 // records. 554 // 555 // If records are found, it returns the matching 556 // route53.ResourceRecordSet and nil for the error. 557 // 558 // If no hosted zone is found, it returns a nil recordset and r53NoHostedZoneFound 559 // error. 560 // 561 // If no matching recordset is found, it returns nil and a r53NoRecordsFound 562 // error 563 // 564 // If there are other errors, it returns nil a nil recordset and passes on the 565 // error. 566 func findRecord(d *schema.ResourceData, meta interface{}) (*route53.ResourceRecordSet, error) { 567 conn := meta.(*AWSClient).r53conn 568 // Scan for a 569 zone := cleanZoneID(d.Get("zone_id").(string)) 570 571 // get expanded name 572 zoneRecord, err := conn.GetHostedZone(&route53.GetHostedZoneInput{Id: aws.String(zone)}) 573 if err != nil { 574 if r53err, ok := err.(awserr.Error); ok && r53err.Code() == "NoSuchHostedZone" { 575 return nil, r53NoHostedZoneFound 576 } 577 return nil, err 578 } 579 580 en := expandRecordName(d.Get("name").(string), *zoneRecord.HostedZone.Name) 581 log.Printf("[DEBUG] Expanded record name: %s", en) 582 d.Set("fqdn", en) 583 584 lopts := &route53.ListResourceRecordSetsInput{ 585 HostedZoneId: aws.String(cleanZoneID(zone)), 586 StartRecordName: aws.String(en), 587 StartRecordType: aws.String(d.Get("type").(string)), 588 } 589 590 log.Printf("[DEBUG] List resource records sets for zone: %s, opts: %s", 591 zone, lopts) 592 resp, err := conn.ListResourceRecordSets(lopts) 593 if err != nil { 594 return nil, err 595 } 596 597 for _, record := range resp.ResourceRecordSets { 598 name := cleanRecordName(*record.Name) 599 if FQDN(strings.ToLower(name)) != FQDN(strings.ToLower(*lopts.StartRecordName)) { 600 continue 601 } 602 if strings.ToUpper(*record.Type) != strings.ToUpper(*lopts.StartRecordType) { 603 continue 604 } 605 606 if record.SetIdentifier != nil && *record.SetIdentifier != d.Get("set_identifier") { 607 continue 608 } 609 // The only safe return where a record is found 610 return record, nil 611 } 612 return nil, r53NoRecordsFound 613 } 614 615 func resourceAwsRoute53RecordDelete(d *schema.ResourceData, meta interface{}) error { 616 conn := meta.(*AWSClient).r53conn 617 // Get the records 618 rec, err := findRecord(d, meta) 619 if err != nil { 620 switch err { 621 case r53NoHostedZoneFound, r53NoRecordsFound: 622 log.Printf("[DEBUG] %s for: %s, removing from state file", err, d.Id()) 623 d.SetId("") 624 return nil 625 default: 626 return err 627 } 628 } 629 630 // Change batch for deleting 631 changeBatch := &route53.ChangeBatch{ 632 Comment: aws.String("Deleted by Terraform"), 633 Changes: []*route53.Change{ 634 { 635 Action: aws.String("DELETE"), 636 ResourceRecordSet: rec, 637 }, 638 }, 639 } 640 641 zone := cleanZoneID(d.Get("zone_id").(string)) 642 643 req := &route53.ChangeResourceRecordSetsInput{ 644 HostedZoneId: aws.String(cleanZoneID(zone)), 645 ChangeBatch: changeBatch, 646 } 647 648 respRaw, err := deleteRoute53RecordSet(conn, req) 649 if err != nil { 650 return errwrap.Wrapf("[ERR]: Error building changeset: {{err}}", err) 651 } 652 653 changeInfo := respRaw.(*route53.ChangeResourceRecordSetsOutput).ChangeInfo 654 if changeInfo == nil { 655 log.Printf("[INFO] No ChangeInfo Found. Waiting for Sync not required") 656 return nil 657 } 658 659 err = waitForRoute53RecordSetToSync(conn, cleanChangeID(*changeInfo.Id)) 660 if err != nil { 661 return err 662 } 663 664 return err 665 } 666 667 func deleteRoute53RecordSet(conn *route53.Route53, input *route53.ChangeResourceRecordSetsInput) (interface{}, error) { 668 wait := resource.StateChangeConf{ 669 Pending: []string{"rejected"}, 670 Target: []string{"accepted"}, 671 Timeout: 5 * time.Minute, 672 MinTimeout: 1 * time.Second, 673 Refresh: func() (interface{}, string, error) { 674 resp, err := conn.ChangeResourceRecordSets(input) 675 if err != nil { 676 if r53err, ok := err.(awserr.Error); ok { 677 if r53err.Code() == "PriorRequestNotComplete" { 678 // There is some pending operation, so just retry 679 // in a bit. 680 return 42, "rejected", nil 681 } 682 683 if r53err.Code() == "InvalidChangeBatch" { 684 // This means that the record is already gone. 685 return resp, "accepted", nil 686 } 687 } 688 689 return 42, "failure", err 690 } 691 692 return resp, "accepted", nil 693 }, 694 } 695 696 return wait.WaitForState() 697 } 698 699 func resourceAwsRoute53RecordBuildSet(d *schema.ResourceData, zoneName string) (*route53.ResourceRecordSet, error) { 700 // get expanded name 701 en := expandRecordName(d.Get("name").(string), zoneName) 702 703 // Create the RecordSet request with the fully expanded name, e.g. 704 // sub.domain.com. Route 53 requires a fully qualified domain name, but does 705 // not require the trailing ".", which it will itself, so we don't call FQDN 706 // here. 707 rec := &route53.ResourceRecordSet{ 708 Name: aws.String(en), 709 Type: aws.String(d.Get("type").(string)), 710 } 711 712 if v, ok := d.GetOk("ttl"); ok { 713 rec.TTL = aws.Int64(int64(v.(int))) 714 } 715 716 // Resource records 717 if v, ok := d.GetOk("records"); ok { 718 recs := v.(*schema.Set).List() 719 rec.ResourceRecords = expandResourceRecords(recs, d.Get("type").(string)) 720 } 721 722 // Alias record 723 if v, ok := d.GetOk("alias"); ok { 724 aliases := v.(*schema.Set).List() 725 if len(aliases) > 1 { 726 return nil, fmt.Errorf("You can only define a single alias target per record") 727 } 728 alias := aliases[0].(map[string]interface{}) 729 rec.AliasTarget = &route53.AliasTarget{ 730 DNSName: aws.String(alias["name"].(string)), 731 EvaluateTargetHealth: aws.Bool(alias["evaluate_target_health"].(bool)), 732 HostedZoneId: aws.String(alias["zone_id"].(string)), 733 } 734 log.Printf("[DEBUG] Creating alias: %#v", alias) 735 } else { 736 if _, ok := d.GetOk("ttl"); !ok { 737 return nil, fmt.Errorf(`provider.aws: aws_route53_record: %s: "ttl": required field is not set`, d.Get("name").(string)) 738 } 739 740 if _, ok := d.GetOk("records"); !ok { 741 return nil, fmt.Errorf(`provider.aws: aws_route53_record: %s: "records": required field is not set`, d.Get("name").(string)) 742 } 743 } 744 745 if v, ok := d.GetOk("failover_routing_policy"); ok { 746 if _, ok := d.GetOk("set_identifier"); !ok { 747 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)) 748 } 749 records := v.([]interface{}) 750 if len(records) > 1 { 751 return nil, fmt.Errorf("You can only define a single failover_routing_policy per record") 752 } 753 failover := records[0].(map[string]interface{}) 754 755 rec.Failover = aws.String(failover["type"].(string)) 756 } 757 758 if v, ok := d.GetOk("health_check_id"); ok { 759 rec.HealthCheckId = aws.String(v.(string)) 760 } 761 762 if v, ok := d.GetOk("weighted_routing_policy"); ok { 763 if _, ok := d.GetOk("set_identifier"); !ok { 764 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)) 765 } 766 records := v.([]interface{}) 767 if len(records) > 1 { 768 return nil, fmt.Errorf("You can only define a single weighed_routing_policy per record") 769 } 770 weight := records[0].(map[string]interface{}) 771 772 rec.Weight = aws.Int64(int64(weight["weight"].(int))) 773 } 774 775 if v, ok := d.GetOk("set_identifier"); ok { 776 rec.SetIdentifier = aws.String(v.(string)) 777 } 778 779 if v, ok := d.GetOk("latency_routing_policy"); ok { 780 if _, ok := d.GetOk("set_identifier"); !ok { 781 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)) 782 } 783 records := v.([]interface{}) 784 if len(records) > 1 { 785 return nil, fmt.Errorf("You can only define a single latency_routing_policy per record") 786 } 787 latency := records[0].(map[string]interface{}) 788 789 rec.Region = aws.String(latency["region"].(string)) 790 } 791 792 if v, ok := d.GetOk("geolocation_routing_policy"); ok { 793 if _, ok := d.GetOk("set_identifier"); !ok { 794 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)) 795 } 796 geolocations := v.([]interface{}) 797 if len(geolocations) > 1 { 798 return nil, fmt.Errorf("You can only define a single geolocation_routing_policy per record") 799 } 800 geolocation := geolocations[0].(map[string]interface{}) 801 802 rec.GeoLocation = &route53.GeoLocation{ 803 ContinentCode: nilString(geolocation["continent"].(string)), 804 CountryCode: nilString(geolocation["country"].(string)), 805 SubdivisionCode: nilString(geolocation["subdivision"].(string)), 806 } 807 log.Printf("[DEBUG] Creating geolocation: %#v", geolocation) 808 } 809 810 return rec, nil 811 } 812 813 func FQDN(name string) string { 814 n := len(name) 815 if n == 0 || name[n-1] == '.' { 816 return name 817 } else { 818 return name + "." 819 } 820 } 821 822 // Route 53 stores the "*" wildcard indicator as ASCII 42 and returns the 823 // octal equivalent, "\\052". Here we look for that, and convert back to "*" 824 // as needed. 825 func cleanRecordName(name string) string { 826 str := name 827 if strings.HasPrefix(name, "\\052") { 828 str = strings.Replace(name, "\\052", "*", 1) 829 log.Printf("[DEBUG] Replacing octal \\052 for * in: %s", name) 830 } 831 return str 832 } 833 834 // Check if the current record name contains the zone suffix. 835 // If it does not, add the zone name to form a fully qualified name 836 // and keep AWS happy. 837 func expandRecordName(name, zone string) string { 838 rn := strings.ToLower(strings.TrimSuffix(name, ".")) 839 zone = strings.TrimSuffix(zone, ".") 840 if !strings.HasSuffix(rn, zone) { 841 if len(name) == 0 { 842 rn = zone 843 } else { 844 rn = strings.Join([]string{name, zone}, ".") 845 } 846 } 847 return rn 848 } 849 850 func resourceAwsRoute53AliasRecordHash(v interface{}) int { 851 var buf bytes.Buffer 852 m := v.(map[string]interface{}) 853 buf.WriteString(fmt.Sprintf("%s-", normalizeAwsAliasName(m["name"].(string)))) 854 buf.WriteString(fmt.Sprintf("%s-", m["zone_id"].(string))) 855 buf.WriteString(fmt.Sprintf("%t-", m["evaluate_target_health"].(bool))) 856 857 return hashcode.String(buf.String()) 858 } 859 860 // nilString takes a string as an argument and returns a string 861 // pointer. The returned pointer is nil if the string argument is 862 // empty, otherwise it is a pointer to a copy of the string. 863 func nilString(s string) *string { 864 if s == "" { 865 return nil 866 } 867 return aws.String(s) 868 } 869 870 func normalizeAwsAliasName(alias interface{}) string { 871 input := alias.(string) 872 if strings.HasPrefix(input, "dualstack.") { 873 return strings.Replace(input, "dualstack.", "", -1) 874 } 875 876 return strings.TrimRight(input, ".") 877 }