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