diff --git a/.changelog/14218.txt b/.changelog/14218.txt new file mode 100644 index 000000000000..d6d6a2efb64f --- /dev/null +++ b/.changelog/14218.txt @@ -0,0 +1,3 @@ +```release-note:new-data-source +aws_ec2_client_vpn_endpoint +``` \ No newline at end of file diff --git a/.changelog/20689.txt b/.changelog/20689.txt new file mode 100644 index 000000000000..8753867cfaf6 --- /dev/null +++ b/.changelog/20689.txt @@ -0,0 +1,3 @@ +```release-note:enhancement +resource/aws_ec2_client_vpn_network_association: Configurable Create and Delete timeouts +``` \ No newline at end of file diff --git a/internal/provider/provider.go b/internal/provider/provider.go index 90d0a9c1d82d..b764368a14dd 100644 --- a/internal/provider/provider.go +++ b/internal/provider/provider.go @@ -450,6 +450,7 @@ func Provider() *schema.Provider { "aws_ebs_snapshot_ids": ec2.DataSourceEBSSnapshotIDs(), "aws_ebs_volume": ec2.DataSourceEBSVolume(), "aws_ebs_volumes": ec2.DataSourceEBSVolumes(), + "aws_ec2_client_vpn_endpoint": ec2.DataSourceClientVPNEndpoint(), "aws_ec2_coip_pool": ec2.DataSourceCoIPPool(), "aws_ec2_coip_pools": ec2.DataSourceCoIPPools(), "aws_ec2_host": ec2.DataSourceHost(), diff --git a/internal/service/ec2/client_vpn_authorization_rule.go b/internal/service/ec2/client_vpn_authorization_rule.go index bd64b9f639db..21faaf001b06 100644 --- a/internal/service/ec2/client_vpn_authorization_rule.go +++ b/internal/service/ec2/client_vpn_authorization_rule.go @@ -113,7 +113,7 @@ func resourceClientVPNAuthorizationRuleRead(d *schema.ResourceData, meta interfa return err } - rule, err := FindClientVPNAuthorizationRuleByEndpointIDTargetNetworkCIDRAndGroupID(conn, endpointID, targetNetworkCIDR, accessGroupID) + rule, err := FindClientVPNAuthorizationRuleByThreePartKey(conn, endpointID, targetNetworkCIDR, accessGroupID) if !d.IsNewResource() && tfresource.NotFound(err) { log.Printf("[WARN] EC2 Client VPN Authorization Rule (%s) not found, removing from state", d.Id()) diff --git a/internal/service/ec2/client_vpn_authorization_rule_test.go b/internal/service/ec2/client_vpn_authorization_rule_test.go index ed5926323044..3e8bb1ade03c 100644 --- a/internal/service/ec2/client_vpn_authorization_rule_test.go +++ b/internal/service/ec2/client_vpn_authorization_rule_test.go @@ -235,7 +235,7 @@ func testAccCheckClientVPNAuthorizationRuleDestroy(s *terraform.State) error { return err } - _, err = tfec2.FindClientVPNAuthorizationRuleByEndpointIDTargetNetworkCIDRAndGroupID(conn, endpointID, targetNetworkCIDR, accessGroupID) + _, err = tfec2.FindClientVPNAuthorizationRuleByThreePartKey(conn, endpointID, targetNetworkCIDR, accessGroupID) if tfresource.NotFound(err) { continue @@ -270,7 +270,7 @@ func testAccCheckClientVPNAuthorizationRuleExists(name string, v *ec2.Authorizat conn := acctest.Provider.Meta().(*conns.AWSClient).EC2Conn - output, err := tfec2.FindClientVPNAuthorizationRuleByEndpointIDTargetNetworkCIDRAndGroupID(conn, endpointID, targetNetworkCIDR, accessGroupID) + output, err := tfec2.FindClientVPNAuthorizationRuleByThreePartKey(conn, endpointID, targetNetworkCIDR, accessGroupID) if err != nil { return err @@ -282,8 +282,11 @@ func testAccCheckClientVPNAuthorizationRuleExists(name string, v *ec2.Authorizat } } -func testAccEc2ClientVpnAuthorizationRuleVpcBase(rName string, subnetCount int) string { - return acctest.ConfigCompose(acctest.ConfigAvailableAZsNoOptInDefaultExclude(), fmt.Sprintf(` +func testAccEc2ClientVpnAuthorizationRuleBaseConfig(rName string, subnetCount int) string { + return acctest.ConfigCompose( + testAccEc2ClientVpnEndpointConfig(rName), + acctest.ConfigAvailableAZsNoOptInDefaultExclude(), + fmt.Sprintf(` resource "aws_vpc" "test" { cidr_block = "10.1.0.0/16" @@ -306,47 +309,14 @@ resource "aws_subnet" "test" { `, rName, subnetCount)) } -func testAccEc2ClientVpnAuthorizationRuleAcmCertificateBase() string { - key := acctest.TLSRSAPrivateKeyPEM(2048) - certificate := acctest.TLSRSAX509SelfSignedCertificatePEM(key, "example.com") - - return fmt.Sprintf(` -resource "aws_acm_certificate" "test" { - certificate_body = "%[1]s" - private_key = "%[2]s" -} -`, acctest.TLSPEMEscapeNewlines(certificate), acctest.TLSPEMEscapeNewlines(key)) -} - func testAccEc2ClientVpnAuthorizationRuleConfigBasic(rName string) string { - return acctest.ConfigCompose( - testAccEc2ClientVpnAuthorizationRuleVpcBase(rName, 1), - testAccEc2ClientVpnAuthorizationRuleAcmCertificateBase(), - fmt.Sprintf(` + return acctest.ConfigCompose(testAccEc2ClientVpnAuthorizationRuleBaseConfig(rName, 1), ` resource "aws_ec2_client_vpn_authorization_rule" "test" { client_vpn_endpoint_id = aws_ec2_client_vpn_endpoint.test.id target_network_cidr = aws_subnet.test[0].cidr_block authorize_all_groups = true } - -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 - } -} -`, rName)) +`) } func testAccEc2ClientVpnAuthorizationRuleConfigGroups(rName string, groupNames map[string]string) string { @@ -361,28 +331,7 @@ resource "aws_ec2_client_vpn_authorization_rule" %[1]q { `, k, v) } - return acctest.ConfigCompose( - testAccEc2ClientVpnAuthorizationRuleVpcBase(rName, 1), - testAccEc2ClientVpnAuthorizationRuleAcmCertificateBase(), - b.String(), - fmt.Sprintf(` -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 - } -}`, rName)) + return acctest.ConfigCompose(testAccEc2ClientVpnAuthorizationRuleBaseConfig(rName, 1), b.String()) } func testAccEc2ClientVpnAuthorizationRuleConfigSubnets(rName string, subnetCount int, groupNames map[string]int) string { @@ -397,26 +346,5 @@ resource "aws_ec2_client_vpn_authorization_rule" %[1]q { `, k, v) } - return acctest.ConfigCompose( - testAccEc2ClientVpnAuthorizationRuleVpcBase(rName, subnetCount), - testAccEc2ClientVpnAuthorizationRuleAcmCertificateBase(), - b.String(), - fmt.Sprintf(` -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 - } -}`, rName)) + return acctest.ConfigCompose(testAccEc2ClientVpnAuthorizationRuleBaseConfig(rName, subnetCount), b.String()) } diff --git a/internal/service/ec2/client_vpn_endpoint_data_source.go b/internal/service/ec2/client_vpn_endpoint_data_source.go new file mode 100644 index 000000000000..1d46f72905c2 --- /dev/null +++ b/internal/service/ec2/client_vpn_endpoint_data_source.go @@ -0,0 +1,238 @@ +package ec2 + +import ( + "fmt" + + "github.com/aws/aws-sdk-go/aws" + "github.com/aws/aws-sdk-go/aws/arn" + "github.com/aws/aws-sdk-go/service/ec2" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" + "github.com/hashicorp/terraform-provider-aws/internal/conns" + tftags "github.com/hashicorp/terraform-provider-aws/internal/tags" + "github.com/hashicorp/terraform-provider-aws/internal/tfresource" +) + +func DataSourceClientVPNEndpoint() *schema.Resource { + return &schema.Resource{ + Read: dataSourceClientVPNEndpointRead, + + Schema: map[string]*schema.Schema{ + "arn": { + Type: schema.TypeString, + Computed: true, + }, + "authentication_options": { + Type: schema.TypeList, + Computed: true, + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "active_directory_id": { + Type: schema.TypeString, + Computed: true, + }, + "root_certificate_chain_arn": { + Type: schema.TypeString, + Computed: true, + }, + "saml_provider_arn": { + Type: schema.TypeString, + Computed: true, + }, + "self_service_saml_provider_arn": { + Type: schema.TypeString, + Computed: true, + }, + "type": { + Type: schema.TypeString, + Computed: true, + }, + }, + }, + }, + "client_cidr_block": { + Type: schema.TypeString, + Computed: true, + }, + "client_connect_options": { + Type: schema.TypeList, + Computed: true, + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "enabled": { + Type: schema.TypeBool, + Computed: true, + }, + "lambda_function_arn": { + Type: schema.TypeString, + Computed: true, + }, + }, + }, + }, + "client_login_banner_options": { + Type: schema.TypeList, + Computed: true, + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "banner_text": { + Type: schema.TypeString, + Computed: true, + }, + "enabled": { + Type: schema.TypeBool, + Computed: true, + }, + }, + }, + }, + "client_vpn_endpoint_id": { + Type: schema.TypeString, + Optional: true, + Computed: true, + }, + "connection_log_options": { + Type: schema.TypeList, + Computed: true, + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "cloudwatch_log_group": { + Type: schema.TypeString, + Computed: true, + }, + "cloudwatch_log_stream": { + Type: schema.TypeString, + Computed: true, + }, + "enabled": { + Type: schema.TypeBool, + Computed: true, + }, + }, + }, + }, + "description": { + Type: schema.TypeString, + Computed: true, + }, + "dns_name": { + Type: schema.TypeString, + Computed: true, + }, + "dns_servers": { + Type: schema.TypeSet, + Computed: true, + Elem: &schema.Schema{Type: schema.TypeString}, + }, + "filter": DataSourceFiltersSchema(), + "self_service_portal": { + Type: schema.TypeString, + Computed: true, + }, + "server_certificate_arn": { + Type: schema.TypeString, + Computed: true, + }, + "session_timeout_hours": { + Type: schema.TypeInt, + Computed: true, + }, + "split_tunnel": { + Type: schema.TypeBool, + Computed: true, + }, + "tags": tftags.TagsSchemaComputed(), + "transport_protocol": { + Type: schema.TypeString, + Computed: true, + }, + "vpn_port": { + Type: schema.TypeInt, + Computed: true, + }, + }, + } +} + +func dataSourceClientVPNEndpointRead(d *schema.ResourceData, meta interface{}) error { + conn := meta.(*conns.AWSClient).EC2Conn + ignoreTagsConfig := meta.(*conns.AWSClient).IgnoreTagsConfig + + input := &ec2.DescribeClientVpnEndpointsInput{} + + if v, ok := d.GetOk("client_vpn_endpoint_id"); ok { + input.ClientVpnEndpointIds = aws.StringSlice([]string{v.(string)}) + } + + input.Filters = append(input.Filters, BuildTagFilterList( + Tags(tftags.New(d.Get("tags").(map[string]interface{}))), + )...) + + input.Filters = append(input.Filters, BuildFiltersDataSource( + d.Get("filter").(*schema.Set), + )...) + + if len(input.Filters) == 0 { + input.Filters = nil + } + + ep, err := FindClientVPNEndpoint(conn, input) + + if err != nil { + return tfresource.SingularDataSourceFindError("EC2 Client VPN Endpoint", err) + } + + d.SetId(aws.StringValue(ep.ClientVpnEndpointId)) + arn := arn.ARN{ + Partition: meta.(*conns.AWSClient).Partition, + Service: ec2.ServiceName, + Region: meta.(*conns.AWSClient).Region, + AccountID: meta.(*conns.AWSClient).AccountID, + Resource: fmt.Sprintf("client-vpn-endpoint/%s", d.Id()), + }.String() + d.Set("arn", arn) + if err := d.Set("authentication_options", flattenClientVpnAuthentications(ep.AuthenticationOptions)); err != nil { + return fmt.Errorf("error setting authentication_options: %w", err) + } + d.Set("client_cidr_block", ep.ClientCidrBlock) + if ep.ClientConnectOptions != nil { + if err := d.Set("client_connect_options", []interface{}{flattenClientConnectResponseOptions(ep.ClientConnectOptions)}); err != nil { + return fmt.Errorf("error setting client_connect_options: %w", err) + } + } else { + d.Set("client_connect_options", nil) + } + if ep.ClientLoginBannerOptions != nil { + if err := d.Set("client_login_banner_options", []interface{}{flattenClientLoginBannerResponseOptions(ep.ClientLoginBannerOptions)}); err != nil { + return fmt.Errorf("error setting client_login_banner_options: %w", err) + } + } else { + d.Set("client_login_banner_options", nil) + } + d.Set("client_vpn_endpoint_id", ep.ClientVpnEndpointId) + if ep.ConnectionLogOptions != nil { + if err := d.Set("connection_log_options", []interface{}{flattenConnectionLogResponseOptions(ep.ConnectionLogOptions)}); err != nil { + return fmt.Errorf("error setting connection_log_options: %w", err) + } + } else { + d.Set("connection_log_options", nil) + } + d.Set("description", ep.Description) + d.Set("dns_name", ep.DnsName) + d.Set("dns_servers", aws.StringValueSlice(ep.DnsServers)) + if aws.StringValue(ep.SelfServicePortalUrl) != "" { + d.Set("self_service_portal", ec2.SelfServicePortalEnabled) + } else { + d.Set("self_service_portal", ec2.SelfServicePortalDisabled) + } + d.Set("server_certificate_arn", ep.ServerCertificateArn) + d.Set("session_timeout_hours", ep.SessionTimeoutHours) + d.Set("split_tunnel", ep.SplitTunnel) + d.Set("transport_protocol", ep.TransportProtocol) + d.Set("vpn_port", ep.VpnPort) + + if err := d.Set("tags", KeyValueTags(ep.Tags).IgnoreAWS().IgnoreConfig(ignoreTagsConfig).Map()); err != nil { + return fmt.Errorf("error setting tags: %w", err) + } + + return 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 new file mode 100644 index 000000000000..3d5b8ac56480 --- /dev/null +++ b/internal/service/ec2/client_vpn_endpoint_data_source_test.go @@ -0,0 +1,106 @@ +package ec2_test + +import ( + "testing" + + "github.com/aws/aws-sdk-go/service/ec2" + sdkacctest "github.com/hashicorp/terraform-plugin-sdk/v2/helper/acctest" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource" + "github.com/hashicorp/terraform-provider-aws/internal/acctest" +) + +func testAccClientVPNEndpointDataSource_basic(t *testing.T) { + rName := sdkacctest.RandomWithPrefix(acctest.ResourcePrefix) + resourceName := "aws_ec2_client_vpn_endpoint.test" + datasource1Name := "data.aws_ec2_client_vpn_endpoint.by_id" + datasource2Name := "data.aws_ec2_client_vpn_endpoint.by_filter" + datasource3Name := "data.aws_ec2_client_vpn_endpoint.by_tags" + + 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: testAccEc2ClientVpnEndpointDataSourceConfig(rName), + Check: resource.ComposeAggregateTestCheckFunc( + resource.TestCheckResourceAttrPair(datasource1Name, "arn", resourceName, "arn"), + resource.TestCheckResourceAttrPair(datasource1Name, "authentication_options.#", resourceName, "authentication_options.#"), + resource.TestCheckResourceAttrPair(datasource1Name, "client_cidr_block", resourceName, "client_cidr_block"), + resource.TestCheckResourceAttrPair(datasource1Name, "client_connect_options.#", resourceName, "client_connect_options.#"), + resource.TestCheckResourceAttrPair(datasource1Name, "client_login_banner_options.#", resourceName, "client_login_banner_options.#"), + resource.TestCheckResourceAttrPair(datasource1Name, "client_vpn_endpoint_id", resourceName, "id"), + resource.TestCheckResourceAttrPair(datasource1Name, "connection_log_options.#", resourceName, "connection_log_options.#"), + 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, "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, "vpn_port", resourceName, "vpn_port"), + + resource.TestCheckResourceAttrPair(datasource2Name, "arn", resourceName, "arn"), + resource.TestCheckResourceAttrPair(datasource2Name, "authentication_options.#", resourceName, "authentication_options.#"), + resource.TestCheckResourceAttrPair(datasource2Name, "client_cidr_block", resourceName, "client_cidr_block"), + resource.TestCheckResourceAttrPair(datasource2Name, "client_connect_options.#", resourceName, "client_connect_options.#"), + resource.TestCheckResourceAttrPair(datasource2Name, "client_login_banner_options.#", resourceName, "client_login_banner_options.#"), + resource.TestCheckResourceAttrPair(datasource2Name, "client_vpn_endpoint_id", resourceName, "id"), + resource.TestCheckResourceAttrPair(datasource2Name, "connection_log_options.#", resourceName, "connection_log_options.#"), + 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, "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, "vpn_port", resourceName, "vpn_port"), + + resource.TestCheckResourceAttrPair(datasource3Name, "arn", resourceName, "arn"), + resource.TestCheckResourceAttrPair(datasource3Name, "authentication_options.#", resourceName, "authentication_options.#"), + resource.TestCheckResourceAttrPair(datasource3Name, "client_cidr_block", resourceName, "client_cidr_block"), + resource.TestCheckResourceAttrPair(datasource3Name, "client_connect_options.#", resourceName, "client_connect_options.#"), + resource.TestCheckResourceAttrPair(datasource3Name, "client_login_banner_options.#", resourceName, "client_login_banner_options.#"), + resource.TestCheckResourceAttrPair(datasource3Name, "client_vpn_endpoint_id", resourceName, "id"), + resource.TestCheckResourceAttrPair(datasource3Name, "connection_log_options.#", resourceName, "connection_log_options.#"), + 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, "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, "vpn_port", resourceName, "vpn_port"), + ), + }, + }, + }) +} + +func testAccEc2ClientVpnEndpointDataSourceConfig(rName string) string { + return acctest.ConfigCompose(testAccEc2ClientVpnEndpointConfig(rName), ` +data "aws_ec2_client_vpn_endpoint" "by_id" { + client_vpn_endpoint_id = aws_ec2_client_vpn_endpoint.test.id +} + +data "aws_ec2_client_vpn_endpoint" "by_tags" { + tags = { + Name = aws_ec2_client_vpn_endpoint.test.tags["Name"] + } +} + +data "aws_ec2_client_vpn_endpoint" "by_filter" { + filter { + name = "endpoint-id" + values = [aws_ec2_client_vpn_endpoint.test.id] + } +} +`) +} diff --git a/internal/service/ec2/client_vpn_endpoint_test.go b/internal/service/ec2/client_vpn_endpoint_test.go index 646c95df9b26..b4a00cf43ad4 100644 --- a/internal/service/ec2/client_vpn_endpoint_test.go +++ b/internal/service/ec2/client_vpn_endpoint_test.go @@ -45,6 +45,7 @@ func TestAccEC2ClientVPNEndpoint_serial(t *testing.T) { "tags": testAccClientVPNEndpoint_tags, "simpleAttributesUpdate": testAccClientVPNEndpoint_simpleAttributesUpdate, "selfServicePortal": testAccClientVPNEndpoint_selfServicePortal, + "basicDataSource": testAccClientVPNEndpointDataSource_basic, }, "AuthorizationRule": { "basic": testAccClientVPNAuthorizationRule_basic, diff --git a/internal/service/ec2/client_vpn_network_association.go b/internal/service/ec2/client_vpn_network_association.go index 9e62cf1bc752..d117ade6a4b8 100644 --- a/internal/service/ec2/client_vpn_network_association.go +++ b/internal/service/ec2/client_vpn_network_association.go @@ -3,6 +3,7 @@ package ec2 import ( "fmt" "log" + "strings" "github.com/aws/aws-sdk-go/aws" "github.com/aws/aws-sdk-go/service/ec2" @@ -10,6 +11,7 @@ import ( "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" "github.com/hashicorp/terraform-provider-aws/internal/conns" "github.com/hashicorp/terraform-provider-aws/internal/flex" + "github.com/hashicorp/terraform-provider-aws/internal/tfresource" ) func ResourceClientVPNNetworkAssociation() *schema.Resource { @@ -18,10 +20,16 @@ func ResourceClientVPNNetworkAssociation() *schema.Resource { Read: resourceClientVPNNetworkAssociationRead, Update: resourceClientVPNNetworkAssociationUpdate, Delete: resourceClientVPNNetworkAssociationDelete, + Importer: &schema.ResourceImporter{ State: resourceClientVPNNetworkAssociationImport, }, + Timeouts: &schema.ResourceTimeout{ + Create: schema.DefaultTimeout(ClientVPNNetworkAssociationCreatedTimeout), + Delete: schema.DefaultTimeout(ClientVPNNetworkAssociationDeletedTimeout), + }, + Schema: map[string]*schema.Schema{ "association_id": { Type: schema.TypeString, @@ -32,11 +40,6 @@ func ResourceClientVPNNetworkAssociation() *schema.Resource { Required: true, ForceNew: true, }, - "subnet_id": { - Type: schema.TypeString, - Required: true, - ForceNew: true, - }, "security_groups": { Type: schema.TypeSet, MinItems: 1, @@ -50,6 +53,11 @@ func ResourceClientVPNNetworkAssociation() *schema.Resource { Type: schema.TypeString, Computed: true, }, + "subnet_id": { + Type: schema.TypeString, + Required: true, + ForceNew: true, + }, "vpc_id": { Type: schema.TypeString, Computed: true, @@ -61,53 +69,39 @@ func ResourceClientVPNNetworkAssociation() *schema.Resource { func resourceClientVPNNetworkAssociationCreate(d *schema.ResourceData, meta interface{}) error { conn := meta.(*conns.AWSClient).EC2Conn - req := &ec2.AssociateClientVpnTargetNetworkInput{ - ClientVpnEndpointId: aws.String(d.Get("client_vpn_endpoint_id").(string)), + endpointID := d.Get("client_vpn_endpoint_id").(string) + input := &ec2.AssociateClientVpnTargetNetworkInput{ + ClientVpnEndpointId: aws.String(endpointID), SubnetId: aws.String(d.Get("subnet_id").(string)), } - log.Printf("[DEBUG] Creating Client VPN network association: %#v", req) - resp, err := conn.AssociateClientVpnTargetNetwork(req) + log.Printf("[DEBUG] Creating EC2 Client VPN Network Association: %s", input) + + output, err := conn.AssociateClientVpnTargetNetwork(input) + if err != nil { - return fmt.Errorf("Error creating Client VPN network association: %w", err) + return fmt.Errorf("error creating EC2 Client VPN Network Association: %w", err) } - d.SetId(aws.StringValue(resp.AssociationId)) + d.SetId(aws.StringValue(output.AssociationId)) + + targetNetwork, err := WaitClientVPNNetworkAssociationCreated(conn, d.Id(), endpointID, d.Timeout(schema.TimeoutCreate)) - log.Printf("[DEBUG] Waiting for Client VPN endpoint to associate with target network: %s", d.Id()) - targetNetwork, err := WaitClientVPNNetworkAssociationAssociated(conn, d.Id(), d.Get("client_vpn_endpoint_id").(string)) if err != nil { - return fmt.Errorf("error waiting for Client VPN endpoint to associate with target network: %w", err) + return fmt.Errorf("error waiting for EC2 Client VPN Network Association (%s) create: %w", d.Id(), err) } if v, ok := d.GetOk("security_groups"); ok { - sgReq := &ec2.ApplySecurityGroupsToClientVpnTargetNetworkInput{ - ClientVpnEndpointId: aws.String(d.Get("client_vpn_endpoint_id").(string)), - VpcId: targetNetwork.VpcId, + input := &ec2.ApplySecurityGroupsToClientVpnTargetNetworkInput{ + ClientVpnEndpointId: aws.String(endpointID), SecurityGroupIds: flex.ExpandStringSet(v.(*schema.Set)), + VpcId: targetNetwork.VpcId, } - _, err := conn.ApplySecurityGroupsToClientVpnTargetNetwork(sgReq) - if err != nil { - return fmt.Errorf("Error applying security groups to Client VPN network association: %s", err) - } - } - - return resourceClientVPNNetworkAssociationRead(d, meta) -} - -func resourceClientVPNNetworkAssociationUpdate(d *schema.ResourceData, meta interface{}) error { - conn := meta.(*conns.AWSClient).EC2Conn + _, err := conn.ApplySecurityGroupsToClientVpnTargetNetwork(input) - if d.HasChange("security_groups") { - input := &ec2.ApplySecurityGroupsToClientVpnTargetNetworkInput{ - ClientVpnEndpointId: aws.String(d.Get("client_vpn_endpoint_id").(string)), - SecurityGroupIds: flex.ExpandStringSet(d.Get("security_groups").(*schema.Set)), - VpcId: aws.String(d.Get("vpc_id").(string)), - } - - if _, err := conn.ApplySecurityGroupsToClientVpnTargetNetwork(input); err != nil { - return fmt.Errorf("error applying security groups to Client VPN Target Network: %s", err) + if err != nil { + return fmt.Errorf("error applying Security Groups to EC2 Client VPN Network Association (%s): %w", d.Id(), err) } } @@ -116,85 +110,83 @@ func resourceClientVPNNetworkAssociationUpdate(d *schema.ResourceData, meta inte func resourceClientVPNNetworkAssociationRead(d *schema.ResourceData, meta interface{}) error { conn := meta.(*conns.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())}, - }) + endpointID := d.Get("client_vpn_endpoint_id").(string) + network, err := FindClientVPNNetworkAssociationByIDs(conn, d.Id(), endpointID) - if tfawserr.ErrMessageContains(err, ErrCodeInvalidClientVpnAssociationIdNotFound, "") || tfawserr.ErrMessageContains(err, ErrCodeInvalidClientVpnEndpointIdNotFound, "") { + if !d.IsNewResource() && tfresource.NotFound(err) { log.Printf("[WARN] EC2 Client VPN Network Association (%s) not found, removing from state", d.Id()) d.SetId("") return nil } if err != nil { - return fmt.Errorf("Error reading Client VPN network association: %w", err) + return fmt.Errorf("error reading EC2 Client VPN Network Association (%s): %w", d.Id(), err) } - if result == nil || len(result.ClientVpnTargetNetworks) == 0 || result.ClientVpnTargetNetworks[0] == nil { - log.Printf("[WARN] EC2 Client VPN Network Association (%s) not found, removing from state", d.Id()) - d.SetId("") - return nil - } - - network := result.ClientVpnTargetNetworks[0] - if network.Status != nil && aws.StringValue(network.Status.Code) == ec2.AssociationStatusCodeDisassociated { - log.Printf("[WARN] EC2 Client VPN Network Association (%s) not found, removing from state", d.Id()) - d.SetId("") - return nil - } - - d.Set("client_vpn_endpoint_id", network.ClientVpnEndpointId) d.Set("association_id", network.AssociationId) + d.Set("client_vpn_endpoint_id", network.ClientVpnEndpointId) + d.Set("security_groups", aws.StringValueSlice(network.SecurityGroups)) d.Set("status", network.Status.Code) d.Set("subnet_id", network.TargetNetworkId) d.Set("vpc_id", network.VpcId) - if err := d.Set("security_groups", aws.StringValueSlice(network.SecurityGroups)); err != nil { - return fmt.Errorf("error setting security_groups: %w", err) - } - return nil } -func resourceClientVPNNetworkAssociationDelete(d *schema.ResourceData, meta interface{}) error { +func resourceClientVPNNetworkAssociationUpdate(d *schema.ResourceData, meta interface{}) error { conn := meta.(*conns.AWSClient).EC2Conn - err := DeleteClientVPNNetworkAssociation(conn, d.Id(), d.Get("client_vpn_endpoint_id").(string)) - if err != nil { - return fmt.Errorf("error deleting Client VPN network association: %w", err) + if d.HasChange("security_groups") { + input := &ec2.ApplySecurityGroupsToClientVpnTargetNetworkInput{ + ClientVpnEndpointId: aws.String(d.Get("client_vpn_endpoint_id").(string)), + SecurityGroupIds: flex.ExpandStringSet(d.Get("security_groups").(*schema.Set)), + VpcId: aws.String(d.Get("vpc_id").(string)), + } + + if _, err := conn.ApplySecurityGroupsToClientVpnTargetNetwork(input); err != nil { + return fmt.Errorf("error applying Security Groups to EC2 Client VPN Network Association (%s): %w", d.Id(), err) + } } - return nil + return resourceClientVPNNetworkAssociationRead(d, meta) } -func DeleteClientVPNNetworkAssociation(conn *ec2.EC2, networkAssociationID, clientVpnEndpointID string) error { +func resourceClientVPNNetworkAssociationDelete(d *schema.ResourceData, meta interface{}) error { + conn := meta.(*conns.AWSClient).EC2Conn + + endpointID := d.Get("client_vpn_endpoint_id").(string) + + log.Printf("[DEBUG] Deleting EC2 Client VPN Network Association: %s", d.Id()) _, err := conn.DisassociateClientVpnTargetNetwork(&ec2.DisassociateClientVpnTargetNetworkInput{ - ClientVpnEndpointId: aws.String(clientVpnEndpointID), - AssociationId: aws.String(networkAssociationID), + ClientVpnEndpointId: aws.String(endpointID), + AssociationId: aws.String(d.Id()), }) - if tfawserr.ErrMessageContains(err, ErrCodeInvalidClientVpnAssociationIdNotFound, "") || tfawserr.ErrMessageContains(err, ErrCodeInvalidClientVpnEndpointIdNotFound, "") { + if tfawserr.ErrCodeEquals(err, ErrCodeInvalidClientVpnAssociationIdNotFound, ErrCodeInvalidClientVpnEndpointIdNotFound) { return nil } + if err != nil { - return err + return fmt.Errorf("error disassociating EC2 Client VPN Network Association (%s): %w", d.Id(), err) } - _, err = WaitClientVPNNetworkAssociationDisassociated(conn, networkAssociationID, clientVpnEndpointID) + if _, err := WaitClientVPNNetworkAssociationDeleted(conn, d.Id(), endpointID, d.Timeout(schema.TimeoutDelete)); err != nil { + return fmt.Errorf("error waiting for EC2 Client VPN Network Association (%s) delete: %w", d.Id(), err) + } - return err + return nil } func resourceClientVPNNetworkAssociationImport(d *schema.ResourceData, meta interface{}) ([]*schema.ResourceData, error) { - endpointID, associationID, err := ClientVPNNetworkAssociationParseID(d.Id()) - if err != nil { - return nil, err + parts := strings.Split(d.Id(), ",") + + if len(parts) != 2 || parts[0] == "" || parts[1] == "" { + return nil, fmt.Errorf("unexpected format for ID (%[1]s), expected EndpointID%[2]sAssociationID", d.Id(), ",") } - d.SetId(associationID) - d.Set("client_vpn_endpoint_id", endpointID) + d.SetId(parts[1]) + d.Set("client_vpn_endpoint_id", parts[0]) + return []*schema.ResourceData{d}, nil } diff --git a/internal/service/ec2/client_vpn_network_association_test.go b/internal/service/ec2/client_vpn_network_association_test.go index 961e97aaaef6..340b79047742 100644 --- a/internal/service/ec2/client_vpn_network_association_test.go +++ b/internal/service/ec2/client_vpn_network_association_test.go @@ -13,15 +13,16 @@ import ( "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 testAccClientVPNNetworkAssociation_basic(t *testing.T) { var assoc ec2.TargetNetwork var group ec2.SecurityGroup - rStr := sdkacctest.RandString(5) + rName := sdkacctest.RandomWithPrefix(acctest.ResourcePrefix) resourceName := "aws_ec2_client_vpn_network_association.test" endpointResourceName := "aws_ec2_client_vpn_endpoint.test" - subnetResourceName := "aws_subnet.test" + subnetResourceName := "aws_subnet.test1" vpcResourceName := "aws_vpc.test" defaultSecurityGroupResourceName := "aws_default_security_group.test" @@ -32,7 +33,7 @@ func testAccClientVPNNetworkAssociation_basic(t *testing.T) { CheckDestroy: testAccCheckClientVPNNetworkAssociationDestroy, Steps: []resource.TestStep{ { - Config: testAccEc2ClientVpnNetworkAssociationConfigBasic(rStr), + Config: testAccEc2ClientVpnNetworkAssociationConfigBasic(rName), Check: resource.ComposeTestCheckFunc( testAccCheckClientVPNNetworkAssociationExists(resourceName, &assoc), resource.TestMatchResourceAttr(resourceName, "association_id", regexp.MustCompile("^cvpn-assoc-[a-z0-9]+$")), @@ -58,10 +59,10 @@ func testAccClientVPNNetworkAssociation_basic(t *testing.T) { func testAccClientVPNNetworkAssociation_multipleSubnets(t *testing.T) { var assoc ec2.TargetNetwork var group ec2.SecurityGroup - rStr := sdkacctest.RandString(5) + rName := sdkacctest.RandomWithPrefix(acctest.ResourcePrefix) resourceNames := []string{"aws_ec2_client_vpn_network_association.test", "aws_ec2_client_vpn_network_association.test2"} endpointResourceName := "aws_ec2_client_vpn_endpoint.test" - subnetResourceNames := []string{"aws_subnet.test", "aws_subnet.test2"} + subnetResourceNames := []string{"aws_subnet.test1", "aws_subnet.test2"} vpcResourceName := "aws_vpc.test" defaultSecurityGroupResourceName := "aws_default_security_group.test" @@ -72,7 +73,7 @@ func testAccClientVPNNetworkAssociation_multipleSubnets(t *testing.T) { CheckDestroy: testAccCheckClientVPNNetworkAssociationDestroy, Steps: []resource.TestStep{ { - Config: testAccEc2ClientVpnNetworkAssociationConfigMultipleSubnets(rStr), + Config: testAccEc2ClientVpnNetworkAssociationConfigMultipleSubnets(rName), Check: resource.ComposeTestCheckFunc( testAccCheckClientVPNNetworkAssociationExists(resourceNames[0], &assoc), resource.TestMatchResourceAttr(resourceNames[0], "association_id", regexp.MustCompile("^cvpn-assoc-[a-z0-9]+$")), @@ -105,7 +106,7 @@ func testAccClientVPNNetworkAssociation_multipleSubnets(t *testing.T) { func testAccClientVPNNetworkAssociation_disappears(t *testing.T) { var assoc ec2.TargetNetwork - rStr := sdkacctest.RandString(5) + rName := sdkacctest.RandomWithPrefix(acctest.ResourcePrefix) resourceName := "aws_ec2_client_vpn_network_association.test" resource.ParallelTest(t, resource.TestCase{ @@ -115,7 +116,7 @@ func testAccClientVPNNetworkAssociation_disappears(t *testing.T) { CheckDestroy: testAccCheckClientVPNNetworkAssociationDestroy, Steps: []resource.TestStep{ { - Config: testAccEc2ClientVpnNetworkAssociationConfigBasic(rStr), + Config: testAccEc2ClientVpnNetworkAssociationConfigBasic(rName), Check: resource.ComposeTestCheckFunc( testAccCheckClientVPNNetworkAssociationExists(resourceName, &assoc), acctest.CheckResourceDisappears(acctest.Provider, tfec2.ResourceClientVPNNetworkAssociation(), resourceName), @@ -129,7 +130,7 @@ func testAccClientVPNNetworkAssociation_disappears(t *testing.T) { func testAccClientVPNNetworkAssociation_securityGroups(t *testing.T) { var assoc1, assoc2 ec2.TargetNetwork var group11, group12, group21 ec2.SecurityGroup - rStr := sdkacctest.RandString(5) + rName := sdkacctest.RandomWithPrefix(acctest.ResourcePrefix) resourceName := "aws_ec2_client_vpn_network_association.test" securityGroup1ResourceName := "aws_security_group.test1" securityGroup2ResourceName := "aws_security_group.test2" @@ -141,7 +142,7 @@ func testAccClientVPNNetworkAssociation_securityGroups(t *testing.T) { CheckDestroy: testAccCheckClientVPNNetworkAssociationDestroy, Steps: []resource.TestStep{ { - Config: testAccEc2ClientVpnNetworkAssociationTwoSecurityGroups(rStr), + Config: testAccEc2ClientVpnNetworkAssociationTwoSecurityGroups(rName), Check: resource.ComposeTestCheckFunc( testAccCheckClientVPNNetworkAssociationExists(resourceName, &assoc1), testAccCheckDefaultSecurityGroupExists(securityGroup1ResourceName, &group11), @@ -158,7 +159,7 @@ func testAccClientVPNNetworkAssociation_securityGroups(t *testing.T) { ImportStateIdFunc: testAccClientVPNNetworkAssociationImportStateIdFunc(resourceName), }, { - Config: testAccEc2ClientVpnNetworkAssociationOneSecurityGroup(rStr), + Config: testAccEc2ClientVpnNetworkAssociationOneSecurityGroup(rName), Check: resource.ComposeTestCheckFunc( testAccCheckClientVPNNetworkAssociationExists(resourceName, &assoc2), testAccCheckDefaultSecurityGroupExists(securityGroup1ResourceName, &group21), @@ -178,22 +179,23 @@ func testAccCheckClientVPNNetworkAssociationDestroy(s *terraform.State) error { continue } - resp, _ := conn.DescribeClientVpnTargetNetworks(&ec2.DescribeClientVpnTargetNetworksInput{ - ClientVpnEndpointId: aws.String(rs.Primary.Attributes["client_vpn_endpoint_id"]), - AssociationIds: []*string{aws.String(rs.Primary.ID)}, - }) + _, err := tfec2.FindClientVPNNetworkAssociationByIDs(conn, rs.Primary.ID, rs.Primary.Attributes["client_vpn_endpoint_id"]) - for _, v := range resp.ClientVpnTargetNetworks { - if *v.AssociationId == rs.Primary.ID && !(*v.Status.Code == ec2.AssociationStatusCodeDisassociated) { - return fmt.Errorf("[DESTROY ERROR] Client VPN network association (%s) not deleted", rs.Primary.ID) - } + if tfresource.NotFound(err) { + continue + } + + if err != nil { + return err } + + return fmt.Errorf("EC2 Client VPN Network Association %s still exists", rs.Primary.ID) } return nil } -func testAccCheckClientVPNNetworkAssociationExists(name string, assoc *ec2.TargetNetwork) resource.TestCheckFunc { +func testAccCheckClientVPNNetworkAssociationExists(name string, v *ec2.TargetNetwork) resource.TestCheckFunc { return func(s *terraform.State) error { rs, ok := s.RootModule().Resources[name] if !ok { @@ -201,28 +203,20 @@ func testAccCheckClientVPNNetworkAssociationExists(name string, assoc *ec2.Targe } if rs.Primary.ID == "" { - return fmt.Errorf("No ID is set") + return fmt.Errorf("No EC2 Client VPN Network Association ID is set") } conn := acctest.Provider.Meta().(*conns.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)}, - }) + output, err := tfec2.FindClientVPNNetworkAssociationByIDs(conn, rs.Primary.ID, rs.Primary.Attributes["client_vpn_endpoint_id"]) if err != nil { - return fmt.Errorf("Error reading Client VPN network association (%s): %w", rs.Primary.ID, err) + return err } - for _, a := range resp.ClientVpnTargetNetworks { - if *a.AssociationId == rs.Primary.ID && !(*a.Status.Code == ec2.AssociationStatusCodeDisassociated) { - *assoc = *a - return nil - } - } + *v = *output - return fmt.Errorf("Client VPN network association (%s) not found", rs.Primary.ID) + return nil } } @@ -239,195 +233,130 @@ func testAccClientVPNNetworkAssociationImportStateIdFunc(resourceName string) re return "", fmt.Errorf("Not found: %s", resourceName) } - return tfec2.ClientVPNNetworkAssociationCreateID(rs.Primary.Attributes["client_vpn_endpoint_id"], rs.Primary.ID), nil + return fmt.Sprintf("%s,%s", rs.Primary.Attributes["client_vpn_endpoint_id"], rs.Primary.ID), nil } } -func testAccEc2ClientVpnNetworkAssociationConfigBasic(rName string) string { +func testAccEc2ClientVpnNetworkAssociationBaseConfig(rName string) string { return acctest.ConfigCompose( - testAccEc2ClientVpnNetworkAssociationVpcBase(rName), - testAccEc2ClientVpnNetworkAssociationAcmCertificateBase(), + testAccEc2ClientVpnEndpointConfig(rName), + acctest.ConfigAvailableAZsNoOptInDefaultExclude(), 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.test.id +resource "aws_vpc" "test" { + cidr_block = "10.1.0.0/16" + + tags = { + Name = %[1]q + } +} + +resource "aws_default_security_group" "test" { + vpc_id = aws_vpc.test.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" +resource "aws_subnet" "test1" { + availability_zone = data.aws_availability_zones.available.names[0] + cidr_block = cidrsubnet(aws_vpc.test.cidr_block, 8, 0) + vpc_id = aws_vpc.test.id + map_public_ip_on_launch = true - authentication_options { - type = "certificate-authentication" - root_certificate_chain_arn = aws_acm_certificate.test.arn + tags = { + Name = %[1]q } +} - connection_log_options { - enabled = false +resource "aws_subnet" "test2" { + availability_zone = data.aws_availability_zones.available.names[1] + cidr_block = cidrsubnet(aws_vpc.test.cidr_block, 8, 1) + vpc_id = aws_vpc.test.id + map_public_ip_on_launch = true + + tags = { + Name = %[1]q } } `, rName)) } +func testAccEc2ClientVpnNetworkAssociationConfigBasic(rName string) string { + return acctest.ConfigCompose(testAccEc2ClientVpnNetworkAssociationBaseConfig(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 +} +`) +} + func testAccEc2ClientVpnNetworkAssociationConfigMultipleSubnets(rName string) string { - return acctest.ConfigCompose( - testAccEc2ClientVpnNetworkAssociationVpcBase(rName), - testAccEc2ClientVpnNetworkAssociationAcmCertificateBase(), - fmt.Sprintf(` + return acctest.ConfigCompose(testAccEc2ClientVpnNetworkAssociationBaseConfig(rName), ` 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 + subnet_id = aws_subnet.test1.id } resource "aws_ec2_client_vpn_network_association" "test2" { client_vpn_endpoint_id = aws_ec2_client_vpn_endpoint.test.id subnet_id = aws_subnet.test2.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 testAccEc2ClientVpnNetworkAssociationTwoSecurityGroups(rName string) string { return acctest.ConfigCompose( - testAccEc2ClientVpnNetworkAssociationVpcBase(rName), - testAccEc2ClientVpnNetworkAssociationAcmCertificateBase(), + testAccEc2ClientVpnNetworkAssociationBaseConfig(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.test.id + subnet_id = aws_subnet.test1.id security_groups = [aws_security_group.test1.id, aws_security_group.test2.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 - } +resource "aws_security_group" "test1" { + name = "%[1]s-1" + vpc_id = aws_vpc.test.id - connection_log_options { - enabled = false + tags = { + Name = %[1]q } } -resource "aws_security_group" "test1" { - name = "terraform_acceptance_test_example_1" - description = "Used in the terraform acceptance tests" - vpc_id = aws_vpc.test.id -} - resource "aws_security_group" "test2" { - name = "terraform_acceptance_test_example_2" - description = "Used in the terraform acceptance tests" - vpc_id = aws_vpc.test.id + name = "%[1]s-2" + vpc_id = aws_vpc.test.id + + tags = { + Name = %[1]q + } } `, rName)) } func testAccEc2ClientVpnNetworkAssociationOneSecurityGroup(rName string) string { return acctest.ConfigCompose( - testAccEc2ClientVpnNetworkAssociationVpcBase(rName), - testAccEc2ClientVpnNetworkAssociationAcmCertificateBase(), + testAccEc2ClientVpnNetworkAssociationBaseConfig(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.test.id + subnet_id = aws_subnet.test1.id security_groups = [aws_security_group.test1.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 - } -} - resource "aws_security_group" "test1" { - name = "terraform_acceptance_test_example_1" - description = "Used in the terraform acceptance tests" - vpc_id = aws_vpc.test.id -} - -resource "aws_security_group" "test2" { - name = "terraform_acceptance_test_example_2" - description = "Used in the terraform acceptance tests" - vpc_id = aws_vpc.test.id -} -`, rName)) -} - -func testAccEc2ClientVpnNetworkAssociationVpcBase(rName string) 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" - } -} - -resource "aws_default_security_group" "test" { + name = "%[1]s-1" vpc_id = aws_vpc.test.id -} - -resource "aws_subnet" "test" { - availability_zone = data.aws_availability_zones.available.names[0] - cidr_block = cidrsubnet(aws_vpc.test.cidr_block, 8, 0) - vpc_id = aws_vpc.test.id - map_public_ip_on_launch = true tags = { - Name = "tf-acc-subnet-%[1]s" + Name = %[1]q } } -resource "aws_subnet" "test2" { - availability_zone = data.aws_availability_zones.available.names[1] - cidr_block = cidrsubnet(aws_vpc.test.cidr_block, 8, 1) - vpc_id = aws_vpc.test.id - map_public_ip_on_launch = true +resource "aws_security_group" "test2" { + name = "%[1]s-2" + vpc_id = aws_vpc.test.id tags = { - Name = "tf-acc-subnet-%[1]s-2" + Name = %[1]q } } `, rName)) } - -func testAccEc2ClientVpnNetworkAssociationAcmCertificateBase() string { - key := acctest.TLSRSAPrivateKeyPEM(2048) - certificate := acctest.TLSRSAX509SelfSignedCertificatePEM(key, "example.com") - - return fmt.Sprintf(` -resource "aws_acm_certificate" "test" { - certificate_body = "%[1]s" - private_key = "%[2]s" -} -`, acctest.TLSPEMEscapeNewlines(certificate), acctest.TLSPEMEscapeNewlines(key)) -} diff --git a/internal/service/ec2/find.go b/internal/service/ec2/find.go index ee5657a18652..a8709ad0b868 100644 --- a/internal/service/ec2/find.go +++ b/internal/service/ec2/find.go @@ -174,7 +174,7 @@ func FindClientVPNAuthorizationRules(conn *ec2.EC2, input *ec2.DescribeClientVpn return output, nil } -func FindClientVPNAuthorizationRuleByEndpointIDTargetNetworkCIDRAndGroupID(conn *ec2.EC2, endpointID, targetNetworkCIDR, accessGroupID string) (*ec2.AuthorizationRule, error) { +func FindClientVPNAuthorizationRuleByThreePartKey(conn *ec2.EC2, endpointID, targetNetworkCIDR, accessGroupID string) (*ec2.AuthorizationRule, error) { filters := map[string]string{ "destination-cidr": targetNetworkCIDR, } @@ -187,7 +187,86 @@ func FindClientVPNAuthorizationRuleByEndpointIDTargetNetworkCIDRAndGroupID(conn } return FindClientVPNAuthorizationRule(conn, input) +} + +func FindClientVPNNetworkAssociation(conn *ec2.EC2, input *ec2.DescribeClientVpnTargetNetworksInput) (*ec2.TargetNetwork, error) { + output, err := FindClientVPNNetworkAssociations(conn, input) + + if err != nil { + return nil, err + } + + if len(output) == 0 || output[0] == nil || output[0].Status == nil { + return nil, tfresource.NewEmptyResultError(input) + } + + if count := len(output); count > 1 { + return nil, tfresource.NewTooManyResultsError(count, input) + } + + return output[0], nil +} + +func FindClientVPNNetworkAssociations(conn *ec2.EC2, input *ec2.DescribeClientVpnTargetNetworksInput) ([]*ec2.TargetNetwork, error) { + var output []*ec2.TargetNetwork + + err := conn.DescribeClientVpnTargetNetworksPages(input, func(page *ec2.DescribeClientVpnTargetNetworksOutput, lastPage bool) bool { + if page == nil { + return !lastPage + } + + for _, v := range page.ClientVpnTargetNetworks { + if v == nil { + continue + } + + output = append(output, v) + } + + return !lastPage + }) + + if tfawserr.ErrCodeEquals(err, ErrCodeInvalidClientVpnEndpointIdNotFound, ErrCodeInvalidClientVpnAssociationIdNotFound) { + return nil, &resource.NotFoundError{ + LastError: err, + LastRequest: input, + } + } + + if err != nil { + return nil, err + } + return output, nil +} + +func FindClientVPNNetworkAssociationByIDs(conn *ec2.EC2, associationID, endpointID string) (*ec2.TargetNetwork, error) { + input := &ec2.DescribeClientVpnTargetNetworksInput{ + AssociationIds: aws.StringSlice([]string{associationID}), + ClientVpnEndpointId: aws.String(endpointID), + } + + output, err := FindClientVPNNetworkAssociation(conn, input) + + if err != nil { + return nil, err + } + + if state := aws.StringValue(output.Status.Code); state == ec2.AssociationStatusCodeDisassociated { + return nil, &resource.NotFoundError{ + Message: state, + LastRequest: input, + } + } + + // Eventual consistency check. + if aws.StringValue(output.ClientVpnEndpointId) != endpointID || aws.StringValue(output.AssociationId) != associationID { + return nil, &resource.NotFoundError{ + LastRequest: input, + } + } + + return output, nil } func FindClientVPNRoute(conn *ec2.EC2, endpointID, targetSubnetID, destinationCidr string) (*ec2.DescribeClientVpnRoutesOutput, error) { diff --git a/internal/service/ec2/id.go b/internal/service/ec2/id.go index 6fcadf045eb0..93b68f3e9ce5 100644 --- a/internal/service/ec2/id.go +++ b/internal/service/ec2/id.go @@ -7,26 +7,6 @@ import ( "github.com/hashicorp/terraform-provider-aws/internal/create" ) -const clientVPNNetworkAssociationIDSeparator = "," - -func ClientVPNNetworkAssociationCreateID(endpointID, associationID string) string { - parts := []string{endpointID, associationID} - id := strings.Join(parts, clientVPNNetworkAssociationIDSeparator) - - return id -} - -func ClientVPNNetworkAssociationParseID(id string) (string, string, error) { - parts := strings.Split(id, clientVPNNetworkAssociationIDSeparator) - - if len(parts) == 2 && parts[0] != "" && parts[1] != "" { - return parts[0], parts[1], nil - } - - return "", "", - fmt.Errorf("unexpected format for ID (%[1]s), expected EndpointID%[2]sAssociationID", id, clientVPNNetworkAssociationIDSeparator) -} - const clientVpnRouteIDSeparator = "," func ClientVPNRouteCreateID(endpointID, targetSubnetID, destinationCidr string) string { diff --git a/internal/service/ec2/status.go b/internal/service/ec2/status.go index 3cfac26b8c68..ebde35ed05ad 100644 --- a/internal/service/ec2/status.go +++ b/internal/service/ec2/status.go @@ -111,7 +111,7 @@ func StatusClientVPNEndpointClientConnectResponseOptionsState(conn *ec2.EC2, id func StatusClientVPNAuthorizationRule(conn *ec2.EC2, endpointID, targetNetworkCIDR, accessGroupID string) resource.StateRefreshFunc { return func() (interface{}, string, error) { - output, err := FindClientVPNAuthorizationRuleByEndpointIDTargetNetworkCIDRAndGroupID(conn, endpointID, targetNetworkCIDR, accessGroupID) + output, err := FindClientVPNAuthorizationRuleByThreePartKey(conn, endpointID, targetNetworkCIDR, accessGroupID) if tfresource.NotFound(err) { return nil, "", nil @@ -125,36 +125,19 @@ func StatusClientVPNAuthorizationRule(conn *ec2.EC2, endpointID, targetNetworkCI } } -const ( - ClientVPNNetworkAssociationStatusNotFound = "NotFound" - - ClientVPNNetworkAssociationStatusUnknown = "Unknown" -) - -func StatusClientVPNNetworkAssociation(conn *ec2.EC2, cvnaID string, cvepID string) resource.StateRefreshFunc { +func StatusClientVPNNetworkAssociation(conn *ec2.EC2, associationID, endpointID string) resource.StateRefreshFunc { return func() (interface{}, string, error) { - result, err := conn.DescribeClientVpnTargetNetworks(&ec2.DescribeClientVpnTargetNetworksInput{ - ClientVpnEndpointId: aws.String(cvepID), - AssociationIds: []*string{aws.String(cvnaID)}, - }) + output, err := FindClientVPNNetworkAssociationByIDs(conn, associationID, endpointID) - if tfawserr.ErrCodeEquals(err, ErrCodeInvalidClientVpnAssociationIdNotFound) || tfawserr.ErrCodeEquals(err, ErrCodeInvalidClientVpnEndpointIdNotFound) { - return nil, ClientVPNNetworkAssociationStatusNotFound, nil - } - if err != nil { - return nil, ClientVPNNetworkAssociationStatusUnknown, err - } - - if result == nil || len(result.ClientVpnTargetNetworks) == 0 || result.ClientVpnTargetNetworks[0] == nil { - return nil, ClientVPNNetworkAssociationStatusNotFound, nil + if tfresource.NotFound(err) { + return nil, "", nil } - network := result.ClientVpnTargetNetworks[0] - if network.Status == nil || network.Status.Code == nil { - return network, ClientVPNNetworkAssociationStatusUnknown, nil + if err != nil { + return nil, "", err } - return network, aws.StringValue(network.Status.Code), nil + return output, aws.StringValue(output.Status.Code), nil } } diff --git a/internal/service/ec2/sweep.go b/internal/service/ec2/sweep.go index ead759e8877e..47bb458f1a0d 100644 --- a/internal/service/ec2/sweep.go +++ b/internal/service/ec2/sweep.go @@ -466,66 +466,66 @@ func sweepClientVPNEndpoints(region string) error { func sweepClientVPNNetworkAssociations(region string) error { client, err := sweep.SharedRegionalSweepClient(region) - if err != nil { return fmt.Errorf("error getting client: %w", err) } - conn := client.(*conns.AWSClient).EC2Conn - + input := &ec2.DescribeClientVpnEndpointsInput{} var sweeperErrs *multierror.Error + sweepResources := make([]*sweep.SweepResource, 0) - input := &ec2.DescribeClientVpnEndpointsInput{} err = conn.DescribeClientVpnEndpointsPages(input, func(page *ec2.DescribeClientVpnEndpointsOutput, lastPage bool) bool { if page == nil { return !lastPage } - for _, clientVpnEndpoint := range page.ClientVpnEndpoints { - + for _, v := range page.ClientVpnEndpoints { input := &ec2.DescribeClientVpnTargetNetworksInput{ - ClientVpnEndpointId: clientVpnEndpoint.ClientVpnEndpointId, + ClientVpnEndpointId: v.ClientVpnEndpointId, } + err := conn.DescribeClientVpnTargetNetworksPages(input, func(page *ec2.DescribeClientVpnTargetNetworksOutput, lastPage bool) bool { if page == nil { return !lastPage } - for _, networkAssociation := range page.ClientVpnTargetNetworks { - networkAssociationID := aws.StringValue(networkAssociation.AssociationId) - clientVpnEndpointID := aws.StringValue(networkAssociation.ClientVpnEndpointId) - - log.Printf("[INFO] Deleting Client VPN network association (%s,%s)", clientVpnEndpointID, networkAssociationID) - err := DeleteClientVPNNetworkAssociation(conn, networkAssociationID, clientVpnEndpointID) + for _, v := range page.ClientVpnTargetNetworks { + r := ResourceClientVPNNetworkAssociation() + d := r.Data(nil) + d.SetId(aws.StringValue(v.AssociationId)) + d.Set("client_vpn_endpoint_id", v.ClientVpnEndpointId) - if err != nil { - sweeperErr := fmt.Errorf("error deleting Client VPN network association (%s,%s): %w", clientVpnEndpointID, networkAssociationID, err) - log.Printf("[ERROR] %s", sweeperErr) - sweeperErrs = multierror.Append(sweeperErrs, sweeperErr) - } + sweepResources = append(sweepResources, sweep.NewSweepResource(r, d, client)) } return !lastPage }) if sweep.SkipSweepError(err) { - log.Printf("[WARN] Skipping Client VPN network association sweeper for %q: %s", region, err) - return false + continue } + if err != nil { - sweeperErrs = multierror.Append(sweeperErrs, fmt.Errorf("error listing Client VPN network associations: %w", err)) - return false + sweeperErrs = multierror.Append(sweeperErrs, fmt.Errorf("error listing EC2 Client VPN Network Associations (%s): %w", region, err)) } } return !lastPage }) + if sweep.SkipSweepError(err) { - log.Printf("[WARN] Skipping Client VPN network association sweep for %s: %s", region, err) + log.Printf("[WARN] Skipping EC2 Client VPN Network Association sweep for %s: %s", region, err) return sweeperErrs.ErrorOrNil() // In case we have completed some pages, but had errors } + + if err != nil { + sweeperErrs = multierror.Append(sweeperErrs, fmt.Errorf("error listing EC2 Client VPN Endpoints (%s): %w", region, err)) + } + + err = sweep.SweepOrchestrator(sweepResources) + if err != nil { - sweeperErrs = multierror.Append(sweeperErrs, fmt.Errorf("error retrieving Client VPN network associations: %w", err)) + sweeperErrs = multierror.Append(sweeperErrs, fmt.Errorf("error sweeping EC2 Client VPN Network Associations (%s): %w", region, err)) } return sweeperErrs.ErrorOrNil() diff --git a/internal/service/ec2/wait.go b/internal/service/ec2/wait.go index ad58faba3fd2..243dc13b1020 100644 --- a/internal/service/ec2/wait.go +++ b/internal/service/ec2/wait.go @@ -200,49 +200,49 @@ func WaitClientVPNAuthorizationRuleDeleted(conn *ec2.EC2, endpointID, targetNetw } const ( - ClientVPNNetworkAssociationAssociatedTimeout = 30 * time.Minute - - ClientVPNNetworkAssociationAssociatedDelay = 4 * time.Minute - - ClientVPNNetworkAssociationDisassociatedTimeout = 30 * time.Minute - - ClientVPNNetworkAssociationDisassociatedDelay = 4 * time.Minute - + ClientVPNNetworkAssociationCreatedTimeout = 30 * time.Minute + ClientVPNNetworkAssociationCreatedDelay = 4 * time.Minute + ClientVPNNetworkAssociationDeletedTimeout = 30 * time.Minute + ClientVPNNetworkAssociationDeletedDelay = 4 * time.Minute ClientVPNNetworkAssociationStatusPollInterval = 10 * time.Second ) -func WaitClientVPNNetworkAssociationAssociated(conn *ec2.EC2, networkAssociationID, clientVpnEndpointID string) (*ec2.TargetNetwork, error) { +func WaitClientVPNNetworkAssociationCreated(conn *ec2.EC2, associationID, endpointID string, timeout time.Duration) (*ec2.TargetNetwork, error) { stateConf := &resource.StateChangeConf{ Pending: []string{ec2.AssociationStatusCodeAssociating}, Target: []string{ec2.AssociationStatusCodeAssociated}, - Refresh: StatusClientVPNNetworkAssociation(conn, networkAssociationID, clientVpnEndpointID), - Timeout: ClientVPNNetworkAssociationAssociatedTimeout, - Delay: ClientVPNNetworkAssociationAssociatedDelay, + Refresh: StatusClientVPNNetworkAssociation(conn, associationID, endpointID), + Timeout: timeout, + Delay: ClientVPNNetworkAssociationCreatedDelay, PollInterval: ClientVPNNetworkAssociationStatusPollInterval, } outputRaw, err := stateConf.WaitForState() if output, ok := outputRaw.(*ec2.TargetNetwork); ok { + tfresource.SetLastError(err, errors.New(aws.StringValue(output.Status.Message))) + return output, err } return nil, err } -func WaitClientVPNNetworkAssociationDisassociated(conn *ec2.EC2, networkAssociationID, clientVpnEndpointID string) (*ec2.TargetNetwork, error) { +func WaitClientVPNNetworkAssociationDeleted(conn *ec2.EC2, associationID, endpointID string, timeout time.Duration) (*ec2.TargetNetwork, error) { stateConf := &resource.StateChangeConf{ Pending: []string{ec2.AssociationStatusCodeDisassociating}, Target: []string{}, - Refresh: StatusClientVPNNetworkAssociation(conn, networkAssociationID, clientVpnEndpointID), - Timeout: ClientVPNNetworkAssociationDisassociatedTimeout, - Delay: ClientVPNNetworkAssociationDisassociatedDelay, + Refresh: StatusClientVPNNetworkAssociation(conn, associationID, endpointID), + Timeout: timeout, + Delay: ClientVPNNetworkAssociationDeletedDelay, PollInterval: ClientVPNNetworkAssociationStatusPollInterval, } outputRaw, err := stateConf.WaitForState() if output, ok := outputRaw.(*ec2.TargetNetwork); 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 new file mode 100644 index 000000000000..89a1ba07ee8c --- /dev/null +++ b/website/docs/d/ec2_client_vpn_endpoint.html.markdown @@ -0,0 +1,69 @@ +--- +subcategory: "EC2" +layout: "aws" +page_title: "AWS: aws_ec2_client_vpn_endpoint" +description: |- + Get information on an EC2 Client VPN endpoint +--- + +# Data Source: aws_ec2_client_vpn_endpoint + +Get information on an EC2 Client VPN endpoint. + +## Example Usage + +### By Filter + +```hcl +data "aws_ec2_client_vpn_endpoint" "example" { + filter { + name = "tag:Name" + values = ["ExampleVpn"] + } +} +``` + +### By Identifier + +```hcl +data "aws_ec2_client_vpn_endpoint" "example" { + client_vpn_endpoint_id = "cvpn-endpoint-083cf50d6eb314f21" +} +``` + +## Argument Reference + +The following arguments are supported: + +* `client_vpn_endpoint_id` - (Optional) The ID of the Client VPN endpoint. +* `filter` - (Optional) One or more configuration blocks containing name-values filters. Detailed below. +* `tags` - (Optional) Map of tags, each pair of which must exactly match a pair on the desired endpoint. + +### filter + +This block allows for complex filters. You can use one or more `filter` blocks. + +The following arguments are required: + +* `name` - (Required) The name of the field to filter by, as defined by [the underlying AWS API](https://docs.aws.amazon.com/AWSEC2/latest/APIReference/API_DescribeClientVpnEndpoints.html). +* `values` - (Required) Set of values that are accepted for the given field. An endpoint will be selected if any one of the given values matches. + +## Attribute Reference + +In addition to all arguments above, the following attributes are exported: + +* `arn` - The ARN of the Client VPN endpoint. +* `authentication_options` - Information about the authentication method used by the Client VPN endpoint. +* `client_cidr_block` - The IPv4 address range, in CIDR notation, from which client IP addresses are assigned. +* `client_connect_options` - The options for managing connection authorization for new client connections. +* `client_login_banner_options` - Options for enabling a customizable text banner that will be displayed on AWS provided clients when a VPN session is established. +* `connection_log_options` - Information about the client connection logging options for the Client VPN endpoint. +* `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. +* `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. +* `vpn_port` - The port number for the Client VPN endpoint. 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 6f8d23558c02..89ba30d77825 100644 --- a/website/docs/r/ec2_client_vpn_network_association.html.markdown +++ b/website/docs/r/ec2_client_vpn_network_association.html.markdown @@ -50,6 +50,13 @@ In addition to all arguments above, the following attributes are exported: * `status` - The current state of the target network association. * `vpc_id` - The ID of the VPC in which the target subnet is located. +## Timeouts + +`aws_ec2_client_vpn_network_association` provides the following [Timeouts](https://www.terraform.io/docs/configuration/blocks/resources/syntax.html#operation-timeouts) configuration options: + +- `create` - (Default `30 minutes`) Used for network association +- `delete` - (Default `30 minutes`) Used for network disassociation + ## Import AWS Client VPN network associations can be imported using the endpoint ID and the association ID. Values are separated by a `,`.