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