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