diff --git a/.changelog/22911.txt b/.changelog/22911.txt new file mode 100644 index 00000000000..0e90e02b8f0 --- /dev/null +++ b/.changelog/22911.txt @@ -0,0 +1,15 @@ +```release-note:note +resource/aws_ec2_client_vpn_network_association: The `security_groups` argument has been deprecated. Use the `security_group_ids` argument of the `aws_ec2_client_vpn_endpoint` resource instead +``` + +```release-note:enhancement +resource/aws_ec2_client_vpn_endpoint: Add `security_group_ids` and `vpc_id` arguments +``` + +```release-note:enhancement +data-source/aws_ec2_client_vpn_endpoint: Add `security_group_ids` and `vpc_id` attributes +``` + +```release-note:note +resource/aws_ec2_client_vpn_route: Add [custom `timeouts`](https://www.terraform.io/docs/language/resources/syntax.html#operation-timeouts) block +``` \ No newline at end of file diff --git a/internal/conns/conns.go b/internal/conns/conns.go index dda1293923b..d0ce21b612a 100644 --- a/internal/conns/conns.go +++ b/internal/conns/conns.go @@ -1718,26 +1718,29 @@ func (c *Config) Client() (interface{}, error) { }) client.EC2Conn.Handlers.Retry.PushBack(func(r *request.Request) { - if r.Operation.Name == "CreateClientVpnEndpoint" { - if tfawserr.ErrMessageContains(r.Error, "OperationNotPermitted", "Endpoint cannot be created while another endpoint is being created") { + switch err := r.Error; r.Operation.Name { + case "AttachVpnGateway", "DetachVpnGateway": + if tfawserr.ErrMessageContains(err, "InvalidParameterValue", "This call cannot be completed because there are pending VPNs or Virtual Interfaces") { r.Retryable = aws.Bool(true) } - } - if r.Operation.Name == "CreateVpnConnection" { - if tfawserr.ErrMessageContains(r.Error, "VpnConnectionLimitExceeded", "maximum number of mutating objects has been reached") { + case "CreateClientVpnEndpoint": + if tfawserr.ErrMessageContains(err, "OperationNotPermitted", "Endpoint cannot be created while another endpoint is being created") { r.Retryable = aws.Bool(true) } - } - if r.Operation.Name == "CreateVpnGateway" { - if tfawserr.ErrMessageContains(r.Error, "VpnGatewayLimitExceeded", "maximum number of mutating objects has been reached") { + case "CreateClientVpnRoute", "DeleteClientVpnRoute": + if tfawserr.ErrMessageContains(err, "ConcurrentMutationLimitExceeded", "Cannot initiate another change for this endpoint at this time") { + r.Retryable = aws.Bool(true) + } + + case "CreateVpnConnection": + if tfawserr.ErrMessageContains(err, "VpnConnectionLimitExceeded", "maximum number of mutating objects has been reached") { r.Retryable = aws.Bool(true) } - } - if r.Operation.Name == "AttachVpnGateway" || r.Operation.Name == "DetachVpnGateway" { - if tfawserr.ErrMessageContains(r.Error, "InvalidParameterValue", "This call cannot be completed because there are pending VPNs or Virtual Interfaces") { + case "CreateVpnGateway": + if tfawserr.ErrMessageContains(err, "VpnGatewayLimitExceeded", "maximum number of mutating objects has been reached") { r.Retryable = aws.Bool(true) } } diff --git a/internal/service/ec2/client_vpn_authorization_rule.go b/internal/service/ec2/client_vpn_authorization_rule.go index 359580cc4a3..e4e8a169b24 100644 --- a/internal/service/ec2/client_vpn_authorization_rule.go +++ b/internal/service/ec2/client_vpn_authorization_rule.go @@ -9,6 +9,7 @@ import ( "github.com/aws/aws-sdk-go/service/ec2" "github.com/hashicorp/aws-sdk-go-base/v2/awsv1shim/v2/tfawserr" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/validation" "github.com/hashicorp/terraform-provider-aws/internal/conns" "github.com/hashicorp/terraform-provider-aws/internal/tfresource" "github.com/hashicorp/terraform-provider-aws/internal/verify" @@ -33,6 +34,7 @@ func ResourceClientVPNAuthorizationRule() *schema.Resource { Type: schema.TypeString, Optional: true, ForceNew: true, + ValidateFunc: validation.StringDoesNotContainAny(","), ExactlyOneOf: []string{"access_group_id", "authorize_all_groups"}, }, "authorize_all_groups": { diff --git a/internal/service/ec2/client_vpn_endpoint.go b/internal/service/ec2/client_vpn_endpoint.go index f1c9eaf41f2..bc8cc0a0ca6 100644 --- a/internal/service/ec2/client_vpn_endpoint.go +++ b/internal/service/ec2/client_vpn_endpoint.go @@ -156,6 +156,14 @@ func ResourceClientVPNEndpoint() *schema.Resource { Optional: true, Elem: &schema.Schema{Type: schema.TypeString}, }, + "security_group_ids": { + Type: schema.TypeSet, + MinItems: 1, + MaxItems: 5, + Optional: true, + Computed: true, + Elem: &schema.Schema{Type: schema.TypeString}, + }, "self_service_portal": { Type: schema.TypeString, Optional: true, @@ -192,6 +200,11 @@ func ResourceClientVPNEndpoint() *schema.Resource { Default: ec2.TransportProtocolUdp, ValidateFunc: validation.StringInSlice(ec2.TransportProtocol_Values(), false), }, + "vpc_id": { + Type: schema.TypeString, + Optional: true, + Computed: true, + }, "vpn_port": { Type: schema.TypeInt, Optional: true, @@ -243,6 +256,10 @@ func resourceClientVPNEndpointCreate(d *schema.ResourceData, meta interface{}) e input.DnsServers = flex.ExpandStringList(v.([]interface{})) } + if v, ok := d.GetOk("security_group_ids"); ok { + input.SecurityGroupIds = flex.ExpandStringSet(v.(*schema.Set)) + } + if v, ok := d.GetOk("self_service_portal"); ok { input.SelfServicePortal = aws.String(v.(string)) } @@ -251,6 +268,10 @@ func resourceClientVPNEndpointCreate(d *schema.ResourceData, meta interface{}) e input.SessionTimeoutHours = aws.Int64(int64(v.(int))) } + if v, ok := d.GetOk("vpc_id"); ok { + input.VpcId = aws.String(v.(string)) + } + log.Printf("[DEBUG] Creating EC2 Client VPN Endpoint: %s", input) output, err := conn.CreateClientVpnEndpoint(input) @@ -316,6 +337,7 @@ func resourceClientVPNEndpointRead(d *schema.ResourceData, meta interface{}) err d.Set("description", ep.Description) d.Set("dns_name", ep.DnsName) d.Set("dns_servers", aws.StringValueSlice(ep.DnsServers)) + d.Set("security_group_ids", aws.StringValueSlice(ep.SecurityGroupIds)) if aws.StringValue(ep.SelfServicePortalUrl) != "" { d.Set("self_service_portal", ec2.SelfServicePortalEnabled) } else { @@ -326,6 +348,7 @@ func resourceClientVPNEndpointRead(d *schema.ResourceData, meta interface{}) err d.Set("split_tunnel", ep.SplitTunnel) d.Set("status", ep.Status.Code) d.Set("transport_protocol", ep.TransportProtocol) + d.Set("vpc_id", ep.VpcId) d.Set("vpn_port", ep.VpnPort) tags := KeyValueTags(ep.Tags).IgnoreAWS().IgnoreConfig(ignoreTagsConfig) @@ -387,6 +410,12 @@ func resourceClientVPNEndpointUpdate(d *schema.ResourceData, meta interface{}) e } } + if d.HasChange("security_group_ids") { + input.SecurityGroupIds = flex.ExpandStringSet(d.Get("security_group_ids").(*schema.Set)) + // "InvalidParameterValue: Security Groups cannot be modified without specifying Vpc Id" + input.VpcId = aws.String(d.Get("vpc_id").(string)) + } + if d.HasChange("self_service_portal") { input.SelfServicePortal = aws.String(d.Get("self_service_portal").(string)) } @@ -407,6 +436,10 @@ func resourceClientVPNEndpointUpdate(d *schema.ResourceData, meta interface{}) e input.VpnPort = aws.Int64(int64(d.Get("vpn_port").(int))) } + if d.HasChange("vpc_id") { + input.VpcId = aws.String(d.Get("vpc_id").(string)) + } + if _, err := conn.ModifyClientVpnEndpoint(input); err != nil { return fmt.Errorf("error modifying EC2 Client VPN Endpoint (%s): %w", d.Id(), err) } diff --git a/internal/service/ec2/client_vpn_endpoint_data_source.go b/internal/service/ec2/client_vpn_endpoint_data_source.go index 1d46f72905c..611a9e7f64c 100644 --- a/internal/service/ec2/client_vpn_endpoint_data_source.go +++ b/internal/service/ec2/client_vpn_endpoint_data_source.go @@ -119,11 +119,16 @@ func DataSourceClientVPNEndpoint() *schema.Resource { Computed: true, }, "dns_servers": { - Type: schema.TypeSet, + Type: schema.TypeList, Computed: true, Elem: &schema.Schema{Type: schema.TypeString}, }, "filter": DataSourceFiltersSchema(), + "security_group_ids": { + Type: schema.TypeList, + Computed: true, + Elem: &schema.Schema{Type: schema.TypeString}, + }, "self_service_portal": { Type: schema.TypeString, Computed: true, @@ -145,6 +150,10 @@ func DataSourceClientVPNEndpoint() *schema.Resource { Type: schema.TypeString, Computed: true, }, + "vpc_id": { + Type: schema.TypeString, + Computed: true, + }, "vpn_port": { Type: schema.TypeInt, Computed: true, @@ -219,6 +228,7 @@ func dataSourceClientVPNEndpointRead(d *schema.ResourceData, meta interface{}) e d.Set("description", ep.Description) d.Set("dns_name", ep.DnsName) d.Set("dns_servers", aws.StringValueSlice(ep.DnsServers)) + d.Set("security_group_ids", aws.StringValueSlice(ep.SecurityGroupIds)) if aws.StringValue(ep.SelfServicePortalUrl) != "" { d.Set("self_service_portal", ec2.SelfServicePortalEnabled) } else { @@ -228,6 +238,7 @@ func dataSourceClientVPNEndpointRead(d *schema.ResourceData, meta interface{}) e d.Set("session_timeout_hours", ep.SessionTimeoutHours) d.Set("split_tunnel", ep.SplitTunnel) d.Set("transport_protocol", ep.TransportProtocol) + d.Set("vpc_id", ep.VpcId) d.Set("vpn_port", ep.VpnPort) if err := d.Set("tags", KeyValueTags(ep.Tags).IgnoreAWS().IgnoreConfig(ignoreTagsConfig).Map()); err != nil { diff --git a/internal/service/ec2/client_vpn_endpoint_data_source_test.go b/internal/service/ec2/client_vpn_endpoint_data_source_test.go index 3d5b8ac5648..24214bb6ca6 100644 --- a/internal/service/ec2/client_vpn_endpoint_data_source_test.go +++ b/internal/service/ec2/client_vpn_endpoint_data_source_test.go @@ -35,12 +35,14 @@ func testAccClientVPNEndpointDataSource_basic(t *testing.T) { resource.TestCheckResourceAttrPair(datasource1Name, "description", resourceName, "description"), resource.TestCheckResourceAttrPair(datasource1Name, "dns_name", resourceName, "dns_name"), resource.TestCheckResourceAttrPair(datasource1Name, "dns_servers.#", resourceName, "dns_servers.#"), + resource.TestCheckResourceAttrPair(datasource1Name, "security_group_ids.#", resourceName, "security_group_ids.#"), resource.TestCheckResourceAttrPair(datasource1Name, "self_service_portal", resourceName, "self_service_portal"), resource.TestCheckResourceAttrPair(datasource1Name, "server_certificate_arn", resourceName, "server_certificate_arn"), resource.TestCheckResourceAttrPair(datasource1Name, "session_timeout_hours", resourceName, "session_timeout_hours"), resource.TestCheckResourceAttrPair(datasource1Name, "split_tunnel", resourceName, "split_tunnel"), resource.TestCheckResourceAttrPair(datasource1Name, "tags.%", resourceName, "tags.%"), resource.TestCheckResourceAttrPair(datasource1Name, "transport_protocol", resourceName, "transport_protocol"), + resource.TestCheckResourceAttrPair(datasource1Name, "vpc_id", resourceName, "vpc_id"), resource.TestCheckResourceAttrPair(datasource1Name, "vpn_port", resourceName, "vpn_port"), resource.TestCheckResourceAttrPair(datasource2Name, "arn", resourceName, "arn"), @@ -53,12 +55,14 @@ func testAccClientVPNEndpointDataSource_basic(t *testing.T) { resource.TestCheckResourceAttrPair(datasource2Name, "description", resourceName, "description"), resource.TestCheckResourceAttrPair(datasource2Name, "dns_name", resourceName, "dns_name"), resource.TestCheckResourceAttrPair(datasource2Name, "dns_servers.#", resourceName, "dns_servers.#"), + resource.TestCheckResourceAttrPair(datasource2Name, "security_group_ids.#", resourceName, "security_group_ids.#"), resource.TestCheckResourceAttrPair(datasource2Name, "self_service_portal", resourceName, "self_service_portal"), resource.TestCheckResourceAttrPair(datasource2Name, "server_certificate_arn", resourceName, "server_certificate_arn"), resource.TestCheckResourceAttrPair(datasource2Name, "session_timeout_hours", resourceName, "session_timeout_hours"), resource.TestCheckResourceAttrPair(datasource2Name, "split_tunnel", resourceName, "split_tunnel"), resource.TestCheckResourceAttrPair(datasource2Name, "tags.%", resourceName, "tags.%"), resource.TestCheckResourceAttrPair(datasource2Name, "transport_protocol", resourceName, "transport_protocol"), + resource.TestCheckResourceAttrPair(datasource2Name, "vpc_id", resourceName, "vpc_id"), resource.TestCheckResourceAttrPair(datasource2Name, "vpn_port", resourceName, "vpn_port"), resource.TestCheckResourceAttrPair(datasource3Name, "arn", resourceName, "arn"), @@ -71,12 +75,14 @@ func testAccClientVPNEndpointDataSource_basic(t *testing.T) { resource.TestCheckResourceAttrPair(datasource3Name, "description", resourceName, "description"), resource.TestCheckResourceAttrPair(datasource3Name, "dns_name", resourceName, "dns_name"), resource.TestCheckResourceAttrPair(datasource3Name, "dns_servers.#", resourceName, "dns_servers.#"), + resource.TestCheckResourceAttrPair(datasource3Name, "security_group_ids.#", resourceName, "security_group_ids.#"), resource.TestCheckResourceAttrPair(datasource3Name, "self_service_portal", resourceName, "self_service_portal"), resource.TestCheckResourceAttrPair(datasource3Name, "server_certificate_arn", resourceName, "server_certificate_arn"), resource.TestCheckResourceAttrPair(datasource3Name, "session_timeout_hours", resourceName, "session_timeout_hours"), resource.TestCheckResourceAttrPair(datasource3Name, "split_tunnel", resourceName, "split_tunnel"), resource.TestCheckResourceAttrPair(datasource3Name, "tags.%", resourceName, "tags.%"), resource.TestCheckResourceAttrPair(datasource3Name, "transport_protocol", resourceName, "transport_protocol"), + resource.TestCheckResourceAttrPair(datasource3Name, "vpc_id", resourceName, "vpc_id"), resource.TestCheckResourceAttrPair(datasource3Name, "vpn_port", resourceName, "vpn_port"), ), }, diff --git a/internal/service/ec2/client_vpn_endpoint_test.go b/internal/service/ec2/client_vpn_endpoint_test.go index f506a6d5a1d..d8e9172a5c4 100644 --- a/internal/service/ec2/client_vpn_endpoint_test.go +++ b/internal/service/ec2/client_vpn_endpoint_test.go @@ -45,6 +45,8 @@ func TestAccEC2ClientVPNEndpoint_serial(t *testing.T) { "tags": testAccClientVPNEndpoint_tags, "simpleAttributesUpdate": testAccClientVPNEndpoint_simpleAttributesUpdate, "selfServicePortal": testAccClientVPNEndpoint_selfServicePortal, + "vpcNoSecurityGroups": testAccClientVPNEndpoint_vpcNoSecurityGroups, + "vpcSecurityGroups": testAccClientVPNEndpoint_vpcSecurityGroups, "basicDataSource": testAccClientVPNEndpointDataSource_basic, }, "AuthorizationRule": { @@ -55,10 +57,11 @@ func TestAccEC2ClientVPNEndpoint_serial(t *testing.T) { "disappearsEndpoint": testAccClientVPNAuthorizationRule_Disappears_endpoint, }, "NetworkAssociation": { - "basic": testAccClientVPNNetworkAssociation_basic, - "multipleSubnets": testAccClientVPNNetworkAssociation_multipleSubnets, - "disappears": testAccClientVPNNetworkAssociation_disappears, - "securityGroups": testAccClientVPNNetworkAssociation_securityGroups, + "basic": testAccClientVPNNetworkAssociation_basic, + "multipleSubnets": testAccClientVPNNetworkAssociation_multipleSubnets, + "disappears": testAccClientVPNNetworkAssociation_disappears, + "securityGroups": testAccClientVPNNetworkAssociation_securityGroups, + "securityGroupsOnEndpoint": testAccClientVPNNetworkAssociation_securityGroupsOnEndpoint, }, "Route": { "basic": testAccClientVPNRoute_basic, @@ -118,6 +121,7 @@ func testAccClientVPNEndpoint_basic(t *testing.T) { resource.TestCheckResourceAttr(resourceName, "description", ""), resource.TestCheckResourceAttrSet(resourceName, "dns_name"), resource.TestCheckResourceAttr(resourceName, "dns_servers.#", "0"), + resource.TestCheckResourceAttr(resourceName, "security_group_ids.#", "0"), resource.TestCheckResourceAttr(resourceName, "self_service_portal", "disabled"), resource.TestCheckResourceAttrSet(resourceName, "server_certificate_arn"), resource.TestCheckResourceAttr(resourceName, "session_timeout_hours", "24"), @@ -125,6 +129,7 @@ func testAccClientVPNEndpoint_basic(t *testing.T) { resource.TestCheckResourceAttr(resourceName, "status", ec2.ClientVpnEndpointStatusCodePendingAssociate), resource.TestCheckResourceAttr(resourceName, "tags.%", "0"), resource.TestCheckResourceAttr(resourceName, "transport_protocol", "udp"), + resource.TestCheckResourceAttr(resourceName, "vpc_id", ""), resource.TestCheckResourceAttr(resourceName, "vpn_port", "443"), ), }, @@ -624,6 +629,79 @@ func testAccClientVPNEndpoint_selfServicePortal(t *testing.T) { }) } +func testAccClientVPNEndpoint_vpcNoSecurityGroups(t *testing.T) { + var v ec2.ClientVpnEndpoint + rName := sdkacctest.RandomWithPrefix(acctest.ResourcePrefix) + resourceName := "aws_ec2_client_vpn_endpoint.test" + defaultSecurityGroupResourceName := "aws_default_security_group.test" + vpcResourceName := "aws_vpc.test" + + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { testAccPreCheckClientVPNSyncronize(t); acctest.PreCheck(t) }, + ErrorCheck: acctest.ErrorCheck(t, ec2.EndpointsID), + Providers: acctest.Providers, + CheckDestroy: testAccCheckClientVPNEndpointDestroy, + Steps: []resource.TestStep{ + { + Config: testAccEc2ClientVpnEndpointConfigSecurityGroups(rName, 0), + Check: resource.ComposeTestCheckFunc( + testAccCheckClientVPNEndpointExists(resourceName, &v), + resource.TestCheckResourceAttr(resourceName, "security_group_ids.#", "1"), + resource.TestCheckTypeSetElemAttrPair(resourceName, "security_group_ids.*", defaultSecurityGroupResourceName, "id"), + resource.TestCheckResourceAttrPair(resourceName, "vpc_id", vpcResourceName, "id"), + ), + }, + { + ResourceName: resourceName, + ImportState: true, + ImportStateVerify: true, + }, + }, + }) +} + +func testAccClientVPNEndpoint_vpcSecurityGroups(t *testing.T) { + var v ec2.ClientVpnEndpoint + rName := sdkacctest.RandomWithPrefix(acctest.ResourcePrefix) + resourceName := "aws_ec2_client_vpn_endpoint.test" + securityGroup1ResourceName := "aws_security_group.test1" + securityGroup2ResourceName := "aws_security_group.test2" + vpcResourceName := "aws_vpc.test" + + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { testAccPreCheckClientVPNSyncronize(t); acctest.PreCheck(t) }, + ErrorCheck: acctest.ErrorCheck(t, ec2.EndpointsID), + Providers: acctest.Providers, + CheckDestroy: testAccCheckClientVPNEndpointDestroy, + Steps: []resource.TestStep{ + { + Config: testAccEc2ClientVpnEndpointConfigSecurityGroups(rName, 2), + Check: resource.ComposeTestCheckFunc( + testAccCheckClientVPNEndpointExists(resourceName, &v), + resource.TestCheckResourceAttr(resourceName, "security_group_ids.#", "2"), + resource.TestCheckTypeSetElemAttrPair(resourceName, "security_group_ids.*", securityGroup1ResourceName, "id"), + resource.TestCheckTypeSetElemAttrPair(resourceName, "security_group_ids.*", securityGroup2ResourceName, "id"), + resource.TestCheckResourceAttrPair(resourceName, "vpc_id", vpcResourceName, "id"), + ), + }, + { + ResourceName: resourceName, + ImportState: true, + ImportStateVerify: true, + }, + { + Config: testAccEc2ClientVpnEndpointConfigSecurityGroups(rName, 1), + Check: resource.ComposeTestCheckFunc( + testAccCheckClientVPNEndpointExists(resourceName, &v), + resource.TestCheckResourceAttr(resourceName, "security_group_ids.#", "1"), + resource.TestCheckTypeSetElemAttrPair(resourceName, "security_group_ids.*", securityGroup1ResourceName, "id"), + resource.TestCheckResourceAttrPair(resourceName, "vpc_id", vpcResourceName, "id"), + ), + }, + }, + }) +} + func testAccPreCheckClientVPNSyncronize(t *testing.T) { sync.TestAccPreCheckSyncronize(t, testAccEc2ClientVpnEndpointSemaphore, "Client VPN") } @@ -688,7 +766,7 @@ resource "aws_acm_certificate" %[1]q { `, n, acctest.TLSPEMEscapeNewlines(certificate), acctest.TLSPEMEscapeNewlines(key)) } -func testAccEc2ClientVpnEndpointMsADBase(rName, domain string) string { +func testAccEc2ClientVpnEndpointConfigMsADBase(rName, domain string) string { return acctest.ConfigCompose( acctest.ConfigAvailableAZsNoOptIn(), fmt.Sprintf(` @@ -724,6 +802,53 @@ resource "aws_subnet" "test" { `, rName, domain)) } +func testAccEc2ClientVpnEndpointConfigVPCBase(rName string) string { + return acctest.ConfigCompose( + acctest.ConfigAvailableAZsNoOptIn(), + fmt.Sprintf(` +resource "aws_vpc" "test" { + cidr_block = "10.1.0.0/16" + + tags = { + Name = %[1]q + } +} + +resource "aws_subnet" "test" { + count = 2 + availability_zone = data.aws_availability_zones.available.names[count.index] + cidr_block = cidrsubnet(aws_vpc.test.cidr_block, 8, count.index) + vpc_id = aws_vpc.test.id + + tags = { + Name = %[1]q + } +} + +resource "aws_default_security_group" "test" { + vpc_id = aws_vpc.test.id +} + +resource "aws_security_group" "test1" { + name = "%[1]s-1" + vpc_id = aws_vpc.test.id + + tags = { + Name = %[1]q + } +} + +resource "aws_security_group" "test2" { + name = "%[1]s-2" + vpc_id = aws_vpc.test.id + + tags = { + Name = %[1]q + } +} +`, rName)) +} + func testAccEc2ClientVpnEndpointConfigBasic() string { return acctest.ConfigCompose(testAccEc2ClientVpnEndpointConfigAcmCertificateBase("test"), ` resource "aws_ec2_client_vpn_endpoint" "test" { @@ -943,7 +1068,7 @@ resource "aws_ec2_client_vpn_endpoint" "test" { func testAccEc2ClientVpnEndpointConfigWithMicrosoftAD(rName, domain string) string { return acctest.ConfigCompose( testAccEc2ClientVpnEndpointConfigAcmCertificateBase("test"), - testAccEc2ClientVpnEndpointMsADBase(rName, domain), + testAccEc2ClientVpnEndpointConfigMsADBase(rName, domain), fmt.Sprintf(` resource "aws_ec2_client_vpn_endpoint" "test" { server_certificate_arn = aws_acm_certificate.test.arn @@ -968,7 +1093,7 @@ resource "aws_ec2_client_vpn_endpoint" "test" { func testAccEc2ClientVpnEndpointConfigWithMutualAuthAndMicrosoftAD(rName, domain string) string { return acctest.ConfigCompose( testAccEc2ClientVpnEndpointConfigAcmCertificateBase("test"), - testAccEc2ClientVpnEndpointMsADBase(rName, domain), + testAccEc2ClientVpnEndpointConfigMsADBase(rName, domain), fmt.Sprintf(` resource "aws_ec2_client_vpn_endpoint" "test" { server_certificate_arn = aws_acm_certificate.test.arn @@ -1189,3 +1314,38 @@ resource "aws_ec2_client_vpn_endpoint" "test" { } `, rName, selfServicePortal, idpEntityID)) } + +func testAccEc2ClientVpnEndpointConfigSecurityGroups(rName string, nSecurityGroups int) string { + return acctest.ConfigCompose( + testAccEc2ClientVpnEndpointConfigAcmCertificateBase("test"), + testAccEc2ClientVpnEndpointConfigVPCBase(rName), + fmt.Sprintf(` +locals { + security_group_count = %[2]d + security_group_ids = local.security_group_count == 0 ? null : (local.security_group_count == 1 ? [aws_security_group.test1.id] : [aws_security_group.test1.id, aws_security_group.test2.id]) +} + +resource "aws_ec2_client_vpn_endpoint" "test" { + server_certificate_arn = aws_acm_certificate.test.arn + client_cidr_block = "10.0.0.0/16" + + authentication_options { + type = "certificate-authentication" + root_certificate_chain_arn = aws_acm_certificate.test.arn + } + + connection_log_options { + enabled = false + } + + tags = { + Name = %[1]q + } + + vpc_id = aws_vpc.test.id + security_group_ids = local.security_group_ids + + depends_on = [aws_subnet.test[0], aws_subnet.test[1]] +} +`, rName, nSecurityGroups)) +} diff --git a/internal/service/ec2/client_vpn_network_association.go b/internal/service/ec2/client_vpn_network_association.go index f841b90322b..9813a239a90 100644 --- a/internal/service/ec2/client_vpn_network_association.go +++ b/internal/service/ec2/client_vpn_network_association.go @@ -41,13 +41,14 @@ func ResourceClientVPNNetworkAssociation() *schema.Resource { ForceNew: true, }, "security_groups": { - Type: schema.TypeSet, - MinItems: 1, - MaxItems: 5, - Optional: true, - Computed: true, - Elem: &schema.Schema{Type: schema.TypeString}, - Set: schema.HashString, + Type: schema.TypeSet, + MinItems: 1, + MaxItems: 5, + Optional: true, + Computed: true, + Elem: &schema.Schema{Type: schema.TypeString}, + Set: schema.HashString, + Deprecated: "Use the `security_group_ids` attribute of the `aws_ec2_client_vpn_endpoint` resource instead.", }, "status": { Type: schema.TypeString, diff --git a/internal/service/ec2/client_vpn_network_association_test.go b/internal/service/ec2/client_vpn_network_association_test.go index 340b7904774..163bde2d146 100644 --- a/internal/service/ec2/client_vpn_network_association_test.go +++ b/internal/service/ec2/client_vpn_network_association_test.go @@ -60,7 +60,7 @@ func testAccClientVPNNetworkAssociation_multipleSubnets(t *testing.T) { var assoc ec2.TargetNetwork var group ec2.SecurityGroup rName := sdkacctest.RandomWithPrefix(acctest.ResourcePrefix) - resourceNames := []string{"aws_ec2_client_vpn_network_association.test", "aws_ec2_client_vpn_network_association.test2"} + resourceNames := []string{"aws_ec2_client_vpn_network_association.test1", "aws_ec2_client_vpn_network_association.test2"} endpointResourceName := "aws_ec2_client_vpn_endpoint.test" subnetResourceNames := []string{"aws_subnet.test1", "aws_subnet.test2"} vpcResourceName := "aws_vpc.test" @@ -142,8 +142,8 @@ func testAccClientVPNNetworkAssociation_securityGroups(t *testing.T) { CheckDestroy: testAccCheckClientVPNNetworkAssociationDestroy, Steps: []resource.TestStep{ { - Config: testAccEc2ClientVpnNetworkAssociationTwoSecurityGroups(rName), - Check: resource.ComposeTestCheckFunc( + Config: testAccEc2ClientVpnNetworkAssociationConfigTwoSecurityGroups(rName), + Check: resource.ComposeAggregateTestCheckFunc( testAccCheckClientVPNNetworkAssociationExists(resourceName, &assoc1), testAccCheckDefaultSecurityGroupExists(securityGroup1ResourceName, &group11), testAccCheckDefaultSecurityGroupExists(securityGroup2ResourceName, &group12), @@ -159,8 +159,8 @@ func testAccClientVPNNetworkAssociation_securityGroups(t *testing.T) { ImportStateIdFunc: testAccClientVPNNetworkAssociationImportStateIdFunc(resourceName), }, { - Config: testAccEc2ClientVpnNetworkAssociationOneSecurityGroup(rName), - Check: resource.ComposeTestCheckFunc( + Config: testAccEc2ClientVpnNetworkAssociationConfigOneSecurityGroup(rName), + Check: resource.ComposeAggregateTestCheckFunc( testAccCheckClientVPNNetworkAssociationExists(resourceName, &assoc2), testAccCheckDefaultSecurityGroupExists(securityGroup1ResourceName, &group21), resource.TestCheckResourceAttr(resourceName, "security_groups.#", "1"), @@ -171,6 +171,35 @@ func testAccClientVPNNetworkAssociation_securityGroups(t *testing.T) { }) } +func testAccClientVPNNetworkAssociation_securityGroupsOnEndpoint(t *testing.T) { + var assoc ec2.TargetNetwork + rName := sdkacctest.RandomWithPrefix(acctest.ResourcePrefix) + resourceName := "aws_ec2_client_vpn_network_association.test" + + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { acctest.PreCheck(t); testAccPreCheckClientVPNSyncronize(t) }, + ErrorCheck: acctest.ErrorCheck(t, ec2.EndpointsID), + Providers: acctest.Providers, + CheckDestroy: testAccCheckClientVPNNetworkAssociationDestroy, + Steps: []resource.TestStep{ + { + Config: testAccEc2ClientVpnNetworkAssociationConfigTwoSecurityGroupsOnEndpoint(rName), + Check: resource.ComposeAggregateTestCheckFunc( + testAccCheckClientVPNNetworkAssociationExists(resourceName, &assoc), + resource.TestCheckResourceAttr(resourceName, "security_groups.#", "2"), + resource.TestCheckResourceAttrSet(resourceName, "vpc_id"), + ), + }, + { + ResourceName: resourceName, + ImportState: true, + ImportStateVerify: true, + ImportStateIdFunc: testAccClientVPNNetworkAssociationImportStateIdFunc(resourceName), + }, + }, + }) +} + func testAccCheckClientVPNNetworkAssociationDestroy(s *terraform.State) error { conn := acctest.Provider.Meta().(*conns.AWSClient).EC2Conn @@ -237,7 +266,7 @@ func testAccClientVPNNetworkAssociationImportStateIdFunc(resourceName string) re } } -func testAccEc2ClientVpnNetworkAssociationBaseConfig(rName string) string { +func testAccEc2ClientVpnNetworkAssociationConfigBase(rName string) string { return acctest.ConfigCompose( testAccEc2ClientVpnEndpointConfig(rName), acctest.ConfigAvailableAZsNoOptInDefaultExclude(), @@ -279,7 +308,7 @@ resource "aws_subnet" "test2" { } func testAccEc2ClientVpnNetworkAssociationConfigBasic(rName string) string { - return acctest.ConfigCompose(testAccEc2ClientVpnNetworkAssociationBaseConfig(rName), ` + return acctest.ConfigCompose(testAccEc2ClientVpnNetworkAssociationConfigBase(rName), ` resource "aws_ec2_client_vpn_network_association" "test" { client_vpn_endpoint_id = aws_ec2_client_vpn_endpoint.test.id subnet_id = aws_subnet.test1.id @@ -288,8 +317,8 @@ resource "aws_ec2_client_vpn_network_association" "test" { } func testAccEc2ClientVpnNetworkAssociationConfigMultipleSubnets(rName string) string { - return acctest.ConfigCompose(testAccEc2ClientVpnNetworkAssociationBaseConfig(rName), ` -resource "aws_ec2_client_vpn_network_association" "test" { + return acctest.ConfigCompose(testAccEc2ClientVpnNetworkAssociationConfigBase(rName), ` +resource "aws_ec2_client_vpn_network_association" "test1" { client_vpn_endpoint_id = aws_ec2_client_vpn_endpoint.test.id subnet_id = aws_subnet.test1.id } @@ -301,10 +330,8 @@ resource "aws_ec2_client_vpn_network_association" "test2" { `) } -func testAccEc2ClientVpnNetworkAssociationTwoSecurityGroups(rName string) string { - return acctest.ConfigCompose( - testAccEc2ClientVpnNetworkAssociationBaseConfig(rName), - fmt.Sprintf(` +func testAccEc2ClientVpnNetworkAssociationConfigTwoSecurityGroups(rName string) string { + return acctest.ConfigCompose(testAccEc2ClientVpnNetworkAssociationConfigBase(rName), fmt.Sprintf(` resource "aws_ec2_client_vpn_network_association" "test" { client_vpn_endpoint_id = aws_ec2_client_vpn_endpoint.test.id subnet_id = aws_subnet.test1.id @@ -331,10 +358,8 @@ resource "aws_security_group" "test2" { `, rName)) } -func testAccEc2ClientVpnNetworkAssociationOneSecurityGroup(rName string) string { - return acctest.ConfigCompose( - testAccEc2ClientVpnNetworkAssociationBaseConfig(rName), - fmt.Sprintf(` +func testAccEc2ClientVpnNetworkAssociationConfigOneSecurityGroup(rName string) string { + return acctest.ConfigCompose(testAccEc2ClientVpnNetworkAssociationConfigBase(rName), fmt.Sprintf(` resource "aws_ec2_client_vpn_network_association" "test" { client_vpn_endpoint_id = aws_ec2_client_vpn_endpoint.test.id subnet_id = aws_subnet.test1.id @@ -360,3 +385,12 @@ resource "aws_security_group" "test2" { } `, rName)) } + +func testAccEc2ClientVpnNetworkAssociationConfigTwoSecurityGroupsOnEndpoint(rName string) string { + return acctest.ConfigCompose(testAccEc2ClientVpnEndpointConfigSecurityGroups(rName, 2), ` +resource "aws_ec2_client_vpn_network_association" "test" { + client_vpn_endpoint_id = aws_ec2_client_vpn_endpoint.test.id + subnet_id = aws_subnet.test[0].id +} +`) +} diff --git a/internal/service/ec2/client_vpn_route.go b/internal/service/ec2/client_vpn_route.go index 23a0c69531b..56b8a1d910b 100644 --- a/internal/service/ec2/client_vpn_route.go +++ b/internal/service/ec2/client_vpn_route.go @@ -3,12 +3,14 @@ package ec2 import ( "fmt" "log" + "strings" "github.com/aws/aws-sdk-go/aws" "github.com/aws/aws-sdk-go/service/ec2" "github.com/hashicorp/aws-sdk-go-base/v2/awsv1shim/v2/tfawserr" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" "github.com/hashicorp/terraform-provider-aws/internal/conns" + "github.com/hashicorp/terraform-provider-aws/internal/tfresource" "github.com/hashicorp/terraform-provider-aws/internal/verify" ) @@ -18,7 +20,12 @@ func ResourceClientVPNRoute() *schema.Resource { Read: resourceClientVPNRouteRead, Delete: resourceClientVPNRouteDelete, Importer: &schema.ResourceImporter{ - State: resourceClientVPNRouteImport, + State: schema.ImportStatePassthrough, + }, + + Timeouts: &schema.ResourceTimeout{ + Create: schema.DefaultTimeout(ClientVPNRouteCreatedTimeout), + Delete: schema.DefaultTimeout(ClientVPNRouteDeletedTimeout), }, Schema: map[string]*schema.Schema{ @@ -27,26 +34,26 @@ func ResourceClientVPNRoute() *schema.Resource { Required: true, ForceNew: true, }, + "description": { + Type: schema.TypeString, + Optional: true, + ForceNew: true, + }, "destination_cidr_block": { Type: schema.TypeString, Required: true, ForceNew: true, ValidateFunc: verify.ValidIPv4CIDRNetworkAddress, }, - "description": { + "origin": { Type: schema.TypeString, - Optional: true, - ForceNew: true, + Computed: true, }, "target_vpc_subnet_id": { Type: schema.TypeString, Required: true, ForceNew: true, }, - "origin": { - Type: schema.TypeString, - Computed: true, - }, "type": { Type: schema.TypeString, Computed: true, @@ -60,66 +67,62 @@ func resourceClientVPNRouteCreate(d *schema.ResourceData, meta interface{}) erro endpointID := d.Get("client_vpn_endpoint_id").(string) targetSubnetID := d.Get("target_vpc_subnet_id").(string) - destinationCidr := d.Get("destination_cidr_block").(string) - - req := &ec2.CreateClientVpnRouteInput{ + destinationCIDR := d.Get("destination_cidr_block").(string) + id := ClientVPNRouteCreateResourceID(endpointID, targetSubnetID, destinationCIDR) + input := &ec2.CreateClientVpnRouteInput{ ClientVpnEndpointId: aws.String(endpointID), - DestinationCidrBlock: aws.String(destinationCidr), + DestinationCidrBlock: aws.String(destinationCIDR), TargetVpcSubnetId: aws.String(targetSubnetID), } if v, ok := d.GetOk("description"); ok { - req.Description = aws.String(v.(string)) + input.Description = aws.String(v.(string)) } - id := ClientVPNRouteCreateID(endpointID, targetSubnetID, destinationCidr) - - _, err := conn.CreateClientVpnRoute(req) + log.Printf("[DEBUG] Creating EC2 Client VPN Route: %s", input) + _, err := tfresource.RetryWhenAWSErrCodeEquals(PropagationTimeout, func() (interface{}, error) { + return conn.CreateClientVpnRoute(input) + }, ErrCodeInvalidClientVpnActiveAssociationNotFound) if err != nil { - return fmt.Errorf("error creating client VPN route %q: %w", id, err) + return fmt.Errorf("error creating EC2 Client VPN Route (%s): %w", id, err) } d.SetId(id) + if _, err := WaitClientVPNRouteCreated(conn, endpointID, targetSubnetID, destinationCIDR, d.Timeout(schema.TimeoutCreate)); err != nil { + return fmt.Errorf("error waiting for EC2 Client VPN Route (%s) create: %w", d.Id(), err) + } + return resourceClientVPNRouteRead(d, meta) } func resourceClientVPNRouteRead(d *schema.ResourceData, meta interface{}) error { conn := meta.(*conns.AWSClient).EC2Conn - resp, err := FindClientVPNRoute(conn, - d.Get("client_vpn_endpoint_id").(string), - d.Get("target_vpc_subnet_id").(string), - d.Get("destination_cidr_block").(string), - ) - - if tfawserr.ErrMessageContains(err, ErrCodeInvalidClientVpnRouteNotFound, "") { - log.Printf("[WARN] EC2 Client VPN Route (%s) not found, removing from state", d.Id()) - d.SetId("") - return nil - } + endpointID, targetSubnetID, destinationCIDR, err := ClientVPNRouteParseResourceID(d.Id()) if err != nil { - return fmt.Errorf("error reading client VPN route (%s): %w", d.Id(), err) + return err } - if resp == nil || len(resp.Routes) == 0 || resp.Routes[0] == nil { + route, err := FindClientVPNRouteByThreePartKey(conn, endpointID, targetSubnetID, destinationCIDR) + + if !d.IsNewResource() && tfresource.NotFound(err) { log.Printf("[WARN] EC2 Client VPN Route (%s) not found, removing from state", d.Id()) d.SetId("") return nil } - if len(resp.Routes) > 1 { - return fmt.Errorf("internal error: found %d results for Client VPN route (%s) status, need 1", len(resp.Routes), d.Id()) + if err != nil { + return fmt.Errorf("error reading EC2 Client VPN Route (%s): %w", d.Id(), err) } - route := resp.Routes[0] d.Set("client_vpn_endpoint_id", route.ClientVpnEndpointId) - d.Set("destination_cidr_block", route.DestinationCidr) d.Set("description", route.Description) - d.Set("target_vpc_subnet_id", route.TargetSubnet) + d.Set("destination_cidr_block", route.DestinationCidr) d.Set("origin", route.Origin) + d.Set("target_vpc_subnet_id", route.TargetSubnet) d.Set("type", route.Type) return nil @@ -128,47 +131,49 @@ func resourceClientVPNRouteRead(d *schema.ResourceData, meta interface{}) error func resourceClientVPNRouteDelete(d *schema.ResourceData, meta interface{}) error { conn := meta.(*conns.AWSClient).EC2Conn - err := deleteClientVpnRoute(conn, &ec2.DeleteClientVpnRouteInput{ - ClientVpnEndpointId: aws.String(d.Get("client_vpn_endpoint_id").(string)), - DestinationCidrBlock: aws.String(d.Get("destination_cidr_block").(string)), - TargetVpcSubnetId: aws.String(d.Get("target_vpc_subnet_id").(string)), - }) + endpointID, targetSubnetID, destinationCIDR, err := ClientVPNRouteParseResourceID(d.Id()) + if err != nil { - return fmt.Errorf("error deleting client VPN route %q: %w", d.Id(), err) + return err } - return nil -} - -func deleteClientVpnRoute(conn *ec2.EC2, input *ec2.DeleteClientVpnRouteInput) error { - id := ClientVPNRouteCreateID( - aws.StringValue(input.ClientVpnEndpointId), - aws.StringValue(input.TargetVpcSubnetId), - aws.StringValue(input.DestinationCidrBlock), - ) + log.Printf("[DEBUG] Deleting EC2 Client VPN Route: %s", d.Id()) + _, err = conn.DeleteClientVpnRoute(&ec2.DeleteClientVpnRouteInput{ + ClientVpnEndpointId: aws.String(endpointID), + DestinationCidrBlock: aws.String(destinationCIDR), + TargetVpcSubnetId: aws.String(targetSubnetID), + }) - _, err := conn.DeleteClientVpnRoute(input) - if tfawserr.ErrMessageContains(err, ErrCodeInvalidClientVpnRouteNotFound, "") { + if tfawserr.ErrCodeEquals(err, ErrCodeInvalidClientVpnEndpointIdNotFound, ErrCodeInvalidClientVpnRouteNotFound) { return nil } + if err != nil { - return err + return fmt.Errorf("error deleting EC2 Client VPN Route (%s): %w", d.Id(), err) } - _, err = WaitClientVPNRouteDeleted(conn, id) + if _, err := WaitClientVPNRouteDeleted(conn, endpointID, targetSubnetID, destinationCIDR, d.Timeout(schema.TimeoutDelete)); err != nil { + return fmt.Errorf("error waiting for EC2 Client VPN Route (%s) delete: %w", d.Id(), err) + } - return err + return nil } -func resourceClientVPNRouteImport(d *schema.ResourceData, meta interface{}) ([]*schema.ResourceData, error) { - endpointID, targetSubnetID, destinationCidr, err := ClientVPNRouteParseID(d.Id()) - if err != nil { - return nil, err - } +const clientVPNRouteIDSeparator = "," + +func ClientVPNRouteCreateResourceID(endpointID, targetSubnetID, destinationCIDR string) string { + parts := []string{endpointID, targetSubnetID, destinationCIDR} + id := strings.Join(parts, clientVPNRouteIDSeparator) + + return id +} + +func ClientVPNRouteParseResourceID(id string) (string, string, string, error) { + parts := strings.Split(id, clientVPNRouteIDSeparator) - d.Set("client_vpn_endpoint_id", endpointID) - d.Set("target_vpc_subnet_id", targetSubnetID) - d.Set("destination_cidr_block", destinationCidr) + if len(parts) == 3 && parts[0] != "" && parts[1] != "" && parts[2] != "" { + return parts[0], parts[1], parts[2], nil + } - return []*schema.ResourceData{d}, nil + return "", "", "", fmt.Errorf("unexpected format for ID (%[1]s), expected EndpointID%[2]sTargetSubnetID%[2]sDestinationCIDRBlock", id, clientVPNRouteIDSeparator) } diff --git a/internal/service/ec2/client_vpn_route_test.go b/internal/service/ec2/client_vpn_route_test.go index 08870a0bd35..e5d68bca7f7 100644 --- a/internal/service/ec2/client_vpn_route_test.go +++ b/internal/service/ec2/client_vpn_route_test.go @@ -5,19 +5,18 @@ import ( "testing" "github.com/aws/aws-sdk-go/service/ec2" - "github.com/hashicorp/aws-sdk-go-base/v2/awsv1shim/v2/tfawserr" sdkacctest "github.com/hashicorp/terraform-plugin-sdk/v2/helper/acctest" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource" "github.com/hashicorp/terraform-plugin-sdk/v2/terraform" "github.com/hashicorp/terraform-provider-aws/internal/acctest" "github.com/hashicorp/terraform-provider-aws/internal/conns" tfec2 "github.com/hashicorp/terraform-provider-aws/internal/service/ec2" + "github.com/hashicorp/terraform-provider-aws/internal/tfresource" ) func testAccClientVPNRoute_basic(t *testing.T) { var v ec2.ClientVpnRoute - rStr := sdkacctest.RandString(5) - + rName := sdkacctest.RandomWithPrefix(acctest.ResourcePrefix) resourceName := "aws_ec2_client_vpn_route.test" endpointResourceName := "aws_ec2_client_vpn_endpoint.test" subnetResourceName := "aws_subnet.test.0" @@ -29,14 +28,14 @@ func testAccClientVPNRoute_basic(t *testing.T) { CheckDestroy: testAccCheckClientVPNRouteDestroy, Steps: []resource.TestStep{ { - Config: testAccEc2ClientVpnRouteConfigBasic(rStr), + Config: testAccEc2ClientVpnRouteConfigBasic(rName), Check: resource.ComposeTestCheckFunc( testAccCheckClientVPNRouteExists(resourceName, &v), resource.TestCheckResourceAttrPair(resourceName, "client_vpn_endpoint_id", endpointResourceName, "id"), - resource.TestCheckResourceAttrPair(resourceName, "target_vpc_subnet_id", subnetResourceName, "id"), - resource.TestCheckResourceAttr(resourceName, "destination_cidr_block", "0.0.0.0/0"), resource.TestCheckResourceAttr(resourceName, "description", ""), + resource.TestCheckResourceAttr(resourceName, "destination_cidr_block", "0.0.0.0/0"), resource.TestCheckResourceAttr(resourceName, "origin", "add-route"), + resource.TestCheckResourceAttrPair(resourceName, "target_vpc_subnet_id", subnetResourceName, "id"), resource.TestCheckResourceAttr(resourceName, "type", "Nat"), ), }, @@ -49,13 +48,10 @@ func testAccClientVPNRoute_basic(t *testing.T) { }) } -func testAccClientVPNRoute_description(t *testing.T) { +func testAccClientVPNRoute_disappears(t *testing.T) { var v ec2.ClientVpnRoute - rStr := sdkacctest.RandString(5) - + rName := sdkacctest.RandomWithPrefix(acctest.ResourcePrefix) resourceName := "aws_ec2_client_vpn_route.test" - endpointResourceName := "aws_ec2_client_vpn_endpoint.test" - subnetResourceName := "aws_subnet.test.0" resource.ParallelTest(t, resource.TestCase{ PreCheck: func() { testAccPreCheckClientVPNSyncronize(t); acctest.PreCheck(t) }, @@ -64,27 +60,20 @@ func testAccClientVPNRoute_description(t *testing.T) { CheckDestroy: testAccCheckClientVPNRouteDestroy, Steps: []resource.TestStep{ { - Config: testAccEc2ClientVpnRouteConfigDescription(rStr), + Config: testAccEc2ClientVpnRouteConfigBasic(rName), Check: resource.ComposeTestCheckFunc( testAccCheckClientVPNRouteExists(resourceName, &v), - resource.TestCheckResourceAttrPair(resourceName, "client_vpn_endpoint_id", endpointResourceName, "id"), - resource.TestCheckResourceAttrPair(resourceName, "target_vpc_subnet_id", subnetResourceName, "id"), - resource.TestCheckResourceAttr(resourceName, "description", "test client VPN route"), + acctest.CheckResourceDisappears(acctest.Provider, tfec2.ResourceClientVPNRoute(), resourceName), ), - }, - { - ResourceName: resourceName, - ImportState: true, - ImportStateVerify: true, + ExpectNonEmptyPlan: true, }, }, }) } -func testAccClientVPNRoute_disappears(t *testing.T) { +func testAccClientVPNRoute_description(t *testing.T) { var v ec2.ClientVpnRoute - rStr := sdkacctest.RandString(5) - + rName := sdkacctest.RandomWithPrefix(acctest.ResourcePrefix) resourceName := "aws_ec2_client_vpn_route.test" resource.ParallelTest(t, resource.TestCase{ @@ -94,12 +83,16 @@ func testAccClientVPNRoute_disappears(t *testing.T) { CheckDestroy: testAccCheckClientVPNRouteDestroy, Steps: []resource.TestStep{ { - Config: testAccEc2ClientVpnRouteConfigBasic(rStr), + Config: testAccEc2ClientVpnRouteConfigDescription(rName), Check: resource.ComposeTestCheckFunc( testAccCheckClientVPNRouteExists(resourceName, &v), - acctest.CheckResourceDisappears(acctest.Provider, tfec2.ResourceClientVPNRoute(), resourceName), + resource.TestCheckResourceAttr(resourceName, "description", "test client VPN route"), ), - ExpectNonEmptyPlan: true, + }, + { + ResourceName: resourceName, + ImportState: true, + ImportStateVerify: true, }, }, }) @@ -113,126 +106,69 @@ func testAccCheckClientVPNRouteDestroy(s *terraform.State) error { continue } - _, err := tfec2.FindClientVPNRouteByID(conn, rs.Primary.ID) - if err == nil { - return fmt.Errorf("Client VPN route (%s) still exists", rs.Primary.ID) + endpointID, targetSubnetID, destinationCIDR, err := tfec2.ClientVPNRouteParseResourceID(rs.Primary.ID) + + if err != nil { + return err } - if tfawserr.ErrMessageContains(err, tfec2.ErrCodeInvalidClientVpnRouteNotFound, "") { + + _, err = tfec2.FindClientVPNRouteByThreePartKey(conn, endpointID, targetSubnetID, destinationCIDR) + + if tfresource.NotFound(err) { continue } + + if err != nil { + return err + } + + return fmt.Errorf("EC2 Client VPN Route %s still exists", rs.Primary.ID) } return nil } -func testAccCheckClientVPNRouteExists(name string, route *ec2.ClientVpnRoute) resource.TestCheckFunc { +func testAccCheckClientVPNRouteExists(name string, v *ec2.ClientVpnRoute) 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") + return fmt.Errorf("No EC2 Client VPN Route ID is set") } - conn := acctest.Provider.Meta().(*conns.AWSClient).EC2Conn + endpointID, targetSubnetID, destinationCIDR, err := tfec2.ClientVPNRouteParseResourceID(rs.Primary.ID) - resp, err := tfec2.FindClientVPNRouteByID(conn, rs.Primary.ID) if err != nil { - return fmt.Errorf("Error reading Client VPN route (%s): %w", rs.Primary.ID, err) + return err } - if resp != nil || len(resp.Routes) == 1 || resp.Routes[0] != nil { - *route = *resp.Routes[0] - return nil - } - - return fmt.Errorf("Client VPN route (%s) not found", rs.Primary.ID) - } -} - -func testAccEc2ClientVpnRouteConfigBasic(rName string) string { - return acctest.ConfigCompose( - testAccEc2ClientVpnRouteVpcBase(rName, 1), - testAccEc2ClientVpnRouteAcmCertificateBase(), - fmt.Sprintf(` -resource "aws_ec2_client_vpn_route" "test" { - client_vpn_endpoint_id = aws_ec2_client_vpn_endpoint.test.id - destination_cidr_block = "0.0.0.0/0" - target_vpc_subnet_id = aws_subnet.test[0].id - - depends_on = [ - aws_ec2_client_vpn_network_association.test, - ] -} + conn := acctest.Provider.Meta().(*conns.AWSClient).EC2Conn -resource "aws_ec2_client_vpn_network_association" "test" { - client_vpn_endpoint_id = aws_ec2_client_vpn_endpoint.test.id - subnet_id = aws_subnet.test[0].id -} + output, err := tfec2.FindClientVPNRouteByThreePartKey(conn, endpointID, targetSubnetID, destinationCIDR) -resource "aws_ec2_client_vpn_endpoint" "test" { - description = "terraform-testacc-clientvpn-%[1]s" - server_certificate_arn = aws_acm_certificate.test.arn - client_cidr_block = "10.0.0.0/16" + if err != nil { + return err + } - authentication_options { - type = "certificate-authentication" - root_certificate_chain_arn = aws_acm_certificate.test.arn - } + *v = *output - connection_log_options { - enabled = false - } -} -`, rName)) + return nil + } } -func testAccEc2ClientVpnRouteConfigDescription(rName string) string { +func testAccEc2ClientVpnRouteConfigBase(rName string, subnetCount int) string { return acctest.ConfigCompose( - testAccEc2ClientVpnRouteVpcBase(rName, 1), - testAccEc2ClientVpnRouteAcmCertificateBase(), + testAccEc2ClientVpnEndpointConfig(rName), + acctest.ConfigAvailableAZsNoOptInDefaultExclude(), fmt.Sprintf(` -resource "aws_ec2_client_vpn_route" "test" { - client_vpn_endpoint_id = aws_ec2_client_vpn_endpoint.test.id - destination_cidr_block = "0.0.0.0/0" - target_vpc_subnet_id = aws_subnet.test[0].id - description = "test client VPN route" - - depends_on = [ - aws_ec2_client_vpn_network_association.test, - ] -} - -resource "aws_ec2_client_vpn_network_association" "test" { - client_vpn_endpoint_id = aws_ec2_client_vpn_endpoint.test.id - subnet_id = aws_subnet.test[0].id -} - -resource "aws_ec2_client_vpn_endpoint" "test" { - description = "terraform-testacc-clientvpn-%[1]s" - server_certificate_arn = aws_acm_certificate.test.arn - client_cidr_block = "10.0.0.0/16" - - authentication_options { - type = "certificate-authentication" - root_certificate_chain_arn = aws_acm_certificate.test.arn - } - - connection_log_options { - enabled = false - } -} -`, rName)) -} - -func testAccEc2ClientVpnRouteVpcBase(rName string, subnetCount int) string { - return acctest.ConfigCompose(acctest.ConfigAvailableAZsNoOptInDefaultExclude(), fmt.Sprintf(` resource "aws_vpc" "test" { cidr_block = "10.1.0.0/16" tags = { - Name = "terraform-testacc-subnet-%[1]s" + Name = %[1]q } } @@ -244,20 +180,39 @@ resource "aws_subnet" "test" { map_public_ip_on_launch = true tags = { - Name = "tf-acc-subnet-%[1]s" + Name = %[1]q } } `, rName, subnetCount)) } -func testAccEc2ClientVpnRouteAcmCertificateBase() string { - key := acctest.TLSRSAPrivateKeyPEM(2048) - certificate := acctest.TLSRSAX509SelfSignedCertificatePEM(key, "example.com") +func testAccEc2ClientVpnRouteConfigBasic(rName string) string { + return acctest.ConfigCompose(testAccEc2ClientVpnRouteConfigBase(rName, 1), ` +resource "aws_ec2_client_vpn_route" "test" { + client_vpn_endpoint_id = aws_ec2_client_vpn_network_association.test.client_vpn_endpoint_id + destination_cidr_block = "0.0.0.0/0" + target_vpc_subnet_id = aws_subnet.test[0].id +} - return fmt.Sprintf(` -resource "aws_acm_certificate" "test" { - certificate_body = "%[1]s" - private_key = "%[2]s" +resource "aws_ec2_client_vpn_network_association" "test" { + client_vpn_endpoint_id = aws_ec2_client_vpn_endpoint.test.id + subnet_id = aws_subnet.test[0].id +} +`) +} + +func testAccEc2ClientVpnRouteConfigDescription(rName string) string { + return acctest.ConfigCompose(testAccEc2ClientVpnRouteConfigBase(rName, 1), ` +resource "aws_ec2_client_vpn_route" "test" { + client_vpn_endpoint_id = aws_ec2_client_vpn_network_association.test.client_vpn_endpoint_id + destination_cidr_block = "0.0.0.0/0" + target_vpc_subnet_id = aws_subnet.test[0].id + description = "test client VPN route" +} + +resource "aws_ec2_client_vpn_network_association" "test" { + client_vpn_endpoint_id = aws_ec2_client_vpn_endpoint.test.id + subnet_id = aws_subnet.test[0].id } -`, acctest.TLSPEMEscapeNewlines(certificate), acctest.TLSPEMEscapeNewlines(key)) +`) } diff --git a/internal/service/ec2/errors.go b/internal/service/ec2/errors.go index a3459abd609..b79cd3028a6 100644 --- a/internal/service/ec2/errors.go +++ b/internal/service/ec2/errors.go @@ -21,7 +21,8 @@ const ( ErrCodeInvalidAssociationIDNotFound = "InvalidAssociationID.NotFound" ErrCodeInvalidAttachmentIDNotFound = "InvalidAttachmentID.NotFound" ErrCodeInvalidCarrierGatewayIDNotFound = "InvalidCarrierGatewayID.NotFound" - ErrCodeInvalidClientVpnAssociationIdNotFound = "InvalidClientVpnAssociationId.NotFound" + ErrCodeInvalidClientVpnActiveAssociationNotFound = "InvalidClientVpnActiveAssociationNotFound" + ErrCodeInvalidClientVpnAssociationIdNotFound = "InvalidClientVpnAssociationIdNotFound" ErrCodeInvalidClientVpnAuthorizationRuleNotFound = "InvalidClientVpnEndpointAuthorizationRuleNotFound" ErrCodeInvalidClientVpnEndpointIdNotFound = "InvalidClientVpnEndpointId.NotFound" ErrCodeInvalidClientVpnRouteNotFound = "InvalidClientVpnRouteNotFound" diff --git a/internal/service/ec2/find.go b/internal/service/ec2/find.go index 7c5ecde82f0..d176a9676c1 100644 --- a/internal/service/ec2/find.go +++ b/internal/service/ec2/find.go @@ -269,27 +269,67 @@ func FindClientVPNNetworkAssociationByIDs(conn *ec2.EC2, associationID, endpoint return output, nil } -func FindClientVPNRoute(conn *ec2.EC2, endpointID, targetSubnetID, destinationCidr string) (*ec2.DescribeClientVpnRoutesOutput, error) { - filters := map[string]string{ - "target-subnet": targetSubnetID, - "destination-cidr": destinationCidr, +func FindClientVPNRoute(conn *ec2.EC2, input *ec2.DescribeClientVpnRoutesInput) (*ec2.ClientVpnRoute, error) { + output, err := FindClientVPNRoutes(conn, input) + + if err != nil { + return nil, err } - input := &ec2.DescribeClientVpnRoutesInput{ - ClientVpnEndpointId: aws.String(endpointID), - Filters: BuildAttributeFilterList(filters), + if len(output) == 0 || output[0] == nil || output[0].Status == nil { + return nil, tfresource.NewEmptyResultError(input) } - return conn.DescribeClientVpnRoutes(input) + if count := len(output); count > 1 { + return nil, tfresource.NewTooManyResultsError(count, input) + } + + return output[0], nil } -func FindClientVPNRouteByID(conn *ec2.EC2, routeID string) (*ec2.DescribeClientVpnRoutesOutput, error) { - endpointID, targetSubnetID, destinationCidr, err := ClientVPNRouteParseID(routeID) +func FindClientVPNRoutes(conn *ec2.EC2, input *ec2.DescribeClientVpnRoutesInput) ([]*ec2.ClientVpnRoute, error) { + var output []*ec2.ClientVpnRoute + + err := conn.DescribeClientVpnRoutesPages(input, func(page *ec2.DescribeClientVpnRoutesOutput, lastPage bool) bool { + if page == nil { + return !lastPage + } + + for _, v := range page.Routes { + if v == nil { + continue + } + + output = append(output, v) + } + + return !lastPage + }) + + if tfawserr.ErrCodeEquals(err, ErrCodeInvalidClientVpnEndpointIdNotFound) { + return nil, &resource.NotFoundError{ + LastError: err, + LastRequest: input, + } + } + if err != nil { return nil, err } - return FindClientVPNRoute(conn, endpointID, targetSubnetID, destinationCidr) + return output, nil +} + +func FindClientVPNRouteByThreePartKey(conn *ec2.EC2, endpointID, targetSubnetID, destinationCIDR string) (*ec2.ClientVpnRoute, error) { + input := &ec2.DescribeClientVpnRoutesInput{ + ClientVpnEndpointId: aws.String(endpointID), + Filters: BuildAttributeFilterList(map[string]string{ + "destination-cidr": destinationCIDR, + "target-subnet": targetSubnetID, + }), + } + + return FindClientVPNRoute(conn, input) } func FindCOIPPools(conn *ec2.EC2, input *ec2.DescribeCoipPoolsInput) ([]*ec2.CoipPool, error) { diff --git a/internal/service/ec2/id.go b/internal/service/ec2/id.go index 93b68f3e9ce..7a5559610e9 100644 --- a/internal/service/ec2/id.go +++ b/internal/service/ec2/id.go @@ -7,25 +7,6 @@ import ( "github.com/hashicorp/terraform-provider-aws/internal/create" ) -const clientVpnRouteIDSeparator = "," - -func ClientVPNRouteCreateID(endpointID, targetSubnetID, destinationCidr string) string { - parts := []string{endpointID, targetSubnetID, destinationCidr} - id := strings.Join(parts, clientVpnRouteIDSeparator) - return id -} - -func ClientVPNRouteParseID(id string) (string, string, string, error) { - parts := strings.Split(id, clientVpnRouteIDSeparator) - if len(parts) == 3 && parts[0] != "" && parts[1] != "" && parts[2] != "" { - return parts[0], parts[1], parts[2], nil - } - - return "", "", "", - fmt.Errorf("unexpected format for ID (%q), expected endpoint-id"+clientVpnRouteIDSeparator+ - "target-subnet-id"+clientVpnRouteIDSeparator+"destination-cidr-block", id) -} - const managedPrefixListEntryIDSeparator = "," func ManagedPrefixListEntryCreateID(prefixListID, cidrBlock string) string { diff --git a/internal/service/ec2/status.go b/internal/service/ec2/status.go index 4c829ca0837..fb736cdcad9 100644 --- a/internal/service/ec2/status.go +++ b/internal/service/ec2/status.go @@ -141,37 +141,19 @@ func StatusClientVPNNetworkAssociation(conn *ec2.EC2, associationID, endpointID } } -const ( - ClientVPNRouteStatusNotFound = "NotFound" - - ClientVPNRouteStatusUnknown = "Unknown" -) - -// StatusClientVPNRoute fetches the Client VPN route and its Status -func StatusClientVPNRoute(conn *ec2.EC2, routeID string) resource.StateRefreshFunc { +func StatusClientVPNRoute(conn *ec2.EC2, endpointID, targetSubnetID, destinationCIDR string) resource.StateRefreshFunc { return func() (interface{}, string, error) { - result, err := FindClientVPNRouteByID(conn, routeID) - if tfawserr.ErrCodeEquals(err, ErrCodeInvalidClientVpnRouteNotFound) { - return nil, ClientVPNRouteStatusNotFound, nil - } - if err != nil { - return nil, ClientVPNRouteStatusUnknown, err - } + output, err := FindClientVPNRouteByThreePartKey(conn, endpointID, targetSubnetID, destinationCIDR) - if result == nil || len(result.Routes) == 0 || result.Routes[0] == nil { - return nil, ClientVPNRouteStatusNotFound, nil - } - - if len(result.Routes) > 1 { - return nil, ClientVPNRouteStatusUnknown, fmt.Errorf("internal error: found %d results for Client VPN route (%s) status, need 1", len(result.Routes), routeID) + if tfresource.NotFound(err) { + return nil, "", nil } - rule := result.Routes[0] - if rule.Status == nil || rule.Status.Code == nil { - return rule, ClientVPNRouteStatusUnknown, nil + if err != nil { + return nil, "", err } - return rule, aws.StringValue(rule.Status.Code), nil + return output, aws.StringValue(output.Status.Code), nil } } diff --git a/internal/service/ec2/wait.go b/internal/service/ec2/wait.go index e09605eb183..987af95a044 100644 --- a/internal/service/ec2/wait.go +++ b/internal/service/ec2/wait.go @@ -250,20 +250,42 @@ func WaitClientVPNNetworkAssociationDeleted(conn *ec2.EC2, associationID, endpoi } const ( + ClientVPNRouteCreatedTimeout = 1 * time.Minute ClientVPNRouteDeletedTimeout = 1 * time.Minute ) -func WaitClientVPNRouteDeleted(conn *ec2.EC2, routeID string) (*ec2.ClientVpnRoute, error) { +func WaitClientVPNRouteCreated(conn *ec2.EC2, endpointID, targetSubnetID, destinationCIDR string, timeout time.Duration) (*ec2.ClientVpnRoute, error) { + stateConf := &resource.StateChangeConf{ + Pending: []string{ec2.ClientVpnRouteStatusCodeCreating}, + Target: []string{ec2.ClientVpnRouteStatusCodeActive}, + Refresh: StatusClientVPNRoute(conn, endpointID, targetSubnetID, destinationCIDR), + Timeout: timeout, + } + + outputRaw, err := stateConf.WaitForState() + + if output, ok := outputRaw.(*ec2.ClientVpnRoute); ok { + tfresource.SetLastError(err, errors.New(aws.StringValue(output.Status.Message))) + + return output, err + } + + return nil, err +} + +func WaitClientVPNRouteDeleted(conn *ec2.EC2, endpointID, targetSubnetID, destinationCIDR string, timeout time.Duration) (*ec2.ClientVpnRoute, error) { stateConf := &resource.StateChangeConf{ Pending: []string{ec2.ClientVpnRouteStatusCodeActive, ec2.ClientVpnRouteStatusCodeDeleting}, Target: []string{}, - Refresh: StatusClientVPNRoute(conn, routeID), - Timeout: ClientVPNRouteDeletedTimeout, + Refresh: StatusClientVPNRoute(conn, endpointID, targetSubnetID, destinationCIDR), + Timeout: timeout, } outputRaw, err := stateConf.WaitForState() if output, ok := outputRaw.(*ec2.ClientVpnRoute); ok { + tfresource.SetLastError(err, errors.New(aws.StringValue(output.Status.Message))) + return output, err } diff --git a/website/docs/d/ec2_client_vpn_endpoint.html.markdown b/website/docs/d/ec2_client_vpn_endpoint.html.markdown index 89a1ba07ee8..8fef734ced3 100644 --- a/website/docs/d/ec2_client_vpn_endpoint.html.markdown +++ b/website/docs/d/ec2_client_vpn_endpoint.html.markdown @@ -61,9 +61,11 @@ In addition to all arguments above, the following attributes are exported: * `description` - A brief description of the endpoint. * `dns_name` - The DNS name to be used by clients when connecting to the Client VPN endpoint. * `dns_servers` - Information about the DNS servers to be used for DNS resolution. +* `security_group_ids` - The IDs of the security groups for the target network associated with the Client VPN endpoint. * `self_service_portal` - Indicates whether the self-service portal for the Client VPN endpoint is enabled. * `server_certificate_arn` - The ARN of the server certificate. * `session_timeout_hours` - The maximum VPN session duration time in hours. * `split_tunnel` - Indicates whether split-tunnel is enabled in the AWS Client VPN endpoint. * `transport_protocol` - The transport protocol used by the Client VPN endpoint. +* `vpc_id` - The ID of the VPC associated with the Client VPN endpoint. * `vpn_port` - The port number for the Client VPN endpoint. diff --git a/website/docs/r/ec2_client_vpn_endpoint.html.markdown b/website/docs/r/ec2_client_vpn_endpoint.html.markdown index 4e8440593c0..3b31b17a294 100644 --- a/website/docs/r/ec2_client_vpn_endpoint.html.markdown +++ b/website/docs/r/ec2_client_vpn_endpoint.html.markdown @@ -11,6 +11,8 @@ description: |- Provides an AWS Client VPN endpoint for OpenVPN clients. 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). +~> **NOTE on Client VPN endpoint target network security groups:** Terraform provides both a standalone [Client VPN endpoint network association](ec2_client_vpn_network_association.html) resource with a (deprecated) `security_groups` argument and a Client VPN endpoint resource with a `security_group_ids` argument. Do not specify security groups in both resources. Doing so will cause a conflict and will overwrite the target network security group association. + ## Example Usage ```terraform @@ -43,12 +45,14 @@ The following arguments are supported: * `connection_log_options` - (Required) Information about the client connection logging options. * `description` - (Optional) A brief description of the Client VPN endpoint. * `dns_servers` - (Optional) Information about the DNS servers to be used for DNS resolution. A Client VPN endpoint can have up to two DNS servers. If no DNS server is specified, the DNS address of the connecting device is used. +* `security_group_ids` - (Optional) The IDs of one or more security groups to apply to the target network. You must also specify the ID of the VPC that contains the security groups. * `self_service_portal` - (Optional) Specify whether to enable the self-service portal for the Client VPN endpoint. Values can be `enabled` or `disabled`. Default value is `disabled`. * `server_certificate_arn` - (Required) The ARN of the ACM server certificate. * `session_timeout_hours` - (Optional) The maximum session duration is a trigger by which end-users are required to re-authenticate prior to establishing a VPN session. Default value is `24` - Valid values: `8 | 10 | 12 | 24` * `split_tunnel` - (Optional) Indicates whether split-tunnel is enabled on VPN endpoint. Default value is `false`. * `tags` - (Optional) A mapping of tags to assign to the resource. If configured with a provider [`default_tags` configuration block](https://www.terraform.io/docs/providers/aws/index.html#default_tags-configuration-block) present, tags with matching keys will overwrite those defined at the provider-level. * `transport_protocol` - (Optional) The transport protocol to be used by the VPN session. Default value is `udp`. +* `vpc_id` - (Optional) The ID of the VPC to associate with the Client VPN endpoint. If no security group IDs are specified in the request, the default security group for the VPC is applied. * `vpn_port` - (Optional) The port number for the Client VPN endpoint. Valid values are `443` and `1194`. Default value is `443`. ### `authentication_options` Argument Reference diff --git a/website/docs/r/ec2_client_vpn_network_association.html.markdown b/website/docs/r/ec2_client_vpn_network_association.html.markdown index d9819acd1ff..8d62045c6e9 100644 --- a/website/docs/r/ec2_client_vpn_network_association.html.markdown +++ b/website/docs/r/ec2_client_vpn_network_association.html.markdown @@ -11,6 +11,8 @@ description: |- 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). +~> **NOTE on Client VPN endpoint target network security groups:** Terraform provides both a standalone Client VPN endpoint network association resource with a (deprecated) `security_groups` argument and a [Client VPN endpoint](ec2_client_vpn_endpoint.html) resource with a `security_group_ids` argument. Do not specify security groups in both resources. Doing so will cause a conflict and will overwrite the target network security group association. + ## Example Usage ### Using default security group @@ -38,7 +40,7 @@ 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. -* `security_groups` - (Optional) A list of up to five custom security groups to apply to the target network. If not specified, the VPC's default security group is assigned. +* `security_groups` - (Optional, **Deprecated** use the `security_group_ids` argument of the `aws_ec2_client_vpn_endpoint` resource instead) A list of up to five custom security groups to apply to the target network. If not specified, the VPC's default security group is assigned. ## Attributes Reference @@ -46,7 +48,6 @@ In addition to all arguments above, the following attributes are exported: * `id` - The unique ID of the target network association. * `association_id` - The unique ID of the target network association. -* `security_groups` - The IDs of the security groups applied to the target network association. * `status` - **Deprecated** The current state of the target network association. * `vpc_id` - The ID of the VPC in which the target subnet is located. diff --git a/website/docs/r/ec2_client_vpn_route.html.markdown b/website/docs/r/ec2_client_vpn_route.html.markdown index 3effe220809..0264f2ae595 100644 --- a/website/docs/r/ec2_client_vpn_route.html.markdown +++ b/website/docs/r/ec2_client_vpn_route.html.markdown @@ -58,6 +58,13 @@ In addition to all arguments above, the following attributes are exported: * `origin` - Indicates how the Client VPN route was added. Will be `add-route` for routes created by this resource. * `type` - The type of the route. +## Timeouts + +`aws_ec2_client_vpn_route` provides the following [Timeouts](https://www.terraform.io/docs/configuration/blocks/resources/syntax.html#operation-timeouts) configuration options: + +- `create` - (Default `1 minute`) Used for route creation +- `delete` - (Default `1 minute`) Used for route deletion + ## Import AWS Client VPN routes can be imported using the endpoint ID, target subnet ID, and destination CIDR block. All values are separated by a `,`.