diff --git a/.changelog/37628.txt b/.changelog/37628.txt new file mode 100644 index 00000000000..6a6c42523b5 --- /dev/null +++ b/.changelog/37628.txt @@ -0,0 +1,3 @@ +```release-note:new-resource +aws_vpc_endpoint_private_dns +``` diff --git a/internal/service/ec2/service_package_gen.go b/internal/service/ec2/service_package_gen.go index 3a5607cdeb1..e8007977448 100644 --- a/internal/service/ec2/service_package_gen.go +++ b/internal/service/ec2/service_package_gen.go @@ -46,6 +46,10 @@ func (p *servicePackage) FrameworkResources(ctx context.Context) []*types.Servic Factory: newInstanceMetadataDefaultsResource, Name: "Instance Metadata Defaults", }, + { + Factory: newResourceEndpointPrivateDNS, + Name: "Endpoint Private DNS", + }, { Factory: newResourceEndpointServicePrivateDNSVerification, Name: "Endpoint Service Private DNS Verification", diff --git a/internal/service/ec2/vpc_endpoint.go b/internal/service/ec2/vpc_endpoint.go index 6a8b356e1c5..a5285982f79 100644 --- a/internal/service/ec2/vpc_endpoint.go +++ b/internal/service/ec2/vpc_endpoint.go @@ -135,7 +135,7 @@ func ResourceVPCEndpoint() *schema.Resource { "private_dns_enabled": { Type: schema.TypeBool, Optional: true, - Default: false, + Computed: true, }, "requester_managed": { Type: schema.TypeBool, @@ -202,7 +202,6 @@ func resourceVPCEndpointCreate(ctx context.Context, d *schema.ResourceData, meta serviceName := d.Get(names.AttrServiceName).(string) input := &ec2.CreateVpcEndpointInput{ ClientToken: aws.String(id.UniqueId()), - PrivateDnsEnabled: aws.Bool(d.Get("private_dns_enabled").(bool)), ServiceName: aws.String(serviceName), TagSpecifications: getTagSpecificationsInV2(ctx, awstypes.ResourceTypeVpcEndpoint), VpcEndpointType: awstypes.VpcEndpointType(d.Get("vpc_endpoint_type").(string)), @@ -233,6 +232,10 @@ func resourceVPCEndpointCreate(ctx context.Context, d *schema.ResourceData, meta input.PolicyDocument = aws.String(policy) } + if v, ok := d.GetOk("private_dns_enabled"); ok { + input.PrivateDnsEnabled = aws.Bool(v.(bool)) + } + if v, ok := d.GetOk("route_table_ids"); ok && v.(*schema.Set).Len() > 0 { input.RouteTableIds = flex.ExpandStringValueSet(v.(*schema.Set)) } @@ -381,7 +384,6 @@ func resourceVPCEndpointUpdate(ctx context.Context, d *schema.ResourceData, meta } if d.HasChanges("dns_options", names.AttrIPAddressType, names.AttrPolicy, "private_dns_enabled", names.AttrSecurityGroupIDs, "route_table_ids", names.AttrSubnetIDs) { - privateDNSEnabled := d.Get("private_dns_enabled").(bool) input := &ec2.ModifyVpcEndpointInput{ VpcEndpointId: aws.String(d.Id()), } @@ -403,6 +405,7 @@ func resourceVPCEndpointUpdate(ctx context.Context, d *schema.ResourceData, meta input.IpAddressType = awstypes.IpAddressType(d.Get(names.AttrIPAddressType).(string)) } + privateDNSEnabled := d.Get("private_dns_enabled").(bool) if d.HasChange("private_dns_enabled") { input.PrivateDnsEnabled = aws.Bool(privateDNSEnabled) } diff --git a/internal/service/ec2/vpc_endpoint_private_dns.go b/internal/service/ec2/vpc_endpoint_private_dns.go new file mode 100644 index 00000000000..51fbd41419b --- /dev/null +++ b/internal/service/ec2/vpc_endpoint_private_dns.go @@ -0,0 +1,162 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package ec2 + +import ( + "context" + "errors" + + "github.com/aws/aws-sdk-go-v2/aws" + "github.com/aws/aws-sdk-go-v2/service/ec2" + "github.com/hashicorp/terraform-plugin-framework/path" + "github.com/hashicorp/terraform-plugin-framework/resource" + "github.com/hashicorp/terraform-plugin-framework/resource/schema" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/planmodifier" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/stringplanmodifier" + "github.com/hashicorp/terraform-plugin-framework/types" + "github.com/hashicorp/terraform-provider-aws/internal/create" + "github.com/hashicorp/terraform-provider-aws/internal/framework" + "github.com/hashicorp/terraform-provider-aws/internal/framework/flex" + "github.com/hashicorp/terraform-provider-aws/internal/tfresource" + "github.com/hashicorp/terraform-provider-aws/names" +) + +// @FrameworkResource("aws_vpc_endpoint_private_dns", name="Endpoint Private DNS") +func newResourceEndpointPrivateDNS(_ context.Context) (resource.ResourceWithConfigure, error) { + return &resourceEndpointPrivateDNS{}, nil +} + +const ( + ResNameEndpointPrivateDNS = "Endpoint Private DNS" +) + +type resourceEndpointPrivateDNS struct { + framework.ResourceWithConfigure + framework.WithNoOpDelete +} + +func (r *resourceEndpointPrivateDNS) Metadata(_ context.Context, req resource.MetadataRequest, resp *resource.MetadataResponse) { + resp.TypeName = "aws_vpc_endpoint_private_dns" +} + +func (r *resourceEndpointPrivateDNS) Schema(ctx context.Context, req resource.SchemaRequest, resp *resource.SchemaResponse) { + resp.Schema = schema.Schema{ + Attributes: map[string]schema.Attribute{ + "private_dns_enabled": schema.BoolAttribute{ + Required: true, + }, + names.AttrVPCEndpointID: schema.StringAttribute{ + Required: true, + PlanModifiers: []planmodifier.String{ + stringplanmodifier.RequiresReplace(), + }, + }, + }, + } +} + +func (r *resourceEndpointPrivateDNS) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) { + conn := r.Meta().EC2Client(ctx) + + var plan resourceEndpointPrivateDNSData + resp.Diagnostics.Append(req.Plan.Get(ctx, &plan)...) + if resp.Diagnostics.HasError() { + return + } + + in := &ec2.ModifyVpcEndpointInput{ + VpcEndpointId: aws.String(plan.VpcEndpointID.ValueString()), + PrivateDnsEnabled: aws.Bool(plan.PrivateDNSEnabled.ValueBool()), + } + + out, err := conn.ModifyVpcEndpoint(ctx, in) + if err != nil { + resp.Diagnostics.AddError( + create.ProblemStandardMessage(names.EC2, create.ErrActionCreating, ResNameEndpointPrivateDNS, plan.VpcEndpointID.String(), err), + err.Error(), + ) + return + } + if out == nil { + resp.Diagnostics.AddError( + create.ProblemStandardMessage(names.EC2, create.ErrActionCreating, ResNameEndpointPrivateDNS, plan.VpcEndpointID.String(), nil), + errors.New("empty output").Error(), + ) + return + } + + resp.Diagnostics.Append(resp.State.Set(ctx, plan)...) +} + +func (r *resourceEndpointPrivateDNS) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) { + conn := r.Meta().EC2Client(ctx) + + var state resourceEndpointPrivateDNSData + resp.Diagnostics.Append(req.State.Get(ctx, &state)...) + if resp.Diagnostics.HasError() { + return + } + + out, err := findVPCEndpointByIDV2(ctx, conn, state.VpcEndpointID.ValueString()) + if tfresource.NotFound(err) { + resp.State.RemoveResource(ctx) + return + } + if err != nil { + resp.Diagnostics.AddError( + create.ProblemStandardMessage(names.EC2, create.ErrActionReading, ResNameEndpointPrivateDNS, state.VpcEndpointID.String(), err), + err.Error(), + ) + return + } + + state.PrivateDNSEnabled = flex.BoolToFramework(ctx, out.PrivateDnsEnabled) + + resp.Diagnostics.Append(resp.State.Set(ctx, &state)...) +} + +func (r *resourceEndpointPrivateDNS) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) { + conn := r.Meta().EC2Client(ctx) + + var plan, state resourceEndpointPrivateDNSData + resp.Diagnostics.Append(req.Plan.Get(ctx, &plan)...) + resp.Diagnostics.Append(req.State.Get(ctx, &state)...) + if resp.Diagnostics.HasError() { + return + } + + if !plan.PrivateDNSEnabled.Equal(state.PrivateDNSEnabled) { + in := &ec2.ModifyVpcEndpointInput{ + VpcEndpointId: aws.String(plan.VpcEndpointID.ValueString()), + PrivateDnsEnabled: aws.Bool(plan.PrivateDNSEnabled.ValueBool()), + } + + out, err := conn.ModifyVpcEndpoint(ctx, in) + if err != nil { + resp.Diagnostics.AddError( + create.ProblemStandardMessage(names.EC2, create.ErrActionCreating, ResNameEndpointPrivateDNS, plan.VpcEndpointID.String(), err), + err.Error(), + ) + return + } + if out == nil { + resp.Diagnostics.AddError( + create.ProblemStandardMessage(names.EC2, create.ErrActionCreating, ResNameEndpointPrivateDNS, plan.VpcEndpointID.String(), nil), + errors.New("empty output").Error(), + ) + return + } + } + + resp.Diagnostics.Append(resp.State.Set(ctx, &plan)...) +} + +func (r *resourceEndpointPrivateDNS) ImportState(ctx context.Context, req resource.ImportStateRequest, resp *resource.ImportStateResponse) { + resource.ImportStatePassthroughID(ctx, path.Root(names.AttrVPCEndpointID), req, resp) +} + +type resourceEndpointPrivateDNSData struct { + VpcEndpointID types.String `tfsdk:"vpc_endpoint_id"` + PrivateDNSEnabled types.Bool `tfsdk:"private_dns_enabled"` +} diff --git a/internal/service/ec2/vpc_endpoint_private_dns_test.go b/internal/service/ec2/vpc_endpoint_private_dns_test.go new file mode 100644 index 00000000000..eb348f66345 --- /dev/null +++ b/internal/service/ec2/vpc_endpoint_private_dns_test.go @@ -0,0 +1,237 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package ec2_test + +import ( + "context" + "errors" + "fmt" + "testing" + + "github.com/aws/aws-sdk-go-v2/aws" + awstypes "github.com/aws/aws-sdk-go-v2/service/ec2/types" + sdkacctest "github.com/hashicorp/terraform-plugin-testing/helper/acctest" + "github.com/hashicorp/terraform-plugin-testing/helper/resource" + "github.com/hashicorp/terraform-plugin-testing/terraform" + "github.com/hashicorp/terraform-provider-aws/internal/acctest" + "github.com/hashicorp/terraform-provider-aws/internal/conns" + "github.com/hashicorp/terraform-provider-aws/internal/create" + tfec2 "github.com/hashicorp/terraform-provider-aws/internal/service/ec2" + "github.com/hashicorp/terraform-provider-aws/names" +) + +func TestAccVPCEndpointPrivateDNS_basic(t *testing.T) { + ctx := acctest.Context(t) + if testing.Short() { + t.Skip("skipping long-running test in short mode") + } + + var endpoint awstypes.VpcEndpoint + rName := sdkacctest.RandomWithPrefix(acctest.ResourcePrefix) + resourceName := "aws_vpc_endpoint_private_dns.test" + endpointResourceName := "aws_vpc_endpoint.test" + + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { + acctest.PreCheck(ctx, t) + acctest.PreCheckPartitionHasService(t, names.EC2) + }, + ErrorCheck: acctest.ErrorCheck(t, names.EC2ServiceID), + ProtoV5ProviderFactories: acctest.ProtoV5ProviderFactories, + CheckDestroy: testAccCheckVPCEndpointDestroy(ctx), + Steps: []resource.TestStep{ + { + Config: testAccVPCEndpointPrivateDNSConfig_basic(rName, true), + Check: resource.ComposeTestCheckFunc( + testAccCheckVPCEndpointExists(ctx, endpointResourceName, &endpoint), + testAccCheckVPCEndpointPrivateDNSEnabled(ctx, endpointResourceName), + resource.TestCheckResourceAttrPair(endpointResourceName, names.AttrID, resourceName, names.AttrVPCEndpointID), + resource.TestCheckResourceAttr(resourceName, "private_dns_enabled", "true"), + ), + }, + { + ResourceName: resourceName, + ImportState: true, + ImportStateIdFunc: testAccVPCEndpointPrivateDNSImportStateIdFunc(resourceName), + ImportStateVerify: true, + ImportStateVerifyIdentifierAttribute: names.AttrVPCEndpointID, + }, + }, + }) +} + +func TestAccVPCEndpointPrivateDNS_disappears_Endpoint(t *testing.T) { + ctx := acctest.Context(t) + if testing.Short() { + t.Skip("skipping long-running test in short mode") + } + + var endpoint awstypes.VpcEndpoint + rName := sdkacctest.RandomWithPrefix(acctest.ResourcePrefix) + endpointResourceName := "aws_vpc_endpoint.test" + + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { + acctest.PreCheck(ctx, t) + acctest.PreCheckPartitionHasService(t, names.EC2) + }, + ErrorCheck: acctest.ErrorCheck(t, names.EC2ServiceID), + ProtoV5ProviderFactories: acctest.ProtoV5ProviderFactories, + CheckDestroy: testAccCheckVPCEndpointDestroy(ctx), + Steps: []resource.TestStep{ + { + Config: testAccVPCEndpointPrivateDNSConfig_basic(rName, true), + Check: resource.ComposeTestCheckFunc( + testAccCheckVPCEndpointExists(ctx, endpointResourceName, &endpoint), + testAccCheckVPCEndpointPrivateDNSEnabled(ctx, endpointResourceName), + acctest.CheckResourceDisappears(ctx, acctest.Provider, tfec2.ResourceVPCEndpoint(), endpointResourceName), + ), + ExpectNonEmptyPlan: true, + }, + }, + }) +} + +func TestAccVPCEndpointPrivateDNS_update(t *testing.T) { + ctx := acctest.Context(t) + if testing.Short() { + t.Skip("skipping long-running test in short mode") + } + + var endpoint awstypes.VpcEndpoint + rName := sdkacctest.RandomWithPrefix(acctest.ResourcePrefix) + resourceName := "aws_vpc_endpoint_private_dns.test" + endpointResourceName := "aws_vpc_endpoint.test" + + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { + acctest.PreCheck(ctx, t) + acctest.PreCheckPartitionHasService(t, names.EC2) + }, + ErrorCheck: acctest.ErrorCheck(t, names.EC2ServiceID), + ProtoV5ProviderFactories: acctest.ProtoV5ProviderFactories, + CheckDestroy: testAccCheckVPCEndpointDestroy(ctx), + Steps: []resource.TestStep{ + { + Config: testAccVPCEndpointPrivateDNSConfig_basic(rName, true), + Check: resource.ComposeTestCheckFunc( + testAccCheckVPCEndpointExists(ctx, endpointResourceName, &endpoint), + testAccCheckVPCEndpointPrivateDNSEnabled(ctx, endpointResourceName), + resource.TestCheckResourceAttrPair(endpointResourceName, names.AttrID, resourceName, names.AttrVPCEndpointID), + resource.TestCheckResourceAttr(resourceName, "private_dns_enabled", "true"), + ), + }, + { + Config: testAccVPCEndpointPrivateDNSConfig_basic(rName, false), + Check: resource.ComposeTestCheckFunc( + testAccCheckVPCEndpointExists(ctx, endpointResourceName, &endpoint), + testAccCheckVPCEndpointPrivateDNSDisabled(ctx, endpointResourceName), + resource.TestCheckResourceAttrPair(endpointResourceName, names.AttrID, resourceName, names.AttrVPCEndpointID), + resource.TestCheckResourceAttr(resourceName, "private_dns_enabled", "false"), + ), + }, + { + Config: testAccVPCEndpointPrivateDNSConfig_basic(rName, true), + Check: resource.ComposeTestCheckFunc( + testAccCheckVPCEndpointExists(ctx, endpointResourceName, &endpoint), + testAccCheckVPCEndpointPrivateDNSEnabled(ctx, endpointResourceName), + resource.TestCheckResourceAttrPair(endpointResourceName, names.AttrID, resourceName, names.AttrVPCEndpointID), + resource.TestCheckResourceAttr(resourceName, "private_dns_enabled", "true"), + ), + }, + }, + }) +} + +// testAccCheckVPCEndpointPrivateDNSEnabled verifies private DNS is enabled for a given VPC endpoint +func testAccCheckVPCEndpointPrivateDNSEnabled(ctx context.Context, name string) resource.TestCheckFunc { + return func(s *terraform.State) error { + rs, ok := s.RootModule().Resources[name] + if !ok { + return create.Error(names.EC2, create.ErrActionCheckingExistence, tfec2.ResNameEndpointPrivateDNS, name, errors.New("not found")) + } + + if rs.Primary.ID == "" { + return create.Error(names.EC2, create.ErrActionCheckingExistence, tfec2.ResNameEndpointPrivateDNS, name, errors.New("not set")) + } + + conn := acctest.Provider.Meta().(*conns.AWSClient).EC2Client(ctx) + out, err := tfec2.FindVPCEndpointByIDV2(ctx, conn, rs.Primary.ID) + if err != nil { + return create.Error(names.EC2, create.ErrActionCheckingExistence, tfec2.ResNameEndpointPrivateDNS, rs.Primary.ID, err) + } + if out.PrivateDnsEnabled != nil && aws.ToBool(out.PrivateDnsEnabled) { + return nil + } + + return create.Error(names.EC2, create.ErrActionCheckingExistence, tfec2.ResNameEndpointPrivateDNS, rs.Primary.ID, errors.New("private DNS not enabled")) + } +} + +// testAccCheckVPCEndpointPrivateDNSDisabled verifies private DNS is not enabled for a given VPC endpoint +func testAccCheckVPCEndpointPrivateDNSDisabled(ctx context.Context, name string) resource.TestCheckFunc { + return func(s *terraform.State) error { + rs, ok := s.RootModule().Resources[name] + if !ok { + return create.Error(names.EC2, create.ErrActionCheckingExistence, tfec2.ResNameEndpointPrivateDNS, name, errors.New("not found")) + } + + if rs.Primary.ID == "" { + return create.Error(names.EC2, create.ErrActionCheckingExistence, tfec2.ResNameEndpointPrivateDNS, name, errors.New("not set")) + } + + conn := acctest.Provider.Meta().(*conns.AWSClient).EC2Client(ctx) + out, err := tfec2.FindVPCEndpointByIDV2(ctx, conn, rs.Primary.ID) + if err != nil { + return create.Error(names.EC2, create.ErrActionCheckingExistence, tfec2.ResNameEndpointPrivateDNS, rs.Primary.ID, err) + } + if out.PrivateDnsEnabled != nil && aws.ToBool(out.PrivateDnsEnabled) { + return create.Error(names.EC2, create.ErrActionCheckingExistence, tfec2.ResNameEndpointPrivateDNS, rs.Primary.ID, errors.New("private DNS enabled")) + } + + return nil + } +} + +func testAccVPCEndpointPrivateDNSImportStateIdFunc(resourceName string) resource.ImportStateIdFunc { + return func(s *terraform.State) (string, error) { + rs, ok := s.RootModule().Resources[resourceName] + if !ok { + return "", fmt.Errorf("Not found: %s", resourceName) + } + + return rs.Primary.Attributes[names.AttrVPCEndpointID], nil + } +} + +func testAccVPCEndpointPrivateDNSConfig_basic(rName string, enabled bool) string { + return fmt.Sprintf(` +data "aws_region" "current" {} + +resource "aws_vpc" "test" { + cidr_block = "10.0.0.0/16" + enable_dns_support = true + enable_dns_hostnames = true + + tags = { + Name = %[1]q + } +} + +resource "aws_vpc_endpoint" "test" { + vpc_id = aws_vpc.test.id + service_name = "com.amazonaws.${data.aws_region.current.name}.ec2" + vpc_endpoint_type = "Interface" + + tags = { + Name = %[1]q + } +} + +resource "aws_vpc_endpoint_private_dns" "test" { + vpc_endpoint_id = aws_vpc_endpoint.test.id + private_dns_enabled = %[2]t +} +`, rName, enabled) +} diff --git a/website/docs/r/vpc_endpoint_private_dns.html.markdown b/website/docs/r/vpc_endpoint_private_dns.html.markdown new file mode 100644 index 00000000000..9f51b00bfa5 --- /dev/null +++ b/website/docs/r/vpc_endpoint_private_dns.html.markdown @@ -0,0 +1,52 @@ +--- +subcategory: "VPC (Virtual Private Cloud)" +layout: "aws" +page_title: "AWS: aws_vpc_endpoint_private_dns" +description: |- + Terraform resource for enabling private DNS on an AWS VPC (Virtual Private Cloud) Endpoint. +--- +# Resource: aws_vpc_endpoint_private_dns + +Terraform resource for enabling private DNS on an AWS VPC (Virtual Private Cloud) Endpoint. + +~> When using this resource, the `private_dns_enabled` argument should be omitted on the parent `aws_vpc_endpoint` resource. +Setting the value both places can lead to unintended behavior and persistent differences. + +## Example Usage + +### Basic Usage + +```terraform +resource "aws_vpc_endpoint_private_dns" "example" { + vpc_endpoint_id = aws_vpc_endpoint.example.id + private_dns_enabled = true +} +``` + +## Argument Reference + +The following arguments are required: + +* `private_dns_enabled` - (Required) Indicates whether a private hosted zone is associated with the VPC. Only applicable for `Interface` endpoints. +* `vpc_endpoint_id` - (Required) VPC endpoint identifier. + +## Attribute Reference + +This resource exports no additional attributes. + +## Import + +In Terraform v1.5.0 and later, use an [`import` block](https://developer.hashicorp.com/terraform/language/import) to import a VPC (Virtual Private Cloud) Endpoint Private DNS using the `vpc_endpoint_id`. For example: + +```terraform +import { + to = aws_vpc_endpoint_private_dns.example + id = "vpce-abcd-1234" +} +``` + +Using `terraform import`, import a VPC (Virtual Private Cloud) Endpoint Private DNS using the `vpc_endpoint_id`. For example: + +```console +% terraform import aws_vpc_endpoint_private_dns.example vpce-abcd-1234 +```