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