Add aws_vpn_gateway_attachment resource. (#7870)

This commit adds VPN Gateway attachment resource, and also an initial tests and
documentation stubs.

Signed-off-by: Krzysztof Wilczynski <krzysztof.wilczynski@linux.com>
This commit is contained in:
Krzysztof Wilczynski 2016-08-07 08:29:51 +09:00 committed by Paul Stack
parent de822d909c
commit 9c54e9c955
7 changed files with 540 additions and 40 deletions

View File

@ -282,6 +282,7 @@ func Provider() terraform.ResourceProvider {
"aws_vpn_connection": resourceAwsVpnConnection(),
"aws_vpn_connection_route": resourceAwsVpnConnectionRoute(),
"aws_vpn_gateway": resourceAwsVpnGateway(),
"aws_vpn_gateway_attachment": resourceAwsVpnGatewayAttachment(),
},
ConfigureFunc: providerConfigure,
}

View File

@ -32,6 +32,7 @@ func resourceAwsVpnGateway() *schema.Resource {
"vpc_id": &schema.Schema{
Type: schema.TypeString,
Optional: true,
Computed: true,
},
"tags": tagsSchema(),
@ -80,17 +81,18 @@ func resourceAwsVpnGatewayRead(d *schema.ResourceData, meta interface{}) error {
}
vpnGateway := resp.VpnGateways[0]
if vpnGateway == nil {
if vpnGateway == nil || *vpnGateway.State == "deleted" {
// Seems we have lost our VPN gateway
d.SetId("")
return nil
}
if len(vpnGateway.VpcAttachments) == 0 || *vpnGateway.VpcAttachments[0].State == "detached" || *vpnGateway.VpcAttachments[0].State == "deleted" {
vpnAttachment := vpnGatewayGetAttachment(vpnGateway)
if len(vpnGateway.VpcAttachments) == 0 || *vpnAttachment.State == "detached" {
// Gateway exists but not attached to the VPC
d.Set("vpc_id", "")
} else {
d.Set("vpc_id", vpnGateway.VpcAttachments[0].VpcId)
d.Set("vpc_id", *vpnAttachment.VpcId)
}
d.Set("availability_zone", vpnGateway.AvailabilityZone)
d.Set("tags", tagsToMap(vpnGateway.Tags))
@ -301,12 +303,21 @@ func vpnGatewayAttachStateRefreshFunc(conn *ec2.EC2, id string, expected string)
}
vpnGateway := resp.VpnGateways[0]
if len(vpnGateway.VpcAttachments) == 0 {
// No attachments, we're detached
return vpnGateway, "detached", nil
}
return vpnGateway, *vpnGateway.VpcAttachments[0].State, nil
vpnAttachment := vpnGatewayGetAttachment(vpnGateway)
return vpnGateway, *vpnAttachment.State, nil
}
}
func vpnGatewayGetAttachment(vgw *ec2.VpnGateway) *ec2.VpcAttachment {
for _, v := range vgw.VpcAttachments {
if *v.State == "attached" {
return v
}
}
return &ec2.VpcAttachment{State: aws.String("detached")}
}

View File

