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