github.com/turtlemonvh/terraform@v0.6.9-0.20151204001754-8e40b6b855e8/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 return err 249 } 250 en := expandRecordName(d.Get("name").(string), *zoneRecord.HostedZone.Name) 251 log.Printf("[DEBUG] Expanded record name: %s", en) 252 d.Set("fqdn", en) 253 254 lopts := &route53.ListResourceRecordSetsInput{ 255 HostedZoneId: aws.String(cleanZoneID(zone)), 256 StartRecordName: aws.String(en), 257 StartRecordType: aws.String(d.Get("type").(string)), 258 } 259 260 log.Printf("[DEBUG] List resource records sets for zone: %s, opts: %s", 261 zone, lopts) 262 resp, err := conn.ListResourceRecordSets(lopts) 263 if err != nil { 264 return err 265 } 266 267 // Scan for a matching record 268 found := false 269 for _, record := range resp.ResourceRecordSets { 270 name := cleanRecordName(*record.Name) 271 if FQDN(strings.ToLower(name)) != FQDN(strings.ToLower(*lopts.StartRecordName)) { 272 continue 273 } 274 if strings.ToUpper(*record.Type) != strings.ToUpper(*lopts.StartRecordType) { 275 continue 276 } 277 278 if record.SetIdentifier != nil && *record.SetIdentifier != d.Get("set_identifier") { 279 continue 280 } 281 282 found = true 283 284 err := d.Set("records", flattenResourceRecords(record.ResourceRecords)) 285 if err != nil { 286 return fmt.Errorf("[DEBUG] Error setting records for: %s, error: %#v", en, err) 287 } 288 289 d.Set("ttl", record.TTL) 290 d.Set("weight", record.Weight) 291 d.Set("set_identifier", record.SetIdentifier) 292 d.Set("failover", record.Failover) 293 d.Set("health_check_id", record.HealthCheckId) 294 295 break 296 } 297 298 if !found { 299 log.Printf("[DEBUG] No matching record found for: %s, removing from state file", en) 300 d.SetId("") 301 } 302 303 return nil 304 } 305 306 func resourceAwsRoute53RecordDelete(d *schema.ResourceData, meta interface{}) error { 307 conn := meta.(*AWSClient).r53conn 308 309 zone := cleanZoneID(d.Get("zone_id").(string)) 310 log.Printf("[DEBUG] Deleting resource records for zone: %s, name: %s", 311 zone, d.Get("name").(string)) 312 var err error 313 zoneRecord, err := conn.GetHostedZone(&route53.GetHostedZoneInput{Id: aws.String(zone)}) 314 if err != nil { 315 return err 316 } 317 // Get the records 318 rec, err := resourceAwsRoute53RecordBuildSet(d, *zoneRecord.HostedZone.Name) 319 if err != nil { 320 return err 321 } 322 323 // Create the new records 324 changeBatch := &route53.ChangeBatch{ 325 Comment: aws.String("Deleted by Terraform"), 326 Changes: []*route53.Change{ 327 &route53.Change{ 328 Action: aws.String("DELETE"), 329 ResourceRecordSet: rec, 330 }, 331 }, 332 } 333 334 req := &route53.ChangeResourceRecordSetsInput{ 335 HostedZoneId: aws.String(cleanZoneID(zone)), 336 ChangeBatch: changeBatch, 337 } 338 339 wait := resource.StateChangeConf{ 340 Pending: []string{"rejected"}, 341 Target: "accepted", 342 Timeout: 5 * time.Minute, 343 MinTimeout: 1 * time.Second, 344 Refresh: func() (interface{}, string, error) { 345 _, err := conn.ChangeResourceRecordSets(req) 346 if err != nil { 347 if r53err, ok := err.(awserr.Error); ok { 348 if r53err.Code() == "PriorRequestNotComplete" { 349 // There is some pending operation, so just retry 350 // in a bit. 351 return 42, "rejected", nil 352 } 353 354 if r53err.Code() == "InvalidChangeBatch" { 355 // This means that the record is already gone. 356 return 42, "accepted", nil 357 } 358 } 359 360 return 42, "failure", err 361 } 362 363 return 42, "accepted", nil 364 }, 365 } 366 367 if _, err := wait.WaitForState(); err != nil { 368 return err 369 } 370 371 return nil 372 } 373 374 func resourceAwsRoute53RecordBuildSet(d *schema.ResourceData, zoneName string) (*route53.ResourceRecordSet, error) { 375 // get expanded name 376 en := expandRecordName(d.Get("name").(string), zoneName) 377 378 // Create the RecordSet request with the fully expanded name, e.g. 379 // sub.domain.com. Route 53 requires a fully qualified domain name, but does 380 // not require the trailing ".", which it will itself, so we don't call FQDN 381 // here. 382 rec := &route53.ResourceRecordSet{ 383 Name: aws.String(en), 384 Type: aws.String(d.Get("type").(string)), 385 } 386 387 if v, ok := d.GetOk("ttl"); ok { 388 rec.TTL = aws.Int64(int64(v.(int))) 389 } 390 391 // Resource records 392 if v, ok := d.GetOk("records"); ok { 393 recs := v.(*schema.Set).List() 394 rec.ResourceRecords = expandResourceRecords(recs, d.Get("type").(string)) 395 } 396 397 // Alias record 398 if v, ok := d.GetOk("alias"); ok { 399 aliases := v.(*schema.Set).List() 400 if len(aliases) > 1 { 401 return nil, fmt.Errorf("You can only define a single alias target per record") 402 } 403 alias := aliases[0].(map[string]interface{}) 404 rec.AliasTarget = &route53.AliasTarget{ 405 DNSName: aws.String(alias["name"].(string)), 406 EvaluateTargetHealth: aws.Bool(alias["evaluate_target_health"].(bool)), 407 HostedZoneId: aws.String(alias["zone_id"].(string)), 408 } 409 log.Printf("[DEBUG] Creating alias: %#v", alias) 410 } else { 411 if _, ok := d.GetOk("ttl"); !ok { 412 return nil, fmt.Errorf(`provider.aws: aws_route53_record: %s: "ttl": required field is not set`, d.Get("name").(string)) 413 } 414 415 if _, ok := d.GetOk("records"); !ok { 416 return nil, fmt.Errorf(`provider.aws: aws_route53_record: %s: "records": required field is not set`, d.Get("name").(string)) 417 } 418 } 419 420 if v, ok := d.GetOk("failover"); ok { 421 rec.Failover = aws.String(v.(string)) 422 } 423 424 if v, ok := d.GetOk("health_check_id"); ok { 425 rec.HealthCheckId = aws.String(v.(string)) 426 } 427 428 if v, ok := d.GetOk("set_identifier"); ok { 429 rec.SetIdentifier = aws.String(v.(string)) 430 } 431 432 if v, ok := d.GetOk("weight"); ok { 433 rec.Weight = aws.Int64(int64(v.(int))) 434 } 435 436 return rec, nil 437 } 438 439 func FQDN(name string) string { 440 n := len(name) 441 if n == 0 || name[n-1] == '.' { 442 return name 443 } else { 444 return name + "." 445 } 446 } 447 448 // Route 53 stores the "*" wildcard indicator as ASCII 42 and returns the 449 // octal equivalent, "\\052". Here we look for that, and convert back to "*" 450 // as needed. 451 func cleanRecordName(name string) string { 452 str := name 453 if strings.HasPrefix(name, "\\052") { 454 str = strings.Replace(name, "\\052", "*", 1) 455 log.Printf("[DEBUG] Replacing octal \\052 for * in: %s", name) 456 } 457 return str 458 } 459 460 // Check if the current record name contains the zone suffix. 461 // If it does not, add the zone name to form a fully qualified name 462 // and keep AWS happy. 463 func expandRecordName(name, zone string) string { 464 rn := strings.ToLower(strings.TrimSuffix(name, ".")) 465 zone = strings.TrimSuffix(zone, ".") 466 if !strings.HasSuffix(rn, zone) { 467 rn = strings.Join([]string{name, zone}, ".") 468 } 469 return rn 470 } 471 472 func resourceAwsRoute53AliasRecordHash(v interface{}) int { 473 var buf bytes.Buffer 474 m := v.(map[string]interface{}) 475 buf.WriteString(fmt.Sprintf("%s-", m["name"].(string))) 476 buf.WriteString(fmt.Sprintf("%s-", m["zone_id"].(string))) 477 buf.WriteString(fmt.Sprintf("%t-", m["evaluate_target_health"].(bool))) 478 479 return hashcode.String(buf.String()) 480 }