diff --git a/builtin/providers/aws/provider.go b/builtin/providers/aws/provider.go index 8f1a81417..d6d877187 100644 --- a/builtin/providers/aws/provider.go +++ b/builtin/providers/aws/provider.go @@ -213,6 +213,7 @@ func Provider() terraform.ResourceProvider { "aws_vpc_dhcp_options": resourceAwsVpcDhcpOptions(), "aws_vpc_peering_connection": resourceAwsVpcPeeringConnection(), "aws_vpc": resourceAwsVpc(), + "aws_vpc_endpoint": resourceAwsVpcEndpoint(), "aws_vpn_connection": resourceAwsVpnConnection(), "aws_vpn_connection_route": resourceAwsVpnConnectionRoute(), "aws_vpn_gateway": resourceAwsVpnGateway(), diff --git a/builtin/providers/aws/resource_aws_route_table.go b/builtin/providers/aws/resource_aws_route_table.go index e85601c1f..f6d6313ec 100644 --- a/builtin/providers/aws/resource_aws_route_table.go +++ b/builtin/providers/aws/resource_aws_route_table.go @@ -148,6 +148,12 @@ func resourceAwsRouteTableRead(d *schema.ResourceData, meta interface{}) error { continue } + if r.DestinationPrefixListID != nil { + // Skipping because VPC endpoint routes are handled separately + // See aws_vpc_endpoint + continue + } + m := make(map[string]interface{}) if r.DestinationCIDRBlock != nil { diff --git a/builtin/providers/aws/resource_aws_vpc_endpoint.go b/builtin/providers/aws/resource_aws_vpc_endpoint.go new file mode 100644 index 000000000..5617faa41 --- /dev/null +++ b/builtin/providers/aws/resource_aws_vpc_endpoint.go @@ -0,0 +1,174 @@ +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 resourceAwsVpcEndpoint() *schema.Resource { + return &schema.Resource{ + Create: resourceAwsVPCEndpointCreate, + Read: resourceAwsVPCEndpointRead, + Update: resourceAwsVPCEndpointUpdate, + Delete: resourceAwsVPCEndpointDelete, + Schema: map[string]*schema.Schema{ + "policy": &schema.Schema{ + Type: schema.TypeString, + Optional: true, + Computed: true, + StateFunc: normalizeJson, + }, + "vpc_id": &schema.Schema{ + Type: schema.TypeString, + Required: true, + ForceNew: true, + }, + "service_name": &schema.Schema{ + Type: schema.TypeString, + Required: true, + ForceNew: true, + }, + "route_table_ids": &schema.Schema{ + Type: schema.TypeSet, + Optional: true, + Elem: &schema.Schema{Type: schema.TypeString}, + Set: func(v interface{}) int { + return hashcode.String(v.(string)) + }, + }, + }, + } +} + +func resourceAwsVPCEndpointCreate(d *schema.ResourceData, meta interface{}) error { + conn := meta.(*AWSClient).ec2conn + input := &ec2.CreateVPCEndpointInput{ + VPCID: aws.String(d.Get("vpc_id").(string)), + RouteTableIDs: expandStringList(d.Get("route_table_ids").(*schema.Set).List()), + ServiceName: aws.String(d.Get("service_name").(string)), + } + + if v, ok := d.GetOk("policy"); ok { + policy := normalizeJson(v) + input.PolicyDocument = aws.String(policy) + } + + log.Printf("[DEBUG] Creating VPC Endpoint: %#v", input) + output, err := conn.CreateVPCEndpoint(input) + if err != nil { + return fmt.Errorf("Error creating VPC Endpoint: %s", err) + } + log.Printf("[DEBUG] VPC Endpoint %q created.", *output.VPCEndpoint.VPCEndpointID) + + d.SetId(*output.VPCEndpoint.VPCEndpointID) + + return resourceAwsVPCEndpointRead(d, meta) +} + +func resourceAwsVPCEndpointRead(d *schema.ResourceData, meta interface{}) error { + conn := meta.(*AWSClient).ec2conn + input := &ec2.DescribeVPCEndpointsInput{ + VPCEndpointIDs: []*string{aws.String(d.Id())}, + } + + log.Printf("[DEBUG] Reading VPC Endpoint: %q", d.Id()) + output, err := conn.DescribeVPCEndpoints(input) + + if err != nil { + ec2err, ok := err.(awserr.Error) + if !ok { + return fmt.Errorf("Error reading VPC Endpoint: %s", err.Error()) + } + + if ec2err.Code() == "InvalidVpcEndpointId.NotFound" { + return nil + } + + return fmt.Errorf("Error reading VPC Endpoint: %s", err.Error()) + } + + if len(output.VPCEndpoints) != 1 { + return fmt.Errorf("There's no unique VPC Endpoint, but %d endpoints: %#v", + len(output.VPCEndpoints), output.VPCEndpoints) + } + + vpce := output.VPCEndpoints[0] + + d.Set("vpc_id", vpce.VPCID) + d.Set("policy", normalizeJson(*vpce.PolicyDocument)) + d.Set("service_name", vpce.ServiceName) + d.Set("route_table_ids", vpce.RouteTableIDs) + + return nil +} + +func resourceAwsVPCEndpointUpdate(d *schema.ResourceData, meta interface{}) error { + conn := meta.(*AWSClient).ec2conn + input := &ec2.ModifyVPCEndpointInput{ + VPCEndpointID: aws.String(d.Id()), + } + + if d.HasChange("route_table_ids") { + o, n := d.GetChange("route_table_ids") + os := o.(*schema.Set) + ns := n.(*schema.Set) + + add := expandStringList(os.Difference(ns).List()) + if len(add) > 0 { + input.AddRouteTableIDs = add + } + + remove := expandStringList(ns.Difference(os).List()) + if len(remove) > 0 { + input.RemoveRouteTableIDs = remove + } + } + + if d.HasChange("policy") { + policy := normalizeJson(d.Get("policy")) + input.PolicyDocument = aws.String(policy) + } + + log.Printf("[DEBUG] Updating VPC Endpoint: %#v", input) + _, err := conn.ModifyVPCEndpoint(input) + if err != nil { + return fmt.Errorf("Error updating VPC Endpoint: %s", err) + } + log.Printf("[DEBUG] VPC Endpoint %q updated", input.VPCEndpointID) + + return nil +} + +func resourceAwsVPCEndpointDelete(d *schema.ResourceData, meta interface{}) error { + conn := meta.(*AWSClient).ec2conn + input := &ec2.DeleteVPCEndpointsInput{ + VPCEndpointIDs: []*string{aws.String(d.Id())}, + } + + log.Printf("[DEBUG] Deleting VPC Endpoint: %#v", input) + _, err := conn.DeleteVPCEndpoints(input) + + if err != nil { + ec2err, ok := err.(awserr.Error) + if !ok { + return fmt.Errorf("Error deleting VPC Endpoint: %s", err.Error()) + } + + if ec2err.Code() == "InvalidVpcEndpointId.NotFound" { + log.Printf("[DEBUG] VPC Endpoint %q is already gone", d.Id()) + } else { + return fmt.Errorf("Error deleting VPC Endpoint: %s", err.Error()) + } + } + + log.Printf("[DEBUG] VPC Endpoint %q deleted", d.Id()) + d.SetId("") + + return nil +} diff --git a/builtin/providers/aws/resource_aws_vpc_endpoint_test.go b/builtin/providers/aws/resource_aws_vpc_endpoint_test.go new file mode 100644 index 000000000..6a29b688e --- /dev/null +++ b/builtin/providers/aws/resource_aws_vpc_endpoint_test.go @@ -0,0 +1,210 @@ +package aws + +import ( + "fmt" + "testing" + + "github.com/aws/aws-sdk-go/aws" + "github.com/aws/aws-sdk-go/service/ec2" + + "github.com/hashicorp/terraform/helper/resource" + "github.com/hashicorp/terraform/terraform" +) + +func TestAccVpcEndpoint_basic(t *testing.T) { + var endpoint ec2.VPCEndpoint + + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + Providers: testAccProviders, + CheckDestroy: testAccCheckVpcEndpointDestroy, + Steps: []resource.TestStep{ + resource.TestStep{ + Config: testAccVpcEndpointConfig, + Check: resource.ComposeTestCheckFunc( + testAccCheckVpcEndpointExists("aws_vpc_endpoint.private-s3", &endpoint), + ), + }, + }, + }) +} + +func TestAccVpcEndpoint_withRouteTableAndPolicy(t *testing.T) { + var endpoint ec2.VPCEndpoint + var routeTable ec2.RouteTable + + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + Providers: testAccProviders, + CheckDestroy: testAccCheckVpcEndpointDestroy, + Steps: []resource.TestStep{ + resource.TestStep{ + Config: testAccVpcEndpointWithRouteTableAndPolicyConfig, + Check: resource.ComposeTestCheckFunc( + testAccCheckVpcEndpointExists("aws_vpc_endpoint.second-private-s3", &endpoint), + testAccCheckRouteTableExists("aws_route_table.default", &routeTable), + ), + }, + resource.TestStep{ + Config: testAccVpcEndpointWithRouteTableAndPolicyConfigModified, + Check: resource.ComposeTestCheckFunc( + testAccCheckVpcEndpointExists("aws_vpc_endpoint.second-private-s3", &endpoint), + testAccCheckRouteTableExists("aws_route_table.default", &routeTable), + ), + }, + }, + }) +} + +func testAccCheckVpcEndpointDestroy(s *terraform.State) error { + conn := testAccProvider.Meta().(*AWSClient).ec2conn + + for _, rs := range s.RootModule().Resources { + if rs.Type != "aws_vpc_endpoint" { + continue + } + + // Try to find the VPC + input := &ec2.DescribeVPCEndpointsInput{ + VPCEndpointIDs: []*string{aws.String(rs.Primary.ID)}, + } + resp, err := conn.DescribeVPCEndpoints(input) + + if len(resp.VPCEndpoints) > 0 { + return fmt.Errorf("VPC Endpoints still exist.") + } + + return err + } + + return nil +} + +func testAccCheckVpcEndpointExists(n string, endpoint *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 VPC Endpoint ID is set") + } + + conn := testAccProvider.Meta().(*AWSClient).ec2conn + input := &ec2.DescribeVPCEndpointsInput{ + VPCEndpointIDs: []*string{aws.String(rs.Primary.ID)}, + } + resp, err := conn.DescribeVPCEndpoints(input) + if err != nil { + return err + } + if len(resp.VPCEndpoints) == 0 { + return fmt.Errorf("VPC Endpoint not found") + } + + *endpoint = *resp.VPCEndpoints[0] + + return nil + } +} + +const testAccVpcEndpointConfig = ` +resource "aws_vpc" "foo" { + cidr_block = "10.1.0.0/16" +} + +resource "aws_vpc_endpoint" "private-s3" { + vpc_id = "${aws_vpc.foo.id}" + service_name = "com.amazonaws.us-west-2.s3" +} +` + +const testAccVpcEndpointWithRouteTableAndPolicyConfig = ` +resource "aws_vpc" "foo" { + cidr_block = "10.0.0.0/16" +} + +resource "aws_subnet" "foo" { + vpc_id = "${aws_vpc.foo.id}" + cidr_block = "10.0.1.0/24" +} + +resource "aws_vpc_endpoint" "second-private-s3" { + vpc_id = "${aws_vpc.foo.id}" + service_name = "com.amazonaws.us-west-2.s3" + route_table_ids = ["${aws_route_table.default.id}"] + policy = <> aws_vpc_dhcp_options_association + + > + aws_vpc_endpoint + > aws_vpc_peering