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