Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

r/aws_ec2_client_vpn_network_association: Adding resource to manage Client VPN network associations #7030

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions aws/provider.go
Original file line number Diff line number Diff line change
Expand Up @@ -429,6 +429,7 @@ func Provider() terraform.ResourceProvider {
"aws_ebs_volume": resourceAwsEbsVolume(),
"aws_ec2_capacity_reservation": resourceAwsEc2CapacityReservation(),
"aws_ec2_client_vpn_endpoint": resourceAwsEc2ClientVpnEndpoint(),
"aws_ec2_client_vpn_network_association": resourceAwsEc2ClientVpnNetworkAssociation(),
"aws_ec2_fleet": resourceAwsEc2Fleet(),
"aws_ec2_transit_gateway": resourceAwsEc2TransitGateway(),
"aws_ec2_transit_gateway_route": resourceAwsEc2TransitGatewayRoute(),
Expand Down
151 changes: 151 additions & 0 deletions aws/resource_aws_ec2_client_vpn_network_association.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,151 @@
package aws

import (
"fmt"
"log"
"strings"

"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/helper/schema"
)

func resourceAwsEc2ClientVpnNetworkAssociation() *schema.Resource {
return &schema.Resource{
Create: resourceAwsEc2ClientVpnNetworkAssociationCreate,
Read: resourceAwsEc2ClientVpnNetworkAssociationRead,
Delete: resourceAwsEc2ClientVpnNetworkAssociationDelete,

Schema: map[string]*schema.Schema{
"client_vpn_endpoint_id": {
Type: schema.TypeString,
Required: true,
ForceNew: true,
},
"subnet_id": {
Type: schema.TypeString,
Required: true,
ForceNew: true,
},
"security_groups": {
Type: schema.TypeSet,
Elem: &schema.Schema{Type: schema.TypeString},
Computed: true,
},
"status": {
Type: schema.TypeString,
Computed: true,
},
"vpc_id": {
Type: schema.TypeString,
Computed: true,
},
},
}
}

func resourceAwsEc2ClientVpnNetworkAssociationCreate(d *schema.ResourceData, meta interface{}) error {
conn := meta.(*AWSClient).ec2conn

req := &ec2.AssociateClientVpnTargetNetworkInput{
ClientVpnEndpointId: aws.String(d.Get("client_vpn_endpoint_id").(string)),
SubnetId: aws.String(d.Get("subnet_id").(string)),
}

log.Printf("[DEBUG] Creating Client VPN network association: %#v", req)
resp, err := conn.AssociateClientVpnTargetNetwork(req)
if err != nil {
return fmt.Errorf("Error creating Client VPN network association: %s", err)
}

d.SetId(*resp.AssociationId)

stateConf := &resource.StateChangeConf{
Pending: []string{ec2.AssociationStatusCodeAssociating},
Target: []string{ec2.AssociationStatusCodeAssociated},
Refresh: clientVpnNetworkAssociationRefreshFunc(conn, d.Id(), d.Get("client_vpn_endpoint_id").(string)),
Timeout: d.Timeout(schema.TimeoutCreate),
}

log.Printf("[DEBUG] Waiting for Client VPN endpoint to associate with target network: %s", d.Id())
_, err = stateConf.WaitForState()
if err != nil {
return fmt.Errorf("Error waiting for Client VPN endpoint to associate with target network: %s", err)
}

return resourceAwsEc2ClientVpnNetworkAssociationRead(d, meta)
}

func resourceAwsEc2ClientVpnNetworkAssociationRead(d *schema.ResourceData, meta interface{}) error {
conn := meta.(*AWSClient).ec2conn
var err error

result, err := conn.DescribeClientVpnTargetNetworks(&ec2.DescribeClientVpnTargetNetworksInput{
ClientVpnEndpointId: aws.String(d.Get("client_vpn_endpoint_id").(string)),
AssociationIds: []*string{aws.String(d.Id())},
})

if err != nil {
return fmt.Errorf("Error reading Client VPN network association: %s", err)
}

d.Set("client_vpn_endpoint_id", result.ClientVpnTargetNetworks[0].ClientVpnEndpointId)
d.Set("status", result.ClientVpnTargetNetworks[0].Status)
d.Set("subnet_id", result.ClientVpnTargetNetworks[0].TargetNetworkId)
d.Set("vpc_id", result.ClientVpnTargetNetworks[0].VpcId)

if err := d.Set("security_groups", aws.StringValueSlice(result.ClientVpnTargetNetworks[0].SecurityGroups)); err != nil {
return fmt.Errorf("error setting security_groups: %s", err)
}

return nil
}

func resourceAwsEc2ClientVpnNetworkAssociationDelete(d *schema.ResourceData, meta interface{}) error {
conn := meta.(*AWSClient).ec2conn

_, err := conn.DisassociateClientVpnTargetNetwork(&ec2.DisassociateClientVpnTargetNetworkInput{
ClientVpnEndpointId: aws.String(d.Get("client_vpn_endpoint_id").(string)),
AssociationId: aws.String(d.Id()),
})
if err != nil {
return fmt.Errorf("Error deleting Client VPN network association: %s", err)
}

slapula marked this conversation as resolved.
Show resolved Hide resolved
stateConf := &resource.StateChangeConf{
Pending: []string{ec2.AssociationStatusCodeDisassociating},
Target: []string{ec2.AssociationStatusCodeDisassociated},
Refresh: clientVpnNetworkAssociationRefreshFunc(conn, d.Id(), d.Get("client_vpn_endpoint_id").(string)),
Timeout: d.Timeout(schema.TimeoutDelete),
}

log.Printf("[DEBUG] Waiting for Client VPN endpoint to disassociate with target network: %s", d.Id())
_, err = stateConf.WaitForState()
if err != nil {
if !strings.Contains(err.Error(), "couldn't find resource") {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Just so we can get this released today, I'm going to fix this last little bit up given my previous comment. I hope you don't mind. 😄

return fmt.Errorf("Error waiting for Client VPN endpoint to disassociate with target network: %s", err)
}
}

return nil
}

func clientVpnNetworkAssociationRefreshFunc(conn *ec2.EC2, cvnaID string, cvepID string) resource.StateRefreshFunc {
return func() (interface{}, string, error) {
resp, err := conn.DescribeClientVpnTargetNetworks(&ec2.DescribeClientVpnTargetNetworksInput{
ClientVpnEndpointId: aws.String(cvepID),
AssociationIds: []*string{aws.String(cvnaID)},
})

if resp == nil || len(resp.ClientVpnTargetNetworks) == 0 {
return nil, ec2.AssociationStatusCodeDisassociated, nil
}

if err != nil {
return nil, "", err
}

return resp.ClientVpnTargetNetworks[0], aws.StringValue(resp.ClientVpnTargetNetworks[0].Status.Code), nil
}
}
154 changes: 154 additions & 0 deletions aws/resource_aws_ec2_client_vpn_network_association_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,154 @@
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/acctest"
"github.com/hashicorp/terraform/helper/resource"
"github.com/hashicorp/terraform/terraform"
)

