From 3088bea2d1bddaafef1a0a7f812a4db6e73a0d63 Mon Sep 17 00:00:00 2001 From: Sharon Nam Date: Thu, 14 Sep 2023 13:45:56 -0700 Subject: [PATCH 01/10] create and read --- internal/service/lexv2models/bot.go | 781 +++++++++++++++++++ internal/service/lexv2models/bot_test.go | 334 ++++++++ website/docs/r/lexv2models_bot.html.markdown | 69 ++ 3 files changed, 1184 insertions(+) create mode 100644 internal/service/lexv2models/bot.go create mode 100644 internal/service/lexv2models/bot_test.go create mode 100644 website/docs/r/lexv2models_bot.html.markdown diff --git a/internal/service/lexv2models/bot.go b/internal/service/lexv2models/bot.go new file mode 100644 index 00000000000..ef049c218f0 --- /dev/null +++ b/internal/service/lexv2models/bot.go @@ -0,0 +1,781 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package lexv2models + +import ( + // TIP: ==== IMPORTS ==== + // This is a common set of imports but not customized to your code since + // your code hasn't been written yet. Make sure you, your IDE, or + // goimports -w fixes these imports. + // + // The provider linter wants your imports to be in two groups: first, + // standard library (i.e., "fmt" or "strings"), second, everything else. + // + // Also, AWS Go SDK v2 may handle nested structures differently than v1, + // using the services/lexv2models/types package. If so, you'll + // need to import types and reference the nested types, e.g., as + // awstypes.. + "context" + "errors" + "time" + + "github.com/aws/aws-sdk-go-v2/aws" + "github.com/aws/aws-sdk-go-v2/service/lexv2models" + awstypes "github.com/aws/aws-sdk-go-v2/service/lexv2models/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/diag" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/retry" + "github.com/hashicorp/terraform-provider-aws/internal/create" + "github.com/hashicorp/terraform-provider-aws/internal/framework" + "github.com/hashicorp/terraform-provider-aws/internal/framework/flex" + "github.com/hashicorp/terraform-provider-aws/internal/tfresource" + "github.com/hashicorp/terraform-provider-aws/names" +) + +// TIP: ==== FILE STRUCTURE ==== +// All resources should follow this basic outline. Improve this resource's +// maintainability by sticking to it. +// +// 1. Package declaration +// 2. Imports +// 3. Main resource struct with schema method +// 4. Create, read, update, delete methods (in that order) +// 5. Other functions (flatteners, expanders, waiters, finders, etc.) + +// Function annotations are used for resource registration to the Provider. DO NOT EDIT. +// @FrameworkResource(name="Bot") +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" +} + +// TIP: ==== SCHEMA ==== +// In the schema, add each of the attributes in snake case (e.g., +// delete_automated_backups). +// +// Formatting rules: +// * Alphabetize attributes to make them easier to find. +// * Do not add a blank line between attributes. +// +// Attribute basics: +// * If a user can provide a value ("configure a value") for an +// attribute (e.g., instances = 5), we call the attribute an +// "argument." +// * You change the way users interact with attributes using: +// - Required +// - Optional +// - Computed +// * There are only four valid combinations: +// +// 1. Required only - the user must provide a value +// Required: true, +// +// 2. Optional only - the user can configure or omit a value; do not +// use Default or DefaultFunc +// Optional: true, +// +// 3. Computed only - the provider can provide a value but the user +// cannot, i.e., read-only +// Computed: true, +// +// 4. Optional AND Computed - the provider or user can provide a value; +// use this combination if you are using Default +// Optional: true, +// Computed: true, +// +// You will typically find arguments in the input struct +// (e.g., CreateDBInstanceInput) for the create operation. Sometimes +// they are only in the input struct (e.g., ModifyDBInstanceInput) for +// the modify operation. +// +// For more about schema options, visit +// https://developer.hashicorp.com/terraform/plugin/framework/handling-data/schemas?page=schemas +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, + // TIP: ==== PLAN MODIFIERS ==== + // Plan modifiers were introduced with Plugin-Framework to provide a mechanism + // for adjusting planned changes prior to apply. The planmodifier subpackage + // provides built-in modifiers for many common use cases such as + // requiring replacement on a value change ("ForceNew: true" in Plugin-SDK + // resources). + // + // See more: + // https://developer.hashicorp.com/terraform/plugin/framework/resources/plan-modification + PlanModifiers: []planmodifier.String{ + stringplanmodifier.RequiresReplace(), + }, + }, + "role_arn": schema.StringAttribute{ + Required: true, + }, + "test_bot_alias_tags": schema.StringAttribute{ + Required: false, + }, + "type": schema.StringAttribute{ + Required: true, + }, + }, + Blocks: map[string]schema.Block{ + "members": schema.ListNestedBlock{ + Validators: []validator.List{ + listvalidator.SizeAtMost(1), + }, + 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.StringAttribute{ + 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) { + // TIP: ==== RESOURCE CREATE ==== + // Generally, the Create function should do the following things. Make + // sure there is a good reason if you don't do one of these. + // + // 1. Get a client connection to the relevant service + // 2. Fetch the plan + // 3. Populate a create input structure + // 4. Call the AWS create/put function + // 5. Using the output from the create function, set the minimum arguments + // and attributes for the Read function to work, as well as any computed + // only attributes. + // 6. Use a waiter to wait for create to complete + // 7. Save the request plan to response state + + // TIP: -- 1. Get a client connection to the relevant service + conn := r.Meta().LexV2ModelsClient(ctx) + + // TIP: -- 2. Fetch the plan + 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, d := expandDataPrivacy(ctx, dp) + resp.Diagnostics.Append(d...) + if resp.Diagnostics.HasError() { + return + } + + // TIP: -- 3. Populate a create input structure + in := &lexv2models.CreateBotInput{ + // TIP: Mandatory or fields that will always be present can be set when + // you create the Input structure. (Replace these with real fields.) + Name: aws.String(plan.Name.ValueString()), + Type: aws.String(plan.Type.ValueString()), + DataPrivacy: dpInput, + IdleSessionTTLInSeconds: aws.Int64(plan.IdleSessionTTLInSeconds.ValueInt64()), + RoleARN: aws.String(plan.RoleARN.ValueString()), + Tags: getTagsIn(ctx), + } + + if !plan.Description.IsNull() { + // TIP: Optional fields should be set based on whether or not they are + // used. + in.Description = aws.String(plan.Description.ValueString()) + } + + // TIP: -- 4. Call the AWS create function + 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 + } + + // TIP: -- 5. Using the output from the create function, set the minimum attributes + plan.ID = flex.StringToFramework(ctx, out.BotId) + + // // TIP: -- 6. Use a waiter to wait for create to complete + // createTimeout := r.CreateTimeout(ctx, plan.Timeouts) + // _, err = waitBotCreated(ctx, conn, plan.ID.ValueString(), createTimeout) + // if err != nil { + // resp.Diagnostics.AddError( + // create.ProblemStandardMessage(names.LexV2Models, create.ErrActionWaitingForCreation, ResNameBot, plan.Name.String(), err), + // err.Error(), + // ) + // return + // } + + // TIP: -- 7. Save the request plan to response state + state := plan + // resp.Diagnostics.Append(state.refreshFromOutput(ctx, out.BotId)...) + resp.Diagnostics.Append(resp.State.Set(ctx, state)...) +} + +func (r *resourceBot) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) { + // TIP: ==== RESOURCE READ ==== + // Generally, the Read function should do the following things. Make + // sure there is a good reason if you don't do one of these. + // + // 1. Get a client connection to the relevant service + // 2. Fetch the state + // 3. Get the resource from AWS + // 4. Remove resource from state if it is not found + // 5. Set the arguments and attributes + // 6. Set the state + + // TIP: -- 1. Get a client connection to the relevant service + conn := r.Meta().LexV2ModelsClient(ctx) + + // TIP: -- 2. Fetch the state + var state resourceBotData + resp.Diagnostics.Append(req.State.Get(ctx, &state)...) + if resp.Diagnostics.HasError() { + return + } + + // TIP: -- 3. Get the resource from AWS using an API Get, List, or Describe- + // type function, or, better yet, using a finder. + out, err := FindBotByID(ctx, conn, state.ID.ValueString()) + // TIP: -- 4. Remove resource from state if it is not found + 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 + } + + // TIP: -- 5. Set the arguments and attributes + // + // For simple data types (i.e., schema.StringAttribute, schema.BoolAttribute, + // schema.Int64Attribute, and schema.Float64Attribue), simply setting the + // appropriate data struct field is sufficient. The flex package implements + // helpers for converting between Go and Plugin-Framework types seamlessly. No + // error or nil checking is necessary. + // + // However, there are some situations where more handling is needed such as + // complex data types (e.g., schema.ListAttribute, schema.SetAttribute). In + // these cases the flatten function may have a diagnostics return value, which + // should be appended to resp.Diagnostics. + state.RoleARN = flex.StringToFramework(ctx, out.RoleARN) + state.ID = flex.StringToFramework(ctx, out.BotId) + state.Name = flex.StringToFramework(ctx, out.BotName) + state.Type = flex.StringToFramework(ctx, out.BotType) + state.Description = flex.StringToFramework(ctx, out.Description) + state.Type = flex.StringToFramework(ctx, out.Type) + state.IdleSessionTTLInSeconds = flex.Int64ToFramework(ctx, out.IdleSessionTTLInSeconds) + + // TIP: Setting a complex type. + datap, d := flattenDataPrivacy(ctx, out.DataPrivacy) + state.DataPrivacy = datap + setTagsOut(ctx, out.Tags) + resp.Diagnostics.Append(d...) + + // TIP: -- 6. Set the state + resp.Diagnostics.Append(resp.State.Set(ctx, &state)...) +} + +func (r *resourceBot) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) { + // TIP: ==== RESOURCE UPDATE ==== + // Not all resources have Update functions. There are a few reasons: + // a. The AWS API does not support changing a resource + // b. All arguments have RequiresReplace() plan modifiers + // c. The AWS API uses a create call to modify an existing resource + // + // In the cases of a. and b., the resource will not have an update method + // defined. In the case of c., Update and Create can be refactored to call + // the same underlying function. + // + // The rest of the time, there should be an Update function and it should + // do the following things. Make sure there is a good reason if you don't + // do one of these. + // + // 1. Get a client connection to the relevant service + // 2. Fetch the plan and state + // 3. Populate a modify input structure and check for changes + // 4. Call the AWS modify/update function + // 5. Use a waiter to wait for update to complete + // 6. Save the request plan to response state + // TIP: -- 1. Get a client connection to the relevant service + conn := r.Meta().LexV2ModelsClient(ctx) + + // TIP: -- 2. Fetch the plan + 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 + } + + // TIP: -- 3. Populate a modify input structure and check for changes + if !plan.Name.Equal(state.Name) || + !plan.Description.Equal(state.Description) || + !plan.ComplexArgument.Equal(state.ComplexArgument) || + !plan.Type.Equal(state.Type) { + + in := &lexv2models.UpdateBotInput{ + // TIP: Mandatory or fields that will always be present can be set when + // you create the Input structure. (Replace these with real fields.) + BotId: aws.String(plan.ID.ValueString()), + BotName: aws.String(plan.Name.ValueString()), + BotType: aws.String(plan.Type.ValueString()), + } + + if !plan.Description.IsNull() { + // TIP: Optional fields should be set based on whether or not they are + // used. + in.Description = aws.String(plan.Description.ValueString()) + } + if !plan.ComplexArgument.IsNull() { + // TIP: Use an expander to assign a complex argument. The elements must be + // deserialized into the appropriate struct before being passed to the expander. + var tfList []complexArgumentData + resp.Diagnostics.Append(plan.ComplexArgument.ElementsAs(ctx, &tfList, false)...) + if resp.Diagnostics.HasError() { + return + } + + in.ComplexArgument = expandComplexArgument(tfList) + } + + // TIP: -- 4. Call the AWS modify/update function + out, 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 + } + if out == nil || out.Bot == nil { + resp.Diagnostics.AddError( + create.ProblemStandardMessage(names.LexV2Models, create.ErrActionUpdating, ResNameBot, plan.ID.String(), nil), + errors.New("empty output").Error(), + ) + return + } + + // TIP: Using the output from the update function, re-set any computed attributes + plan.ARN = flex.StringToFramework(ctx, out.Bot.Arn) + plan.ID = flex.StringToFramework(ctx, out.Bot.BotId) + } + + + // TIP: -- 5. Use a waiter to wait for update to complete + updateTimeout := r.UpdateTimeout(ctx, plan.Timeouts) + _, 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 + } + + + // TIP: -- 6. Save the request plan to response state + resp.Diagnostics.Append(resp.State.Set(ctx, &plan)...) +} + +func (r *resourceBot) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) { + // TIP: ==== RESOURCE DELETE ==== + // Most resources have Delete functions. There are rare situations + // where you might not need a delete: + // a. The AWS API does not provide a way to delete the resource + // b. The point of your resource is to perform an action (e.g., reboot a + // server) and deleting serves no purpose. + // + // The Delete function should do the following things. Make sure there + // is a good reason if you don't do one of these. + // + // 1. Get a client connection to the relevant service + // 2. Fetch the state + // 3. Populate a delete input structure + // 4. Call the AWS delete function + // 5. Use a waiter to wait for delete to complete + // TIP: -- 1. Get a client connection to the relevant service + conn := r.Meta().LexV2ModelsClient(ctx) + + // TIP: -- 2. Fetch the state + var state resourceBotData + resp.Diagnostics.Append(req.State.Get(ctx, &state)...) + if resp.Diagnostics.HasError() { + return + } + + // TIP: -- 3. Populate a delete input structure + in := &lexv2models.DeleteBotInput{ + BotId: aws.String(state.ID.ValueString()), + } + + // TIP: -- 4. Call the AWS delete function + _, err := conn.DeleteBot(ctx, in) + // TIP: On rare occassions, the API returns a not found error after deleting a + // resource. If that happens, we don't want it to show up as an error. + 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 + } + + // TIP: -- 5. Use a waiter to wait for delete to complete + 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 + } +} + +// TIP: ==== TERRAFORM IMPORTING ==== +// If Read can get all the information it needs from the Identifier +// (i.e., path.Root("id")), you can use the PassthroughID importer. Otherwise, +// you'll need a custom import function. +// +// See more: +// https://developer.hashicorp.com/terraform/plugin/framework/resources/import +func (r *resourceBot) ImportState(ctx context.Context, req resource.ImportStateRequest, resp *resource.ImportStateResponse) { + resource.ImportStatePassthroughID(ctx, path.Root("id"), req, resp) +} + + +// TIP: ==== STATUS CONSTANTS ==== +// Create constants for states and statuses if the service does not +// already have suitable constants. We prefer that you use the constants +// provided in the service if available (e.g., amp.WorkspaceStatusCodeActive). +const ( + statusChangePending = "Pending" + statusDeleting = "Deleting" + statusNormal = "Normal" + statusUpdated = "Updated" +) + +// TIP: ==== WAITERS ==== +// Some resources of some services have waiters provided by the AWS API. +// Unless they do not work properly, use them rather than defining new ones +// here. +// +// Sometimes we define the wait, status, and find functions in separate +// files, wait.go, status.go, and find.go. Follow the pattern set out in the +// service and define these where it makes the most sense. +// +// If these functions are used in the _test.go file, they will need to be +// exported (i.e., capitalized). +// +// You will need to adjust the parameters and names to fit the service. +func waitBotCreated(ctx context.Context, conn *lexv2models.Client, id string, timeout time.Duration) (*lexv2models.Bot, error) { + stateConf := &retry.StateChangeConf{ + Pending: []string{}, + Target: []string{statusNormal}, + Refresh: statusBot(ctx, conn, id), + Timeout: timeout, + NotFoundChecks: 20, + ContinuousTargetOccurence: 2, + } + + outputRaw, err := stateConf.WaitForStateContext(ctx) + if out, ok := outputRaw.(*lexv2models.Bot); ok { + return out, err + } + + return nil, err +} + +// TIP: It is easier to determine whether a resource is updated for some +// resources than others. The best case is a status flag that tells you when +// the update has been fully realized. Other times, you can check to see if a +// key resource argument is updated to a new value or not. +func waitBotUpdated(ctx context.Context, conn *lexv2models.Client, id string, timeout time.Duration) (*lexv2models.Bot, error) { + stateConf := &retry.StateChangeConf{ + Pending: []string{statusChangePending}, + Target: []string{statusUpdated}, + Refresh: statusBot(ctx, conn, id), + Timeout: timeout, + NotFoundChecks: 20, + ContinuousTargetOccurence: 2, + } + + outputRaw, err := stateConf.WaitForStateContext(ctx) + if out, ok := outputRaw.(*lexv2models.Bot); ok { + return out, err + } + + return nil, err +} + +// TIP: A deleted waiter is almost like a backwards created waiter. There may +// be additional pending states, however. +func waitBotDeleted(ctx context.Context, conn *lexv2models.Client, id string, timeout time.Duration) (*lexv2models.Bot, error) { + stateConf := &retry.StateChangeConf{ + Pending: []string{statusDeleting, statusNormal}, + Target: []string{}, + Refresh: statusBot(ctx, conn, id), + Timeout: timeout, + } + + outputRaw, err := stateConf.WaitForStateContext(ctx) + if out, ok := outputRaw.(*lexv2models.Bot); ok { + return out, err + } + + return nil, err +} + +// TIP: ==== STATUS ==== +// The status function can return an actual status when that field is +// available from the API (e.g., out.Status). Otherwise, you can use custom +// statuses to communicate the states of the resource. +// +// Waiters consume the values returned by status functions. Design status so +// that it can be reused by a create, update, and delete waiter, if possible. +func statusBot(ctx context.Context, conn *lexv2models.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(out.Status), nil + } +} + +// TIP: ==== FINDERS ==== +// The find function is not strictly necessary. You could do the API +// request from the status function. However, we have found that find often +// comes in handy in other places besides the status function. As a result, it +// is good practice to define it separately. +func FindBotByID(ctx context.Context, conn *lexv2models.Client, id string) (*awstypes.Bot, error) { + in := &lexv2models.GetBotInput{ + BotId: aws.String(id), + } + + out, err := conn.ListBot(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.Bot, nil +} + +// TIP: ==== FLEX ==== +// Flatteners and expanders ("flex" functions) help handle complex data +// types. Flatteners take an API data type and return the equivalent Plugin-Framework +// type. In other words, flatteners translate from AWS -> Terraform. +// +// On the other hand, expanders take a Terraform data structure and return +// something that you can send to the AWS API. In other words, expanders +// translate from Terraform -> AWS. +// +// See more: +// https://hashicorp.github.io/terraform-provider-aws/data-handling-and-conversion/ +func flattenDataPrivacy(ctx context.Context, 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": flex.StringValueToFramework(ctx, 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 +} + +// TIP: Remember, as mentioned above, expanders take a Terraform data structure +// and return something that you can send to the AWS API. In other words, +// expanders translate from Terraform -> AWS. +// +// See more: +// https://hashicorp.github.io/terraform-provider-aws/data-handling-and-conversion/ +func expandDataPrivacy(ctx context.Context, tfList []dataPrivacyData) (*awstypes.DataPrivacy, diag.Diagnostics) { + var diags diag.Diagnostics + if len(tfList) == 0 { + return nil + } + + dp := tfList[0] + return &awstypes.DataPrivacy{ + ChildDirected: aws.String(dp.ChildDirected.ValueString()), + }, diags +} + +// TIP: Even when you have a list with max length of 1, this plural function +// works brilliantly. However, if the AWS API takes a structure rather than a +// slice of structures, you will not need it. +func expandMembers(ctx context.Context, tfList []membersData) ([]*awstypes.Members, diag.Diagnostics) { + var diags diag.Diagnostics + + if len(tfList) == 0 { + return nil + } + + mb := tfList[0] + return &awstypes.DataPrivacy{ + AliasID: aws.String(mb.AliasID.ValueString()), + AliasName: aws.String(mb.AliasName.ValueString()), + ID: aws.String(mb.ID.ValueString()), + Name: aws.String(mb.Name.ValueString()), + Version: aws.String(mb.Version.ValueString()), + }, diags +} + +// TIP: ==== DATA STRUCTURES ==== +// With Terraform Plugin-Framework configurations are deserialized into +// Go types, providing type safety without the need for type assertions. +// These structs should match the schema definition exactly, and the `tfsdk` +// tag value should match the attribute name. +// +// Nested objects are represented in their own data struct. These will +// also have a corresponding attribute type mapping for use inside flex +// functions. +// +// See more: +// https://developer.hashicorp.com/terraform/plugin/framework/handling-data/accessing-values +type resourceBotData struct { + 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 types.String `tfsdk:"role_arn"` + tags types.Map `tfsdk:"tags"` + TestBotAliasTags types.Map `tfsdk:"test_bot_alias_tags"` + Timeouts timeouts.Value `tfsdk:"timeouts"` + Type types.String `tfsdk:"type"` +} + +type dataPrivacyData struct { + ChildDirected types.String `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.StringType, +} \ No newline at end of file diff --git a/internal/service/lexv2models/bot_test.go b/internal/service/lexv2models/bot_test.go new file mode 100644 index 00000000000..4b37b4cf2c8 --- /dev/null +++ b/internal/service/lexv2models/bot_test.go @@ -0,0 +1,334 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package lexv2models_test +// **PLEASE DELETE THIS AND ALL TIP COMMENTS BEFORE SUBMITTING A PR FOR REVIEW!** +// +// TIP: ==== INTRODUCTION ==== +// Thank you for trying the skaff tool! +// +// You have opted to include these helpful comments. They all include "TIP:" +// to help you find and remove them when you're done with them. +// +// While some aspects of this file are customized to your input, the +// scaffold tool does *not* look at the AWS API and ensure it has correct +// function, structure, and variable names. It makes guesses based on +// commonalities. You will need to make significant adjustments. +// +// In other words, as generated, this is a rough outline of the work you will +// need to do. If something doesn't make sense for your situation, get rid of +// it. + +import ( + // TIP: ==== IMPORTS ==== + // This is a common set of imports but not customized to your code since + // your code hasn't been written yet. Make sure you, your IDE, or + // goimports -w fixes these imports. + // + // The provider linter wants your imports to be in two groups: first, + // standard library (i.e., "fmt" or "strings"), second, everything else. + // + // Also, AWS Go SDK v2 may handle nested structures differently than v1, + // using the services/lexv2models/types package. If so, you'll + // need to import types and reference the nested types, e.g., as + // types.. + "context" + "fmt" + "regexp" + "strings" + "testing" + + "github.com/YakDriver/regexache" + + "github.com/aws/aws-sdk-go-v2/aws" + "github.com/aws/aws-sdk-go-v2/service/lexv2models" + "github.com/aws/aws-sdk-go-v2/service/lexv2models/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" + "github.com/hashicorp/terraform-provider-aws/internal/errs" + "github.com/hashicorp/terraform-provider-aws/names" + + // TIP: You will often need to import the package that this test file lives + // in. Since it is in the "test" context, it must import the package to use + // any normal context constants, variables, or functions. + tflexv2models "github.com/hashicorp/terraform-provider-aws/internal/service/lexv2models" +) + +// TIP: File Structure. The basic outline for all test files should be as +// follows. Improve this resource's maintainability by following this +// outline. +// +// 1. Package declaration (add "_test" since this is a test file) +// 2. Imports +// 3. Unit tests +// 4. Basic test +// 5. Disappears test +// 6. All the other tests +// 7. Helper functions (exists, destroy, check, etc.) +// 8. Functions that return Terraform configurations + +// TIP: ==== UNIT TESTS ==== +// This is an example of a unit test. Its name is not prefixed with +// "TestAcc" like an acceptance test. +// +// Unlike acceptance tests, unit tests do not access AWS and are focused on a +// function (or method). Because of this, they are quick and cheap to run. +// +// In designing a resource's implementation, isolate complex bits from AWS bits +// so that they can be tested through a unit test. We encourage more unit tests +// in the provider. +// +// Cut and dry functions using well-used patterns, like typical flatteners and +// expanders, don't need unit testing. However, if they are complex or +// intricate, they should be unit tested. +func TestBotExampleUnitTest(t *testing.T) { + t.Parallel() + + testCases := []struct { + TestName string + Input string + Expected string + Error bool + }{ + { + TestName: "empty", + Input: "", + Expected: "", + Error: true, + }, + { + TestName: "descriptive name", + Input: "some input", + Expected: "some output", + Error: false, + }, + { + TestName: "another descriptive name", + Input: "more input", + Expected: "more output", + Error: false, + }, + } + + for _, testCase := range testCases { + testCase := testCase + t.Run(testCase.TestName, func(t *testing.T) { + t.Parallel() + got, err := tflexv2models.FunctionFromResource(testCase.Input) + + if err != nil && !testCase.Error { + t.Errorf("got error (%s), expected no error", err) + } + + if err == nil && testCase.Error { + t.Errorf("got (%s) and no error, expected error", got) + } + + if got != testCase.Expected { + t.Errorf("got %s, expected %s", got, testCase.Expected) + } + }) + } +} + +// TIP: ==== ACCEPTANCE TESTS ==== +// This is an example of a basic acceptance test. This should test as much of +// standard functionality of the resource as possible, and test importing, if +// applicable. We prefix its name with "TestAcc", the service, and the +// resource name. +// +// Acceptance test access AWS and cost money to run. +func TestAccLexV2ModelsBot_basic(t *testing.T) { + ctx := acctest.Context(t) + // TIP: This is a long-running test guard for tests that run longer than + // 300s (5 min) generally. + if testing.Short() { + t.Skip("skipping long-running test in short mode") + } + + var bot lexv2models.DescribeBotResponse + 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), + Check: resource.ComposeTestCheckFunc( + testAccCheckBotExists(ctx, resourceName, &bot), + resource.TestCheckResourceAttr(resourceName, "auto_minor_version_upgrade", "false"), + resource.TestCheckResourceAttrSet(resourceName, "maintenance_window_start_time.0.day_of_week"), + resource.TestCheckTypeSetElemNestedAttrs(resourceName, "user.*", map[string]string{ + "console_access": "false", + "groups.#": "0", + "username": "Test", + "password": "TestTest1234", + }), + acctest.MatchResourceAttrRegionalARN(resourceName, "arn", "lexv2models", regexache.MustCompile(`bot:+.`)), + ), + }, + { + ResourceName: resourceName, + ImportState: true, + ImportStateVerify: true, + ImportStateVerifyIgnore: []string{"apply_immediately", "user"}, + }, + }, + }) +} + +func TestAccLexV2ModelsBot_disappears(t *testing.T) { + ctx := acctest.Context(t) + if testing.Short() { + t.Skip("skipping long-running test in short mode") + } + + var bot lexv2models.DescribeBotResponse + 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(t) + }, + ErrorCheck: acctest.ErrorCheck(t, names.LexV2ModelsEndpointID), + ProtoV5ProviderFactories: acctest.ProtoV5ProviderFactories, + CheckDestroy: testAccCheckBotDestroy(ctx), + Steps: []resource.TestStep{ + { + Config: testAccBotConfig_basic(rName, testAccBotVersionNewer), + Check: resource.ComposeTestCheckFunc( + testAccCheckBotExists(ctx, resourceName, &bot), + // TIP: The Plugin-Framework disappears helper is similar to the Plugin-SDK version, + // but expects a new resource factory function as the third argument. To expose this + // private function to the testing package, you may need to add a line like the following + // to exports_test.go: + // + // var ResourceBot = newResourceBot + 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 + } + + input := &lexv2models.DescribeBotInput{ + BotId: aws.String(rs.Primary.ID), + } + _, err := conn.DescribeBot(ctx, &lexv2models.DescribeBotInput{ + BotId: aws.String(rs.Primary.ID), + }) + if errs.IsA[*types.ResourceNotFoundException](err){ + return nil + } + if err != nil { + return nil + } + + 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 *lexv2models.DescribeBotResponse) 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 := conn.DescribeBot(ctx, &lexv2models.DescribeBotInput{ + BotId: aws.String(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 := &lexv2models.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 testAccCheckBotNotRecreated(before, after *lexv2models.DescribeBotResponse) resource.TestCheckFunc { + return func(s *terraform.State) error { + if before, after := aws.ToString(before.BotId), aws.ToString(after.BotId); before != after { + return create.Error(names.LexV2Models, create.ErrActionCheckingNotRecreated, tflexv2models.ResNameBot, aws.ToString(before.BotId), errors.New("recreated")) + } + + return nil + } +} + +func testAccBotConfig_basic(rName, version string) string { + return fmt.Sprintf(` +resource "aws_security_group" "test" { + name = %[1]q +} + +resource "aws_lexv2models_bot" "test" { + bot_name = %[1]q + engine_type = "ActiveLexV2Models" + engine_version = %[2]q + host_instance_type = "lexv2models.t2.micro" + security_groups = [aws_security_group.test.id] + authentication_strategy = "simple" + storage_type = "efs" + + logs { + general = true + } + + user { + username = "Test" + password = "TestTest1234" + } +} +`, rName, version) +} diff --git a/website/docs/r/lexv2models_bot.html.markdown b/website/docs/r/lexv2models_bot.html.markdown new file mode 100644 index 00000000000..2b058edb498 --- /dev/null +++ b/website/docs/r/lexv2models_bot.html.markdown @@ -0,0 +1,69 @@ +--- +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" { +} +``` + +## Argument Reference + +The following arguments are required: + +* `example_arg` - (Required) Concise argument description. Do not begin the description with "An", "The", "Defines", "Indicates", or "Specifies," as these are verbose. In other words, "Indicates the amount of storage," can be rewritten as "Amount of storage," without losing any information. + +The following arguments are optional: + +* `optional_arg` - (Optional) Concise argument description. Do not begin the description with "An", "The", "Defines", "Indicates", or "Specifies," as these are verbose. In other words, "Indicates the amount of storage," can be rewritten as "Amount of storage," without losing any information. + +## Attribute Reference + +This resource exports the following attributes in addition to the arguments above: + +* `arn` - ARN of the Bot. Do not begin the description with "An", "The", "Defines", "Indicates", or "Specifies," as these are verbose. In other words, "Indicates the amount of storage," can be rewritten as "Amount of storage," without losing any information. +* `example_attribute` - Concise description. Do not begin the description with "An", "The", "Defines", "Indicates", or "Specifies," as these are verbose. In other words, "Indicates the amount of storage," can be rewritten as "Amount of storage," without losing any information. + +## Timeouts + +[Configuration options](https://developer.hashicorp.com/terraform/language/resources/syntax#operation-timeouts): + +* `create` - (Default `60m`) +* `update` - (Default `180m`) +* `delete` - (Default `90m`) + +## 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 +``` From e1a62f7a48f91eb84b6781e8b43fc870797b0d1b Mon Sep 17 00:00:00 2001 From: Sharon Nam Date: Tue, 19 Sep 2023 00:39:24 -0700 Subject: [PATCH 02/10] update delete and helper functions --- internal/service/lexv2models/bot.go | 278 +++++++++++++++------------- 1 file changed, 149 insertions(+), 129 deletions(-) diff --git a/internal/service/lexv2models/bot.go b/internal/service/lexv2models/bot.go index ef049c218f0..4409dca9e58 100644 --- a/internal/service/lexv2models/bot.go +++ b/internal/service/lexv2models/bot.go @@ -18,7 +18,7 @@ import ( // awstypes.. "context" "errors" - "time" + // "time" "github.com/aws/aws-sdk-go-v2/aws" "github.com/aws/aws-sdk-go-v2/service/lexv2models" @@ -34,11 +34,12 @@ import ( "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/diag" + // "github.com/hashicorp/terraform-plugin-sdk/v2/diag" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/retry" "github.com/hashicorp/terraform-provider-aws/internal/create" "github.com/hashicorp/terraform-provider-aws/internal/framework" "github.com/hashicorp/terraform-provider-aws/internal/framework/flex" + tftags "github.com/hashicorp/terraform-provider-aws/internal/tags" "github.com/hashicorp/terraform-provider-aws/internal/tfresource" "github.com/hashicorp/terraform-provider-aws/names" ) @@ -145,6 +146,8 @@ func (r *resourceBot) Schema(ctx context.Context, req resource.SchemaRequest, re stringplanmodifier.RequiresReplace(), }, }, + names.AttrTags: tftags.TagsAttribute(), + names.AttrTagsAll: tftags.TagsAttributeComputedOnly(), "role_arn": schema.StringAttribute{ Required: true, }, @@ -202,20 +205,6 @@ func (r *resourceBot) Schema(ctx context.Context, req resource.SchemaRequest, re } func (r *resourceBot) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) { - // TIP: ==== RESOURCE CREATE ==== - // Generally, the Create function should do the following things. Make - // sure there is a good reason if you don't do one of these. - // - // 1. Get a client connection to the relevant service - // 2. Fetch the plan - // 3. Populate a create input structure - // 4. Call the AWS create/put function - // 5. Using the output from the create function, set the minimum arguments - // and attributes for the Read function to work, as well as any computed - // only attributes. - // 6. Use a waiter to wait for create to complete - // 7. Save the request plan to response state - // TIP: -- 1. Get a client connection to the relevant service conn := r.Meta().LexV2ModelsClient(ctx) @@ -343,19 +332,8 @@ func (r *resourceBot) Read(ctx context.Context, req resource.ReadRequest, resp * // complex data types (e.g., schema.ListAttribute, schema.SetAttribute). In // these cases the flatten function may have a diagnostics return value, which // should be appended to resp.Diagnostics. - state.RoleARN = flex.StringToFramework(ctx, out.RoleARN) - state.ID = flex.StringToFramework(ctx, out.BotId) - state.Name = flex.StringToFramework(ctx, out.BotName) - state.Type = flex.StringToFramework(ctx, out.BotType) - state.Description = flex.StringToFramework(ctx, out.Description) - state.Type = flex.StringToFramework(ctx, out.Type) - state.IdleSessionTTLInSeconds = flex.Int64ToFramework(ctx, out.IdleSessionTTLInSeconds) - // TIP: Setting a complex type. - datap, d := flattenDataPrivacy(ctx, out.DataPrivacy) - state.DataPrivacy = datap - setTagsOut(ctx, out.Tags) - resp.Diagnostics.Append(d...) + resp.Diagnostics.Append(state.refreshFromOutput(ctx, out)...) // TIP: -- 6. Set the state resp.Diagnostics.Append(resp.State.Set(ctx, &state)...) @@ -384,7 +362,7 @@ func (r *resourceBot) Update(ctx context.Context, req resource.UpdateRequest, re // 6. Save the request plan to response state // TIP: -- 1. Get a client connection to the relevant service conn := r.Meta().LexV2ModelsClient(ctx) - + // TIP: -- 2. Fetch the plan var plan, state resourceBotData resp.Diagnostics.Append(req.Plan.Get(ctx, &plan)...) @@ -392,38 +370,60 @@ func (r *resourceBot) Update(ctx context.Context, req resource.UpdateRequest, re if resp.Diagnostics.HasError() { return } - + // TIP: -- 3. Populate a modify input structure and check for changes if !plan.Name.Equal(state.Name) || !plan.Description.Equal(state.Description) || - !plan.ComplexArgument.Equal(state.ComplexArgument) || + !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, d := expandDataPrivacy(ctx, dp) + resp.Diagnostics.Append(d...) + if resp.Diagnostics.HasError() { + return + } + in := &lexv2models.UpdateBotInput{ // TIP: Mandatory or fields that will always be present can be set when // you create the Input structure. (Replace these with real fields.) - BotId: aws.String(plan.ID.ValueString()), - BotName: aws.String(plan.Name.ValueString()), - BotType: aws.String(plan.Type.ValueString()), + Id: aws.String(plan.ID.ValueString()), + Name: aws.String(plan.Name.ValueString()), + Type: aws.String(plan.Type.ValueString()), + IdleSessionTTLInSeconds: aws.Int64(plan.IdleSessionTTLInSeconds.ValueInt64()), + DataPrivacy: dpInput, + RoleARN: aws.String(plan.RoleARN.ValueString()), } if !plan.Description.IsNull() { - // TIP: Optional fields should be set based on whether or not they are - // used. in.Description = aws.String(plan.Description.ValueString()) } - if !plan.ComplexArgument.IsNull() { + if !plan.Type.IsNull() { + in.Type = aws.String(plan.Type.ValueString()) + } + if !plan.Members.IsNull() { // TIP: Use an expander to assign a complex argument. The elements must be // deserialized into the appropriate struct before being passed to the expander. - var tfList []complexArgumentData - resp.Diagnostics.Append(plan.ComplexArgument.ElementsAs(ctx, &tfList, false)...) + var tfList []membersData + resp.Diagnostics.Append(plan.Members.ElementsAs(ctx, &tfList, false)...) if resp.Diagnostics.HasError() { return } - in.ComplexArgument = expandComplexArgument(tfList) + in.ComplexArgument, d = expandMembers(ctx, tfList) + resp.Diagnostics.Append(d...) + if resp.Diagnostics.HasError() { + return + } } - // TIP: -- 4. Call the AWS modify/update function out, err := conn.UpdateBot(ctx, in) if err != nil { @@ -433,32 +433,29 @@ func (r *resourceBot) Update(ctx context.Context, req resource.UpdateRequest, re ) return } - if out == nil || out.Bot == nil { + 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 } - // TIP: Using the output from the update function, re-set any computed attributes - plan.ARN = flex.StringToFramework(ctx, out.Bot.Arn) - plan.ID = flex.StringToFramework(ctx, out.Bot.BotId) + state.refreshFromOutput(ctx, out.BotId) } // TIP: -- 5. Use a waiter to wait for update to complete - updateTimeout := r.UpdateTimeout(ctx, plan.Timeouts) - _, 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 - } + // updateTimeout := r.UpdateTimeout(ctx, plan.Timeouts) + // _, 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 + // } - // TIP: -- 6. Save the request plan to response state resp.Diagnostics.Append(resp.State.Set(ctx, &plan)...) } @@ -481,19 +478,19 @@ func (r *resourceBot) Delete(ctx context.Context, req resource.DeleteRequest, re // 5. Use a waiter to wait for delete to complete // TIP: -- 1. Get a client connection to the relevant service conn := r.Meta().LexV2ModelsClient(ctx) - + // TIP: -- 2. Fetch the state var state resourceBotData resp.Diagnostics.Append(req.State.Get(ctx, &state)...) if resp.Diagnostics.HasError() { return } - + // TIP: -- 3. Populate a delete input structure in := &lexv2models.DeleteBotInput{ BotId: aws.String(state.ID.ValueString()), } - + // TIP: -- 4. Call the AWS delete function _, err := conn.DeleteBot(ctx, in) // TIP: On rare occassions, the API returns a not found error after deleting a @@ -509,17 +506,17 @@ func (r *resourceBot) Delete(ctx context.Context, req resource.DeleteRequest, re ) return } - + // TIP: -- 5. Use a waiter to wait for delete to complete - 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 - } + // 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 + // } } // TIP: ==== TERRAFORM IMPORTING ==== @@ -558,63 +555,63 @@ const ( // exported (i.e., capitalized). // // You will need to adjust the parameters and names to fit the service. -func waitBotCreated(ctx context.Context, conn *lexv2models.Client, id string, timeout time.Duration) (*lexv2models.Bot, error) { - stateConf := &retry.StateChangeConf{ - Pending: []string{}, - Target: []string{statusNormal}, - Refresh: statusBot(ctx, conn, id), - Timeout: timeout, - NotFoundChecks: 20, - ContinuousTargetOccurence: 2, - } - - outputRaw, err := stateConf.WaitForStateContext(ctx) - if out, ok := outputRaw.(*lexv2models.Bot); ok { - return out, err - } - - return nil, err -} +// func waitBotCreated(ctx context.Context, conn *lexv2models.Client, id string, timeout time.Duration) (*lexv2models.Bot, error) { +// stateConf := &retry.StateChangeConf{ +// Pending: []string{}, +// Target: []string{statusNormal}, +// Refresh: statusBot(ctx, conn, id), +// Timeout: timeout, +// NotFoundChecks: 20, +// ContinuousTargetOccurence: 2, +// } + +// outputRaw, err := stateConf.WaitForStateContext(ctx) +// if out, ok := outputRaw.(*lexv2models.Bot); ok { +// return out, err +// } + +// return nil, err +// } // TIP: It is easier to determine whether a resource is updated for some // resources than others. The best case is a status flag that tells you when // the update has been fully realized. Other times, you can check to see if a // key resource argument is updated to a new value or not. -func waitBotUpdated(ctx context.Context, conn *lexv2models.Client, id string, timeout time.Duration) (*lexv2models.Bot, error) { - stateConf := &retry.StateChangeConf{ - Pending: []string{statusChangePending}, - Target: []string{statusUpdated}, - Refresh: statusBot(ctx, conn, id), - Timeout: timeout, - NotFoundChecks: 20, - ContinuousTargetOccurence: 2, - } - - outputRaw, err := stateConf.WaitForStateContext(ctx) - if out, ok := outputRaw.(*lexv2models.Bot); ok { - return out, err - } - - return nil, err -} +// func waitBotUpdated(ctx context.Context, conn *lexv2models.Client, id string, timeout time.Duration) (*lexv2models.Bot, error) { +// stateConf := &retry.StateChangeConf{ +// Pending: []string{statusChangePending}, +// Target: []string{statusUpdated}, +// Refresh: statusBot(ctx, conn, id), +// Timeout: timeout, +// NotFoundChecks: 20, +// ContinuousTargetOccurence: 2, +// } + +// outputRaw, err := stateConf.WaitForStateContext(ctx) +// if out, ok := outputRaw.(*lexv2models.Bot); ok { +// return out, err +// } + +// return nil, err +// } // TIP: A deleted waiter is almost like a backwards created waiter. There may // be additional pending states, however. -func waitBotDeleted(ctx context.Context, conn *lexv2models.Client, id string, timeout time.Duration) (*lexv2models.Bot, error) { - stateConf := &retry.StateChangeConf{ - Pending: []string{statusDeleting, statusNormal}, - Target: []string{}, - Refresh: statusBot(ctx, conn, id), - Timeout: timeout, - } - - outputRaw, err := stateConf.WaitForStateContext(ctx) - if out, ok := outputRaw.(*lexv2models.Bot); ok { - return out, err - } - - return nil, err -} +// func waitBotDeleted(ctx context.Context, conn *lexv2models.Client, id string, timeout time.Duration) (*lexv2models.Bot, error) { +// stateConf := &retry.StateChangeConf{ +// Pending: []string{statusDeleting, statusNormal}, +// Target: []string{}, +// Refresh: statusBot(ctx, conn, id), +// Timeout: timeout, +// } + +// outputRaw, err := stateConf.WaitForStateContext(ctx) +// if out, ok := outputRaw.(*lexv2models.Bot); ok { +// return out, err +// } + +// return nil, err +// } // TIP: ==== STATUS ==== // The status function can return an actual status when that field is @@ -623,20 +620,20 @@ func waitBotDeleted(ctx context.Context, conn *lexv2models.Client, id string, ti // // Waiters consume the values returned by status functions. Design status so // that it can be reused by a create, update, and delete waiter, if possible. -func statusBot(ctx context.Context, conn *lexv2models.Client, id string) retry.StateRefreshFunc { - return func() (interface{}, string, error) { - out, err := findBotByID(ctx, conn, id) - if tfresource.NotFound(err) { - return nil, "", nil - } +// func statusBot(ctx context.Context, conn *lexv2models.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 - } +// if err != nil { +// return nil, "", err +// } - return out, aws.ToString(out.Status), nil - } -} +// return out, aws.ToString(out.Status), nil +// } +// } // TIP: ==== FINDERS ==== // The find function is not strictly necessary. You could do the API @@ -737,6 +734,29 @@ func expandMembers(ctx context.Context, tfList []membersData) ([]*awstypes.Membe }, diags } +func (rd *resourceBotData) refreshFromOutput(ctx context.Context, out *awstypes.Bot) diag.Diagnostics { + var diags diag.Diagnostics + + if out == nil { + return diags + } + rd.RoleARN = flex.StringToFramework(ctx, out.RoleARN) + rd.ID = flex.StringToFramework(ctx, out.BotId) + rd.Name = flex.StringToFramework(ctx, out.BotName) + rd.Type = flex.StringToFramework(ctx, out.BotType) + rd.Description = flex.StringToFramework(ctx, out.Description) + rd.Type = flex.StringToFramework(ctx, out.Type) + rd.IdleSessionTTLInSeconds = flex.Int64ToFramework(ctx, out.IdleSessionTTLInSeconds) + + // TIP: Setting a complex type. + datap, d := flattenDataPrivacy(ctx, out.DataPrivacy) + diags.Append(d...) + rd.DataPrivacy = datap + setTagsOut(ctx, out.Tags) + + return diags +} + // TIP: ==== DATA STRUCTURES ==== // With Terraform Plugin-Framework configurations are deserialized into // Go types, providing type safety without the need for type assertions. From 6c10a2de06d2cf3e315b0594b50f67d3e0076b18 Mon Sep 17 00:00:00 2001 From: Sharon Nam Date: Wed, 20 Sep 2023 13:41:55 -0700 Subject: [PATCH 03/10] added tests and other working parts --- internal/framework/flex/int.go | 8 + internal/framework/flex/int_test.go | 36 ++ internal/service/lexv2models/bot.go | 580 +++++------------- internal/service/lexv2models/bot_test.go | 253 ++------ internal/service/lexv2models/exports_test.go | 9 + .../lexv2models/service_package_gen.go | 7 +- names/names.go | 1 + 7 files changed, 285 insertions(+), 609 deletions(-) create mode 100644 internal/service/lexv2models/exports_test.go 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 index 4409dca9e58..c906c5649da 100644 --- a/internal/service/lexv2models/bot.go +++ b/internal/service/lexv2models/bot.go @@ -4,25 +4,14 @@ package lexv2models import ( - // TIP: ==== IMPORTS ==== - // This is a common set of imports but not customized to your code since - // your code hasn't been written yet. Make sure you, your IDE, or - // goimports -w fixes these imports. - // - // The provider linter wants your imports to be in two groups: first, - // standard library (i.e., "fmt" or "strings"), second, everything else. - // - // Also, AWS Go SDK v2 may handle nested structures differently than v1, - // using the services/lexv2models/types package. If so, you'll - // need to import types and reference the nested types, e.g., as - // awstypes.. "context" "errors" - // "time" + "strconv" + "time" "github.com/aws/aws-sdk-go-v2/aws" - "github.com/aws/aws-sdk-go-v2/service/lexv2models" - awstypes "github.com/aws/aws-sdk-go-v2/service/lexv2models/types" + "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" @@ -34,7 +23,6 @@ import ( "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/diag" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/retry" "github.com/hashicorp/terraform-provider-aws/internal/create" "github.com/hashicorp/terraform-provider-aws/internal/framework" @@ -44,24 +32,13 @@ import ( "github.com/hashicorp/terraform-provider-aws/names" ) -// TIP: ==== FILE STRUCTURE ==== -// All resources should follow this basic outline. Improve this resource's -// maintainability by sticking to it. -// -// 1. Package declaration -// 2. Imports -// 3. Main resource struct with schema method -// 4. Create, read, update, delete methods (in that order) -// 5. Other functions (flatteners, expanders, waiters, finders, etc.) - -// Function annotations are used for resource registration to the Provider. DO NOT EDIT. // @FrameworkResource(name="Bot") func newResourceBot(_ context.Context) (resource.ResourceWithConfigure, error) { r := &resourceBot{} - // r.SetDefaultCreateTimeout(30 * time.Minute) - // r.SetDefaultUpdateTimeout(30 * time.Minute) - // r.SetDefaultDeleteTimeout(30 * time.Minute) + r.SetDefaultCreateTimeout(30 * time.Minute) + r.SetDefaultUpdateTimeout(30 * time.Minute) + r.SetDefaultDeleteTimeout(30 * time.Minute) return r, nil } @@ -79,51 +56,9 @@ func (r *resourceBot) Metadata(_ context.Context, req resource.MetadataRequest, resp.TypeName = "aws_lexv2models_bot" } -// TIP: ==== SCHEMA ==== -// In the schema, add each of the attributes in snake case (e.g., -// delete_automated_backups). -// -// Formatting rules: -// * Alphabetize attributes to make them easier to find. -// * Do not add a blank line between attributes. -// -// Attribute basics: -// * If a user can provide a value ("configure a value") for an -// attribute (e.g., instances = 5), we call the attribute an -// "argument." -// * You change the way users interact with attributes using: -// - Required -// - Optional -// - Computed -// * There are only four valid combinations: -// -// 1. Required only - the user must provide a value -// Required: true, -// -// 2. Optional only - the user can configure or omit a value; do not -// use Default or DefaultFunc -// Optional: true, -// -// 3. Computed only - the provider can provide a value but the user -// cannot, i.e., read-only -// Computed: true, -// -// 4. Optional AND Computed - the provider or user can provide a value; -// use this combination if you are using Default -// Optional: true, -// Computed: true, -// -// You will typically find arguments in the input struct -// (e.g., CreateDBInstanceInput) for the create operation. Sometimes -// they are only in the input struct (e.g., ModifyDBInstanceInput) for -// the modify operation. -// -// For more about schema options, visit -// https://developer.hashicorp.com/terraform/plugin/framework/handling-data/schemas?page=schemas 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, }, @@ -133,15 +68,6 @@ func (r *resourceBot) Schema(ctx context.Context, req resource.SchemaRequest, re }, "name": schema.StringAttribute{ Required: true, - // TIP: ==== PLAN MODIFIERS ==== - // Plan modifiers were introduced with Plugin-Framework to provide a mechanism - // for adjusting planned changes prior to apply. The planmodifier subpackage - // provides built-in modifiers for many common use cases such as - // requiring replacement on a value change ("ForceNew: true" in Plugin-SDK - // resources). - // - // See more: - // https://developer.hashicorp.com/terraform/plugin/framework/resources/plan-modification PlanModifiers: []planmodifier.String{ stringplanmodifier.RequiresReplace(), }, @@ -152,10 +78,10 @@ func (r *resourceBot) Schema(ctx context.Context, req resource.SchemaRequest, re Required: true, }, "test_bot_alias_tags": schema.StringAttribute{ - Required: false, + Optional: true, }, "type": schema.StringAttribute{ - Required: true, + Optional: true, }, }, Blocks: map[string]schema.Block{ @@ -189,26 +115,24 @@ func (r *resourceBot) Schema(ctx context.Context, req resource.SchemaRequest, re }, NestedObject: schema.NestedBlockObject{ Attributes: map[string]schema.Attribute{ - "child_directed": schema.StringAttribute{ + "child_directed": schema.BoolAttribute{ Required: true, }, }, }, }, - // "timeouts": timeouts.Block(ctx, timeouts.Opts{ - // Create: true, - // Update: true, - // Delete: 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) { - // TIP: -- 1. Get a client connection to the relevant service conn := r.Meta().LexV2ModelsClient(ctx) - // TIP: -- 2. Fetch the plan var plan resourceBotData resp.Diagnostics.Append(req.Plan.Get(ctx, &plan)...) if resp.Diagnostics.HasError() { @@ -227,26 +151,18 @@ func (r *resourceBot) Create(ctx context.Context, req resource.CreateRequest, re return } - // TIP: -- 3. Populate a create input structure - in := &lexv2models.CreateBotInput{ - // TIP: Mandatory or fields that will always be present can be set when - // you create the Input structure. (Replace these with real fields.) - Name: aws.String(plan.Name.ValueString()), - Type: aws.String(plan.Type.ValueString()), + in := lexmodelsv2.CreateBotInput{ + BotName: aws.String(plan.Name.ValueString()), DataPrivacy: dpInput, - IdleSessionTTLInSeconds: aws.Int64(plan.IdleSessionTTLInSeconds.ValueInt64()), - RoleARN: aws.String(plan.RoleARN.ValueString()), - Tags: getTagsIn(ctx), + IdleSessionTTLInSeconds: aws.Int32(int32(plan.IdleSessionTTLInSeconds.ValueInt64())), + RoleArn: aws.String(plan.RoleARN.ValueString()), } if !plan.Description.IsNull() { - // TIP: Optional fields should be set based on whether or not they are - // used. in.Description = aws.String(plan.Description.ValueString()) } - // TIP: -- 4. Call the AWS create function - out, err := conn.CreateBot(ctx, in) + out, err := conn.CreateBot(ctx, &in) if err != nil { resp.Diagnostics.AddError( create.ProblemStandardMessage(names.LexV2Models, create.ErrActionCreating, ResNameBot, plan.Name.String(), err), @@ -262,52 +178,22 @@ func (r *resourceBot) Create(ctx context.Context, req resource.CreateRequest, re return } - // TIP: -- 5. Using the output from the create function, set the minimum attributes plan.ID = flex.StringToFramework(ctx, out.BotId) - - // // TIP: -- 6. Use a waiter to wait for create to complete - // createTimeout := r.CreateTimeout(ctx, plan.Timeouts) - // _, err = waitBotCreated(ctx, conn, plan.ID.ValueString(), createTimeout) - // if err != nil { - // resp.Diagnostics.AddError( - // create.ProblemStandardMessage(names.LexV2Models, create.ErrActionWaitingForCreation, ResNameBot, plan.Name.String(), err), - // err.Error(), - // ) - // return - // } - - // TIP: -- 7. Save the request plan to response state state := plan // resp.Diagnostics.Append(state.refreshFromOutput(ctx, out.BotId)...) resp.Diagnostics.Append(resp.State.Set(ctx, state)...) } func (r *resourceBot) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) { - // TIP: ==== RESOURCE READ ==== - // Generally, the Read function should do the following things. Make - // sure there is a good reason if you don't do one of these. - // - // 1. Get a client connection to the relevant service - // 2. Fetch the state - // 3. Get the resource from AWS - // 4. Remove resource from state if it is not found - // 5. Set the arguments and attributes - // 6. Set the state - - // TIP: -- 1. Get a client connection to the relevant service conn := r.Meta().LexV2ModelsClient(ctx) - // TIP: -- 2. Fetch the state var state resourceBotData resp.Diagnostics.Append(req.State.Get(ctx, &state)...) if resp.Diagnostics.HasError() { return } - // TIP: -- 3. Get the resource from AWS using an API Get, List, or Describe- - // type function, or, better yet, using a finder. out, err := FindBotByID(ctx, conn, state.ID.ValueString()) - // TIP: -- 4. Remove resource from state if it is not found if tfresource.NotFound(err) { resp.State.RemoveResource(ctx) return @@ -320,50 +206,22 @@ func (r *resourceBot) Read(ctx context.Context, req resource.ReadRequest, resp * return } - // TIP: -- 5. Set the arguments and attributes - // - // For simple data types (i.e., schema.StringAttribute, schema.BoolAttribute, - // schema.Int64Attribute, and schema.Float64Attribue), simply setting the - // appropriate data struct field is sufficient. The flex package implements - // helpers for converting between Go and Plugin-Framework types seamlessly. No - // error or nil checking is necessary. - // - // However, there are some situations where more handling is needed such as - // complex data types (e.g., schema.ListAttribute, schema.SetAttribute). In - // these cases the flatten function may have a diagnostics return value, which - // should be appended to resp.Diagnostics. + state.RoleARN = flex.StringToFramework(ctx, out.RoleArn) + state.ID = flex.StringToFramework(ctx, out.BotId) + state.Name = flex.StringToFramework(ctx, out.BotName) + state.Type = flex.StringToFramework(ctx, (*string)(&out.BotType)) + state.Description = flex.StringToFramework(ctx, out.Description) + state.IdleSessionTTLInSeconds = flex.Int32ToFramework(ctx, out.IdleSessionTTLInSeconds) - resp.Diagnostics.Append(state.refreshFromOutput(ctx, out)...) + datap, _ := flattenDataPrivacy(ctx, out.DataPrivacy) - // TIP: -- 6. Set the state + state.DataPrivacy = datap resp.Diagnostics.Append(resp.State.Set(ctx, &state)...) } func (r *resourceBot) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) { - // TIP: ==== RESOURCE UPDATE ==== - // Not all resources have Update functions. There are a few reasons: - // a. The AWS API does not support changing a resource - // b. All arguments have RequiresReplace() plan modifiers - // c. The AWS API uses a create call to modify an existing resource - // - // In the cases of a. and b., the resource will not have an update method - // defined. In the case of c., Update and Create can be refactored to call - // the same underlying function. - // - // The rest of the time, there should be an Update function and it should - // do the following things. Make sure there is a good reason if you don't - // do one of these. - // - // 1. Get a client connection to the relevant service - // 2. Fetch the plan and state - // 3. Populate a modify input structure and check for changes - // 4. Call the AWS modify/update function - // 5. Use a waiter to wait for update to complete - // 6. Save the request plan to response state - // TIP: -- 1. Get a client connection to the relevant service conn := r.Meta().LexV2ModelsClient(ctx) - // TIP: -- 2. Fetch the plan var plan, state resourceBotData resp.Diagnostics.Append(req.Plan.Get(ctx, &plan)...) resp.Diagnostics.Append(req.State.Get(ctx, &state)...) @@ -371,7 +229,6 @@ func (r *resourceBot) Update(ctx context.Context, req resource.UpdateRequest, re return } - // TIP: -- 3. Populate a modify input structure and check for changes if !plan.Name.Equal(state.Name) || !plan.Description.Equal(state.Description) || !plan.IdleSessionTTLInSeconds.Equal(state.IdleSessionTTLInSeconds) || @@ -392,40 +249,27 @@ func (r *resourceBot) Update(ctx context.Context, req resource.UpdateRequest, re return } - in := &lexv2models.UpdateBotInput{ - // TIP: Mandatory or fields that will always be present can be set when - // you create the Input structure. (Replace these with real fields.) - Id: aws.String(plan.ID.ValueString()), - Name: aws.String(plan.Name.ValueString()), - Type: aws.String(plan.Type.ValueString()), - IdleSessionTTLInSeconds: aws.Int64(plan.IdleSessionTTLInSeconds.ValueInt64()), + in := lexmodelsv2.UpdateBotInput{ + BotId: aws.String(plan.ID.ValueString()), + BotName: aws.String(plan.Name.ValueString()), + IdleSessionTTLInSeconds: aws.Int32(int32(plan.IdleSessionTTLInSeconds.ValueInt64())), DataPrivacy: dpInput, - RoleARN: aws.String(plan.RoleARN.ValueString()), + RoleArn: aws.String(plan.RoleARN.ValueString()), } if !plan.Description.IsNull() { in.Description = aws.String(plan.Description.ValueString()) } - if !plan.Type.IsNull() { - in.Type = aws.String(plan.Type.ValueString()) - } + if !plan.Members.IsNull() { - // TIP: Use an expander to assign a complex argument. The elements must be - // deserialized into the appropriate struct before being passed to the expander. var tfList []membersData resp.Diagnostics.Append(plan.Members.ElementsAs(ctx, &tfList, false)...) if resp.Diagnostics.HasError() { return } - - in.ComplexArgument, d = expandMembers(ctx, tfList) - resp.Diagnostics.Append(d...) - if resp.Diagnostics.HasError() { - return - } } - // TIP: -- 4. Call the AWS modify/update function - out, err := conn.UpdateBot(ctx, in) + + out, err := conn.UpdateBot(ctx, &in) if err != nil { resp.Diagnostics.AddError( create.ProblemStandardMessage(names.LexV2Models, create.ErrActionUpdating, ResNameBot, plan.ID.String(), err), @@ -440,61 +284,37 @@ func (r *resourceBot) Update(ctx context.Context, req resource.UpdateRequest, re ) return } - // TIP: Using the output from the update function, re-set any computed attributes - state.refreshFromOutput(ctx, out.BotId) - } - - - // TIP: -- 5. Use a waiter to wait for update to complete - // updateTimeout := r.UpdateTimeout(ctx, plan.Timeouts) - // _, 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 - // } - - // TIP: -- 6. Save the request plan to response state + + state.refreshFromOutput(ctx, out) + } + + updateTimeout := r.UpdateTimeout(ctx, plan.Timeouts) + _, 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 + } + resp.Diagnostics.Append(resp.State.Set(ctx, &plan)...) } func (r *resourceBot) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) { - // TIP: ==== RESOURCE DELETE ==== - // Most resources have Delete functions. There are rare situations - // where you might not need a delete: - // a. The AWS API does not provide a way to delete the resource - // b. The point of your resource is to perform an action (e.g., reboot a - // server) and deleting serves no purpose. - // - // The Delete function should do the following things. Make sure there - // is a good reason if you don't do one of these. - // - // 1. Get a client connection to the relevant service - // 2. Fetch the state - // 3. Populate a delete input structure - // 4. Call the AWS delete function - // 5. Use a waiter to wait for delete to complete - // TIP: -- 1. Get a client connection to the relevant service conn := r.Meta().LexV2ModelsClient(ctx) - // TIP: -- 2. Fetch the state var state resourceBotData resp.Diagnostics.Append(req.State.Get(ctx, &state)...) if resp.Diagnostics.HasError() { return } - // TIP: -- 3. Populate a delete input structure - in := &lexv2models.DeleteBotInput{ + in := &lexmodelsv2.DeleteBotInput{ BotId: aws.String(state.ID.ValueString()), } - // TIP: -- 4. Call the AWS delete function _, err := conn.DeleteBot(ctx, in) - // TIP: On rare occassions, the API returns a not found error after deleting a - // resource. If that happens, we don't want it to show up as an error. if err != nil { var nfe *awstypes.ResourceNotFoundException if errors.As(err, &nfe) { @@ -507,34 +327,21 @@ func (r *resourceBot) Delete(ctx context.Context, req resource.DeleteRequest, re return } - // TIP: -- 5. Use a waiter to wait for delete to complete - // 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 - // } + 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 + } } -// TIP: ==== TERRAFORM IMPORTING ==== -// If Read can get all the information it needs from the Identifier -// (i.e., path.Root("id")), you can use the PassthroughID importer. Otherwise, -// you'll need a custom import function. -// -// See more: -// https://developer.hashicorp.com/terraform/plugin/framework/resources/import func (r *resourceBot) ImportState(ctx context.Context, req resource.ImportStateRequest, resp *resource.ImportStateResponse) { resource.ImportStatePassthroughID(ctx, path.Root("id"), req, resp) } - -// TIP: ==== STATUS CONSTANTS ==== -// Create constants for states and statuses if the service does not -// already have suitable constants. We prefer that you use the constants -// provided in the service if available (e.g., amp.WorkspaceStatusCodeActive). const ( statusChangePending = "Pending" statusDeleting = "Deleting" @@ -542,110 +349,79 @@ const ( statusUpdated = "Updated" ) -// TIP: ==== WAITERS ==== -// Some resources of some services have waiters provided by the AWS API. -// Unless they do not work properly, use them rather than defining new ones -// here. -// -// Sometimes we define the wait, status, and find functions in separate -// files, wait.go, status.go, and find.go. Follow the pattern set out in the -// service and define these where it makes the most sense. -// -// If these functions are used in the _test.go file, they will need to be -// exported (i.e., capitalized). -// -// You will need to adjust the parameters and names to fit the service. -// func waitBotCreated(ctx context.Context, conn *lexv2models.Client, id string, timeout time.Duration) (*lexv2models.Bot, error) { -// stateConf := &retry.StateChangeConf{ -// Pending: []string{}, -// Target: []string{statusNormal}, -// Refresh: statusBot(ctx, conn, id), -// Timeout: timeout, -// NotFoundChecks: 20, -// ContinuousTargetOccurence: 2, -// } - -// outputRaw, err := stateConf.WaitForStateContext(ctx) -// if out, ok := outputRaw.(*lexv2models.Bot); ok { -// return out, err -// } - -// return nil, err -// } - -// TIP: It is easier to determine whether a resource is updated for some -// resources than others. The best case is a status flag that tells you when -// the update has been fully realized. Other times, you can check to see if a -// key resource argument is updated to a new value or not. -// func waitBotUpdated(ctx context.Context, conn *lexv2models.Client, id string, timeout time.Duration) (*lexv2models.Bot, error) { -// stateConf := &retry.StateChangeConf{ -// Pending: []string{statusChangePending}, -// Target: []string{statusUpdated}, -// Refresh: statusBot(ctx, conn, id), -// Timeout: timeout, -// NotFoundChecks: 20, -// ContinuousTargetOccurence: 2, -// } - -// outputRaw, err := stateConf.WaitForStateContext(ctx) -// if out, ok := outputRaw.(*lexv2models.Bot); ok { -// return out, err -// } - -// return nil, err -// } - -// TIP: A deleted waiter is almost like a backwards created waiter. There may -// be additional pending states, however. -// func waitBotDeleted(ctx context.Context, conn *lexv2models.Client, id string, timeout time.Duration) (*lexv2models.Bot, error) { -// stateConf := &retry.StateChangeConf{ -// Pending: []string{statusDeleting, statusNormal}, -// Target: []string{}, -// Refresh: statusBot(ctx, conn, id), -// Timeout: timeout, -// } - -// outputRaw, err := stateConf.WaitForStateContext(ctx) -// if out, ok := outputRaw.(*lexv2models.Bot); ok { -// return out, err -// } - -// return nil, err -// } - -// TIP: ==== STATUS ==== -// The status function can return an actual status when that field is -// available from the API (e.g., out.Status). Otherwise, you can use custom -// statuses to communicate the states of the resource. -// -// Waiters consume the values returned by status functions. Design status so -// that it can be reused by a create, update, and delete waiter, if possible. -// func statusBot(ctx context.Context, conn *lexv2models.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(out.Status), nil -// } -// } - -// TIP: ==== FINDERS ==== -// The find function is not strictly necessary. You could do the API -// request from the status function. However, we have found that find often -// comes in handy in other places besides the status function. As a result, it -// is good practice to define it separately. -func FindBotByID(ctx context.Context, conn *lexv2models.Client, id string) (*awstypes.Bot, error) { - in := &lexv2models.GetBotInput{ +func waitBotCreated(ctx context.Context, conn *lexmodelsv2.Client, id string, timeout time.Duration) (*lexmodelsv2.DescribeBotOutput, error) { + stateConf := &retry.StateChangeConf{ + Pending: []string{}, + Target: []string{statusNormal}, + 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: []string{statusChangePending}, + Target: []string{statusUpdated}, + 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: []string{statusDeleting, statusNormal}, + 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.ListBot(ctx, in) + out, err := conn.DescribeBot(ctx, in) if err != nil { var nfe *awstypes.ResourceNotFoundException if errors.As(err, &nfe) { @@ -662,20 +438,9 @@ func FindBotByID(ctx context.Context, conn *lexv2models.Client, id string) (*aws return nil, tfresource.NewEmptyResultError(in) } - return out.Bot, nil + return out, nil } -// TIP: ==== FLEX ==== -// Flatteners and expanders ("flex" functions) help handle complex data -// types. Flatteners take an API data type and return the equivalent Plugin-Framework -// type. In other words, flatteners translate from AWS -> Terraform. -// -// On the other hand, expanders take a Terraform data structure and return -// something that you can send to the AWS API. In other words, expanders -// translate from Terraform -> AWS. -// -// See more: -// https://hashicorp.github.io/terraform-provider-aws/data-handling-and-conversion/ func flattenDataPrivacy(ctx context.Context, apiObject *awstypes.DataPrivacy) (types.List, diag.Diagnostics) { var diags diag.Diagnostics elemType := types.ObjectType{AttrTypes: dataPrivacyAttrTypes} @@ -685,7 +450,7 @@ func flattenDataPrivacy(ctx context.Context, apiObject *awstypes.DataPrivacy) (t } obj := map[string]attr.Value{ - "child_directed": flex.StringValueToFramework(ctx, apiObject.ChildDirected), + "child_directed": types.BoolValue(aws.ToBool(&apiObject.ChildDirected)), } objVal, d := types.ObjectValue(dataPrivacyAttrTypes, obj) diags.Append(d...) @@ -696,89 +461,69 @@ func flattenDataPrivacy(ctx context.Context, apiObject *awstypes.DataPrivacy) (t return listVal, diags } -// TIP: Remember, as mentioned above, expanders take a Terraform data structure -// and return something that you can send to the AWS API. In other words, -// expanders translate from Terraform -> AWS. -// -// See more: -// https://hashicorp.github.io/terraform-provider-aws/data-handling-and-conversion/ func expandDataPrivacy(ctx context.Context, tfList []dataPrivacyData) (*awstypes.DataPrivacy, diag.Diagnostics) { var diags diag.Diagnostics if len(tfList) == 0 { - return nil + return nil, diags } dp := tfList[0] + cdBool, _ := strconv.ParseBool(dp.ChildDirected.ValueString()) + return &awstypes.DataPrivacy{ - ChildDirected: aws.String(dp.ChildDirected.ValueString()), + ChildDirected: aws.ToBool(&cdBool), }, diags } -// TIP: Even when you have a list with max length of 1, this plural function -// works brilliantly. However, if the AWS API takes a structure rather than a -// slice of structures, you will not need it. -func expandMembers(ctx context.Context, tfList []membersData) ([]*awstypes.Members, diag.Diagnostics) { +func expandMembers(ctx context.Context, tfList []membersData) (*awstypes.BotMember, diag.Diagnostics) { var diags diag.Diagnostics - if len(tfList) == 0 { - return nil - } + if len(tfList) == 0 { + return nil, diags + } mb := tfList[0] - return &awstypes.DataPrivacy{ - AliasID: aws.String(mb.AliasID.ValueString()), - AliasName: aws.String(mb.AliasName.ValueString()), - ID: aws.String(mb.ID.ValueString()), - Name: aws.String(mb.Name.ValueString()), - Version: aws.String(mb.Version.ValueString()), + return &awstypes.BotMember{ + BotMemberAliasId: aws.String(mb.AliasID.ValueString()), + BotMemberAliasName: aws.String(mb.AliasName.ValueString()), + BotMemberId: aws.String(mb.ID.ValueString()), + BotMemberName: aws.String(mb.Name.ValueString()), + BotMemberVersion: aws.String(mb.Version.ValueString()), }, diags } -func (rd *resourceBotData) refreshFromOutput(ctx context.Context, out *awstypes.Bot) diag.Diagnostics { +func (rd *resourceBotData) refreshFromOutput(ctx context.Context, out *lexmodelsv2.UpdateBotOutput) diag.Diagnostics { var diags diag.Diagnostics if out == nil { return diags } - rd.RoleARN = flex.StringToFramework(ctx, out.RoleARN) + rd.RoleARN = flex.StringToFramework(ctx, out.RoleArn) rd.ID = flex.StringToFramework(ctx, out.BotId) rd.Name = flex.StringToFramework(ctx, out.BotName) - rd.Type = flex.StringToFramework(ctx, out.BotType) + rd.Type = flex.StringToFramework(ctx, (*string)(&out.BotType)) rd.Description = flex.StringToFramework(ctx, out.Description) - rd.Type = flex.StringToFramework(ctx, out.Type) - rd.IdleSessionTTLInSeconds = flex.Int64ToFramework(ctx, out.IdleSessionTTLInSeconds) + rd.IdleSessionTTLInSeconds = flex.Int32ToFramework(ctx, out.IdleSessionTTLInSeconds) // TIP: Setting a complex type. datap, d := flattenDataPrivacy(ctx, out.DataPrivacy) diags.Append(d...) rd.DataPrivacy = datap - setTagsOut(ctx, out.Tags) return diags } -// TIP: ==== DATA STRUCTURES ==== -// With Terraform Plugin-Framework configurations are deserialized into -// Go types, providing type safety without the need for type assertions. -// These structs should match the schema definition exactly, and the `tfsdk` -// tag value should match the attribute name. -// -// Nested objects are represented in their own data struct. These will -// also have a corresponding attribute type mapping for use inside flex -// functions. -// -// See more: -// https://developer.hashicorp.com/terraform/plugin/framework/handling-data/accessing-values type resourceBotData struct { 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` + IdleSessionTTLInSeconds types.Int64 `tfsdk:"idle_session_ttl_in_seconds"` Name types.String `tfsdk:"name"` - Members types.List `tfsdk:"members"` - RoleARN types.String `tfsdk:"role_arn"` - tags types.Map `tfsdk:"tags"` - TestBotAliasTags types.Map `tfsdk:"test_bot_alias_tags"` + Members types.List `tfsdk:"members"` + RoleARN types.String `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"` } @@ -788,14 +533,13 @@ type dataPrivacyData struct { } 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"` - + 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.StringType, -} \ No newline at end of file +} diff --git a/internal/service/lexv2models/bot_test.go b/internal/service/lexv2models/bot_test.go index 4b37b4cf2c8..a4a2e211719 100644 --- a/internal/service/lexv2models/bot_test.go +++ b/internal/service/lexv2models/bot_test.go @@ -2,155 +2,35 @@ // SPDX-License-Identifier: MPL-2.0 package lexv2models_test -// **PLEASE DELETE THIS AND ALL TIP COMMENTS BEFORE SUBMITTING A PR FOR REVIEW!** -// -// TIP: ==== INTRODUCTION ==== -// Thank you for trying the skaff tool! -// -// You have opted to include these helpful comments. They all include "TIP:" -// to help you find and remove them when you're done with them. -// -// While some aspects of this file are customized to your input, the -// scaffold tool does *not* look at the AWS API and ensure it has correct -// function, structure, and variable names. It makes guesses based on -// commonalities. You will need to make significant adjustments. -// -// In other words, as generated, this is a rough outline of the work you will -// need to do. If something doesn't make sense for your situation, get rid of -// it. import ( - // TIP: ==== IMPORTS ==== - // This is a common set of imports but not customized to your code since - // your code hasn't been written yet. Make sure you, your IDE, or - // goimports -w fixes these imports. - // - // The provider linter wants your imports to be in two groups: first, - // standard library (i.e., "fmt" or "strings"), second, everything else. - // - // Also, AWS Go SDK v2 may handle nested structures differently than v1, - // using the services/lexv2models/types package. If so, you'll - // need to import types and reference the nested types, e.g., as - // types.. "context" + "errors" "fmt" - "regexp" - "strings" "testing" - "github.com/YakDriver/regexache" - - "github.com/aws/aws-sdk-go-v2/aws" - "github.com/aws/aws-sdk-go-v2/service/lexv2models" - "github.com/aws/aws-sdk-go-v2/service/lexv2models/types" + "github.com/aws/aws-sdk-go-v2/service/lexmodelsv2" + "github.com/aws/aws-sdk-go-v2/service/lexmodelsv2/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" - "github.com/hashicorp/terraform-provider-aws/internal/errs" "github.com/hashicorp/terraform-provider-aws/names" - // TIP: You will often need to import the package that this test file lives - // in. Since it is in the "test" context, it must import the package to use - // any normal context constants, variables, or functions. tflexv2models "github.com/hashicorp/terraform-provider-aws/internal/service/lexv2models" ) -// TIP: File Structure. The basic outline for all test files should be as -// follows. Improve this resource's maintainability by following this -// outline. -// -// 1. Package declaration (add "_test" since this is a test file) -// 2. Imports -// 3. Unit tests -// 4. Basic test -// 5. Disappears test -// 6. All the other tests -// 7. Helper functions (exists, destroy, check, etc.) -// 8. Functions that return Terraform configurations - -// TIP: ==== UNIT TESTS ==== -// This is an example of a unit test. Its name is not prefixed with -// "TestAcc" like an acceptance test. -// -// Unlike acceptance tests, unit tests do not access AWS and are focused on a -// function (or method). Because of this, they are quick and cheap to run. -// -// In designing a resource's implementation, isolate complex bits from AWS bits -// so that they can be tested through a unit test. We encourage more unit tests -// in the provider. -// -// Cut and dry functions using well-used patterns, like typical flatteners and -// expanders, don't need unit testing. However, if they are complex or -// intricate, they should be unit tested. -func TestBotExampleUnitTest(t *testing.T) { - t.Parallel() - - testCases := []struct { - TestName string - Input string - Expected string - Error bool - }{ - { - TestName: "empty", - Input: "", - Expected: "", - Error: true, - }, - { - TestName: "descriptive name", - Input: "some input", - Expected: "some output", - Error: false, - }, - { - TestName: "another descriptive name", - Input: "more input", - Expected: "more output", - Error: false, - }, - } - - for _, testCase := range testCases { - testCase := testCase - t.Run(testCase.TestName, func(t *testing.T) { - t.Parallel() - got, err := tflexv2models.FunctionFromResource(testCase.Input) - - if err != nil && !testCase.Error { - t.Errorf("got error (%s), expected no error", err) - } - - if err == nil && testCase.Error { - t.Errorf("got (%s) and no error, expected error", got) - } - - if got != testCase.Expected { - t.Errorf("got %s, expected %s", got, testCase.Expected) - } - }) - } -} - -// TIP: ==== ACCEPTANCE TESTS ==== -// This is an example of a basic acceptance test. This should test as much of -// standard functionality of the resource as possible, and test importing, if -// applicable. We prefix its name with "TestAcc", the service, and the -// resource name. -// -// Acceptance test access AWS and cost money to run. func TestAccLexV2ModelsBot_basic(t *testing.T) { ctx := acctest.Context(t) // TIP: This is a long-running test guard for tests that run longer than // 300s (5 min) generally. - if testing.Short() { - t.Skip("skipping long-running test in short mode") - } + // if testing.Short() { + // t.Skip("skipping long-running test in short mode") + // } - var bot lexv2models.DescribeBotResponse + var bot lexmodelsv2.DescribeBotOutput rName := sdkacctest.RandomWithPrefix(acctest.ResourcePrefix) resourceName := "aws_lexv2models_bot.test" @@ -168,22 +48,17 @@ func TestAccLexV2ModelsBot_basic(t *testing.T) { Config: testAccBotConfig_basic(rName), Check: resource.ComposeTestCheckFunc( testAccCheckBotExists(ctx, resourceName, &bot), - resource.TestCheckResourceAttr(resourceName, "auto_minor_version_upgrade", "false"), - resource.TestCheckResourceAttrSet(resourceName, "maintenance_window_start_time.0.day_of_week"), - resource.TestCheckTypeSetElemNestedAttrs(resourceName, "user.*", map[string]string{ - "console_access": "false", - "groups.#": "0", - "username": "Test", - "password": "TestTest1234", - }), - acctest.MatchResourceAttrRegionalARN(resourceName, "arn", "lexv2models", regexache.MustCompile(`bot:+.`)), + resource.TestCheckResourceAttr(resourceName, "name", rName), + resource.TestCheckResourceAttr(resourceName, "idle_session_ttl_in_seconds", "5"), + resource.TestCheckResourceAttr(resourceName, "role_arn", "bot_role_arn"), + resource.TestCheckResourceAttr(resourceName, "data_privacy.#", "1"), + resource.TestCheckResourceAttr(resourceName, "data_privacy.0.child_directed", "true"), ), }, { - ResourceName: resourceName, - ImportState: true, - ImportStateVerify: true, - ImportStateVerifyIgnore: []string{"apply_immediately", "user"}, + ResourceName: resourceName, + ImportState: true, + ImportStateVerify: true, }, }, }) @@ -195,7 +70,7 @@ func TestAccLexV2ModelsBot_disappears(t *testing.T) { t.Skip("skipping long-running test in short mode") } - var bot lexv2models.DescribeBotResponse + var bot lexmodelsv2.DescribeBotOutput rName := sdkacctest.RandomWithPrefix(acctest.ResourcePrefix) resourceName := "aws_lexv2models_bot.test" @@ -203,22 +78,16 @@ func TestAccLexV2ModelsBot_disappears(t *testing.T) { PreCheck: func() { acctest.PreCheck(ctx, t) acctest.PreCheckPartitionHasService(t, names.LexV2ModelsEndpointID) - testAccPreCheck(t) + testAccPreCheck(ctx, t) }, ErrorCheck: acctest.ErrorCheck(t, names.LexV2ModelsEndpointID), ProtoV5ProviderFactories: acctest.ProtoV5ProviderFactories, CheckDestroy: testAccCheckBotDestroy(ctx), Steps: []resource.TestStep{ { - Config: testAccBotConfig_basic(rName, testAccBotVersionNewer), + Config: testAccBotConfig_basic(rName), Check: resource.ComposeTestCheckFunc( testAccCheckBotExists(ctx, resourceName, &bot), - // TIP: The Plugin-Framework disappears helper is similar to the Plugin-SDK version, - // but expects a new resource factory function as the third argument. To expose this - // private function to the testing package, you may need to add a line like the following - // to exports_test.go: - // - // var ResourceBot = newResourceBot acctest.CheckFrameworkResourceDisappears(ctx, acctest.Provider, tflexv2models.ResourceBot, resourceName), ), ExpectNonEmptyPlan: true, @@ -236,17 +105,13 @@ func testAccCheckBotDestroy(ctx context.Context) resource.TestCheckFunc { continue } - input := &lexv2models.DescribeBotInput{ - BotId: aws.String(rs.Primary.ID), - } - _, err := conn.DescribeBot(ctx, &lexv2models.DescribeBotInput{ - BotId: aws.String(rs.Primary.ID), - }) - if errs.IsA[*types.ResourceNotFoundException](err){ - return nil - } + _, err := tflexv2models.FindBotByID(ctx, conn, rs.Primary.ID) if err != nil { - return nil + var nfe *types.ResourceNotFoundException + if errors.As(err, &nfe) { + return nil + } + return err } return create.Error(names.LexV2Models, create.ErrActionCheckingDestroyed, tflexv2models.ResNameBot, rs.Primary.ID, errors.New("not destroyed")) @@ -256,7 +121,7 @@ func testAccCheckBotDestroy(ctx context.Context) resource.TestCheckFunc { } } -func testAccCheckBotExists(ctx context.Context, name string, bot *lexv2models.DescribeBotResponse) resource.TestCheckFunc { +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 { @@ -268,10 +133,7 @@ func testAccCheckBotExists(ctx context.Context, name string, bot *lexv2models.De } conn := acctest.Provider.Meta().(*conns.AWSClient).LexV2ModelsClient(ctx) - resp, err := conn.DescribeBot(ctx, &lexv2models.DescribeBotInput{ - BotId: aws.String(rs.Primary.ID), - }) - + 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) } @@ -285,7 +147,7 @@ func testAccCheckBotExists(ctx context.Context, name string, bot *lexv2models.De func testAccPreCheck(ctx context.Context, t *testing.T) { conn := acctest.Provider.Meta().(*conns.AWSClient).LexV2ModelsClient(ctx) - input := &lexv2models.ListBotsInput{} + input := &lexmodelsv2.ListBotsInput{} _, err := conn.ListBots(ctx, input) if acctest.PreCheckSkipError(err) { @@ -296,39 +158,50 @@ func testAccPreCheck(ctx context.Context, t *testing.T) { } } -func testAccCheckBotNotRecreated(before, after *lexv2models.DescribeBotResponse) resource.TestCheckFunc { - return func(s *terraform.State) error { - if before, after := aws.ToString(before.BotId), aws.ToString(after.BotId); before != after { - return create.Error(names.LexV2Models, create.ErrActionCheckingNotRecreated, tflexv2models.ResNameBot, aws.ToString(before.BotId), errors.New("recreated")) - } +// func testAccCheckBotNotRecreated(before, after *lexmodelsv2.DescribeBotOutput) resource.TestCheckFunc { +// return func(s *terraform.State) error { +// if before, after := aws.ToString(before.BotId), aws.ToString(after.BotId); before != after { +// return create.Error(names.LexV2Models, create.ErrActionCheckingNotRecreated, tflexv2models.ResNameBot, aws.ToString(before.BotId), errors.New("recreated")) +// } - return nil - } -} +// return nil +// } +// } -func testAccBotConfig_basic(rName, version string) string { +func testAccBotConfig_basic(rName string) string { return fmt.Sprintf(` -resource "aws_security_group" "test" { - name = %[1]q +resource "aws_lexv2models_bot" "test" { + name = %[1]q + idle_session_ttl_in_seconds = "5" + role_arn = "bot_role_arn" + + data_privacy { + child_directed = true + } +} +`, rName) } +func testAccBotConfig_optional(rName, description, botType, aliasId, aliasName, memberId, memberName string) string { + return fmt.Sprintf(` resource "aws_lexv2models_bot" "test" { - bot_name = %[1]q - engine_type = "ActiveLexV2Models" - engine_version = %[2]q - host_instance_type = "lexv2models.t2.micro" - security_groups = [aws_security_group.test.id] - authentication_strategy = "simple" - storage_type = "efs" - - logs { - general = true + bot_name = %[1]q + description = %[2]q + idle_session_ttl_in_seconds = "5" + type = %[3]q + role_arn = "bot_role_arn" + + data_privacy { + child_directed = true } - user { - username = "Test" - password = "TestTest1234" + members { + alias_id = %[4]q + alias_name = %[5]q + id = %[6]q + name = %[7]q + version = "2.0" } } -`, rName, version) +`, rName, description, botType, aliasId, aliasName, memberId, memberName) } 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..db745222bf8 100644 --- a/internal/service/lexv2models/service_package_gen.go +++ b/internal/service/lexv2models/service_package_gen.go @@ -19,7 +19,12 @@ 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", + }, + } } 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" From f6da4c43ef69a6e59373b84f2f7814e39c33d247 Mon Sep 17 00:00:00 2001 From: Sharon Nam Date: Wed, 20 Sep 2023 16:02:30 -0700 Subject: [PATCH 04/10] markdown and some more fixes --- internal/service/lexv2models/bot.go | 70 ++++++-------------- website/docs/r/lexv2models_bot.html.markdown | 39 +++++++---- 2 files changed, 49 insertions(+), 60 deletions(-) diff --git a/internal/service/lexv2models/bot.go b/internal/service/lexv2models/bot.go index c906c5649da..476c159428e 100644 --- a/internal/service/lexv2models/bot.go +++ b/internal/service/lexv2models/bot.go @@ -6,7 +6,6 @@ package lexv2models import ( "context" "errors" - "strconv" "time" "github.com/aws/aws-sdk-go-v2/aws" @@ -23,6 +22,7 @@ import ( "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-framework/types/basetypes" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/retry" "github.com/hashicorp/terraform-provider-aws/internal/create" "github.com/hashicorp/terraform-provider-aws/internal/framework" @@ -109,15 +109,10 @@ func (r *resourceBot) Schema(ctx context.Context, req resource.SchemaRequest, re }, }, }, - "data_privacy": schema.ListNestedBlock{ - Validators: []validator.List{ - listvalidator.SizeAtLeast(1), - }, - NestedObject: schema.NestedBlockObject{ - Attributes: map[string]schema.Attribute{ - "child_directed": schema.BoolAttribute{ - Required: true, - }, + "data_privacy": schema.SingleNestedBlock{ + Attributes: map[string]schema.Attribute{ + "child_directed": schema.BoolAttribute{ + Required: true, }, }, }, @@ -139,13 +134,7 @@ func (r *resourceBot) Create(ctx context.Context, req resource.CreateRequest, re return } - var dp []dataPrivacyData - resp.Diagnostics.Append(plan.DataPrivacy.ElementsAs(ctx, &dp, false)...) - if resp.Diagnostics.HasError() { - return - } - - dpInput, d := expandDataPrivacy(ctx, dp) + dpInput, d := expandDataPrivacy(ctx, plan.DataPrivacy) resp.Diagnostics.Append(d...) if resp.Diagnostics.HasError() { return @@ -237,23 +226,13 @@ func (r *resourceBot) Update(ctx context.Context, req resource.UpdateRequest, re !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, d := expandDataPrivacy(ctx, dp) - resp.Diagnostics.Append(d...) - if resp.Diagnostics.HasError() { - return - } + dp, _ := expandDataPrivacy(ctx, plan.DataPrivacy) in := lexmodelsv2.UpdateBotInput{ BotId: aws.String(plan.ID.ValueString()), BotName: aws.String(plan.Name.ValueString()), IdleSessionTTLInSeconds: aws.Int32(int32(plan.IdleSessionTTLInSeconds.ValueInt64())), - DataPrivacy: dpInput, + DataPrivacy: dp, RoleArn: aws.String(plan.RoleARN.ValueString()), } @@ -441,37 +420,32 @@ func FindBotByID(ctx context.Context, conn *lexmodelsv2.Client, id string) (*lex return out, nil } -func flattenDataPrivacy(ctx context.Context, apiObject *awstypes.DataPrivacy) (types.List, diag.Diagnostics) { +func flattenDataPrivacy(ctx context.Context, apiObject *awstypes.DataPrivacy) (types.Object, diag.Diagnostics) { var diags diag.Diagnostics - elemType := types.ObjectType{AttrTypes: dataPrivacyAttrTypes} + attributeTypes := flex.AttributeTypesMust[dataPrivacyData](ctx) if apiObject == nil { - return types.ListValueMust(elemType, []attr.Value{}), diags + return types.ObjectNull(attributeTypes), diags } - obj := map[string]attr.Value{ - "child_directed": types.BoolValue(aws.ToBool(&apiObject.ChildDirected)), - } - objVal, d := types.ObjectValue(dataPrivacyAttrTypes, obj) - diags.Append(d...) + obj := map[string]attr.Value{} + obj["child_directed"] = flex.BoolToFramework(ctx, &apiObject.ChildDirected) - listVal, d := types.ListValue(elemType, []attr.Value{objVal}) - diags.Append(d...) - - return listVal, diags + return types.ObjectValueMust(attributeTypes, obj), diags } -func expandDataPrivacy(ctx context.Context, tfList []dataPrivacyData) (*awstypes.DataPrivacy, diag.Diagnostics) { +func expandDataPrivacy(ctx context.Context, object types.Object) (*awstypes.DataPrivacy, diag.Diagnostics) { var diags diag.Diagnostics - if len(tfList) == 0 { + var dp dataPrivacyData + diags.Append(object.As(ctx, &dp, basetypes.ObjectAsOptions{})...) + if diags.HasError() { return nil, diags } - dp := tfList[0] - cdBool, _ := strconv.ParseBool(dp.ChildDirected.ValueString()) + cdBool := flex.BoolFromFramework(ctx, dp.ChildDirected) return &awstypes.DataPrivacy{ - ChildDirected: aws.ToBool(&cdBool), + ChildDirected: aws.ToBool(cdBool), }, diags } @@ -514,7 +488,7 @@ func (rd *resourceBotData) refreshFromOutput(ctx context.Context, out *lexmodels } type resourceBotData struct { - DataPrivacy types.List `tfsdk:"data_privacy"` + DataPrivacy types.Object `tfsdk:"data_privacy"` Description types.String `tfsdk:"description"` ID types.String `tfsdk:"id"` IdleSessionTTLInSeconds types.Int64 `tfsdk:"idle_session_ttl_in_seconds"` @@ -529,7 +503,7 @@ type resourceBotData struct { } type dataPrivacyData struct { - ChildDirected types.String `tfsdk:"child_directed"` + ChildDirected types.Bool `tfsdk:"child_directed"` } type membersData struct { diff --git a/website/docs/r/lexv2models_bot.html.markdown b/website/docs/r/lexv2models_bot.html.markdown index 2b058edb498..3e690dde0e8 100644 --- a/website/docs/r/lexv2models_bot.html.markdown +++ b/website/docs/r/lexv2models_bot.html.markdown @@ -5,14 +5,7 @@ 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. @@ -23,6 +16,12 @@ Terraform resource for managing an AWS Lex V2 Models Bot. ```terraform resource "aws_lexv2models_bot" "example" { + name = "example" + data_privacy { + child_directed = “boolean”, + } + idle_session_ttl_in_seconds = 10 + role_arn = “bot_example_arn” } ``` @@ -30,19 +29,35 @@ resource "aws_lexv2models_bot" "example" { The following arguments are required: -* `example_arg` - (Required) Concise argument description. Do not begin the description with "An", "The", "Defines", "Indicates", or "Specifies," as these are verbose. In other words, "Indicates the amount of storage," can be rewritten as "Amount of storage," without losing any information. +* `name` - The 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` - The 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` - The Amazon Resource Name (ARN) of an IAM role that has permission to access the bot. The following arguments are optional: -* `optional_arg` - (Optional) Concise argument description. Do not begin the description with "An", "The", "Defines", "Indicates", or "Specifies," as these are verbose. In other words, "Indicates the amount of storage," can be rewritten as "Amount of storage," without losing any information. +* `members` - List of bot members in a network to be created. See [`bot_members`](#bot-members). +* `bot_tags` - A list of tags to add to the bot. You can only add tags when you create a bot. +* `bot_type` - The type of a bot to create. +* `description` - A description of the bot. It appears in lists to help you identify a particular bot. +* `test_bot_alias_tags` - A 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: -* `arn` - ARN of the Bot. Do not begin the description with "An", "The", "Defines", "Indicates", or "Specifies," as these are verbose. In other words, "Indicates the amount of storage," can be rewritten as "Amount of storage," without losing any information. -* `example_attribute` - Concise description. Do not begin the description with "An", "The", "Defines", "Indicates", or "Specifies," as these are verbose. In other words, "Indicates the amount of storage," can be rewritten as "Amount of storage," without losing any information. +* `id` - A 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) - The alias ID of a bot that is a member of this network of bots. +* `alias_name` (Required) - The alias ID of a bot that is a member of this network of bots. +* `id` (Required) - The unique ID of a bot that is a member of this network of bots. +* `name` (Required) - The unique name of a bot that is a member of this network of bots. +* `version` (Required) - The 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): From 975df4c90efa8caf317a696a250625b289ee437f Mon Sep 17 00:00:00 2001 From: Sharon Nam Date: Wed, 20 Sep 2023 22:08:33 -0700 Subject: [PATCH 05/10] fixes --- internal/service/lexv2models/bot.go | 73 ++++++++------- internal/service/lexv2models/bot_test.go | 110 +++++++++++++++-------- 2 files changed, 112 insertions(+), 71 deletions(-) diff --git a/internal/service/lexv2models/bot.go b/internal/service/lexv2models/bot.go index 476c159428e..7d44721ab8b 100644 --- a/internal/service/lexv2models/bot.go +++ b/internal/service/lexv2models/bot.go @@ -25,6 +25,7 @@ import ( "github.com/hashicorp/terraform-plugin-framework/types/basetypes" "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" tftags "github.com/hashicorp/terraform-provider-aws/internal/tags" @@ -59,6 +60,7 @@ func (r *resourceBot) Metadata(_ context.Context, req resource.MetadataRequest, 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, }, @@ -77,11 +79,16 @@ func (r *resourceBot) Schema(ctx context.Context, req resource.SchemaRequest, re "role_arn": schema.StringAttribute{ Required: true, }, - "test_bot_alias_tags": schema.StringAttribute{ - Optional: true, + "test_bot_alias_tags": schema.MapAttribute{ + ElementType: types.StringType, + Optional: true, }, "type": schema.StringAttribute{ Optional: true, + Computed: true, + PlanModifiers: []planmodifier.String{ + stringplanmodifier.UseStateForUnknown(), + }, }, }, Blocks: map[string]schema.Block{ @@ -147,6 +154,10 @@ func (r *resourceBot) Create(ctx context.Context, req resource.CreateRequest, re RoleArn: aws.String(plan.RoleARN.ValueString()), } + if !plan.TestBotAliasTags.IsNull() { + in.TestBotAliasTags = flex.ExpandFrameworkStringValueMap(ctx, plan.TestBotAliasTags) + } + if !plan.Description.IsNull() { in.Description = aws.String(plan.Description.ValueString()) } @@ -169,6 +180,8 @@ func (r *resourceBot) Create(ctx context.Context, req resource.CreateRequest, re plan.ID = flex.StringToFramework(ctx, out.BotId) state := plan + state.Type = flex.StringValueToFramework(ctx, out.BotType) + // state.ARN = flex.StringValueToFramework(ctx, out.) // resp.Diagnostics.Append(state.refreshFromOutput(ctx, out.BotId)...) resp.Diagnostics.Append(resp.State.Set(ctx, state)...) } @@ -198,7 +211,7 @@ func (r *resourceBot) Read(ctx context.Context, req resource.ReadRequest, resp * state.RoleARN = flex.StringToFramework(ctx, out.RoleArn) state.ID = flex.StringToFramework(ctx, out.BotId) state.Name = flex.StringToFramework(ctx, out.BotName) - state.Type = flex.StringToFramework(ctx, (*string)(&out.BotType)) + state.Type = flex.StringValueToFramework(ctx, out.BotType) state.Description = flex.StringToFramework(ctx, out.Description) state.IdleSessionTTLInSeconds = flex.Int32ToFramework(ctx, out.IdleSessionTTLInSeconds) @@ -218,8 +231,7 @@ func (r *resourceBot) Update(ctx context.Context, req resource.UpdateRequest, re return } - if !plan.Name.Equal(state.Name) || - !plan.Description.Equal(state.Description) || + if !plan.Description.Equal(state.Description) || !plan.IdleSessionTTLInSeconds.Equal(state.IdleSessionTTLInSeconds) || !plan.RoleARN.Equal(state.RoleARN) || !plan.TestBotAliasTags.Equal(state.TestBotAliasTags) || @@ -248,7 +260,7 @@ func (r *resourceBot) Update(ctx context.Context, req resource.UpdateRequest, re } } - out, err := conn.UpdateBot(ctx, &in) + _, err := conn.UpdateBot(ctx, &in) if err != nil { resp.Diagnostics.AddError( create.ProblemStandardMessage(names.LexV2Models, create.ErrActionUpdating, ResNameBot, plan.ID.String(), err), @@ -256,6 +268,16 @@ func (r *resourceBot) Update(ctx context.Context, req resource.UpdateRequest, re ) 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), @@ -263,18 +285,7 @@ func (r *resourceBot) Update(ctx context.Context, req resource.UpdateRequest, re ) return } - - state.refreshFromOutput(ctx, out) - } - - updateTimeout := r.UpdateTimeout(ctx, plan.Timeouts) - _, 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 + resp.Diagnostics.Append(plan.refreshFromOutput(ctx, out)...) } resp.Diagnostics.Append(resp.State.Set(ctx, &plan)...) @@ -317,21 +328,18 @@ func (r *resourceBot) Delete(ctx context.Context, req resource.DeleteRequest, re } } +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) } -const ( - statusChangePending = "Pending" - statusDeleting = "Deleting" - statusNormal = "Normal" - statusUpdated = "Updated" -) - func waitBotCreated(ctx context.Context, conn *lexmodelsv2.Client, id string, timeout time.Duration) (*lexmodelsv2.DescribeBotOutput, error) { stateConf := &retry.StateChangeConf{ - Pending: []string{}, - Target: []string{statusNormal}, + Pending: enum.Slice(awstypes.BotStatusCreating), + Target: enum.Slice(awstypes.BotStatusAvailable), Refresh: statusBot(ctx, conn, id), Timeout: timeout, NotFoundChecks: 20, @@ -348,8 +356,8 @@ func waitBotCreated(ctx context.Context, conn *lexmodelsv2.Client, id string, ti func waitBotUpdated(ctx context.Context, conn *lexmodelsv2.Client, id string, timeout time.Duration) (*lexmodelsv2.DescribeBotOutput, error) { stateConf := &retry.StateChangeConf{ - Pending: []string{statusChangePending}, - Target: []string{statusUpdated}, + Pending: enum.Slice(awstypes.BotStatusUpdating), + Target: enum.Slice(awstypes.BotStatusAvailable), Refresh: statusBot(ctx, conn, id), Timeout: timeout, NotFoundChecks: 20, @@ -366,7 +374,7 @@ func waitBotUpdated(ctx context.Context, conn *lexmodelsv2.Client, id string, ti func waitBotDeleted(ctx context.Context, conn *lexmodelsv2.Client, id string, timeout time.Duration) (*lexmodelsv2.DescribeBotOutput, error) { stateConf := &retry.StateChangeConf{ - Pending: []string{statusDeleting, statusNormal}, + Pending: enum.Slice(awstypes.BotStatusDeleting), Target: []string{}, Refresh: statusBot(ctx, conn, id), Timeout: timeout, @@ -466,7 +474,7 @@ func expandMembers(ctx context.Context, tfList []membersData) (*awstypes.BotMemb }, diags } -func (rd *resourceBotData) refreshFromOutput(ctx context.Context, out *lexmodelsv2.UpdateBotOutput) diag.Diagnostics { +func (rd *resourceBotData) refreshFromOutput(ctx context.Context, out *lexmodelsv2.DescribeBotOutput) diag.Diagnostics { var diags diag.Diagnostics if out == nil { @@ -488,6 +496,7 @@ func (rd *resourceBotData) refreshFromOutput(ctx context.Context, out *lexmodels } type resourceBotData struct { + ARN types.String `tfsdk:"arn"` DataPrivacy types.Object `tfsdk:"data_privacy"` Description types.String `tfsdk:"description"` ID types.String `tfsdk:"id"` @@ -515,5 +524,5 @@ type membersData struct { } var dataPrivacyAttrTypes = map[string]attr.Type{ - "child_directed": types.StringType, + "child_directed": types.BoolType, } diff --git a/internal/service/lexv2models/bot_test.go b/internal/service/lexv2models/bot_test.go index a4a2e211719..93b7fda4499 100644 --- a/internal/service/lexv2models/bot_test.go +++ b/internal/service/lexv2models/bot_test.go @@ -45,14 +45,14 @@ func TestAccLexV2ModelsBot_basic(t *testing.T) { CheckDestroy: testAccCheckBotDestroy(ctx), Steps: []resource.TestStep{ { - Config: testAccBotConfig_basic(rName), + Config: testAccBotConfig_basic(rName, 10, true), Check: resource.ComposeTestCheckFunc( testAccCheckBotExists(ctx, resourceName, &bot), resource.TestCheckResourceAttr(resourceName, "name", rName), - resource.TestCheckResourceAttr(resourceName, "idle_session_ttl_in_seconds", "5"), + resource.TestCheckResourceAttr(resourceName, "idle_session_ttl_in_seconds", "10"), resource.TestCheckResourceAttr(resourceName, "role_arn", "bot_role_arn"), - resource.TestCheckResourceAttr(resourceName, "data_privacy.#", "1"), - resource.TestCheckResourceAttr(resourceName, "data_privacy.0.child_directed", "true"), + // resource.TestCheckResourceAttr(resourceName, "data_privacy.#", "1"), + // resource.TestCheckResourceAttr(resourceName, "data_privacy.0.child_directed", "true"), ), }, { @@ -64,37 +64,37 @@ func TestAccLexV2ModelsBot_basic(t *testing.T) { }) } -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" +// func TestAccLexV2ModelsBot_disappears(t *testing.T) { +// ctx := acctest.Context(t) +// if testing.Short() { +// t.Skip("skipping long-running test in short mode") +// } - 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), - Check: resource.ComposeTestCheckFunc( - testAccCheckBotExists(ctx, resourceName, &bot), - acctest.CheckFrameworkResourceDisappears(ctx, acctest.Provider, tflexv2models.ResourceBot, resourceName), - ), - ExpectNonEmptyPlan: true, - }, - }, - }) -} +// 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), +// 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 { @@ -168,18 +168,50 @@ func testAccPreCheck(ctx context.Context, t *testing.T) { // } // } -func testAccBotConfig_basic(rName string) string { +func testAccBotBaseConfig() string { return fmt.Sprintf(` +resource "aws_iam_role" "test_role" { + name = "test_role" + assume_role_policy = jsonencode({ + Version = "2012-10-17" + Statement = [ + { + Action = "sts:AssumeRole" + Effect = "Allow" + Sid = "" + Principal = { + Service = "lexv2.amazonaws.com" + } + }, + ] + }) + + tags = { + tag-key = "tag-value" + } +} + +resource "aws_iam_role_policy_attachment" "test-attach" { + role = aws_iam_role.test_role.name + policy_arn = "arn:aws:iam::aws:policy/AmazonLexFullAccess" +} +`) +} + +func testAccBotConfig_basic(rName string, ttl int, dp bool) string { + return acctest.ConfigCompose( + testAccBotBaseConfig(), + fmt.Sprintf(` resource "aws_lexv2models_bot" "test" { - name = %[1]q - idle_session_ttl_in_seconds = "5" + name = %[1]q + idle_session_ttl_in_seconds = %[2]d role_arn = "bot_role_arn" data_privacy { - child_directed = true + child_directed = %[3]t } } -`, rName) +`, rName, ttl, dp)) } func testAccBotConfig_optional(rName, description, botType, aliasId, aliasName, memberId, memberName string) string { From ca3774c6714848c206f9e5d8dd5a02231c65803e Mon Sep 17 00:00:00 2001 From: Sharon Nam Date: Thu, 21 Sep 2023 11:43:18 -0700 Subject: [PATCH 06/10] add tests --- internal/service/lexv2models/bot.go | 71 +++++---- internal/service/lexv2models/bot_test.go | 142 ++++++++++++------ .../lexv2models/service_package_gen.go | 3 + 3 files changed, 147 insertions(+), 69 deletions(-) diff --git a/internal/service/lexv2models/bot.go b/internal/service/lexv2models/bot.go index 7d44721ab8b..f1887e10fb1 100644 --- a/internal/service/lexv2models/bot.go +++ b/internal/service/lexv2models/bot.go @@ -6,13 +6,15 @@ 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-validators/objectvalidator" "github.com/hashicorp/terraform-plugin-framework/attr" "github.com/hashicorp/terraform-plugin-framework/diag" "github.com/hashicorp/terraform-plugin-framework/path" @@ -28,12 +30,14 @@ import ( "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{} @@ -77,7 +81,8 @@ func (r *resourceBot) Schema(ctx context.Context, req resource.SchemaRequest, re names.AttrTags: tftags.TagsAttribute(), names.AttrTagsAll: tftags.TagsAttributeComputedOnly(), "role_arn": schema.StringAttribute{ - Required: true, + CustomType: fwtypes.ARNType, + Required: true, }, "test_bot_alias_tags": schema.MapAttribute{ ElementType: types.StringType, @@ -86,6 +91,9 @@ func (r *resourceBot) Schema(ctx context.Context, req resource.SchemaRequest, re "type": schema.StringAttribute{ Optional: true, Computed: true, + Validators: []validator.String{ + enum.FrameworkValidate[awstypes.BotType](), + }, PlanModifiers: []planmodifier.String{ stringplanmodifier.UseStateForUnknown(), }, @@ -93,9 +101,6 @@ func (r *resourceBot) Schema(ctx context.Context, req resource.SchemaRequest, re }, Blocks: map[string]schema.Block{ "members": schema.ListNestedBlock{ - Validators: []validator.List{ - listvalidator.SizeAtMost(1), - }, NestedObject: schema.NestedBlockObject{ Attributes: map[string]schema.Attribute{ "alias_id": schema.StringAttribute{ @@ -117,6 +122,9 @@ func (r *resourceBot) Schema(ctx context.Context, req resource.SchemaRequest, re }, }, "data_privacy": schema.SingleNestedBlock{ + Validators: []validator.Object{ + objectvalidator.IsRequired(), + }, Attributes: map[string]schema.Attribute{ "child_directed": schema.BoolAttribute{ Required: true, @@ -151,7 +159,8 @@ func (r *resourceBot) Create(ctx context.Context, req resource.CreateRequest, re BotName: aws.String(plan.Name.ValueString()), DataPrivacy: dpInput, IdleSessionTTLInSeconds: aws.Int32(int32(plan.IdleSessionTTLInSeconds.ValueInt64())), - RoleArn: aws.String(plan.RoleARN.ValueString()), + RoleArn: flex.ARNStringFromFramework(ctx, plan.RoleARN), + BotTags: getTagsIn(ctx), } if !plan.TestBotAliasTags.IsNull() { @@ -177,18 +186,24 @@ func (r *resourceBot) Create(ctx context.Context, req resource.CreateRequest, re ) 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.BotName)), + }.String() plan.ID = flex.StringToFramework(ctx, out.BotId) state := plan state.Type = flex.StringValueToFramework(ctx, out.BotType) - // state.ARN = flex.StringValueToFramework(ctx, out.) + state.ARN = flex.StringValueToFramework(ctx, botArn) // resp.Diagnostics.Append(state.refreshFromOutput(ctx, out.BotId)...) resp.Diagnostics.Append(resp.State.Set(ctx, state)...) } 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() { @@ -208,14 +223,22 @@ func (r *resourceBot) Read(ctx context.Context, req resource.ReadRequest, resp * return } - state.RoleARN = flex.StringToFramework(ctx, out.RoleArn) + 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.BotName)), + }.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(ctx, out.DataPrivacy) + datap := flattenDataPrivacy(ctx, out.DataPrivacy) state.DataPrivacy = datap resp.Diagnostics.Append(resp.State.Set(ctx, &state)...) @@ -241,11 +264,11 @@ func (r *resourceBot) Update(ctx context.Context, req resource.UpdateRequest, re dp, _ := expandDataPrivacy(ctx, plan.DataPrivacy) in := lexmodelsv2.UpdateBotInput{ - BotId: aws.String(plan.ID.ValueString()), - BotName: aws.String(plan.Name.ValueString()), + BotId: flex.StringFromFramework(ctx, plan.ID), + BotName: flex.StringFromFramework(ctx, plan.Name), IdleSessionTTLInSeconds: aws.Int32(int32(plan.IdleSessionTTLInSeconds.ValueInt64())), DataPrivacy: dp, - RoleArn: aws.String(plan.RoleARN.ValueString()), + RoleArn: flex.ARNStringFromFramework(ctx, plan.RoleARN), } if !plan.Description.IsNull() { @@ -428,18 +451,17 @@ func FindBotByID(ctx context.Context, conn *lexmodelsv2.Client, id string) (*lex return out, nil } -func flattenDataPrivacy(ctx context.Context, apiObject *awstypes.DataPrivacy) (types.Object, diag.Diagnostics) { - var diags diag.Diagnostics +func flattenDataPrivacy(ctx context.Context, apiObject *awstypes.DataPrivacy) types.Object { attributeTypes := flex.AttributeTypesMust[dataPrivacyData](ctx) if apiObject == nil { - return types.ObjectNull(attributeTypes), diags + return types.ObjectNull(attributeTypes) } obj := map[string]attr.Value{} obj["child_directed"] = flex.BoolToFramework(ctx, &apiObject.ChildDirected) - return types.ObjectValueMust(attributeTypes, obj), diags + return types.ObjectValueMust(attributeTypes, obj) } func expandDataPrivacy(ctx context.Context, object types.Object) (*awstypes.DataPrivacy, diag.Diagnostics) { @@ -457,7 +479,7 @@ func expandDataPrivacy(ctx context.Context, object types.Object) (*awstypes.Data }, diags } -func expandMembers(ctx context.Context, tfList []membersData) (*awstypes.BotMember, diag.Diagnostics) { +func expandMembers(tfList []membersData) (*awstypes.BotMember, diag.Diagnostics) { var diags diag.Diagnostics if len(tfList) == 0 { @@ -480,7 +502,7 @@ func (rd *resourceBotData) refreshFromOutput(ctx context.Context, out *lexmodels if out == nil { return diags } - rd.RoleARN = flex.StringToFramework(ctx, out.RoleArn) + 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)) @@ -488,8 +510,7 @@ func (rd *resourceBotData) refreshFromOutput(ctx context.Context, out *lexmodels rd.IdleSessionTTLInSeconds = flex.Int32ToFramework(ctx, out.IdleSessionTTLInSeconds) // TIP: Setting a complex type. - datap, d := flattenDataPrivacy(ctx, out.DataPrivacy) - diags.Append(d...) + datap := flattenDataPrivacy(ctx, out.DataPrivacy) rd.DataPrivacy = datap return diags @@ -503,7 +524,7 @@ type resourceBotData struct { IdleSessionTTLInSeconds types.Int64 `tfsdk:"idle_session_ttl_in_seconds"` Name types.String `tfsdk:"name"` Members types.List `tfsdk:"members"` - RoleARN types.String `tfsdk:"role_arn"` + 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"` @@ -522,7 +543,3 @@ type membersData struct { 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 index 93b7fda4499..207a69cb5a3 100644 --- a/internal/service/lexv2models/bot_test.go +++ b/internal/service/lexv2models/bot_test.go @@ -9,17 +9,16 @@ import ( "fmt" "testing" + "github.com/aws/aws-sdk-go-v2/service/acm/types" "github.com/aws/aws-sdk-go-v2/service/lexmodelsv2" - "github.com/aws/aws-sdk-go-v2/service/lexmodelsv2/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" - "github.com/hashicorp/terraform-provider-aws/names" - tflexv2models "github.com/hashicorp/terraform-provider-aws/internal/service/lexv2models" + "github.com/hashicorp/terraform-provider-aws/names" ) func TestAccLexV2ModelsBot_basic(t *testing.T) { @@ -45,14 +44,13 @@ func TestAccLexV2ModelsBot_basic(t *testing.T) { CheckDestroy: testAccCheckBotDestroy(ctx), Steps: []resource.TestStep{ { - Config: testAccBotConfig_basic(rName, 10, true), + 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", "10"), - resource.TestCheckResourceAttr(resourceName, "role_arn", "bot_role_arn"), - // resource.TestCheckResourceAttr(resourceName, "data_privacy.#", "1"), - // resource.TestCheckResourceAttr(resourceName, "data_privacy.0.child_directed", "true"), + resource.TestCheckResourceAttr(resourceName, "idle_session_ttl_in_seconds", "60"), + resource.TestCheckResourceAttrSet(resourceName, "role_arn"), + resource.TestCheckResourceAttrSet(resourceName, "data_privacy.%"), ), }, { @@ -64,6 +62,58 @@ func TestAccLexV2ModelsBot_basic(t *testing.T) { }) } +// 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() { @@ -78,14 +128,13 @@ func TestAccLexV2ModelsBot_basic(t *testing.T) { // 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), +// Config: testAccBotConfig_basic(rName, 60, true), // Check: resource.ComposeTestCheckFunc( // testAccCheckBotExists(ctx, resourceName, &bot), // acctest.CheckFrameworkResourceDisappears(ctx, acctest.Provider, tflexv2models.ResourceBot, resourceName), @@ -158,27 +207,17 @@ func testAccPreCheck(ctx context.Context, t *testing.T) { } } -// func testAccCheckBotNotRecreated(before, after *lexmodelsv2.DescribeBotOutput) resource.TestCheckFunc { -// return func(s *terraform.State) error { -// if before, after := aws.ToString(before.BotId), aws.ToString(after.BotId); before != after { -// return create.Error(names.LexV2Models, create.ErrActionCheckingNotRecreated, tflexv2models.ResNameBot, aws.ToString(before.BotId), errors.New("recreated")) -// } - -// return nil -// } -// } - func testAccBotBaseConfig() string { return fmt.Sprintf(` resource "aws_iam_role" "test_role" { - name = "test_role" + name = "test_role" assume_role_policy = jsonencode({ - Version = "2012-10-17" - Statement = [ + Version = "2012-10-17" + Statement = [ { - Action = "sts:AssumeRole" - Effect = "Allow" - Sid = "" + Action = "sts:AssumeRole" + Effect = "Allow" + Sid = "" Principal = { Service = "lexv2.amazonaws.com" } @@ -205,7 +244,7 @@ func testAccBotConfig_basic(rName string, ttl int, dp bool) string { resource "aws_lexv2models_bot" "test" { name = %[1]q idle_session_ttl_in_seconds = %[2]d - role_arn = "bot_role_arn" + role_arn = aws_iam_role.test_role.arn data_privacy { child_directed = %[3]t @@ -214,26 +253,45 @@ resource "aws_lexv2models_bot" "test" { `, rName, ttl, dp)) } -func testAccBotConfig_optional(rName, description, botType, aliasId, aliasName, memberId, memberName string) string { - return fmt.Sprintf(` +func testAccBotConfig_tags1(rName string, ttl int, dp bool, tagKey1, tagValue1 string) string { + return acctest.ConfigCompose( + testAccBotBaseConfig(), + 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(), + fmt.Sprintf(` resource "aws_lexv2models_bot" "test" { - bot_name = %[1]q - description = %[2]q - idle_session_ttl_in_seconds = "5" - type = %[3]q - role_arn = "bot_role_arn" + name = %[1]q + idle_session_ttl_in_seconds = %[2]d + role_arn = aws_iam_role.test_role.arn data_privacy { - child_directed = true + child_directed = %[3]t } +} - members { - alias_id = %[4]q - alias_name = %[5]q - id = %[6]q - name = %[7]q - version = "2.0" + tags = { + %[4]q = %[5]q + %[6]q = %[7]q } } -`, rName, description, botType, aliasId, aliasName, memberId, memberName) +`, rName, ttl, dp, tagKey1, tagValue1, tagKey2, tagValue2)) } diff --git a/internal/service/lexv2models/service_package_gen.go b/internal/service/lexv2models/service_package_gen.go index db745222bf8..55ef56c5226 100644 --- a/internal/service/lexv2models/service_package_gen.go +++ b/internal/service/lexv2models/service_package_gen.go @@ -23,6 +23,9 @@ func (p *servicePackage) FrameworkResources(ctx context.Context) []*types.Servic { Factory: newResourceBot, Name: "Bot", + Tags: &types.ServicePackageResourceTags{ + IdentifierAttribute: "arn", + }, }, } } From decbb65f2426e9066b914dc2b396bdeab9ad2af2 Mon Sep 17 00:00:00 2001 From: Sharon Nam Date: Thu, 21 Sep 2023 13:04:23 -0700 Subject: [PATCH 07/10] fix tests --- internal/service/lexv2models/bot.go | 97 ++++++--- internal/service/lexv2models/bot_test.go | 196 +++++++++---------- website/docs/r/lexv2models_bot.html.markdown | 8 +- 3 files changed, 170 insertions(+), 131 deletions(-) diff --git a/internal/service/lexv2models/bot.go b/internal/service/lexv2models/bot.go index f1887e10fb1..bb1060a1337 100644 --- a/internal/service/lexv2models/bot.go +++ b/internal/service/lexv2models/bot.go @@ -14,7 +14,7 @@ import ( "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/objectvalidator" + "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" @@ -24,7 +24,6 @@ import ( "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-framework/types/basetypes" "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" @@ -121,13 +120,15 @@ func (r *resourceBot) Schema(ctx context.Context, req resource.SchemaRequest, re }, }, }, - "data_privacy": schema.SingleNestedBlock{ - Validators: []validator.Object{ - objectvalidator.IsRequired(), + "data_privacy": schema.ListNestedBlock{ + Validators: []validator.List{ + listvalidator.SizeAtLeast(1), }, - Attributes: map[string]schema.Attribute{ - "child_directed": schema.BoolAttribute{ - Required: true, + NestedObject: schema.NestedBlockObject{ + Attributes: map[string]schema.Attribute{ + "child_directed": schema.BoolAttribute{ + Required: true, + }, }, }, }, @@ -149,7 +150,13 @@ func (r *resourceBot) Create(ctx context.Context, req resource.CreateRequest, re return } - dpInput, d := expandDataPrivacy(ctx, plan.DataPrivacy) + var dp []dataPrivacyData + resp.Diagnostics.Append(plan.DataPrivacy.ElementsAs(ctx, &dp, false)...) + if resp.Diagnostics.HasError() { + return + } + + dpInput, d := expandDataPrivacy(ctx, dp) resp.Diagnostics.Append(d...) if resp.Diagnostics.HasError() { return @@ -191,14 +198,24 @@ func (r *resourceBot) Create(ctx context.Context, req resource.CreateRequest, re Service: "lex", Region: r.Meta().Region, AccountID: r.Meta().AccountID, - Resource: fmt.Sprintf("bot:%s", aws.ToString(out.BotName)), + 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(state.refreshFromOutput(ctx, out.BotId)...) + 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) { @@ -228,7 +245,7 @@ func (r *resourceBot) Read(ctx context.Context, req resource.ReadRequest, resp * Service: "lex", Region: r.Meta().Region, AccountID: r.Meta().AccountID, - Resource: fmt.Sprintf("bot:%s", aws.ToString(out.BotName)), + Resource: fmt.Sprintf("bot/%s", aws.ToString(out.BotId)), }.String() state.ARN = flex.StringValueToFramework(ctx, botArn) state.RoleARN = flex.StringToFrameworkARN(ctx, out.RoleArn, &diags) @@ -238,7 +255,10 @@ func (r *resourceBot) Read(ctx context.Context, req resource.ReadRequest, resp * state.Description = flex.StringToFramework(ctx, out.Description) state.IdleSessionTTLInSeconds = flex.Int32ToFramework(ctx, out.IdleSessionTTLInSeconds) - datap := flattenDataPrivacy(ctx, out.DataPrivacy) + datap, _ := flattenDataPrivacy(ctx, out.DataPrivacy) + if resp.Diagnostics.HasError() { + return + } state.DataPrivacy = datap resp.Diagnostics.Append(resp.State.Set(ctx, &state)...) @@ -261,13 +281,23 @@ func (r *resourceBot) Update(ctx context.Context, req resource.UpdateRequest, re !plan.DataPrivacy.Equal(state.DataPrivacy) || !plan.Type.Equal(state.Type) { - dp, _ := expandDataPrivacy(ctx, plan.DataPrivacy) + var dp []dataPrivacyData + resp.Diagnostics.Append(plan.DataPrivacy.ElementsAs(ctx, &dp, false)...) + if resp.Diagnostics.HasError() { + return + } + + dpInput, d := expandDataPrivacy(ctx, dp) + resp.Diagnostics.Append(d...) + if resp.Diagnostics.HasError() { + return + } in := lexmodelsv2.UpdateBotInput{ BotId: flex.StringFromFramework(ctx, plan.ID), BotName: flex.StringFromFramework(ctx, plan.Name), IdleSessionTTLInSeconds: aws.Int32(int32(plan.IdleSessionTTLInSeconds.ValueInt64())), - DataPrivacy: dp, + DataPrivacy: dpInput, RoleArn: flex.ARNStringFromFramework(ctx, plan.RoleARN), } @@ -451,27 +481,35 @@ func FindBotByID(ctx context.Context, conn *lexmodelsv2.Client, id string) (*lex return out, nil } -func flattenDataPrivacy(ctx context.Context, apiObject *awstypes.DataPrivacy) types.Object { - attributeTypes := flex.AttributeTypesMust[dataPrivacyData](ctx) +func flattenDataPrivacy(ctx context.Context, apiObject *awstypes.DataPrivacy) (types.List, diag.Diagnostics) { + // attributeTypes := flex.AttributeTypesMust[dataPrivacyData](ctx) + + var diags diag.Diagnostics + elemType := types.ObjectType{AttrTypes: dataPrivacyAttrTypes} if apiObject == nil { - return types.ObjectNull(attributeTypes) + 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...) - obj := map[string]attr.Value{} - obj["child_directed"] = flex.BoolToFramework(ctx, &apiObject.ChildDirected) + listVal, d := types.ListValue(elemType, []attr.Value{objVal}) + diags.Append(d...) - return types.ObjectValueMust(attributeTypes, obj) + return listVal, diags } -func expandDataPrivacy(ctx context.Context, object types.Object) (*awstypes.DataPrivacy, diag.Diagnostics) { +func expandDataPrivacy(ctx context.Context, tfList []dataPrivacyData) (*awstypes.DataPrivacy, diag.Diagnostics) { var diags diag.Diagnostics - var dp dataPrivacyData - diags.Append(object.As(ctx, &dp, basetypes.ObjectAsOptions{})...) - if diags.HasError() { + if len(tfList) == 0 { return nil, diags } + dp := tfList[0] cdBool := flex.BoolFromFramework(ctx, dp.ChildDirected) return &awstypes.DataPrivacy{ @@ -510,7 +548,8 @@ func (rd *resourceBotData) refreshFromOutput(ctx context.Context, out *lexmodels rd.IdleSessionTTLInSeconds = flex.Int32ToFramework(ctx, out.IdleSessionTTLInSeconds) // TIP: Setting a complex type. - datap := flattenDataPrivacy(ctx, out.DataPrivacy) + datap, d := flattenDataPrivacy(ctx, out.DataPrivacy) + diags.Append(d...) rd.DataPrivacy = datap return diags @@ -518,7 +557,7 @@ func (rd *resourceBotData) refreshFromOutput(ctx context.Context, out *lexmodels type resourceBotData struct { ARN types.String `tfsdk:"arn"` - DataPrivacy types.Object `tfsdk:"data_privacy"` + 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"` @@ -543,3 +582,7 @@ type membersData struct { 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 index 207a69cb5a3..c30f5e2fc74 100644 --- a/internal/service/lexv2models/bot_test.go +++ b/internal/service/lexv2models/bot_test.go @@ -9,7 +9,6 @@ import ( "fmt" "testing" - "github.com/aws/aws-sdk-go-v2/service/acm/types" "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" @@ -18,6 +17,7 @@ import ( "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" ) @@ -50,7 +50,7 @@ func TestAccLexV2ModelsBot_basic(t *testing.T) { resource.TestCheckResourceAttr(resourceName, "name", rName), resource.TestCheckResourceAttr(resourceName, "idle_session_ttl_in_seconds", "60"), resource.TestCheckResourceAttrSet(resourceName, "role_arn"), - resource.TestCheckResourceAttrSet(resourceName, "data_privacy.%"), + resource.TestCheckResourceAttrSet(resourceName, "data_privacy.0.child_directed"), ), }, { @@ -62,88 +62,88 @@ func TestAccLexV2ModelsBot_basic(t *testing.T) { }) } -// 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 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 { @@ -155,11 +155,11 @@ func testAccCheckBotDestroy(ctx context.Context) resource.TestCheckFunc { } _, err := tflexv2models.FindBotByID(ctx, conn, rs.Primary.ID) + if tfresource.NotFound(err) { + continue + } + if err != nil { - var nfe *types.ResourceNotFoundException - if errors.As(err, &nfe) { - return nil - } return err } @@ -207,10 +207,10 @@ func testAccPreCheck(ctx context.Context, t *testing.T) { } } -func testAccBotBaseConfig() string { +func testAccBotBaseConfig(rName string) string { return fmt.Sprintf(` resource "aws_iam_role" "test_role" { - name = "test_role" + name = %[1]q assume_role_policy = jsonencode({ Version = "2012-10-17" Statement = [ @@ -224,22 +224,18 @@ resource "aws_iam_role" "test_role" { }, ] }) - - tags = { - tag-key = "tag-value" - } } resource "aws_iam_role_policy_attachment" "test-attach" { role = aws_iam_role.test_role.name policy_arn = "arn:aws:iam::aws:policy/AmazonLexFullAccess" } -`) +`, rName) } func testAccBotConfig_basic(rName string, ttl int, dp bool) string { return acctest.ConfigCompose( - testAccBotBaseConfig(), + testAccBotBaseConfig(rName), fmt.Sprintf(` resource "aws_lexv2models_bot" "test" { name = %[1]q @@ -247,7 +243,7 @@ resource "aws_lexv2models_bot" "test" { role_arn = aws_iam_role.test_role.arn data_privacy { - child_directed = %[3]t + child_directed = "%[3]t" } } `, rName, ttl, dp)) @@ -255,7 +251,7 @@ resource "aws_lexv2models_bot" "test" { func testAccBotConfig_tags1(rName string, ttl int, dp bool, tagKey1, tagValue1 string) string { return acctest.ConfigCompose( - testAccBotBaseConfig(), + testAccBotBaseConfig(rName), fmt.Sprintf(` resource "aws_lexv2models_bot" "test" { name = %[1]q @@ -265,7 +261,6 @@ resource "aws_lexv2models_bot" "test" { data_privacy { child_directed = %[3]t } -} tags = { %[4]q = %[5]q @@ -276,7 +271,7 @@ resource "aws_lexv2models_bot" "test" { func testAccBotConfig_tags2(rName string, ttl int, dp bool, tagKey1, tagValue1, tagKey2, tagValue2 string) string { return acctest.ConfigCompose( - testAccBotBaseConfig(), + testAccBotBaseConfig(rName), fmt.Sprintf(` resource "aws_lexv2models_bot" "test" { name = %[1]q @@ -286,7 +281,6 @@ resource "aws_lexv2models_bot" "test" { data_privacy { child_directed = %[3]t } -} tags = { %[4]q = %[5]q diff --git a/website/docs/r/lexv2models_bot.html.markdown b/website/docs/r/lexv2models_bot.html.markdown index 3e690dde0e8..e3fb776af5b 100644 --- a/website/docs/r/lexv2models_bot.html.markdown +++ b/website/docs/r/lexv2models_bot.html.markdown @@ -18,10 +18,10 @@ Terraform resource for managing an AWS Lex V2 Models Bot. resource "aws_lexv2models_bot" "example" { name = "example" data_privacy { - child_directed = “boolean”, + child_directed = "boolean" } idle_session_ttl_in_seconds = 10 - role_arn = “bot_example_arn” + role_arn = "bot_example_arn" } ``` @@ -48,16 +48,18 @@ This resource exports the following attributes in addition to the arguments abov * `id` - A 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) - The alias ID of a bot that is a member of this network of bots. * `alias_name` (Required) - The alias ID of a bot that is a member of this network of bots. * `id` (Required) - The unique ID of a bot that is a member of this network of bots. * `name` (Required) - The unique name of a bot that is a member of this network of bots. * `version` (Required) - The 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): From 02472af052bef2dac0ade7ef4ddcbe7b46a89454 Mon Sep 17 00:00:00 2001 From: Sharon Nam Date: Thu, 21 Sep 2023 13:57:44 -0700 Subject: [PATCH 08/10] use partition datasource --- internal/service/lexv2models/bot.go | 62 +++++++++----------- internal/service/lexv2models/bot_test.go | 9 +-- website/docs/r/lexv2models_bot.html.markdown | 22 +++---- 3 files changed, 43 insertions(+), 50 deletions(-) diff --git a/internal/service/lexv2models/bot.go b/internal/service/lexv2models/bot.go index bb1060a1337..7a7181a0f95 100644 --- a/internal/service/lexv2models/bot.go +++ b/internal/service/lexv2models/bot.go @@ -156,11 +156,7 @@ func (r *resourceBot) Create(ctx context.Context, req resource.CreateRequest, re return } - dpInput, d := expandDataPrivacy(ctx, dp) - resp.Diagnostics.Append(d...) - if resp.Diagnostics.HasError() { - return - } + dpInput := expandDataPrivacy(ctx, dp) in := lexmodelsv2.CreateBotInput{ BotName: aws.String(plan.Name.ValueString()), @@ -178,6 +174,12 @@ func (r *resourceBot) Create(ctx context.Context, req resource.CreateRequest, re 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( @@ -255,7 +257,7 @@ func (r *resourceBot) Read(ctx context.Context, req resource.ReadRequest, resp * state.Description = flex.StringToFramework(ctx, out.Description) state.IdleSessionTTLInSeconds = flex.Int32ToFramework(ctx, out.IdleSessionTTLInSeconds) - datap, _ := flattenDataPrivacy(ctx, out.DataPrivacy) + datap, _ := flattenDataPrivacy(out.DataPrivacy) if resp.Diagnostics.HasError() { return } @@ -280,18 +282,13 @@ func (r *resourceBot) Update(ctx context.Context, req resource.UpdateRequest, re !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, d := expandDataPrivacy(ctx, dp) - resp.Diagnostics.Append(d...) - if resp.Diagnostics.HasError() { - return - } + dpInput := expandDataPrivacy(ctx, dp) in := lexmodelsv2.UpdateBotInput{ BotId: flex.StringFromFramework(ctx, plan.ID), @@ -481,9 +478,7 @@ func FindBotByID(ctx context.Context, conn *lexmodelsv2.Client, id string) (*lex return out, nil } -func flattenDataPrivacy(ctx context.Context, apiObject *awstypes.DataPrivacy) (types.List, diag.Diagnostics) { - // attributeTypes := flex.AttributeTypesMust[dataPrivacyData](ctx) - +func flattenDataPrivacy(apiObject *awstypes.DataPrivacy) (types.List, diag.Diagnostics) { var diags diag.Diagnostics elemType := types.ObjectType{AttrTypes: dataPrivacyAttrTypes} @@ -503,10 +498,9 @@ func flattenDataPrivacy(ctx context.Context, apiObject *awstypes.DataPrivacy) (t return listVal, diags } -func expandDataPrivacy(ctx context.Context, tfList []dataPrivacyData) (*awstypes.DataPrivacy, diag.Diagnostics) { - var diags diag.Diagnostics +func expandDataPrivacy(ctx context.Context, tfList []dataPrivacyData) *awstypes.DataPrivacy { if len(tfList) == 0 { - return nil, diags + return nil } dp := tfList[0] @@ -514,24 +508,27 @@ func expandDataPrivacy(ctx context.Context, tfList []dataPrivacyData) (*awstypes return &awstypes.DataPrivacy{ ChildDirected: aws.ToBool(cdBool), - }, diags + } } -func expandMembers(tfList []membersData) (*awstypes.BotMember, diag.Diagnostics) { - var diags diag.Diagnostics - +func expandMembers(ctx context.Context, tfList []membersData) []awstypes.BotMember { if len(tfList) == 0 { - return nil, diags + 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) } - mb := tfList[0] - return &awstypes.BotMember{ - BotMemberAliasId: aws.String(mb.AliasID.ValueString()), - BotMemberAliasName: aws.String(mb.AliasName.ValueString()), - BotMemberId: aws.String(mb.ID.ValueString()), - BotMemberName: aws.String(mb.Name.ValueString()), - BotMemberVersion: aws.String(mb.Version.ValueString()), - }, diags + return mb } func (rd *resourceBotData) refreshFromOutput(ctx context.Context, out *lexmodelsv2.DescribeBotOutput) diag.Diagnostics { @@ -547,8 +544,7 @@ func (rd *resourceBotData) refreshFromOutput(ctx context.Context, out *lexmodels rd.Description = flex.StringToFramework(ctx, out.Description) rd.IdleSessionTTLInSeconds = flex.Int32ToFramework(ctx, out.IdleSessionTTLInSeconds) - // TIP: Setting a complex type. - datap, d := flattenDataPrivacy(ctx, out.DataPrivacy) + datap, d := flattenDataPrivacy(out.DataPrivacy) diags.Append(d...) rd.DataPrivacy = datap diff --git a/internal/service/lexv2models/bot_test.go b/internal/service/lexv2models/bot_test.go index c30f5e2fc74..1e6d826b836 100644 --- a/internal/service/lexv2models/bot_test.go +++ b/internal/service/lexv2models/bot_test.go @@ -23,11 +23,6 @@ import ( func TestAccLexV2ModelsBot_basic(t *testing.T) { ctx := acctest.Context(t) - // TIP: This is a long-running test guard for tests that run longer than - // 300s (5 min) generally. - // if testing.Short() { - // t.Skip("skipping long-running test in short mode") - // } var bot lexmodelsv2.DescribeBotOutput rName := sdkacctest.RandomWithPrefix(acctest.ResourcePrefix) @@ -209,6 +204,8 @@ func testAccPreCheck(ctx context.Context, t *testing.T) { 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({ @@ -228,7 +225,7 @@ resource "aws_iam_role" "test_role" { resource "aws_iam_role_policy_attachment" "test-attach" { role = aws_iam_role.test_role.name - policy_arn = "arn:aws:iam::aws:policy/AmazonLexFullAccess" + policy_arn = "arn:${data.aws_partition.current.partition}:iam::aws:policy/AmazonLexFullAccess" } `, rName) } diff --git a/website/docs/r/lexv2models_bot.html.markdown b/website/docs/r/lexv2models_bot.html.markdown index e3fb776af5b..824a06b310c 100644 --- a/website/docs/r/lexv2models_bot.html.markdown +++ b/website/docs/r/lexv2models_bot.html.markdown @@ -29,24 +29,24 @@ resource "aws_lexv2models_bot" "example" { The following arguments are required: -* `name` - The 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. +* `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` - The 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` - The Amazon Resource Name (ARN) of an IAM role that has permission to access the bot. +* `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` - A list of tags to add to the bot. You can only add tags when you create a bot. -* `bot_type` - The type of a bot to create. -* `description` - A description of the bot. It appears in lists to help you identify a particular bot. -* `test_bot_alias_tags` - A list of tags to add to the test alias for a bot. You can only add tags when you create a bot. +* `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` - A unique identifier for a particular bot. +* `id` - Unique identifier for a particular bot. ### Data Privacy @@ -64,9 +64,9 @@ This resource exports the following attributes in addition to the arguments abov [Configuration options](https://developer.hashicorp.com/terraform/language/resources/syntax#operation-timeouts): -* `create` - (Default `60m`) -* `update` - (Default `180m`) -* `delete` - (Default `90m`) +* `create` - (Default `30m`) +* `update` - (Default `30m`) +* `delete` - (Default `30m`) ## Import From 2d3f8a010eda4797d2bc6b0926fe70e8c5b70ee1 Mon Sep 17 00:00:00 2001 From: Sharon Nam Date: Wed, 27 Sep 2023 14:01:29 -0700 Subject: [PATCH 09/10] fix markdown --- website/docs/r/lexv2models_bot.html.markdown | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/website/docs/r/lexv2models_bot.html.markdown b/website/docs/r/lexv2models_bot.html.markdown index 824a06b310c..d7af1a59ae1 100644 --- a/website/docs/r/lexv2models_bot.html.markdown +++ b/website/docs/r/lexv2models_bot.html.markdown @@ -54,11 +54,11 @@ This resource exports the following attributes in addition to the arguments abov ### Bot Members -* `alias_id` (Required) - The alias ID of a bot that is a member of this network of bots. -* `alias_name` (Required) - The alias ID of a bot that is a member of this network of bots. -* `id` (Required) - The unique ID of a bot that is a member of this network of bots. -* `name` (Required) - The unique name of a bot that is a member of this network of bots. -* `version` (Required) - The version of a bot that is a member of this network of bots. +* `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 From d0e3323c845e1f23d7c79ab49b377b7ab301c102 Mon Sep 17 00:00:00 2001 From: Sharon Nam Date: Wed, 27 Sep 2023 14:02:54 -0700 Subject: [PATCH 10/10] add changelog --- .changelog/33475.txt | 3 +++ 1 file changed, 3 insertions(+) create mode 100644 .changelog/33475.txt 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