485 lines
13 KiB
Go
485 lines
13 KiB
Go
package aws
|
|
|
|
import (
|
|
"bytes"
|
|
"encoding/xml"
|
|
"fmt"
|
|
"log"
|
|
"sort"
|
|
"time"
|
|
|
|
"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/errwrap"
|
|
"github.com/hashicorp/terraform/helper/hashcode"
|
|
"github.com/hashicorp/terraform/helper/resource"
|
|
"github.com/hashicorp/terraform/helper/schema"
|
|
)
|
|
|
|
type XmlVpnConnectionConfig struct {
|
|
Tunnels []XmlIpsecTunnel `xml:"ipsec_tunnel"`
|
|
}
|
|
|
|
type XmlIpsecTunnel struct {
|
|
OutsideAddress string `xml:"vpn_gateway>tunnel_outside_address>ip_address"`
|
|
PreSharedKey string `xml:"ike>pre_shared_key"`
|
|
CgwInsideAddress string `xml:"customer_gateway>tunnel_inside_address>ip_address"`
|
|
VgwInsideAddress string `xml:"vpn_gateway>tunnel_inside_address>ip_address"`
|
|
}
|
|
|
|
type TunnelInfo struct {
|
|
Tunnel1Address string
|
|
Tunnel1CgwInsideAddress string
|
|
Tunnel1VgwInsideAddress string
|
|
Tunnel1PreSharedKey string
|
|
Tunnel2Address string
|
|
Tunnel2CgwInsideAddress string
|
|
Tunnel2VgwInsideAddress string
|
|
Tunnel2PreSharedKey string
|
|
}
|
|
|
|
func (slice XmlVpnConnectionConfig) Len() int {
|
|
return len(slice.Tunnels)
|
|
}
|
|
|
|
func (slice XmlVpnConnectionConfig) Less(i, j int) bool {
|
|
return slice.Tunnels[i].OutsideAddress < slice.Tunnels[j].OutsideAddress
|
|
}
|
|
|
|
func (slice XmlVpnConnectionConfig) Swap(i, j int) {
|
|
slice.Tunnels[i], slice.Tunnels[j] = slice.Tunnels[j], slice.Tunnels[i]
|
|
}
|
|
|
|
func resourceAwsVpnConnection() *schema.Resource {
|
|
return &schema.Resource{
|
|
Create: resourceAwsVpnConnectionCreate,
|
|
Read: resourceAwsVpnConnectionRead,
|
|
Update: resourceAwsVpnConnectionUpdate,
|
|
Delete: resourceAwsVpnConnectionDelete,
|
|
Importer: &schema.ResourceImporter{
|
|
State: schema.ImportStatePassthrough,
|
|
},
|
|
|
|
Schema: map[string]*schema.Schema{
|
|
"vpn_gateway_id": {
|
|
Type: schema.TypeString,
|
|
Required: true,
|
|
ForceNew: true,
|
|
},
|
|
|
|
"customer_gateway_id": {
|
|
Type: schema.TypeString,
|
|
Required: true,
|
|
ForceNew: true,
|
|
},
|
|
|
|
"type": {
|
|
Type: schema.TypeString,
|
|
Required: true,
|
|
ForceNew: true,
|
|
},
|
|
|
|
"static_routes_only": {
|
|
Type: schema.TypeBool,
|
|
Optional: true,
|
|
Computed: true,
|
|
ForceNew: true,
|
|
},
|
|
|
|
"tags": tagsSchema(),
|
|
|
|
// Begin read only attributes
|
|
"customer_gateway_configuration": {
|
|
Type: schema.TypeString,
|
|
Computed: true,
|
|
Optional: true,
|
|
},
|
|
|
|
"tunnel1_address": {
|
|
Type: schema.TypeString,
|
|
Computed: true,
|
|
},
|
|
|
|
"tunnel1_cgw_inside_address": {
|
|
Type: schema.TypeString,
|
|
Computed: true,
|
|
},
|
|
|
|
"tunnel1_vgw_inside_address": {
|
|
Type: schema.TypeString,
|
|
Computed: true,
|
|
},
|
|
|
|
"tunnel1_preshared_key": {
|
|
Type: schema.TypeString,
|
|
Computed: true,
|
|
},
|
|
|
|
"tunnel2_address": {
|
|
Type: schema.TypeString,
|
|
Computed: true,
|
|
},
|
|
|
|
"tunnel2_cgw_inside_address": {
|
|
Type: schema.TypeString,
|
|
Computed: true,
|
|
},
|
|
|
|
"tunnel2_vgw_inside_address": {
|
|
Type: schema.TypeString,
|
|
Computed: true,
|
|
},
|
|
|
|
"tunnel2_preshared_key": {
|
|
Type: schema.TypeString,
|
|
Computed: true,
|
|
},
|
|
|
|
"routes": {
|
|
Type: schema.TypeSet,
|
|
Computed: true,
|
|
Optional: true,
|
|
Elem: &schema.Resource{
|
|
Schema: map[string]*schema.Schema{
|
|
"destination_cidr_block": {
|
|
Type: schema.TypeString,
|
|
Computed: true,
|
|
Optional: true,
|
|
},
|
|
|
|
"source": {
|
|
Type: schema.TypeString,
|
|
Computed: true,
|
|
Optional: true,
|
|
},
|
|
|
|
"state": {
|
|
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": {
|
|
Type: schema.TypeSet,
|
|
Computed: true,
|
|
Optional: true,
|
|
Elem: &schema.Resource{
|
|
Schema: map[string]*schema.Schema{
|
|
"accepted_route_count": {
|
|
Type: schema.TypeInt,
|
|
Computed: true,
|
|
Optional: true,
|
|
},
|
|
|
|
"last_status_change": {
|
|
Type: schema.TypeString,
|
|
Computed: true,
|
|
Optional: true,
|
|
},
|
|
|
|
"outside_ip_address": {
|
|
Type: schema.TypeString,
|
|
Computed: true,
|
|
Optional: true,
|
|
},
|
|
|
|
"status": {
|
|
Type: schema.TypeString,
|
|
Computed: true,
|
|
Optional: true,
|
|
},
|
|
|
|
"status_message": {
|
|
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.Bool(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: []string{"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 := setTags(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.(awserr.Error); 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.(awserr.Error); 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]
|
|
if vpnConnection == nil || *vpnConnection.State == "deleted" {
|
|
// Seems we have lost our VPN Connection
|
|
d.SetId("")
|
|
return nil
|
|
}
|
|
|
|
// 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", tagsToMap(vpnConnection.Tags))
|
|
|
|
if vpnConnection.Options != nil {
|
|
if err := d.Set("static_routes_only", vpnConnection.Options.StaticRoutesOnly); err != nil {
|
|
return err
|
|
}
|
|
} else {
|
|
//If there no Options on the connection then we do not support *static_routes*
|
|
d.Set("static_routes_only", false)
|
|
}
|
|
|
|
// Set read only attributes.
|
|
d.Set("customer_gateway_configuration", vpnConnection.CustomerGatewayConfiguration)
|
|
|
|
if vpnConnection.CustomerGatewayConfiguration != nil {
|
|
if tunnelInfo, err := xmlConfigToTunnelInfo(*vpnConnection.CustomerGatewayConfiguration); err != nil {
|
|
log.Printf("[ERR] Error unmarshaling XML configuration for (%s): %s", d.Id(), err)
|
|
} else {
|
|
d.Set("tunnel1_address", tunnelInfo.Tunnel1Address)
|
|
d.Set("tunnel1_cgw_inside_address", tunnelInfo.Tunnel1CgwInsideAddress)
|
|
d.Set("tunnel1_vgw_inside_address", tunnelInfo.Tunnel1VgwInsideAddress)
|
|
d.Set("tunnel1_preshared_key", tunnelInfo.Tunnel1PreSharedKey)
|
|
d.Set("tunnel2_address", tunnelInfo.Tunnel2Address)
|
|
d.Set("tunnel2_preshared_key", tunnelInfo.Tunnel2PreSharedKey)
|
|
d.Set("tunnel2_cgw_inside_address", tunnelInfo.Tunnel2CgwInsideAddress)
|
|
d.Set("tunnel2_vgw_inside_address", tunnelInfo.Tunnel2VgwInsideAddress)
|
|
}
|
|
}
|
|
|
|
if err := d.Set("vgw_telemetry", telemetryToMapList(vpnConnection.VgwTelemetry)); err != nil {
|
|
return err
|
|
}
|
|
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 := setTags(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.(awserr.Error); 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: []string{"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
|
|
}
|
|
|
|
func xmlConfigToTunnelInfo(xmlConfig string) (*TunnelInfo, error) {
|
|
var vpnConfig XmlVpnConnectionConfig
|
|
if err := xml.Unmarshal([]byte(xmlConfig), &vpnConfig); err != nil {
|
|
return nil, errwrap.Wrapf("Error Unmarshalling XML: {{err}}", err)
|
|
}
|
|
|
|
// don't expect consistent ordering from the XML
|
|
sort.Sort(vpnConfig)
|
|
|
|
tunnelInfo := TunnelInfo{
|
|
Tunnel1Address: vpnConfig.Tunnels[0].OutsideAddress,
|
|
Tunnel1PreSharedKey: vpnConfig.Tunnels[0].PreSharedKey,
|
|
Tunnel1CgwInsideAddress: vpnConfig.Tunnels[0].CgwInsideAddress,
|
|
Tunnel1VgwInsideAddress: vpnConfig.Tunnels[0].VgwInsideAddress,
|
|
|
|
Tunnel2Address: vpnConfig.Tunnels[1].OutsideAddress,
|
|
Tunnel2PreSharedKey: vpnConfig.Tunnels[1].PreSharedKey,
|
|
Tunnel2CgwInsideAddress: vpnConfig.Tunnels[1].CgwInsideAddress,
|
|
Tunnel2VgwInsideAddress: vpnConfig.Tunnels[1].VgwInsideAddress,
|
|
}
|
|
|
|
return &tunnelInfo, nil
|
|
}
|