func TestAccAwsEc2ClientVpnNetworkAssociation_basic(t *testing.T) {
var assoc1 ec2.TargetNetwork
rStr := acctest.RandString(5)

resource.ParallelTest(t, resource.TestCase{
PreCheck: func() { testAccPreCheck(t) },
Providers: testAccProvidersWithTLS,
CheckDestroy: testAccCheckAwsEc2ClientVpnNetworkAssociationDestroy,
Steps: []resource.TestStep{
{
Config: testAccEc2ClientVpnNetworkAssociationConfig(rStr),
Check: resource.ComposeTestCheckFunc(
testAccCheckAwsEc2ClientVpnNetworkAssociationExists("aws_ec2_client_vpn_network_association.test", &assoc1),
),
},
},
})
}

func testAccCheckAwsEc2ClientVpnNetworkAssociationDestroy(s *terraform.State) error {
conn := testAccProvider.Meta().(*AWSClient).ec2conn

for _, rs := range s.RootModule().Resources {
if rs.Type != "aws_ec2_client_vpn_network_association" {
continue
}

resp, _ := conn.DescribeClientVpnTargetNetworks(&ec2.DescribeClientVpnTargetNetworksInput{
ClientVpnEndpointId: aws.String(rs.Primary.Attributes["client_vpn_endpoint_id"]),
AssociationIds: []*string{aws.String(rs.Primary.ID)},
})

for _, v := range resp.ClientVpnTargetNetworks {
if *v.AssociationId == rs.Primary.ID && !(*v.Status.Code == "Disassociated") {
return fmt.Errorf("[DESTROY ERROR] Client VPN network association (%s) not deleted", rs.Primary.ID)
}
}
}

return nil
}

