github.com/nicgrayson/terraform@v0.4.3-0.20150415203910-c4de50829380/builtin/providers/aws/resource_aws_route53_record.go (about) 1 package aws 2 3 import ( 4 "fmt" 5 "log" 6 "strings" 7 "time" 8 9 "github.com/hashicorp/terraform/helper/hashcode" 10 "github.com/hashicorp/terraform/helper/resource" 11 "github.com/hashicorp/terraform/helper/schema" 12 13 "github.com/hashicorp/aws-sdk-go/aws" 14 "github.com/hashicorp/aws-sdk-go/gen/route53" 15 ) 16 17 func resourceAwsRoute53Record() *schema.Resource { 18 return &schema.Resource{ 19 Create: resourceAwsRoute53RecordCreate, 20 Read: resourceAwsRoute53RecordRead, 21 Update: resourceAwsRoute53RecordUpdate, 22 Delete: resourceAwsRoute53RecordDelete, 23 24 Schema: map[string]*schema.Schema{ 25 "name": &schema.Schema{ 26 Type: schema.TypeString, 27 Required: true, 28 ForceNew: true, 29 }, 30 31 "type": &schema.Schema{ 32 Type: schema.TypeString, 33 Required: true, 34 ForceNew: true, 35 }, 36 37 "zone_id": &schema.Schema{ 38 Type: schema.TypeString, 39 Required: true, 40 ForceNew: true, 41 }, 42 43 "ttl": &schema.Schema{ 44 Type: schema.TypeInt, 45 Required: true, 46 }, 47 48 "records": &schema.Schema{ 49 Type: schema.TypeSet, 50 Elem: &schema.Schema{Type: schema.TypeString}, 51 Required: true, 52 Set: func(v interface{}) int { 53 return hashcode.String(v.(string)) 54 }, 55 }, 56 }, 57 } 58 } 59 60 func resourceAwsRoute53RecordUpdate(d *schema.ResourceData, meta interface{}) error { 61 // Route 53 supports CREATE, DELETE, and UPSERT actions. We use UPSERT, and 62 // AWS dynamically determines if a record should be created or updated. 63 // Amazon Route 53 can update an existing resource record set only when all 64 // of the following values match: Name, Type 65 // (and SetIdentifier, which we don't use yet). 66 // See http://docs.aws.amazon.com/Route53/latest/APIReference/API_ChangeResourceRecordSets_Requests.html#change-rrsets-request-action 67 // 68 // Because we use UPSERT, for resouce update here we simply fall through to 69 // our resource create function. 70 return resourceAwsRoute53RecordCreate(d, meta) 71 } 72 73 func resourceAwsRoute53RecordCreate(d *schema.ResourceData, meta interface{}) error { 74 conn := meta.(*AWSClient).r53conn 75 zone := cleanZoneID(d.Get("zone_id").(string)) 76 77 zoneRecord, err := conn.GetHostedZone(&route53.GetHostedZoneRequest{ID: aws.String(zone)}) 78 if err != nil { 79 return err 80 } 81 82 // Get the record 83 rec, err := resourceAwsRoute53RecordBuildSet(d, *zoneRecord.HostedZone.Name) 84 if err != nil { 85 return err 86 } 87 88 // Create the new records. We abuse StateChangeConf for this to 89 // retry for us since Route53 sometimes returns errors about another 90 // operation happening at the same time. 91 changeBatch := &route53.ChangeBatch{ 92 Comment: aws.String("Managed by Terraform"), 93 Changes: []route53.Change{ 94 route53.Change{ 95 Action: aws.String("UPSERT"), 96 ResourceRecordSet: rec, 97 }, 98 }, 99 } 100 101 req := &route53.ChangeResourceRecordSetsRequest{ 102 HostedZoneID: aws.String(cleanZoneID(*zoneRecord.HostedZone.ID)), 103 ChangeBatch: changeBatch, 104 } 105 106 log.Printf("[DEBUG] Creating resource records for zone: %s, name: %s", 107 zone, *rec.Name) 108 109 wait := resource.StateChangeConf{ 110 Pending: []string{"rejected"}, 111 Target: "accepted", 112 Timeout: 5 * time.Minute, 113 MinTimeout: 1 * time.Second, 114 Refresh: func() (interface{}, string, error) { 115 resp, err := conn.ChangeResourceRecordSets(req) 116 if err != nil { 117 if r53err, ok := err.(aws.APIError); ok { 118 if r53err.Code == "PriorRequestNotComplete" { 119 // There is some pending operation, so just retry 120 // in a bit. 121 return nil, "rejected", nil 122 } 123 } 124 125 return nil, "failure", err 126 } 127 128 return resp, "accepted", nil 129 }, 130 } 131 132 respRaw, err := wait.WaitForState() 133 if err != nil { 134 return err 135 } 136 changeInfo := respRaw.(*route53.ChangeResourceRecordSetsResponse).ChangeInfo 137 138 // Generate an ID 139 d.SetId(fmt.Sprintf("%s_%s_%s", zone, d.Get("name").(string), d.Get("type").(string))) 140 141 // Wait until we are done 142 wait = resource.StateChangeConf{ 143 Delay: 30 * time.Second, 144 Pending: []string{"PENDING"}, 145 Target: "INSYNC", 146 Timeout: 30 * time.Minute, 147 MinTimeout: 5 * time.Second, 148 Refresh: func() (result interface{}, state string, err error) { 149 changeRequest := &route53.GetChangeRequest{ 150 ID: aws.String(cleanChangeID(*changeInfo.ID)), 151 } 152 return resourceAwsGoRoute53Wait(conn, changeRequest) 153 }, 154 } 155 _, err = wait.WaitForState() 156 if err != nil { 157 return err 158 } 159 160 return nil 161 } 162 163 func resourceAwsRoute53RecordRead(d *schema.ResourceData, meta interface{}) error { 164 conn := meta.(*AWSClient).r53conn 165 166 zone := cleanZoneID(d.Get("zone_id").(string)) 167 168 // get expanded name 169 zoneRecord, err := conn.GetHostedZone(&route53.GetHostedZoneRequest{ID: aws.String(zone)}) 170 if err != nil { 171 return err 172 } 173 en := expandRecordName(d.Get("name").(string), *zoneRecord.HostedZone.Name) 174 175 lopts := &route53.ListResourceRecordSetsRequest{ 176 HostedZoneID: aws.String(cleanZoneID(zone)), 177 StartRecordName: aws.String(en), 178 StartRecordType: aws.String(d.Get("type").(string)), 179 } 180 181 resp, err := conn.ListResourceRecordSets(lopts) 182 if err != nil { 183 return err 184 } 185 186 // Scan for a matching record 187 found := false 188 for _, record := range resp.ResourceRecordSets { 189 name := cleanRecordName(*record.Name) 190 if FQDN(name) != FQDN(*lopts.StartRecordName) { 191 continue 192 } 193 if strings.ToUpper(*record.Type) != strings.ToUpper(*lopts.StartRecordType) { 194 continue 195 } 196 197 found = true 198 199 err := d.Set("records", flattenResourceRecords(record.ResourceRecords)) 200 if err != nil { 201 return fmt.Errorf("[DEBUG] Error setting records for: %s, error: %#v", en, err) 202 } 203 d.Set("ttl", record.TTL) 204 205 break 206 } 207 208 if !found { 209 d.SetId("") 210 } 211 212 return nil 213 } 214 215 func resourceAwsRoute53RecordDelete(d *schema.ResourceData, meta interface{}) error { 216 conn := meta.(*AWSClient).r53conn 217 218 zone := cleanZoneID(d.Get("zone_id").(string)) 219 log.Printf("[DEBUG] Deleting resource records for zone: %s, name: %s", 220 zone, d.Get("name").(string)) 221 zoneRecord, err := conn.GetHostedZone(&route53.GetHostedZoneRequest{ID: aws.String(zone)}) 222 if err != nil { 223 return err 224 } 225 // Get the records 226 rec, err := resourceAwsRoute53RecordBuildSet(d, *zoneRecord.HostedZone.Name) 227 if err != nil { 228 return err 229 } 230 231 // Create the new records 232 changeBatch := &route53.ChangeBatch{ 233 Comment: aws.String("Deleted by Terraform"), 234 Changes: []route53.Change{ 235 route53.Change{ 236 Action: aws.String("DELETE"), 237 ResourceRecordSet: rec, 238 }, 239 }, 240 } 241 242 req := &route53.ChangeResourceRecordSetsRequest{ 243 HostedZoneID: aws.String(cleanZoneID(zone)), 244 ChangeBatch: changeBatch, 245 } 246 247 wait := resource.StateChangeConf{ 248 Pending: []string{"rejected"}, 249 Target: "accepted", 250 Timeout: 5 * time.Minute, 251 MinTimeout: 1 * time.Second, 252 Refresh: func() (interface{}, string, error) { 253 _, err := conn.ChangeResourceRecordSets(req) 254 if err != nil { 255 if r53err, ok := err.(aws.APIError); ok { 256 if r53err.Code == "PriorRequestNotComplete" { 257 // There is some pending operation, so just retry 258 // in a bit. 259 return 42, "rejected", nil 260 } 261 262 if r53err.Code == "InvalidChangeBatch" { 263 // This means that the record is already gone. 264 return 42, "accepted", nil 265 } 266 } 267 268 return 42, "failure", err 269 } 270 271 return 42, "accepted", nil 272 }, 273 } 274 275 if _, err := wait.WaitForState(); err != nil { 276 return err 277 } 278 279 return nil 280 } 281 282 func resourceAwsRoute53RecordBuildSet(d *schema.ResourceData, zoneName string) (*route53.ResourceRecordSet, error) { 283 recs := d.Get("records").(*schema.Set).List() 284 285 records := expandResourceRecords(recs, d.Get("type").(string)) 286 287 // get expanded name 288 en := expandRecordName(d.Get("name").(string), zoneName) 289 290 // Create the RecordSet request with the fully expanded name, e.g. 291 // sub.domain.com. Route 53 requires a fully qualified domain name, but does 292 // not require the trailing ".", which it will itself, so we don't call FQDN 293 // here. 294 rec := &route53.ResourceRecordSet{ 295 Name: aws.String(en), 296 Type: aws.String(d.Get("type").(string)), 297 TTL: aws.Long(int64(d.Get("ttl").(int))), 298 ResourceRecords: records, 299 } 300 return rec, nil 301 } 302 303 func FQDN(name string) string { 304 n := len(name) 305 if n == 0 || name[n-1] == '.' { 306 return name 307 } else { 308 return name + "." 309 } 310 } 311 312 // Route 53 stores the "*" wildcard indicator as ASCII 42 and returns the 313 // octal equivalent, "\\052". Here we look for that, and convert back to "*" 314 // as needed. 315 func cleanRecordName(name string) string { 316 str := name 317 if strings.HasPrefix(name, "\\052") { 318 str = strings.Replace(name, "\\052", "*", 1) 319 log.Printf("[DEBUG] Replacing octal \\052 for * in: %s", name) 320 } 321 return str 322 } 323 324 // Check if the current record name contains the zone suffix. 325 // If it does not, add the zone name to form a fully qualified name 326 // and keep AWS happy. 327 func expandRecordName(name, zone string) string { 328 rn := strings.TrimSuffix(name, ".") 329 zone = strings.TrimSuffix(zone, ".") 330 if !strings.HasSuffix(rn, zone) { 331 rn = strings.Join([]string{name, zone}, ".") 332 } 333 return rn 334 }