diff --git a/builtin/providers/aws/provider.go b/builtin/providers/aws/provider.go index 8b0e8c518..c801f2d36 100644 --- a/builtin/providers/aws/provider.go +++ b/builtin/providers/aws/provider.go @@ -358,6 +358,7 @@ func Provider() terraform.ResourceProvider { "aws_vpc_peering_connection": resourceAwsVpcPeeringConnection(), "aws_vpc": resourceAwsVpc(), "aws_vpc_endpoint": resourceAwsVpcEndpoint(), + "aws_vpc_endpoint_route_table_association": resourceAwsVpcEndpointRouteTableAssociation(), "aws_vpn_connection": resourceAwsVpnConnection(), "aws_vpn_connection_route": resourceAwsVpnConnectionRoute(), "aws_vpn_gateway": resourceAwsVpnGateway(), diff --git a/builtin/providers/aws/resource_aws_vpc_endpoint.go b/builtin/providers/aws/resource_aws_vpc_endpoint.go index e78a50a7e..b07940326 100644 --- a/builtin/providers/aws/resource_aws_vpc_endpoint.go +++ b/builtin/providers/aws/resource_aws_vpc_endpoint.go @@ -45,6 +45,7 @@ func resourceAwsVpcEndpoint() *schema.Resource { "route_table_ids": &schema.Schema{ Type: schema.TypeSet, Optional: true, + Computed: true, Elem: &schema.Schema{Type: schema.TypeString}, Set: schema.HashString, }, diff --git a/builtin/providers/aws/resource_aws_vpc_endpoint_route_table_association.go b/builtin/providers/aws/resource_aws_vpc_endpoint_route_table_association.go new file mode 100644 index 000000000..f12c55629 --- /dev/null +++ b/builtin/providers/aws/resource_aws_vpc_endpoint_route_table_association.go @@ -0,0 +1,151 @@ +package aws + +import ( + "fmt" + "log" + + "github.com/aws/aws-sdk-go/aws" + "github.com/aws/aws-sdk-go/aws/awserr" + "github.com/aws/aws-sdk-go/service/ec2" + "github.com/hashicorp/terraform/helper/hashcode" + "github.com/hashicorp/terraform/helper/schema" +) + +func resourceAwsVpcEndpointRouteTableAssociation() *schema.Resource { + return &schema.Resource{ + Create: resourceAwsVPCEndpointRouteTableAssociationCreate, + Read: resourceAwsVPCEndpointRouteTableAssociationRead, + Delete: resourceAwsVPCEndpointRouteTableAssociationDelete, + Importer: &schema.ResourceImporter{ + State: schema.ImportStatePassthrough, + }, + + Schema: map[string]*schema.Schema{ + "vpc_endpoint_id": { + Type: schema.TypeString, + Required: true, + ForceNew: true, + }, + "route_table_id": { + Type: schema.TypeString, + Required: true, + ForceNew: true, + }, + }, + } +} + +func resourceAwsVPCEndpointRouteTableAssociationCreate(d *schema.ResourceData, meta interface{}) error { + conn := meta.(*AWSClient).ec2conn + endpointId := d.Get("vpc_endpoint_id").(string) + rtId := d.Get("route_table_id").(string) + + _, err := findResourceVPCEndpoint(conn, endpointId) + if err != nil { + return err + } + + log.Printf( + "[INFO] Creating VPC Endpoint/Route Table association: %s => %s", + endpointId, rtId) + + input := &ec2.ModifyVpcEndpointInput{ + VpcEndpointId: aws.String(endpointId), + AddRouteTableIds: aws.StringSlice([]string{rtId}), + } + + _, err = conn.ModifyVpcEndpoint(input) + if err != nil { + return fmt.Errorf("Error creating VPC Endpoint/Route Table association: %s", err.Error()) + } + id := vpcEndpointIdRouteTableIdHash(endpointId, rtId) + log.Printf("[DEBUG] VPC Endpoint/Route Table association %q created.", id) + + d.SetId(id) + + return resourceAwsVPCEndpointRouteTableAssociationRead(d, meta) +} + +func resourceAwsVPCEndpointRouteTableAssociationRead(d *schema.ResourceData, meta interface{}) error { + conn := meta.(*AWSClient).ec2conn + endpointId := d.Get("vpc_endpoint_id").(string) + rtId := d.Get("route_table_id").(string) + + vpce, err := findResourceVPCEndpoint(conn, endpointId) + if err, ok := err.(awserr.Error); ok && err.Code() == "InvalidVpcEndpointId.NotFound" { + d.SetId("") + return nil + } + + found := false + for _, id := range vpce.RouteTableIds { + if id != nil && *id == rtId { + found = true + break + } + } + if !found { + // The association no longer exists. + d.SetId("") + return nil + } + + id := vpcEndpointIdRouteTableIdHash(endpointId, rtId) + log.Printf("[DEBUG] Computed VPC Endpoint/Route Table ID %s", id) + d.SetId(id) + + return nil +} + +func resourceAwsVPCEndpointRouteTableAssociationDelete(d *schema.ResourceData, meta interface{}) error { + conn := meta.(*AWSClient).ec2conn + endpointId := d.Get("vpc_endpoint_id").(string) + rtId := d.Get("route_table_id").(string) + + input := &ec2.ModifyVpcEndpointInput{ + VpcEndpointId: aws.String(endpointId), + RemoveRouteTableIds: aws.StringSlice([]string{rtId}), + } + + _, err := conn.ModifyVpcEndpoint(input) + if err != nil { + ec2err, ok := err.(awserr.Error) + if !ok { + return fmt.Errorf("Error deleting VPC Endpoint/Route Table association: %s", err.Error()) + } + + switch ec2err.Code() { + case "InvalidVpcEndpointId.NotFound": + fallthrough + case "InvalidRouteTableId.NotFound": + fallthrough + case "InvalidParameter": + log.Printf("[DEBUG] VPC Endpoint/Route Table association is already gone") + default: + return fmt.Errorf("Error deleting VPC Endpoint/Route Table association: %s", err.Error()) + } + } + + log.Printf("[DEBUG] VPC Endpoint/Route Table association %q deleted", d.Id()) + d.SetId("") + + return nil +} + +func findResourceVPCEndpoint(conn *ec2.EC2, id string) (*ec2.VpcEndpoint, error) { + input := &ec2.DescribeVpcEndpointsInput{ + VpcEndpointIds: aws.StringSlice([]string{id}), + } + + log.Printf("[DEBUG] Reading VPC Endpoint: %q", id) + output, err := conn.DescribeVpcEndpoints(input) + if err != nil { + return nil, err + } + + return output.VpcEndpoints[0], nil +} + +func vpcEndpointIdRouteTableIdHash(endpointId, rtId string) string { + return fmt.Sprintf("a-%s%d", endpointId, hashcode.String(rtId)) +} diff --git a/builtin/providers/aws/resource_aws_vpc_endpoint_route_table_association_test.go b/builtin/providers/aws/resource_aws_vpc_endpoint_route_table_association_test.go new file mode 100644 index 000000000..450b6dd9b --- /dev/null +++ b/builtin/providers/aws/resource_aws_vpc_endpoint_route_table_association_test.go @@ -0,0 +1,131 @@ +package aws + +import ( + "fmt" + "testing" + + "github.com/aws/aws-sdk-go/aws" + "github.com/aws/aws-sdk-go/aws/awserr" + "github.com/aws/aws-sdk-go/service/ec2" + "github.com/hashicorp/terraform/helper/resource" + "github.com/hashicorp/terraform/terraform" +) + +func TestAccAWSVpcEndpointRouteTableAssociation_basic(t *testing.T) { + var vpce ec2.VpcEndpoint + + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + Providers: testAccProviders, + CheckDestroy: testAccCheckVpcEndpointRouteTableAssociationDestroy, + Steps: []resource.TestStep{ + resource.TestStep{ + Config: testAccVpcEndpointRouteTableAssociationConfig, + Check: resource.ComposeTestCheckFunc( + testAccCheckVpcEndpointRouteTableAssociationExists( + "aws_vpc_endpoint_route_table_association.a", &vpce), + ), + }, + }, + }) +} + +func testAccCheckVpcEndpointRouteTableAssociationDestroy(s *terraform.State) error { + conn := testAccProvider.Meta().(*AWSClient).ec2conn + + for _, rs := range s.RootModule().Resources { + if rs.Type != "aws_vpc_endpoint_route_table_association" { + continue + } + + // Try to find the resource + resp, err := conn.DescribeVpcEndpoints(&ec2.DescribeVpcEndpointsInput{ + VpcEndpointIds: aws.StringSlice([]string{rs.Primary.Attributes["vpc_endpoint_id"]}), + }) + if err != nil { + // Verify the error is what we want + ec2err, ok := err.(awserr.Error) + if !ok { + return err + } + if ec2err.Code() != "InvalidVpcEndpointId.NotFound" { + return err + } + return nil + } + + vpce := resp.VpcEndpoints[0] + if len(vpce.RouteTableIds) > 0 { + return fmt.Errorf( + "VPC endpoint %s has route tables", *vpce.VpcEndpointId) + } + } + + return nil +} + +func testAccCheckVpcEndpointRouteTableAssociationExists(n string, vpce *ec2.VpcEndpoint) 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 ID is set") + } + + conn := testAccProvider.Meta().(*AWSClient).ec2conn + resp, err := conn.DescribeVpcEndpoints(&ec2.DescribeVpcEndpointsInput{ + VpcEndpointIds: aws.StringSlice([]string{rs.Primary.Attributes["vpc_endpoint_id"]}), + }) + if err != nil { + return err + } + if len(resp.VpcEndpoints) == 0 { + return fmt.Errorf("VPC endpoint not found") + } + + *vpce = *resp.VpcEndpoints[0] + + if len(vpce.RouteTableIds) == 0 { + return fmt.Errorf("no route table associations") + } + + for _, id := range vpce.RouteTableIds { + if *id == rs.Primary.Attributes["route_table_id"] { + return nil + } + } + + return fmt.Errorf("route table association not found") + } +} + +const testAccVpcEndpointRouteTableAssociationConfig = ` +provider "aws" { + region = "us-west-2" +} + +resource "aws_vpc" "foo" { + cidr_block = "10.0.0.0/16" +} + +resource "aws_vpc_endpoint" "s3" { + vpc_id = "${aws_vpc.foo.id}" + service_name = "com.amazonaws.us-west-2.s3" +} + +resource "aws_route_table" "rt" { + vpc_id = "${aws_vpc.foo.id}" + + tags { + Name = "test" + } +} + +resource "aws_vpc_endpoint_route_table_association" "a" { + vpc_endpoint_id = "${aws_vpc_endpoint.s3.id}" + route_table_id = "${aws_route_table.rt.id}" +} +` diff --git a/website/source/docs/providers/aws/r/vpc_endpoint.html.markdown b/website/source/docs/providers/aws/r/vpc_endpoint.html.markdown index 72778c123..591f300c0 100644 --- a/website/source/docs/providers/aws/r/vpc_endpoint.html.markdown +++ b/website/source/docs/providers/aws/r/vpc_endpoint.html.markdown @@ -10,6 +10,13 @@ description: |- Provides a VPC Endpoint resource. +~> **NOTE on VPC Endpoints and VPC Endpoint Route Table Associations:** Terraform provides +both a standalone [VPC Endpoint Route Table Association](vpc_endpoint_route_table_association.html) +(an association between a VPC endpoint and a single `route_table_id`) and a VPC Endpoint resource +with a `route_table_ids` attribute. Do not use the same route table ID in both a VPC Endpoint resource +and a VPC Endpoint Route Table Association resource. Doing so will cause a conflict of associations +and will overwrite the association. + ## Example Usage Basic usage: @@ -41,7 +48,7 @@ The following attributes are exported: ## Import -VPC Endpoints can be imported using the `vpc endpoint id`, e.g. +VPC Endpoints can be imported using the `vpc endpoint id`, e.g. ``` $ terraform import aws_vpc_endpoint.endpoint1 vpce-3ecf2a57 diff --git a/website/source/docs/providers/aws/r/vpc_endpoint_route_table_association.html.markdown b/website/source/docs/providers/aws/r/vpc_endpoint_route_table_association.html.markdown new file mode 100644 index 000000000..7c3f54d39 --- /dev/null +++ b/website/source/docs/providers/aws/r/vpc_endpoint_route_table_association.html.markdown @@ -0,0 +1,41 @@ +--- +layout: "aws" +page_title: "AWS: aws_vpc_endpoint_route_table_association" +sidebar_current: "docs-aws-resource-vpc-endpoint-route-table-association" +description: |- + Provides a resource to create an association between a VPC endpoint and routing table. +--- + +# aws\_vpc\_endpoint\_route\_table\_association + +Provides a resource to create an association between a VPC endpoint and routing table. + +~> **NOTE on VPC Endpoints and VPC Endpoint Route Table Associations:** Terraform provides +both a standalone VPC Endpoint Route Table Association (an association between a VPC endpoint +and a single `route_table_id`) and a [VPC Endpoint](vpc_endpoint.html) resource with a `route_table_ids` +attribute. Do not use the same route table ID in both a VPC Endpoint resource and a VPC Endpoint Route +Table Association resource. Doing so will cause a conflict of associations and will overwrite the association. + +## Example Usage + +Basic usage: + +``` +resource "aws_vpc_endpoint_route_table_association" "private_s3" { + vpc_endpoint_id = "${aws_vpc_endpoint.s3.id}" + route_table_id = "${aws_route_table.private.id}" +} +``` + +## Argument Reference + +The following arguments are supported: + +* `vpc_endpoint_id` - (Required) The ID of the VPC endpoint with which the routing table will be associated. +* `route_table_id` - (Required) The ID of the routing table to be associated with the VPC endpoint. + +## Attributes Reference + +The following attributes are exported: + +* `id` - The ID of the association. diff --git a/website/source/layouts/aws.erb b/website/source/layouts/aws.erb index cc569b434..7b08ff647 100644 --- a/website/source/layouts/aws.erb +++ b/website/source/layouts/aws.erb @@ -1076,6 +1076,10 @@ aws_vpc_endpoint + > + aws_vpc_endpoint_route_table_association + + > aws_vpc_peering_connection