diff --git a/.changelog/31091.txt b/.changelog/31091.txt new file mode 100644 index 000000000000..29ab877f12bd --- /dev/null +++ b/.changelog/31091.txt @@ -0,0 +1,3 @@ +```release-note:new-resource +aws_opensearchserverless_collection +``` diff --git a/internal/service/opensearchserverless/collection.go b/internal/service/opensearchserverless/collection.go new file mode 100644 index 000000000000..42620fae9127 --- /dev/null +++ b/internal/service/opensearchserverless/collection.go @@ -0,0 +1,354 @@ +package opensearchserverless + +import ( + "context" + "errors" + "regexp" + "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/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/stringplanmodifier" + "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" + tftags "github.com/hashicorp/terraform-provider-aws/internal/tags" + "github.com/hashicorp/terraform-provider-aws/internal/tfresource" + "github.com/hashicorp/terraform-provider-aws/names" +) + +// @FrameworkResource(name="Collection") +// @Tags(identifierAttribute="arn") +func newResourceCollection(_ context.Context) (resource.ResourceWithConfigure, error) { + r := resourceCollection{} + r.SetDefaultCreateTimeout(20 * time.Minute) + r.SetDefaultDeleteTimeout(20 * time.Minute) + + return &r, nil +} + +type resourceCollectionData struct { + ARN types.String `tfsdk:"arn"` + CollectionEndpoint types.String `tfsdk:"collection_endpoint"` + DashboardEndpoint types.String `tfsdk:"dashboard_endpoint"` + Description types.String `tfsdk:"description"` + ID types.String `tfsdk:"id"` + KmsKeyARN types.String `tfsdk:"kms_key_arn"` + Name types.String `tfsdk:"name"` + Tags types.Map `tfsdk:"tags"` + TagsAll types.Map `tfsdk:"tags_all"` + Timeouts timeouts.Value `tfsdk:"timeouts"` + Type types.String `tfsdk:"type"` +} + +const ( + ResNameCollection = "Collection" +) + +type resourceCollection struct { + framework.ResourceWithConfigure + framework.WithTimeouts +} + +func (r *resourceCollection) Metadata(_ context.Context, request resource.MetadataRequest, response *resource.MetadataResponse) { + response.TypeName = "aws_opensearchserverless_collection" +} + +func (r *resourceCollection) Schema(ctx context.Context, req resource.SchemaRequest, resp *resource.SchemaResponse) { + resp.Schema = schema.Schema{ + Attributes: map[string]schema.Attribute{ + "arn": framework.ARNAttributeComputedOnly(), + "collection_endpoint": schema.StringAttribute{ + Computed: true, + PlanModifiers: []planmodifier.String{ + stringplanmodifier.UseStateForUnknown(), + }, + }, + "dashboard_endpoint": schema.StringAttribute{ + Computed: true, + PlanModifiers: []planmodifier.String{ + stringplanmodifier.UseStateForUnknown(), + }, + }, + "description": schema.StringAttribute{ + Optional: true, + Validators: []validator.String{ + stringvalidator.LengthBetween(0, 1000), + }, + }, + "id": framework.IDAttribute(), + "kms_key_arn": schema.StringAttribute{ + Computed: true, + PlanModifiers: []planmodifier.String{ + stringplanmodifier.UseStateForUnknown(), + }, + }, + "name": schema.StringAttribute{ + Required: true, + PlanModifiers: []planmodifier.String{ + stringplanmodifier.RequiresReplace(), + }, + Validators: []validator.String{ + stringvalidator.LengthBetween(3, 32), + stringvalidator.RegexMatches(regexp.MustCompile(`^[a-z][a-z0-9-]+$`), + `must start with any lower case letter and can can include any lower case letter, number, or "-"`), + }, + }, + names.AttrTags: tftags.TagsAttribute(), + names.AttrTagsAll: tftags.TagsAttributeComputedOnly(), + "type": schema.StringAttribute{ + Optional: true, + Computed: true, + PlanModifiers: []planmodifier.String{ + stringplanmodifier.RequiresReplace(), + stringplanmodifier.UseStateForUnknown(), + }, + Validators: []validator.String{ + enum.FrameworkValidate[awstypes.CollectionType](), + }, + }, + }, + Blocks: map[string]schema.Block{ + "timeouts": timeouts.Block(ctx, timeouts.Opts{ + Create: true, + Delete: true, + }), + }, + } +} + +func (r *resourceCollection) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) { + var plan resourceCollectionData + + resp.Diagnostics.Append(req.Plan.Get(ctx, &plan)...) + + if resp.Diagnostics.HasError() { + return + } + + conn := r.Meta().OpenSearchServerlessClient(ctx) + + in := &opensearchserverless.CreateCollectionInput{ + ClientToken: aws.String(id.UniqueId()), + Name: aws.String(plan.Name.ValueString()), + Tags: GetTagsIn(ctx), + } + + if !plan.Description.IsNull() { + in.Description = aws.String(plan.Description.ValueString()) + } + + if !plan.Type.IsNull() { + in.Type = awstypes.CollectionType(plan.Type.ValueString()) + } + + out, err := conn.CreateCollection(ctx, in) + if err != nil { + resp.Diagnostics.AddError( + create.ProblemStandardMessage(names.OpenSearchServerless, create.ErrActionCreating, ResNameCollection, plan.Name.ValueString(), nil), + err.Error(), + ) + return + } + + state := plan + state.ID = flex.StringToFramework(ctx, out.CreateCollectionDetail.Id) + + createTimeout := r.CreateTimeout(ctx, plan.Timeouts) + waitOut, err := waitCollectionCreated(ctx, conn, aws.ToString(out.CreateCollectionDetail.Id), createTimeout) + + if err != nil { + resp.Diagnostics.AddError( + create.ProblemStandardMessage(names.OpenSearchServerless, create.ErrActionWaitingForCreation, ResNameCollection, plan.Name.ValueString(), err), + err.Error(), + ) + return + } + + state.ARN = flex.StringToFramework(ctx, waitOut.Arn) + state.CollectionEndpoint = flex.StringToFramework(ctx, waitOut.CollectionEndpoint) + state.DashboardEndpoint = flex.StringToFramework(ctx, waitOut.DashboardEndpoint) + state.Description = flex.StringToFramework(ctx, waitOut.Description) + state.KmsKeyARN = flex.StringToFramework(ctx, waitOut.KmsKeyArn) + state.Name = flex.StringToFramework(ctx, waitOut.Name) + state.Type = flex.StringValueToFramework(ctx, waitOut.Type) + + resp.Diagnostics.Append(resp.State.Set(ctx, &state)...) +} + +func (r *resourceCollection) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) { + conn := r.Meta().OpenSearchServerlessClient(ctx) + + var state resourceCollectionData + resp.Diagnostics.Append(req.State.Get(ctx, &state)...) + if resp.Diagnostics.HasError() { + return + } + + out, err := FindCollectionByID(ctx, conn, state.ID.ValueString()) + if tfresource.NotFound(err) { + resp.Diagnostics.Append(fwdiag.NewResourceNotFoundWarningDiagnostic(err)) + resp.State.RemoveResource(ctx) + return + } + + state.ARN = flex.StringToFramework(ctx, out.Arn) + state.CollectionEndpoint = flex.StringToFramework(ctx, out.CollectionEndpoint) + state.DashboardEndpoint = flex.StringToFramework(ctx, out.DashboardEndpoint) + state.Description = flex.StringToFramework(ctx, out.Description) + state.ID = flex.StringToFramework(ctx, out.Id) + state.KmsKeyARN = flex.StringToFramework(ctx, out.KmsKeyArn) + state.Name = flex.StringToFramework(ctx, out.Name) + state.Type = flex.StringValueToFramework(ctx, out.Type) + + resp.Diagnostics.Append(resp.State.Set(ctx, &state)...) +} + +func (r *resourceCollection) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) { + conn := r.Meta().OpenSearchServerlessClient(ctx) + + var plan, state resourceCollectionData + resp.Diagnostics.Append(req.State.Get(ctx, &state)...) + resp.Diagnostics.Append(req.Plan.Get(ctx, &plan)...) + if resp.Diagnostics.HasError() { + return + } + + if !plan.Description.Equal(state.Description) { + input := &opensearchserverless.UpdateCollectionInput{ + ClientToken: aws.String(id.UniqueId()), + Id: flex.StringFromFramework(ctx, plan.ID), + Description: flex.StringFromFramework(ctx, plan.Description), + } + + out, err := conn.UpdateCollection(ctx, input) + + if err != nil { + resp.Diagnostics.AddError( + create.ProblemStandardMessage(names.OpenSearchServerless, create.ErrActionUpdating, ResNameCollection, state.ID.ValueString(), err), + err.Error(), + ) + return + } + + plan.ARN = flex.StringToFramework(ctx, out.UpdateCollectionDetail.Arn) + plan.Description = flex.StringToFramework(ctx, out.UpdateCollectionDetail.Description) + plan.ID = flex.StringToFramework(ctx, out.UpdateCollectionDetail.Id) + plan.Name = flex.StringToFramework(ctx, out.UpdateCollectionDetail.Name) + plan.Type = flex.StringValueToFramework(ctx, out.UpdateCollectionDetail.Type) + } + + resp.Diagnostics.Append(resp.State.Set(ctx, &plan)...) +} + +func (r *resourceCollection) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) { + conn := r.Meta().OpenSearchServerlessClient(ctx) + + var state resourceCollectionData + resp.Diagnostics.Append(req.State.Get(ctx, &state)...) + if resp.Diagnostics.HasError() { + return + } + + _, err := conn.DeleteCollection(ctx, &opensearchserverless.DeleteCollectionInput{ + 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, ResNameCollection, state.Name.ValueString(), nil), + err.Error(), + ) + } + + deleteTimeout := r.DeleteTimeout(ctx, state.Timeouts) + _, err = waitCollectionDeleted(ctx, conn, state.ID.ValueString(), deleteTimeout) + + if err != nil { + resp.Diagnostics.AddError( + create.ProblemStandardMessage(names.OpenSearchServerless, create.ErrActionWaitingForCreation, ResNameCollection, state.Name.ValueString(), err), + err.Error(), + ) + return + } +} + +func (r *resourceCollection) ModifyPlan(ctx context.Context, req resource.ModifyPlanRequest, resp *resource.ModifyPlanResponse) { + r.SetTagsAll(ctx, req, resp) +} + +func (r *resourceCollection) ImportState(ctx context.Context, req resource.ImportStateRequest, resp *resource.ImportStateResponse) { + resource.ImportStatePassthroughID(ctx, path.Root("id"), req, resp) +} + +func waitCollectionCreated(ctx context.Context, conn *opensearchserverless.Client, id string, timeout time.Duration) (*awstypes.CollectionDetail, error) { + stateConf := &retry.StateChangeConf{ + Pending: enum.Slice(awstypes.CollectionStatusCreating), + Target: enum.Slice(awstypes.CollectionStatusActive), + Refresh: statusCollection(ctx, conn, id), + Timeout: timeout, + MinTimeout: 10 * time.Second, + Delay: 30 * time.Second, + } + + outputRaw, err := stateConf.WaitForStateContext(ctx) + + if output, ok := outputRaw.(*awstypes.CollectionDetail); ok { + return output, err + } + + return nil, err +} + +func waitCollectionDeleted(ctx context.Context, conn *opensearchserverless.Client, id string, timeout time.Duration) (*awstypes.CollectionDetail, error) { + stateConf := &retry.StateChangeConf{ + Pending: enum.Slice(awstypes.CollectionStatusDeleting), + Target: []string{}, + Refresh: statusCollection(ctx, conn, id), + Timeout: timeout, + MinTimeout: 10 * time.Second, + Delay: 30 * time.Second, + } + + outputRaw, err := stateConf.WaitForStateContext(ctx) + + if output, ok := outputRaw.(*awstypes.CollectionDetail); ok { + return output, err + } + + return nil, err +} + +func statusCollection(ctx context.Context, conn *opensearchserverless.Client, id string) retry.StateRefreshFunc { + return func() (interface{}, string, error) { + output, err := FindCollectionByID(ctx, conn, id) + + if tfresource.NotFound(err) { + return nil, "", nil + } + + if err != nil { + return nil, "", err + } + + return output, string(output.Status), nil + } +} diff --git a/internal/service/opensearchserverless/collection_test.go b/internal/service/opensearchserverless/collection_test.go new file mode 100644 index 000000000000..a81354c199e5 --- /dev/null +++ b/internal/service/opensearchserverless/collection_test.go @@ -0,0 +1,312 @@ +package opensearchserverless_test + +import ( + "context" + "errors" + "fmt" + "testing" + + "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-sdk/v2/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 TestAccOpenSearchServerlessCollection_basic(t *testing.T) { + ctx := acctest.Context(t) + var collection types.CollectionDetail + rName := sdkacctest.RandomWithPrefix(acctest.ResourcePrefix) + resourceName := "aws_opensearchserverless_collection.test" + + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { + acctest.PreCheck(ctx, t) + acctest.PreCheckPartitionHasService(t, names.OpenSearchServerlessEndpointID) + testAccPreCheckCollection(ctx, t) + }, + ErrorCheck: acctest.ErrorCheck(t, names.OpenSearchServerlessEndpointID), + ProtoV5ProviderFactories: acctest.ProtoV5ProviderFactories, + CheckDestroy: testAccCheckCollectionDestroy(ctx), + Steps: []resource.TestStep{ + { + Config: testAccCollectionConfig_basic(rName), + Check: resource.ComposeTestCheckFunc( + testAccCheckCollectionExists(ctx, resourceName, &collection), + resource.TestCheckResourceAttrSet(resourceName, "type"), + resource.TestCheckResourceAttrSet(resourceName, "collection_endpoint"), + resource.TestCheckResourceAttrSet(resourceName, "dashboard_endpoint"), + resource.TestCheckResourceAttrSet(resourceName, "kms_key_arn"), + ), + }, + { + ResourceName: resourceName, + ImportState: true, + ImportStateVerify: true, + }, + }, + }) +} + +func TestAccOpenSearchServerlessCollection_tags(t *testing.T) { + ctx := acctest.Context(t) + var collection types.CollectionDetail + rName := sdkacctest.RandomWithPrefix(acctest.ResourcePrefix) + resourceName := "aws_opensearchserverless_collection.test" + + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { + acctest.PreCheck(ctx, t) + acctest.PreCheckPartitionHasService(t, names.OpenSearchServerlessEndpointID) + testAccPreCheckCollection(ctx, t) + }, + ErrorCheck: acctest.ErrorCheck(t, names.OpenSearchServerlessEndpointID), + ProtoV5ProviderFactories: acctest.ProtoV5ProviderFactories, + CheckDestroy: testAccCheckCollectionDestroy(ctx), + Steps: []resource.TestStep{ + { + Config: testAccCollectionConfig_tags1(rName, "key1", "value1"), + Check: resource.ComposeTestCheckFunc( + testAccCheckCollectionExists(ctx, resourceName, &collection), + resource.TestCheckResourceAttr(resourceName, "tags.%", "1"), + resource.TestCheckResourceAttr(resourceName, "tags.key1", "value1"), + ), + }, + { + Config: testAccCollectionConfig_tags2(rName, "key1", "value1", "key2", "value2"), + Check: resource.ComposeTestCheckFunc( + testAccCheckCollectionExists(ctx, resourceName, &collection), + resource.TestCheckResourceAttr(resourceName, "tags.%", "2"), + resource.TestCheckResourceAttr(resourceName, "tags.key1", "value1"), + resource.TestCheckResourceAttr(resourceName, "tags.key2", "value2"), + ), + }, + { + Config: testAccCollectionConfig_tags1(rName, "key2", "value2"), + Check: resource.ComposeTestCheckFunc( + testAccCheckCollectionExists(ctx, resourceName, &collection), + resource.TestCheckResourceAttr(resourceName, "tags.%", "1"), + resource.TestCheckResourceAttr(resourceName, "tags.key2", "value2"), + ), + }, + }, + }) +} + +func TestAccOpenSearchServerlessCollection_update(t *testing.T) { + ctx := acctest.Context(t) + var collection types.CollectionDetail + rName := sdkacctest.RandomWithPrefix(acctest.ResourcePrefix) + resourceName := "aws_opensearchserverless_collection.test" + + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { + acctest.PreCheck(ctx, t) + acctest.PreCheckPartitionHasService(t, names.OpenSearchServerlessEndpointID) + testAccPreCheckCollection(ctx, t) + }, + ErrorCheck: acctest.ErrorCheck(t, names.OpenSearchServerlessEndpointID), + ProtoV5ProviderFactories: acctest.ProtoV5ProviderFactories, + CheckDestroy: testAccCheckCollectionDestroy(ctx), + Steps: []resource.TestStep{ + { + Config: testAccCollectionConfig_update(rName, "description"), + Check: resource.ComposeTestCheckFunc( + testAccCheckCollectionExists(ctx, resourceName, &collection), + resource.TestCheckResourceAttrSet(resourceName, "type"), + resource.TestCheckResourceAttr(resourceName, "description", "description"), + ), + }, + { + Config: testAccCollectionConfig_update(rName, "description updated"), + Check: resource.ComposeTestCheckFunc( + testAccCheckCollectionExists(ctx, resourceName, &collection), + resource.TestCheckResourceAttrSet(resourceName, "type"), + resource.TestCheckResourceAttr(resourceName, "description", "description updated"), + ), + }, + }, + }) +} + +func TestAccOpenSearchServerlessCollection_disappears(t *testing.T) { + ctx := acctest.Context(t) + + var collection types.CollectionDetail + rName := sdkacctest.RandomWithPrefix(acctest.ResourcePrefix) + resourceName := "aws_opensearchserverless_collection.test" + + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { + acctest.PreCheck(ctx, t) + acctest.PreCheckPartitionHasService(t, names.OpenSearchServerlessEndpointID) + testAccPreCheckCollection(ctx, t) + }, + ErrorCheck: acctest.ErrorCheck(t, names.OpenSearchServerlessEndpointID), + ProtoV5ProviderFactories: acctest.ProtoV5ProviderFactories, + CheckDestroy: testAccCheckCollectionDestroy(ctx), + Steps: []resource.TestStep{ + { + Config: testAccCollectionConfig_basic(rName), + Check: resource.ComposeTestCheckFunc( + testAccCheckCollectionExists(ctx, resourceName, &collection), + acctest.CheckFrameworkResourceDisappears(ctx, acctest.Provider, tfopensearchserverless.ResourceCollection, resourceName), + ), + ExpectNonEmptyPlan: true, + }, + }, + }) +} + +func testAccCheckCollectionDestroy(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_collection" { + continue + } + + _, err := tfopensearchserverless.FindCollectionByID(ctx, conn, rs.Primary.ID) + + if tfresource.NotFound(err) { + continue + } + + if err != nil { + return err + } + + return create.Error(names.OpenSearchServerless, create.ErrActionCheckingDestroyed, tfopensearchserverless.ResNameCollection, rs.Primary.ID, errors.New("not destroyed")) + } + + return nil + } +} + +func testAccCheckCollectionExists(ctx context.Context, name string, collection *types.CollectionDetail) resource.TestCheckFunc { + return func(s *terraform.State) error { + rs, ok := s.RootModule().Resources[name] + if !ok { + return create.Error(names.OpenSearchServerless, create.ErrActionCheckingExistence, tfopensearchserverless.ResNameCollection, name, errors.New("not found")) + } + + if rs.Primary.ID == "" { + return create.Error(names.OpenSearchServerless, create.ErrActionCheckingExistence, tfopensearchserverless.ResNameCollection, name, errors.New("not set")) + } + + conn := acctest.Provider.Meta().(*conns.AWSClient).OpenSearchServerlessClient(ctx) + resp, err := tfopensearchserverless.FindCollectionByID(ctx, conn, rs.Primary.ID) + + if err != nil { + return create.Error(names.OpenSearchServerless, create.ErrActionCheckingExistence, tfopensearchserverless.ResNameCollection, rs.Primary.ID, err) + } + + *collection = *resp + + return nil + } +} + +func testAccPreCheckCollection(ctx context.Context, t *testing.T) { + conn := acctest.Provider.Meta().(*conns.AWSClient).OpenSearchServerlessClient(ctx) + + input := &opensearchserverless.ListCollectionsInput{} + _, err := conn.ListCollections(ctx, input) + + if acctest.PreCheckSkipError(err) { + t.Skipf("skipping acceptance testing: %s", err) + } + + if err != nil { + t.Fatalf("unexpected PreCheck error: %s", err) + } +} + +func testAccCollectionBaseConfig(rName string) string { + return fmt.Sprintf(` +resource "aws_opensearchserverless_security_policy" "test" { + name = %[1]q + type = "encryption" + policy = jsonencode({ + "Rules" = [ + { + "Resource" = [ + "collection/%[1]s" + ], + "ResourceType" = "collection" + } + ], + "AWSOwnedKey" = true + }) +} +`, rName) +} + +func testAccCollectionConfig_basic(rName string) string { + return acctest.ConfigCompose( + testAccCollectionBaseConfig(rName), + fmt.Sprintf(` +resource "aws_opensearchserverless_collection" "test" { + name = %[1]q + + depends_on = [aws_opensearchserverless_security_policy.test] +} +`, rName), + ) +} + +func testAccCollectionConfig_update(rName, description string) string { + return acctest.ConfigCompose( + testAccCollectionBaseConfig(rName), + fmt.Sprintf(` +resource "aws_opensearchserverless_collection" "test" { + name = %[1]q + description = %[2]q + + depends_on = [aws_opensearchserverless_security_policy.test] +} +`, rName, description), + ) +} + +func testAccCollectionConfig_tags1(rName, key1, value1 string) string { + return acctest.ConfigCompose( + testAccCollectionBaseConfig(rName), + fmt.Sprintf(` +resource "aws_opensearchserverless_collection" "test" { + name = %[1]q + + tags = { + %[2]q = %[3]q + } + + depends_on = [aws_opensearchserverless_security_policy.test] +} +`, rName, key1, value1), + ) +} + +func testAccCollectionConfig_tags2(rName, key1, value1, key2, value2 string) string { + return acctest.ConfigCompose( + testAccCollectionBaseConfig(rName), + fmt.Sprintf(` +resource "aws_opensearchserverless_collection" "test" { + name = %[1]q + + tags = { + %[2]q = %[3]q + %[4]q = %[5]q + } + + depends_on = [aws_opensearchserverless_security_policy.test] +} +`, rName, key1, value1, key2, value2), + ) +} diff --git a/internal/service/opensearchserverless/exports_test.go b/internal/service/opensearchserverless/exports_test.go index 6bb7d2a0a47c..bc27bad3ea0a 100644 --- a/internal/service/opensearchserverless/exports_test.go +++ b/internal/service/opensearchserverless/exports_test.go @@ -2,6 +2,7 @@ package opensearchserverless // Exports for use in tests only. var ( + ResourceCollection = newResourceCollection ResourceAccessPolicy = newResourceAccessPolicy ResourceSecurityPolicy = newResourceSecurityPolicy ) diff --git a/internal/service/opensearchserverless/find.go b/internal/service/opensearchserverless/find.go index e7090ea8db7e..de08321934d4 100644 --- a/internal/service/opensearchserverless/find.go +++ b/internal/service/opensearchserverless/find.go @@ -36,6 +36,30 @@ func FindAccessPolicyByNameAndType(ctx context.Context, conn *opensearchserverle return out.AccessPolicyDetail, nil } +func FindCollectionByID(ctx context.Context, conn *opensearchserverless.Client, id string) (*types.CollectionDetail, error) { + in := &opensearchserverless.BatchGetCollectionInput{ + Ids: []string{id}, + } + out, err := conn.BatchGetCollection(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.CollectionDetails == nil || len(out.CollectionDetails) == 0 { + return nil, tfresource.NewEmptyResultError(in) + } + + return &out.CollectionDetails[0], nil +} + func FindSecurityPolicyByNameAndType(ctx context.Context, conn *opensearchserverless.Client, name, policyType string) (*types.SecurityPolicyDetail, error) { in := &opensearchserverless.GetSecurityPolicyInput{ Name: aws.String(name), diff --git a/internal/service/opensearchserverless/service_package_gen.go b/internal/service/opensearchserverless/service_package_gen.go index 8ce92e2ffa1e..d12c6135eb78 100644 --- a/internal/service/opensearchserverless/service_package_gen.go +++ b/internal/service/opensearchserverless/service_package_gen.go @@ -20,6 +20,13 @@ func (p *servicePackage) FrameworkResources(ctx context.Context) []*types.Servic { Factory: newResourceAccessPolicy, }, + { + Factory: newResourceCollection, + Name: "Collection", + Tags: &types.ServicePackageResourceTags{ + IdentifierAttribute: "arn", + }, + }, { Factory: newResourceSecurityPolicy, }, diff --git a/internal/service/opensearchserverless/sweep.go b/internal/service/opensearchserverless/sweep.go new file mode 100644 index 000000000000..2f0060cdd303 --- /dev/null +++ b/internal/service/opensearchserverless/sweep.go @@ -0,0 +1,203 @@ +//go:build sweep +// +build sweep + +package opensearchserverless + +import ( + "fmt" + "log" + + "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" + "github.com/hashicorp/go-multierror" + "github.com/hashicorp/terraform-plugin-testing/helper/resource" + "github.com/hashicorp/terraform-provider-aws/internal/conns" + "github.com/hashicorp/terraform-provider-aws/internal/sweep" +) + +func init() { + resource.AddTestSweepers("aws_opensearchserverless_access_policy", &resource.Sweeper{ + Name: "aws_opensearchserverless_access_policy", + F: sweepAccessPolicies, + }) + resource.AddTestSweepers("aws_opensearchserverless_collection", &resource.Sweeper{ + Name: "aws_opensearchserverless_collection", + F: sweepCollections, + }) + resource.AddTestSweepers("aws_opensearchserverless_security_policy", &resource.Sweeper{ + Name: "aws_opensearchserverless_security_policy", + F: sweepSecurityPolicies, + }) +} + +func sweepAccessPolicies(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.ListAccessPoliciesInput{ + Type: types.AccessPolicyTypeData, + } + + pages := opensearchserverless.NewListAccessPoliciesPaginator(conn, input) + + for pages.HasMorePages() { + page, err := pages.NextPage(ctx) + if sweep.SkipSweepError(err) { + log.Printf("[WARN] Skipping OpenSearch Serverless Access Policies sweep for %s: %s", region, err) + return nil + } + if err != nil { + return fmt.Errorf("error retrieving OpenSearch Serverless Access Policies: %w", err) + } + + for _, ap := range page.AccessPolicySummaries { + name := aws.ToString(ap.Name) + + supAttributes := sweep.NewFrameworkSupplementalAttributes() + supAttributes.Add("type", ap.Type) + supAttributes.Add("name", ap.Name) + + log.Printf("[INFO] Deleting OpenSearch Serverless Access Policy: %s", name) + sweepResources = append(sweepResources, sweep.NewSweepFrameworkResource(newResourceAccessPolicy, name, client, supAttributes...)) + } + } + + if err := sweep.SweepOrchestratorWithContext(ctx, sweepResources); err != nil { + errs = multierror.Append(errs, fmt.Errorf("error sweeping OpenSearch Serverless Access Policies for %s: %w", region, err)) + } + if sweep.SkipSweepError(err) { + log.Printf("[WARN] Skipping OpenSearch Serverless Access Policies sweep for %s: %s", region, errs) + return nil + } + + return errs.ErrorOrNil() +} + +func sweepCollections(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.ListCollectionsInput{} + + pages := opensearchserverless.NewListCollectionsPaginator(conn, input) + + for pages.HasMorePages() { + page, err := pages.NextPage(ctx) + if sweep.SkipSweepError(err) { + log.Printf("[WARN] Skipping OpenSearch Serverless Collections sweep for %s: %s", region, err) + return nil + } + if err != nil { + return fmt.Errorf("error retrieving OpenSearch Serverless Collections: %w", err) + } + + for _, collection := range page.CollectionSummaries { + id := aws.ToString(collection.Id) + + log.Printf("[INFO] Deleting OpenSearch Serverless Collection: %s", id) + sweepResources = append(sweepResources, sweep.NewSweepFrameworkResource(newResourceCollection, id, client)) + } + } + + if err := sweep.SweepOrchestratorWithContext(ctx, sweepResources); err != nil { + errs = multierror.Append(errs, fmt.Errorf("error sweeping OpenSearch Serverless Collections for %s: %w", region, err)) + } + if sweep.SkipSweepError(err) { + log.Printf("[WARN] Skipping OpenSearch Serverless Collections sweep for %s: %s", region, errs) + return nil + } + + return errs.ErrorOrNil() +} + +func sweepSecurityPolicies(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 + + inputEncryption := &opensearchserverless.ListSecurityPoliciesInput{ + Type: types.SecurityPolicyTypeEncryption, + } + pagesEncryption := opensearchserverless.NewListSecurityPoliciesPaginator(conn, inputEncryption) + + for pagesEncryption.HasMorePages() { + page, err := pagesEncryption.NextPage(ctx) + if sweep.SkipSweepError(err) { + log.Printf("[WARN] Skipping OpenSearch Serverless Security Policies sweep for %s: %s", region, err) + return nil + } + if err != nil { + return fmt.Errorf("error retrieving OpenSearch Serverless Security Policies: %w", err) + } + + for _, sp := range page.SecurityPolicySummaries { + name := aws.ToString(sp.Name) + + supAttributes := sweep.NewFrameworkSupplementalAttributes() + supAttributes.Add("type", sp.Type) + supAttributes.Add("name", sp.Name) + + log.Printf("[INFO] Deleting OpenSearch Serverless Security Policy: %s", name) + sweepResources = append(sweepResources, sweep.NewSweepFrameworkResource(newResourceSecurityPolicy, name, client, supAttributes...)) + } + } + + inputNetwork := &opensearchserverless.ListSecurityPoliciesInput{ + Type: types.SecurityPolicyTypeNetwork, + } + pagesNetwork := opensearchserverless.NewListSecurityPoliciesPaginator(conn, inputNetwork) + + for pagesNetwork.HasMorePages() { + page, err := pagesNetwork.NextPage(ctx) + if sweep.SkipSweepError(err) { + log.Printf("[WARN] Skipping OpenSearch Serverless Security Policies sweep for %s: %s", region, err) + return nil + } + if err != nil { + return fmt.Errorf("error retrieving OpenSearch Serverless Security Policies: %w", err) + } + + for _, sp := range page.SecurityPolicySummaries { + name := aws.ToString(sp.Name) + + supAttributes := sweep.NewFrameworkSupplementalAttributes() + supAttributes.Add("type", sp.Type) + supAttributes.Add("name", sp.Name) + + log.Printf("[INFO] Deleting OpenSearch Serverless Security Policy: %s", name) + sweepResources = append(sweepResources, sweep.NewSweepFrameworkResource(newResourceSecurityPolicy, name, client, supAttributes...)) + } + } + + if err := sweep.SweepOrchestratorWithContext(ctx, sweepResources); err != nil { + errs = multierror.Append(errs, fmt.Errorf("error sweeping OpenSearch Serverless Security Policies for %s: %w", region, err)) + } + if sweep.SkipSweepError(err) { + log.Printf("[WARN] Skipping OpenSearch Serverless Security Policies sweep for %s: %s", region, errs) + return nil + } + + return errs.ErrorOrNil() +} diff --git a/internal/sweep/sweep_test.go b/internal/sweep/sweep_test.go index ff46cd35a7a0..0fdc6e04977c 100644 --- a/internal/sweep/sweep_test.go +++ b/internal/sweep/sweep_test.go @@ -105,6 +105,7 @@ import ( _ "github.com/hashicorp/terraform-provider-aws/internal/service/networkmanager" _ "github.com/hashicorp/terraform-provider-aws/internal/service/oam" _ "github.com/hashicorp/terraform-provider-aws/internal/service/opensearch" + _ "github.com/hashicorp/terraform-provider-aws/internal/service/opensearchserverless" _ "github.com/hashicorp/terraform-provider-aws/internal/service/opsworks" _ "github.com/hashicorp/terraform-provider-aws/internal/service/pinpoint" _ "github.com/hashicorp/terraform-provider-aws/internal/service/pipes" diff --git a/website/docs/r/opensearchserverless_collection.html.markdown b/website/docs/r/opensearchserverless_collection.html.markdown new file mode 100644 index 000000000000..ad66ad1eb951 --- /dev/null +++ b/website/docs/r/opensearchserverless_collection.html.markdown @@ -0,0 +1,76 @@ +--- +subcategory: "OpenSearch Serverless" +layout: "aws" +page_title: "AWS: aws_opensearchserverless_collection" +description: |- + Terraform resource for managing an AWS OpenSearch Collection. +--- + +# Resource: aws_opensearchserverless_collection + +Terraform resource for managing an AWS OpenSearch Serverless Collection. + +## Example Usage + +### Basic Usage + +```terraform +resource "aws_opensearchserverless_security_policy" "example" { + name = "example" + type = "encryption" + policy = jsonencode({ + "Rules" = [ + { + "Resource" = [ + "collection/example" + ], + "ResourceType" = "collection" + } + ], + "AWSOwnedKey" = true + }) +} + +resource "aws_opensearchserverless_collection" "example" { + name = "example" + + depends_on = [aws_opensearchserverless_security_policy.example] +} +``` + +## Argument Reference + +The following arguments are required: + +* `name` - (Required) Name of the collection. + +The following arguments are optional: + +* `description` - (Optional) Description of the collection. +* `tags` - (Optional) A map of tags to assign to the collection. If configured with a provider [`default_tags` configuration block](https://registry.terraform.io/providers/hashicorp/aws/latest/docs#default_tags-configuration-block) present, tags with matching keys will overwrite those defined at the provider-level. +* `type` - (Optional) Type of collection. One of `SEARCH` or `TIMESERIES`. + +## Attributes Reference + +In addition to all arguments above, the following attributes are exported: + +* `arn` - Amazon Resource Name (ARN) of the collection. +* `collection_endpoint` - Collection-specific endpoint used to submit index, search, and data upload requests to an OpenSearch Serverless collection. +* `dashboard_endpont` - Collection-specific endpoint used to access OpenSearch Dashboards. +* `kms_key_arn` - The ARN of the Amazon Web Services KMS key used to encrypt the collection. +* `id` - Unique identifier for the collection. + +## Timeouts + +[Configuration options](https://developer.hashicorp.com/terraform/language/resources/syntax#operation-timeouts): + +- `create` - (Default `20m`) +- `delete` - (Default `20m`) + +## Import + +OpenSearchServerless Collection can be imported using the `id`, e.g., + +``` +$ terraform import aws_opensearchserverless_collection.example example +```