From e85b28ed23c173984895f9c03f5f3b29aa9a9539 Mon Sep 17 00:00:00 2001 From: Subham Mukhopadhyay Date: Sat, 18 Oct 2025 01:56:36 +0530 Subject: [PATCH 01/10] add schema and model --- .../fis/target_account_configuration.go | 527 ++++++++++++++++++ 1 file changed, 527 insertions(+) create mode 100644 internal/service/fis/target_account_configuration.go diff --git a/internal/service/fis/target_account_configuration.go b/internal/service/fis/target_account_configuration.go new file mode 100644 index 000000000000..eb17117264ac --- /dev/null +++ b/internal/service/fis/target_account_configuration.go @@ -0,0 +1,527 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package fis + +import ( + "context" + "errors" + "time" + + "github.com/YakDriver/smarterr" + "github.com/aws/aws-sdk-go-v2/aws" + "github.com/aws/aws-sdk-go-v2/service/fis" + awstypes "github.com/aws/aws-sdk-go-v2/service/fis/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/stringvalidator" + "github.com/hashicorp/terraform-plugin-framework/path" + "github.com/hashicorp/terraform-plugin-framework/resource" + "github.com/hashicorp/terraform-plugin-framework/resource/schema" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/planmodifier" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/stringplanmodifier" + "github.com/hashicorp/terraform-plugin-framework/schema/validator" + "github.com/hashicorp/terraform-plugin-framework/types" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/retry" + "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/internal/errs/fwdiag" + "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" + "github.com/hashicorp/terraform-provider-aws/internal/smerr" + "github.com/hashicorp/terraform-provider-aws/internal/sweep" + sweepfw "github.com/hashicorp/terraform-provider-aws/internal/sweep/framework" + "github.com/hashicorp/terraform-provider-aws/internal/tfresource" + "github.com/hashicorp/terraform-provider-aws/names" +) + +// Function annotations are used for resource registration to the Provider. DO NOT EDIT. +// @FrameworkResource("aws_fis_target_account_configuration", name="Target Account Configuration") +func newResourceTargetAccountConfiguration(_ context.Context) (resource.ResourceWithConfigure, error) { + r := &resourceTargetAccountConfiguration{} + + r.SetDefaultCreateTimeout(30 * time.Minute) + r.SetDefaultUpdateTimeout(30 * time.Minute) + r.SetDefaultDeleteTimeout(30 * time.Minute) + + return r, nil +} + +const ( + ResNameTargetAccountConfiguration = "Target Account Configuration" +) + +type resourceTargetAccountConfiguration struct { + framework.ResourceWithModel[resourceTargetAccountConfigurationModel] + framework.WithTimeouts +} + +func (r *resourceTargetAccountConfiguration) Schema(ctx context.Context, req resource.SchemaRequest, resp *resource.SchemaResponse) { + resp.Schema = schema.Schema{ + Attributes: map[string]schema.Attribute{ + "account_id": schema.StringAttribute{ + Required: true, + Validators: []validator.String{stringvalidator.LengthBetween(12, 48)}, + PlanModifiers: []planmodifier.String{ + stringplanmodifier.RequiresReplace(), + }, + }, + names.AttrDescription: schema.StringAttribute{ + Optional: true, + Computed: true, + }, + "experiment_id": schema.StringAttribute{ + Required: true, + Validators: []validator.String{stringvalidator.LengthBetween(1, 64)}, + PlanModifiers: []planmodifier.String{ + stringplanmodifier.RequiresReplace(), + }, + }, + + "roleArn": schema.StringAttribute{ + Optional: true, + Computed: true, + }, + }, + Blocks: map[string]schema.Block{ + names.AttrTimeouts: timeouts.Block(ctx, timeouts.Opts{ + Create: true, + Update: true, + Delete: true, + }), + }, + } +} + +func (r *resourceTargetAccountConfiguration) 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().FISClient(ctx) + + // TIP: -- 2. Fetch the plan + var plan resourceTargetAccountConfigurationModel + smerr.EnrichAppend(ctx, &resp.Diagnostics, req.Plan.Get(ctx, &plan)) + if resp.Diagnostics.HasError() { + return + } + + // TIP: -- 3. Populate a Create input structure + var input fis.CreateTargetAccountConfigurationInput + // TIP: Using a field name prefix allows mapping fields such as `ID` to `TargetAccountConfigurationId` + smerr.EnrichAppend(ctx, &resp.Diagnostics, flex.Expand(ctx, plan, &input, flex.WithFieldNamePrefix("TargetAccountConfiguration"))) + if resp.Diagnostics.HasError() { + return + } + + // TIP: -- 4. Call the AWS Create function + out, err := conn.CreateTargetAccountConfiguration(ctx, &input) + if err != nil { + // TIP: Since ID has not been set yet, you cannot use plan.ID.String() + // in error messages at this point. + smerr.AddError(ctx, &resp.Diagnostics, err, smerr.ID, plan.Name.String()) + return + } + if out == nil || out.TargetAccountConfiguration == nil { + smerr.AddError(ctx, &resp.Diagnostics, errors.New("empty output"), smerr.ID, plan.Name.String()) + return + } + + // TIP: -- 5. Using the output from the create function, set attributes + smerr.EnrichAppend(ctx, &resp.Diagnostics, flex.Flatten(ctx, out, &plan)) + if resp.Diagnostics.HasError() { + return + } + + // TIP: -- 6. Use a waiter to wait for create to complete + createTimeout := r.CreateTimeout(ctx, plan.Timeouts) + _, err = waitTargetAccountConfigurationCreated(ctx, conn, plan.ID.ValueString(), createTimeout) + if err != nil { + smerr.AddError(ctx, &resp.Diagnostics, err, smerr.ID, plan.Name.String()) + return + } + + // TIP: -- 7. Save the request plan to response state + smerr.EnrichAppend(ctx, &resp.Diagnostics, resp.State.Set(ctx, plan)) +} + +func (r *resourceTargetAccountConfiguration) 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().FISClient(ctx) + + // TIP: -- 2. Fetch the state + var state resourceTargetAccountConfigurationModel + smerr.EnrichAppend(ctx, &resp.Diagnostics, 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 := findTargetAccountConfigurationByID(ctx, conn, state.ID.ValueString()) + // TIP: -- 4. Remove resource from state if it is not found + if tfresource.NotFound(err) { + resp.Diagnostics.Append(fwdiag.NewResourceNotFoundWarningDiagnostic(err)) + resp.State.RemoveResource(ctx) + return + } + if err != nil { + smerr.AddError(ctx, &resp.Diagnostics, err, smerr.ID, state.ID.String()) + return + } + + // TIP: -- 5. Set the arguments and attributes + smerr.EnrichAppend(ctx, &resp.Diagnostics, flex.Flatten(ctx, out, &state)) + if resp.Diagnostics.HasError() { + return + } + + // TIP: -- 6. Set the state + smerr.EnrichAppend(ctx, &resp.Diagnostics, resp.State.Set(ctx, &state)) +} + +func (r *resourceTargetAccountConfiguration) 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().FISClient(ctx) + + // TIP: -- 2. Fetch the plan + var plan, state resourceTargetAccountConfigurationModel + smerr.EnrichAppend(ctx, &resp.Diagnostics, req.Plan.Get(ctx, &plan)) + smerr.EnrichAppend(ctx, &resp.Diagnostics, req.State.Get(ctx, &state)) + if resp.Diagnostics.HasError() { + return + } + + // TIP: -- 3. Get the difference between the plan and state, if any + diff, d := flex.Diff(ctx, plan, state) + smerr.EnrichAppend(ctx, &resp.Diagnostics, d) + if resp.Diagnostics.HasError() { + return + } + + if diff.HasChanges() { + var input fis.UpdateTargetAccountConfigurationInput + smerr.EnrichAppend(ctx, &resp.Diagnostics, flex.Expand(ctx, plan, &input, flex.WithFieldNamePrefix("Test"))) + if resp.Diagnostics.HasError() { + return + } + + // TIP: -- 4. Call the AWS modify/update function + out, err := conn.UpdateTargetAccountConfiguration(ctx, &input) + if err != nil { + smerr.AddError(ctx, &resp.Diagnostics, err, smerr.ID, plan.ID.String()) + return + } + if out == nil || out.TargetAccountConfiguration == nil { + smerr.AddError(ctx, &resp.Diagnostics, errors.New("empty output"), smerr.ID, plan.ID.String()) + return + } + + // TIP: Using the output from the update function, re-set any computed attributes + smerr.EnrichAppend(ctx, &resp.Diagnostics, flex.Flatten(ctx, out, &plan)) + if resp.Diagnostics.HasError() { + return + } + } + + // TIP: -- 5. Use a waiter to wait for update to complete + updateTimeout := r.UpdateTimeout(ctx, plan.Timeouts) + _, err := waitTargetAccountConfigurationUpdated(ctx, conn, plan.ID.ValueString(), updateTimeout) + if err != nil { + smerr.AddError(ctx, &resp.Diagnostics, err, smerr.ID, plan.ID.String()) + return + } + + // TIP: -- 6. Save the request plan to response state + smerr.EnrichAppend(ctx, &resp.Diagnostics, resp.State.Set(ctx, &plan)) +} + +func (r *resourceTargetAccountConfiguration) 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().FISClient(ctx) + + // TIP: -- 2. Fetch the state + var state resourceTargetAccountConfigurationModel + smerr.EnrichAppend(ctx, &resp.Diagnostics, req.State.Get(ctx, &state)) + if resp.Diagnostics.HasError() { + return + } + + // TIP: -- 3. Populate a delete input structure + input := fis.DeleteTargetAccountConfigurationInput{ + TargetAccountConfigurationId: state.ID.ValueStringPointer(), + } + + // TIP: -- 4. Call the AWS delete function + _, err := conn.DeleteTargetAccountConfiguration(ctx, &input) + // 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 { + if errs.IsA[*awstypes.ResourceNotFoundException](err) { + return + } + + smerr.AddError(ctx, &resp.Diagnostics, err, smerr.ID, state.ID.String()) + return + } + + // TIP: -- 5. Use a waiter to wait for delete to complete + deleteTimeout := r.DeleteTimeout(ctx, state.Timeouts) + _, err = waitTargetAccountConfigurationDeleted(ctx, conn, state.ID.ValueString(), deleteTimeout) + if err != nil { + smerr.AddError(ctx, &resp.Diagnostics, err, smerr.ID, state.ID.String()) + 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 *resourceTargetAccountConfiguration) ImportState(ctx context.Context, req resource.ImportStateRequest, resp *resource.ImportStateResponse) { + resource.ImportStatePassthroughID(ctx, path.Root(names.AttrID), 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., awstypes.StatusInProgress). +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 waitTargetAccountConfigurationCreated(ctx context.Context, conn *fis.Client, id string, timeout time.Duration) (*awstypes.TargetAccountConfiguration, error) { + stateConf := &retry.StateChangeConf{ + Pending: []string{}, + Target: []string{statusNormal}, + Refresh: statusTargetAccountConfiguration(ctx, conn, id), + Timeout: timeout, + NotFoundChecks: 20, + ContinuousTargetOccurence: 2, + } + + outputRaw, err := stateConf.WaitForStateContext(ctx) + if out, ok := outputRaw.(*fis.TargetAccountConfiguration); ok { + return out, smarterr.NewError(err) + } + + return nil, smarterr.NewError(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 waitTargetAccountConfigurationUpdated(ctx context.Context, conn *fis.Client, id string, timeout time.Duration) (*awstypes.TargetAccountConfiguration, error) { + stateConf := &retry.StateChangeConf{ + Pending: []string{statusChangePending}, + Target: []string{statusUpdated}, + Refresh: statusTargetAccountConfiguration(ctx, conn, id), + Timeout: timeout, + NotFoundChecks: 20, + ContinuousTargetOccurence: 2, + } + + outputRaw, err := stateConf.WaitForStateContext(ctx) + if out, ok := outputRaw.(*fis.TargetAccountConfiguration); ok { + return out, smarterr.NewError(err) + } + + return nil, smarterr.NewError(err) +} + +// TIP: A deleted waiter is almost like a backwards created waiter. There may +// be additional pending states, however. +func waitTargetAccountConfigurationDeleted(ctx context.Context, conn *fis.Client, id string, timeout time.Duration) (*awstypes.TargetAccountConfiguration, error) { + stateConf := &retry.StateChangeConf{ + Pending: []string{statusDeleting, statusNormal}, + Target: []string{}, + Refresh: statusTargetAccountConfiguration(ctx, conn, id), + Timeout: timeout, + } + + outputRaw, err := stateConf.WaitForStateContext(ctx) + if out, ok := outputRaw.(*fis.TargetAccountConfiguration); ok { + return out, smarterr.NewError(err) + } + + return nil, smarterr.NewError(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 statusTargetAccountConfiguration(ctx context.Context, conn *fis.Client, id string) retry.StateRefreshFunc { + return func() (any, string, error) { + out, err := findTargetAccountConfigurationByID(ctx, conn, id) + if tfresource.NotFound(err) { + return nil, "", nil + } + + if err != nil { + return nil, "", smarterr.NewError(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 findTargetAccountConfigurationByID(ctx context.Context, conn *fis.Client, id string) (*awstypes.TargetAccountConfiguration, error) { + input := fis.GetTargetAccountConfigurationInput{ + Id: aws.String(id), + } + + out, err := conn.GetTargetAccountConfiguration(ctx, &input) + if err != nil { + if errs.IsA[*awstypes.ResourceNotFoundException](err) { + return nil, smarterr.NewError(&retry.NotFoundError{ + LastError: err, + LastRequest: &input, + }) + } + + return nil, smarterr.NewError(err) + } + + if out == nil || out.TargetAccountConfiguration == nil { + return nil, smarterr.NewError(tfresource.NewEmptyResultError(&input)) + } + + return out.TargetAccountConfiguration, nil +} + +type resourceTargetAccountConfigurationModel struct { + framework.WithRegionModel + AccountId types.String `tfsdk:"account_id"` + ExperimentId types.String `tfsdk:"experiment_id"` + Description types.String `tfsdk:"description"` + RoleArn types.String `tfsdk:"role_arn"` + Timeouts timeouts.Value `tfsdk:"timeouts"` +} + +// TIP: ==== SWEEPERS ==== +// When acceptance testing resources, interrupted or failed tests may +// leave behind orphaned resources in an account. To facilitate cleaning +// up lingering resources, each resource implementation should include +// a corresponding "sweeper" function. +// +// The sweeper function lists all resources of a given type and sets the +// appropriate identifers required to delete the resource via the Delete +// method implemented above. +// +// Once the sweeper function is implemented, register it in sweep.go +// as follows: +// +// awsv2.Register("aws_fis_target_account_configuration", sweepTargetAccountConfigurations) +// +// See more: +// https://hashicorp.github.io/terraform-provider-aws/running-and-writing-acceptance-tests/#acceptance-test-sweepers +func sweepTargetAccountConfigurations(ctx context.Context, client *conns.AWSClient) ([]sweep.Sweepable, error) { + input := fis.ListTargetAccountConfigurationsInput{} + conn := client.FISClient(ctx) + var sweepResources []sweep.Sweepable + + pages := fis.NewListTargetAccountConfigurationsPaginator(conn, &input) + for pages.HasMorePages() { + page, err := pages.NextPage(ctx) + if err != nil { + return nil, smarterr.NewError(err) + } + + for _, v := range page.TargetAccountConfigurations { + sweepResources = append(sweepResources, sweepfw.NewSweepResource(newResourceTargetAccountConfiguration, client, + sweepfw.NewAttribute(names.AttrID, aws.ToString(v.TargetAccountConfigurationId))), + ) + } + } + + return sweepResources, nil +} From 254f14cc059af223f13e56f4f62270b12512c7a0 Mon Sep 17 00:00:00 2001 From: Subham Mukhopadhyay Date: Mon, 27 Oct 2025 23:02:19 +0530 Subject: [PATCH 02/10] stable changes --- internal/service/fis/service_package_gen.go | 9 +- internal/service/fis/sweep.go | 1 + .../fis/target_account_configuration.go | 345 ++++-------------- 3 files changed, 72 insertions(+), 283 deletions(-) diff --git a/internal/service/fis/service_package_gen.go b/internal/service/fis/service_package_gen.go index 248151156c33..fd9ca82939a2 100644 --- a/internal/service/fis/service_package_gen.go +++ b/internal/service/fis/service_package_gen.go @@ -29,7 +29,14 @@ func (p *servicePackage) FrameworkDataSources(ctx context.Context) []*inttypes.S } func (p *servicePackage) FrameworkResources(ctx context.Context) []*inttypes.ServicePackageFrameworkResource { - return []*inttypes.ServicePackageFrameworkResource{} + return []*inttypes.ServicePackageFrameworkResource{ + { + Factory: newResourceTargetAccountConfiguration, + TypeName: "aws_fis_target_account_configuration", + Name: "Target Account Configuration", + Region: unique.Make(inttypes.ResourceRegionDefault()), + }, + } } func (p *servicePackage) SDKDataSources(ctx context.Context) []*inttypes.ServicePackageSDKDataSource { diff --git a/internal/service/fis/sweep.go b/internal/service/fis/sweep.go index 3ab41eb1dd00..04d20e5d62a7 100644 --- a/internal/service/fis/sweep.go +++ b/internal/service/fis/sweep.go @@ -15,6 +15,7 @@ import ( func RegisterSweepers() { awsv2.Register("aws_fis_experiment_template", sweepExperimentTemplates) + awsv2.Register("aws_fis_target_account_configuration", sweepTargetAccountConfigurations) } func sweepExperimentTemplates(ctx context.Context, client *conns.AWSClient) ([]sweep.Sweepable, error) { diff --git a/internal/service/fis/target_account_configuration.go b/internal/service/fis/target_account_configuration.go index eb17117264ac..4a6ace011d11 100644 --- a/internal/service/fis/target_account_configuration.go +++ b/internal/service/fis/target_account_configuration.go @@ -6,6 +6,8 @@ package fis import ( "context" "errors" + "fmt" + "strings" "time" "github.com/YakDriver/smarterr" @@ -13,7 +15,6 @@ import ( "github.com/aws/aws-sdk-go-v2/service/fis" awstypes "github.com/aws/aws-sdk-go-v2/service/fis/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/stringvalidator" "github.com/hashicorp/terraform-plugin-framework/path" "github.com/hashicorp/terraform-plugin-framework/resource" @@ -24,12 +25,10 @@ import ( "github.com/hashicorp/terraform-plugin-framework/types" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/retry" "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/internal/errs/fwdiag" "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" "github.com/hashicorp/terraform-provider-aws/internal/smerr" "github.com/hashicorp/terraform-provider-aws/internal/sweep" sweepfw "github.com/hashicorp/terraform-provider-aws/internal/sweep/framework" @@ -69,20 +68,21 @@ func (r *resourceTargetAccountConfiguration) Schema(ctx context.Context, req res }, }, names.AttrDescription: schema.StringAttribute{ - Optional: true, - Computed: true, + Optional: true, + Computed: true, + Validators: []validator.String{stringvalidator.LengthAtMost(512)}, }, - "experiment_id": schema.StringAttribute{ + "experiment_template_id": schema.StringAttribute{ Required: true, Validators: []validator.String{stringvalidator.LengthBetween(1, 64)}, PlanModifiers: []planmodifier.String{ stringplanmodifier.RequiresReplace(), }, }, - - "roleArn": schema.StringAttribute{ - Optional: true, - Computed: true, + "role_arn": schema.StringAttribute{ + Optional: true, + Computed: true, + Validators: []validator.String{stringvalidator.LengthBetween(20, 2048)}, }, }, Blocks: map[string]schema.Block{ @@ -96,140 +96,68 @@ func (r *resourceTargetAccountConfiguration) Schema(ctx context.Context, req res } func (r *resourceTargetAccountConfiguration) 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().FISClient(ctx) - // TIP: -- 2. Fetch the plan var plan resourceTargetAccountConfigurationModel smerr.EnrichAppend(ctx, &resp.Diagnostics, req.Plan.Get(ctx, &plan)) if resp.Diagnostics.HasError() { return } - // TIP: -- 3. Populate a Create input structure var input fis.CreateTargetAccountConfigurationInput - // TIP: Using a field name prefix allows mapping fields such as `ID` to `TargetAccountConfigurationId` - smerr.EnrichAppend(ctx, &resp.Diagnostics, flex.Expand(ctx, plan, &input, flex.WithFieldNamePrefix("TargetAccountConfiguration"))) + smerr.EnrichAppend(ctx, &resp.Diagnostics, flex.Expand(ctx, plan, &input)) if resp.Diagnostics.HasError() { return } - // TIP: -- 4. Call the AWS Create function out, err := conn.CreateTargetAccountConfiguration(ctx, &input) if err != nil { - // TIP: Since ID has not been set yet, you cannot use plan.ID.String() - // in error messages at this point. - smerr.AddError(ctx, &resp.Diagnostics, err, smerr.ID, plan.Name.String()) + smerr.AddError(ctx, &resp.Diagnostics, err, smerr.ID, plan.ExperimentTemplateId.String()) return } if out == nil || out.TargetAccountConfiguration == nil { - smerr.AddError(ctx, &resp.Diagnostics, errors.New("empty output"), smerr.ID, plan.Name.String()) + smerr.AddError(ctx, &resp.Diagnostics, errors.New("empty output"), smerr.ID, plan.ExperimentTemplateId.String()) return } - // TIP: -- 5. Using the output from the create function, set attributes smerr.EnrichAppend(ctx, &resp.Diagnostics, flex.Flatten(ctx, out, &plan)) if resp.Diagnostics.HasError() { return } - - // TIP: -- 6. Use a waiter to wait for create to complete - createTimeout := r.CreateTimeout(ctx, plan.Timeouts) - _, err = waitTargetAccountConfigurationCreated(ctx, conn, plan.ID.ValueString(), createTimeout) - if err != nil { - smerr.AddError(ctx, &resp.Diagnostics, err, smerr.ID, plan.Name.String()) - return - } - - // TIP: -- 7. Save the request plan to response state smerr.EnrichAppend(ctx, &resp.Diagnostics, resp.State.Set(ctx, plan)) } func (r *resourceTargetAccountConfiguration) 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().FISClient(ctx) - // TIP: -- 2. Fetch the state var state resourceTargetAccountConfigurationModel smerr.EnrichAppend(ctx, &resp.Diagnostics, 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 := findTargetAccountConfigurationByID(ctx, conn, state.ID.ValueString()) - // TIP: -- 4. Remove resource from state if it is not found + out, err := findTargetAccountConfigurationByID(ctx, conn, state.AccountId.ValueStringPointer(), state.ExperimentTemplateId.ValueStringPointer()) if tfresource.NotFound(err) { resp.Diagnostics.Append(fwdiag.NewResourceNotFoundWarningDiagnostic(err)) resp.State.RemoveResource(ctx) return } if err != nil { - smerr.AddError(ctx, &resp.Diagnostics, err, smerr.ID, state.ID.String()) + smerr.AddError(ctx, &resp.Diagnostics, err, smerr.ID, state.ExperimentTemplateId.String()) return } - // TIP: -- 5. Set the arguments and attributes smerr.EnrichAppend(ctx, &resp.Diagnostics, flex.Flatten(ctx, out, &state)) if resp.Diagnostics.HasError() { return } - // TIP: -- 6. Set the state smerr.EnrichAppend(ctx, &resp.Diagnostics, resp.State.Set(ctx, &state)) } func (r *resourceTargetAccountConfiguration) 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().FISClient(ctx) - // TIP: -- 2. Fetch the plan var plan, state resourceTargetAccountConfigurationModel smerr.EnrichAppend(ctx, &resp.Diagnostics, req.Plan.Get(ctx, &plan)) smerr.EnrichAppend(ctx, &resp.Diagnostics, req.State.Get(ctx, &state)) @@ -237,7 +165,6 @@ func (r *resourceTargetAccountConfiguration) Update(ctx context.Context, req res return } - // TIP: -- 3. Get the difference between the plan and state, if any diff, d := flex.Diff(ctx, plan, state) smerr.EnrichAppend(ctx, &resp.Diagnostics, d) if resp.Diagnostics.HasError() { @@ -246,217 +173,70 @@ func (r *resourceTargetAccountConfiguration) Update(ctx context.Context, req res if diff.HasChanges() { var input fis.UpdateTargetAccountConfigurationInput - smerr.EnrichAppend(ctx, &resp.Diagnostics, flex.Expand(ctx, plan, &input, flex.WithFieldNamePrefix("Test"))) + smerr.EnrichAppend(ctx, &resp.Diagnostics, flex.Expand(ctx, plan, &input)) if resp.Diagnostics.HasError() { return } - // TIP: -- 4. Call the AWS modify/update function out, err := conn.UpdateTargetAccountConfiguration(ctx, &input) if err != nil { - smerr.AddError(ctx, &resp.Diagnostics, err, smerr.ID, plan.ID.String()) + smerr.AddError(ctx, &resp.Diagnostics, err, smerr.ID, plan.ExperimentTemplateId.String()) return } if out == nil || out.TargetAccountConfiguration == nil { - smerr.AddError(ctx, &resp.Diagnostics, errors.New("empty output"), smerr.ID, plan.ID.String()) + smerr.AddError(ctx, &resp.Diagnostics, errors.New("empty output"), smerr.ID, plan.ExperimentTemplateId.String()) return } - // TIP: Using the output from the update function, re-set any computed attributes smerr.EnrichAppend(ctx, &resp.Diagnostics, flex.Flatten(ctx, out, &plan)) if resp.Diagnostics.HasError() { return } } - // TIP: -- 5. Use a waiter to wait for update to complete - updateTimeout := r.UpdateTimeout(ctx, plan.Timeouts) - _, err := waitTargetAccountConfigurationUpdated(ctx, conn, plan.ID.ValueString(), updateTimeout) - if err != nil { - smerr.AddError(ctx, &resp.Diagnostics, err, smerr.ID, plan.ID.String()) - return - } - - // TIP: -- 6. Save the request plan to response state smerr.EnrichAppend(ctx, &resp.Diagnostics, resp.State.Set(ctx, &plan)) } func (r *resourceTargetAccountConfiguration) 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().FISClient(ctx) - // TIP: -- 2. Fetch the state var state resourceTargetAccountConfigurationModel smerr.EnrichAppend(ctx, &resp.Diagnostics, req.State.Get(ctx, &state)) if resp.Diagnostics.HasError() { return } - - // TIP: -- 3. Populate a delete input structure input := fis.DeleteTargetAccountConfigurationInput{ - TargetAccountConfigurationId: state.ID.ValueStringPointer(), + AccountId: state.AccountId.ValueStringPointer(), + ExperimentTemplateId: state.ExperimentTemplateId.ValueStringPointer(), } - - // TIP: -- 4. Call the AWS delete function _, err := conn.DeleteTargetAccountConfiguration(ctx, &input) - // 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 { if errs.IsA[*awstypes.ResourceNotFoundException](err) { return } - smerr.AddError(ctx, &resp.Diagnostics, err, smerr.ID, state.ID.String()) - return - } - - // TIP: -- 5. Use a waiter to wait for delete to complete - deleteTimeout := r.DeleteTimeout(ctx, state.Timeouts) - _, err = waitTargetAccountConfigurationDeleted(ctx, conn, state.ID.ValueString(), deleteTimeout) - if err != nil { - smerr.AddError(ctx, &resp.Diagnostics, err, smerr.ID, state.ID.String()) + smerr.AddError(ctx, &resp.Diagnostics, err, smerr.ID, state.ExperimentTemplateId.String()) 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 *resourceTargetAccountConfiguration) ImportState(ctx context.Context, req resource.ImportStateRequest, resp *resource.ImportStateResponse) { - resource.ImportStatePassthroughID(ctx, path.Root(names.AttrID), 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., awstypes.StatusInProgress). -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 waitTargetAccountConfigurationCreated(ctx context.Context, conn *fis.Client, id string, timeout time.Duration) (*awstypes.TargetAccountConfiguration, error) { - stateConf := &retry.StateChangeConf{ - Pending: []string{}, - Target: []string{statusNormal}, - Refresh: statusTargetAccountConfiguration(ctx, conn, id), - Timeout: timeout, - NotFoundChecks: 20, - ContinuousTargetOccurence: 2, - } - - outputRaw, err := stateConf.WaitForStateContext(ctx) - if out, ok := outputRaw.(*fis.TargetAccountConfiguration); ok { - return out, smarterr.NewError(err) - } - - return nil, smarterr.NewError(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 waitTargetAccountConfigurationUpdated(ctx context.Context, conn *fis.Client, id string, timeout time.Duration) (*awstypes.TargetAccountConfiguration, error) { - stateConf := &retry.StateChangeConf{ - Pending: []string{statusChangePending}, - Target: []string{statusUpdated}, - Refresh: statusTargetAccountConfiguration(ctx, conn, id), - Timeout: timeout, - NotFoundChecks: 20, - ContinuousTargetOccurence: 2, - } - - outputRaw, err := stateConf.WaitForStateContext(ctx) - if out, ok := outputRaw.(*fis.TargetAccountConfiguration); ok { - return out, smarterr.NewError(err) - } - - return nil, smarterr.NewError(err) -} - -// TIP: A deleted waiter is almost like a backwards created waiter. There may -// be additional pending states, however. -func waitTargetAccountConfigurationDeleted(ctx context.Context, conn *fis.Client, id string, timeout time.Duration) (*awstypes.TargetAccountConfiguration, error) { - stateConf := &retry.StateChangeConf{ - Pending: []string{statusDeleting, statusNormal}, - Target: []string{}, - Refresh: statusTargetAccountConfiguration(ctx, conn, id), - Timeout: timeout, - } - - outputRaw, err := stateConf.WaitForStateContext(ctx) - if out, ok := outputRaw.(*fis.TargetAccountConfiguration); ok { - return out, smarterr.NewError(err) + idParts := strings.Split(req.ID, ",") + if len(idParts) != 2 || idParts[0] == "" || idParts[1] == "" { + smerr.AddError(ctx, &resp.Diagnostics, fmt.Errorf("Unexpected Import Identifier, expected import identifier with format: account_id,experiment_id. Got: %q", req.ID), smerr.ID, ctx) + return } - return nil, smarterr.NewError(err) + smerr.EnrichAppend(ctx, &resp.Diagnostics, + resp.State.SetAttribute(ctx, path.Root(names.AttrAccountID), idParts[0]), + resp.State.SetAttribute(ctx, path.Root("experiment_template_id"), idParts[1]), + ) } -// 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 statusTargetAccountConfiguration(ctx context.Context, conn *fis.Client, id string) retry.StateRefreshFunc { - return func() (any, string, error) { - out, err := findTargetAccountConfigurationByID(ctx, conn, id) - if tfresource.NotFound(err) { - return nil, "", nil - } - - if err != nil { - return nil, "", smarterr.NewError(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 findTargetAccountConfigurationByID(ctx context.Context, conn *fis.Client, id string) (*awstypes.TargetAccountConfiguration, error) { +func findTargetAccountConfigurationByID(ctx context.Context, conn *fis.Client, accountId, experimentId *string) (*awstypes.TargetAccountConfiguration, error) { input := fis.GetTargetAccountConfigurationInput{ - Id: aws.String(id), + AccountId: accountId, + ExperimentTemplateId: experimentId, } out, err := conn.GetTargetAccountConfiguration(ctx, &input) @@ -480,46 +260,47 @@ func findTargetAccountConfigurationByID(ctx context.Context, conn *fis.Client, i type resourceTargetAccountConfigurationModel struct { framework.WithRegionModel - AccountId types.String `tfsdk:"account_id"` - ExperimentId types.String `tfsdk:"experiment_id"` - Description types.String `tfsdk:"description"` - RoleArn types.String `tfsdk:"role_arn"` - Timeouts timeouts.Value `tfsdk:"timeouts"` + AccountId types.String `tfsdk:"account_id"` + Description types.String `tfsdk:"description"` + ExperimentTemplateId types.String `tfsdk:"experiment_template_id"` + RoleArn types.String `tfsdk:"role_arn"` + Timeouts timeouts.Value `tfsdk:"timeouts"` } -// TIP: ==== SWEEPERS ==== -// When acceptance testing resources, interrupted or failed tests may -// leave behind orphaned resources in an account. To facilitate cleaning -// up lingering resources, each resource implementation should include -// a corresponding "sweeper" function. -// -// The sweeper function lists all resources of a given type and sets the -// appropriate identifers required to delete the resource via the Delete -// method implemented above. -// -// Once the sweeper function is implemented, register it in sweep.go -// as follows: -// -// awsv2.Register("aws_fis_target_account_configuration", sweepTargetAccountConfigurations) -// -// See more: -// https://hashicorp.github.io/terraform-provider-aws/running-and-writing-acceptance-tests/#acceptance-test-sweepers func sweepTargetAccountConfigurations(ctx context.Context, client *conns.AWSClient) ([]sweep.Sweepable, error) { - input := fis.ListTargetAccountConfigurationsInput{} conn := client.FISClient(ctx) var sweepResources []sweep.Sweepable - pages := fis.NewListTargetAccountConfigurationsPaginator(conn, &input) - for pages.HasMorePages() { - page, err := pages.NextPage(ctx) + experimentsInput := &fis.ListExperimentTemplatesInput{} + experimentPages := fis.NewListExperimentTemplatesPaginator(conn, experimentsInput) + + for experimentPages.HasMorePages() { + experimentPage, err := experimentPages.NextPage(ctx) if err != nil { return nil, smarterr.NewError(err) } - for _, v := range page.TargetAccountConfigurations { - sweepResources = append(sweepResources, sweepfw.NewSweepResource(newResourceTargetAccountConfiguration, client, - sweepfw.NewAttribute(names.AttrID, aws.ToString(v.TargetAccountConfigurationId))), - ) + for _, experiment := range experimentPage.ExperimentTemplates { + input := &fis.ListTargetAccountConfigurationsInput{ + ExperimentTemplateId: experiment.Id, + } + + pages := fis.NewListTargetAccountConfigurationsPaginator(conn, input) + for pages.HasMorePages() { + page, err := pages.NextPage(ctx) + if err != nil { + return nil, smarterr.NewError(err) + } + + for _, v := range page.TargetAccountConfigurations { + sweepResources = append(sweepResources, sweepfw.NewSweepResource( + newResourceTargetAccountConfiguration, + client, + sweepfw.NewAttribute(names.AttrAccountID, aws.ToString(v.AccountId)), + sweepfw.NewAttribute("experiment_template_id", aws.ToString(experiment.Id)), + )) + } + } } } From 9479cbf944315dd9aee5fbf3949e80539bb1e4cf Mon Sep 17 00:00:00 2001 From: Subham Mukhopadhyay Date: Thu, 30 Oct 2025 00:13:21 +0530 Subject: [PATCH 03/10] Add acceptance tests --- internal/service/fis/exports_test.go | 4 + .../fis/target_account_configuration_test.go | 396 ++++++++++++++++++ 2 files changed, 400 insertions(+) create mode 100644 internal/service/fis/target_account_configuration_test.go diff --git a/internal/service/fis/exports_test.go b/internal/service/fis/exports_test.go index d22424e77e12..e37a2ba30d29 100644 --- a/internal/service/fis/exports_test.go +++ b/internal/service/fis/exports_test.go @@ -8,4 +8,8 @@ var ( ResourceExperimentTemplate = resourceExperimentTemplate FindExperimentTemplateByID = findExperimentTemplateByID + + ResourceTargetAccountConfiguration = newResourceTargetAccountConfiguration + + FindTargetAccountConfigurationByID = findTargetAccountConfigurationByID ) diff --git a/internal/service/fis/target_account_configuration_test.go b/internal/service/fis/target_account_configuration_test.go new file mode 100644 index 000000000000..0db499f6395b --- /dev/null +++ b/internal/service/fis/target_account_configuration_test.go @@ -0,0 +1,396 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package fis_test + +import ( + "context" + "errors" + "fmt" + "testing" + + "github.com/YakDriver/regexache" + "github.com/aws/aws-sdk-go-v2/aws" + "github.com/aws/aws-sdk-go-v2/service/fis" + awstypes "github.com/aws/aws-sdk-go-v2/service/fis/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/tfresource" + "github.com/hashicorp/terraform-provider-aws/names" + + tffis "github.com/hashicorp/terraform-provider-aws/internal/service/fis" +) + +func TestAccFISTargetAccountConfiguration_basic(t *testing.T) { + ctx := acctest.Context(t) + + var targetaccountconfiguration awstypes.TargetAccountConfiguration + rName := sdkacctest.RandomWithPrefix(acctest.ResourcePrefix) + resourceName := "aws_fis_target_account_configuration.test" + + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { + acctest.PreCheck(ctx, t) + testAccPreCheck(ctx, t) + }, + ErrorCheck: acctest.ErrorCheck(t, names.FISServiceID), + ProtoV5ProviderFactories: acctest.ProtoV5ProviderFactories, + CheckDestroy: testAccCheckTargetAccountConfigurationDestroy(ctx), + Steps: []resource.TestStep{ + { + Config: testAccTargetAccountConfigurationConfig_basic(rName), + Check: resource.ComposeAggregateTestCheckFunc( + testAccCheckTargetAccountConfigurationExists(ctx, resourceName, &targetaccountconfiguration), + resource.TestCheckResourceAttrSet(resourceName, names.AttrAccountID), + resource.TestCheckResourceAttrSet(resourceName, "experiment_template_id"), + resource.TestCheckResourceAttrSet(resourceName, "role_arn"), + resource.TestCheckResourceAttr(resourceName, "description", fmt.Sprintf("%s target account configuration", rName)), + acctest.MatchResourceAttrGlobalARN(ctx, resourceName, "role_arn", "iam", regexache.MustCompile(`role/.+$`)), + ), + }, + { + ResourceName: resourceName, + ImportState: true, + ImportStateVerify: true, + ImportStateIdFunc: testAccTargetAccountConfigurationImportStateIdFunc(resourceName), + ImportStateVerifyIdentifierAttribute: names.AttrAccountID, + }, + }, + }) +} + +func TestAccFISTargetAccountConfiguration_update(t *testing.T) { + ctx := acctest.Context(t) + + var before, after awstypes.TargetAccountConfiguration + rName := sdkacctest.RandomWithPrefix(acctest.ResourcePrefix) + resourceName := "aws_fis_target_account_configuration.test" + + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { + acctest.PreCheck(ctx, t) + testAccPreCheck(ctx, t) + }, + ErrorCheck: acctest.ErrorCheck(t, names.FISServiceID), + ProtoV5ProviderFactories: acctest.ProtoV5ProviderFactories, + CheckDestroy: testAccCheckTargetAccountConfigurationDestroy(ctx), + Steps: []resource.TestStep{ + { + Config: testAccTargetAccountConfigurationConfig_basic(rName), + Check: resource.ComposeAggregateTestCheckFunc( + testAccCheckTargetAccountConfigurationExists(ctx, resourceName, &before), + resource.TestCheckResourceAttr(resourceName, "description", fmt.Sprintf("%s target account configuration", rName)), + ), + }, + { + Config: testAccTargetAccountConfigurationConfig_update(rName), + Check: resource.ComposeAggregateTestCheckFunc( + testAccCheckTargetAccountConfigurationExists(ctx, resourceName, &after), + testAccCheckTargetAccountConfigurationNotRecreated(&before, &after), + resource.TestCheckResourceAttr(resourceName, "description", fmt.Sprintf("%s target account configuration updated", rName)), + ), + }, + { + ResourceName: resourceName, + ImportState: true, + ImportStateVerify: true, + ImportStateIdFunc: testAccTargetAccountConfigurationImportStateIdFunc(resourceName), + ImportStateVerifyIdentifierAttribute: names.AttrAccountID, + }, + }, + }) +} + +func TestAccFISTargetAccountConfiguration_disappears(t *testing.T) { + ctx := acctest.Context(t) + + var targetaccountconfiguration awstypes.TargetAccountConfiguration + rName := sdkacctest.RandomWithPrefix(acctest.ResourcePrefix) + resourceName := "aws_fis_target_account_configuration.test" + + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { + acctest.PreCheck(ctx, t) + testAccPreCheck(ctx, t) + }, + ErrorCheck: acctest.ErrorCheck(t, names.FISServiceID), + ProtoV5ProviderFactories: acctest.ProtoV5ProviderFactories, + CheckDestroy: testAccCheckTargetAccountConfigurationDestroy(ctx), + Steps: []resource.TestStep{ + { + Config: testAccTargetAccountConfigurationConfig_basic(rName), + Check: resource.ComposeAggregateTestCheckFunc( + testAccCheckTargetAccountConfigurationExists(ctx, resourceName, &targetaccountconfiguration), + acctest.CheckFrameworkResourceDisappears(ctx, acctest.Provider, tffis.ResourceTargetAccountConfiguration, resourceName), + ), + ExpectNonEmptyPlan: true, + }, + }, + }) +} + +func testAccCheckTargetAccountConfigurationDestroy(ctx context.Context) resource.TestCheckFunc { + return func(s *terraform.State) error { + conn := acctest.Provider.Meta().(*conns.AWSClient).FISClient(ctx) + + for _, rs := range s.RootModule().Resources { + if rs.Type != "aws_fis_target_account_configuration" { + continue + } + + accountId := aws.String(rs.Primary.Attributes[names.AttrAccountID]) + experimentId := aws.String(rs.Primary.Attributes["experiment_template_id"]) + _, err := tffis.FindTargetAccountConfigurationByID(ctx, conn, accountId, experimentId) + if tfresource.NotFound(err) { + return nil + } + if err != nil { + return create.Error(names.FIS, create.ErrActionCheckingDestroyed, tffis.ResNameTargetAccountConfiguration, *experimentId, err) + } + + return create.Error(names.FIS, create.ErrActionCheckingDestroyed, tffis.ResNameTargetAccountConfiguration, *experimentId, errors.New("not destroyed")) + } + + return nil + } +} + +func testAccCheckTargetAccountConfigurationExists(ctx context.Context, name string, targetaccountconfiguration *awstypes.TargetAccountConfiguration) resource.TestCheckFunc { + return func(s *terraform.State) error { + rs, ok := s.RootModule().Resources[name] + if !ok { + return create.Error(names.FIS, create.ErrActionCheckingExistence, tffis.ResNameTargetAccountConfiguration, name, errors.New("not found")) + } + + if rs.Primary.ID == "" { + return create.Error(names.FIS, create.ErrActionCheckingExistence, tffis.ResNameTargetAccountConfiguration, name, errors.New("not set")) + } + + conn := acctest.Provider.Meta().(*conns.AWSClient).FISClient(ctx) + + accountId := aws.String(rs.Primary.Attributes[names.AttrAccountID]) + experimentTemplateId := aws.String(rs.Primary.Attributes["experiment_template_id"]) + + resp, err := tffis.FindTargetAccountConfigurationByID(ctx, conn, accountId, experimentTemplateId) + if err != nil { + return create.Error(names.FIS, create.ErrActionCheckingExistence, tffis.ResNameTargetAccountConfiguration, rs.Primary.ID, err) + } + + *targetaccountconfiguration = *resp + + return nil + } +} + +func testAccPreCheck(ctx context.Context, t *testing.T) { + conn := acctest.Provider.Meta().(*conns.AWSClient).FISClient(ctx) + + input := &fis.ListExperimentTemplatesInput{} + + _, err := conn.ListExperimentTemplates(ctx, input) + + if acctest.PreCheckSkipError(err) { + t.Skipf("skipping acceptance testing: %s", err) + } + if err != nil { + t.Fatalf("unexpected PreCheck error: %s", err) + } +} + +func testAccCheckTargetAccountConfigurationNotRecreated(before, after *awstypes.TargetAccountConfiguration) resource.TestCheckFunc { + return func(s *terraform.State) error { + if before, after := aws.ToString(before.AccountId), aws.ToString(after.AccountId); before != after { + return create.Error(names.FIS, create.ErrActionCheckingNotRecreated, tffis.ResNameTargetAccountConfiguration, before, errors.New("recreated")) + } + + return nil + } +} + +func testAccTargetAccountConfigurationImportStateIdFunc(resourceName string) resource.ImportStateIdFunc { + return func(s *terraform.State) (string, error) { + rs, ok := s.RootModule().Resources[resourceName] + if !ok { + return "", fmt.Errorf("resource %s not found", resourceName) + } + + experimentTemplateId := rs.Primary.Attributes["experiment_template_id"] + accountId := rs.Primary.Attributes[names.AttrAccountID] + + if experimentTemplateId == "" || accountId == "" { + return "", fmt.Errorf("experiment_template_id or account_id not set") + } + + return fmt.Sprintf("%s,%s", accountId, experimentTemplateId), nil + } +} + +func testAccTargetAccountConfigurationConfig_basic(rName string) string { + return fmt.Sprintf(` +data "aws_caller_identity" "current" {} +data "aws_partition" "current" {} + +resource "aws_iam_role" "test" { + name = "%[1]s-role" + assume_role_policy = jsonencode({ + Version = "2012-10-17" + Statement = [{ + Action = "sts:AssumeRole" + Effect = "Allow" + Principal = { + Service = "fis.${data.aws_partition.current.dns_suffix}" + } + }] + }) +} + +resource "aws_iam_role_policy_attachment" "test" { + role = aws_iam_role.test.name + policy_arn = "arn:aws:iam::aws:policy/service-role/AWSFaultInjectionSimulatorEC2Access" +} + +resource "aws_fis_experiment_template" "test" { + description = "%[1]s experiment template" + role_arn = aws_iam_role.test.arn + + experiment_options { + account_targeting = "multi-account" + empty_target_resolution_mode = "fail" + } + + action { + name = "stop-instances" + action_id = "aws:ec2:stop-instances" + + parameter { + key = "startInstancesAfterDuration" + value = "PT10M" + } + + target { + key = "Instances" + value = "test-instances" + } + } + + target { + name = "test-instances" + resource_type = "aws:ec2:instance" + selection_mode = "PERCENT(50)" + + resource_tag { + key = "Environment" + value = "test" + } + } + + stop_condition { + source = "none" + } + + tags = { + Name = "%[1]s-experiment-template" + } +} + +resource "aws_fis_target_account_configuration" "test" { + experiment_template_id = aws_fis_experiment_template.test.id + account_id = data.aws_caller_identity.current.account_id + role_arn = aws_iam_role.test.arn + description = "%[1]s target account configuration" + + timeouts { + create = "5m" + update = "5m" + delete = "5m" + } +} +`, rName) +} + +func testAccTargetAccountConfigurationConfig_update(rName string) string { + return fmt.Sprintf(` +data "aws_caller_identity" "current" {} +data "aws_partition" "current" {} + +resource "aws_iam_role" "test" { + name = "%[1]s-role" + assume_role_policy = jsonencode({ + Version = "2012-10-17" + Statement = [{ + Action = "sts:AssumeRole" + Effect = "Allow" + Principal = { + Service = "fis.${data.aws_partition.current.dns_suffix}" + } + }] + }) +} + +resource "aws_iam_role_policy_attachment" "test" { + role = aws_iam_role.test.name + policy_arn = "arn:aws:iam::aws:policy/service-role/AWSFaultInjectionSimulatorEC2Access" +} + +resource "aws_fis_experiment_template" "test" { + description = "%[1]s experiment template" + role_arn = aws_iam_role.test.arn + + experiment_options { + account_targeting = "multi-account" + empty_target_resolution_mode = "fail" + } + + action { + name = "stop-instances" + action_id = "aws:ec2:stop-instances" + + parameter { + key = "startInstancesAfterDuration" + value = "PT10M" + } + + target { + key = "Instances" + value = "test-instances" + } + } + + target { + name = "test-instances" + resource_type = "aws:ec2:instance" + selection_mode = "PERCENT(50)" + + resource_tag { + key = "Environment" + value = "test" + } + } + + stop_condition { + source = "none" + } + + tags = { + Name = "%[1]s-experiment-template" + } +} + +resource "aws_fis_target_account_configuration" "test" { + experiment_template_id = aws_fis_experiment_template.test.id + account_id = data.aws_caller_identity.current.account_id + role_arn = aws_iam_role.test.arn + description = "%[1]s target account configuration updated" + + timeouts { + create = "5m" + update = "5m" + delete = "5m" + } +} +`, rName) +} From 13463e45f1aff48c75860f7ca62bd731ea3b8ffb Mon Sep 17 00:00:00 2001 From: Subham Mukhopadhyay Date: Thu, 30 Oct 2025 04:09:13 +0530 Subject: [PATCH 04/10] add documentation, fix lint --- internal/service/fis/target_account_configuration.go | 7 +++---- .../service/fis/target_account_configuration_test.go | 12 ++++++------ 2 files changed, 9 insertions(+), 10 deletions(-) diff --git a/internal/service/fis/target_account_configuration.go b/internal/service/fis/target_account_configuration.go index 4a6ace011d11..742924388a22 100644 --- a/internal/service/fis/target_account_configuration.go +++ b/internal/service/fis/target_account_configuration.go @@ -36,14 +36,13 @@ import ( "github.com/hashicorp/terraform-provider-aws/names" ) -// Function annotations are used for resource registration to the Provider. DO NOT EDIT. // @FrameworkResource("aws_fis_target_account_configuration", name="Target Account Configuration") func newResourceTargetAccountConfiguration(_ context.Context) (resource.ResourceWithConfigure, error) { r := &resourceTargetAccountConfiguration{} - r.SetDefaultCreateTimeout(30 * time.Minute) - r.SetDefaultUpdateTimeout(30 * time.Minute) - r.SetDefaultDeleteTimeout(30 * time.Minute) + r.SetDefaultCreateTimeout(5 * time.Minute) + r.SetDefaultUpdateTimeout(5 * time.Minute) + r.SetDefaultDeleteTimeout(5 * time.Minute) return r, nil } diff --git a/internal/service/fis/target_account_configuration_test.go b/internal/service/fis/target_account_configuration_test.go index 0db499f6395b..23b7e2566362 100644 --- a/internal/service/fis/target_account_configuration_test.go +++ b/internal/service/fis/target_account_configuration_test.go @@ -299,9 +299,9 @@ resource "aws_fis_experiment_template" "test" { resource "aws_fis_target_account_configuration" "test" { experiment_template_id = aws_fis_experiment_template.test.id - account_id = data.aws_caller_identity.current.account_id - role_arn = aws_iam_role.test.arn - description = "%[1]s target account configuration" + account_id = data.aws_caller_identity.current.account_id + role_arn = aws_iam_role.test.arn + description = "%[1]s target account configuration" timeouts { create = "5m" @@ -382,9 +382,9 @@ resource "aws_fis_experiment_template" "test" { resource "aws_fis_target_account_configuration" "test" { experiment_template_id = aws_fis_experiment_template.test.id - account_id = data.aws_caller_identity.current.account_id - role_arn = aws_iam_role.test.arn - description = "%[1]s target account configuration updated" + account_id = data.aws_caller_identity.current.account_id + role_arn = aws_iam_role.test.arn + description = "%[1]s target account configuration updated" timeouts { create = "5m" From d21beac43ad6fa1d4634b788f1aa7115aacb70a4 Mon Sep 17 00:00:00 2001 From: Subham Mukhopadhyay Date: Thu, 30 Oct 2025 04:19:37 +0530 Subject: [PATCH 05/10] add documentation --- ...target_account_configuration.html.markdown | 65 +++++++++++++++++++ 1 file changed, 65 insertions(+) create mode 100644 website/docs/r/fis_target_account_configuration.html.markdown diff --git a/website/docs/r/fis_target_account_configuration.html.markdown b/website/docs/r/fis_target_account_configuration.html.markdown new file mode 100644 index 000000000000..d38796177b90 --- /dev/null +++ b/website/docs/r/fis_target_account_configuration.html.markdown @@ -0,0 +1,65 @@ +--- +subcategory: "FIS (Fault Injection Simulator)" +layout: "aws" +page_title: "AWS: aws_fis_target_account_configuration" +description: |- + Manages an AWS FIS (Fault Injection Simulator) Target Account Configuration. +--- + +# Resource: aws_fis_target_account_configuration + +Manages an AWS FIS (Fault Injection Simulator) Target Account Configuration. + +## Example Usage + +### Basic Usage + +```terraform +resource "aws_fis_target_account_configuration" "example" { + experiment_template_id = aws_fis_experiment_template.example.id + account_id = data.aws_caller_identity.current.account_id + role_arn = aws_iam_role.fis_role.arn + description = "Example" +} +``` + +## Argument Reference + +The following arguments are required: + +* `account_id` - (Required) Account ID of the target account. +* `experiment_template_id` - (Required) Experiment Template ID. + +The following arguments are optional: + +* `description` - (Optional) Description of the target account. +* `role_arn` - (Optional) ARN of the IAM Role for the target account. + +## Attribute Reference + +This resource exports no additional attributes. + +## Timeouts + +[Configuration options](https://developer.hashicorp.com/terraform/language/resources/syntax#operation-timeouts): + +* `create` - (Default `5m`) +* `update` - (Default `5m`) +* `delete` - (Default `5m`) + +## Import + +In Terraform v1.5.0 and later, use an [`import` block](https://developer.hashicorp.com/terraform/language/import) to import FIS (Fault Injection Simulator) Target Account Configuration using the `account_id,experiment_template_id`. For example: + +```terraform +import { + to = aws_fis_target_account_configuration.example + id = "abcd123456789,123456789012" +} +``` + +Using `terraform import`, import FIS (Fault Injection Simulator) Target Account Configuration using the `account_id,experiment_template_id`. For example: + +```console +% terraform import aws_fis_target_account_configuration.example abcd123456789,123456789012 +``` From bd50933b97ed08b0eaa7c5281c7d1b79742fab5f Mon Sep 17 00:00:00 2001 From: Subham Mukhopadhyay Date: Thu, 30 Oct 2025 12:14:41 +0530 Subject: [PATCH 06/10] add changelog file, replace hardcoded string with constant --- .changelog/44875.txt | 3 +++ internal/service/fis/target_account_configuration.go | 2 +- 2 files changed, 4 insertions(+), 1 deletion(-) create mode 100644 .changelog/44875.txt diff --git a/.changelog/44875.txt b/.changelog/44875.txt new file mode 100644 index 000000000000..a18e014146e1 --- /dev/null +++ b/.changelog/44875.txt @@ -0,0 +1,3 @@ +```release-note:new-resource +aws_fis_target_account_configuration +``` \ No newline at end of file diff --git a/internal/service/fis/target_account_configuration.go b/internal/service/fis/target_account_configuration.go index 742924388a22..e9efd9820172 100644 --- a/internal/service/fis/target_account_configuration.go +++ b/internal/service/fis/target_account_configuration.go @@ -59,7 +59,7 @@ type resourceTargetAccountConfiguration struct { func (r *resourceTargetAccountConfiguration) Schema(ctx context.Context, req resource.SchemaRequest, resp *resource.SchemaResponse) { resp.Schema = schema.Schema{ Attributes: map[string]schema.Attribute{ - "account_id": schema.StringAttribute{ + names.AttrAccountID: schema.StringAttribute{ Required: true, Validators: []validator.String{stringvalidator.LengthBetween(12, 48)}, PlanModifiers: []planmodifier.String{ From e05fbdf00910f4577611353df585a43b2cfee471 Mon Sep 17 00:00:00 2001 From: Subham Mukhopadhyay Date: Thu, 30 Oct 2025 13:34:01 +0530 Subject: [PATCH 07/10] fix lint --- .../service/fis/target_account_configuration.go | 2 +- .../fis/target_account_configuration_test.go | 17 ++++++++--------- ...s_target_account_configuration.html.markdown | 1 + 3 files changed, 10 insertions(+), 10 deletions(-) diff --git a/internal/service/fis/target_account_configuration.go b/internal/service/fis/target_account_configuration.go index e9efd9820172..ebcaa9178e67 100644 --- a/internal/service/fis/target_account_configuration.go +++ b/internal/service/fis/target_account_configuration.go @@ -78,7 +78,7 @@ func (r *resourceTargetAccountConfiguration) Schema(ctx context.Context, req res stringplanmodifier.RequiresReplace(), }, }, - "role_arn": schema.StringAttribute{ + names.AttrRoleARN: schema.StringAttribute{ Optional: true, Computed: true, Validators: []validator.String{stringvalidator.LengthBetween(20, 2048)}, diff --git a/internal/service/fis/target_account_configuration_test.go b/internal/service/fis/target_account_configuration_test.go index 23b7e2566362..ebd3f6158f05 100644 --- a/internal/service/fis/target_account_configuration_test.go +++ b/internal/service/fis/target_account_configuration_test.go @@ -19,10 +19,9 @@ import ( "github.com/hashicorp/terraform-provider-aws/internal/acctest" "github.com/hashicorp/terraform-provider-aws/internal/conns" "github.com/hashicorp/terraform-provider-aws/internal/create" + tffis "github.com/hashicorp/terraform-provider-aws/internal/service/fis" "github.com/hashicorp/terraform-provider-aws/internal/tfresource" "github.com/hashicorp/terraform-provider-aws/names" - - tffis "github.com/hashicorp/terraform-provider-aws/internal/service/fis" ) func TestAccFISTargetAccountConfiguration_basic(t *testing.T) { @@ -47,9 +46,9 @@ func TestAccFISTargetAccountConfiguration_basic(t *testing.T) { testAccCheckTargetAccountConfigurationExists(ctx, resourceName, &targetaccountconfiguration), resource.TestCheckResourceAttrSet(resourceName, names.AttrAccountID), resource.TestCheckResourceAttrSet(resourceName, "experiment_template_id"), - resource.TestCheckResourceAttrSet(resourceName, "role_arn"), - resource.TestCheckResourceAttr(resourceName, "description", fmt.Sprintf("%s target account configuration", rName)), - acctest.MatchResourceAttrGlobalARN(ctx, resourceName, "role_arn", "iam", regexache.MustCompile(`role/.+$`)), + resource.TestCheckResourceAttrSet(resourceName, names.AttrRoleARN), + resource.TestCheckResourceAttr(resourceName, names.AttrDescription, fmt.Sprintf("%s target account configuration", rName)), + acctest.MatchResourceAttrGlobalARN(ctx, resourceName, names.AttrRoleARN, "iam", regexache.MustCompile(`role/.+$`)), ), }, { @@ -83,7 +82,7 @@ func TestAccFISTargetAccountConfiguration_update(t *testing.T) { Config: testAccTargetAccountConfigurationConfig_basic(rName), Check: resource.ComposeAggregateTestCheckFunc( testAccCheckTargetAccountConfigurationExists(ctx, resourceName, &before), - resource.TestCheckResourceAttr(resourceName, "description", fmt.Sprintf("%s target account configuration", rName)), + resource.TestCheckResourceAttr(resourceName, names.AttrDescription, fmt.Sprintf("%s target account configuration", rName)), ), }, { @@ -91,7 +90,7 @@ func TestAccFISTargetAccountConfiguration_update(t *testing.T) { Check: resource.ComposeAggregateTestCheckFunc( testAccCheckTargetAccountConfigurationExists(ctx, resourceName, &after), testAccCheckTargetAccountConfigurationNotRecreated(&before, &after), - resource.TestCheckResourceAttr(resourceName, "description", fmt.Sprintf("%s target account configuration updated", rName)), + resource.TestCheckResourceAttr(resourceName, names.AttrDescription, fmt.Sprintf("%s target account configuration updated", rName)), ), }, { @@ -250,7 +249,7 @@ resource "aws_iam_role" "test" { resource "aws_iam_role_policy_attachment" "test" { role = aws_iam_role.test.name - policy_arn = "arn:aws:iam::aws:policy/service-role/AWSFaultInjectionSimulatorEC2Access" + policy_arn = "arn:${data.aws_partition.current.partition}:iam::aws:policy/service-role/AWSFaultInjectionSimulatorEC2Access" } resource "aws_fis_experiment_template" "test" { @@ -333,7 +332,7 @@ resource "aws_iam_role" "test" { resource "aws_iam_role_policy_attachment" "test" { role = aws_iam_role.test.name - policy_arn = "arn:aws:iam::aws:policy/service-role/AWSFaultInjectionSimulatorEC2Access" + policy_arn = "arn:${data.aws_partition.current.partition}:iam::aws:policy/service-role/AWSFaultInjectionSimulatorEC2Access" } resource "aws_fis_experiment_template" "test" { diff --git a/website/docs/r/fis_target_account_configuration.html.markdown b/website/docs/r/fis_target_account_configuration.html.markdown index d38796177b90..ea05f1e7fd3f 100644 --- a/website/docs/r/fis_target_account_configuration.html.markdown +++ b/website/docs/r/fis_target_account_configuration.html.markdown @@ -34,6 +34,7 @@ The following arguments are optional: * `description` - (Optional) Description of the target account. * `role_arn` - (Optional) ARN of the IAM Role for the target account. +* `region` - (Optional) Region where this resource will be [managed](https://docs.aws.amazon.com/general/latest/gr/rande.html#regional-endpoints). Defaults to the Region set in the [provider configuration](https://registry.terraform.io/providers/hashicorp/aws/latest/docs#aws-configuration-reference). ## Attribute Reference From 666e456be9ea705e449344a743770b0ea64c1f74 Mon Sep 17 00:00:00 2001 From: Subham Mukhopadhyay Date: Fri, 31 Oct 2025 00:41:38 +0530 Subject: [PATCH 08/10] minor fixes --- internal/service/fis/sweep.go | 43 ++++++++++++++++++ .../fis/target_account_configuration.go | 44 ------------------- .../fis/target_account_configuration_test.go | 34 +------------- ...target_account_configuration.html.markdown | 4 +- 4 files changed, 47 insertions(+), 78 deletions(-) diff --git a/internal/service/fis/sweep.go b/internal/service/fis/sweep.go index 04d20e5d62a7..2ce192ff039a 100644 --- a/internal/service/fis/sweep.go +++ b/internal/service/fis/sweep.go @@ -6,11 +6,14 @@ package fis import ( "context" + "github.com/YakDriver/smarterr" "github.com/aws/aws-sdk-go-v2/aws" "github.com/aws/aws-sdk-go-v2/service/fis" "github.com/hashicorp/terraform-provider-aws/internal/conns" "github.com/hashicorp/terraform-provider-aws/internal/sweep" "github.com/hashicorp/terraform-provider-aws/internal/sweep/awsv2" + sweepfw "github.com/hashicorp/terraform-provider-aws/internal/sweep/framework" + "github.com/hashicorp/terraform-provider-aws/names" ) func RegisterSweepers() { @@ -42,3 +45,43 @@ func sweepExperimentTemplates(ctx context.Context, client *conns.AWSClient) ([]s return sweepResources, nil } + +func sweepTargetAccountConfigurations(ctx context.Context, client *conns.AWSClient) ([]sweep.Sweepable, error) { + conn := client.FISClient(ctx) + var sweepResources []sweep.Sweepable + + experimentsInput := &fis.ListExperimentTemplatesInput{} + experimentPages := fis.NewListExperimentTemplatesPaginator(conn, experimentsInput) + + for experimentPages.HasMorePages() { + experimentPage, err := experimentPages.NextPage(ctx) + if err != nil { + return nil, smarterr.NewError(err) + } + + for _, experiment := range experimentPage.ExperimentTemplates { + input := &fis.ListTargetAccountConfigurationsInput{ + ExperimentTemplateId: experiment.Id, + } + + pages := fis.NewListTargetAccountConfigurationsPaginator(conn, input) + for pages.HasMorePages() { + page, err := pages.NextPage(ctx) + if err != nil { + return nil, smarterr.NewError(err) + } + + for _, v := range page.TargetAccountConfigurations { + sweepResources = append(sweepResources, sweepfw.NewSweepResource( + newResourceTargetAccountConfiguration, + client, + sweepfw.NewAttribute(names.AttrAccountID, aws.ToString(v.AccountId)), + sweepfw.NewAttribute("experiment_template_id", aws.ToString(experiment.Id)), + )) + } + } + } + } + + return sweepResources, nil +} diff --git a/internal/service/fis/target_account_configuration.go b/internal/service/fis/target_account_configuration.go index ebcaa9178e67..232eb776af20 100644 --- a/internal/service/fis/target_account_configuration.go +++ b/internal/service/fis/target_account_configuration.go @@ -11,7 +11,6 @@ import ( "time" "github.com/YakDriver/smarterr" - "github.com/aws/aws-sdk-go-v2/aws" "github.com/aws/aws-sdk-go-v2/service/fis" awstypes "github.com/aws/aws-sdk-go-v2/service/fis/types" "github.com/hashicorp/terraform-plugin-framework-timeouts/resource/timeouts" @@ -24,14 +23,11 @@ import ( "github.com/hashicorp/terraform-plugin-framework/schema/validator" "github.com/hashicorp/terraform-plugin-framework/types" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/retry" - "github.com/hashicorp/terraform-provider-aws/internal/conns" "github.com/hashicorp/terraform-provider-aws/internal/errs" "github.com/hashicorp/terraform-provider-aws/internal/errs/fwdiag" "github.com/hashicorp/terraform-provider-aws/internal/framework" "github.com/hashicorp/terraform-provider-aws/internal/framework/flex" "github.com/hashicorp/terraform-provider-aws/internal/smerr" - "github.com/hashicorp/terraform-provider-aws/internal/sweep" - sweepfw "github.com/hashicorp/terraform-provider-aws/internal/sweep/framework" "github.com/hashicorp/terraform-provider-aws/internal/tfresource" "github.com/hashicorp/terraform-provider-aws/names" ) @@ -265,43 +261,3 @@ type resourceTargetAccountConfigurationModel struct { RoleArn types.String `tfsdk:"role_arn"` Timeouts timeouts.Value `tfsdk:"timeouts"` } - -func sweepTargetAccountConfigurations(ctx context.Context, client *conns.AWSClient) ([]sweep.Sweepable, error) { - conn := client.FISClient(ctx) - var sweepResources []sweep.Sweepable - - experimentsInput := &fis.ListExperimentTemplatesInput{} - experimentPages := fis.NewListExperimentTemplatesPaginator(conn, experimentsInput) - - for experimentPages.HasMorePages() { - experimentPage, err := experimentPages.NextPage(ctx) - if err != nil { - return nil, smarterr.NewError(err) - } - - for _, experiment := range experimentPage.ExperimentTemplates { - input := &fis.ListTargetAccountConfigurationsInput{ - ExperimentTemplateId: experiment.Id, - } - - pages := fis.NewListTargetAccountConfigurationsPaginator(conn, input) - for pages.HasMorePages() { - page, err := pages.NextPage(ctx) - if err != nil { - return nil, smarterr.NewError(err) - } - - for _, v := range page.TargetAccountConfigurations { - sweepResources = append(sweepResources, sweepfw.NewSweepResource( - newResourceTargetAccountConfiguration, - client, - sweepfw.NewAttribute(names.AttrAccountID, aws.ToString(v.AccountId)), - sweepfw.NewAttribute("experiment_template_id", aws.ToString(experiment.Id)), - )) - } - } - } - } - - return sweepResources, nil -} diff --git a/internal/service/fis/target_account_configuration_test.go b/internal/service/fis/target_account_configuration_test.go index ebd3f6158f05..cc4a670d21d1 100644 --- a/internal/service/fis/target_account_configuration_test.go +++ b/internal/service/fis/target_account_configuration_test.go @@ -55,7 +55,7 @@ func TestAccFISTargetAccountConfiguration_basic(t *testing.T) { ResourceName: resourceName, ImportState: true, ImportStateVerify: true, - ImportStateIdFunc: testAccTargetAccountConfigurationImportStateIdFunc(resourceName), + ImportStateIdFunc: acctest.AttrsImportStateIdFunc(resourceName, ",", names.AttrAccountID, "experiment_template_id"), ImportStateVerifyIdentifierAttribute: names.AttrAccountID, }, }, @@ -97,7 +97,7 @@ func TestAccFISTargetAccountConfiguration_update(t *testing.T) { ResourceName: resourceName, ImportState: true, ImportStateVerify: true, - ImportStateIdFunc: testAccTargetAccountConfigurationImportStateIdFunc(resourceName), + ImportStateIdFunc: acctest.AttrsImportStateIdFunc(resourceName, ",", names.AttrAccountID, "experiment_template_id"), ImportStateVerifyIdentifierAttribute: names.AttrAccountID, }, }, @@ -210,24 +210,6 @@ func testAccCheckTargetAccountConfigurationNotRecreated(before, after *awstypes. } } -func testAccTargetAccountConfigurationImportStateIdFunc(resourceName string) resource.ImportStateIdFunc { - return func(s *terraform.State) (string, error) { - rs, ok := s.RootModule().Resources[resourceName] - if !ok { - return "", fmt.Errorf("resource %s not found", resourceName) - } - - experimentTemplateId := rs.Primary.Attributes["experiment_template_id"] - accountId := rs.Primary.Attributes[names.AttrAccountID] - - if experimentTemplateId == "" || accountId == "" { - return "", fmt.Errorf("experiment_template_id or account_id not set") - } - - return fmt.Sprintf("%s,%s", accountId, experimentTemplateId), nil - } -} - func testAccTargetAccountConfigurationConfig_basic(rName string) string { return fmt.Sprintf(` data "aws_caller_identity" "current" {} @@ -301,12 +283,6 @@ resource "aws_fis_target_account_configuration" "test" { account_id = data.aws_caller_identity.current.account_id role_arn = aws_iam_role.test.arn description = "%[1]s target account configuration" - - timeouts { - create = "5m" - update = "5m" - delete = "5m" - } } `, rName) } @@ -384,12 +360,6 @@ resource "aws_fis_target_account_configuration" "test" { account_id = data.aws_caller_identity.current.account_id role_arn = aws_iam_role.test.arn description = "%[1]s target account configuration updated" - - timeouts { - create = "5m" - update = "5m" - delete = "5m" - } } `, rName) } diff --git a/website/docs/r/fis_target_account_configuration.html.markdown b/website/docs/r/fis_target_account_configuration.html.markdown index ea05f1e7fd3f..c5192d52ddf5 100644 --- a/website/docs/r/fis_target_account_configuration.html.markdown +++ b/website/docs/r/fis_target_account_configuration.html.markdown @@ -55,12 +55,12 @@ In Terraform v1.5.0 and later, use an [`import` block](https://developer.hashico ```terraform import { to = aws_fis_target_account_configuration.example - id = "abcd123456789,123456789012" + id = "123456789012,abcd123456789" } ``` Using `terraform import`, import FIS (Fault Injection Simulator) Target Account Configuration using the `account_id,experiment_template_id`. For example: ```console -% terraform import aws_fis_target_account_configuration.example abcd123456789,123456789012 +% terraform import aws_fis_target_account_configuration.example 123456789012,abcd123456789 ``` From bb88abe067029739cca32f51037c197986f82ee1 Mon Sep 17 00:00:00 2001 From: Subham Mukhopadhyay Date: Fri, 31 Oct 2025 00:48:25 +0530 Subject: [PATCH 09/10] refactor import function --- .../service/fis/target_account_configuration.go | 16 +++++++--------- 1 file changed, 7 insertions(+), 9 deletions(-) diff --git a/internal/service/fis/target_account_configuration.go b/internal/service/fis/target_account_configuration.go index 232eb776af20..208aabea69d7 100644 --- a/internal/service/fis/target_account_configuration.go +++ b/internal/service/fis/target_account_configuration.go @@ -7,7 +7,6 @@ import ( "context" "errors" "fmt" - "strings" "time" "github.com/YakDriver/smarterr" @@ -25,6 +24,7 @@ import ( "github.com/hashicorp/terraform-plugin-sdk/v2/helper/retry" "github.com/hashicorp/terraform-provider-aws/internal/errs" "github.com/hashicorp/terraform-provider-aws/internal/errs/fwdiag" + intflex "github.com/hashicorp/terraform-provider-aws/internal/flex" "github.com/hashicorp/terraform-provider-aws/internal/framework" "github.com/hashicorp/terraform-provider-aws/internal/framework/flex" "github.com/hashicorp/terraform-provider-aws/internal/smerr" @@ -216,16 +216,14 @@ func (r *resourceTargetAccountConfiguration) Delete(ctx context.Context, req res } func (r *resourceTargetAccountConfiguration) ImportState(ctx context.Context, req resource.ImportStateRequest, resp *resource.ImportStateResponse) { - idParts := strings.Split(req.ID, ",") - if len(idParts) != 2 || idParts[0] == "" || idParts[1] == "" { - smerr.AddError(ctx, &resp.Diagnostics, fmt.Errorf("Unexpected Import Identifier, expected import identifier with format: account_id,experiment_id. Got: %q", req.ID), smerr.ID, ctx) + const idParts = 2 + parts, err := intflex.ExpandResourceId(req.ID, idParts, false) + if err != nil { + resp.Diagnostics.AddError("Resource Import Invalid ID", fmt.Sprintf(`Unexpected format for import ID (%s), use: "account_id,experiment_template_id"`, req.ID)) return } - - smerr.EnrichAppend(ctx, &resp.Diagnostics, - resp.State.SetAttribute(ctx, path.Root(names.AttrAccountID), idParts[0]), - resp.State.SetAttribute(ctx, path.Root("experiment_template_id"), idParts[1]), - ) + smerr.EnrichAppend(ctx, &resp.Diagnostics, resp.State.SetAttribute(ctx, path.Root(names.AttrAccountID), parts[0])) + smerr.EnrichAppend(ctx, &resp.Diagnostics, resp.State.SetAttribute(ctx, path.Root("experiment_template_id"), parts[1])) } func findTargetAccountConfigurationByID(ctx context.Context, conn *fis.Client, accountId, experimentId *string) (*awstypes.TargetAccountConfiguration, error) { From dadb991f878048d9d8e4dec8ddb32b6be121474c Mon Sep 17 00:00:00 2001 From: Subham Mukhopadhyay Date: Fri, 31 Oct 2025 03:25:33 +0530 Subject: [PATCH 10/10] remove timeouts --- .../fis/target_account_configuration.go | 24 ++++--------------- ...target_account_configuration.html.markdown | 8 ------- 2 files changed, 4 insertions(+), 28 deletions(-) diff --git a/internal/service/fis/target_account_configuration.go b/internal/service/fis/target_account_configuration.go index 208aabea69d7..c0088b3dd506 100644 --- a/internal/service/fis/target_account_configuration.go +++ b/internal/service/fis/target_account_configuration.go @@ -7,12 +7,10 @@ import ( "context" "errors" "fmt" - "time" "github.com/YakDriver/smarterr" "github.com/aws/aws-sdk-go-v2/service/fis" awstypes "github.com/aws/aws-sdk-go-v2/service/fis/types" - "github.com/hashicorp/terraform-plugin-framework-timeouts/resource/timeouts" "github.com/hashicorp/terraform-plugin-framework-validators/stringvalidator" "github.com/hashicorp/terraform-plugin-framework/path" "github.com/hashicorp/terraform-plugin-framework/resource" @@ -35,11 +33,6 @@ import ( // @FrameworkResource("aws_fis_target_account_configuration", name="Target Account Configuration") func newResourceTargetAccountConfiguration(_ context.Context) (resource.ResourceWithConfigure, error) { r := &resourceTargetAccountConfiguration{} - - r.SetDefaultCreateTimeout(5 * time.Minute) - r.SetDefaultUpdateTimeout(5 * time.Minute) - r.SetDefaultDeleteTimeout(5 * time.Minute) - return r, nil } @@ -49,7 +42,6 @@ const ( type resourceTargetAccountConfiguration struct { framework.ResourceWithModel[resourceTargetAccountConfigurationModel] - framework.WithTimeouts } func (r *resourceTargetAccountConfiguration) Schema(ctx context.Context, req resource.SchemaRequest, resp *resource.SchemaResponse) { @@ -80,13 +72,6 @@ func (r *resourceTargetAccountConfiguration) Schema(ctx context.Context, req res Validators: []validator.String{stringvalidator.LengthBetween(20, 2048)}, }, }, - Blocks: map[string]schema.Block{ - names.AttrTimeouts: timeouts.Block(ctx, timeouts.Opts{ - Create: true, - Update: true, - Delete: true, - }), - }, } } @@ -253,9 +238,8 @@ func findTargetAccountConfigurationByID(ctx context.Context, conn *fis.Client, a type resourceTargetAccountConfigurationModel struct { framework.WithRegionModel - AccountId types.String `tfsdk:"account_id"` - Description types.String `tfsdk:"description"` - ExperimentTemplateId types.String `tfsdk:"experiment_template_id"` - RoleArn types.String `tfsdk:"role_arn"` - Timeouts timeouts.Value `tfsdk:"timeouts"` + AccountId types.String `tfsdk:"account_id"` + Description types.String `tfsdk:"description"` + ExperimentTemplateId types.String `tfsdk:"experiment_template_id"` + RoleArn types.String `tfsdk:"role_arn"` } diff --git a/website/docs/r/fis_target_account_configuration.html.markdown b/website/docs/r/fis_target_account_configuration.html.markdown index c5192d52ddf5..ec77cb1a00c9 100644 --- a/website/docs/r/fis_target_account_configuration.html.markdown +++ b/website/docs/r/fis_target_account_configuration.html.markdown @@ -40,14 +40,6 @@ The following arguments are optional: This resource exports no additional attributes. -## Timeouts - -[Configuration options](https://developer.hashicorp.com/terraform/language/resources/syntax#operation-timeouts): - -* `create` - (Default `5m`) -* `update` - (Default `5m`) -* `delete` - (Default `5m`) - ## Import In Terraform v1.5.0 and later, use an [`import` block](https://developer.hashicorp.com/terraform/language/import) to import FIS (Fault Injection Simulator) Target Account Configuration using the `account_id,experiment_template_id`. For example: