diff --git a/aws/provider.go b/aws/provider.go index d3e9c8130356..6a815c5c5739 100644 --- a/aws/provider.go +++ b/aws/provider.go @@ -771,6 +771,7 @@ func Provider() terraform.ResourceProvider { "aws_route53_query_log": resourceAwsRoute53QueryLog(), "aws_route53_record": resourceAwsRoute53Record(), "aws_route53_zone_association": resourceAwsRoute53ZoneAssociation(), + "aws_route53_vpc_association_authorization": resourceAwsRoute53VPCAssociationAuthorization(), "aws_route53_zone": resourceAwsRoute53Zone(), "aws_route53_health_check": resourceAwsRoute53HealthCheck(), "aws_route53_resolver_endpoint": resourceAwsRoute53ResolverEndpoint(), diff --git a/aws/resource_aws_route53_vpc_association_authorization.go b/aws/resource_aws_route53_vpc_association_authorization.go new file mode 100644 index 000000000000..146732fbb38e --- /dev/null +++ b/aws/resource_aws_route53_vpc_association_authorization.go @@ -0,0 +1,155 @@ +package aws + +import ( + "fmt" + "log" + "strings" + + "github.com/hashicorp/terraform-plugin-sdk/helper/schema" + + "github.com/aws/aws-sdk-go/aws" + "github.com/aws/aws-sdk-go/service/route53" +) + +func resourceAwsRoute53VPCAssociationAuthorization() *schema.Resource { + return &schema.Resource{ + Create: resourceAwsRoute53VPCAssociationAuthorizationCreate, + Read: resourceAwsRoute53VPCAssociationAuthorizationRead, + Delete: resourceAwsRoute53VPCAssociationAuthorizationDelete, + Importer: &schema.ResourceImporter{ + State: schema.ImportStatePassthrough, + }, + + Schema: map[string]*schema.Schema{ + "zone_id": { + Type: schema.TypeString, + Required: true, + ForceNew: true, + }, + + "vpc_id": { + Type: schema.TypeString, + Required: true, + ForceNew: true, + }, + + "vpc_region": { + Type: schema.TypeString, + Optional: true, + Computed: true, + ForceNew: true, + }, + }, + } +} + +func resourceAwsRoute53VPCAssociationAuthorizationCreate(d *schema.ResourceData, meta interface{}) error { + conn := meta.(*AWSClient).r53conn + + req := &route53.CreateVPCAssociationAuthorizationInput{ + HostedZoneId: aws.String(d.Get("zone_id").(string)), + VPC: &route53.VPC{ + VPCId: aws.String(d.Get("vpc_id").(string)), + VPCRegion: aws.String(meta.(*AWSClient).region), + }, + } + + if v, ok := d.GetOk("vpc_region"); ok { + req.VPC.VPCRegion = aws.String(v.(string)) + } + + log.Printf("[DEBUG] Creating Route53 VPC Association Authorization for hosted zone %s with VPC %s and region %s", *req.HostedZoneId, *req.VPC.VPCId, *req.VPC.VPCRegion) + _, err := conn.CreateVPCAssociationAuthorization(req) + if err != nil { + return fmt.Errorf("Error creating Route53 VPC Association Authorization: %s", err) + } + + // Store association id + d.SetId(fmt.Sprintf("%s:%s", *req.HostedZoneId, *req.VPC.VPCId)) + + return resourceAwsRoute53VPCAssociationAuthorizationRead(d, meta) +} + +func resourceAwsRoute53VPCAssociationAuthorizationRead(d *schema.ResourceData, meta interface{}) error { + conn := meta.(*AWSClient).r53conn + + zone_id, vpc_id, err := resourceAwsRoute53VPCAssociationAuthorizationParseId(d.Id()) + if err != nil { + return err + } + + req := route53.ListVPCAssociationAuthorizationsInput{ + HostedZoneId: aws.String(zone_id), + } + + for { + log.Printf("[DEBUG] Listing Route53 VPC Association Authorizations for hosted zone %s", zone_id) + res, err := conn.ListVPCAssociationAuthorizations(&req) + + if isAWSErr(err, route53.ErrCodeNoSuchHostedZone, "") { + log.Printf("[WARN] Route53 VPC Association Authorization (%s) not found, removing from state", d.Id()) + d.SetId("") + return nil + } + + if err != nil { + return fmt.Errorf("Error listing Route53 VPC Association Authorizations: %s", err) + } + + for _, vpc := range res.VPCs { + if vpc_id == aws.StringValue(vpc.VPCId) { + d.Set("vpc_id", vpc.VPCId) + d.Set("vpc_region", vpc.VPCRegion) + d.Set("zone_id", zone_id) + return nil + } + } + + // Loop till we find our authorization or we reach the end + if res.NextToken != nil { + req.NextToken = res.NextToken + } else { + break + } + } + + // no association found + log.Printf("[WARN] Route53 VPC Association Authorization (%s) not found, removing from state", d.Id()) + d.SetId("") + return nil +} + +func resourceAwsRoute53VPCAssociationAuthorizationDelete(d *schema.ResourceData, meta interface{}) error { + conn := meta.(*AWSClient).r53conn + + zone_id, vpc_id, err := resourceAwsRoute53VPCAssociationAuthorizationParseId(d.Id()) + if err != nil { + return err + } + + req := route53.DeleteVPCAssociationAuthorizationInput{ + HostedZoneId: aws.String(zone_id), + VPC: &route53.VPC{ + VPCId: aws.String(vpc_id), + VPCRegion: aws.String(d.Get("vpc_region").(string)), + }, + } + + log.Printf("[DEBUG] Deleting Route53 Assocatiation Authorization for hosted zone %s for VPC %s", zone_id, vpc_id) + _, err = conn.DeleteVPCAssociationAuthorization(&req) + if err != nil { + return fmt.Errorf("Error deleting Route53 VPC Association Authorization: %s", err) + } + + return nil +} + +func resourceAwsRoute53VPCAssociationAuthorizationParseId(id string) (string, string, error) { + parts := strings.SplitN(id, ":", 2) + + if len(parts) != 2 || parts[0] == "" || parts[1] == "" { + return "", "", fmt.Errorf("Unexpected format of ID (%q), expected ZONEID:VPCID", id) + } + + return parts[0], parts[1], nil +} diff --git a/aws/resource_aws_route53_vpc_association_authorization_test.go b/aws/resource_aws_route53_vpc_association_authorization_test.go new file mode 100644 index 000000000000..9b0ce6e3c48e --- /dev/null +++ b/aws/resource_aws_route53_vpc_association_authorization_test.go @@ -0,0 +1,141 @@ +package aws + +import ( + "fmt" + "testing" + + "github.com/hashicorp/terraform-plugin-sdk/helper/resource" + "github.com/hashicorp/terraform-plugin-sdk/helper/schema" + "github.com/hashicorp/terraform-plugin-sdk/terraform" + + "github.com/aws/aws-sdk-go/aws" + "github.com/aws/aws-sdk-go/service/route53" +) + +func TestAccAWSRoute53VpcAssociationAuthorization_basic(t *testing.T) { + var providers []*schema.Provider + resourceName := "aws_route53_vpc_association_authorization.test" + + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { + testAccPreCheck(t) + testAccAlternateAccountPreCheck(t) + }, + ProviderFactories: testAccProviderFactories(&providers), + CheckDestroy: testAccCheckRoute53VPCAssociationAuthorizationDestroy, + Steps: []resource.TestStep{ + { + Config: testAccRoute53VPCAssociationAuthorizationConfig(), + Check: resource.ComposeTestCheckFunc( + testAccCheckRoute53VPCAssociationAuthorizationExists(resourceName), + ), + }, + { + Config: testAccRoute53VPCAssociationAuthorizationConfig(), + ResourceName: resourceName, + ImportState: true, + ImportStateVerify: true, + }, + }, + }) +} + +func testAccCheckRoute53VPCAssociationAuthorizationDestroy(s *terraform.State) error { + conn := testAccProvider.Meta().(*AWSClient).r53conn + + for _, rs := range s.RootModule().Resources { + if rs.Type != "aws_route53_vpc_association_authorization" { + continue + } + + zone_id, vpc_id, err := resourceAwsRoute53ZoneAssociationParseId(rs.Primary.ID) + if err != nil { + return err + } + + req := route53.ListVPCAssociationAuthorizationsInput{ + HostedZoneId: aws.String(zone_id), + } + + res, err := conn.ListVPCAssociationAuthorizations(&req) + if isAWSErr(err, route53.ErrCodeNoSuchHostedZone, "") { + return nil + } + if err != nil { + return err + } + + for _, vpc := range res.VPCs { + if vpc_id == *vpc.VPCId { + return fmt.Errorf("VPC association authorization for zone %v with %v still exists", zone_id, vpc_id) + } + } + } + return nil +} + +func testAccCheckRoute53VPCAssociationAuthorizationExists(n string) 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 association authorization ID is set") + } + + zone_id, vpc_id, err := resourceAwsRoute53ZoneAssociationParseId(rs.Primary.ID) + if err != nil { + return err + } + + conn := testAccProvider.Meta().(*AWSClient).r53conn + + req := route53.ListVPCAssociationAuthorizationsInput{ + HostedZoneId: aws.String(zone_id), + } + + res, err := conn.ListVPCAssociationAuthorizations(&req) + if err != nil { + return err + } + + for _, vpc := range res.VPCs { + if vpc_id == *vpc.VPCId { + return nil + } + } + + return fmt.Errorf("VPC association authorization not found") + } +} + +func testAccRoute53VPCAssociationAuthorizationConfig() string { + return testAccAlternateAccountProviderConfig() + ` +resource "aws_vpc" "test" { + cidr_block = "10.6.0.0/16" + enable_dns_hostnames = true + enable_dns_support = true +} + +resource "aws_route53_zone" "test" { + name = "example.com" + vpc { + vpc_id = aws_vpc.test.id + } +} + +resource "aws_vpc" "alternate" { + provider = "awsalternate" + cidr_block = "10.7.0.0/16" + enable_dns_hostnames = true + enable_dns_support = true +} + +resource "aws_route53_vpc_association_authorization" "test" { + zone_id = aws_route53_zone.test.id + vpc_id = aws_vpc.alternate.id +} +` +} diff --git a/aws/resource_aws_route53_zone_association.go b/aws/resource_aws_route53_zone_association.go index 63b00a754769..6fac23346a1c 100644 --- a/aws/resource_aws_route53_zone_association.go +++ b/aws/resource_aws_route53_zone_association.go @@ -41,6 +41,11 @@ func resourceAwsRoute53ZoneAssociation() *schema.Resource { Computed: true, ForceNew: true, }, + + "owning_account": { + Type: schema.TypeString, + Computed: true, + }, }, } } @@ -61,7 +66,7 @@ func resourceAwsRoute53ZoneAssociationCreate(d *schema.ResourceData, meta interf } log.Printf("[DEBUG] Associating Route53 Private Zone %s with VPC %s with region %s", *req.HostedZoneId, *req.VPC.VPCId, *req.VPC.VPCRegion) - var err error + resp, err := r53.AssociateVPCWithHostedZone(req) if err != nil { return err @@ -73,16 +78,11 @@ func resourceAwsRoute53ZoneAssociationCreate(d *schema.ResourceData, meta interf // Wait until we are done initializing wait := resource.StateChangeConf{ Delay: 30 * time.Second, - Pending: []string{"PENDING"}, - Target: []string{"INSYNC"}, + Pending: []string{route53.ChangeStatusPending}, + Target: []string{route53.ChangeStatusInsync}, 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) - }, + Refresh: resourceAwsRoute53ZoneAssociationRefreshFunc(r53, cleanChangeID(*resp.ChangeInfo.Id), d.Id()), } _, err = wait.WaitForState() if err != nil { @@ -96,32 +96,28 @@ func resourceAwsRoute53ZoneAssociationRead(d *schema.ResourceData, meta interfac conn := meta.(*AWSClient).r53conn zoneID, vpcID, err := resourceAwsRoute53ZoneAssociationParseId(d.Id()) + vpcRegion := meta.(*AWSClient).region if err != nil { return err } - vpc, err := route53GetZoneAssociation(conn, zoneID, vpcID) - - if isAWSErr(err, route53.ErrCodeNoSuchHostedZone, "") { - log.Printf("[WARN] Route 53 Hosted Zone (%s) not found, removing from state", zoneID) - d.SetId("") - return nil - } + hostedZoneSummary, err := route53GetZoneAssociation(conn, zoneID, vpcID, vpcRegion) if err != nil { return fmt.Errorf("error getting Route 53 Hosted Zone (%s): %s", zoneID, err) } - if vpc == nil { + if hostedZoneSummary == nil { log.Printf("[WARN] Route 53 Hosted Zone (%s) Association (%s) not found, removing from state", zoneID, vpcID) d.SetId("") return nil } - d.Set("vpc_id", vpc.VPCId) - d.Set("vpc_region", vpc.VPCRegion) + d.Set("vpc_id", vpcID) + d.Set("vpc_region", vpcRegion) d.Set("zone_id", zoneID) + d.Set("owning_account", hostedZoneSummary.Owner.OwningAccount) return nil } @@ -163,24 +159,39 @@ func resourceAwsRoute53ZoneAssociationParseId(id string) (string, string, error) return parts[0], parts[1], nil } -func route53GetZoneAssociation(conn *route53.Route53, zoneID, vpcID string) (*route53.VPC, error) { - input := &route53.GetHostedZoneInput{ - Id: aws.String(zoneID), +func resourceAwsRoute53ZoneAssociationRefreshFunc(conn *route53.Route53, changeId, id string) resource.StateRefreshFunc { + return func() (interface{}, string, error) { + changeRequest := &route53.GetChangeInput{ + Id: aws.String(changeId), + } + result, state, err := resourceAwsGoRoute53Wait(conn, changeRequest) + if isAWSErr(err, "AccessDenied", "") { + log.Printf("[WARN] AccessDenied when trying to get Route 53 change progress for %s - ignoring due to likely cross account issue", id) + return true, route53.ChangeStatusInsync, nil + } + return result, state, err + } +} + +func route53GetZoneAssociation(conn *route53.Route53, zoneID, vpcID, vpcRegion string) (*route53.HostedZoneSummary, error) { + input := &route53.ListHostedZonesByVPCInput{ + VPCId: aws.String(vpcID), + VPCRegion: aws.String(vpcRegion), } - output, err := conn.GetHostedZone(input) + output, err := conn.ListHostedZonesByVPC(input) if err != nil { return nil, err } - var vpc *route53.VPC - for _, zoneVPC := range output.VPCs { - if vpcID == aws.StringValue(zoneVPC.VPCId) { - vpc = zoneVPC + var associatedHostedZoneSummary *route53.HostedZoneSummary + for _, hostedZoneSummary := range output.HostedZoneSummaries { + if zoneID == aws.StringValue(hostedZoneSummary.HostedZoneId) { + associatedHostedZoneSummary = hostedZoneSummary break } } - return vpc, nil + return associatedHostedZoneSummary, nil } diff --git a/aws/resource_aws_route53_zone_association_test.go b/aws/resource_aws_route53_zone_association_test.go index 0961e6175338..eff4b0c4405c 100644 --- a/aws/resource_aws_route53_zone_association_test.go +++ b/aws/resource_aws_route53_zone_association_test.go @@ -155,7 +155,7 @@ func testAccCheckRoute53ZoneAssociationDestroy(s *terraform.State) error { return err } - vpc, err := route53GetZoneAssociation(conn, zoneID, vpcID) + vpc, err := testAccCheckRoute53ZoneAssociationGet(conn, zoneID, vpcID) if isAWSErr(err, route53.ErrCodeNoSuchHostedZone, "") { continue @@ -191,7 +191,7 @@ func testAccCheckRoute53ZoneAssociationExists(n string, vpc *route53.VPC) resour conn := testAccProvider.Meta().(*AWSClient).r53conn - associationVPC, err := route53GetZoneAssociation(conn, zoneID, vpcID) + associationVPC, err := testAccCheckRoute53ZoneAssociationGet(conn, zoneID, vpcID) if err != nil { return err @@ -223,6 +223,28 @@ func testAccCheckRoute53ZoneAssociationDisappears(zone *route53.GetHostedZoneOut } } +func testAccCheckRoute53ZoneAssociationGet(conn *route53.Route53, zoneID, vpcID string) (*route53.VPC, error) { + input := &route53.GetHostedZoneInput{ + Id: aws.String(zoneID), + } + + output, err := conn.GetHostedZone(input) + + if err != nil { + return nil, err + } + + var vpc *route53.VPC + for _, zoneVPC := range output.VPCs { + if vpcID == aws.StringValue(zoneVPC.VPCId) { + vpc = zoneVPC + break + } + } + + return vpc, nil +} + const testAccRoute53ZoneAssociationConfig = ` resource "aws_vpc" "foo" { cidr_block = "10.6.0.0/16" diff --git a/website/aws.erb b/website/aws.erb index 7e4b3fcb9835..70287e8e4f57 100644 --- a/website/aws.erb +++ b/website/aws.erb @@ -2769,6 +2769,9 @@
  • aws_route53_record
  • +
  • + aws_route53_vpc_association_authorization +
  • aws_route53_zone
  • diff --git a/website/docs/r/route53_vpc_association_authorization.html.markdown b/website/docs/r/route53_vpc_association_authorization.html.markdown new file mode 100644 index 000000000000..cf88ccf5437e --- /dev/null +++ b/website/docs/r/route53_vpc_association_authorization.html.markdown @@ -0,0 +1,75 @@ +--- +subcategory: "Route53" +layout: "aws" +page_title: "AWS: aws_route53_vpc_association_authorization" +description: |- + Authorizes a VPC in a peer account to be associated with a local Route53 Hosted Zone +--- + +# Resource: aws_route53_vpc_association_authorization + +Authorizes a VPC in a peer account to be associated with a local Route53 Hosted Zone. + +## Example Usage + +```hcl +provider "aws" { +} + +provider "aws" { + alias = "alternate" +} + +resource "aws_vpc" "example" { + cidr_block = "10.6.0.0/16" + enable_dns_hostnames = true + enable_dns_support = true +} + +resource "aws_route53_zone" "example" { + name = "example.com" + vpc { + vpc_id = aws_vpc.example.id + } +} + +resource "aws_vpc" "alternate" { + provider = "aws.alternate" + cidr_block = "10.7.0.0/16" + enable_dns_hostnames = true + enable_dns_support = true +} + +resource "aws_route53_vpc_association_authorization" "example" { + zone_id = aws_route53_zone.example.id + vpc_id = aws_vpc.alternate.id +} + +resource "aws_route53_zone_association" "alternate" { + provider = "aws.alternate" + zone_id = aws_route53_zone.example.zone_id + vpc_id = aws_vpc.alternate.id +} +``` + +## Argument Reference + +The following arguments are supported: + +* `zone_id` - (Required) The ID of the private hosted zone that you want to authorize associating a VPC with. +* `vpc_id` - (Required) The VPC to authorize for association 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. + +## Import + +Route 53 VPC Association Authorizations can be imported via the Hosted Zone ID and VPC ID, separated by a colon (`:`), e.g. + +``` +$ terraform import aws_route53_vpc_association_authorization.example Z123456ABCDEFG:vpc-12345678 +``` diff --git a/website/docs/r/route53_zone_association.html.markdown b/website/docs/r/route53_zone_association.html.markdown index 346290ea0bee..aeffc4234a85 100644 --- a/website/docs/r/route53_zone_association.html.markdown +++ b/website/docs/r/route53_zone_association.html.markdown @@ -68,6 +68,7 @@ In addition to all arguments above, the following attributes are exported: * `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. +* `owning_account` - The account ID of the account that created the hosted zone. ## Import