From bf97d6a80f4a7d432e4db0bbfed8ddd6230cf2ed Mon Sep 17 00:00:00 2001 From: John Engelman Date: Mon, 13 Apr 2015 12:42:20 -0500 Subject: [PATCH 01/18] AWS/Route53Zone - create private hosted zone associated with VPC. --- .../aws/resource_aws_route53_zone.go | 21 ++++++++++++ .../aws/resource_aws_route53_zone_test.go | 32 +++++++++++++++++++ .../aws/r/route53_zone.html.markdown | 2 ++ 3 files changed, 55 insertions(+) diff --git a/builtin/providers/aws/resource_aws_route53_zone.go b/builtin/providers/aws/resource_aws_route53_zone.go index 59937fbc08..d8f688e740 100644 --- a/builtin/providers/aws/resource_aws_route53_zone.go +++ b/builtin/providers/aws/resource_aws_route53_zone.go @@ -28,6 +28,18 @@ func resourceAwsRoute53Zone() *schema.Resource { ForceNew: true, }, + "vpc_id": &schema.Schema{ + Type: schema.TypeString, + Optional: true, + ForceNew: true, + }, + + "vpc_region": &schema.Schema{ + Type: schema.TypeString, + Optional: true, + ForceNew: true, + }, + "zone_id": &schema.Schema{ Type: schema.TypeString, Computed: true, @@ -53,6 +65,15 @@ func resourceAwsRoute53ZoneCreate(d *schema.ResourceData, meta interface{}) erro HostedZoneConfig: comment, CallerReference: aws.String(time.Now().Format(time.RFC3339Nano)), } + if v := d.Get("vpc_id"); v != nil { + req.VPC = &route53.VPC{ + VPCID: aws.String(v.(string)), + VPCRegion: aws.String(meta.(*AWSClient).region), + } + if w := d.Get("vpc_region"); w != nil { + req.VPC.VPCRegion = aws.String(w.(string)) + } + } log.Printf("[DEBUG] Creating Route53 hosted zone: %s", *req.Name) resp, err := r53.CreateHostedZone(req) diff --git a/builtin/providers/aws/resource_aws_route53_zone_test.go b/builtin/providers/aws/resource_aws_route53_zone_test.go index 0a32cb2cdf..12ab623d30 100644 --- a/builtin/providers/aws/resource_aws_route53_zone_test.go +++ b/builtin/providers/aws/resource_aws_route53_zone_test.go @@ -84,6 +84,24 @@ func TestAccRoute53Zone(t *testing.T) { }) } +func TestAccRoute53PrivateZone(t *testing.T) { + var zone route53.HostedZone + + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + Providers: testAccProviders, + CheckDestroy: testAccCheckRoute53ZoneDestroy, + Steps: []resource.TestStep{ + resource.TestStep{ + Config: testAccRoute53PrivateZoneConfig, + Check: resource.ComposeTestCheckFunc( + testAccCheckRoute53ZoneExists("aws_route53_zone.main", &zone), + ), + }, + }, + }) +} + func testAccCheckRoute53ZoneDestroy(s *terraform.State) error { conn := testAccProvider.Meta().(*AWSClient).r53conn for _, rs := range s.RootModule().Resources { @@ -167,3 +185,17 @@ resource "aws_route53_zone" "main" { } } ` + +const testAccRoute53PrivateZoneConfig = ` +resource "aws_vpc" "main" { + cidr_block = "172.29.0.0/24" + instance_tenancy = "default" + enable_dns_support = true + enable_dns_hostnames = true +} + +resource "aws_route53_zone" "main" { + name = "hashicorp.com" + vpc_id = "${aws_vpc.main.id}" +} +` diff --git a/website/source/docs/providers/aws/r/route53_zone.html.markdown b/website/source/docs/providers/aws/r/route53_zone.html.markdown index 71c03a10e9..9e09606103 100644 --- a/website/source/docs/providers/aws/r/route53_zone.html.markdown +++ b/website/source/docs/providers/aws/r/route53_zone.html.markdown @@ -55,6 +55,8 @@ The following arguments are supported: * `name` - (Required) This is the name of the hosted zone. * `tags` - (Optional) A mapping of tags to assign to the zone. +* `vpc_id` - (Optional) The VPC to associate with a private hosted zone. Specifying `vpc_id` will create a private hosted zone. +* `vpc_region` - (Optional) The VPC's region. Defaults to the region of the AWS provider. ## Attributes Reference From a51bc6007dcad83c567c6644e58f54c378ba077a Mon Sep 17 00:00:00 2001 From: John Engelman Date: Fri, 8 May 2015 08:58:35 -0500 Subject: [PATCH 02/18] DelegationSet is not support in private zones. --- .../aws/resource_aws_route53_zone.go | 20 ++++++++++------- .../aws/resource_aws_route53_zone_test.go | 22 ++++++++++--------- 2 files changed, 24 insertions(+), 18 deletions(-) diff --git a/builtin/providers/aws/resource_aws_route53_zone.go b/builtin/providers/aws/resource_aws_route53_zone.go index d8f688e740..e89b782c08 100644 --- a/builtin/providers/aws/resource_aws_route53_zone.go +++ b/builtin/providers/aws/resource_aws_route53_zone.go @@ -70,7 +70,7 @@ func resourceAwsRoute53ZoneCreate(d *schema.ResourceData, meta interface{}) erro VPCID: aws.String(v.(string)), VPCRegion: aws.String(meta.(*AWSClient).region), } - if w := d.Get("vpc_region"); w != nil { + if w := d.Get("vpc_region"); w != "" { req.VPC.VPCRegion = aws.String(w.(string)) } } @@ -119,13 +119,17 @@ func resourceAwsRoute53ZoneRead(d *schema.ResourceData, meta interface{}) error return err } - ns := make([]string, len(zone.DelegationSet.NameServers)) - for i := range zone.DelegationSet.NameServers { - ns[i] = *zone.DelegationSet.NameServers[i] - } - sort.Strings(ns) - if err := d.Set("name_servers", ns); err != nil { - return fmt.Errorf("[DEBUG] Error setting name servers for: %s, error: %#v", d.Id(), err) + if zone.DelegationSet != nil { + ns := make([]string, len(zone.DelegationSet.NameServers)) + for i := range zone.DelegationSet.NameServers { + ns[i] = *zone.DelegationSet.NameServers[i] + } + sort.Strings(ns) + if err := d.Set("name_servers", ns); err != nil { + return fmt.Errorf("[DEBUG] Error setting name servers for: %s, error: %#v", d.Id(), err) + } + } else { + d.Set("name_servers", nil); } // get tags diff --git a/builtin/providers/aws/resource_aws_route53_zone_test.go b/builtin/providers/aws/resource_aws_route53_zone_test.go index 12ab623d30..0fda00815d 100644 --- a/builtin/providers/aws/resource_aws_route53_zone_test.go +++ b/builtin/providers/aws/resource_aws_route53_zone_test.go @@ -134,16 +134,18 @@ func testAccCheckRoute53ZoneExists(n string, zone *route53.HostedZone) resource. return fmt.Errorf("Hosted zone err: %v", err) } - sorted_ns := make([]string, len(resp.DelegationSet.NameServers)) - for i, ns := range resp.DelegationSet.NameServers { - sorted_ns[i] = *ns - } - sort.Strings(sorted_ns) - for idx, ns := range sorted_ns { - attribute := fmt.Sprintf("name_servers.%d", idx) - dsns := rs.Primary.Attributes[attribute] - if dsns != ns { - return fmt.Errorf("Got: %v for %v, Expected: %v", dsns, attribute, ns) + if resp.DelegationSet != nil { + sorted_ns := make([]string, len(resp.DelegationSet.NameServers)) + for i, ns := range resp.DelegationSet.NameServers { + sorted_ns[i] = *ns + } + sort.Strings(sorted_ns) + for idx, ns := range sorted_ns { + attribute := fmt.Sprintf("name_servers.%d", idx) + dsns := rs.Primary.Attributes[attribute] + if dsns != ns { + return fmt.Errorf("Got: %v for %v, Expected: %v", dsns, attribute, ns) + } } } From 8e62a14f29826740bb80c324219fc5b7fd5d3f50 Mon Sep 17 00:00:00 2001 From: John Engelman Date: Fri, 8 May 2015 09:28:47 -0500 Subject: [PATCH 03/18] verify VPC association on private zone --- .../aws/resource_aws_route53_zone.go | 3 +- .../aws/resource_aws_route53_zone_test.go | 39 +++++++++++++++---- 2 files changed, 34 insertions(+), 8 deletions(-) diff --git a/builtin/providers/aws/resource_aws_route53_zone.go b/builtin/providers/aws/resource_aws_route53_zone.go index e89b782c08..9a25dfd5dc 100644 --- a/builtin/providers/aws/resource_aws_route53_zone.go +++ b/builtin/providers/aws/resource_aws_route53_zone.go @@ -119,7 +119,7 @@ func resourceAwsRoute53ZoneRead(d *schema.ResourceData, meta interface{}) error return err } - if zone.DelegationSet != nil { + if ! *zone.HostedZone.Config.PrivateZone { ns := make([]string, len(zone.DelegationSet.NameServers)) for i := range zone.DelegationSet.NameServers { ns[i] = *zone.DelegationSet.NameServers[i] @@ -130,6 +130,7 @@ func resourceAwsRoute53ZoneRead(d *schema.ResourceData, meta interface{}) error } } else { d.Set("name_servers", nil); + //TODO Verify that the configure VPC is still associated } // get tags diff --git a/builtin/providers/aws/resource_aws_route53_zone_test.go b/builtin/providers/aws/resource_aws_route53_zone_test.go index 0fda00815d..6c59adcd7a 100644 --- a/builtin/providers/aws/resource_aws_route53_zone_test.go +++ b/builtin/providers/aws/resource_aws_route53_zone_test.go @@ -64,7 +64,7 @@ func TestCleanChangeID(t *testing.T) { } func TestAccRoute53Zone(t *testing.T) { - var zone route53.HostedZone + var zone route53.GetHostedZoneOutput var td route53.ResourceTagSet resource.Test(t, resource.TestCase{ @@ -85,7 +85,7 @@ func TestAccRoute53Zone(t *testing.T) { } func TestAccRoute53PrivateZone(t *testing.T) { - var zone route53.HostedZone + var zone route53.GetHostedZoneOutput resource.Test(t, resource.TestCase{ PreCheck: func() { testAccPreCheck(t) }, @@ -96,6 +96,7 @@ func TestAccRoute53PrivateZone(t *testing.T) { Config: testAccRoute53PrivateZoneConfig, Check: resource.ComposeTestCheckFunc( testAccCheckRoute53ZoneExists("aws_route53_zone.main", &zone), + testAccCheckRoute53ZoneAssociationExists("aws_vpc.main", &zone), ), }, }, @@ -117,7 +118,7 @@ func testAccCheckRoute53ZoneDestroy(s *terraform.State) error { return nil } -func testAccCheckRoute53ZoneExists(n string, zone *route53.HostedZone) resource.TestCheckFunc { +func testAccCheckRoute53ZoneExists(n string, zone *route53.GetHostedZoneOutput) resource.TestCheckFunc { return func(s *terraform.State) error { rs, ok := s.RootModule().Resources[n] if !ok { @@ -134,7 +135,7 @@ func testAccCheckRoute53ZoneExists(n string, zone *route53.HostedZone) resource. return fmt.Errorf("Hosted zone err: %v", err) } - if resp.DelegationSet != nil { + if ! *resp.HostedZone.Config.PrivateZone { sorted_ns := make([]string, len(resp.DelegationSet.NameServers)) for i, ns := range resp.DelegationSet.NameServers { sorted_ns[i] = *ns @@ -149,16 +150,40 @@ func testAccCheckRoute53ZoneExists(n string, zone *route53.HostedZone) resource. } } - *zone = *resp.HostedZone + *zone = *resp return nil } } -func testAccLoadTagsR53(zone *route53.HostedZone, td *route53.ResourceTagSet) resource.TestCheckFunc { +func testAccCheckRoute53ZoneAssociationExists(n string, zone *route53.GetHostedZoneOutput) resource.TestCheckFunc { + return func(s *terraform.State) error { + rs, ok := s.RootModule().Resources[n] + if !ok { + return fmt.Errorf("Not found: %s", n) + } + + if rs.Primary.ID == "" { + return fmt.Errorf("No VPC ID is set") + } + + var associatedVPC *route53.VPC + for _, vpc := range zone.VPCs { + if *vpc.VPCID == rs.Primary.ID { + associatedVPC = vpc + } + } + if associatedVPC == nil { + return fmt.Errorf("VPC: %v is not associated to Zone: %v") + } + return nil + } +} + +func testAccLoadTagsR53(zone *route53.GetHostedZoneOutput, td *route53.ResourceTagSet) resource.TestCheckFunc { return func(s *terraform.State) error { conn := testAccProvider.Meta().(*AWSClient).r53conn - zone := cleanZoneID(*zone.ID) + zone := cleanZoneID(*zone.HostedZone.ID) req := &route53.ListTagsForResourceInput{ ResourceID: aws.String(zone), ResourceType: aws.String("hostedzone"), From 052ff83670d8b634ed7c36bc26fdd1a68f5131fa Mon Sep 17 00:00:00 2001 From: John Engelman Date: Fri, 8 May 2015 10:06:27 -0500 Subject: [PATCH 04/18] Handle public zones correctly. Check for associate when reading. --- builtin/providers/aws/resource_aws_route53_zone.go | 12 ++++++++++-- .../providers/aws/resource_aws_route53_zone_test.go | 2 +- 2 files changed, 11 insertions(+), 3 deletions(-) diff --git a/builtin/providers/aws/resource_aws_route53_zone.go b/builtin/providers/aws/resource_aws_route53_zone.go index 9a25dfd5dc..f9ad99f4c4 100644 --- a/builtin/providers/aws/resource_aws_route53_zone.go +++ b/builtin/providers/aws/resource_aws_route53_zone.go @@ -65,7 +65,7 @@ func resourceAwsRoute53ZoneCreate(d *schema.ResourceData, meta interface{}) erro HostedZoneConfig: comment, CallerReference: aws.String(time.Now().Format(time.RFC3339Nano)), } - if v := d.Get("vpc_id"); v != nil { + if v := d.Get("vpc_id"); v != "" { req.VPC = &route53.VPC{ VPCID: aws.String(v.(string)), VPCRegion: aws.String(meta.(*AWSClient).region), @@ -130,7 +130,15 @@ func resourceAwsRoute53ZoneRead(d *schema.ResourceData, meta interface{}) error } } else { d.Set("name_servers", nil); - //TODO Verify that the configure VPC is still associated + var associatedVPC *route53.VPC + for _, vpc := range zone.VPCs { + if (*vpc.VPCID == d.Get("vpc_id")) { + associatedVPC = vpc + } + } + if associatedVPC == nil { + return fmt.Errorf("[DEBUG] VPC: %v is not associated with Zone: %v", d.Get("vpc_id"), d.Id()) + } } // get tags diff --git a/builtin/providers/aws/resource_aws_route53_zone_test.go b/builtin/providers/aws/resource_aws_route53_zone_test.go index 6c59adcd7a..e84cb1b1ef 100644 --- a/builtin/providers/aws/resource_aws_route53_zone_test.go +++ b/builtin/providers/aws/resource_aws_route53_zone_test.go @@ -173,7 +173,7 @@ func testAccCheckRoute53ZoneAssociationExists(n string, zone *route53.GetHostedZ } } if associatedVPC == nil { - return fmt.Errorf("VPC: %v is not associated to Zone: %v") + return fmt.Errorf("VPC: %v is not associated to Zone: %v", n, cleanZoneID(*zone.HostedZone.ID)) } return nil } From 258422621f4588560a46105677a9fb51b5b4e28c Mon Sep 17 00:00:00 2001 From: John Engelman Date: Fri, 8 May 2015 10:18:31 -0500 Subject: [PATCH 05/18] Update documentation and changelog for route53_hosted_zone. --- CHANGELOG.md | 1 + .../source/docs/providers/aws/r/route53_zone.html.markdown | 6 +++++- 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 92f0c9d6c1..78ba3ce660 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -15,6 +15,7 @@ IMPROVEMENTS: * provider/aws: `aws_elasticache_cluster` add support for Tags [GH-1965] * provider/aws: `aws_s3_bucket` exports `hosted_zone_id` and `region` [GH-1865] * provider/aws: `aws_route53_record` exports `fqdn` [GH-1847] + * provider/aws: `aws_route53_hosted_zone` can create private hosted zones [GH-1526] * provider/google: `google_compute_instance` `scratch` attribute added [GH-1920] BUG FIXES: diff --git a/website/source/docs/providers/aws/r/route53_zone.html.markdown b/website/source/docs/providers/aws/r/route53_zone.html.markdown index 9e09606103..665811d08e 100644 --- a/website/source/docs/providers/aws/r/route53_zone.html.markdown +++ b/website/source/docs/providers/aws/r/route53_zone.html.markdown @@ -49,6 +49,10 @@ resource "aws_route53_record" "dev-ns" { } ``` +~> **NOTE:** The `name_servers` set is populated only for public Hosted Zones. +Private Zones will contain any empty set since AWS does not return a `DelegationSet` +for private Hosted Zones. + ## Argument Reference The following arguments are supported: @@ -63,5 +67,5 @@ The following arguments are supported: The following attributes are exported: * `zone_id` - The Hosted Zone ID. This can be referenced by zone records. -* `name_servers` - A list of name servers in a default delegation set. +* `name_servers` - A list of name servers in a default delegation set. Support only for Public Hosted Zones. Find more about delegation sets in [AWS docs](http://docs.aws.amazon.com/Route53/latest/APIReference/actions-on-reusable-delegation-sets.html). From 9c8748d2dc6e0ea0679142e3209a0c230b9032cb Mon Sep 17 00:00:00 2001 From: Panagiotis Moustafellos Date: Wed, 6 May 2015 19:01:19 +0300 Subject: [PATCH 06/18] providers/aws: resource aws_route53_zone_association --- builtin/providers/aws/provider.go | 1 + .../resource_aws_route53_zone_association.go | 120 ++++++++++++++++++ ...ource_aws_route53_zone_association_test.go | 101 +++++++++++++++ 3 files changed, 222 insertions(+) create mode 100644 builtin/providers/aws/resource_aws_route53_zone_association.go create mode 100644 builtin/providers/aws/resource_aws_route53_zone_association_test.go diff --git a/builtin/providers/aws/provider.go b/builtin/providers/aws/provider.go index 095e392ddd..66446a9026 100644 --- a/builtin/providers/aws/provider.go +++ b/builtin/providers/aws/provider.go @@ -115,6 +115,7 @@ func Provider() terraform.ResourceProvider { "aws_network_interface": resourceAwsNetworkInterface(), "aws_proxy_protocol_policy": resourceAwsProxyProtocolPolicy(), "aws_route53_record": resourceAwsRoute53Record(), + "aws_route53_zone_association": resourceAwsRoute53ZoneAssociation(), "aws_route53_zone": resourceAwsRoute53Zone(), "aws_route_table_association": resourceAwsRouteTableAssociation(), "aws_route_table": resourceAwsRouteTable(), diff --git a/builtin/providers/aws/resource_aws_route53_zone_association.go b/builtin/providers/aws/resource_aws_route53_zone_association.go new file mode 100644 index 0000000000..4e1049fcce --- /dev/null +++ b/builtin/providers/aws/resource_aws_route53_zone_association.go @@ -0,0 +1,120 @@ +package aws + +import ( + "log" + + "github.com/hashicorp/terraform/helper/schema" + + "github.com/awslabs/aws-sdk-go/aws" + "github.com/awslabs/aws-sdk-go/service/route53" +) + +func resourceAwsRoute53ZoneAssociation() *schema.Resource { + return &schema.Resource{ + Create: resourceAwsRoute53ZoneAssociationCreate, + Read: resourceAwsRoute53ZoneAssociationRead, + Update: resourceAwsRoute53ZoneAssociationUpdate, + Delete: resourceAwsRoute53ZoneAssociationDelete, + + Schema: map[string]*schema.Schema{ + "zone_id": &schema.Schema{ + Type: schema.TypeString, + Required: true, + }, + + "vpc_id": &schema.Schema{ + Type: schema.TypeString, + Required: true, + }, + + "region": &schema.Schema{ + Type: schema.TypeString, + Required: true, + }, + + "association_id": &schema.Schema{ + Type: schema.TypeString, + Computed: true, + }, + }, + } +} + +func resourceAwsRoute53ZoneAssociationCreate(d *schema.ResourceData, meta interface{}) error { + r53 := meta.(*AWSClient).r53conn + + req := &route53.AssociateVPCWithHostedZoneInput{ + HostedZoneID: aws.String(d.Get("zone_id").(string)), + VPC: &route53.VPC{ + VPCID: aws.String(d.Get("vpc_id").(string)), + VPCRegion: aws.String(d.Get("region").(string)), + }, + Comment: aws.String("Managed by Terraform"), + } + + log.Printf("[DEBUG] Associating Route53 Private Zone %s with VPC %s", *req.HostedZoneID, *req.VPC.VPCID) + resp, err := r53.AssociateVPCWithHostedZone(req) + if err != nil { + return err + } + + // Store association id + association_id := *resp.ChangeInfo.ID + d.Set("association_id", association_id) + d.SetId(association_id) + + return resourceAwsRoute53ZoneAssociationUpdate(d, meta) +} + +func resourceAwsRoute53ZoneAssociationRead(d *schema.ResourceData, meta interface{}) error { + r53 := meta.(*AWSClient).r53conn + zone, err := r53.GetHostedZone(&route53.GetHostedZoneInput{ID: aws.String(d.Id())}) + if err != nil { + // Handle a deleted zone + if r53err, ok := err.(aws.APIError); ok && r53err.Code == "NoSuchHostedZone" { + d.SetId("") + return nil + } + return err + } + + vpc_id := d.Get("vpc_id") + + for i := range zone.VPCs { + if vpc_id == *zone.VPCs[i].VPCID { + // association is there, return + return nil + } + } + + // no association found + d.SetId("") + return nil +} + +func resourceAwsRoute53ZoneAssociationUpdate(d *schema.ResourceData, meta interface{}) error { + return resourceAwsRoute53ZoneAssociationRead(d, meta) +} + +func resourceAwsRoute53ZoneAssociationDelete(d *schema.ResourceData, meta interface{}) error { + r53 := meta.(*AWSClient).r53conn + + log.Printf("[DEBUG] Deleting Route53 Private Zone (%s) association (ID: %s)", + d.Get("zone_id").(string), d.Id()) + + req := &route53.DisassociateVPCFromHostedZoneInput{ + HostedZoneID: aws.String(d.Get("zone_id").(string)), + VPC: &route53.VPC{ + VPCID: aws.String(d.Get("vpc_id").(string)), + VPCRegion: aws.String(d.Get("region").(string)), + }, + Comment: aws.String("Managed by Terraform"), + } + + _, err := r53.DisassociateVPCFromHostedZone(req) + if err != nil { + return err + } + + return nil +} diff --git a/builtin/providers/aws/resource_aws_route53_zone_association_test.go b/builtin/providers/aws/resource_aws_route53_zone_association_test.go new file mode 100644 index 0000000000..f9f1d8ff10 --- /dev/null +++ b/builtin/providers/aws/resource_aws_route53_zone_association_test.go @@ -0,0 +1,101 @@ +package aws + +import ( + "fmt" + "testing" + + "github.com/hashicorp/terraform/helper/resource" + "github.com/hashicorp/terraform/terraform" + + "github.com/awslabs/aws-sdk-go/aws" + "github.com/awslabs/aws-sdk-go/service/route53" +) + +func TestAccRoute53ZoneAssociation(t *testing.T) { + var zone route53.HostedZone + + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + Providers: testAccProviders, + CheckDestroy: testAccCheckRoute53ZoneAssociationDestroy, + Steps: []resource.TestStep{ + resource.TestStep{ + Config: testAccRoute53ZoneAssociationConfig, + Check: resource.ComposeTestCheckFunc( + testAccCheckRoute53ZoneAssociationExists("aws_route53_zone_association.main", &zone), + ), + }, + }, + }) +} + +func testAccCheckRoute53ZoneAssociationDestroy(s *terraform.State) error { + conn := testAccProvider.Meta().(*AWSClient).r53conn + for _, rs := range s.RootModule().Resources { + if rs.Type != "aws_route53_zone" { + continue + } + + _, err := conn.GetHostedZone(&route53.GetHostedZoneInput{ID: aws.String(rs.Primary.ID)}) + if err == nil { + return fmt.Errorf("Hosted zone still exists") + } + } + return nil +} + +func testAccCheckRoute53ZoneAssociationExists(n string, zone *route53.HostedZone) resource.TestCheckFunc { + return func(s *terraform.State) error { + rs, ok := s.RootModule().Resources[n] + if !ok { + return fmt.Errorf("Not found: %s", n) + } + + if rs.Primary.ID == "" { + return fmt.Errorf("No hosted zone ID is set") + } + + conn := testAccProvider.Meta().(*AWSClient).r53conn + resp, err := conn.GetHostedZone(&route53.GetHostedZoneInput{ID: aws.String(rs.Primary.ID)}) + if err != nil { + return fmt.Errorf("Hosted zone err: %v", err) + } + + exists := false + for i := range resp.VPCs { + if rs.Primary.Meta["vpc_id"] == *resp.VPCs[i].VPCID { + exists = true + } + } + if !exists { + return fmt.Errorf("Hosted zone association not found") + } + + *zone = *resp.HostedZone + return nil + } +} + +const testAccRoute53ZoneAssociationConfig = ` +resource "aws_vpc" "mosakos" { + cidr_block = "10.6.0.0/16" + + enable_dns_hostnames = true + enable_dns_support = true +} + +resource "aws_route53_zone" "main" { + name = "mosakos.com" + + tags { + foo = "bar" + Name = "tf-route53-tag-test" + } +} + +resource "aws_route53_zone_association" "main" { + vpc_id = "${aws_vpc.mosakos.id}" + zone_id = "${aws_route53_zone.main.id}" + region = "us-west-2" +} +` From 450c42f166935acd1ebdc39d5c489c1d0439199f Mon Sep 17 00:00:00 2001 From: Panagiotis Moustafellos Date: Wed, 6 May 2015 19:30:48 +0300 Subject: [PATCH 07/18] keep clean changeinfo as res id --- builtin/providers/aws/resource_aws_route53_zone_association.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/builtin/providers/aws/resource_aws_route53_zone_association.go b/builtin/providers/aws/resource_aws_route53_zone_association.go index 4e1049fcce..ab8b5ad76b 100644 --- a/builtin/providers/aws/resource_aws_route53_zone_association.go +++ b/builtin/providers/aws/resource_aws_route53_zone_association.go @@ -59,7 +59,7 @@ func resourceAwsRoute53ZoneAssociationCreate(d *schema.ResourceData, meta interf } // Store association id - association_id := *resp.ChangeInfo.ID + association_id := cleanChangeID(*resp.ChangeInfo.ID) d.Set("association_id", association_id) d.SetId(association_id) From d02e247fc7f15b00ce3483ccdec22c06d4c29212 Mon Sep 17 00:00:00 2001 From: Panagiotis Moustafellos Date: Fri, 8 May 2015 10:16:24 +0300 Subject: [PATCH 08/18] renamed region to vpc_region for clarity and made optional, updated tests --- .../resource_aws_route53_zone_association.go | 14 +++++++--- ...ource_aws_route53_zone_association_test.go | 26 +++++++++---------- 2 files changed, 23 insertions(+), 17 deletions(-) diff --git a/builtin/providers/aws/resource_aws_route53_zone_association.go b/builtin/providers/aws/resource_aws_route53_zone_association.go index ab8b5ad76b..084246b75a 100644 --- a/builtin/providers/aws/resource_aws_route53_zone_association.go +++ b/builtin/providers/aws/resource_aws_route53_zone_association.go @@ -27,9 +27,9 @@ func resourceAwsRoute53ZoneAssociation() *schema.Resource { Required: true, }, - "region": &schema.Schema{ + "vpc_region": &schema.Schema{ Type: schema.TypeString, - Required: true, + Optional: true, }, "association_id": &schema.Schema{ @@ -47,10 +47,13 @@ func resourceAwsRoute53ZoneAssociationCreate(d *schema.ResourceData, meta interf HostedZoneID: aws.String(d.Get("zone_id").(string)), VPC: &route53.VPC{ VPCID: aws.String(d.Get("vpc_id").(string)), - VPCRegion: aws.String(d.Get("region").(string)), + VPCRegion: aws.String(meta.(*AWSClient).region), }, Comment: aws.String("Managed by Terraform"), } + if w := d.Get("vpc_region"); w != nil { + req.VPC.VPCRegion = aws.String(w.(string)) + } log.Printf("[DEBUG] Associating Route53 Private Zone %s with VPC %s", *req.HostedZoneID, *req.VPC.VPCID) resp, err := r53.AssociateVPCWithHostedZone(req) @@ -106,10 +109,13 @@ func resourceAwsRoute53ZoneAssociationDelete(d *schema.ResourceData, meta interf HostedZoneID: aws.String(d.Get("zone_id").(string)), VPC: &route53.VPC{ VPCID: aws.String(d.Get("vpc_id").(string)), - VPCRegion: aws.String(d.Get("region").(string)), + VPCRegion: aws.String(meta.(*AWSClient).region), }, Comment: aws.String("Managed by Terraform"), } + if w := d.Get("vpc_region"); w != nil { + req.VPC.VPCRegion = aws.String(w.(string)) + } _, err := r53.DisassociateVPCFromHostedZone(req) if err != nil { diff --git a/builtin/providers/aws/resource_aws_route53_zone_association_test.go b/builtin/providers/aws/resource_aws_route53_zone_association_test.go index f9f1d8ff10..691e2cb926 100644 --- a/builtin/providers/aws/resource_aws_route53_zone_association_test.go +++ b/builtin/providers/aws/resource_aws_route53_zone_association_test.go @@ -77,25 +77,25 @@ func testAccCheckRoute53ZoneAssociationExists(n string, zone *route53.HostedZone } const testAccRoute53ZoneAssociationConfig = ` -resource "aws_vpc" "mosakos" { +resource "aws_vpc" "foo" { cidr_block = "10.6.0.0/16" - enable_dns_hostnames = true enable_dns_support = true } -resource "aws_route53_zone" "main" { - name = "mosakos.com" - - tags { - foo = "bar" - Name = "tf-route53-tag-test" - } +resource "aws_vpc" "bar" { + cidr_block = "10.7.0.0/16" + enable_dns_hostnames = true + enable_dns_support = true } -resource "aws_route53_zone_association" "main" { - vpc_id = "${aws_vpc.mosakos.id}" - zone_id = "${aws_route53_zone.main.id}" - region = "us-west-2" +resource "aws_route53_zone" "foo" { + name = "foo.com" + vpc_id = "${aws_vpc.foo.id}" +} + +resource "aws_route53_zone_association" "foobar" { + zone_id = "${aws_route53_zone.foo.id}" + vpc_id = "${aws_vpc.bar.id}" } ` From 9da89974fccf1c5dfbbb5e642fdc666a8c8b859c Mon Sep 17 00:00:00 2001 From: Panagiotis Moustafellos Date: Fri, 8 May 2015 15:40:53 +0300 Subject: [PATCH 09/18] fix vpc_region param check --- .../providers/aws/resource_aws_route53_zone_association.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/builtin/providers/aws/resource_aws_route53_zone_association.go b/builtin/providers/aws/resource_aws_route53_zone_association.go index 084246b75a..eb23083201 100644 --- a/builtin/providers/aws/resource_aws_route53_zone_association.go +++ b/builtin/providers/aws/resource_aws_route53_zone_association.go @@ -51,7 +51,7 @@ func resourceAwsRoute53ZoneAssociationCreate(d *schema.ResourceData, meta interf }, Comment: aws.String("Managed by Terraform"), } - if w := d.Get("vpc_region"); w != nil { + if w := d.Get("vpc_region"); w != "" { req.VPC.VPCRegion = aws.String(w.(string)) } @@ -113,7 +113,7 @@ func resourceAwsRoute53ZoneAssociationDelete(d *schema.ResourceData, meta interf }, Comment: aws.String("Managed by Terraform"), } - if w := d.Get("vpc_region"); w != nil { + if w := d.Get("vpc_region"); w != "" { req.VPC.VPCRegion = aws.String(w.(string)) } From 3507c0618beed2438435013b9939cc5c39a24800 Mon Sep 17 00:00:00 2001 From: John Engelman Date: Fri, 8 May 2015 10:59:31 -0500 Subject: [PATCH 10/18] cleanup of zone_association resource --- .../resource_aws_route53_zone_association.go | 63 ++++++++++++------- ...ource_aws_route53_zone_association_test.go | 4 +- .../aws/resource_aws_route53_zone_test.go | 4 +- 3 files changed, 46 insertions(+), 25 deletions(-) diff --git a/builtin/providers/aws/resource_aws_route53_zone_association.go b/builtin/providers/aws/resource_aws_route53_zone_association.go index eb23083201..9b174985c1 100644 --- a/builtin/providers/aws/resource_aws_route53_zone_association.go +++ b/builtin/providers/aws/resource_aws_route53_zone_association.go @@ -1,8 +1,12 @@ package aws import ( + "fmt" "log" + "strings" + "time" + "github.com/hashicorp/terraform/helper/resource" "github.com/hashicorp/terraform/helper/schema" "github.com/awslabs/aws-sdk-go/aws" @@ -32,10 +36,6 @@ func resourceAwsRoute53ZoneAssociation() *schema.Resource { Optional: true, }, - "association_id": &schema.Schema{ - Type: schema.TypeString, - Computed: true, - }, }, } } @@ -62,16 +62,35 @@ func resourceAwsRoute53ZoneAssociationCreate(d *schema.ResourceData, meta interf } // Store association id - association_id := cleanChangeID(*resp.ChangeInfo.ID) - d.Set("association_id", association_id) - d.SetId(association_id) + d.SetId(fmt.Sprintf("%s:%s", *req.HostedZoneID, *req.VPC.VPCID)) + d.Set("vpc_region", req.VPC.VPCRegion) + + // Wait until we are done initializing + wait := resource.StateChangeConf{ + Delay: 30 * time.Second, + Pending: []string{"PENDING"}, + Target: "INSYNC", + Timeout: 10 * time.Minute, + MinTimeout: 2 * time.Second, + Refresh: func() (result interface{}, state string, err error) { + changeRequest := &route53.GetChangeInput{ + ID: aws.String(cleanChangeID(*resp.ChangeInfo.ID)), + } + return resourceAwsGoRoute53Wait(r53, changeRequest) + }, + } + _, err = wait.WaitForState() + if err != nil { + return err + } return resourceAwsRoute53ZoneAssociationUpdate(d, meta) } func resourceAwsRoute53ZoneAssociationRead(d *schema.ResourceData, meta interface{}) error { r53 := meta.(*AWSClient).r53conn - zone, err := r53.GetHostedZone(&route53.GetHostedZoneInput{ID: aws.String(d.Id())}) + zone_id, vpc_id := resourceAwsRoute53ZoneAssociationParseId(d.Id()) + zone, err := r53.GetHostedZone(&route53.GetHostedZoneInput{ID: aws.String(zone_id)}) if err != nil { // Handle a deleted zone if r53err, ok := err.(aws.APIError); ok && r53err.Code == "NoSuchHostedZone" { @@ -81,10 +100,8 @@ func resourceAwsRoute53ZoneAssociationRead(d *schema.ResourceData, meta interfac return err } - vpc_id := d.Get("vpc_id") - - for i := range zone.VPCs { - if vpc_id == *zone.VPCs[i].VPCID { + for _, vpc := range zone.VPCs { + if vpc_id == *vpc.VPCID { // association is there, return return nil } @@ -101,21 +118,18 @@ func resourceAwsRoute53ZoneAssociationUpdate(d *schema.ResourceData, meta interf func resourceAwsRoute53ZoneAssociationDelete(d *schema.ResourceData, meta interface{}) error { r53 := meta.(*AWSClient).r53conn - - log.Printf("[DEBUG] Deleting Route53 Private Zone (%s) association (ID: %s)", - d.Get("zone_id").(string), d.Id()) + zone_id, vpc_id := resourceAwsRoute53ZoneAssociationParseId(d.Id()) + log.Printf("[DEBUG] Deleting Route53 Private Zone (%s) association (VPC: %s)", + zone_id, vpc_id) req := &route53.DisassociateVPCFromHostedZoneInput{ - HostedZoneID: aws.String(d.Get("zone_id").(string)), + HostedZoneID: aws.String(zone_id), VPC: &route53.VPC{ - VPCID: aws.String(d.Get("vpc_id").(string)), - VPCRegion: aws.String(meta.(*AWSClient).region), + VPCID: aws.String(vpc_id), + VPCRegion: aws.String(d.Get("vpc_region").(string)), }, Comment: aws.String("Managed by Terraform"), } - if w := d.Get("vpc_region"); w != "" { - req.VPC.VPCRegion = aws.String(w.(string)) - } _, err := r53.DisassociateVPCFromHostedZone(req) if err != nil { @@ -124,3 +138,10 @@ func resourceAwsRoute53ZoneAssociationDelete(d *schema.ResourceData, meta interf return nil } + +func resourceAwsRoute53ZoneAssociationParseId(id string) (zone_id, vpc_id string) { + parts := strings.SplitN(id, ":", 2) + zone_id = parts[0] + vpc_id = parts[1] + return +} diff --git a/builtin/providers/aws/resource_aws_route53_zone_association_test.go b/builtin/providers/aws/resource_aws_route53_zone_association_test.go index 691e2cb926..4addc53577 100644 --- a/builtin/providers/aws/resource_aws_route53_zone_association_test.go +++ b/builtin/providers/aws/resource_aws_route53_zone_association_test.go @@ -62,8 +62,8 @@ func testAccCheckRoute53ZoneAssociationExists(n string, zone *route53.HostedZone } exists := false - for i := range resp.VPCs { - if rs.Primary.Meta["vpc_id"] == *resp.VPCs[i].VPCID { + for _, vpc := range resp.VPCs { + if rs.Primary.Meta["vpc_id"] == *vpc.VPCID { exists = true } } diff --git a/builtin/providers/aws/resource_aws_route53_zone_test.go b/builtin/providers/aws/resource_aws_route53_zone_test.go index e84cb1b1ef..ecb285374e 100644 --- a/builtin/providers/aws/resource_aws_route53_zone_test.go +++ b/builtin/providers/aws/resource_aws_route53_zone_test.go @@ -96,7 +96,7 @@ func TestAccRoute53PrivateZone(t *testing.T) { Config: testAccRoute53PrivateZoneConfig, Check: resource.ComposeTestCheckFunc( testAccCheckRoute53ZoneExists("aws_route53_zone.main", &zone), - testAccCheckRoute53ZoneAssociationExists("aws_vpc.main", &zone), + testAccCheckRoute53ZoneAssociatesWithVpc("aws_vpc.main", &zone), ), }, }, @@ -155,7 +155,7 @@ func testAccCheckRoute53ZoneExists(n string, zone *route53.GetHostedZoneOutput) } } -func testAccCheckRoute53ZoneAssociationExists(n string, zone *route53.GetHostedZoneOutput) resource.TestCheckFunc { +func testAccCheckRoute53ZoneAssociatesWithVpc(n string, zone *route53.GetHostedZoneOutput) resource.TestCheckFunc { return func(s *terraform.State) error { rs, ok := s.RootModule().Resources[n] if !ok { From 766aead4a540d4236058ce0058266d8db39886ac Mon Sep 17 00:00:00 2001 From: John Engelman Date: Fri, 8 May 2015 14:47:42 -0500 Subject: [PATCH 11/18] Add tests for cross region VPC associations. --- .../aws/resource_aws_route53_zone.go | 2 + .../resource_aws_route53_zone_association.go | 3 +- ...ource_aws_route53_zone_association_test.go | 175 +++++++++++++++--- .../aws/resource_aws_route53_zone_test.go | 150 ++++++++++++--- 4 files changed, 272 insertions(+), 58 deletions(-) diff --git a/builtin/providers/aws/resource_aws_route53_zone.go b/builtin/providers/aws/resource_aws_route53_zone.go index f9ad99f4c4..fd74ed1670 100644 --- a/builtin/providers/aws/resource_aws_route53_zone.go +++ b/builtin/providers/aws/resource_aws_route53_zone.go @@ -38,6 +38,7 @@ func resourceAwsRoute53Zone() *schema.Resource { Type: schema.TypeString, Optional: true, ForceNew: true, + Computed: true, }, "zone_id": &schema.Schema{ @@ -85,6 +86,7 @@ func resourceAwsRoute53ZoneCreate(d *schema.ResourceData, meta interface{}) erro zone := cleanZoneID(*resp.HostedZone.ID) d.Set("zone_id", zone) d.SetId(zone) + d.Set("vpc_region", req.VPC.VPCRegion) // Wait until we are done initializing wait := resource.StateChangeConf{ diff --git a/builtin/providers/aws/resource_aws_route53_zone_association.go b/builtin/providers/aws/resource_aws_route53_zone_association.go index 9b174985c1..6d53d937f6 100644 --- a/builtin/providers/aws/resource_aws_route53_zone_association.go +++ b/builtin/providers/aws/resource_aws_route53_zone_association.go @@ -34,6 +34,7 @@ func resourceAwsRoute53ZoneAssociation() *schema.Resource { "vpc_region": &schema.Schema{ Type: schema.TypeString, Optional: true, + Computed: true, }, }, @@ -55,7 +56,7 @@ func resourceAwsRoute53ZoneAssociationCreate(d *schema.ResourceData, meta interf req.VPC.VPCRegion = aws.String(w.(string)) } - log.Printf("[DEBUG] Associating Route53 Private Zone %s with VPC %s", *req.HostedZoneID, *req.VPC.VPCID) + log.Printf("[DEBUG] Associating Route53 Private Zone %s with VPC %s with region %s", *req.HostedZoneID, *req.VPC.VPCID, *req.VPC.VPCRegion) resp, err := r53.AssociateVPCWithHostedZone(req) if err != nil { return err diff --git a/builtin/providers/aws/resource_aws_route53_zone_association_test.go b/builtin/providers/aws/resource_aws_route53_zone_association_test.go index 4addc53577..d8389e35df 100644 --- a/builtin/providers/aws/resource_aws_route53_zone_association_test.go +++ b/builtin/providers/aws/resource_aws_route53_zone_association_test.go @@ -5,6 +5,7 @@ import ( "testing" "github.com/hashicorp/terraform/helper/resource" + "github.com/hashicorp/terraform/helper/schema" "github.com/hashicorp/terraform/terraform" "github.com/awslabs/aws-sdk-go/aws" @@ -22,7 +23,36 @@ func TestAccRoute53ZoneAssociation(t *testing.T) { resource.TestStep{ Config: testAccRoute53ZoneAssociationConfig, Check: resource.ComposeTestCheckFunc( - testAccCheckRoute53ZoneAssociationExists("aws_route53_zone_association.main", &zone), + testAccCheckRoute53ZoneAssociationExists("aws_route53_zone_association.foobar", &zone), + ), + }, + }, + }) +} + +func TestAccRoute53ZoneAssociationWithRegion(t *testing.T) { + var zone route53.HostedZone + + // record the initialized providers so that we can use them to + // check for the instances in each region + var providers []*schema.Provider + providerFactories := map[string]terraform.ResourceProviderFactory{ + "aws": func() (terraform.ResourceProvider, error) { + p := Provider() + providers = append(providers, p.(*schema.Provider)) + return p, nil + }, + } + + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + ProviderFactories: providerFactories, + CheckDestroy: testAccCheckRoute53ZoneAssociationDestroyWithProviders(&providers), + Steps: []resource.TestStep{ + resource.TestStep{ + Config: testAccRoute53ZoneAssociationRegionConfig, + Check: resource.ComposeTestCheckFunc( + testAccCheckRoute53ZoneAssociationExistsWithProviders("aws_route53_zone_association.foobar", &zone, &providers), ), }, }, @@ -30,15 +60,43 @@ func TestAccRoute53ZoneAssociation(t *testing.T) { } func testAccCheckRoute53ZoneAssociationDestroy(s *terraform.State) error { - conn := testAccProvider.Meta().(*AWSClient).r53conn + return testAccCheckRoute53ZoneAssociationDestroyWithProvider(s, testAccProvider) +} + +func testAccCheckRoute53ZoneAssociationDestroyWithProviders(providers *[]*schema.Provider) resource.TestCheckFunc { + return func(s *terraform.State) error { + for _, provider := range *providers { + if provider.Meta() == nil { + continue + } + if err := testAccCheckRoute53ZoneAssociationDestroyWithProvider(s, provider); err != nil { + return err + } + } + return nil + } +} + +func testAccCheckRoute53ZoneAssociationDestroyWithProvider(s *terraform.State, provider *schema.Provider) error { + conn := provider.Meta().(*AWSClient).r53conn for _, rs := range s.RootModule().Resources { - if rs.Type != "aws_route53_zone" { + if rs.Type != "aws_route53_zone_association" { continue } - _, err := conn.GetHostedZone(&route53.GetHostedZoneInput{ID: aws.String(rs.Primary.ID)}) - if err == nil { - return fmt.Errorf("Hosted zone still exists") + zone_id, vpc_id := resourceAwsRoute53ZoneAssociationParseId(rs.Primary.ID) + + resp, err := conn.GetHostedZone(&route53.GetHostedZoneInput{ID: aws.String(zone_id)}) + if err != nil { + exists := false + for _, vpc := range resp.VPCs { + if vpc_id == *vpc.VPCID { + exists = true + } + } + if exists { + return fmt.Errorf("VPC: %v is still associated to HostedZone: %v", vpc_id, zone_id) + } } } return nil @@ -46,36 +104,56 @@ func testAccCheckRoute53ZoneAssociationDestroy(s *terraform.State) error { func testAccCheckRoute53ZoneAssociationExists(n string, zone *route53.HostedZone) resource.TestCheckFunc { return func(s *terraform.State) error { - rs, ok := s.RootModule().Resources[n] - if !ok { - return fmt.Errorf("Not found: %s", n) - } + return testAccCheckRoute53ZoneAssociationExistsWithProvider(s, n, zone, testAccProvider) + } +} - if rs.Primary.ID == "" { - return fmt.Errorf("No hosted zone ID is set") - } - - conn := testAccProvider.Meta().(*AWSClient).r53conn - resp, err := conn.GetHostedZone(&route53.GetHostedZoneInput{ID: aws.String(rs.Primary.ID)}) - if err != nil { - return fmt.Errorf("Hosted zone err: %v", err) - } - - exists := false - for _, vpc := range resp.VPCs { - if rs.Primary.Meta["vpc_id"] == *vpc.VPCID { - exists = true +func testAccCheckRoute53ZoneAssociationExistsWithProviders(n string, zone *route53.HostedZone, providers *[]*schema.Provider) resource.TestCheckFunc { + return func(s *terraform.State) error { + for _, provider := range *providers { + if provider.Meta() == nil { + continue + } + if err := testAccCheckRoute53ZoneAssociationExistsWithProvider(s, n, zone, provider); err != nil { + return err } } - if !exists { - return fmt.Errorf("Hosted zone association not found") - } - - *zone = *resp.HostedZone return nil } } +func testAccCheckRoute53ZoneAssociationExistsWithProvider(s *terraform.State, n string, zone *route53.HostedZone, provider *schema.Provider) error { + rs, ok := s.RootModule().Resources[n] + if !ok { + return fmt.Errorf("Not found: %s", n) + } + + if rs.Primary.ID == "" { + return fmt.Errorf("No zone association ID is set") + } + + zone_id, vpc_id := resourceAwsRoute53ZoneAssociationParseId(rs.Primary.ID) + + conn := provider.Meta().(*AWSClient).r53conn + resp, err := conn.GetHostedZone(&route53.GetHostedZoneInput{ID: aws.String(zone_id)}) + if err != nil { + return fmt.Errorf("Hosted zone err: %v", err) + } + + exists := false + for _, vpc := range resp.VPCs { + if vpc_id == *vpc.VPCID { + exists = true + } + } + if !exists { + return fmt.Errorf("Hosted zone association not found") + } + + *zone = *resp.HostedZone + return nil +} + const testAccRoute53ZoneAssociationConfig = ` resource "aws_vpc" "foo" { cidr_block = "10.6.0.0/16" @@ -99,3 +177,42 @@ resource "aws_route53_zone_association" "foobar" { vpc_id = "${aws_vpc.bar.id}" } ` + +const testAccRoute53ZoneAssociationRegionConfig = ` +provider "aws" { + alias = "west" + region = "us-west-2" +} + +provider "aws" { + alias = "east" + region = "us-east-1" +} + +resource "aws_vpc" "foo" { + provider = "aws.west" + cidr_block = "10.6.0.0/16" + enable_dns_hostnames = true + enable_dns_support = true +} + +resource "aws_vpc" "bar" { + provider = "aws.east" + cidr_block = "10.7.0.0/16" + enable_dns_hostnames = true + enable_dns_support = true +} + +resource "aws_route53_zone" "foo" { + provider = "aws.west" + name = "foo.com" + vpc_id = "${aws_vpc.foo.id}" +} + +resource "aws_route53_zone_association" "foobar" { + provider = "aws.west" + zone_id = "${aws_route53_zone.foo.id}" + vpc_id = "${aws_vpc.bar.id}" + vpc_region = "us-east-1" +} +` diff --git a/builtin/providers/aws/resource_aws_route53_zone_test.go b/builtin/providers/aws/resource_aws_route53_zone_test.go index ecb285374e..1ca3927ecb 100644 --- a/builtin/providers/aws/resource_aws_route53_zone_test.go +++ b/builtin/providers/aws/resource_aws_route53_zone_test.go @@ -6,6 +6,7 @@ import ( "testing" "github.com/hashicorp/terraform/helper/resource" + "github.com/hashicorp/terraform/helper/schema" "github.com/hashicorp/terraform/terraform" "github.com/awslabs/aws-sdk-go/aws" @@ -103,8 +104,56 @@ func TestAccRoute53PrivateZone(t *testing.T) { }) } +func TestAccRoute53PrivateZoneWithRegion(t *testing.T) { + var zone route53.GetHostedZoneOutput + + // record the initialized providers so that we can use them to + // check for the instances in each region + var providers []*schema.Provider + providerFactories := map[string]terraform.ResourceProviderFactory{ + "aws": func() (terraform.ResourceProvider, error) { + p := Provider() + providers = append(providers, p.(*schema.Provider)) + return p, nil + }, + } + + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + ProviderFactories: providerFactories, + CheckDestroy: testAccCheckRoute53ZoneDestroyWithProviders(&providers), + Steps: []resource.TestStep{ + resource.TestStep{ + Config: testAccRoute53PrivateZoneRegionConfig, + Check: resource.ComposeTestCheckFunc( + testAccCheckRoute53ZoneExistsWithProviders("aws_route53_zone.main", &zone, &providers), + testAccCheckRoute53ZoneAssociatesWithVpc("aws_vpc.main", &zone), + ), + }, + }, + }) +} + func testAccCheckRoute53ZoneDestroy(s *terraform.State) error { - conn := testAccProvider.Meta().(*AWSClient).r53conn + return testAccCheckRoute53ZoneDestroyWithProvider(s, testAccProvider) +} + +func testAccCheckRoute53ZoneDestroyWithProviders(providers *[]*schema.Provider) resource.TestCheckFunc { + return func(s *terraform.State) error { + for _, provider := range *providers { + if provider.Meta() == nil { + continue + } + if err := testAccCheckRoute53ZoneDestroyWithProvider(s, provider); err != nil { + return err + } + } + return nil + } +} + +func testAccCheckRoute53ZoneDestroyWithProvider(s *terraform.State, provider *schema.Provider) error { + conn := provider.Meta().(*AWSClient).r53conn for _, rs := range s.RootModule().Resources { if rs.Type != "aws_route53_zone" { continue @@ -120,41 +169,59 @@ func testAccCheckRoute53ZoneDestroy(s *terraform.State) error { func testAccCheckRoute53ZoneExists(n string, zone *route53.GetHostedZoneOutput) resource.TestCheckFunc { return func(s *terraform.State) error { - rs, ok := s.RootModule().Resources[n] - if !ok { - return fmt.Errorf("Not found: %s", n) - } + return testAccCheckRoute53ZoneExistsWithProvider(s, n, zone, testAccProvider) + } +} - if rs.Primary.ID == "" { - return fmt.Errorf("No hosted zone ID is set") - } - - conn := testAccProvider.Meta().(*AWSClient).r53conn - resp, err := conn.GetHostedZone(&route53.GetHostedZoneInput{ID: aws.String(rs.Primary.ID)}) - if err != nil { - return fmt.Errorf("Hosted zone err: %v", err) - } - - if ! *resp.HostedZone.Config.PrivateZone { - sorted_ns := make([]string, len(resp.DelegationSet.NameServers)) - for i, ns := range resp.DelegationSet.NameServers { - sorted_ns[i] = *ns +func testAccCheckRoute53ZoneExistsWithProviders(n string, zone *route53.GetHostedZoneOutput, providers *[]*schema.Provider) resource.TestCheckFunc { + return func(s *terraform.State) error { + for _, provider := range *providers { + if provider.Meta() == nil { + continue } - sort.Strings(sorted_ns) - for idx, ns := range sorted_ns { - attribute := fmt.Sprintf("name_servers.%d", idx) - dsns := rs.Primary.Attributes[attribute] - if dsns != ns { - return fmt.Errorf("Got: %v for %v, Expected: %v", dsns, attribute, ns) - } + if err := testAccCheckRoute53ZoneExistsWithProvider(s, n, zone, provider); err != nil { + return err } } - - *zone = *resp return nil } } +func testAccCheckRoute53ZoneExistsWithProvider(s *terraform.State, n string, zone *route53.GetHostedZoneOutput, provider *schema.Provider) error { + rs, ok := s.RootModule().Resources[n] + if !ok { + return fmt.Errorf("Not found: %s", n) + } + + if rs.Primary.ID == "" { + return fmt.Errorf("No hosted zone ID is set") + } + + conn := provider.Meta().(*AWSClient).r53conn + resp, err := conn.GetHostedZone(&route53.GetHostedZoneInput{ID: aws.String(rs.Primary.ID)}) + if err != nil { + return fmt.Errorf("Hosted zone err: %v", err) + } + + if ! *resp.HostedZone.Config.PrivateZone { + sorted_ns := make([]string, len(resp.DelegationSet.NameServers)) + for i, ns := range resp.DelegationSet.NameServers { + sorted_ns[i] = *ns + } + sort.Strings(sorted_ns) + for idx, ns := range sorted_ns { + attribute := fmt.Sprintf("name_servers.%d", idx) + dsns := rs.Primary.Attributes[attribute] + if dsns != ns { + return fmt.Errorf("Got: %v for %v, Expected: %v", dsns, attribute, ns) + } + } + } + + *zone = *resp + return nil +} + func testAccCheckRoute53ZoneAssociatesWithVpc(n string, zone *route53.GetHostedZoneOutput) resource.TestCheckFunc { return func(s *terraform.State) error { rs, ok := s.RootModule().Resources[n] @@ -226,3 +293,30 @@ resource "aws_route53_zone" "main" { vpc_id = "${aws_vpc.main.id}" } ` + +const testAccRoute53PrivateZoneRegionConfig = ` +provider "aws" { + alias = "west" + region = "us-west-2" +} + +provider "aws" { + alias = "east" + region = "us-east-1" +} + +resource "aws_vpc" "main" { + provider = "aws.east" + cidr_block = "172.29.0.0/24" + instance_tenancy = "default" + enable_dns_support = true + enable_dns_hostnames = true +} + +resource "aws_route53_zone" "main" { + provider = "aws.west" + name = "hashicorp.com" + vpc_id = "${aws_vpc.main.id}" + vpc_region = "us-east-1" +} +` From b78490849115bd4fcf330f2168cd3339f85cf886 Mon Sep 17 00:00:00 2001 From: John Engelman Date: Fri, 8 May 2015 22:21:22 -0500 Subject: [PATCH 12/18] Add documentation and changelog for route53_zone_association --- CHANGELOG.md | 1 + .../aws/resource_aws_route53_zone.go | 6 +-- .../resource_aws_route53_zone_association.go | 1 - ...ource_aws_route53_zone_association_test.go | 4 +- .../aws/resource_aws_route53_zone_test.go | 6 +-- .../r/route53_zone_association.html.markdown | 54 +++++++++++++++++++ 6 files changed, 63 insertions(+), 9 deletions(-) create mode 100644 website/source/docs/providers/aws/r/route53_zone_association.html.markdown diff --git a/CHANGELOG.md b/CHANGELOG.md index 78ba3ce660..282716e33c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,7 @@ IMPROVEMENTS: * **New config function: `formatlist`** - Format lists in a similar way to `format`. Useful for creating URLs from a list of IPs. [GH-1829] + * **New resource: `aws_route53_zone_association`** * provider/aws: `aws_autoscaling_group` can wait for capacity in ELB via `min_elb_capacity` [GH-1970] * provider/aws: `aws_db_instances` supports `license_model` [GH-1966] diff --git a/builtin/providers/aws/resource_aws_route53_zone.go b/builtin/providers/aws/resource_aws_route53_zone.go index fd74ed1670..7711a187d2 100644 --- a/builtin/providers/aws/resource_aws_route53_zone.go +++ b/builtin/providers/aws/resource_aws_route53_zone.go @@ -121,7 +121,7 @@ func resourceAwsRoute53ZoneRead(d *schema.ResourceData, meta interface{}) error return err } - if ! *zone.HostedZone.Config.PrivateZone { + if !*zone.HostedZone.Config.PrivateZone { ns := make([]string, len(zone.DelegationSet.NameServers)) for i := range zone.DelegationSet.NameServers { ns[i] = *zone.DelegationSet.NameServers[i] @@ -131,10 +131,10 @@ func resourceAwsRoute53ZoneRead(d *schema.ResourceData, meta interface{}) error return fmt.Errorf("[DEBUG] Error setting name servers for: %s, error: %#v", d.Id(), err) } } else { - d.Set("name_servers", nil); + d.Set("name_servers", nil) var associatedVPC *route53.VPC for _, vpc := range zone.VPCs { - if (*vpc.VPCID == d.Get("vpc_id")) { + if *vpc.VPCID == d.Get("vpc_id") { associatedVPC = vpc } } diff --git a/builtin/providers/aws/resource_aws_route53_zone_association.go b/builtin/providers/aws/resource_aws_route53_zone_association.go index 6d53d937f6..d2fc2a2c4b 100644 --- a/builtin/providers/aws/resource_aws_route53_zone_association.go +++ b/builtin/providers/aws/resource_aws_route53_zone_association.go @@ -36,7 +36,6 @@ func resourceAwsRoute53ZoneAssociation() *schema.Resource { Optional: true, Computed: true, }, - }, } } diff --git a/builtin/providers/aws/resource_aws_route53_zone_association_test.go b/builtin/providers/aws/resource_aws_route53_zone_association_test.go index d8389e35df..d3031739d5 100644 --- a/builtin/providers/aws/resource_aws_route53_zone_association_test.go +++ b/builtin/providers/aws/resource_aws_route53_zone_association_test.go @@ -45,9 +45,9 @@ func TestAccRoute53ZoneAssociationWithRegion(t *testing.T) { } resource.Test(t, resource.TestCase{ - PreCheck: func() { testAccPreCheck(t) }, + PreCheck: func() { testAccPreCheck(t) }, ProviderFactories: providerFactories, - CheckDestroy: testAccCheckRoute53ZoneAssociationDestroyWithProviders(&providers), + CheckDestroy: testAccCheckRoute53ZoneAssociationDestroyWithProviders(&providers), Steps: []resource.TestStep{ resource.TestStep{ Config: testAccRoute53ZoneAssociationRegionConfig, diff --git a/builtin/providers/aws/resource_aws_route53_zone_test.go b/builtin/providers/aws/resource_aws_route53_zone_test.go index 1ca3927ecb..3a2d1279f9 100644 --- a/builtin/providers/aws/resource_aws_route53_zone_test.go +++ b/builtin/providers/aws/resource_aws_route53_zone_test.go @@ -119,9 +119,9 @@ func TestAccRoute53PrivateZoneWithRegion(t *testing.T) { } resource.Test(t, resource.TestCase{ - PreCheck: func() { testAccPreCheck(t) }, + PreCheck: func() { testAccPreCheck(t) }, ProviderFactories: providerFactories, - CheckDestroy: testAccCheckRoute53ZoneDestroyWithProviders(&providers), + CheckDestroy: testAccCheckRoute53ZoneDestroyWithProviders(&providers), Steps: []resource.TestStep{ resource.TestStep{ Config: testAccRoute53PrivateZoneRegionConfig, @@ -203,7 +203,7 @@ func testAccCheckRoute53ZoneExistsWithProvider(s *terraform.State, n string, zon return fmt.Errorf("Hosted zone err: %v", err) } - if ! *resp.HostedZone.Config.PrivateZone { + if !*resp.HostedZone.Config.PrivateZone { sorted_ns := make([]string, len(resp.DelegationSet.NameServers)) for i, ns := range resp.DelegationSet.NameServers { sorted_ns[i] = *ns diff --git a/website/source/docs/providers/aws/r/route53_zone_association.html.markdown b/website/source/docs/providers/aws/r/route53_zone_association.html.markdown new file mode 100644 index 0000000000..49995fb63d --- /dev/null +++ b/website/source/docs/providers/aws/r/route53_zone_association.html.markdown @@ -0,0 +1,54 @@ +--- +layout: "aws" +page_title: "AWS: aws_route53_zone_association" +sidebar_current: "docs-aws-resource-route53-zone-association" +description: |- + Provides a Route53 private Hosted Zone to VPC association resource. +--- + +# aws\_route53\_zone\_association + +Provides a Route53 private Hosted Zone to VPC association resource. + +## Example Usage + +``` +resource "aws_vpc" "primary" { + cidr_block = "10.6.0.0/16" + enable_dns_hostnames = true + enable_dns_support = true +} + +resource "aws_vpc" "secondary" { + cidr_block = "10.7.0.0/16" + enable_dns_hostnames = true + enable_dns_support = true +} + +resource "aws_route53_zone" "example" { + name = "example.com" + vpc_id = "${aws_vpc.primary.id}" +} + +resource "aws_route53_zone_assocation" "secondary" { + zone_id = "${aws_route53_zone.example.id}" + vpc_id = "${aws_vpc.secondary.id}" +} +``` + +## Argument Reference + +The following arguments are supported: + +* `zone_id` - (Required) The private hosted zone to associate. +* `vpc_id` - (Required) The VPC to associate with the private hosted zone. +* `vpc_region` - (Optional) The VPC's region. Defaults to the region of the AWS provider. + +## Attributes Reference + +The following attributes are exported: + +* `id` - The calculated unique identifier for the association. +* `zone_id` - The ID of the hosted zone for the association. +* `vpc_id` - The ID of the VPC for the association. +* `vpc_region` - The region in which the VPC identified by `vpc_id` was created. From f1d85f955a7fc1650f5694e8579a795a94739919 Mon Sep 17 00:00:00 2001 From: John Engelman Date: Fri, 8 May 2015 22:33:05 -0500 Subject: [PATCH 13/18] Fix typo in docs --- website/source/docs/providers/aws/r/route53_zone.html.markdown | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/website/source/docs/providers/aws/r/route53_zone.html.markdown b/website/source/docs/providers/aws/r/route53_zone.html.markdown index 665811d08e..7d027bd18f 100644 --- a/website/source/docs/providers/aws/r/route53_zone.html.markdown +++ b/website/source/docs/providers/aws/r/route53_zone.html.markdown @@ -67,5 +67,5 @@ The following arguments are supported: The following attributes are exported: * `zone_id` - The Hosted Zone ID. This can be referenced by zone records. -* `name_servers` - A list of name servers in a default delegation set. Support only for Public Hosted Zones. +* `name_servers` - A list of name servers in a default delegation set. Supported only for Public Hosted Zones. Find more about delegation sets in [AWS docs](http://docs.aws.amazon.com/Route53/latest/APIReference/actions-on-reusable-delegation-sets.html). From b3d54cef4e4df2365762b19abd3b7e6053490c99 Mon Sep 17 00:00:00 2001 From: John Engelman Date: Mon, 11 May 2015 08:23:26 -0500 Subject: [PATCH 14/18] fix typo in Changelog --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 282716e33c..3fc9bc864f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -16,7 +16,7 @@ IMPROVEMENTS: * provider/aws: `aws_elasticache_cluster` add support for Tags [GH-1965] * provider/aws: `aws_s3_bucket` exports `hosted_zone_id` and `region` [GH-1865] * provider/aws: `aws_route53_record` exports `fqdn` [GH-1847] - * provider/aws: `aws_route53_hosted_zone` can create private hosted zones [GH-1526] + * provider/aws: `aws_route53_zone` can create private hosted zones [GH-1526] * provider/google: `google_compute_instance` `scratch` attribute added [GH-1920] BUG FIXES: From 3275ab0ba1a7e5f4380f14507ba9f27ccb366bee Mon Sep 17 00:00:00 2001 From: John Engelman Date: Mon, 11 May 2015 09:41:59 -0500 Subject: [PATCH 15/18] update acceptance test function names --- .../aws/resource_aws_route53_zone_association_test.go | 4 ++-- builtin/providers/aws/resource_aws_route53_zone_test.go | 6 +++--- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/builtin/providers/aws/resource_aws_route53_zone_association_test.go b/builtin/providers/aws/resource_aws_route53_zone_association_test.go index d3031739d5..8a734463dc 100644 --- a/builtin/providers/aws/resource_aws_route53_zone_association_test.go +++ b/builtin/providers/aws/resource_aws_route53_zone_association_test.go @@ -12,7 +12,7 @@ import ( "github.com/awslabs/aws-sdk-go/service/route53" ) -func TestAccRoute53ZoneAssociation(t *testing.T) { +func TestAccRoute53ZoneAssociation_basic(t *testing.T) { var zone route53.HostedZone resource.Test(t, resource.TestCase{ @@ -30,7 +30,7 @@ func TestAccRoute53ZoneAssociation(t *testing.T) { }) } -func TestAccRoute53ZoneAssociationWithRegion(t *testing.T) { +func TestAccRoute53ZoneAssociation_region(t *testing.T) { var zone route53.HostedZone // record the initialized providers so that we can use them to diff --git a/builtin/providers/aws/resource_aws_route53_zone_test.go b/builtin/providers/aws/resource_aws_route53_zone_test.go index 3a2d1279f9..6009093d68 100644 --- a/builtin/providers/aws/resource_aws_route53_zone_test.go +++ b/builtin/providers/aws/resource_aws_route53_zone_test.go @@ -64,7 +64,7 @@ func TestCleanChangeID(t *testing.T) { } } -func TestAccRoute53Zone(t *testing.T) { +func TestAccRoute53Zone_basic(t *testing.T) { var zone route53.GetHostedZoneOutput var td route53.ResourceTagSet @@ -85,7 +85,7 @@ func TestAccRoute53Zone(t *testing.T) { }) } -func TestAccRoute53PrivateZone(t *testing.T) { +func TestAccRoute53Zone_private_basic(t *testing.T) { var zone route53.GetHostedZoneOutput resource.Test(t, resource.TestCase{ @@ -104,7 +104,7 @@ func TestAccRoute53PrivateZone(t *testing.T) { }) } -func TestAccRoute53PrivateZoneWithRegion(t *testing.T) { +func TestAccRoute53Zone_private_region(t *testing.T) { var zone route53.GetHostedZoneOutput // record the initialized providers so that we can use them to From a6fdb048296e1237538bbba175f844511cf55409 Mon Sep 17 00:00:00 2001 From: John Engelman Date: Mon, 11 May 2015 09:47:08 -0500 Subject: [PATCH 16/18] Only d.Set("vpc_region") when creating a private zone. --- builtin/providers/aws/resource_aws_route53_zone.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/builtin/providers/aws/resource_aws_route53_zone.go b/builtin/providers/aws/resource_aws_route53_zone.go index 7711a187d2..d3c8a21cfb 100644 --- a/builtin/providers/aws/resource_aws_route53_zone.go +++ b/builtin/providers/aws/resource_aws_route53_zone.go @@ -74,6 +74,7 @@ func resourceAwsRoute53ZoneCreate(d *schema.ResourceData, meta interface{}) erro if w := d.Get("vpc_region"); w != "" { req.VPC.VPCRegion = aws.String(w.(string)) } + d.Set("vpc_region", req.VPC.VPCRegion) } log.Printf("[DEBUG] Creating Route53 hosted zone: %s", *req.Name) @@ -86,7 +87,6 @@ func resourceAwsRoute53ZoneCreate(d *schema.ResourceData, meta interface{}) erro zone := cleanZoneID(*resp.HostedZone.ID) d.Set("zone_id", zone) d.SetId(zone) - d.Set("vpc_region", req.VPC.VPCRegion) // Wait until we are done initializing wait := resource.StateChangeConf{ From bd6ff34aa2934cb13a7bf93d2ce72ce9e44b5684 Mon Sep 17 00:00:00 2001 From: John Engelman Date: Mon, 11 May 2015 10:20:34 -0500 Subject: [PATCH 17/18] Retrieve nameservers for private hosted zone. --- .../aws/resource_aws_route53_zone.go | 29 ++++++++++++++++++- 1 file changed, 28 insertions(+), 1 deletion(-) diff --git a/builtin/providers/aws/resource_aws_route53_zone.go b/builtin/providers/aws/resource_aws_route53_zone.go index d3c8a21cfb..b899073714 100644 --- a/builtin/providers/aws/resource_aws_route53_zone.go +++ b/builtin/providers/aws/resource_aws_route53_zone.go @@ -131,7 +131,14 @@ func resourceAwsRoute53ZoneRead(d *schema.ResourceData, meta interface{}) error return fmt.Errorf("[DEBUG] Error setting name servers for: %s, error: %#v", d.Id(), err) } } else { - d.Set("name_servers", nil) + ns, err := getNameServers(d.Id(), d.Get("name").(string), r53) + if err != nil { + return err + } + if err := d.Set("name_servers", ns); err != nil { + return fmt.Errorf("[DEBUG] Error setting name servers for: %s, error: %#v", d.Id(), err) + } + var associatedVPC *route53.VPC for _, vpc := range zone.VPCs { if *vpc.VPCID == d.Get("vpc_id") { @@ -217,3 +224,23 @@ func cleanPrefix(ID, prefix string) string { } return ID } + +func getNameServers(zoneId string, zoneName string, r53 *route53.Route53) ([]string, error) { + resp, err := r53.ListResourceRecordSets(&route53.ListResourceRecordSetsInput{ + HostedZoneID: aws.String(zoneId), + StartRecordName: aws.String(zoneName), + StartRecordType: aws.String("NS"), + }) + if err != nil { + return nil, err + } + if len(resp.ResourceRecordSets) == 0 { + return nil, nil + } + ns := make([]string, len(resp.ResourceRecordSets[0].ResourceRecords)) + for i := range resp.ResourceRecordSets[0].ResourceRecords { + ns[i] = *resp.ResourceRecordSets[0].ResourceRecords[i].Value + } + sort.Strings(ns) + return ns, nil +} From 43607523faff9217369bdd3ecb366ddbdc9c67c6 Mon Sep 17 00:00:00 2001 From: John Engelman Date: Tue, 12 May 2015 18:47:08 -0500 Subject: [PATCH 18/18] Add missing sidebar item for zone_association. --- website/source/layouts/aws.erb | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/website/source/layouts/aws.erb b/website/source/layouts/aws.erb index 528946ae9f..4731528646 100644 --- a/website/source/layouts/aws.erb +++ b/website/source/layouts/aws.erb @@ -144,6 +144,10 @@ aws_route53_zone + > + aws_route53_zone_association + + > aws_s3_bucket