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 5 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 @@ -426,6 +426,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
136 changes: 136 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,136 @@
package aws

import (
"fmt"
"log"
"time"

"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,
Importer: &schema.ResourceImporter{
Copy link
Contributor

Choose a reason for hiding this comment

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

This import support is missing:

  • Acceptance testing in the form of a TestStep that includes ImportState: true and ImportStateVerify: true
  • Documentation in the form of a ## Import section in the resource documentation.

The support should be fully implemented or removed from the pull request. 👍

If you are opting to keep the support, you will notice that you will need to either implement a custom import function to that calls d.Set("client_vpn_endpoint_id", ...) and trims that part of the ID for d.SetId() (then use ImportStateIdFunc in acceptance testing) or switch the resource to implementing a "complex" ID that includes both IDs in its string, e.g. ENDPOINT/ASSOCIATION. For new implementations, we have generally preferred the latter form where we parse the complex ID and continue to use ImportStatePassthrough, but either form is acceptable.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Oh, this is cruft from my copy/paste when I started working on this. I'm going to opt to not include import functionality here since I wasn't planning on adding it initially. I can be swayed to add it however because I can see the need for it.

State: schema.ImportStatePassthrough,
},

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,
},
},

Timeouts: &schema.ResourceTimeout{
slapula marked this conversation as resolved.
Show resolved Hide resolved
Create: schema.DefaultTimeout(10 * time.Minute),
Delete: schema.DefaultTimeout(10 * time.Minute),
},
}
}

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{"associating"},
Target: []string{"associated"},
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("security_groups", result.ClientVpnTargetNetworks[0].SecurityGroups)
slapula marked this conversation as resolved.
Show resolved Hide resolved
d.Set("status", result.ClientVpnTargetNetworks[0].Status)
d.Set("subnet_id", result.ClientVpnTargetNetworks[0].TargetNetworkId)
d.Set("vpc_id", result.ClientVpnTargetNetworks[0].VpcId)

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
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 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.Test(t, resource.TestCase{
slapula marked this conversation as resolved.
Show resolved Hide resolved
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 @@ -1081,6 +1081,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.