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