@ -0,0 +1,210 @@
package aws
import (
"fmt"
"log"
"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/terraform/helper/hashcode"
"github.com/hashicorp/terraform/helper/resource"
"github.com/hashicorp/terraform/helper/schema"
)
func resourceAwsVpnGatewayAttachment() *schema.Resource {
return &schema.Resource{
Create: resourceAwsVpnGatewayAttachmentCreate,
Read: resourceAwsVpnGatewayAttachmentRead,
Delete: resourceAwsVpnGatewayAttachmentDelete,
Schema: map[string]*schema.Schema{
"vpc_id": &schema.Schema{
Type: schema.TypeString,
Required: true,
ForceNew: true,
},
"vpn_gateway_id": &schema.Schema{
Type: schema.TypeString,
Required: true,
ForceNew: true,
},
},
}
}
func resourceAwsVpnGatewayAttachmentCreate(d *schema.ResourceData, meta interface{}) error {
conn := meta.(*AWSClient).ec2conn
vpcId := d.Get("vpc_id").(string)
vgwId := d.Get("vpn_gateway_id").(string)
createOpts := &ec2.AttachVpnGatewayInput{
VpcId: aws.String(vpcId),
VpnGatewayId: aws.String(vgwId),
}
log.Printf("[DEBUG] VPN Gateway attachment options: %#v", *createOpts)
_, err := conn.AttachVpnGateway(createOpts)
if err != nil {
return fmt.Errorf("Error attaching VPN Gateway %q to VPC %q: %s",
vgwId, vpcId, err)
}
d.SetId(vpnGatewayAttachmentId(vpcId, vgwId))
log.Printf("[INFO] VPN Gateway %q attachment ID: %s", vgwId, d.Id())
stateConf := &resource.StateChangeConf{
Pending: []string{"detached", "attaching"},
Target: []string{"attached"},
Refresh: vpnGatewayAttachmentStateRefresh(conn, vpcId, vgwId),
Timeout: 5 * time.Minute,
Delay: 10 * time.Second,
MinTimeout: 3 * time.Second,
}
_, err = stateConf.WaitForState()
if err != nil {
return fmt.Errorf("Error waiting for VPN Gateway %q to attach to VPC %q: %s",
vgwId, vpcId, err)
}
log.Printf("[DEBUG] VPN Gateway %q attached to VPC %q.", vgwId, vpcId)
return resourceAwsVpnGatewayAttachmentRead(d, meta)
}
func resourceAwsVpnGatewayAttachmentRead(d *schema.ResourceData, meta interface{}) error {
conn := meta.(*AWSClient).ec2conn
vgwId := d.Get("vpn_gateway_id").(string)
resp, err := conn.DescribeVpnGateways(&ec2.DescribeVpnGatewaysInput{
VpnGatewayIds: []*string{aws.String(vgwId)},
})
if err != nil {
awsErr, ok := err.(awserr.Error)
if ok && awsErr.Code() == "InvalidVPNGatewayID.NotFound" {
log.Printf("[WARN] VPN Gateway %q not found.", vgwId)
d.SetId("")
return nil
}
return err
}
vgw := resp.VpnGateways[0]
if *vgw.State == "deleted" {
log.Printf("[INFO] VPN Gateway %q appears to have been deleted.", vgwId)
d.SetId("")
return nil
}
vga := vpnGatewayGetAttachment(vgw)
if len(vgw.VpcAttachments) == 0 || *vga.State == "detached" {
d.Set("vpc_id", "")
return nil
}
d.Set("vpc_id", *vga.VpcId)
return nil
}
func resourceAwsVpnGatewayAttachmentDelete(d *schema.ResourceData, meta interface{}) error {
conn := meta.(*AWSClient).ec2conn
vpcId := d.Get("vpc_id").(string)
vgwId := d.Get("vpn_gateway_id").(string)
if vpcId == "" {
log.Printf("[DEBUG] Not detaching VPN Gateway %q as no VPC ID is set.", vgwId)
return nil
}
_, err := conn.DetachVpnGateway(&ec2.DetachVpnGatewayInput{
VpcId: aws.String(vpcId),
VpnGatewayId: aws.String(vgwId),
})
if err != nil {
awsErr, ok := err.(awserr.Error)
if ok {
switch awsErr.Code() {
case "InvalidVPNGatewayID.NotFound":
log.Printf("[WARN] VPN Gateway %q not found.", vgwId)
d.SetId("")
return nil
case "InvalidVpnGatewayAttachment.NotFound":
log.Printf(
"[WARN] VPN Gateway %q attachment to VPC %q not found.",
vgwId, vpcId)
d.SetId("")
return nil
}
}
return fmt.Errorf("Error detaching VPN Gateway %q from VPC %q: %s",
vgwId, vpcId, err)
}
stateConf := &resource.StateChangeConf{
Pending: []string{"attached", "detaching"},
Target: []string{"detached"},
Refresh: vpnGatewayAttachmentStateRefresh(conn, vpcId, vgwId),
Timeout: 5 * time.Minute,
Delay: 10 * time.Second,
MinTimeout: 3 * time.Second,
}
_, err = stateConf.WaitForState()
if err != nil {
return fmt.Errorf("Error waiting for VPN Gateway %q to detach from VPC %q: %s",
vgwId, vpcId, err)
}
log.Printf("[DEBUG] VPN Gateway %q detached from VPC %q.", vgwId, vpcId)
d.SetId("")
return nil
}
func vpnGatewayAttachmentStateRefresh(conn *ec2.EC2, vpcId, vgwId string) resource.StateRefreshFunc {
return func() (interface{}, string, error) {
resp, err := conn.DescribeVpnGateways(&ec2.DescribeVpnGatewaysInput{
Filters: []*ec2.Filter{
&ec2.Filter{
Name: aws.String("attachment.vpc-id"),
Values: []*string{aws.String(vpcId)},
},
},
VpnGatewayIds: []*string{aws.String(vgwId)},
})
if err != nil {
awsErr, ok := err.(awserr.Error)
if ok {
switch awsErr.Code() {
case "InvalidVPNGatewayID.NotFound":
fallthrough
case "InvalidVpnGatewayAttachment.NotFound":
return nil, "", nil
}
}
return nil, "", err
}
vgw := resp.VpnGateways[0]
if len(vgw.VpcAttachments) == 0 {
return vgw, "detached", nil
}
vga := vpnGatewayGetAttachment(vgw)
log.Printf("[DEBUG] VPN Gateway %q attachment status: %s", vgwId, *vga.State)
return vgw, *vga.State, nil
}
}
func vpnGatewayAttachmentId(vpcId, vgwId string) string {
return fmt.Sprintf("vpn-attachment-%x", hashcode.String(fmt.Sprintf("%s-%s", vpcId, vgwId)))
}