func testAccCheckAwsEc2ClientVpnNetworkAssociationExists(name string, assoc *ec2.TargetNetwork) resource.TestCheckFunc {
return func(s *terraform.State) error {
rs, ok := s.RootModule().Resources[name]
if !ok {
return fmt.Errorf("Not found: %s", name)
}

if rs.Primary.ID == "" {
return fmt.Errorf("No ID is set")
}

conn := testAccProvider.Meta().(*AWSClient).ec2conn

resp, err := conn.DescribeClientVpnTargetNetworks(&ec2.DescribeClientVpnTargetNetworksInput{
ClientVpnEndpointId: aws.String(rs.Primary.Attributes["client_vpn_endpoint_id"]),
AssociationIds: []*string{aws.String(rs.Primary.ID)},
})

if err != nil {
return fmt.Errorf("Error reading Client VPN network association (%s): %s", rs.Primary.ID, err)
}

for _, a := range resp.ClientVpnTargetNetworks {
if *a.AssociationId == rs.Primary.ID && !(*a.Status.Code == "Disassociated") {
*assoc = *a
return nil
}
}

return fmt.Errorf("Client VPN network association (%s) not found", rs.Primary.ID)
}
}

func testAccEc2ClientVpnNetworkAssociationConfig(rName string) string {
return fmt.Sprintf(`
resource "aws_vpc" "test" {
cidr_block = "10.1.0.0/16"
tags = {
Name = "terraform-testacc-subnet-%s"
}
}

resource "aws_subnet" "test" {
cidr_block = "10.1.1.0/24"
vpc_id = "${aws_vpc.test.id}"
map_public_ip_on_launch = true
tags = {
Name = "tf-acc-subnet-%s"
}
}

resource "tls_private_key" "example" {
algorithm = "RSA"
}

resource "tls_self_signed_cert" "example" {
key_algorithm = "RSA"
private_key_pem = "${tls_private_key.example.private_key_pem}"

subject {
common_name = "example.com"
organization = "ACME Examples, Inc"
}

validity_period_hours = 12

allowed_uses = [
"key_encipherment",
"digital_signature",
"server_auth",
]
}

resource "aws_acm_certificate" "cert" {
private_key = "${tls_private_key.example.private_key_pem}"
certificate_body = "${tls_self_signed_cert.example.cert_pem}"
}

resource "aws_ec2_client_vpn_endpoint" "test" {
description = "terraform-testacc-clientvpn-%s"
server_certificate_arn = "${aws_acm_certificate.cert.arn}"
client_cidr_block = "10.0.0.0/16"

authentication_options {
type = "certificate-authentication"
root_certificate_chain_arn = "${aws_acm_certificate.cert.arn}"
}

connection_log_options {
enabled = false
}
}

resource "aws_ec2_client_vpn_network_association" "test" {
client_vpn_endpoint_id = "${aws_ec2_client_vpn_endpoint.test.id}"
subnet_id = "${aws_subnet.test.id}"
}
`, rName, rName, rName)
}
4 changes: 4 additions & 0 deletions website/aws.erb
Original file line number Diff line number Diff line change
Expand Up @@ -1098,6 +1098,10 @@
<a href="/docs/providers/aws/r/ec2_client_vpn_endpoint.html">aws_ec2_client_vpn_endpoint</a>
</li>

<li<%= sidebar_current("docs-aws-resource-ec2-client-vpn-network-association") %>>
<a href="/docs/providers/aws/r/ec2_client_vpn_network_association.html">aws_ec2_client_vpn_network_association</a>
</li>

<li<%= sidebar_current("docs-aws-resource-ec2-fleet") %>>
<a href="/docs/providers/aws/r/ec2_fleet.html">aws_ec2_fleet</a>
</li>
Expand Down
37 changes: 37 additions & 0 deletions website/docs/r/ec2_client_vpn_network_association.html.markdown
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
---
layout: "aws"
page_title: "AWS: aws_ec2_client_vpn_network_association"
sidebar_current: "docs-aws-resource-ec2-client-vpn-network-association"
description: |-
Provides network associations for AWS Client VPN endpoints.
---

# aws_ec2_client_vpn_network_association

Provides network associations for AWS Client VPN endpoints. For more information on usage, please see the
[AWS Client VPN Administrator's Guide](https://docs.aws.amazon.com/vpn/latest/clientvpn-admin/what-is.html).

## Example Usage

```hcl
resource "aws_ec2_client_vpn_network_association" "example" {
client_vpn_endpoint_id = "${aws_ec2_client_vpn_endpoint.example.id}"
subnet_id = "${aws_subnet.example.id}"
}
```

## Argument Reference

The following arguments are supported:

* `client_vpn_endpoint_id` - (Required) The ID of the Client VPN endpoint.
* `subnet_id` - (Required) The ID of the subnet to associate with the Client VPN endpoint.

## Attributes Reference

In addition to all arguments above, the following attributes are exported:

* `id` - The unique ID of the target network association.
* `security_groups` - The IDs of the security groups applied to the target network association.
* `status` - The current state of the target network association.
* `vpc_id` - The ID of the VPC in which the target network (subnet) is located.