github.com/richardbowden/terraform@v0.6.12-0.20160901200758-30ea22c25211/builtin/providers/aws/resource_aws_route53_zone.go (about) 1 package aws 2 3 import ( 4 "fmt" 5 "log" 6 "sort" 7 "strings" 8 "time" 9 10 "github.com/hashicorp/terraform/helper/resource" 11 "github.com/hashicorp/terraform/helper/schema" 12 13 "github.com/aws/aws-sdk-go/aws" 14 "github.com/aws/aws-sdk-go/aws/awserr" 15 "github.com/aws/aws-sdk-go/service/route53" 16 ) 17 18 func resourceAwsRoute53Zone() *schema.Resource { 19 return &schema.Resource{ 20 Create: resourceAwsRoute53ZoneCreate, 21 Read: resourceAwsRoute53ZoneRead, 22 Update: resourceAwsRoute53ZoneUpdate, 23 Delete: resourceAwsRoute53ZoneDelete, 24 Importer: &schema.ResourceImporter{ 25 State: schema.ImportStatePassthrough, 26 }, 27 28 Schema: map[string]*schema.Schema{ 29 "name": &schema.Schema{ 30 Type: schema.TypeString, 31 Required: true, 32 ForceNew: true, 33 }, 34 35 "comment": &schema.Schema{ 36 Type: schema.TypeString, 37 Optional: true, 38 Default: "Managed by Terraform", 39 }, 40 41 "vpc_id": &schema.Schema{ 42 Type: schema.TypeString, 43 Optional: true, 44 ForceNew: true, 45 ConflictsWith: []string{"delegation_set_id"}, 46 }, 47 48 "vpc_region": &schema.Schema{ 49 Type: schema.TypeString, 50 Optional: true, 51 ForceNew: true, 52 Computed: true, 53 }, 54 55 "zone_id": &schema.Schema{ 56 Type: schema.TypeString, 57 Computed: true, 58 }, 59 60 "delegation_set_id": &schema.Schema{ 61 Type: schema.TypeString, 62 Optional: true, 63 ForceNew: true, 64 ConflictsWith: []string{"vpc_id"}, 65 }, 66 67 "name_servers": &schema.Schema{ 68 Type: schema.TypeList, 69 Elem: &schema.Schema{Type: schema.TypeString}, 70 Computed: true, 71 }, 72 73 "tags": tagsSchema(), 74 75 "force_destroy": &schema.Schema{ 76 Type: schema.TypeBool, 77 Optional: true, 78 Default: false, 79 }, 80 }, 81 } 82 } 83 84 func resourceAwsRoute53ZoneCreate(d *schema.ResourceData, meta interface{}) error { 85 r53 := meta.(*AWSClient).r53conn 86 87 req := &route53.CreateHostedZoneInput{ 88 Name: aws.String(d.Get("name").(string)), 89 HostedZoneConfig: &route53.HostedZoneConfig{Comment: aws.String(d.Get("comment").(string))}, 90 CallerReference: aws.String(time.Now().Format(time.RFC3339Nano)), 91 } 92 if v := d.Get("vpc_id"); v != "" { 93 req.VPC = &route53.VPC{ 94 VPCId: aws.String(v.(string)), 95 VPCRegion: aws.String(meta.(*AWSClient).region), 96 } 97 if w := d.Get("vpc_region"); w != "" { 98 req.VPC.VPCRegion = aws.String(w.(string)) 99 } 100 d.Set("vpc_region", req.VPC.VPCRegion) 101 } 102 103 if v, ok := d.GetOk("delegation_set_id"); ok { 104 req.DelegationSetId = aws.String(v.(string)) 105 } 106 107 log.Printf("[DEBUG] Creating Route53 hosted zone: %s", *req.Name) 108 var err error 109 resp, err := r53.CreateHostedZone(req) 110 if err != nil { 111 return err 112 } 113 114 // Store the zone_id 115 zone := cleanZoneID(*resp.HostedZone.Id) 116 d.Set("zone_id", zone) 117 d.SetId(zone) 118 119 // Wait until we are done initializing 120 wait := resource.StateChangeConf{ 121 Delay: 30 * time.Second, 122 Pending: []string{"PENDING"}, 123 Target: []string{"INSYNC"}, 124 Timeout: 10 * time.Minute, 125 MinTimeout: 2 * time.Second, 126 Refresh: func() (result interface{}, state string, err error) { 127 changeRequest := &route53.GetChangeInput{ 128 Id: aws.String(cleanChangeID(*resp.ChangeInfo.Id)), 129 } 130 return resourceAwsGoRoute53Wait(r53, changeRequest) 131 }, 132 } 133 _, err = wait.WaitForState() 134 if err != nil { 135 return err 136 } 137 return resourceAwsRoute53ZoneUpdate(d, meta) 138 } 139 140 func resourceAwsRoute53ZoneRead(d *schema.ResourceData, meta interface{}) error { 141 r53 := meta.(*AWSClient).r53conn 142 zone, err := r53.GetHostedZone(&route53.GetHostedZoneInput{Id: aws.String(d.Id())}) 143 if err != nil { 144 // Handle a deleted zone 145 if r53err, ok := err.(awserr.Error); ok && r53err.Code() == "NoSuchHostedZone" { 146 d.SetId("") 147 return nil 148 } 149 return err 150 } 151 152 // In the import case this will be empty 153 if _, ok := d.GetOk("zone_id"); !ok { 154 d.Set("zone_id", d.Id()) 155 } 156 if _, ok := d.GetOk("name"); !ok { 157 d.Set("name", zone.HostedZone.Name) 158 } 159 160 if !*zone.HostedZone.Config.PrivateZone { 161 ns := make([]string, len(zone.DelegationSet.NameServers)) 162 for i := range zone.DelegationSet.NameServers { 163 ns[i] = *zone.DelegationSet.NameServers[i] 164 } 165 sort.Strings(ns) 166 if err := d.Set("name_servers", ns); err != nil { 167 return fmt.Errorf("[DEBUG] Error setting name servers for: %s, error: %#v", d.Id(), err) 168 } 169 } else { 170 ns, err := getNameServers(d.Id(), d.Get("name").(string), r53) 171 if err != nil { 172 return err 173 } 174 if err := d.Set("name_servers", ns); err != nil { 175 return fmt.Errorf("[DEBUG] Error setting name servers for: %s, error: %#v", d.Id(), err) 176 } 177 178 // In the import case we just associate it with the first VPC 179 if _, ok := d.GetOk("vpc_id"); !ok { 180 if len(zone.VPCs) > 1 { 181 return fmt.Errorf( 182 "Can't import a route53_zone with more than one VPC attachment") 183 } 184 185 if len(zone.VPCs) > 0 { 186 d.Set("vpc_id", zone.VPCs[0].VPCId) 187 d.Set("vpc_region", zone.VPCs[0].VPCRegion) 188 } 189 } 190 191 var associatedVPC *route53.VPC 192 for _, vpc := range zone.VPCs { 193 if *vpc.VPCId == d.Get("vpc_id") { 194 associatedVPC = vpc 195 break 196 } 197 } 198 if associatedVPC == nil { 199 return fmt.Errorf("[DEBUG] VPC: %v is not associated with Zone: %v", d.Get("vpc_id"), d.Id()) 200 } 201 } 202 203 if zone.DelegationSet != nil && zone.DelegationSet.Id != nil { 204 d.Set("delegation_set_id", cleanDelegationSetId(*zone.DelegationSet.Id)) 205 } 206 207 if zone.HostedZone != nil && zone.HostedZone.Config != nil && zone.HostedZone.Config.Comment != nil { 208 d.Set("comment", zone.HostedZone.Config.Comment) 209 } 210 211 // get tags 212 req := &route53.ListTagsForResourceInput{ 213 ResourceId: aws.String(d.Id()), 214 ResourceType: aws.String("hostedzone"), 215 } 216 217 resp, err := r53.ListTagsForResource(req) 218 if err != nil { 219 return err 220 } 221 222 var tags []*route53.Tag 223 if resp.ResourceTagSet != nil { 224 tags = resp.ResourceTagSet.Tags 225 } 226 227 if err := d.Set("tags", tagsToMapR53(tags)); err != nil { 228 return err 229 } 230 231 return nil 232 } 233 234 func resourceAwsRoute53ZoneUpdate(d *schema.ResourceData, meta interface{}) error { 235 conn := meta.(*AWSClient).r53conn 236 237 d.Partial(true) 238 239 if d.HasChange("comment") { 240 zoneInput := route53.UpdateHostedZoneCommentInput{ 241 Id: aws.String(d.Id()), 242 Comment: aws.String(d.Get("comment").(string)), 243 } 244 245 _, err := conn.UpdateHostedZoneComment(&zoneInput) 246 if err != nil { 247 return err 248 } else { 249 d.SetPartial("comment") 250 } 251 } 252 253 if err := setTagsR53(conn, d, "hostedzone"); err != nil { 254 return err 255 } else { 256 d.SetPartial("tags") 257 } 258 259 d.Partial(false) 260 261 return resourceAwsRoute53ZoneRead(d, meta) 262 } 263 264 func resourceAwsRoute53ZoneDelete(d *schema.ResourceData, meta interface{}) error { 265 r53 := meta.(*AWSClient).r53conn 266 267 if d.Get("force_destroy").(bool) { 268 deleteAllRecordsInHostedZoneId(d.Id(), d.Get("name").(string), r53) 269 } 270 271 log.Printf("[DEBUG] Deleting Route53 hosted zone: %s (ID: %s)", 272 d.Get("name").(string), d.Id()) 273 _, err := r53.DeleteHostedZone(&route53.DeleteHostedZoneInput{Id: aws.String(d.Id())}) 274 if err != nil { 275 if r53err, ok := err.(awserr.Error); ok && r53err.Code() == "NoSuchHostedZone" { 276 log.Printf("[DEBUG] No matching Route 53 Zone found for: %s, removing from state file", d.Id()) 277 d.SetId("") 278 return nil 279 } 280 return err 281 } 282 283 return nil 284 } 285 286 func deleteAllRecordsInHostedZoneId(hostedZoneId, hostedZoneName string, conn *route53.Route53) error { 287 input := &route53.ListResourceRecordSetsInput{ 288 HostedZoneId: aws.String(hostedZoneId), 289 } 290 291 var lastDeleteErr, lastErrorFromWaiter error 292 var pageNum = 0 293 err := conn.ListResourceRecordSetsPages(input, func(page *route53.ListResourceRecordSetsOutput, isLastPage bool) bool { 294 sets := page.ResourceRecordSets 295 pageNum += 1 296 297 changes := make([]*route53.Change, 0) 298 // 100 items per page returned by default 299 for _, set := range sets { 300 if *set.Name == hostedZoneName+"." && (*set.Type == "NS" || *set.Type == "SOA") { 301 // Zone NS & SOA records cannot be deleted 302 continue 303 } 304 changes = append(changes, &route53.Change{ 305 Action: aws.String("DELETE"), 306 ResourceRecordSet: set, 307 }) 308 } 309 log.Printf("[DEBUG] Deleting %d records (page %d) from %s", 310 len(changes), pageNum, hostedZoneId) 311 312 req := &route53.ChangeResourceRecordSetsInput{ 313 HostedZoneId: aws.String(hostedZoneId), 314 ChangeBatch: &route53.ChangeBatch{ 315 Comment: aws.String("Deleted by Terraform"), 316 Changes: changes, 317 }, 318 } 319 320 var resp interface{} 321 resp, lastDeleteErr = deleteRoute53RecordSet(conn, req) 322 if out, ok := resp.(*route53.ChangeResourceRecordSetsOutput); ok { 323 log.Printf("[DEBUG] Waiting for change batch to become INSYNC: %#v", out) 324 lastErrorFromWaiter = waitForRoute53RecordSetToSync(conn, cleanChangeID(*out.ChangeInfo.Id)) 325 } else { 326 log.Printf("[DEBUG] Unable to wait for change batch because of an error: %s", lastDeleteErr) 327 } 328 329 return !isLastPage 330 }) 331 if err != nil { 332 return fmt.Errorf("Failed listing/deleting record sets: %s\nLast error from deletion: %s\nLast error from waiter: %s", 333 err, lastDeleteErr, lastErrorFromWaiter) 334 } 335 336 return nil 337 } 338 339 func resourceAwsGoRoute53Wait(r53 *route53.Route53, ref *route53.GetChangeInput) (result interface{}, state string, err error) { 340 341 status, err := r53.GetChange(ref) 342 if err != nil { 343 return nil, "UNKNOWN", err 344 } 345 return true, *status.ChangeInfo.Status, nil 346 } 347 348 // cleanChangeID is used to remove the leading /change/ 349 func cleanChangeID(ID string) string { 350 return cleanPrefix(ID, "/change/") 351 } 352 353 // cleanZoneID is used to remove the leading /hostedzone/ 354 func cleanZoneID(ID string) string { 355 return cleanPrefix(ID, "/hostedzone/") 356 } 357 358 // cleanPrefix removes a string prefix from an ID 359 func cleanPrefix(ID, prefix string) string { 360 if strings.HasPrefix(ID, prefix) { 361 ID = strings.TrimPrefix(ID, prefix) 362 } 363 return ID 364 } 365 366 func getNameServers(zoneId string, zoneName string, r53 *route53.Route53) ([]string, error) { 367 resp, err := r53.ListResourceRecordSets(&route53.ListResourceRecordSetsInput{ 368 HostedZoneId: aws.String(zoneId), 369 StartRecordName: aws.String(zoneName), 370 StartRecordType: aws.String("NS"), 371 }) 372 if err != nil { 373 return nil, err 374 } 375 if len(resp.ResourceRecordSets) == 0 { 376 return nil, nil 377 } 378 ns := make([]string, len(resp.ResourceRecordSets[0].ResourceRecords)) 379 for i := range resp.ResourceRecordSets[0].ResourceRecords { 380 ns[i] = *resp.ResourceRecordSets[0].ResourceRecords[i].Value 381 } 382 sort.Strings(ns) 383 return ns, nil 384 }