diff --git a/CHANGELOG.md b/CHANGELOG.md index 9769581af..a6c694a37 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -14,6 +14,7 @@ FEATURES: * **New provider: `dme` (DNSMadeEasy)** [GH-855] * **New command: `taint`** - Manually mark a resource as tainted, causing a destroy and recreate on the next plan/apply. + * **New resource: `aws_vpn_gateway`** [GH-1137] * **Self-variables** can be used to reference the current resource's attributes within a provisioner. Ex. `${self.private_ip_address}` [GH-1033] * **Continous state** saving during `terraform apply`. The state file is diff --git a/builtin/providers/aws/provider.go b/builtin/providers/aws/provider.go index 4963bfa48..50596512e 100644 --- a/builtin/providers/aws/provider.go +++ b/builtin/providers/aws/provider.go @@ -68,6 +68,7 @@ func Provider() terraform.ResourceProvider { "aws_subnet": resourceAwsSubnet(), "aws_vpc": resourceAwsVpc(), "aws_vpc_peering_connection": resourceAwsVpcPeeringConnection(), + "aws_vpn_gateway": resourceAwsVpnGateway(), }, ConfigureFunc: providerConfigure, diff --git a/builtin/providers/aws/resource_aws_vpn_gateway.go b/builtin/providers/aws/resource_aws_vpn_gateway.go new file mode 100644 index 000000000..b6ecba581 --- /dev/null +++ b/builtin/providers/aws/resource_aws_vpn_gateway.go @@ -0,0 +1,318 @@ +package aws + +import ( + "fmt" + "log" + "time" + + "github.com/hashicorp/aws-sdk-go/aws" + "github.com/hashicorp/aws-sdk-go/gen/ec2" + "github.com/hashicorp/terraform/helper/resource" + "github.com/hashicorp/terraform/helper/schema" +) + +func resourceAwsVpnGateway() *schema.Resource { + return &schema.Resource{ + Create: resourceAwsVpnGatewayCreate, + Read: resourceAwsVpnGatewayRead, + Update: resourceAwsVpnGatewayUpdate, + Delete: resourceAwsVpnGatewayDelete, + + Schema: map[string]*schema.Schema{ + "availability_zone": &schema.Schema{ + Type: schema.TypeString, + Optional: true, + ForceNew: true, + }, + + "vpc_id": &schema.Schema{ + Type: schema.TypeString, + Optional: true, + }, + + "tags": tagsSchema(), + }, + } +} + +func resourceAwsVpnGatewayCreate(d *schema.ResourceData, meta interface{}) error { + ec2conn := meta.(*AWSClient).ec2conn + + createOpts := &ec2.CreateVPNGatewayRequest{ + AvailabilityZone: aws.String(d.Get("availability_zone").(string)), + Type: aws.String("ipsec.1"), + } + + // Create the VPN gateway + log.Printf("[DEBUG] Creating VPN gateway") + resp, err := ec2conn.CreateVPNGateway(createOpts) + if err != nil { + return fmt.Errorf("Error creating VPN gateway: %s", err) + } + + // Get the ID and store it + vpnGateway := resp.VPNGateway + d.SetId(*vpnGateway.VPNGatewayID) + log.Printf("[INFO] VPN Gateway ID: %s", *vpnGateway.VPNGatewayID) + + // Attach the VPN gateway to the correct VPC + return resourceAwsVpnGatewayUpdate(d, meta) +} + +func resourceAwsVpnGatewayRead(d *schema.ResourceData, meta interface{}) error { + ec2conn := meta.(*AWSClient).ec2conn + + vpnGatewayRaw, _, err := vpnGatewayStateRefreshFunc(ec2conn, d.Id())() + if err != nil { + return err + } + if vpnGatewayRaw == nil { + // Seems we have lost our VPN gateway + d.SetId("") + return nil + } + + vpnGateway := vpnGatewayRaw.(*ec2.VPNGateway) + if len(vpnGateway.VPCAttachments) == 0 { + // Gateway exists but not attached to the VPC + d.Set("vpc_id", "") + } else { + d.Set("vpc_id", vpnGateway.VPCAttachments[0].VPCID) + } + d.Set("availability_zone", vpnGateway.AvailabilityZone) + d.Set("tags", tagsToMap(vpnGateway.Tags)) + + return nil +} + +func resourceAwsVpnGatewayUpdate(d *schema.ResourceData, meta interface{}) error { + if d.HasChange("vpc_id") { + // If we're already attached, detach it first + if err := resourceAwsVpnGatewayDetach(d, meta); err != nil { + return err + } + + // Attach the VPN gateway to the new vpc + if err := resourceAwsVpnGatewayAttach(d, meta); err != nil { + return err + } + } + + ec2conn := meta.(*AWSClient).ec2conn + + if err := setTags(ec2conn, d); err != nil { + return err + } + + d.SetPartial("tags") + + return resourceAwsVpnGatewayRead(d, meta) +} + +func resourceAwsVpnGatewayDelete(d *schema.ResourceData, meta interface{}) error { + ec2conn := meta.(*AWSClient).ec2conn + + // Detach if it is attached + if err := resourceAwsVpnGatewayDetach(d, meta); err != nil { + return err + } + + log.Printf("[INFO] Deleting VPN gateway: %s", d.Id()) + + return resource.Retry(5*time.Minute, func() error { + err := ec2conn.DeleteVPNGateway(&ec2.DeleteVPNGatewayRequest{ + VPNGatewayID: aws.String(d.Id()), + }) + if err == nil { + return nil + } + + ec2err, ok := err.(aws.APIError) + if !ok { + return err + } + + switch ec2err.Code { + case "InvalidVpnGatewayID.NotFound": + return nil + case "IncorrectState": + return err // retry + } + + return resource.RetryError{Err: err} + }) +} + +func resourceAwsVpnGatewayAttach(d *schema.ResourceData, meta interface{}) error { + ec2conn := meta.(*AWSClient).ec2conn + + if d.Get("vpc_id").(string) == "" { + log.Printf( + "[DEBUG] Not attaching VPN Gateway '%s' as no VPC ID is set", + d.Id()) + return nil + } + + log.Printf( + "[INFO] Attaching VPN Gateway '%s' to VPC '%s'", + d.Id(), + d.Get("vpc_id").(string)) + + _, err := ec2conn.AttachVPNGateway(&ec2.AttachVPNGatewayRequest{ + VPNGatewayID: aws.String(d.Id()), + VPCID: aws.String(d.Get("vpc_id").(string)), + }) + if err != nil { + return err + } + + // A note on the states below: the AWS docs (as of July, 2014) say + // that the states would be: attached, attaching, detached, detaching, + // but when running, I noticed that the state is usually "available" when + // it is attached. + + // Wait for it to be fully attached before continuing + log.Printf("[DEBUG] Waiting for VPN gateway (%s) to attach", d.Id()) + stateConf := &resource.StateChangeConf{ + Pending: []string{"detached", "attaching"}, + Target: "available", + Refresh: VpnGatewayAttachStateRefreshFunc(ec2conn, d.Id(), "available"), + Timeout: 1 * time.Minute, + } + if _, err := stateConf.WaitForState(); err != nil { + return fmt.Errorf( + "Error waiting for VPN gateway (%s) to attach: %s", + d.Id(), err) + } + + return nil +} + +func resourceAwsVpnGatewayDetach(d *schema.ResourceData, meta interface{}) error { + ec2conn := meta.(*AWSClient).ec2conn + + // Get the old VPC ID to detach from + vpcID, _ := d.GetChange("vpc_id") + + if vpcID.(string) == "" { + log.Printf( + "[DEBUG] Not detaching VPN Gateway '%s' as no VPC ID is set", + d.Id()) + return nil + } + + log.Printf( + "[INFO] Detaching VPN Gateway '%s' from VPC '%s'", + d.Id(), + vpcID.(string)) + + wait := true + err := ec2conn.DetachVPNGateway(&ec2.DetachVPNGatewayRequest{ + VPNGatewayID: aws.String(d.Id()), + VPCID: aws.String(d.Get("vpc_id").(string)), + }) + if err != nil { + ec2err, ok := err.(aws.APIError) + if ok { + if ec2err.Code == "InvalidVpnGatewayID.NotFound" { + err = nil + wait = false + } else if ec2err.Code == "InvalidVpnGatewayAttachment.NotFound" { + err = nil + wait = false + } + } + + if err != nil { + return err + } + } + + if !wait { + return nil + } + + // Wait for it to be fully detached before continuing + log.Printf("[DEBUG] Waiting for VPN gateway (%s) to detach", d.Id()) + stateConf := &resource.StateChangeConf{ + Pending: []string{"attached", "detaching", "available"}, + Target: "detached", + Refresh: VpnGatewayAttachStateRefreshFunc(ec2conn, d.Id(), "detached"), + Timeout: 1 * time.Minute, + } + if _, err := stateConf.WaitForState(); err != nil { + return fmt.Errorf( + "Error waiting for vpn gateway (%s) to detach: %s", + d.Id(), err) + } + + return nil +} + +// vpnGatewayStateRefreshFunc returns a resource.StateRefreshFunc that is used to watch a VPNGateway. +func vpnGatewayStateRefreshFunc(conn *ec2.EC2, id string) resource.StateRefreshFunc { + return func() (interface{}, string, error) { + resp, err := conn.DescribeVPNGateways(&ec2.DescribeVPNGatewaysRequest{ + VPNGatewayIDs: []string{id}, + }) + if err != nil { + if ec2err, ok := err.(aws.APIError); ok && ec2err.Code == "InvalidVpnGatewayID.NotFound" { + resp = nil + } else { + log.Printf("[ERROR] Error on VpnGatewayStateRefresh: %s", err) + return nil, "", err + } + } + + if resp == nil { + // Sometimes AWS just has consistency issues and doesn't see + // our instance yet. Return an empty state. + return nil, "", nil + } + + vpnGateway := &resp.VPNGateways[0] + return vpnGateway, *vpnGateway.State, nil + } +} + +// VpnGatewayAttachStateRefreshFunc returns a resource.StateRefreshFunc that is used to watch +// the state of a VPN gateway's attachment +func VpnGatewayAttachStateRefreshFunc(conn *ec2.EC2, id string, expected string) resource.StateRefreshFunc { + var start time.Time + return func() (interface{}, string, error) { + if start.IsZero() { + start = time.Now() + } + + resp, err := conn.DescribeVPNGateways(&ec2.DescribeVPNGatewaysRequest{ + VPNGatewayIDs: []string{id}, + }) + if err != nil { + if ec2err, ok := err.(aws.APIError); ok && ec2err.Code == "InvalidVpnGatewayID.NotFound" { + resp = nil + } else { + log.Printf("[ERROR] Error on VpnGatewayStateRefresh: %s", err) + return nil, "", err + } + } + + if resp == nil { + // Sometimes AWS just has consistency issues and doesn't see + // our instance yet. Return an empty state. + return nil, "", nil + } + + vpnGateway := &resp.VPNGateways[0] + + if time.Now().Sub(start) > 10*time.Second { + return vpnGateway, expected, nil + } + + if len(vpnGateway.VPCAttachments) == 0 { + // No attachments, we're detached + return vpnGateway, "detached", nil + } + + return vpnGateway, *vpnGateway.VPCAttachments[0].State, nil + } +} diff --git a/builtin/providers/aws/resource_aws_vpn_gateway_test.go b/builtin/providers/aws/resource_aws_vpn_gateway_test.go new file mode 100644 index 000000000..21ccb980c --- /dev/null +++ b/builtin/providers/aws/resource_aws_vpn_gateway_test.go @@ -0,0 +1,232 @@ +package aws + +import ( + "fmt" + "testing" + + "github.com/hashicorp/aws-sdk-go/aws" + "github.com/hashicorp/aws-sdk-go/gen/ec2" + "github.com/hashicorp/terraform/helper/resource" + "github.com/hashicorp/terraform/terraform" +) + +func TestAccAWSVpnGateway(t *testing.T) { + var v, v2 ec2.VPNGateway + + testNotEqual := func(*terraform.State) error { + if len(v.VPCAttachments) == 0 { + return fmt.Errorf("VPN gateway A is not attached") + } + if len(v2.VPCAttachments) == 0 { + return fmt.Errorf("VPN gateway B is not attached") + } + + id1 := v.VPCAttachments[0].VPCID + id2 := v2.VPCAttachments[0].VPCID + if id1 == id2 { + return fmt.Errorf("Both attachment IDs are the same") + } + + return nil + } + + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + Providers: testAccProviders, + CheckDestroy: testAccCheckVpnGatewayDestroy, + Steps: []resource.TestStep{ + resource.TestStep{ + Config: testAccVpnGatewayConfig, + Check: resource.ComposeTestCheckFunc( + testAccCheckVpnGatewayExists( + "aws_vpn_gateway.foo", &v), + ), + }, + + resource.TestStep{ + Config: testAccVpnGatewayConfigChangeVPC, + Check: resource.ComposeTestCheckFunc( + testAccCheckVpnGatewayExists( + "aws_vpn_gateway.foo", &v2), + testNotEqual, + ), + }, + }, + }) +} + +func TestAccAWSVpnGateway_delete(t *testing.T) { + var vpnGateway ec2.VPNGateway + + testDeleted := func(r string) resource.TestCheckFunc { + return func(s *terraform.State) error { + _, ok := s.RootModule().Resources[r] + if ok { + return fmt.Errorf("VPN Gateway %q should have been deleted", r) + } + return nil + } + } + + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + Providers: testAccProviders, + CheckDestroy: testAccCheckVpnGatewayDestroy, + Steps: []resource.TestStep{ + resource.TestStep{ + Config: testAccVpnGatewayConfig, + Check: resource.ComposeTestCheckFunc( + testAccCheckVpnGatewayExists("aws_vpn_gateway.foo", &vpnGateway)), + }, + resource.TestStep{ + Config: testAccNoVpnGatewayConfig, + Check: resource.ComposeTestCheckFunc(testDeleted("aws_vpn_gateway.foo")), + }, + }, + }) +} + +func TestAccVpnGateway_tags(t *testing.T) { + var v ec2.VPNGateway + + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + Providers: testAccProviders, + CheckDestroy: testAccCheckVpnGatewayDestroy, + Steps: []resource.TestStep{ + resource.TestStep{ + Config: testAccCheckVpnGatewayConfigTags, + Check: resource.ComposeTestCheckFunc( + testAccCheckVpnGatewayExists("aws_vpn_gateway.foo", &v), + testAccCheckTags(&v.Tags, "foo", "bar"), + ), + }, + + resource.TestStep{ + Config: testAccCheckVpnGatewayConfigTagsUpdate, + Check: resource.ComposeTestCheckFunc( + testAccCheckVpnGatewayExists("aws_vpn_gateway.foo", &v), + testAccCheckTags(&v.Tags, "foo", ""), + testAccCheckTags(&v.Tags, "bar", "baz"), + ), + }, + }, + }) +} + +func testAccCheckVpnGatewayDestroy(s *terraform.State) error { + ec2conn := testAccProvider.Meta().(*AWSClient).ec2conn + + for _, rs := range s.RootModule().Resources { + if rs.Type != "aws_vpn_gateway" { + continue + } + + // Try to find the resource + resp, err := ec2conn.DescribeVPNGateways(&ec2.DescribeVPNGatewaysRequest{ + VPNGatewayIDs: []string{rs.Primary.ID}, + }) + if err == nil { + if len(resp.VPNGateways) > 0 { + return fmt.Errorf("still exists") + } + + return nil + } + + // Verify the error is what we want + ec2err, ok := err.(aws.APIError) + if !ok { + return err + } + if ec2err.Code != "InvalidVpnGatewayID.NotFound" { + return err + } + } + + return nil +} + +func testAccCheckVpnGatewayExists(n string, ig *ec2.VPNGateway) 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") + } + + ec2conn := testAccProvider.Meta().(*AWSClient).ec2conn + resp, err := ec2conn.DescribeVPNGateways(&ec2.DescribeVPNGatewaysRequest{ + VPNGatewayIDs: []string{rs.Primary.ID}, + }) + if err != nil { + return err + } + if len(resp.VPNGateways) == 0 { + return fmt.Errorf("VPNGateway not found") + } + + *ig = resp.VPNGateways[0] + + return nil + } +} + +const testAccNoVpnGatewayConfig = ` +resource "aws_vpc" "foo" { + cidr_block = "10.1.0.0/16" +} +` + +const testAccVpnGatewayConfig = ` +resource "aws_vpc" "foo" { + cidr_block = "10.1.0.0/16" +} + +resource "aws_vpn_gateway" "foo" { + vpc_id = "${aws_vpc.foo.id}" +} +` + +const testAccVpnGatewayConfigChangeVPC = ` +resource "aws_vpc" "foo" { + cidr_block = "10.1.0.0/16" +} + +resource "aws_vpc" "bar" { + cidr_block = "10.2.0.0/16" +} + +resource "aws_vpn_gateway" "foo" { + vpc_id = "${aws_vpc.bar.id}" +} +` + +const testAccCheckVpnGatewayConfigTags = ` +resource "aws_vpc" "foo" { + cidr_block = "10.1.0.0/16" +} + +resource "aws_vpn_gateway" "foo" { + vpc_id = "${aws_vpc.foo.id}" + tags { + foo = "bar" + } +} +` + +const testAccCheckVpnGatewayConfigTagsUpdate = ` +resource "aws_vpc" "foo" { + cidr_block = "10.1.0.0/16" +} + +resource "aws_vpn_gateway" "foo" { + vpc_id = "${aws_vpc.foo.id}" + tags { + bar = "baz" + } +} +` diff --git a/website/source/docs/providers/aws/r/vpn_gateway.html.markdown b/website/source/docs/providers/aws/r/vpn_gateway.html.markdown new file mode 100644 index 000000000..b64000ce5 --- /dev/null +++ b/website/source/docs/providers/aws/r/vpn_gateway.html.markdown @@ -0,0 +1,38 @@ +--- +layout: "aws" +page_title: "AWS: aws_vpn_gateway" +sidebar_current: "docs-aws-resource-vpn-gateway" +description: |- + Provides a resource to create a VPC VPN Gateway. +--- + +# aws\_vpn\_gateway + +Provides a resource to create a VPC VPN Gateway. + +## Example Usage + +``` +resource "aws_vpn_gateway" "vpn_gw" { + vpc_id = "${aws_vpc.main.id}" + + tags { + Name = "main" + } +} +``` + +## Argument Reference + +The following arguments are supported: + +* `vpc_id` - (Required) The VPC ID to create in. +* `availability_zone` - (Optional) The Availability Zone for the virtual private gateway. +* `tags` - (Optional) A mapping of tags to assign to the resource. + +## Attributes Reference + +The following attributes are exported: + +* `id` - The ID of the VPN Gateway. + diff --git a/website/source/layouts/aws.erb b/website/source/layouts/aws.erb index 0bcff9cd8..8e5875ecb 100644 --- a/website/source/layouts/aws.erb +++ b/website/source/layouts/aws.erb @@ -98,7 +98,11 @@ > aws_vpc_peering - + + > + aws_vpn_gateway + +