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