diff --git a/.changelog/28651.txt b/.changelog/28651.txt new file mode 100644 index 000000000000..ca15a3ec4084 --- /dev/null +++ b/.changelog/28651.txt @@ -0,0 +1,3 @@ +```release-note:new-resource +aws_opensearchserverless_vpc_endpoint +``` diff --git a/internal/service/opensearchserverless/exports_test.go b/internal/service/opensearchserverless/exports_test.go index bc27bad3ea0a..02f64b8ec319 100644 --- a/internal/service/opensearchserverless/exports_test.go +++ b/internal/service/opensearchserverless/exports_test.go @@ -5,4 +5,5 @@ var ( ResourceCollection = newResourceCollection ResourceAccessPolicy = newResourceAccessPolicy ResourceSecurityPolicy = newResourceSecurityPolicy + ResourceVPCEndpoint = newResourceVPCEndpoint ) diff --git a/internal/service/opensearchserverless/find.go b/internal/service/opensearchserverless/find.go index de08321934d4..3921606b449d 100644 --- a/internal/service/opensearchserverless/find.go +++ b/internal/service/opensearchserverless/find.go @@ -84,3 +84,28 @@ func FindSecurityPolicyByNameAndType(ctx context.Context, conn *opensearchserver return out.SecurityPolicyDetail, nil } + +func FindVPCEndpointByID(ctx context.Context, conn *opensearchserverless.Client, id string) (*types.VpcEndpointDetail, error) { + in := &opensearchserverless.BatchGetVpcEndpointInput{ + Ids: []string{id}, + } + out, err := conn.BatchGetVpcEndpoint(ctx, in) + + if err != nil { + var nfe *types.ResourceNotFoundException + if errors.As(err, &nfe) { + return nil, &retry.NotFoundError{ + LastError: err, + LastRequest: in, + } + } + + return nil, err + } + + if out == nil || out.VpcEndpointDetails == nil || len(out.VpcEndpointDetails) == 0 { + return nil, tfresource.NewEmptyResultError(in) + } + + return &out.VpcEndpointDetails[0], nil +} diff --git a/internal/service/opensearchserverless/service_package_gen.go b/internal/service/opensearchserverless/service_package_gen.go index d12c6135eb78..40623a0184a2 100644 --- a/internal/service/opensearchserverless/service_package_gen.go +++ b/internal/service/opensearchserverless/service_package_gen.go @@ -30,6 +30,9 @@ func (p *servicePackage) FrameworkResources(ctx context.Context) []*types.Servic { Factory: newResourceSecurityPolicy, }, + { + Factory: newResourceVPCEndpoint, + }, } } diff --git a/internal/service/opensearchserverless/sweep.go b/internal/service/opensearchserverless/sweep.go index 2f0060cdd303..66896d5eb81d 100644 --- a/internal/service/opensearchserverless/sweep.go +++ b/internal/service/opensearchserverless/sweep.go @@ -29,6 +29,10 @@ func init() { Name: "aws_opensearchserverless_security_policy", F: sweepSecurityPolicies, }) + resource.AddTestSweepers("aws_opensearchserverless_vpc_endpoint", &resource.Sweeper{ + Name: "aws_opensearchserverless_vpc_endpoint", + F: sweepVPCEndpoints, + }) } func sweepAccessPolicies(region string) error { @@ -201,3 +205,47 @@ func sweepSecurityPolicies(region string) error { return errs.ErrorOrNil() } + +func sweepVPCEndpoints(region string) error { + ctx := sweep.Context(region) + client, err := sweep.SharedRegionalSweepClient(region) + + if err != nil { + return fmt.Errorf("error getting client: %w", err) + } + + conn := client.(*conns.AWSClient).OpenSearchServerlessClient(ctx) + sweepResources := make([]sweep.Sweepable, 0) + var errs *multierror.Error + input := &opensearchserverless.ListVpcEndpointsInput{} + + pages := opensearchserverless.NewListVpcEndpointsPaginator(conn, input) + + for pages.HasMorePages() { + page, err := pages.NextPage(ctx) + if sweep.SkipSweepError(err) { + log.Printf("[WARN] Skipping OpenSearch Serverless VPC Endpoints sweep for %s: %s", region, err) + return nil + } + if err != nil { + return fmt.Errorf("error retrieving OpenSearch Serverless VPC Endpoints: %w", err) + } + + for _, endpoint := range page.VpcEndpointSummaries { + id := aws.ToString(endpoint.Id) + + log.Printf("[INFO] Deleting OpenSearch Serverless VPC Endpoint: %s", id) + sweepResources = append(sweepResources, sweep.NewSweepFrameworkResource(newResourceVPCEndpoint, id, client)) + } + } + + if err := sweep.SweepOrchestratorWithContext(ctx, sweepResources); err != nil { + errs = multierror.Append(errs, fmt.Errorf("error sweeping OpenSearch Serverless VPC Endpoints for %s: %w", region, err)) + } + if sweep.SkipSweepError(err) { + log.Printf("[WARN] Skipping OpenSearch Serverless VPC Endpoint sweep for %s: %s", region, errs) + return nil + } + + return errs.ErrorOrNil() +} diff --git a/internal/service/opensearchserverless/vpc_endpoint.go b/internal/service/opensearchserverless/vpc_endpoint.go new file mode 100644 index 000000000000..782b6fef7f51 --- /dev/null +++ b/internal/service/opensearchserverless/vpc_endpoint.go @@ -0,0 +1,388 @@ +package opensearchserverless + +import ( + "context" + "errors" + "fmt" + "log" + "time" + + "github.com/aws/aws-sdk-go-v2/aws" + "github.com/aws/aws-sdk-go-v2/service/opensearchserverless" + awstypes "github.com/aws/aws-sdk-go-v2/service/opensearchserverless/types" + "github.com/hashicorp/terraform-plugin-framework-timeouts/resource/timeouts" + "github.com/hashicorp/terraform-plugin-framework-validators/setvalidator" + "github.com/hashicorp/terraform-plugin-framework-validators/stringvalidator" + "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/setplanmodifier" + "github.com/hashicorp/terraform-plugin-framework/schema/validator" + "github.com/hashicorp/terraform-plugin-framework/types" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/id" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/retry" + "github.com/hashicorp/terraform-provider-aws/internal/create" + "github.com/hashicorp/terraform-provider-aws/internal/enum" + "github.com/hashicorp/terraform-provider-aws/internal/errs/fwdiag" + "github.com/hashicorp/terraform-provider-aws/internal/flex" + "github.com/hashicorp/terraform-provider-aws/internal/framework" + "github.com/hashicorp/terraform-provider-aws/internal/tfresource" + "github.com/hashicorp/terraform-provider-aws/names" +) + +// @FrameworkResource +func newResourceVPCEndpoint(_ context.Context) (resource.ResourceWithConfigure, error) { + r := resourceVpcEndpoint{} + r.SetDefaultCreateTimeout(30 * time.Minute) + r.SetDefaultUpdateTimeout(30 * time.Minute) + r.SetDefaultDeleteTimeout(30 * time.Minute) + + return &r, nil +} + +type resourceVpcEndpointData struct { + ID types.String `tfsdk:"id"` + Name types.String `tfsdk:"name"` + SecurityGroupIds types.Set `tfsdk:"security_group_ids"` + SubnetIds types.Set `tfsdk:"subnet_ids"` + Timeouts timeouts.Value `tfsdk:"timeouts"` + VpcId types.String `tfsdk:"vpc_id"` +} + +const ( + ResNameVPCEndpoint = "VPC Endpoint" +) + +type resourceVpcEndpoint struct { + framework.ResourceWithConfigure + framework.WithTimeouts +} + +func (r *resourceVpcEndpoint) Metadata(_ context.Context, request resource.MetadataRequest, response *resource.MetadataResponse) { + response.TypeName = "aws_opensearchserverless_vpc_endpoint" +} + +func (r *resourceVpcEndpoint) Schema(ctx context.Context, req resource.SchemaRequest, resp *resource.SchemaResponse) { + resp.Schema = schema.Schema{ + Attributes: map[string]schema.Attribute{ + "id": framework.IDAttribute(), + "name": schema.StringAttribute{ + Required: true, + Validators: []validator.String{ + stringvalidator.LengthBetween(3, 32), + }, + }, + "security_group_ids": schema.SetAttribute{ + ElementType: types.StringType, + Optional: true, + Computed: true, + Validators: []validator.Set{ + setvalidator.SizeBetween(1, 5), + }, + PlanModifiers: []planmodifier.Set{ + setplanmodifier.UseStateForUnknown(), + }, + }, + "subnet_ids": schema.SetAttribute{ + ElementType: types.StringType, + Required: true, + Validators: []validator.Set{ + setvalidator.SizeBetween(1, 6), + }, + }, + "vpc_id": schema.StringAttribute{ + Required: true, + Validators: []validator.String{ + stringvalidator.LengthBetween(1, 255), + }, + }, + }, + Blocks: map[string]schema.Block{ + "timeouts": timeouts.Block(ctx, timeouts.Opts{ + Create: true, + Update: true, + Delete: true, + }), + }, + } +} + +func (r *resourceVpcEndpoint) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) { + var plan resourceVpcEndpointData + + resp.Diagnostics.Append(req.Plan.Get(ctx, &plan)...) + + if resp.Diagnostics.HasError() { + return + } + + conn := r.Meta().OpenSearchServerlessClient(ctx) + + in := &opensearchserverless.CreateVpcEndpointInput{ + ClientToken: aws.String(id.UniqueId()), + Name: aws.String(plan.Name.ValueString()), + SubnetIds: flex.ExpandFrameworkStringValueSet(ctx, plan.SubnetIds), + VpcId: aws.String(plan.VpcId.ValueString()), + } + + if !plan.SecurityGroupIds.IsNull() && !plan.SecurityGroupIds.IsUnknown() { + in.SecurityGroupIds = flex.ExpandFrameworkStringValueSet(ctx, plan.SecurityGroupIds) + } + + out, err := conn.CreateVpcEndpoint(ctx, in) + if err != nil { + resp.Diagnostics.AddError( + create.ProblemStandardMessage(names.OpenSearchServerless, create.ErrActionCreating, ResNameVPCEndpoint, plan.Name.String(), nil), + err.Error(), + ) + return + } + + createTimeout := r.CreateTimeout(ctx, plan.Timeouts) + if _, err := waitVPCEndpointCreated(ctx, conn, *out.CreateVpcEndpointDetail.Id, createTimeout); err != nil { + resp.Diagnostics.AddError( + create.ProblemStandardMessage(names.OpenSearchServerless, create.ErrActionWaitingForCreation, ResNameVPCEndpoint, plan.Name.String(), nil), + err.Error(), + ) + return + } + + // The create operation only returns the Id and Name so retrieve the newly + // created VPC Endpoint so we can store the possibly computed + // security_group_ids in state + vpcEndpoint, err := FindVPCEndpointByID(ctx, conn, aws.ToString(out.CreateVpcEndpointDetail.Id)) + if err != nil { + resp.Diagnostics.AddError( + create.ProblemStandardMessage(names.OpenSearchServerless, create.ErrActionChecking, ResNameVPCEndpoint, plan.Name.String(), nil), + err.Error(), + ) + return + } + + state := plan + state.refreshFromOutput(ctx, vpcEndpoint) + resp.Diagnostics.Append(resp.State.Set(ctx, &state)...) +} + +func (r *resourceVpcEndpoint) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) { + conn := r.Meta().OpenSearchServerlessClient(ctx) + + var state resourceVpcEndpointData + resp.Diagnostics.Append(req.State.Get(ctx, &state)...) + if resp.Diagnostics.HasError() { + return + } + + out, err := FindVPCEndpointByID(ctx, conn, state.ID.ValueString()) + if tfresource.NotFound(err) { + resp.Diagnostics.Append(fwdiag.NewResourceNotFoundWarningDiagnostic(err)) + resp.State.RemoveResource(ctx) + return + } + + state.refreshFromOutput(ctx, out) + resp.Diagnostics.Append(resp.State.Set(ctx, &state)...) +} + +func (r *resourceVpcEndpoint) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) { + conn := r.Meta().OpenSearchServerlessClient(ctx) + + update := false + + var plan, state resourceVpcEndpointData + resp.Diagnostics.Append(req.State.Get(ctx, &state)...) + resp.Diagnostics.Append(req.Plan.Get(ctx, &plan)...) + if resp.Diagnostics.HasError() { + return + } + + input := &opensearchserverless.UpdateVpcEndpointInput{ + ClientToken: aws.String(id.UniqueId()), + Id: aws.String(plan.ID.ValueString()), + } + + if !plan.SecurityGroupIds.Equal(state.SecurityGroupIds) { + newSGs := flex.ExpandFrameworkStringValueSet(ctx, plan.SecurityGroupIds) + oldSGs := flex.ExpandFrameworkStringValueSet(ctx, state.SecurityGroupIds) + + if add := newSGs.Difference(oldSGs); len(add) > 0 { + input.AddSecurityGroupIds = add + } + + if del := oldSGs.Difference(newSGs); len(del) > 0 { + input.RemoveSecurityGroupIds = del + } + + update = true + } + + if !plan.SubnetIds.Equal(state.SubnetIds) { + old := flex.ExpandFrameworkStringValueSet(ctx, state.SubnetIds) + new := flex.ExpandFrameworkStringValueSet(ctx, plan.SubnetIds) + + if add := new.Difference(old); len(add) > 0 { + input.AddSubnetIds = add + } + + if del := old.Difference(new); len(del) > 0 { + input.RemoveSubnetIds = del + } + + update = true + } + + if !update { + return + } + + log.Printf("[DEBUG] Updating OpenSearchServerless VPC Endpoint (%s): %#v", plan.ID.ValueString(), input) + out, err := conn.UpdateVpcEndpoint(ctx, input) + if err != nil { + resp.Diagnostics.AddError(fmt.Sprintf("updating VPC Endpoint (%s)", plan.ID.ValueString()), err.Error()) + return + } + + updateTimeout := r.UpdateTimeout(ctx, plan.Timeouts) + if _, err := waitVPCEndpointUpdated(ctx, conn, *out.UpdateVpcEndpointDetail.Id, updateTimeout); err != nil { + resp.Diagnostics.AddError( + create.ProblemStandardMessage(names.OpenSearchServerless, create.ErrActionWaitingForUpdate, ResNameVPCEndpoint, plan.Name.String(), nil), + err.Error(), + ) + return + } + + // The update operation only returns security_group_ids if those were + // changed so retrieve the updated VPC Endpoint so we can store the + // actual security_group_ids in state + vpcEndpoint, err := FindVPCEndpointByID(ctx, conn, *out.UpdateVpcEndpointDetail.Id) + if err != nil { + resp.Diagnostics.AddError( + create.ProblemStandardMessage(names.OpenSearchServerless, create.ErrActionChecking, ResNameVPCEndpoint, plan.Name.String(), nil), + err.Error(), + ) + return + } + + plan.refreshFromOutput(ctx, vpcEndpoint) + resp.Diagnostics.Append(resp.State.Set(ctx, &plan)...) +} + +func (r *resourceVpcEndpoint) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) { + conn := r.Meta().OpenSearchServerlessClient(ctx) + + var state resourceVpcEndpointData + resp.Diagnostics.Append(req.State.Get(ctx, &state)...) + if resp.Diagnostics.HasError() { + return + } + + _, err := conn.DeleteVpcEndpoint(ctx, &opensearchserverless.DeleteVpcEndpointInput{ + ClientToken: aws.String(id.UniqueId()), + Id: aws.String(state.ID.ValueString()), + }) + + if err != nil { + var nfe *awstypes.ResourceNotFoundException + if errors.As(err, &nfe) { + return + } + resp.Diagnostics.AddError( + create.ProblemStandardMessage(names.OpenSearchServerless, create.ErrActionDeleting, ResNameVPCEndpoint, state.Name.String(), nil), + err.Error(), + ) + } + + deleteTimeout := r.DeleteTimeout(ctx, state.Timeouts) + if _, err := waitVPCEndpointDeleted(ctx, conn, state.ID.ValueString(), deleteTimeout); err != nil { + resp.Diagnostics.AddError( + create.ProblemStandardMessage(names.OpenSearchServerless, create.ErrActionWaitingForDeletion, ResNameVPCEndpoint, state.Name.String(), nil), + err.Error(), + ) + return + } +} + +func (r *resourceVpcEndpoint) ImportState(ctx context.Context, req resource.ImportStateRequest, resp *resource.ImportStateResponse) { + resource.ImportStatePassthroughID(ctx, path.Root("id"), req, resp) +} + +// refreshFromOutput writes state data from an AWS response object +func (rd *resourceVpcEndpointData) refreshFromOutput(ctx context.Context, out *awstypes.VpcEndpointDetail) { + if out == nil { + return + } + + rd.ID = flex.StringToFramework(ctx, out.Id) + rd.Name = flex.StringToFramework(ctx, out.Name) + rd.SecurityGroupIds = flex.FlattenFrameworkStringValueSet(ctx, out.SecurityGroupIds) + rd.SubnetIds = flex.FlattenFrameworkStringValueSet(ctx, out.SubnetIds) + rd.VpcId = flex.StringToFramework(ctx, out.VpcId) +} + +func waitVPCEndpointCreated(ctx context.Context, conn *opensearchserverless.Client, id string, timeout time.Duration) (*awstypes.VpcEndpointDetail, error) { + stateConf := &retry.StateChangeConf{ + Pending: enum.Slice(awstypes.VpcEndpointStatusPending), + Target: enum.Slice(awstypes.VpcEndpointStatusActive), + Refresh: statusVPCEndpoint(ctx, conn, id), + Timeout: timeout, + NotFoundChecks: 20, + ContinuousTargetOccurence: 2, + } + + outputRaw, err := stateConf.WaitForStateContext(ctx) + if out, ok := outputRaw.(*awstypes.VpcEndpointDetail); ok { + return out, err + } + + return nil, err +} + +func waitVPCEndpointUpdated(ctx context.Context, conn *opensearchserverless.Client, id string, timeout time.Duration) (*awstypes.VpcEndpointDetail, error) { + stateConf := &retry.StateChangeConf{ + Pending: enum.Slice(awstypes.VpcEndpointStatusPending), + Target: enum.Slice(awstypes.VpcEndpointStatusActive), + Refresh: statusVPCEndpoint(ctx, conn, id), + Timeout: timeout, + NotFoundChecks: 20, + ContinuousTargetOccurence: 2, + } + + outputRaw, err := stateConf.WaitForStateContext(ctx) + if out, ok := outputRaw.(*awstypes.VpcEndpointDetail); ok { + return out, err + } + + return nil, err +} + +func waitVPCEndpointDeleted(ctx context.Context, conn *opensearchserverless.Client, id string, timeout time.Duration) (*awstypes.VpcEndpointDetail, error) { + stateConf := &retry.StateChangeConf{ + Pending: enum.Slice(awstypes.VpcEndpointStatusDeleting, awstypes.VpcEndpointStatusActive), + Target: []string{}, + Refresh: statusVPCEndpoint(ctx, conn, id), + Timeout: timeout, + } + + outputRaw, err := stateConf.WaitForStateContext(ctx) + if out, ok := outputRaw.(*awstypes.VpcEndpointDetail); ok { + return out, err + } + + return nil, err +} + +func statusVPCEndpoint(ctx context.Context, conn *opensearchserverless.Client, id string) retry.StateRefreshFunc { + return func() (interface{}, string, error) { + out, err := FindVPCEndpointByID(ctx, conn, id) + if tfresource.NotFound(err) { + return nil, "", nil + } + + if err != nil { + return nil, "", err + } + + return out, string(out.Status), nil + } +} diff --git a/internal/service/opensearchserverless/vpc_endpoint_test.go b/internal/service/opensearchserverless/vpc_endpoint_test.go new file mode 100644 index 000000000000..fe2abe78dff9 --- /dev/null +++ b/internal/service/opensearchserverless/vpc_endpoint_test.go @@ -0,0 +1,361 @@ +package opensearchserverless_test + +import ( + "context" + "errors" + "fmt" + "testing" + + "github.com/aws/aws-sdk-go-v2/aws" + "github.com/aws/aws-sdk-go-v2/service/opensearchserverless" + "github.com/aws/aws-sdk-go-v2/service/opensearchserverless/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" + tfopensearchserverless "github.com/hashicorp/terraform-provider-aws/internal/service/opensearchserverless" + "github.com/hashicorp/terraform-provider-aws/internal/tfresource" + "github.com/hashicorp/terraform-provider-aws/names" +) + +func TestAccOpenSearchServerlessVPCEndpoint_basic(t *testing.T) { + if testing.Short() { + t.Skip("skipping long-running test in short mode") + } + ctx := acctest.Context(t) + var vpcendpoint types.VpcEndpointDetail + rName := sdkacctest.RandomWithPrefix(acctest.ResourcePrefix) + resourceName := "aws_opensearchserverless_vpc_endpoint.test" + + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { + acctest.PreCheck(ctx, t) + acctest.PreCheckPartitionHasService(t, names.OpenSearchServerlessEndpointID) + testAccPreCheckVPCEndpoint(ctx, t) + }, + ErrorCheck: acctest.ErrorCheck(t, names.OpenSearchServerlessEndpointID), + ProtoV5ProviderFactories: acctest.ProtoV5ProviderFactories, + CheckDestroy: testAccCheckVPCEndpointDestroy(ctx), + Steps: []resource.TestStep{ + { + Config: testAccVPCEndpointConfig_basic(rName), + Check: resource.ComposeTestCheckFunc( + testAccCheckVPCEndpointExists(ctx, resourceName, &vpcendpoint), + resource.TestCheckResourceAttr(resourceName, "subnet_ids.#", "1"), + resource.TestCheckResourceAttr(resourceName, "security_group_ids.#", "1"), + ), + }, + { + ResourceName: resourceName, + ImportState: true, + ImportStateVerify: true, + }, + }, + }) +} + +func TestAccOpenSearchServerlessVPCEndpoint_securityGroups(t *testing.T) { + if testing.Short() { + t.Skip("skipping long-running test in short mode") + } + ctx := acctest.Context(t) + var vpcendpoint1, vpcendpoint2, vpcendpoint3 types.VpcEndpointDetail + rName := sdkacctest.RandomWithPrefix(acctest.ResourcePrefix) + resourceName := "aws_opensearchserverless_vpc_endpoint.test" + + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { + acctest.PreCheck(ctx, t) + acctest.PreCheckPartitionHasService(t, names.OpenSearchServerlessEndpointID) + testAccPreCheckVPCEndpoint(ctx, t) + }, + ErrorCheck: acctest.ErrorCheck(t, names.OpenSearchServerlessEndpointID), + ProtoV5ProviderFactories: acctest.ProtoV5ProviderFactories, + CheckDestroy: testAccCheckVPCEndpointDestroy(ctx), + Steps: []resource.TestStep{ + { + Config: testAccVPCEndpointConfig_basic(rName), + Check: resource.ComposeTestCheckFunc( + testAccCheckVPCEndpointExists(ctx, resourceName, &vpcendpoint1), + resource.TestCheckResourceAttr(resourceName, "security_group_ids.#", "1"), + ), + }, + { + Config: testAccVPCEndpointConfig_multiple_securityGroups(rName), + Check: resource.ComposeTestCheckFunc( + testAccCheckVPCEndpointExists(ctx, resourceName, &vpcendpoint2), + testAccCheckVPCEndpointNotRecreated(&vpcendpoint1, &vpcendpoint2), + resource.TestCheckResourceAttr(resourceName, "security_group_ids.#", "2"), + ), + }, + { + Config: testAccVPCEndpointConfig_single_securityGroup(rName), + Check: resource.ComposeTestCheckFunc( + testAccCheckVPCEndpointExists(ctx, resourceName, &vpcendpoint3), + testAccCheckVPCEndpointNotRecreated(&vpcendpoint1, &vpcendpoint3), + resource.TestCheckResourceAttr(resourceName, "security_group_ids.#", "1"), + ), + }, + }, + }) +} + +func TestAccOpenSearchServerlessVPCEndpoint_update(t *testing.T) { + if testing.Short() { + t.Skip("skipping long-running test in short mode") + } + ctx := acctest.Context(t) + var vpcendpoint1, vpcendpoint2, vpcendpoint3 types.VpcEndpointDetail + rName := sdkacctest.RandomWithPrefix(acctest.ResourcePrefix) + resourceName := "aws_opensearchserverless_vpc_endpoint.test" + + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { + acctest.PreCheck(ctx, t) + acctest.PreCheckPartitionHasService(t, names.OpenSearchServerlessEndpointID) + testAccPreCheckVPCEndpoint(ctx, t) + }, + ErrorCheck: acctest.ErrorCheck(t, names.OpenSearchServerlessEndpointID), + ProtoV5ProviderFactories: acctest.ProtoV5ProviderFactories, + CheckDestroy: testAccCheckVPCEndpointDestroy(ctx), + Steps: []resource.TestStep{ + { + Config: testAccVPCEndpointConfig_basic(rName), + Check: resource.ComposeTestCheckFunc( + testAccCheckVPCEndpointExists(ctx, resourceName, &vpcendpoint1), + resource.TestCheckResourceAttr(resourceName, "subnet_ids.#", "1"), + resource.TestCheckResourceAttr(resourceName, "security_group_ids.#", "1"), + ), + }, + { + Config: testAccVPCEndpointConfig_multiple_subnets(rName), + Check: resource.ComposeTestCheckFunc( + testAccCheckVPCEndpointExists(ctx, resourceName, &vpcendpoint2), + testAccCheckVPCEndpointNotRecreated(&vpcendpoint1, &vpcendpoint2), + resource.TestCheckResourceAttr(resourceName, "subnet_ids.#", "2"), + resource.TestCheckResourceAttr(resourceName, "security_group_ids.#", "1"), + ), + }, + { + Config: testAccVPCEndpointConfig_basic(rName), + Check: resource.ComposeTestCheckFunc( + testAccCheckVPCEndpointExists(ctx, resourceName, &vpcendpoint3), + testAccCheckVPCEndpointNotRecreated(&vpcendpoint2, &vpcendpoint3), + resource.TestCheckResourceAttr(resourceName, "subnet_ids.#", "1"), + resource.TestCheckResourceAttr(resourceName, "security_group_ids.#", "1"), + ), + }, + { + ResourceName: resourceName, + ImportState: true, + ImportStateVerify: true, + }, + }, + }) +} + +func TestAccOpenSearchServerlessVPCEndpoint_disappears(t *testing.T) { + if testing.Short() { + t.Skip("skipping long-running test in short mode") + } + ctx := acctest.Context(t) + var vpcendpoint types.VpcEndpointDetail + rName := sdkacctest.RandomWithPrefix(acctest.ResourcePrefix) + resourceName := "aws_opensearchserverless_vpc_endpoint.test" + + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { + acctest.PreCheck(ctx, t) + acctest.PreCheckPartitionHasService(t, names.OpenSearchServerlessEndpointID) + testAccPreCheckVPCEndpoint(ctx, t) + }, + ErrorCheck: acctest.ErrorCheck(t, names.OpenSearchServerlessEndpointID), + ProtoV5ProviderFactories: acctest.ProtoV5ProviderFactories, + CheckDestroy: testAccCheckVPCEndpointDestroy(ctx), + Steps: []resource.TestStep{ + { + Config: testAccVPCEndpointConfig_basic(rName), + Check: resource.ComposeTestCheckFunc( + testAccCheckVPCEndpointExists(ctx, resourceName, &vpcendpoint), + acctest.CheckFrameworkResourceDisappears(ctx, acctest.Provider, tfopensearchserverless.ResourceVPCEndpoint, resourceName), + ), + ExpectNonEmptyPlan: true, + }, + }, + }) +} + +func testAccCheckVPCEndpointDestroy(ctx context.Context) resource.TestCheckFunc { + return func(s *terraform.State) error { + conn := acctest.Provider.Meta().(*conns.AWSClient).OpenSearchServerlessClient(ctx) + + for _, rs := range s.RootModule().Resources { + if rs.Type != "aws_opensearchserverless_vpc_endpointa" { + continue + } + + _, err := tfopensearchserverless.FindVPCEndpointByID(ctx, conn, rs.Primary.ID) + + if tfresource.NotFound(err) { + continue + } + + if err != nil { + return err + } + + return create.Error(names.OpenSearchServerless, create.ErrActionCheckingDestroyed, tfopensearchserverless.ResNameVPCEndpoint, rs.Primary.ID, errors.New("not destroyed")) + } + + return nil + } +} + +func testAccCheckVPCEndpointExists(ctx context.Context, name string, vpcendpoint *types.VpcEndpointDetail) resource.TestCheckFunc { + return func(s *terraform.State) error { + rs, ok := s.RootModule().Resources[name] + if !ok { + return create.Error(names.OpenSearchServerless, create.ErrActionCheckingExistence, tfopensearchserverless.ResNameVPCEndpoint, name, errors.New("not found")) + } + + if rs.Primary.ID == "" { + return create.Error(names.OpenSearchServerless, create.ErrActionCheckingExistence, tfopensearchserverless.ResNameVPCEndpoint, name, errors.New("not set")) + } + + conn := acctest.Provider.Meta().(*conns.AWSClient).OpenSearchServerlessClient(ctx) + resp, err := tfopensearchserverless.FindVPCEndpointByID(ctx, conn, rs.Primary.ID) + + if err != nil { + return create.Error(names.OpenSearchServerless, create.ErrActionCheckingExistence, tfopensearchserverless.ResNameVPCEndpoint, rs.Primary.ID, err) + } + + *vpcendpoint = *resp + + return nil + } +} + +func testAccPreCheckVPCEndpoint(ctx context.Context, t *testing.T) { + conn := acctest.Provider.Meta().(*conns.AWSClient).OpenSearchServerlessClient(ctx) + + input := &opensearchserverless.ListVpcEndpointsInput{} + _, err := conn.ListVpcEndpoints(ctx, input) + + if acctest.PreCheckSkipError(err) { + t.Skipf("skipping acceptance testing: %s", err) + } + + if err != nil { + t.Fatalf("unexpected PreCheck error: %s", err) + } +} + +func testAccCheckVPCEndpointNotRecreated(before, after *types.VpcEndpointDetail) resource.TestCheckFunc { + return func(s *terraform.State) error { + if before, after := aws.ToString(before.Id), aws.ToString(after.Id); before != after { + return create.Error(names.OpenSearchServerless, create.ErrActionCheckingNotRecreated, tfopensearchserverless.ResNameVPCEndpoint, before, errors.New("recreated")) + } + + return nil + } +} + +func testAccVPCEndpointConfig_networkingBase(rName string, subnetCount int) string { + return acctest.ConfigCompose( + acctest.ConfigAvailableAZsNoOptInDefaultExclude(), + fmt.Sprintf(` +resource "aws_vpc" "test" { + cidr_block = "10.0.0.0/16" + enable_dns_hostnames = true + + tags = { + Name = %[1]q + } +} + +resource "aws_subnet" "test" { + count = %[2]d + + vpc_id = aws_vpc.test.id + availability_zone = data.aws_availability_zones.available.names[count.index] + cidr_block = cidrsubnet(aws_vpc.test.cidr_block, 8, count.index) + + tags = { + Name = %[1]q + } +} +`, rName, subnetCount), + ) +} + +func testAccVPCEndpointConfig_securityGroupBase(rName string, sgCount int) string { + return acctest.ConfigCompose( + fmt.Sprintf(` +resource "aws_security_group" "test" { + count = %[2]d + name = "%[1]s-${count.index}" + vpc_id = aws_vpc.test.id + + tags = { + Name = %[1]q + } +} +`, rName, sgCount), + ) +} + +func testAccVPCEndpointConfig_basic(rName string) string { + return acctest.ConfigCompose( + testAccVPCEndpointConfig_networkingBase(rName, 2), + fmt.Sprintf(` +resource "aws_opensearchserverless_vpc_endpoint" "test" { + name = %[1]q + subnet_ids = [aws_subnet.test[0].id] + vpc_id = aws_vpc.test.id +} +`, rName)) +} + +func testAccVPCEndpointConfig_multiple_subnets(rName string) string { + return acctest.ConfigCompose( + testAccVPCEndpointConfig_networkingBase(rName, 2), + fmt.Sprintf(` +resource "aws_opensearchserverless_vpc_endpoint" "test" { + name = %[1]q + subnet_ids = aws_subnet.test[*].id + vpc_id = aws_vpc.test.id +} +`, rName)) +} + +func testAccVPCEndpointConfig_multiple_securityGroups(rName string) string { + return acctest.ConfigCompose( + testAccVPCEndpointConfig_networkingBase(rName, 2), + testAccVPCEndpointConfig_securityGroupBase(rName, 2), + fmt.Sprintf(` +resource "aws_opensearchserverless_vpc_endpoint" "test" { + name = %[1]q + subnet_ids = aws_subnet.test[*].id + vpc_id = aws_vpc.test.id + + security_group_ids = aws_security_group.test[*].id +} +`, rName)) +} + +func testAccVPCEndpointConfig_single_securityGroup(rName string) string { + return acctest.ConfigCompose( + testAccVPCEndpointConfig_networkingBase(rName, 2), + testAccVPCEndpointConfig_securityGroupBase(rName, 2), + fmt.Sprintf(` +resource "aws_opensearchserverless_vpc_endpoint" "test" { + name = %[1]q + subnet_ids = aws_subnet.test[*].id + vpc_id = aws_vpc.test.id + + security_group_ids = [aws_security_group.test[0].id] +} +`, rName)) +} diff --git a/website/docs/r/opensearchserverless_vpc_endpoint.html.markdown b/website/docs/r/opensearchserverless_vpc_endpoint.html.markdown new file mode 100644 index 000000000000..577be166e362 --- /dev/null +++ b/website/docs/r/opensearchserverless_vpc_endpoint.html.markdown @@ -0,0 +1,57 @@ +--- +subcategory: "OpenSearch Serverless" +layout: "aws" +page_title: "AWS: aws_opensearchserverless_vpc_endpoint" +description: |- + Terraform resource for managing an AWS OpenSearch Serverless VPC Endpoint. +--- + +# Resource: aws_opensearchserverless_vpc_endpoint + +Terraform resource for managing an AWS OpenSearchServerless VPC Endpoint. + +## Example Usage + +### Basic Usage + +```terraform +resource "aws_opensearchserverless_vpc_endpoint" "example" { + name = "myendpoint" + subnet_ids = [aws_subnet.example.id] + vpc_id = aws_vpc.example.id +} +``` + +## Argument Reference + +The following arguments are required: + +* `name` - (Required) Name of the interface endpoint. +* `subnet_ids` - (Required) One or more subnet IDs from which you'll access OpenSearch Serverless. Up to 6 subnets can be provided. +* `vpc_id` - (Required) ID of the VPC from which you'll access OpenSearch Serverless. + +The following arguments are optional: + +* `security_group_ids` - (Optional) One or more security groups that define the ports, protocols, and sources for inbound traffic that you are authorizing into your endpoint. Up to 5 security groups can be provided. + +## Attributes Reference + +In addition to all arguments above, the following attributes are exported: + +* `id` - Unique identified of the Vpc Endpoint. + +## Timeouts + +[Configuration options](https://developer.hashicorp.com/terraform/language/resources/syntax#operation-timeouts): + +* `create` - (Default `30m`) +* `update` - (Default `30m`) +* `delete` - (Default `30m`) + +## Import + +OpenSearchServerless Vpc Endpointa can be imported using the `id`, e.g., + +``` +$ terraform import aws_opensearchserverless_vpc_endpoint.example vpce-8012925589 +```