Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[Enhancement]: support service_region with aws_vpc_endpoint (privateLink consumer) #40583

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions .changelog/40583.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
```release-note:enhancement
resource/aws_vpc_endpoint: Add `service_region` argument
```

```release-note:enhancement
data-source/aws_vpc_endpoint: Add `service_region` attribute
```
14 changes: 7 additions & 7 deletions internal/conns/awsclient.go
Original file line number Diff line number Diff line change
Expand Up @@ -110,21 +110,21 @@ func (c *AWSClient) GlobalARN(ctx context.Context, service, resource string) str

// RegionalARN returns a regional ARN for the specified service namespace and resource.
func (c *AWSClient) RegionalARN(ctx context.Context, service, resource string) string {
return arn.ARN{
Partition: c.Partition(ctx),
Service: service,
Region: c.Region(ctx),
AccountID: c.AccountID(ctx),
Resource: resource,
}.String()
return c.RegionalARNWithAccount(ctx, service, c.AccountID(ctx), resource)
}

// RegionalARNNoAccount returns a regional ARN for the specified service namespace and resource without AWS account ID.
func (c *AWSClient) RegionalARNNoAccount(ctx context.Context, service, resource string) string {
return c.RegionalARNWithAccount(ctx, service, "", resource)
}

// RegionalARNWithAccount returns a regional ARN for the specified service namespace, resource and account ID.
func (c *AWSClient) RegionalARNWithAccount(ctx context.Context, service, accountID, resource string) string {
return arn.ARN{
Partition: c.Partition(ctx),
Service: service,
Region: c.Region(ctx),
AccountID: accountID,
Resource: resource,
}.String()
}
Expand Down
30 changes: 19 additions & 11 deletions internal/service/ec2/vpc_endpoint.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,6 @@ import (
"time"

"github.com/aws/aws-sdk-go-v2/aws"
"github.com/aws/aws-sdk-go-v2/aws/arn"
"github.com/aws/aws-sdk-go-v2/service/ec2"
awstypes "github.com/aws/aws-sdk-go-v2/service/ec2/types"
"github.com/hashicorp/aws-sdk-go-base/v2/tfawserr"
Expand Down Expand Up @@ -159,6 +158,12 @@ func resourceVPCEndpoint() *schema.Resource {
Required: true,
ForceNew: true,
},
"service_region": {
Type: schema.TypeString,
Optional: true,
Computed: true,
ForceNew: true,
},
names.AttrState: {
Type: schema.TypeString,
Computed: true,
Expand Down Expand Up @@ -272,6 +277,10 @@ func resourceVPCEndpointCreate(ctx context.Context, d *schema.ResourceData, meta
input.SubnetIds = flex.ExpandStringValueSet(v.(*schema.Set))
}

if v, ok := d.GetOk("service_region"); ok {
input.ServiceRegion = aws.String(v.(string))
}

output, err := conn.CreateVpcEndpoint(ctx, input)

// Some partitions (e.g. ISO) may not support tag-on-create.
Expand Down Expand Up @@ -330,15 +339,8 @@ func resourceVPCEndpointRead(ctx context.Context, d *schema.ResourceData, meta i
return sdkdiag.AppendErrorf(diags, "reading VPC Endpoint (%s): %s", d.Id(), err)
}

arn := arn.ARN{
Partition: meta.(*conns.AWSClient).Partition(ctx),
Service: names.EC2,
Region: meta.(*conns.AWSClient).Region(ctx),
AccountID: aws.ToString(vpce.OwnerId),
Resource: fmt.Sprintf("vpc-endpoint/%s", d.Id()),
}.String()
serviceName := aws.ToString(vpce.ServiceName)
d.Set(names.AttrARN, arn)
ownerID := aws.ToString(vpce.OwnerId)
d.Set(names.AttrARN, vpcEndpointARN(ctx, meta.(*conns.AWSClient), ownerID, d.Id()))
if err := d.Set("dns_entry", flattenDNSEntries(vpce.DnsEntries)); err != nil {
return sdkdiag.AppendErrorf(diags, "setting dns_entry: %s", err)
}
Expand All @@ -351,12 +353,14 @@ func resourceVPCEndpointRead(ctx context.Context, d *schema.ResourceData, meta i
}
d.Set(names.AttrIPAddressType, vpce.IpAddressType)
d.Set("network_interface_ids", vpce.NetworkInterfaceIds)
d.Set(names.AttrOwnerID, vpce.OwnerId)
d.Set(names.AttrOwnerID, ownerID)
d.Set("private_dns_enabled", vpce.PrivateDnsEnabled)
d.Set("requester_managed", vpce.RequesterManaged)
d.Set("route_table_ids", vpce.RouteTableIds)
d.Set(names.AttrSecurityGroupIDs, flattenSecurityGroupIdentifiers(vpce.Groups))
serviceName := aws.ToString(vpce.ServiceName)
d.Set(names.AttrServiceName, serviceName)
d.Set("service_region", vpce.ServiceRegion)
d.Set(names.AttrState, vpce.State)
d.Set(names.AttrSubnetIDs, vpce.SubnetIds)
// VPC endpoints don't have types in GovCloud, so set type to default if empty
Expand Down Expand Up @@ -769,3 +773,7 @@ func flattenSubnetConfigurations(apiObjects []subnetConfiguration) []interface{}

return tfList
}

func vpcEndpointARN(ctx context.Context, c *conns.AWSClient, ownerID, vpceID string) string {
return c.RegionalARNWithAccount(ctx, "ec2", ownerID, "vpc-endpoint/"+vpceID)
}
17 changes: 4 additions & 13 deletions internal/service/ec2/vpc_endpoint_data_source.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,11 +5,9 @@ package ec2

import (
"context"
"fmt"
"time"

"github.com/aws/aws-sdk-go-v2/aws"
"github.com/aws/aws-sdk-go-v2/aws/arn"
"github.com/aws/aws-sdk-go-v2/service/ec2"
awstypes "github.com/aws/aws-sdk-go-v2/service/ec2/types"
"github.com/hashicorp/terraform-plugin-sdk/v2/diag"
Expand Down Expand Up @@ -185,16 +183,8 @@ func dataSourceVPCEndpointRead(ctx context.Context, d *schema.ResourceData, meta

d.SetId(aws.ToString(vpce.VpcEndpointId))

arn := arn.ARN{
Partition: meta.(*conns.AWSClient).Partition(ctx),
Service: names.EC2,
Region: meta.(*conns.AWSClient).Region(ctx),
AccountID: aws.ToString(vpce.OwnerId),
Resource: fmt.Sprintf("vpc-endpoint/%s", d.Id()),
}.String()
serviceName := aws.ToString(vpce.ServiceName)

d.Set(names.AttrARN, arn)
ownerID := aws.ToString(vpce.OwnerId)
d.Set(names.AttrARN, vpcEndpointARN(ctx, meta.(*conns.AWSClient), ownerID, d.Id()))
if err := d.Set("dns_entry", flattenDNSEntries(vpce.DnsEntries)); err != nil {
return sdkdiag.AppendErrorf(diags, "setting dns_entry: %s", err)
}
Expand All @@ -207,11 +197,12 @@ func dataSourceVPCEndpointRead(ctx context.Context, d *schema.ResourceData, meta
}
d.Set(names.AttrIPAddressType, vpce.IpAddressType)
d.Set("network_interface_ids", vpce.NetworkInterfaceIds)
d.Set(names.AttrOwnerID, vpce.OwnerId)
d.Set(names.AttrOwnerID, ownerID)
d.Set("private_dns_enabled", vpce.PrivateDnsEnabled)
d.Set("requester_managed", vpce.RequesterManaged)
d.Set("route_table_ids", vpce.RouteTableIds)
d.Set(names.AttrSecurityGroupIDs, flattenSecurityGroupIdentifiers(vpce.Groups))
serviceName := aws.ToString(vpce.ServiceName)
d.Set(names.AttrServiceName, serviceName)
d.Set(names.AttrState, vpce.State)
d.Set(names.AttrSubnetIDs, vpce.SubnetIds)
Expand Down
1 change: 1 addition & 0 deletions internal/service/ec2/vpc_endpoint_data_source_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@ func TestAccVPCEndpointDataSource_gatewayBasic(t *testing.T) {
resource.TestCheckResourceAttrPair(datasourceName, "route_table_ids.#", resourceName, "route_table_ids.#"),
resource.TestCheckResourceAttrPair(datasourceName, "security_group_ids.#", resourceName, "security_group_ids.#"),
resource.TestCheckResourceAttrPair(datasourceName, names.AttrServiceName, resourceName, names.AttrServiceName),
resource.TestCheckResourceAttrPair(datasourceName, "service_region", resourceName, "service_region"),
resource.TestCheckResourceAttrPair(datasourceName, names.AttrState, resourceName, names.AttrState),
resource.TestCheckResourceAttrPair(datasourceName, "subnet_ids.#", resourceName, "subnet_ids.#"),
resource.TestCheckResourceAttrPair(datasourceName, acctest.CtTagsPercent, resourceName, acctest.CtTagsPercent),
Expand Down
125 changes: 125 additions & 0 deletions internal/service/ec2/vpc_endpoint_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import (

"github.com/YakDriver/regexache"
awstypes "github.com/aws/aws-sdk-go-v2/service/ec2/types"
"github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema"
sdkacctest "github.com/hashicorp/terraform-plugin-testing/helper/acctest"
"github.com/hashicorp/terraform-plugin-testing/helper/resource"
"github.com/hashicorp/terraform-plugin-testing/terraform"
Expand Down Expand Up @@ -862,6 +863,75 @@ func TestAccVPCEndpoint_VPCEndpointType_gatewayLoadBalancer(t *testing.T) {
})
}

func TestAccVPCEndpoint_crossRegionService(t *testing.T) {
ctx := acctest.Context(t)
var endpoint awstypes.VpcEndpoint
var svcCfg awstypes.ServiceConfiguration
serviceResourceName := "aws_vpc_endpoint_service.test"
endpointResourceName := "aws_vpc_endpoint.test"
rName := sdkacctest.RandomWithPrefix("tfacctest") // 32 character limit

// record the initialized providers so that we can use them to
// check for the vpc endpoints in each region
var providers []*schema.Provider

resource.ParallelTest(t, resource.TestCase{
PreCheck: func() {
acctest.PreCheck(ctx, t)
acctest.PreCheckMultipleRegion(t, 2)
},
ErrorCheck: acctest.ErrorCheck(t, names.EC2ServiceID),
ProtoV5ProviderFactories: acctest.ProtoV5FactoriesPlusProvidersAlternate(ctx, t, &providers),
CheckDestroy: resource.ComposeAggregateTestCheckFunc(
testAccCheckVPCEndpointDestroy(ctx),
testAccCheckVPCEndpointServiceDestroy(ctx),
),
Steps: []resource.TestStep{
{
Config: testAccVPCEndpointConfig_crossRegionService(rName, acctest.Region()), // Cross-region
Check: resource.ComposeAggregateTestCheckFunc(
testAccCheckVPCEndpointExistsWithProvider(ctx, endpointResourceName, &endpoint, acctest.RegionProviderFunc(ctx, acctest.AlternateRegion(), &providers)),
testAccCheckVPCEndpointServiceExists(ctx, serviceResourceName, &svcCfg),
resource.TestCheckResourceAttr(endpointResourceName, "service_region", acctest.Region()),
),
},
{
ResourceName: endpointResourceName,
ImportState: true,
ImportStateVerify: true,
},
},
})
}

func TestAccVPCEndpoint_invalidCrossRegionService(t *testing.T) {
ctx := acctest.Context(t)
rName := sdkacctest.RandomWithPrefix("tfacctest") // 32 character limit

// record the initialized providers so that we can use them to
// check for the vpc endpoints in each region
var providers []*schema.Provider

resource.ParallelTest(t, resource.TestCase{
PreCheck: func() {
acctest.PreCheck(ctx, t)
acctest.PreCheckMultipleRegion(t, 3)
},
ErrorCheck: acctest.ErrorCheck(t, names.EC2ServiceID),
ProtoV5ProviderFactories: acctest.ProtoV5FactoriesPlusProvidersAlternate(ctx, t, &providers),
CheckDestroy: resource.ComposeAggregateTestCheckFunc(
testAccCheckVPCEndpointDestroy(ctx),
testAccCheckVPCEndpointServiceDestroy(ctx),
),
Steps: []resource.TestStep{
{
Config: testAccVPCEndpointConfig_crossRegionService(rName, acctest.ThirdRegion()), // Cross-region
ExpectError: regexache.MustCompile(`The Vpc Endpoint Service 'com\.amazonaws\.vpce\.([A-Za-z0-9]+(-[A-Za-z0-9]+)+)\.vpce-svc-[A-Za-z0-9]+' does not exist`),
},
},
})
}

func testAccCheckVPCEndpointDestroy(ctx context.Context) resource.TestCheckFunc {
return func(s *terraform.State) error {
conn := acctest.Provider.Meta().(*conns.AWSClient).EC2Client(ctx)
Expand Down Expand Up @@ -911,6 +981,31 @@ func testAccCheckVPCEndpointExists(ctx context.Context, n string, v *awstypes.Vp
}
}

func testAccCheckVPCEndpointExistsWithProvider(ctx context.Context, n string, v *awstypes.VpcEndpoint, providerF func() *schema.Provider) resource.TestCheckFunc {
return func(s *terraform.State) error {
rs, ok := s.RootModule().Resources[n]
if !ok {
return fmt.Errorf("Not found: %s", n)
}

if rs.Primary.ID == "" {
return fmt.Errorf("No EC2 VPC Endpoint ID is set")
}

conn := providerF().Meta().(*conns.AWSClient).EC2Client(ctx)

output, err := tfec2.FindVPCEndpointByID(ctx, conn, rs.Primary.ID)

if err != nil {
return err
}

*v = *output

return nil
}
}

func testAccVPCEndpointConfig_gatewayBasic(rName string) string {
return fmt.Sprintf(`
resource "aws_vpc" "test" {
Expand Down Expand Up @@ -1681,3 +1776,33 @@ resource "aws_vpc_endpoint" "test" {
}
`, rName)
}

func testAccVPCEndpointConfig_crossRegionService(rName, serviceRegion string) string {
return acctest.ConfigCompose(
acctest.ConfigMultipleRegionProvider(2),
testAccVPCEndpointServiceConfig_crossRegion(rName, acctest.Region(), acctest.AlternateRegion()),
fmt.Sprintf(`

resource "aws_vpc" "endpoint" {
provider = "awsalternate"
cidr_block = "10.0.0.0/16"

tags = {
Name = %[1]q
}
}

resource "aws_vpc_endpoint" "test" {
provider = "awsalternate"
vpc_id = aws_vpc.endpoint.id
vpc_endpoint_type = "Interface"
service_name = aws_vpc_endpoint_service.test.service_name
service_region = "%[2]s"

tags = {
Name = "%[1]s"
}
}
`, rName, serviceRegion),
)
}
1 change: 1 addition & 0 deletions website/docs/d/vpc_endpoint.html.markdown
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,7 @@ In addition to all arguments above except `filter`, the following attributes are
* `requester_managed` - Whether or not the VPC Endpoint is being managed by its service - `true` or `false`.
* `route_table_ids` - One or more route tables associated with the VPC Endpoint. Applicable for endpoints of type `Gateway`.
* `security_group_ids` - One or more security groups associated with the network interfaces. Applicable for endpoints of type `Interface`.
* `service_region` - The AWS region of the VPC Endpoint Service. Applicable for endpoints of type `Interface`.
* `subnet_ids` - One or more subnets in which the VPC Endpoint is located. Applicable for endpoints of type `Interface`.
* `vpc_endpoint_type` - VPC Endpoint type, `Gateway` or `Interface`.

Expand Down
1 change: 1 addition & 0 deletions website/docs/r/vpc_endpoint.html.markdown
Original file line number Diff line number Diff line change
Expand Up @@ -144,6 +144,7 @@ Defaults to `false`.
* `dns_options` - (Optional) The DNS options for the endpoint. See dns_options below.
* `ip_address_type` - (Optional) The IP address type for the endpoint. Valid values are `ipv4`, `dualstack`, and `ipv6`.
* `route_table_ids` - (Optional) One or more route table IDs. Applicable for endpoints of type `Gateway`.
* `service_region` - (Optional) - The AWS region of the VPC Endpoint Service. If specified, the VPC endpoint will connect to the service in the provided region. Applicable for endpoints of type `Interface`.
* `subnet_configuration` - (Optional) Subnet configuration for the endpoint, used to select specific IPv4 and/or IPv6 addresses to the endpoint. See subnet_configuration below.
* `subnet_ids` - (Optional) The ID of one or more subnets in which to create a network interface for the endpoint. Applicable for endpoints of type `GatewayLoadBalancer` and `Interface`. Interface type endpoints cannot function without being assigned to a subnet.
* `security_group_ids` - (Optional) The ID of one or more security groups to associate with the network interface. Applicable for endpoints of type `Interface`.
Expand Down
Loading