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