diff --git a/aws/provider.go b/aws/provider.go index 08ee425c12d9..f0e15e3a10f3 100644 --- a/aws/provider.go +++ b/aws/provider.go @@ -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(), diff --git a/aws/resource_aws_ec2_client_vpn_network_association.go b/aws/resource_aws_ec2_client_vpn_network_association.go new file mode 100644 index 000000000000..a03cdb198f7a --- /dev/null +++ b/aws/resource_aws_ec2_client_vpn_network_association.go @@ -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) + } + + 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") { + 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 + } +} diff --git a/aws/resource_aws_ec2_client_vpn_network_association_test.go b/aws/resource_aws_ec2_client_vpn_network_association_test.go new file mode 100644 index 000000000000..65f5f3af2920 --- /dev/null +++ b/aws/resource_aws_ec2_client_vpn_network_association_test.go @@ -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) +} diff --git a/website/aws.erb b/website/aws.erb index b6b27de73431..02662126d6e2 100644 --- a/website/aws.erb +++ b/website/aws.erb @@ -1098,6 +1098,10 @@ aws_ec2_client_vpn_endpoint +