diff --git a/.changelog/33475.txt b/.changelog/33475.txt new file mode 100644 index 00000000000..e2ddae20a41 --- /dev/null +++ b/.changelog/33475.txt @@ -0,0 +1,3 @@ +```release-note:new-resource +aws_lexv2models_bot +``` \ No newline at end of file diff --git a/internal/framework/flex/int.go b/internal/framework/flex/int.go index 3ab1e8304ff..a19917f130e 100644 --- a/internal/framework/flex/int.go +++ b/internal/framework/flex/int.go @@ -48,3 +48,11 @@ func Int64FromFrameworkLegacy(_ context.Context, v types.Int64) *int64 { return aws.Int64(i) } + +func Int32ToFramework(ctx context.Context, v *int32) types.Int64 { + var output types.Int64 + + panicOnError(Flatten(ctx, v, &output)) + + return output +} diff --git a/internal/framework/flex/int_test.go b/internal/framework/flex/int_test.go index a0a032098f6..94cf8801932 100644 --- a/internal/framework/flex/int_test.go +++ b/internal/framework/flex/int_test.go @@ -124,3 +124,39 @@ func TestInt64ToFrameworkLegacy(t *testing.T) { }) } } + +func TestInt32ToFramework(t *testing.T) { + t.Parallel() + + type testCase struct { + input *int32 + expected types.Int64 + } + tests := map[string]testCase{ + "valid int64": { + input: aws.Int32(42), + expected: types.Int64Value(42), + }, + "zero int64": { + input: aws.Int32(0), + expected: types.Int64Value(0), + }, + "nil int64": { + input: nil, + expected: types.Int64Null(), + }, + } + + for name, test := range tests { + name, test := name, test + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := flex.Int32ToFramework(context.Background(), test.input) + + if diff := cmp.Diff(got, test.expected); diff != "" { + t.Errorf("unexpected diff (+wanted, -got): %s", diff) + } + }) + } +} diff --git a/internal/service/lexv2models/bot.go b/internal/service/lexv2models/bot.go new file mode 100644 index 00000000000..7a7181a0f95 --- /dev/null +++ b/internal/service/lexv2models/bot.go @@ -0,0 +1,584 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package lexv2models + +import ( + "context" + "errors" + "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/lexmodelsv2" + awstypes "github.com/aws/aws-sdk-go-v2/service/lexmodelsv2/types" + "github.com/hashicorp/terraform-plugin-framework-timeouts/resource/timeouts" + "github.com/hashicorp/terraform-plugin-framework-validators/listvalidator" + "github.com/hashicorp/terraform-plugin-framework/attr" + "github.com/hashicorp/terraform-plugin-framework/diag" + "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/retry" + "github.com/hashicorp/terraform-provider-aws/internal/create" + "github.com/hashicorp/terraform-provider-aws/internal/enum" + "github.com/hashicorp/terraform-provider-aws/internal/framework" + "github.com/hashicorp/terraform-provider-aws/internal/framework/flex" + fwtypes "github.com/hashicorp/terraform-provider-aws/internal/framework/types" + 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="Bot") +// @Tags(identifierAttribute="arn") +func newResourceBot(_ context.Context) (resource.ResourceWithConfigure, error) { + r := &resourceBot{} + + r.SetDefaultCreateTimeout(30 * time.Minute) + r.SetDefaultUpdateTimeout(30 * time.Minute) + r.SetDefaultDeleteTimeout(30 * time.Minute) + + return r, nil +} + +const ( + ResNameBot = "Bot" +) + +type resourceBot struct { + framework.ResourceWithConfigure + framework.WithTimeouts +} + +func (r *resourceBot) Metadata(_ context.Context, req resource.MetadataRequest, resp *resource.MetadataResponse) { + resp.TypeName = "aws_lexv2models_bot" +} + +func (r *resourceBot) Schema(ctx context.Context, req resource.SchemaRequest, resp *resource.SchemaResponse) { + resp.Schema = schema.Schema{ + Attributes: map[string]schema.Attribute{ + "arn": framework.ARNAttributeComputedOnly(), + "description": schema.StringAttribute{ + Optional: true, + }, + "id": framework.IDAttribute(), + "idle_session_ttl_in_seconds": schema.Int64Attribute{ + Required: true, + }, + "name": schema.StringAttribute{ + Required: true, + PlanModifiers: []planmodifier.String{ + stringplanmodifier.RequiresReplace(), + }, + }, + names.AttrTags: tftags.TagsAttribute(), + names.AttrTagsAll: tftags.TagsAttributeComputedOnly(), + "role_arn": schema.StringAttribute{ + CustomType: fwtypes.ARNType, + Required: true, + }, + "test_bot_alias_tags": schema.MapAttribute{ + ElementType: types.StringType, + Optional: true, + }, + "type": schema.StringAttribute{ + Optional: true, + Computed: true, + Validators: []validator.String{ + enum.FrameworkValidate[awstypes.BotType](), + }, + PlanModifiers: []planmodifier.String{ + stringplanmodifier.UseStateForUnknown(), + }, + }, + }, + Blocks: map[string]schema.Block{ + "members": schema.ListNestedBlock{ + NestedObject: schema.NestedBlockObject{ + Attributes: map[string]schema.Attribute{ + "alias_id": schema.StringAttribute{ + Required: true, + }, + "alias_name": schema.StringAttribute{ + Required: true, + }, + "id": schema.StringAttribute{ + Required: true, + }, + "name": schema.StringAttribute{ + Required: true, + }, + "version": schema.StringAttribute{ + Required: true, + }, + }, + }, + }, + "data_privacy": schema.ListNestedBlock{ + Validators: []validator.List{ + listvalidator.SizeAtLeast(1), + }, + NestedObject: schema.NestedBlockObject{ + Attributes: map[string]schema.Attribute{ + "child_directed": schema.BoolAttribute{ + Required: true, + }, + }, + }, + }, + "timeouts": timeouts.Block(ctx, timeouts.Opts{ + Create: true, + Update: true, + Delete: true, + }), + }, + } +} + +func (r *resourceBot) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) { + conn := r.Meta().LexV2ModelsClient(ctx) + + var plan resourceBotData + resp.Diagnostics.Append(req.Plan.Get(ctx, &plan)...) + if resp.Diagnostics.HasError() { + return + } + + var dp []dataPrivacyData + resp.Diagnostics.Append(plan.DataPrivacy.ElementsAs(ctx, &dp, false)...) + if resp.Diagnostics.HasError() { + return + } + + dpInput := expandDataPrivacy(ctx, dp) + + in := lexmodelsv2.CreateBotInput{ + BotName: aws.String(plan.Name.ValueString()), + DataPrivacy: dpInput, + IdleSessionTTLInSeconds: aws.Int32(int32(plan.IdleSessionTTLInSeconds.ValueInt64())), + RoleArn: flex.ARNStringFromFramework(ctx, plan.RoleARN), + BotTags: getTagsIn(ctx), + } + + if !plan.TestBotAliasTags.IsNull() { + in.TestBotAliasTags = flex.ExpandFrameworkStringValueMap(ctx, plan.TestBotAliasTags) + } + + if !plan.Description.IsNull() { + in.Description = aws.String(plan.Description.ValueString()) + } + + var bm []membersData + if !plan.Members.IsNull() { + bmInput := expandMembers(ctx, bm) + in.BotMembers = bmInput + } + + out, err := conn.CreateBot(ctx, &in) + if err != nil { + resp.Diagnostics.AddError( + create.ProblemStandardMessage(names.LexV2Models, create.ErrActionCreating, ResNameBot, plan.Name.String(), err), + err.Error(), + ) + return + } + if out == nil || out.BotId == nil { + resp.Diagnostics.AddError( + create.ProblemStandardMessage(names.LexV2Models, create.ErrActionCreating, ResNameBot, plan.Name.String(), nil), + errors.New("empty output").Error(), + ) + return + } + botArn := arn.ARN{ + Partition: r.Meta().Partition, + Service: "lex", + Region: r.Meta().Region, + AccountID: r.Meta().AccountID, + Resource: fmt.Sprintf("bot/%s", aws.ToString(out.BotId)), + }.String() + plan.ID = flex.StringToFramework(ctx, out.BotId) + state := plan + state.Type = flex.StringValueToFramework(ctx, out.BotType) + state.ARN = flex.StringValueToFramework(ctx, botArn) + + resp.Diagnostics.Append(resp.State.Set(ctx, state)...) + + createTimeout := r.CreateTimeout(ctx, state.Timeouts) + _, err = waitBotCreated(ctx, conn, state.ID.ValueString(), createTimeout) + if err != nil { + resp.Diagnostics.AddError( + create.ProblemStandardMessage(names.LexV2Models, create.ErrActionWaitingForDeletion, ResNameBot, state.ID.String(), err), + err.Error(), + ) + return + } +} + +func (r *resourceBot) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) { + conn := r.Meta().LexV2ModelsClient(ctx) + var diags diag.Diagnostics + var state resourceBotData + resp.Diagnostics.Append(req.State.Get(ctx, &state)...) + if resp.Diagnostics.HasError() { + return + } + + out, err := FindBotByID(ctx, conn, state.ID.ValueString()) + if tfresource.NotFound(err) { + resp.State.RemoveResource(ctx) + return + } + if err != nil { + resp.Diagnostics.AddError( + create.ProblemStandardMessage(names.LexV2Models, create.ErrActionSetting, ResNameBot, state.ID.String(), err), + err.Error(), + ) + return + } + + botArn := arn.ARN{ + Partition: r.Meta().Partition, + Service: "lex", + Region: r.Meta().Region, + AccountID: r.Meta().AccountID, + Resource: fmt.Sprintf("bot/%s", aws.ToString(out.BotId)), + }.String() + state.ARN = flex.StringValueToFramework(ctx, botArn) + state.RoleARN = flex.StringToFrameworkARN(ctx, out.RoleArn, &diags) + state.ID = flex.StringToFramework(ctx, out.BotId) + state.Name = flex.StringToFramework(ctx, out.BotName) + state.Type = flex.StringValueToFramework(ctx, out.BotType) + state.Description = flex.StringToFramework(ctx, out.Description) + state.IdleSessionTTLInSeconds = flex.Int32ToFramework(ctx, out.IdleSessionTTLInSeconds) + + datap, _ := flattenDataPrivacy(out.DataPrivacy) + if resp.Diagnostics.HasError() { + return + } + + state.DataPrivacy = datap + resp.Diagnostics.Append(resp.State.Set(ctx, &state)...) +} + +func (r *resourceBot) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) { + conn := r.Meta().LexV2ModelsClient(ctx) + + var plan, state resourceBotData + resp.Diagnostics.Append(req.Plan.Get(ctx, &plan)...) + resp.Diagnostics.Append(req.State.Get(ctx, &state)...) + if resp.Diagnostics.HasError() { + return + } + + if !plan.Description.Equal(state.Description) || + !plan.IdleSessionTTLInSeconds.Equal(state.IdleSessionTTLInSeconds) || + !plan.RoleARN.Equal(state.RoleARN) || + !plan.TestBotAliasTags.Equal(state.TestBotAliasTags) || + !plan.DataPrivacy.Equal(state.DataPrivacy) || + !plan.Type.Equal(state.Type) { + var dp []dataPrivacyData + resp.Diagnostics.Append(plan.DataPrivacy.ElementsAs(ctx, &dp, false)...) + if resp.Diagnostics.HasError() { + return + } + + dpInput := expandDataPrivacy(ctx, dp) + + in := lexmodelsv2.UpdateBotInput{ + BotId: flex.StringFromFramework(ctx, plan.ID), + BotName: flex.StringFromFramework(ctx, plan.Name), + IdleSessionTTLInSeconds: aws.Int32(int32(plan.IdleSessionTTLInSeconds.ValueInt64())), + DataPrivacy: dpInput, + RoleArn: flex.ARNStringFromFramework(ctx, plan.RoleARN), + } + + if !plan.Description.IsNull() { + in.Description = aws.String(plan.Description.ValueString()) + } + + if !plan.Members.IsNull() { + var tfList []membersData + resp.Diagnostics.Append(plan.Members.ElementsAs(ctx, &tfList, false)...) + if resp.Diagnostics.HasError() { + return + } + } + + _, err := conn.UpdateBot(ctx, &in) + if err != nil { + resp.Diagnostics.AddError( + create.ProblemStandardMessage(names.LexV2Models, create.ErrActionUpdating, ResNameBot, plan.ID.String(), err), + err.Error(), + ) + return + } + updateTimeout := r.UpdateTimeout(ctx, plan.Timeouts) + out, err := waitBotUpdated(ctx, conn, plan.ID.ValueString(), updateTimeout) + if err != nil { + resp.Diagnostics.AddError( + create.ProblemStandardMessage(names.LexV2Models, create.ErrActionWaitingForUpdate, ResNameBot, plan.ID.String(), err), + err.Error(), + ) + return + } + + if out == nil || out.BotId == nil { + resp.Diagnostics.AddError( + create.ProblemStandardMessage(names.LexV2Models, create.ErrActionUpdating, ResNameBot, plan.ID.String(), nil), + errors.New("empty output").Error(), + ) + return + } + resp.Diagnostics.Append(plan.refreshFromOutput(ctx, out)...) + } + + resp.Diagnostics.Append(resp.State.Set(ctx, &plan)...) +} + +func (r *resourceBot) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) { + conn := r.Meta().LexV2ModelsClient(ctx) + + var state resourceBotData + resp.Diagnostics.Append(req.State.Get(ctx, &state)...) + if resp.Diagnostics.HasError() { + return + } + + in := &lexmodelsv2.DeleteBotInput{ + BotId: aws.String(state.ID.ValueString()), + } + + _, err := conn.DeleteBot(ctx, in) + if err != nil { + var nfe *awstypes.ResourceNotFoundException + if errors.As(err, &nfe) { + return + } + resp.Diagnostics.AddError( + create.ProblemStandardMessage(names.LexV2Models, create.ErrActionDeleting, ResNameBot, state.ID.String(), err), + err.Error(), + ) + return + } + + deleteTimeout := r.DeleteTimeout(ctx, state.Timeouts) + _, err = waitBotDeleted(ctx, conn, state.ID.ValueString(), deleteTimeout) + if err != nil { + resp.Diagnostics.AddError( + create.ProblemStandardMessage(names.LexV2Models, create.ErrActionWaitingForDeletion, ResNameBot, state.ID.String(), err), + err.Error(), + ) + return + } +} + +func (r *resourceBot) ModifyPlan(ctx context.Context, req resource.ModifyPlanRequest, resp *resource.ModifyPlanResponse) { + r.SetTagsAll(ctx, req, resp) +} + +func (r *resourceBot) ImportState(ctx context.Context, req resource.ImportStateRequest, resp *resource.ImportStateResponse) { + resource.ImportStatePassthroughID(ctx, path.Root("id"), req, resp) +} + +func waitBotCreated(ctx context.Context, conn *lexmodelsv2.Client, id string, timeout time.Duration) (*lexmodelsv2.DescribeBotOutput, error) { + stateConf := &retry.StateChangeConf{ + Pending: enum.Slice(awstypes.BotStatusCreating), + Target: enum.Slice(awstypes.BotStatusAvailable), + Refresh: statusBot(ctx, conn, id), + Timeout: timeout, + NotFoundChecks: 20, + ContinuousTargetOccurence: 2, + } + + outputRaw, err := stateConf.WaitForStateContext(ctx) + if out, ok := outputRaw.(*lexmodelsv2.DescribeBotOutput); ok { + return out, err + } + + return nil, err +} + +func waitBotUpdated(ctx context.Context, conn *lexmodelsv2.Client, id string, timeout time.Duration) (*lexmodelsv2.DescribeBotOutput, error) { + stateConf := &retry.StateChangeConf{ + Pending: enum.Slice(awstypes.BotStatusUpdating), + Target: enum.Slice(awstypes.BotStatusAvailable), + Refresh: statusBot(ctx, conn, id), + Timeout: timeout, + NotFoundChecks: 20, + ContinuousTargetOccurence: 2, + } + + outputRaw, err := stateConf.WaitForStateContext(ctx) + if out, ok := outputRaw.(*lexmodelsv2.DescribeBotOutput); ok { + return out, err + } + + return nil, err +} + +func waitBotDeleted(ctx context.Context, conn *lexmodelsv2.Client, id string, timeout time.Duration) (*lexmodelsv2.DescribeBotOutput, error) { + stateConf := &retry.StateChangeConf{ + Pending: enum.Slice(awstypes.BotStatusDeleting), + Target: []string{}, + Refresh: statusBot(ctx, conn, id), + Timeout: timeout, + } + + outputRaw, err := stateConf.WaitForStateContext(ctx) + if out, ok := outputRaw.(*lexmodelsv2.DescribeBotOutput); ok { + return out, err + } + + return nil, err +} + +func statusBot(ctx context.Context, conn *lexmodelsv2.Client, id string) retry.StateRefreshFunc { + return func() (interface{}, string, error) { + out, err := FindBotByID(ctx, conn, id) + if tfresource.NotFound(err) { + return nil, "", nil + } + + if err != nil { + return nil, "", err + } + + return out, aws.ToString((*string)(&out.BotStatus)), nil + } +} + +func FindBotByID(ctx context.Context, conn *lexmodelsv2.Client, id string) (*lexmodelsv2.DescribeBotOutput, error) { + in := &lexmodelsv2.DescribeBotInput{ + BotId: aws.String(id), + } + + out, err := conn.DescribeBot(ctx, in) + if err != nil { + var nfe *awstypes.ResourceNotFoundException + if errors.As(err, &nfe) { + return nil, &retry.NotFoundError{ + LastError: err, + LastRequest: in, + } + } + + return nil, err + } + + if out == nil || out.BotId == nil { + return nil, tfresource.NewEmptyResultError(in) + } + + return out, nil +} + +func flattenDataPrivacy(apiObject *awstypes.DataPrivacy) (types.List, diag.Diagnostics) { + var diags diag.Diagnostics + elemType := types.ObjectType{AttrTypes: dataPrivacyAttrTypes} + + if apiObject == nil { + return types.ListValueMust(elemType, []attr.Value{}), diags + } + + obj := map[string]attr.Value{ + "child_directed": types.BoolValue(aws.ToBool(&apiObject.ChildDirected)), + } + objVal, d := types.ObjectValue(dataPrivacyAttrTypes, obj) + diags.Append(d...) + + listVal, d := types.ListValue(elemType, []attr.Value{objVal}) + diags.Append(d...) + + return listVal, diags +} + +func expandDataPrivacy(ctx context.Context, tfList []dataPrivacyData) *awstypes.DataPrivacy { + if len(tfList) == 0 { + return nil + } + + dp := tfList[0] + cdBool := flex.BoolFromFramework(ctx, dp.ChildDirected) + + return &awstypes.DataPrivacy{ + ChildDirected: aws.ToBool(cdBool), + } +} + +func expandMembers(ctx context.Context, tfList []membersData) []awstypes.BotMember { + if len(tfList) == 0 { + return nil + } + var mb []awstypes.BotMember + + for _, item := range tfList { + new := awstypes.BotMember{ + BotMemberAliasId: flex.StringFromFramework(ctx, item.AliasID), + BotMemberAliasName: flex.StringFromFramework(ctx, item.AliasName), + BotMemberId: flex.StringFromFramework(ctx, item.ID), + BotMemberName: flex.StringFromFramework(ctx, item.Name), + BotMemberVersion: flex.StringFromFramework(ctx, item.Version), + } + mb = append(mb, new) + } + + return mb +} + +func (rd *resourceBotData) refreshFromOutput(ctx context.Context, out *lexmodelsv2.DescribeBotOutput) diag.Diagnostics { + var diags diag.Diagnostics + + if out == nil { + return diags + } + rd.RoleARN = flex.StringToFrameworkARN(ctx, out.RoleArn, &diags) + rd.ID = flex.StringToFramework(ctx, out.BotId) + rd.Name = flex.StringToFramework(ctx, out.BotName) + rd.Type = flex.StringToFramework(ctx, (*string)(&out.BotType)) + rd.Description = flex.StringToFramework(ctx, out.Description) + rd.IdleSessionTTLInSeconds = flex.Int32ToFramework(ctx, out.IdleSessionTTLInSeconds) + + datap, d := flattenDataPrivacy(out.DataPrivacy) + diags.Append(d...) + rd.DataPrivacy = datap + + return diags +} + +type resourceBotData struct { + ARN types.String `tfsdk:"arn"` + DataPrivacy types.List `tfsdk:"data_privacy"` + Description types.String `tfsdk:"description"` + ID types.String `tfsdk:"id"` + IdleSessionTTLInSeconds types.Int64 `tfsdk:"idle_session_ttl_in_seconds"` + Name types.String `tfsdk:"name"` + Members types.List `tfsdk:"members"` + RoleARN fwtypes.ARN `tfsdk:"role_arn"` + Tags types.Map `tfsdk:"tags"` + TagsAll types.Map `tfsdk:"tags_all"` + TestBotAliasTags types.Map `tfsdk:"test_bot_alias_tags"` + Timeouts timeouts.Value `tfsdk:"timeouts"` + Type types.String `tfsdk:"type"` +} + +type dataPrivacyData struct { + ChildDirected types.Bool `tfsdk:"child_directed"` +} + +type membersData struct { + AliasID types.String `tfsdk:"alias_id"` + AliasName types.String `tfsdk:"alias_name"` + ID types.String `tfsdk:"id"` + Name types.String `tfsdk:"name"` + Version types.String `tfsdk:"version"` +} + +var dataPrivacyAttrTypes = map[string]attr.Type{ + "child_directed": types.BoolType, +} diff --git a/internal/service/lexv2models/bot_test.go b/internal/service/lexv2models/bot_test.go new file mode 100644 index 00000000000..1e6d826b836 --- /dev/null +++ b/internal/service/lexv2models/bot_test.go @@ -0,0 +1,288 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package lexv2models_test + +import ( + "context" + "errors" + "fmt" + "testing" + + "github.com/aws/aws-sdk-go-v2/service/lexmodelsv2" + 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" + tflexv2models "github.com/hashicorp/terraform-provider-aws/internal/service/lexv2models" + "github.com/hashicorp/terraform-provider-aws/internal/tfresource" + "github.com/hashicorp/terraform-provider-aws/names" +) + +func TestAccLexV2ModelsBot_basic(t *testing.T) { + ctx := acctest.Context(t) + + var bot lexmodelsv2.DescribeBotOutput + rName := sdkacctest.RandomWithPrefix(acctest.ResourcePrefix) + resourceName := "aws_lexv2models_bot.test" + + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { + acctest.PreCheck(ctx, t) + acctest.PreCheckPartitionHasService(t, names.LexV2ModelsEndpointID) + testAccPreCheck(ctx, t) + }, + ErrorCheck: acctest.ErrorCheck(t, names.LexV2ModelsEndpointID), + ProtoV5ProviderFactories: acctest.ProtoV5ProviderFactories, + CheckDestroy: testAccCheckBotDestroy(ctx), + Steps: []resource.TestStep{ + { + Config: testAccBotConfig_basic(rName, 60, true), + Check: resource.ComposeTestCheckFunc( + testAccCheckBotExists(ctx, resourceName, &bot), + resource.TestCheckResourceAttr(resourceName, "name", rName), + resource.TestCheckResourceAttr(resourceName, "idle_session_ttl_in_seconds", "60"), + resource.TestCheckResourceAttrSet(resourceName, "role_arn"), + resource.TestCheckResourceAttrSet(resourceName, "data_privacy.0.child_directed"), + ), + }, + { + ResourceName: resourceName, + ImportState: true, + ImportStateVerify: true, + }, + }, + }) +} + +func TestAccLexV2ModelsBot_tags(t *testing.T) { + ctx := acctest.Context(t) + var bot lexmodelsv2.DescribeBotOutput + rName := sdkacctest.RandomWithPrefix(acctest.ResourcePrefix) + resourceName := "aws_lexv2models_bot.test" + + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { + acctest.PreCheck(ctx, t) + acctest.PreCheckPartitionHasService(t, names.LexV2ModelsEndpointID) + }, + ErrorCheck: acctest.ErrorCheck(t, names.LexV2ModelsEndpointID), + ProtoV5ProviderFactories: acctest.ProtoV5ProviderFactories, + CheckDestroy: testAccCheckBotDestroy(ctx), + Steps: []resource.TestStep{ + { + Config: testAccBotConfig_tags1(rName, 60, true, "key1", "value1"), + Check: resource.ComposeTestCheckFunc( + testAccCheckBotExists(ctx, resourceName, &bot), + resource.TestCheckResourceAttr(resourceName, "name", rName), + resource.TestCheckResourceAttr(resourceName, "tags.%", "1"), + resource.TestCheckResourceAttr(resourceName, "tags.key1", "value1"), + ), + }, + { + ResourceName: resourceName, + ImportState: true, + ImportStateVerify: true, + }, + { + Config: testAccBotConfig_tags2(rName, 60, true, "key1", "value1updated", "key2", "value2"), + Check: resource.ComposeTestCheckFunc( + testAccCheckBotExists(ctx, resourceName, &bot), + resource.TestCheckResourceAttr(resourceName, "name", rName), + resource.TestCheckResourceAttr(resourceName, "tags.%", "2"), + resource.TestCheckResourceAttr(resourceName, "tags.key1", "value1updated"), + resource.TestCheckResourceAttr(resourceName, "tags.key2", "value2"), + ), + }, + { + Config: testAccBotConfig_tags1(rName, 60, true, "key2", "value2"), + Check: resource.ComposeTestCheckFunc( + testAccCheckBotExists(ctx, resourceName, &bot), + resource.TestCheckResourceAttr(resourceName, "name", rName), + resource.TestCheckResourceAttr(resourceName, "tags.%", "1"), + resource.TestCheckResourceAttr(resourceName, "tags.key2", "value2"), + ), + }, + }, + }) +} + +func TestAccLexV2ModelsBot_disappears(t *testing.T) { + ctx := acctest.Context(t) + if testing.Short() { + t.Skip("skipping long-running test in short mode") + } + + var bot lexmodelsv2.DescribeBotOutput + rName := sdkacctest.RandomWithPrefix(acctest.ResourcePrefix) + resourceName := "aws_lexv2models_bot.test" + + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { + acctest.PreCheck(ctx, t) + acctest.PreCheckPartitionHasService(t, names.LexV2ModelsEndpointID) + }, + ErrorCheck: acctest.ErrorCheck(t, names.LexV2ModelsEndpointID), + ProtoV5ProviderFactories: acctest.ProtoV5ProviderFactories, + CheckDestroy: testAccCheckBotDestroy(ctx), + Steps: []resource.TestStep{ + { + Config: testAccBotConfig_basic(rName, 60, true), + Check: resource.ComposeTestCheckFunc( + testAccCheckBotExists(ctx, resourceName, &bot), + acctest.CheckFrameworkResourceDisappears(ctx, acctest.Provider, tflexv2models.ResourceBot, resourceName), + ), + ExpectNonEmptyPlan: true, + }, + }, + }) +} + +func testAccCheckBotDestroy(ctx context.Context) resource.TestCheckFunc { + return func(s *terraform.State) error { + conn := acctest.Provider.Meta().(*conns.AWSClient).LexV2ModelsClient(ctx) + + for _, rs := range s.RootModule().Resources { + if rs.Type != "aws_lexv2models_bot" { + continue + } + + _, err := tflexv2models.FindBotByID(ctx, conn, rs.Primary.ID) + if tfresource.NotFound(err) { + continue + } + + if err != nil { + return err + } + + return create.Error(names.LexV2Models, create.ErrActionCheckingDestroyed, tflexv2models.ResNameBot, rs.Primary.ID, errors.New("not destroyed")) + } + + return nil + } +} + +func testAccCheckBotExists(ctx context.Context, name string, bot *lexmodelsv2.DescribeBotOutput) resource.TestCheckFunc { + return func(s *terraform.State) error { + rs, ok := s.RootModule().Resources[name] + if !ok { + return create.Error(names.LexV2Models, create.ErrActionCheckingExistence, tflexv2models.ResNameBot, name, errors.New("not found")) + } + + if rs.Primary.ID == "" { + return create.Error(names.LexV2Models, create.ErrActionCheckingExistence, tflexv2models.ResNameBot, name, errors.New("not set")) + } + + conn := acctest.Provider.Meta().(*conns.AWSClient).LexV2ModelsClient(ctx) + resp, err := tflexv2models.FindBotByID(ctx, conn, rs.Primary.ID) + if err != nil { + return create.Error(names.LexV2Models, create.ErrActionCheckingExistence, tflexv2models.ResNameBot, rs.Primary.ID, err) + } + + *bot = *resp + + return nil + } +} + +func testAccPreCheck(ctx context.Context, t *testing.T) { + conn := acctest.Provider.Meta().(*conns.AWSClient).LexV2ModelsClient(ctx) + + input := &lexmodelsv2.ListBotsInput{} + _, err := conn.ListBots(ctx, input) + + if acctest.PreCheckSkipError(err) { + t.Skipf("skipping acceptance testing: %s", err) + } + if err != nil { + t.Fatalf("unexpected PreCheck error: %s", err) + } +} + +func testAccBotBaseConfig(rName string) string { + return fmt.Sprintf(` +data "aws_partition" "current" {} + +resource "aws_iam_role" "test_role" { + name = %[1]q + assume_role_policy = jsonencode({ + Version = "2012-10-17" + Statement = [ + { + Action = "sts:AssumeRole" + Effect = "Allow" + Sid = "" + Principal = { + Service = "lexv2.amazonaws.com" + } + }, + ] + }) +} + +resource "aws_iam_role_policy_attachment" "test-attach" { + role = aws_iam_role.test_role.name + policy_arn = "arn:${data.aws_partition.current.partition}:iam::aws:policy/AmazonLexFullAccess" +} +`, rName) +} + +func testAccBotConfig_basic(rName string, ttl int, dp bool) string { + return acctest.ConfigCompose( + testAccBotBaseConfig(rName), + fmt.Sprintf(` +resource "aws_lexv2models_bot" "test" { + name = %[1]q + idle_session_ttl_in_seconds = %[2]d + role_arn = aws_iam_role.test_role.arn + + data_privacy { + child_directed = "%[3]t" + } +} +`, rName, ttl, dp)) +} + +func testAccBotConfig_tags1(rName string, ttl int, dp bool, tagKey1, tagValue1 string) string { + return acctest.ConfigCompose( + testAccBotBaseConfig(rName), + fmt.Sprintf(` +resource "aws_lexv2models_bot" "test" { + name = %[1]q + idle_session_ttl_in_seconds = %[2]d + role_arn = aws_iam_role.test_role.arn + + data_privacy { + child_directed = %[3]t + } + + tags = { + %[4]q = %[5]q + } +} +`, rName, ttl, dp, tagKey1, tagValue1)) +} + +func testAccBotConfig_tags2(rName string, ttl int, dp bool, tagKey1, tagValue1, tagKey2, tagValue2 string) string { + return acctest.ConfigCompose( + testAccBotBaseConfig(rName), + fmt.Sprintf(` +resource "aws_lexv2models_bot" "test" { + name = %[1]q + idle_session_ttl_in_seconds = %[2]d + role_arn = aws_iam_role.test_role.arn + + data_privacy { + child_directed = %[3]t + } + + tags = { + %[4]q = %[5]q + %[6]q = %[7]q + } +} +`, rName, ttl, dp, tagKey1, tagValue1, tagKey2, tagValue2)) +} diff --git a/internal/service/lexv2models/exports_test.go b/internal/service/lexv2models/exports_test.go new file mode 100644 index 00000000000..2b3035db1b7 --- /dev/null +++ b/internal/service/lexv2models/exports_test.go @@ -0,0 +1,9 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package lexv2models + +// Exports for use in tests only. +var ( + ResourceBot = newResourceBot +) diff --git a/internal/service/lexv2models/service_package_gen.go b/internal/service/lexv2models/service_package_gen.go index 25c038d098b..55ef56c5226 100644 --- a/internal/service/lexv2models/service_package_gen.go +++ b/internal/service/lexv2models/service_package_gen.go @@ -19,7 +19,15 @@ func (p *servicePackage) FrameworkDataSources(ctx context.Context) []*types.Serv } func (p *servicePackage) FrameworkResources(ctx context.Context) []*types.ServicePackageFrameworkResource { - return []*types.ServicePackageFrameworkResource{} + return []*types.ServicePackageFrameworkResource{ + { + Factory: newResourceBot, + Name: "Bot", + Tags: &types.ServicePackageResourceTags{ + IdentifierAttribute: "arn", + }, + }, + } } func (p *servicePackage) SDKDataSources(ctx context.Context) []*types.ServicePackageSDKDataSource { diff --git a/names/names.go b/names/names.go index 101d272f568..4805c4bee4a 100644 --- a/names/names.go +++ b/names/names.go @@ -45,6 +45,7 @@ const ( KendraEndpointID = "kendra" KeyspacesEndpointID = "keyspaces" LambdaEndpointID = "lambda" + LexV2ModelsEndpointID = "models-v2-lex" MediaLiveEndpointID = "medialive" ObservabilityAccessManagerEndpointID = "oam" OpenSearchServerlessEndpointID = "aoss" diff --git a/website/docs/r/lexv2models_bot.html.markdown b/website/docs/r/lexv2models_bot.html.markdown new file mode 100644 index 00000000000..d7af1a59ae1 --- /dev/null +++ b/website/docs/r/lexv2models_bot.html.markdown @@ -0,0 +1,86 @@ +--- +subcategory: "Lex V2 Models" +layout: "aws" +page_title: "AWS: aws_lexv2models_bot" +description: |- + Terraform resource for managing an AWS Lex V2 Models Bot. +--- + +# Resource: aws_lexv2models_bot + +Terraform resource for managing an AWS Lex V2 Models Bot. + +## Example Usage + +### Basic Usage + +```terraform +resource "aws_lexv2models_bot" "example" { + name = "example" + data_privacy { + child_directed = "boolean" + } + idle_session_ttl_in_seconds = 10 + role_arn = "bot_example_arn" +} +``` + +## Argument Reference + +The following arguments are required: + +* `name` - Name of the bot. The bot name must be unique in the account that creates the bot. Type String. Length Constraints: Minimum length of 1. Maximum length of 100. +* `data_privacy` - Provides information on additional privacy protections Amazon Lex should use with the bot's data. See [`data_privacy`](#data-privacy) +* `idle_session_ttl_in_seconds` - Time, in seconds, that Amazon Lex should keep information about a user's conversation with the bot. You can specify between 60 (1 minute) and 86,400 (24 hours) seconds. +* `role_arn` - ARN of an IAM role that has permission to access the bot. + +The following arguments are optional: + +* `members` - List of bot members in a network to be created. See [`bot_members`](#bot-members). +* `bot_tags` - List of tags to add to the bot. You can only add tags when you create a bot. +* `bot_type` - Type of a bot to create. +* `description` - Description of the bot. It appears in lists to help you identify a particular bot. +* `test_bot_alias_tags` - List of tags to add to the test alias for a bot. You can only add tags when you create a bot. + +## Attribute Reference + +This resource exports the following attributes in addition to the arguments above: + +* `id` - Unique identifier for a particular bot. + +### Data Privacy + +* `child_directed` (Required) - For each Amazon Lex bot created with the Amazon Lex Model Building Service, you must specify whether your use of Amazon Lex is related to a website, program, or other application that is directed or targeted, in whole or in part, to children under age 13 and subject to the Children's Online Privacy Protection Act (COPPA) by specifying true or false in the childDirected field. + +### Bot Members + +* `alias_id` (Required) - Alias ID of a bot that is a member of this network of bots. +* `alias_name` (Required) - Alias name of a bot that is a member of this network of bots. +* `id` (Required) - Unique ID of a bot that is a member of this network of bots. +* `name` (Required) - Unique name of a bot that is a member of this network of bots. +* `version` (Required) - Version of a bot that is a member of this network of bots. + +## Timeouts + +[Configuration options](https://developer.hashicorp.com/terraform/language/resources/syntax#operation-timeouts): + +* `create` - (Default `30m`) +* `update` - (Default `30m`) +* `delete` - (Default `30m`) + +## Import + +In Terraform v1.5.0 and later, use an [`import` block](https://developer.hashicorp.com/terraform/language/import) to import Lex V2 Models Bot using the `example_id_arg`. For example: + +```terraform +import { + to = aws_lexv2models_bot.example + id = "bot-id-12345678" +} +``` + +Using `terraform import`, import Lex V2 Models Bot using the `example_id_arg`. For example: + +```console +% terraform import aws_lexv2models_bot.example bot-id-12345678 +```