From 814403f45cc7a0552d765acc37c12ce08429d6ca Mon Sep 17 00:00:00 2001 From: Christopher Tiwald Date: Fri, 1 May 2015 12:22:52 -0400 Subject: [PATCH 1/3] aws: Add support for AWS VPN connections --- builtin/providers/aws/provider.go | 1 + .../aws/resource_aws_vpn_connection.go | 356 ++++++++++++++++++ 2 files changed, 357 insertions(+) create mode 100644 builtin/providers/aws/resource_aws_vpn_connection.go diff --git a/builtin/providers/aws/provider.go b/builtin/providers/aws/provider.go index 4e2c2b5708b9..5ad97b9208d9 100644 --- a/builtin/providers/aws/provider.go +++ b/builtin/providers/aws/provider.go @@ -107,6 +107,7 @@ func Provider() terraform.ResourceProvider { "aws_vpc_peering_connection": resourceAwsVpcPeeringConnection(), "aws_vpc_dhcp_options": resourceAwsVpcDhcpOptions(), "aws_vpc_dhcp_options_association": resourceAwsVpcDhcpOptionsAssociation(), + "aws_vpn_connection": resourceAwsVpnConnection(), "aws_vpn_gateway": resourceAwsVpnGateway(), }, diff --git a/builtin/providers/aws/resource_aws_vpn_connection.go b/builtin/providers/aws/resource_aws_vpn_connection.go new file mode 100644 index 000000000000..e44b2a5d533b --- /dev/null +++ b/builtin/providers/aws/resource_aws_vpn_connection.go @@ -0,0 +1,356 @@ +package aws + +import ( + "bytes" + "fmt" + "log" + "time" + + "github.com/awslabs/aws-sdk-go/aws" + "github.com/awslabs/aws-sdk-go/service/ec2" + + "github.com/hashicorp/terraform/helper/hashcode" + "github.com/hashicorp/terraform/helper/resource" + "github.com/hashicorp/terraform/helper/schema" +) + +func resourceAwsVpnConnection() *schema.Resource { + return &schema.Resource{ + Create: resourceAwsVpnConnectionCreate, + Read: resourceAwsVpnConnectionRead, + Update: resourceAwsVpnConnectionUpdate, + Delete: resourceAwsVpnConnectionDelete, + + Schema: map[string]*schema.Schema{ + "vpn_gateway_id": &schema.Schema{ + Type: schema.TypeString, + Required: true, + ForceNew: true, + }, + + "customer_gateway_id": &schema.Schema{ + Type: schema.TypeString, + Required: true, + ForceNew: true, + }, + + "type": &schema.Schema{ + Type: schema.TypeString, + Required: true, + ForceNew: true, + }, + + "static_routes_only": &schema.Schema{ + Type: schema.TypeBool, + Required: true, + ForceNew: true, + }, + + "tags": tagsSchema(), + + // Begin read only attributes + "customer_gateway_configuration": &schema.Schema{ + Type: schema.TypeString, + Computed: true, + Optional: true, + }, + + "routes": &schema.Schema{ + Type: schema.TypeSet, + Computed: true, + Optional: true, + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "destination_cidr_block": &schema.Schema{ + Type: schema.TypeString, + Computed: true, + Optional: true, + }, + + "source": &schema.Schema{ + Type: schema.TypeString, + Computed: true, + Optional: true, + }, + + "state": &schema.Schema{ + Type: schema.TypeString, + Computed: true, + Optional: true, + }, + }, + }, + Set: func(v interface{}) int { + var buf bytes.Buffer + m := v.(map[string]interface{}) + buf.WriteString(fmt.Sprintf("%s-", m["destination_cidr_block"].(string))) + buf.WriteString(fmt.Sprintf("%s-", m["source"].(string))) + buf.WriteString(fmt.Sprintf("%s-", m["state"].(string))) + return hashcode.String(buf.String()) + }, + }, + + "vgw_telemetry": &schema.Schema{ + Type: schema.TypeSet, + Computed: true, + Optional: true, + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "accepted_route_count": &schema.Schema{ + Type: schema.TypeInt, + Computed: true, + Optional: true, + }, + + "last_status_change": &schema.Schema{ + Type: schema.TypeString, + Computed: true, + Optional: true, + }, + + "outside_ip_address": &schema.Schema{ + Type: schema.TypeString, + Computed: true, + Optional: true, + }, + + "status": &schema.Schema{ + Type: schema.TypeString, + Computed: true, + Optional: true, + }, + + "status_message": &schema.Schema{ + Type: schema.TypeString, + Computed: true, + Optional: true, + }, + }, + }, + Set: func(v interface{}) int { + var buf bytes.Buffer + m := v.(map[string]interface{}) + buf.WriteString(fmt.Sprintf("%s-", m["outside_ip_address"].(string))) + return hashcode.String(buf.String()) + }, + }, + }, + } +} + +func resourceAwsVpnConnectionCreate(d *schema.ResourceData, meta interface{}) error { + conn := meta.(*AWSClient).ec2conn + + connectOpts := &ec2.VPNConnectionOptionsSpecification{ + StaticRoutesOnly: aws.Boolean(d.Get("static_routes_only").(bool)), + } + + createOpts := &ec2.CreateVPNConnectionInput{ + CustomerGatewayID: aws.String(d.Get("customer_gateway_id").(string)), + Options: connectOpts, + Type: aws.String(d.Get("type").(string)), + VPNGatewayID: aws.String(d.Get("vpn_gateway_id").(string)), + } + + // Create the VPN Connection + log.Printf("[DEBUG] Creating vpn connection") + resp, err := conn.CreateVPNConnection(createOpts) + if err != nil { + return fmt.Errorf("Error creating vpn connection: %s", err) + } + + // Store the ID + vpnConnection := resp.VPNConnection + d.SetId(*vpnConnection.VPNConnectionID) + log.Printf("[INFO] VPN connection ID: %s", *vpnConnection.VPNConnectionID) + + // Wait for the connection to become available. This has an obscenely + // high default timeout because AWS VPN connections are notoriously + // slow at coming up or going down. There's also no point in checking + // more frequently than every ten seconds. + stateConf := &resource.StateChangeConf{ + Pending: []string{"pending"}, + Target: "available", + Refresh: vpnConnectionRefreshFunc(conn, *vpnConnection.VPNConnectionID), + Timeout: 30 * time.Minute, + Delay: 10 * time.Second, + MinTimeout: 10 * time.Second, + } + + _, stateErr := stateConf.WaitForState() + if stateErr != nil { + return fmt.Errorf( + "Error waiting for VPN connection (%s) to become ready: %s", + *vpnConnection.VPNConnectionID, err) + } + + // Create tags. + if err := setTagsSDK(conn, d); err != nil { + return err + } + + // Read off the API to populate our RO fields. + return resourceAwsVpnConnectionRead(d, meta) +} + +func vpnConnectionRefreshFunc(conn *ec2.EC2, connectionId string) resource.StateRefreshFunc { + return func() (interface{}, string, error) { + resp, err := conn.DescribeVPNConnections(&ec2.DescribeVPNConnectionsInput{ + VPNConnectionIDs: []*string{aws.String(connectionId)}, + }) + + if err != nil { + if ec2err, ok := err.(aws.APIError); ok && ec2err.Code == "InvalidVpnConnectionID.NotFound" { + resp = nil + } else { + log.Printf("Error on VPNConnectionRefresh: %s", err) + return nil, "", err + } + } + + if resp == nil || len(resp.VPNConnections) == 0 { + return nil, "", nil + } + + connection := resp.VPNConnections[0] + return connection, *connection.State, nil + } +} + +func resourceAwsVpnConnectionRead(d *schema.ResourceData, meta interface{}) error { + conn := meta.(*AWSClient).ec2conn + + resp, err := conn.DescribeVPNConnections(&ec2.DescribeVPNConnectionsInput{ + VPNConnectionIDs: []*string{aws.String(d.Id())}, + }) + if err != nil { + if ec2err, ok := err.(aws.APIError); ok && ec2err.Code == "InvalidVpnConnectionID.NotFound" { + d.SetId("") + return nil + } else { + log.Printf("[ERROR] Error finding VPN connection: %s", err) + return err + } + } + + if len(resp.VPNConnections) != 1 { + return fmt.Errorf("[ERROR] Error finding VPN connection: %s", d.Id()) + } + + vpnConnection := resp.VPNConnections[0] + + // Set attributes under the user's control. + d.Set("vpn_gateway_id", vpnConnection.VPNGatewayID) + d.Set("customer_gateway_id", vpnConnection.CustomerGatewayID) + d.Set("type", vpnConnection.Type) + d.Set("tags", tagsToMapSDK(vpnConnection.Tags)) + + if vpnConnection.Options != nil { + if err := d.Set("static_routes_only", vpnConnection.Options.StaticRoutesOnly); err != nil { + return err + } + } + + // Set read only attributes. + d.Set("customer_gateway_configuration", vpnConnection.CustomerGatewayConfiguration) + if err := d.Set("vgw_telemetry", telemetryToMapList(vpnConnection.VGWTelemetry)); err != nil { + return err + } + if vpnConnection.Routes != nil { + if err := d.Set("routes", routesToMapList(vpnConnection.Routes)); err != nil { + return err + } + } + + return nil +} + +func resourceAwsVpnConnectionUpdate(d *schema.ResourceData, meta interface{}) error { + conn := meta.(*AWSClient).ec2conn + + // Update tags if required. + if err := setTagsSDK(conn, d); err != nil { + return err + } + + d.SetPartial("tags") + + return resourceAwsVpnConnectionRead(d, meta) +} + +func resourceAwsVpnConnectionDelete(d *schema.ResourceData, meta interface{}) error { + conn := meta.(*AWSClient).ec2conn + + _, err := conn.DeleteVPNConnection(&ec2.DeleteVPNConnectionInput{ + VPNConnectionID: aws.String(d.Id()), + }) + if err != nil { + if ec2err, ok := err.(aws.APIError); ok && ec2err.Code == "InvalidVpnConnectionID.NotFound" { + d.SetId("") + return nil + } else { + log.Printf("[ERROR] Error deleting VPN connection: %s", err) + return err + } + } + + // These things can take quite a while to tear themselves down and any + // attempt to modify resources they reference (e.g. CustomerGateways or + // VPN Gateways) before deletion will result in an error. Furthermore, + // they don't just disappear. The go into "deleted" state. We need to + // wait to ensure any other modifications the user might make to their + // VPC stack can safely run. + stateConf := &resource.StateChangeConf{ + Pending: []string{"deleting"}, + Target: "deleted", + Refresh: vpnConnectionRefreshFunc(conn, d.Id()), + Timeout: 30 * time.Minute, + Delay: 10 * time.Second, + MinTimeout: 10 * time.Second, + } + + _, stateErr := stateConf.WaitForState() + if stateErr != nil { + return fmt.Errorf( + "Error waiting for VPN connection (%s) to delete: %s", d.Id(), err) + } + + return nil +} + +// routesToMapList turns the list of routes into a list of maps. +func routesToMapList(routes []*ec2.VPNStaticRoute) []map[string]interface{} { + result := make([]map[string]interface{}, 0, len(routes)) + for _, r := range routes { + staticRoute := make(map[string]interface{}) + staticRoute["destination_cidr_block"] = *r.DestinationCIDRBlock + staticRoute["state"] = *r.State + + if r.Source != nil { + staticRoute["source"] = *r.Source + } + + result = append(result, staticRoute) + } + + return result +} + +// telemetryToMapList turns the VGW telemetry into a list of maps. +func telemetryToMapList(telemetry []*ec2.VGWTelemetry) []map[string]interface{} { + result := make([]map[string]interface{}, 0, len(telemetry)) + for _, t := range telemetry { + vgw := make(map[string]interface{}) + vgw["accepted_route_count"] = *t.AcceptedRouteCount + vgw["outside_ip_address"] = *t.OutsideIPAddress + vgw["status"] = *t.Status + vgw["status_message"] = *t.StatusMessage + + // LastStatusChange is a time.Time(). Convert it into a string + // so it can be handled by schema's type system. + vgw["last_status_change"] = t.LastStatusChange.String() + result = append(result, vgw) + } + + return result +} From f255fd8c426921d679bfacf113e7316820e54317 Mon Sep 17 00:00:00 2001 From: Christopher Tiwald Date: Fri, 1 May 2015 12:23:16 -0400 Subject: [PATCH 2/3] aws: Add acceptance tests for aws_vpn_connection resources. --- .../aws/resource_aws_vpn_connection_test.go | 138 ++++++++++++++++++ 1 file changed, 138 insertions(+) create mode 100644 builtin/providers/aws/resource_aws_vpn_connection_test.go diff --git a/builtin/providers/aws/resource_aws_vpn_connection_test.go b/builtin/providers/aws/resource_aws_vpn_connection_test.go new file mode 100644 index 000000000000..e416606a9ac4 --- /dev/null +++ b/builtin/providers/aws/resource_aws_vpn_connection_test.go @@ -0,0 +1,138 @@ +package aws + +import ( + "fmt" + "testing" + + "github.com/awslabs/aws-sdk-go/aws" + "github.com/awslabs/aws-sdk-go/service/ec2" + + "github.com/hashicorp/terraform/helper/resource" + "github.com/hashicorp/terraform/terraform" +) + +func TestAccAwsVpnConnection(t *testing.T) { + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + Providers: testAccProviders, + CheckDestroy: testAccAwsVpnConnectionDestroy, + Steps: []resource.TestStep{ + resource.TestStep{ + Config: testAccAwsVpnConnectionConfig, + Check: resource.ComposeTestCheckFunc( + testAccAwsVpnConnection( + "aws_vpc.vpc", + "aws_vpn_gateway.vpn_gateway", + "aws_customer_gateway.customer_gateway", + "aws_vpn_connection.foo", + ), + ), + }, + resource.TestStep{ + Config: testAccAwsVpnConnectionConfigUpdate, + Check: resource.ComposeTestCheckFunc( + testAccAwsVpnConnection( + "aws_vpc.vpc", + "aws_vpn_gateway.vpn_gateway", + "aws_customer_gateway.customer_gateway", + "aws_vpn_connection.bar", + ), + ), + }, + }, + }) +} + +func testAccAwsVpnConnectionDestroy(s *terraform.State) error { + if len(s.RootModule().Resources) > 0 { + return fmt.Errorf("Expected all resources to be gone, but found: %#v", s.RootModule().Resources) + } + + return nil +} + +func testAccAwsVpnConnection( + vpcResource string, + vpnGatewayResource string, + customerGatewayResource string, + vpnConnectionResource string) resource.TestCheckFunc { + return func(s *terraform.State) error { + rs, ok := s.RootModule().Resources[vpnConnectionResource] + if !ok { + return fmt.Errorf("Not found: %s", vpnConnectionResource) + } + + if rs.Primary.ID == "" { + return fmt.Errorf("No ID is set") + } + connection, ok := s.RootModule().Resources[vpnConnectionResource] + if !ok { + return fmt.Errorf("Not found: %s", vpnConnectionResource) + } + + ec2conn := testAccProvider.Meta().(*AWSClient).ec2conn + + _, err := ec2conn.DescribeVPNConnections(&ec2.DescribeVPNConnectionsInput{ + VPNConnectionIDs: []*string{aws.String(connection.Primary.ID)}, + }) + + if err != nil { + return err + } + + return nil + } +} + +const testAccAwsVpnConnectionConfig = ` +resource "aws_vpc" "vpc" { + cidr_block = "10.0.0.0/16" +} + +resource "aws_vpn_gateway" "vpn_gateway" { + vpc_id = "${aws_vpc.vpc.id}" +} + +resource "aws_customer_gateway" "customer_gateway" { + bgp_asn = 60000 + ip_address = "172.0.0.1" + type = ipsec.1 +} + +resource "aws_vpn_connection" "foo" { + vpn_gateway_id = "${aws_vpn_gateway.vpn_gateway.id}" + customer_gateway_id = "${aws_customer_gateway.customer_gateway.id}" + type = "ipsec.1" + static_routes_only = true +} +` + +const testAccAwsVpnConnectionConfigUpdate = ` +resource "aws_vpc" "vpc" { + cidr_block = "10.0.0.0/16" +} + +resource "aws_vpn_gateway" "vpn_gateway" { + vpc_id = "${aws_vpc.vpc.id}" +} + +resource "aws_customer_gateway" "customer_gateway" { + bgp_asn = 60000 + ip_address = "172.0.0.1" + type = ipsec.1 +} + +resource "aws_vpn_connection" "foo" { + vpn_gateway_id = "${aws_vpn_gateway.vpn_gateway.id}" + customer_gateway_id = "${aws_customer_gateway.customer_gateway.id}" + type = "ipsec.1" + static_routes_only = true +} + +resource "aws_vpn_connection" "bar" { + vpn_gateway_id = "${aws_vpn_gateway.vpn_gateway.id}" + customer_gateway_id = "${aws_customer_gateway.customer_gateway.id}" + type = "ipsec.1" + static_routes_only = false +} +` From 282c96f0e915899433b16cda55c0ceb9343b2254 Mon Sep 17 00:00:00 2001 From: Christopher Tiwald Date: Fri, 1 May 2015 12:23:39 -0400 Subject: [PATCH 3/3] aws: Add docs for aws_vpn_connection resources. --- .../aws/r/vpn_connection.html.markdown | 58 +++++++++++++++++++ website/source/layouts/aws.erb | 7 +++ 2 files changed, 65 insertions(+) create mode 100644 website/source/docs/providers/aws/r/vpn_connection.html.markdown diff --git a/website/source/docs/providers/aws/r/vpn_connection.html.markdown b/website/source/docs/providers/aws/r/vpn_connection.html.markdown new file mode 100644 index 000000000000..c48b6e77054c --- /dev/null +++ b/website/source/docs/providers/aws/r/vpn_connection.html.markdown @@ -0,0 +1,58 @@ +--- +layout: "aws" +page_title: "AWS: aws_vpn_connection" +sidebar_current: "docs-aws-vpn-connection" +description: |- + Provides a VPN connection connected to a VPC. These objects can be connected to customer gateways, and allow you to establish tunnels between your network and the VPC. +--- + +# aws\_vpn\_connection + + +Provides a VPN connection connected to a VPC. These objects can be connected to customer gateways, and allow you to establish tunnels between your network and the VPC. + +## Example Usage + +``` +resource "aws_vpc" "vpc" { + cidr_block = "10.0.0.0/16" +} + +resource "aws_vpn_gateway" "vpn_gateway" { + vpc_id = "${aws_vpc.vpc.id}" +} + +resource "aws_customer_gateway" "customer_gateway" { + bgp_asn = 60000 + ip_address = "172.0.0.1" + type = ipsec.1 +} + +resource "aws_vpn_connection" "main" { + vpn_gateway_id = "${aws_vpn_gateway.vpn_gateway.id}" + customer_gateway_id = "${aws_customer_gateway.customer_gateway.id}" + type = "ipsec.1" + static_routes_only = true +} +``` + +## Argument Reference + +The following arguments are supported: + +* `customer_gateway_id` - (Required) The ID of the customer gateway. +* `static_routes_only` - (Required) Whether the VPN connection uses static routes exclusively. Static routes must be used for devices that don't support BGP. * `tags` - (Optional) Tags to apply to the connection. +* `type` - (Required) The type of VPN connection. The only type AWS supports at this time is "ipsec.1". +* `vpn_gateway_id` - (Required) The ID of the virtual private gateway. + +## Attrubute Reference + +The following attributes are exported: + +* `id` - The amazon-assigned ID of the VPN connection. +* `customer_gateway_configuration` - The configuration information for the VPN connection's customer gateway (in the native XML format). +* `customer_gateway_id` - The ID of the customer gateway to which the connection is attached. +* `static_routes_only` - Whether the VPN connection uses static routes exclusively. +* `tags` - Tags applied to the connection. +* `type` - The type of VPN connection. +* `vpn_gateway_id` - The ID of the virtual private gateway to which the connection is attached. diff --git a/website/source/layouts/aws.erb b/website/source/layouts/aws.erb index b10ac513a34f..4e83e735d8a8 100644 --- a/website/source/layouts/aws.erb +++ b/website/source/layouts/aws.erb @@ -118,8 +118,15 @@ > aws_vpc_dhcp_options_association + + > + aws_vpn_connection + > + aws_vpn_gateway + + > aws_vpn_gateway