View File

@ -0,0 +1,163 @@
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 TestAccAWSVpnGatewayAttachment_basic(t *testing.T) {
var vpc ec2.Vpc
var vgw ec2.VpnGateway
resource.Test(t, resource.TestCase{
PreCheck: func() { testAccPreCheck(t) },
IDRefreshName: "aws_vpn_gateway_attachment.test",
Providers: testAccProviders,
CheckDestroy: testAccCheckVpnGatewayAttachmentDestroy,
Steps: []resource.TestStep{
resource.TestStep{
Config: testAccVpnGatewayAttachmentConfig,
Check: resource.ComposeTestCheckFunc(
testAccCheckVpcExists(
"aws_vpc.test",
&vpc),
testAccCheckVpnGatewayExists(
"aws_vpn_gateway.test",
&vgw),
testAccCheckVpnGatewayAttachmentExists(
"aws_vpn_gateway_attachment.test",
&vpc, &vgw),
),
},
},
})
}
func TestAccAWSVpnGatewayAttachment_deleted(t *testing.T) {
var vpc ec2.Vpc
var vgw ec2.VpnGateway
testDeleted := func(n string) resource.TestCheckFunc {
return func(s *terraform.State) error {
_, ok := s.RootModule().Resources[n]
if ok {
return fmt.Errorf("Expected VPN Gateway attachment resource %q to be deleted.", n)
}
return nil
}
}
resource.Test(t, resource.TestCase{
PreCheck: func() { testAccPreCheck(t) },
IDRefreshName: "aws_vpn_gateway_attachment.test",
Providers: testAccProviders,
CheckDestroy: testAccCheckVpnGatewayAttachmentDestroy,
Steps: []resource.TestStep{
resource.TestStep{
Config: testAccVpnGatewayAttachmentConfig,
Check: resource.ComposeTestCheckFunc(
testAccCheckVpcExists(
"aws_vpc.test",
&vpc),
testAccCheckVpnGatewayExists(
"aws_vpn_gateway.test",
&vgw),
testAccCheckVpnGatewayAttachmentExists(
"aws_vpn_gateway_attachment.test",
&vpc, &vgw),
),
},
resource.TestStep{
Config: testAccNoVpnGatewayAttachmentConfig,
Check: resource.ComposeTestCheckFunc(
testDeleted("aws_vpn_gateway_attachment.test"),
),
},
},
})
}
func testAccCheckVpnGatewayAttachmentExists(n string, vpc *ec2.Vpc, vgw *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")
}
vpcId := rs.Primary.Attributes["vpc_id"]
vgwId := rs.Primary.Attributes["vpn_gateway_id"]
if len(vgw.VpcAttachments) == 0 {
return fmt.Errorf("VPN Gateway %q has no attachments.", vgwId)
}
if *vgw.VpcAttachments[0].State != "attached" {
return fmt.Errorf("Expected VPN Gateway %q to be in attached state, but got: %q",
vgwId, *vgw.VpcAttachments[0].State)
}
if *vgw.VpcAttachments[0].VpcId != *vpc.VpcId {
return fmt.Errorf("Expected VPN Gateway %q to be attached to VPC %q, but got: %q",
vgwId, vpcId, *vgw.VpcAttachments[0].VpcId)
}
return nil
}
}
func testAccCheckVpnGatewayAttachmentDestroy(s *terraform.State) error {
conn := testAccProvider.Meta().(*AWSClient).ec2conn
for _, rs := range s.RootModule().Resources {
if rs.Type != "aws_vpn_gateway_attachment" {
continue
}
vgwId := rs.Primary.Attributes["vpn_gateway_id"]
resp, err := conn.DescribeVpnGateways(&ec2.DescribeVpnGatewaysInput{
VpnGatewayIds: []*string{aws.String(vgwId)},
})
if err != nil {
return err
}
vgw := resp.VpnGateways[0]
if *vgw.VpcAttachments[0].State != "detached" {
return fmt.Errorf("Expected VPN Gateway %q to be in detached state, but got: %q",
vgwId, *vgw.VpcAttachments[0].State)
}
}
return nil
}
const testAccNoVpnGatewayAttachmentConfig = `
resource "aws_vpc" "test" {
cidr_block = "10.0.0.0/16"
}
resource "aws_vpn_gateway" "test" { }
`
const testAccVpnGatewayAttachmentConfig = `
resource "aws_vpc" "test" {
cidr_block = "10.0.0.0/16"
}
resource "aws_vpn_gateway" "test" { }
resource "aws_vpn_gateway_attachment" "test" {
vpc_id = "${aws_vpc.test.id}"
vpn_gateway_id = "${aws_vpn_gateway.test.id}"
}
`

