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