From a2cb192c892e5dea217a80540d0b58a46a352b4e Mon Sep 17 00:00:00 2001 From: Brian Flad Date: Sat, 3 Oct 2020 12:36:25 -0400 Subject: [PATCH] data-source/aws_vpc_endpoint_service: Accept service_type as argument Allow practitioners to prevent the following error in certain environments: ``` Error: multiple VPC Endpoint Services matched; use additional constraints to reduce matches to a single VPC Endpoint Service ``` Output from acceptance testing in AWS Commercial: ``` --- PASS: TestAccDataSourceAwsVpcEndpointService_ServiceType_Interface (13.90s) --- PASS: TestAccDataSourceAwsVpcEndpointService_gateway (13.91s) --- PASS: TestAccDataSourceAwsVpcEndpointService_ServiceType_Gateway (14.00s) --- PASS: TestAccDataSourceAwsVpcEndpointService_interface (14.69s) --- PASS: TestAccDataSourceAwsVpcEndpointService_custom (243.09s) --- PASS: TestAccDataSourceAwsVpcEndpointService_custom_filter_tags (243.14s) --- PASS: TestAccDataSourceAwsVpcEndpointService_custom_filter (253.55s) ``` Output from acceptance testing in AWS GovCloud (US): ``` --- PASS: TestAccDataSourceAwsVpcEndpointService_ServiceType_Gateway (18.12s) --- PASS: TestAccDataSourceAwsVpcEndpointService_ServiceType_Interface (18.20s) --- PASS: TestAccDataSourceAwsVpcEndpointService_interface (18.66s) --- PASS: TestAccDataSourceAwsVpcEndpointService_gateway (18.67s) --- PASS: TestAccDataSourceAwsVpcEndpointService_custom (236.92s) --- PASS: TestAccDataSourceAwsVpcEndpointService_custom_filter_tags (258.50s) --- PASS: TestAccDataSourceAwsVpcEndpointService_custom_filter (259.55s) ``` --- aws/data_source_aws_vpc_endpoint_service.go | 23 +++++++- ...ta_source_aws_vpc_endpoint_service_test.go | 55 +++++++++++++++++-- aws/provider_test.go | 33 +++++++++++ .../docs/d/vpc_endpoint_service.html.markdown | 7 ++- 4 files changed, 107 insertions(+), 11 deletions(-) diff --git a/aws/data_source_aws_vpc_endpoint_service.go b/aws/data_source_aws_vpc_endpoint_service.go index 3c2b550caea..e03fc0aafc1 100644 --- a/aws/data_source_aws_vpc_endpoint_service.go +++ b/aws/data_source_aws_vpc_endpoint_service.go @@ -67,6 +67,7 @@ func dataSourceAwsVpcEndpointService() *schema.Resource { }, "service_type": { Type: schema.TypeString, + Optional: true, Computed: true, }, "tags": tagsSchemaComputed(), @@ -133,11 +134,29 @@ func dataSourceAwsVpcEndpointServiceRead(d *schema.ResourceData, meta interface{ return fmt.Errorf("no matching VPC Endpoint Service found") } - if len(resp.ServiceDetails) > 1 { + var serviceDetails []*ec2.ServiceDetail + + // Client-side filtering. When the EC2 API supports this functionality + // server-side it should be moved. + for _, serviceDetail := range resp.ServiceDetails { + if serviceDetail == nil { + continue + } + + if v, ok := d.GetOk("service_type"); ok { + if len(serviceDetail.ServiceType) > 0 && serviceDetail.ServiceType[0] != nil && v.(string) != aws.StringValue(serviceDetail.ServiceType[0].ServiceType) { + continue + } + } + + serviceDetails = append(serviceDetails, serviceDetail) + } + + if len(serviceDetails) > 1 { return fmt.Errorf("multiple VPC Endpoint Services matched; use additional constraints to reduce matches to a single VPC Endpoint Service") } - sd := resp.ServiceDetails[0] + sd := serviceDetails[0] serviceId := aws.StringValue(sd.ServiceId) serviceName = aws.StringValue(sd.ServiceName) diff --git a/aws/data_source_aws_vpc_endpoint_service_test.go b/aws/data_source_aws_vpc_endpoint_service_test.go index 85b3162fcdc..755edaba519 100644 --- a/aws/data_source_aws_vpc_endpoint_service_test.go +++ b/aws/data_source_aws_vpc_endpoint_service_test.go @@ -11,7 +11,6 @@ import ( func TestAccDataSourceAwsVpcEndpointService_gateway(t *testing.T) { datasourceName := "data.aws_vpc_endpoint_service.test" - region := testAccGetRegion() resource.ParallelTest(t, resource.TestCase{ PreCheck: func() { testAccPreCheck(t) }, @@ -20,7 +19,7 @@ func TestAccDataSourceAwsVpcEndpointService_gateway(t *testing.T) { { Config: testAccDataSourceAwsVpcEndpointServiceGatewayConfig, Check: resource.ComposeTestCheckFunc( - resource.TestCheckResourceAttr(datasourceName, "service_name", fmt.Sprintf("com.amazonaws.%s.s3", region)), + testAccCheckResourceAttrRegionalReverseDnsService(datasourceName, "service_name", "dynamodb"), resource.TestCheckResourceAttr(datasourceName, "acceptance_required", "false"), resource.TestCheckResourceAttrPair(datasourceName, "availability_zones.#", "data.aws_availability_zones.available", "names.#"), resource.TestCheckResourceAttr(datasourceName, "base_endpoint_dns_names.#", "1"), @@ -39,7 +38,6 @@ func TestAccDataSourceAwsVpcEndpointService_gateway(t *testing.T) { func TestAccDataSourceAwsVpcEndpointService_interface(t *testing.T) { datasourceName := "data.aws_vpc_endpoint_service.test" - region := testAccGetRegion() resource.ParallelTest(t, resource.TestCase{ PreCheck: func() { testAccPreCheck(t) }, @@ -48,12 +46,12 @@ func TestAccDataSourceAwsVpcEndpointService_interface(t *testing.T) { { Config: testAccDataSourceAwsVpcEndpointServiceInterfaceConfig, Check: resource.ComposeTestCheckFunc( - resource.TestCheckResourceAttr(datasourceName, "service_name", fmt.Sprintf("com.amazonaws.%s.ec2", region)), + testAccCheckResourceAttrRegionalReverseDnsService(datasourceName, "service_name", "ec2"), resource.TestCheckResourceAttr(datasourceName, "acceptance_required", "false"), resource.TestCheckResourceAttr(datasourceName, "base_endpoint_dns_names.#", "1"), resource.TestCheckResourceAttr(datasourceName, "manages_vpc_endpoints", "false"), resource.TestCheckResourceAttr(datasourceName, "owner", "amazon"), - resource.TestCheckResourceAttr(datasourceName, "private_dns_name", fmt.Sprintf("ec2.%s.%s", region, testAccGetPartitionDNSSuffix())), + testAccCheckResourceAttrRegionalHostnameService(datasourceName, "private_dns_name", "ec2"), resource.TestCheckResourceAttr(datasourceName, "service_type", "Interface"), resource.TestCheckResourceAttr(datasourceName, "vpc_endpoint_policy_supported", "true"), resource.TestCheckResourceAttr(datasourceName, "tags.%", "0"), @@ -142,11 +140,47 @@ func TestAccDataSourceAwsVpcEndpointService_custom_filter_tags(t *testing.T) { }) } +func TestAccDataSourceAwsVpcEndpointService_ServiceType_Gateway(t *testing.T) { + datasourceName := "data.aws_vpc_endpoint_service.test" + + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + Providers: testAccProviders, + Steps: []resource.TestStep{ + { + Config: testAccDataSourceAwsVpcEndpointServiceConfig_ServiceType("s3", "Gateway"), + Check: resource.ComposeTestCheckFunc( + testAccCheckResourceAttrRegionalReverseDnsService(datasourceName, "service_name", "s3"), + resource.TestCheckResourceAttr(datasourceName, "service_type", "Gateway"), + ), + }, + }, + }) +} + +func TestAccDataSourceAwsVpcEndpointService_ServiceType_Interface(t *testing.T) { + datasourceName := "data.aws_vpc_endpoint_service.test" + + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + Providers: testAccProviders, + Steps: []resource.TestStep{ + { + Config: testAccDataSourceAwsVpcEndpointServiceConfig_ServiceType("ec2", "Interface"), + Check: resource.ComposeTestCheckFunc( + testAccCheckResourceAttrRegionalReverseDnsService(datasourceName, "service_name", "ec2"), + resource.TestCheckResourceAttr(datasourceName, "service_type", "Interface"), + ), + }, + }, + }) +} + const testAccDataSourceAwsVpcEndpointServiceGatewayConfig = ` data "aws_availability_zones" "available" {} data "aws_vpc_endpoint_service" "test" { - service = "s3" + service = "dynamodb" } ` @@ -156,6 +190,15 @@ data "aws_vpc_endpoint_service" "test" { } ` +func testAccDataSourceAwsVpcEndpointServiceConfig_ServiceType(service string, serviceType string) string { + return fmt.Sprintf(` +data "aws_vpc_endpoint_service" "test" { + service = %[1]q + service_type = %[2]q +} +`, service, serviceType) +} + func testAccDataSourceAwsVpcEndpointServiceCustomConfigBase(rName string) string { return fmt.Sprintf(` resource "aws_vpc" "test" { diff --git a/aws/provider_test.go b/aws/provider_test.go index 3515dffa528..432c232e29d 100644 --- a/aws/provider_test.go +++ b/aws/provider_test.go @@ -7,6 +7,7 @@ import ( "os" "reflect" "regexp" + "sort" "strings" "testing" @@ -163,6 +164,28 @@ func testAccCheckResourceAttrRegionalHostname(resourceName, attributeName, servi } } +// testAccCheckResourceAttrRegionalHostnameService ensures the Terraform state exactly matches a service DNS hostname with region and partition DNS suffix +// +// For example: ec2.us-west-2.amazonaws.com +func testAccCheckResourceAttrRegionalHostnameService(resourceName, attributeName, serviceName string) resource.TestCheckFunc { + return func(s *terraform.State) error { + hostname := fmt.Sprintf("%s.%s.%s", serviceName, testAccGetRegion(), testAccGetPartitionDNSSuffix()) + + return resource.TestCheckResourceAttr(resourceName, attributeName, hostname)(s) + } +} + +// testAccCheckResourceAttrRegionalReverseDnsService ensures the Terraform state exactly matches a service reverse DNS hostname with region and partition DNS suffix +// +// For example: com.amazonaws.us-west-2.s3 +func testAccCheckResourceAttrRegionalReverseDnsService(resourceName, attributeName, serviceName string) resource.TestCheckFunc { + return func(s *terraform.State) error { + reverseDns := fmt.Sprintf("%s.%s.%s", testAccGetPartitionReverseDNSPrefix(), testAccGetRegion(), serviceName) + + return resource.TestCheckResourceAttr(resourceName, attributeName, reverseDns)(s) + } +} + // testAccCheckResourceAttrHostnameWithPort ensures the Terraform state regexp matches a formatted DNS hostname with prefix, partition DNS suffix, and given port func testAccCheckResourceAttrHostnameWithPort(resourceName, attributeName, serviceName, hostnamePrefix string, port int) resource.TestCheckFunc { return func(s *terraform.State) error { @@ -450,6 +473,16 @@ func testAccGetPartitionDNSSuffix() string { return "amazonaws.com" } +func testAccGetPartitionReverseDNSPrefix() string { + if partition, ok := endpoints.PartitionForRegion(endpoints.DefaultPartitions(), testAccGetRegion()); ok { + dnsParts := strings.Split(partition.DNSSuffix(), ".") + sort.Sort(sort.Reverse(sort.StringSlice(dnsParts))) + return strings.Join(dnsParts, ".") + } + + return "com.amazonaws" +} + func testAccGetAlternateRegionPartition() string { if partition, ok := endpoints.PartitionForRegion(endpoints.DefaultPartitions(), testAccGetAlternateRegion()); ok { return partition.ID() diff --git a/website/docs/d/vpc_endpoint_service.html.markdown b/website/docs/d/vpc_endpoint_service.html.markdown index aec2c196978..201da1070cd 100644 --- a/website/docs/d/vpc_endpoint_service.html.markdown +++ b/website/docs/d/vpc_endpoint_service.html.markdown @@ -18,7 +18,8 @@ can be specified when creating a VPC endpoint within the region configured in th ```hcl # Declare the data source data "aws_vpc_endpoint_service" "s3" { - service = "s3" + service = "s3" + service_type = "Gateway" } # Create a VPC @@ -57,9 +58,10 @@ data "aws_vpc_endpoint_service" "test" { The arguments of this data source act as filters for querying the available VPC endpoint services. The given filters must match exactly one VPC endpoint service whose data will be exported as attributes. +* `filter` - (Optional) Configuration block(s) for filtering. Detailed below. * `service` - (Optional) The common name of an AWS service (e.g. `s3`). * `service_name` - (Optional) The service name that is specified when creating a VPC endpoint. For AWS services the service name is usually in the form `com.amazonaws..` (the SageMaker Notebook service is an exception to this rule, the service name is in the form `aws.sagemaker..notebook`). -* `filter` - (Optional) Configuration block(s) for filtering. Detailed below. +* `service_type` - (Optional) The service type, `Gateway` or `Interface`. * `tags` - (Optional) A map of tags, each pair of which must exactly match a pair on the desired VPC Endpoint Service. ~> **NOTE:** Specifying `service` will not work for non-AWS services or AWS services that don't follow the standard `service_name` pattern of `com.amazonaws..`. @@ -83,6 +85,5 @@ In addition to all arguments above, the following attributes are exported: * `owner` - The AWS account ID of the service owner or `amazon`. * `private_dns_name` - The private DNS name for the service. * `service_id` - The ID of the endpoint service. -* `service_type` - The service type, `Gateway` or `Interface`. * `tags` - A map of tags assigned to the resource. * `vpc_endpoint_policy_supported` - Whether or not the service supports endpoint policies - `true` or `false`.