View File

@ -16,10 +16,10 @@ func TestAccAWSVpnGateway_basic(t *testing.T) {
testNotEqual := func(*terraform.State) error {
if len(v.VpcAttachments) == 0 {
return fmt.Errorf("VPN gateway A is not attached")
return fmt.Errorf("VPN Gateway A is not attached")
}
if len(v2.VpcAttachments) == 0 {
return fmt.Errorf("VPN gateway B is not attached")
return fmt.Errorf("VPN Gateway B is not attached")
}
id1 := v.VpcAttachments[0].VpcId
@ -58,20 +58,38 @@ func TestAccAWSVpnGateway_basic(t *testing.T) {
}
func TestAccAWSVpnGateway_reattach(t *testing.T) {
var v ec2.VpnGateway
var vpc1, vpc2 ec2.Vpc
var vgw1, vgw2 ec2.VpnGateway
genTestStateFunc := func(expectedState string) func(*terraform.State) error {
testAttachmentFunc := func(vgw *ec2.VpnGateway, vpc *ec2.Vpc) func(*terraform.State) error {
return func(*terraform.State) error {
if len(v.VpcAttachments) == 0 {
if expectedState != "detached" {
return fmt.Errorf("VPN gateway has no VPC attachments")
if len(vgw.VpcAttachments) == 0 {
return fmt.Errorf("VPN Gateway %q has no VPC attachments.",
*vgw.VpnGatewayId)
}
if len(vgw.VpcAttachments) > 1 {
count := 0
for _, v := range vgw.VpcAttachments {
if *v.State == "attached" {
count += 1
}
}
} else if len(v.VpcAttachments) == 1 {
if *v.VpcAttachments[0].State != expectedState {
return fmt.Errorf("Expected VPC gateway VPC attachment to be in '%s' state, but was not: %s", expectedState, v)
if count > 1 {
return fmt.Errorf(
"VPN Gateway %q has an unexpected number of VPC attachments (more than 1): %#v",
*vgw.VpnGatewayId, vgw.VpcAttachments)
}
} else {
return fmt.Errorf("VPN gateway has unexpected number of VPC attachments(more than 1): %s", v)
}
if *vgw.VpcAttachments[0].State != "attached" {
return fmt.Errorf("Expected VPN Gateway %q to be attached.",
*vgw.VpnGatewayId)
}
if *vgw.VpcAttachments[0].VpcId != *vpc.VpcId {
return fmt.Errorf("Expected VPN Gateway %q to be attached to VPC %q, but got: %q",
*vgw.VpnGatewayId, *vpc.VpcId, *vgw.VpcAttachments[0].VpcId)
}
return nil
}
@ -84,27 +102,38 @@ func TestAccAWSVpnGateway_reattach(t *testing.T) {
CheckDestroy: testAccCheckVpnGatewayDestroy,
Steps: []resource.TestStep{
resource.TestStep{
Config: testAccVpnGatewayConfig,
Config: testAccCheckVpnGatewayConfigReattach,
Check: resource.ComposeTestCheckFunc(
testAccCheckVpcExists("aws_vpc.foo", &vpc1),
testAccCheckVpcExists("aws_vpc.bar", &vpc2),
testAccCheckVpnGatewayExists(
"aws_vpn_gateway.foo", &v),
genTestStateFunc("attached"),
"aws_vpn_gateway.foo", &vgw1),
testAccCheckVpnGatewayExists(
"aws_vpn_gateway.bar", &vgw2),
testAttachmentFunc(&vgw1, &vpc1),
testAttachmentFunc(&vgw2, &vpc2),
),
},
resource.TestStep{
Config: testAccVpnGatewayConfigDetach,
Config: testAccCheckVpnGatewayConfigReattachChange,
Check: resource.ComposeTestCheckFunc(
testAccCheckVpnGatewayExists(
"aws_vpn_gateway.foo", &v),
genTestStateFunc("detached"),
"aws_vpn_gateway.foo", &vgw1),
testAccCheckVpnGatewayExists(
"aws_vpn_gateway.bar", &vgw2),
testAttachmentFunc(&vgw2, &vpc1),
testAttachmentFunc(&vgw1, &vpc2),
),
},
resource.TestStep{
Config: testAccVpnGatewayConfig,
Config: testAccCheckVpnGatewayConfigReattach,
Check: resource.ComposeTestCheckFunc(
testAccCheckVpnGatewayExists(
"aws_vpn_gateway.foo", &v),
genTestStateFunc("attached"),
"aws_vpn_gateway.foo", &vgw1),
testAccCheckVpnGatewayExists(
"aws_vpn_gateway.bar", &vgw2),
testAttachmentFunc(&vgw1, &vpc1),
testAttachmentFunc(&vgw2, &vpc2),
),
},
},
@ -118,7 +147,7 @@ func TestAccAWSVpnGateway_delete(t *testing.T) {
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 fmt.Errorf("VPN Gateway %q should have been deleted.", r)
}
return nil
}
@ -159,7 +188,6 @@ func TestAccAWSVpnGateway_tags(t *testing.T) {
testAccCheckTags(&v.Tags, "foo", "bar"),
),
},
resource.TestStep{
Config: testAccCheckVpnGatewayConfigTagsUpdate,
Check: resource.ComposeTestCheckFunc(
@ -198,7 +226,7 @@ func testAccCheckVpnGatewayDestroy(s *terraform.State) error {
}
if *v.State != "deleted" {
return fmt.Errorf("Expected VpnGateway to be in deleted state, but was not: %s", v)
return fmt.Errorf("Expected VPN Gateway to be in deleted state, but was not: %s", v)
}
return nil
}
@ -235,7 +263,7 @@ func testAccCheckVpnGatewayExists(n string, ig *ec2.VpnGateway) resource.TestChe
return err
}
if len(resp.VpnGateways) == 0 {
return fmt.Errorf("VPNGateway not found")
return fmt.Errorf("VPN Gateway not found")
}
*ig = *resp.VpnGateways[0]
@ -270,16 +298,6 @@ resource "aws_vpn_gateway" "foo" {
}
`
const testAccVpnGatewayConfigDetach = `
resource "aws_vpc" "foo" {
cidr_block = "10.1.0.0/16"
}
resource "aws_vpn_gateway" "foo" {
vpc_id = ""
}
`
const testAccCheckVpnGatewayConfigTags = `
resource "aws_vpc" "foo" {
cidr_block = "10.1.0.0/16"
@ -305,3 +323,39 @@ resource "aws_vpn_gateway" "foo" {
}
}
`
const testAccCheckVpnGatewayConfigReattach = `
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.foo.id}"
}
resource "aws_vpn_gateway" "bar" {
vpc_id = "${aws_vpc.bar.id}"
}
`
const testAccCheckVpnGatewayConfigReattachChange = `
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}"
}
resource "aws_vpn_gateway" "bar" {
vpc_id = "${aws_vpc.foo.id}"
}
`

View File

@ -0,0 +1,57 @@
---
layout: "aws"
page_title: "AWS: aws_vpn_gateway_attachment"
sidebar_current: "docs-aws-resource-vpn-gateway-attachment"
description: |-
Provides a Virtual Private Gateway attachment resource.
---
# aws\_vpn\_gateway\_attachment
Provides a Virtual Private Gateway attachment resource, allowing for an existing
hardware VPN gateway to be attached and/or detached from a VPC.
-> **Note:** The [`aws_vpn_gateway`](vpn_gateway.html)
resource can also automatically attach the Virtual Private Gateway it creates
to an existing VPC by setting the [`vpc_id`](vpn_gateway.html#vpc_id) attribute accordingly.
## Example Usage
```
resource "aws_vpc" "network" {
cidr_block = "10.0.0.0/16"
}
resource "aws_vpn_gateway" "vpn" {
tags {
Name = "example-vpn-gateway"
}
}
resource "aws_vpn_gateway_attachment" "vpn_attachment" {
vpc_id = "${aws_vpc.network.id}"
vpn_gateway_id = "${aws_vpn_gateway.vpn.id}"
}
```
See [Virtual Private Cloud](http://docs.aws.amazon.com/AmazonVPC/latest/UserGuide/VPC_Introduction.html)
and [Virtual Private Gateway](http://docs.aws.amazon.com/AmazonVPC/latest/UserGuide/VPC_VPN.html) user
guides for more information.
## Argument Reference
The following arguments are supported:
* `vpc_id` - (Required) The ID of the VPC.
* `vpn_gateway_id` - (Required) The ID of the Virtual Private Gateway.
## Attributes Reference
The following attributes are exported:
* `vpc_id` - The ID of the VPC that Virtual Private Gateway is attached to.
* `vpn_gateway_id` - The ID of the Virtual Private Gateway.
## Import
This resource does not support importing.

View File

@ -885,6 +885,10 @@
<a href="/docs/providers/aws/r/vpn_gateway.html">aws_vpn_gateway</a>
</li>
<li<%= sidebar_current("docs-aws-resource-vpn-gateway-attachment") %>>
<a href="/docs/providers/aws/r/vpn_gateway_attachment.html">aws_vpn_gateway_attachment</a>
</li>
</ul>
</li>