From 814403f45cc7a0552d765acc37c12ce08429d6ca Mon Sep 17 00:00:00 2001 From: Christopher Tiwald Date: Fri, 1 May 2015 12:22:52 -0400 Subject: [PATCH] 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 4e2c2b570..5ad97b920 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 000000000..e44b2a5d5 --- /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 +}