diff --git a/.changelog/36900.txt b/.changelog/36900.txt new file mode 100644 index 00000000000..e630c5cfa54 --- /dev/null +++ b/.changelog/36900.txt @@ -0,0 +1,3 @@ +```release-note:enhancement +resource/aws_fis_experiment_template: Add `experiment_options` configuration block +``` diff --git a/internal/service/fis/experiment_template.go b/internal/service/fis/experiment_template.go index b8007e71423..7aaf7376259 100644 --- a/internal/service/fis/experiment_template.go +++ b/internal/service/fis/experiment_template.go @@ -6,33 +6,32 @@ package fis import ( "context" "errors" + "log" "time" "github.com/YakDriver/regexache" "github.com/aws/aws-sdk-go-v2/aws" "github.com/aws/aws-sdk-go-v2/service/fis" - "github.com/aws/aws-sdk-go-v2/service/fis/types" - "github.com/hashicorp/aws-sdk-go-base/v2/awsv1shim/v2/tfawserr" + awstypes "github.com/aws/aws-sdk-go-v2/service/fis/types" "github.com/hashicorp/terraform-plugin-sdk/v2/diag" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/id" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/retry" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/validation" "github.com/hashicorp/terraform-provider-aws/internal/conns" - "github.com/hashicorp/terraform-provider-aws/internal/create" + "github.com/hashicorp/terraform-provider-aws/internal/enum" + "github.com/hashicorp/terraform-provider-aws/internal/errs" + "github.com/hashicorp/terraform-provider-aws/internal/errs/sdkdiag" "github.com/hashicorp/terraform-provider-aws/internal/flex" tftags "github.com/hashicorp/terraform-provider-aws/internal/tags" + "github.com/hashicorp/terraform-provider-aws/internal/tfresource" "github.com/hashicorp/terraform-provider-aws/internal/verify" "github.com/hashicorp/terraform-provider-aws/names" ) -const ( - ErrCodeNotFound = 404 - ResNameExperimentTemplate = "Experiment Template" -) - // @SDKResource("aws_fis_experiment_template", name="Experiment Template") // @Tags -func ResourceExperimentTemplate() *schema.Resource { +func resourceExperimentTemplate() *schema.Resource { return &schema.Resource{ CreateWithoutTimeout: resourceExperimentTemplateCreate, ReadWithoutTimeout: resourceExperimentTemplateRead, @@ -96,7 +95,6 @@ func ResourceExperimentTemplate() *schema.Resource { "start_after": { Type: schema.TypeSet, Optional: true, - Set: schema.HashString, Elem: &schema.Schema{ Type: schema.TypeString, ValidateFunc: validation.StringLenBetween(0, 64), @@ -129,6 +127,26 @@ func ResourceExperimentTemplate() *schema.Resource { Required: true, ValidateFunc: validation.StringLenBetween(0, 512), }, + "experiment_options": { + Type: schema.TypeList, + Optional: true, + Computed: true, + MaxItems: 1, + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "account_targeting": { + Type: schema.TypeString, + Optional: true, + ValidateDiagFunc: enum.Validate[awstypes.AccountTargeting](), + }, + "empty_target_resolution_mode": { + Type: schema.TypeString, + Optional: true, + ValidateDiagFunc: enum.Validate[awstypes.EmptyTargetResolutionMode](), + }, + }, + }, + }, "log_configuration": { Type: schema.TypeList, Optional: true, @@ -213,7 +231,6 @@ func ResourceExperimentTemplate() *schema.Resource { names.AttrValues: { Type: schema.TypeSet, Required: true, - Set: schema.HashString, Elem: &schema.Schema{ Type: schema.TypeString, ValidateFunc: validation.StringLenBetween(0, 128), @@ -236,7 +253,6 @@ func ResourceExperimentTemplate() *schema.Resource { Type: schema.TypeSet, Optional: true, MaxItems: 5, - Set: schema.HashString, Elem: &schema.Schema{ Type: schema.TypeString, ValidateFunc: verify.ValidARN, @@ -285,7 +301,6 @@ func ResourceExperimentTemplate() *schema.Resource { func resourceExperimentTemplateCreate(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics { var diags diag.Diagnostics - conn := meta.(*conns.AWSClient).FISClient(ctx) input := &fis.CreateExperimentTemplateInput{ @@ -298,15 +313,20 @@ func resourceExperimentTemplateCreate(ctx context.Context, d *schema.ResourceDat Tags: getTagsIn(ctx), } - targets, err := expandExperimentTemplateTargets(d.Get(names.AttrTarget).(*schema.Set)) - if err != nil { - return create.AppendDiagError(diags, names.FIS, create.ErrActionCreating, ResNameExperimentTemplate, d.Get(names.AttrDescription).(string), err) + if v, ok := d.GetOk("experiment_options"); ok { + input.ExperimentOptions = expandCreateExperimentTemplateExperimentOptionsInput(v.([]interface{})) + } + + if targets, err := expandExperimentTemplateTargets(d.Get(names.AttrTarget).(*schema.Set)); err != nil { + return sdkdiag.AppendFromErr(diags, err) + } else { + input.Targets = targets } - input.Targets = targets output, err := conn.CreateExperimentTemplate(ctx, input) + if err != nil { - return create.AppendDiagError(diags, names.FIS, create.ErrActionCreating, ResNameExperimentTemplate, d.Get(names.AttrDescription).(string), err) + return sdkdiag.AppendErrorf(diags, "creating FIS Experiment Template: %s", err) } d.SetId(aws.ToString(output.ExperimentTemplate.Id)) @@ -316,52 +336,37 @@ func resourceExperimentTemplateCreate(ctx context.Context, d *schema.ResourceDat func resourceExperimentTemplateRead(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics { var diags diag.Diagnostics - conn := meta.(*conns.AWSClient).FISClient(ctx) - input := &fis.GetExperimentTemplateInput{Id: aws.String(d.Id())} - out, err := conn.GetExperimentTemplate(ctx, input) - - var nf *types.ResourceNotFoundException - if !d.IsNewResource() && errors.As(err, &nf) { - create.LogNotFoundRemoveState(names.FIS, create.ErrActionReading, ResNameExperimentTemplate, d.Id()) - d.SetId("") - return diags - } + experimentTemplate, err := findExperimentTemplateByID(ctx, conn, d.Id()) - if !d.IsNewResource() && tfawserr.ErrStatusCodeEquals(err, ErrCodeNotFound) { - create.LogNotFoundRemoveState(names.FIS, create.ErrActionReading, ResNameExperimentTemplate, d.Id()) + if !d.IsNewResource() && tfresource.NotFound(err) { + log.Printf("[WARN] FIS Experiment Template (%s) not found, removing from state", d.Id()) d.SetId("") return diags } if err != nil { - return create.AppendDiagError(diags, names.FIS, create.ErrActionReading, ResNameExperimentTemplate, d.Id(), err) - } - - experimentTemplate := out.ExperimentTemplate - if experimentTemplate == nil { - return create.AppendDiagError(diags, names.FIS, create.ErrActionReading, ResNameExperimentTemplate, d.Id(), errors.New("empty result")) + return sdkdiag.AppendErrorf(diags, "reading FIS Experiment Template (%s): %s", d.Id(), err) } d.SetId(aws.ToString(experimentTemplate.Id)) + if err := d.Set(names.AttrAction, flattenExperimentTemplateActions(experimentTemplate.Actions)); err != nil { + return sdkdiag.AppendErrorf(diags, "setting action: %s", err) + } d.Set(names.AttrRoleARN, experimentTemplate.RoleArn) d.Set(names.AttrDescription, experimentTemplate.Description) - - if err := d.Set(names.AttrAction, flattenExperimentTemplateActions(experimentTemplate.Actions)); err != nil { - return create.AppendDiagSettingError(diags, names.FIS, ResNameExperimentTemplate, d.Id(), names.AttrAction, err) + if err := d.Set("experiment_options", flattenExperimentTemplateExperimentOptions(experimentTemplate.ExperimentOptions)); err != nil { + return sdkdiag.AppendErrorf(diags, "setting experiment_options: %s", err) } - if err := d.Set("log_configuration", flattenExperimentTemplateLogConfiguration(experimentTemplate.LogConfiguration)); err != nil { - return create.AppendDiagSettingError(diags, names.FIS, ResNameExperimentTemplate, d.Id(), "log_configuration", err) + return sdkdiag.AppendErrorf(diags, "setting log_configuration: %s", err) } - if err := d.Set("stop_condition", flattenExperimentTemplateStopConditions(experimentTemplate.StopConditions)); err != nil { - return create.AppendDiagSettingError(diags, names.FIS, ResNameExperimentTemplate, d.Id(), "stop_condition", err) + return sdkdiag.AppendErrorf(diags, "setting stop_condition: %s", err) } - if err := d.Set(names.AttrTarget, flattenExperimentTemplateTargets(experimentTemplate.Targets)); err != nil { - return create.AppendDiagSettingError(diags, names.FIS, ResNameExperimentTemplate, d.Id(), names.AttrTarget, err) + return sdkdiag.AppendErrorf(diags, "setting target: %s", err) } setTagsOut(ctx, experimentTemplate.Tags) @@ -387,6 +392,10 @@ func resourceExperimentTemplateUpdate(ctx context.Context, d *schema.ResourceDat input.Description = aws.String(d.Get(names.AttrDescription).(string)) } + if d.HasChange("experiment_options") { + input.ExperimentOptions = expandUpdateExperimentTemplateExperimentOptionsInput(d.Get("experiment_options").([]interface{})) + } + if d.HasChange("log_configuration") { config := expandExperimentTemplateLogConfigurationForUpdate(d.Get("log_configuration").([]interface{})) input.LogConfiguration = config @@ -401,16 +410,17 @@ func resourceExperimentTemplateUpdate(ctx context.Context, d *schema.ResourceDat } if d.HasChange(names.AttrTarget) { - targets, err := expandExperimentTemplateTargetsForUpdate(d.Get(names.AttrTarget).(*schema.Set)) - if err != nil { - return create.AppendDiagError(diags, names.FIS, create.ErrActionUpdating, ResNameExperimentTemplate, d.Id(), err) + if targets, err := expandExperimentTemplateTargetsForUpdate(d.Get(names.AttrTarget).(*schema.Set)); err != nil { + return sdkdiag.AppendFromErr(diags, err) + } else { + input.Targets = targets } - input.Targets = targets } _, err := conn.UpdateExperimentTemplate(ctx, input) + if err != nil { - return create.AppendDiagError(diags, names.FIS, create.ErrActionUpdating, ResNameExperimentTemplate, d.Id(), err) + return sdkdiag.AppendErrorf(diags, "updating FIS Experiment Template (%s): %s", d.Id(), err) } } @@ -419,38 +429,59 @@ func resourceExperimentTemplateUpdate(ctx context.Context, d *schema.ResourceDat func resourceExperimentTemplateDelete(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics { var diags diag.Diagnostics - conn := meta.(*conns.AWSClient).FISClient(ctx) + + log.Printf("[DEBUG] Deleting FIS Experiment Template: %s", d.Id()) _, err := conn.DeleteExperimentTemplate(ctx, &fis.DeleteExperimentTemplateInput{ Id: aws.String(d.Id()), }) - var nf *types.ResourceNotFoundException - if errors.As(err, &nf) { + if errs.IsA[*awstypes.ResourceNotFoundException](err) { return diags } - if tfawserr.ErrStatusCodeEquals(err, ErrCodeNotFound) { - return diags + if err != nil { + return sdkdiag.AppendErrorf(diags, "deleting FIS Experiment Template (%s): %s", d.Id(), err) + } + + return diags +} + +func findExperimentTemplateByID(ctx context.Context, conn *fis.Client, id string) (*awstypes.ExperimentTemplate, error) { + input := &fis.GetExperimentTemplateInput{ + Id: aws.String(id), + } + + output, err := conn.GetExperimentTemplate(ctx, input) + + if errs.IsA[*awstypes.ResourceNotFoundException](err) { + return nil, &retry.NotFoundError{ + LastError: err, + LastRequest: input, + } } if err != nil { - return create.AppendDiagError(diags, names.FIS, create.ErrActionDeleting, ResNameExperimentTemplate, d.Id(), err) + return nil, err } - return diags + if output == nil || output.ExperimentTemplate == nil { + return nil, tfresource.NewEmptyResultError(input) + } + + return output.ExperimentTemplate, nil } -func expandExperimentTemplateActions(l *schema.Set) map[string]types.CreateExperimentTemplateActionInput { +func expandExperimentTemplateActions(l *schema.Set) map[string]awstypes.CreateExperimentTemplateActionInput { if l.Len() == 0 { return nil } - attrs := make(map[string]types.CreateExperimentTemplateActionInput, l.Len()) + attrs := make(map[string]awstypes.CreateExperimentTemplateActionInput, l.Len()) for _, m := range l.List() { raw := m.(map[string]interface{}) - config := types.CreateExperimentTemplateActionInput{} + config := awstypes.CreateExperimentTemplateActionInput{} if v, ok := raw["action_id"].(string); ok && v != "" { config.ActionId = aws.String(v) @@ -480,16 +511,16 @@ func expandExperimentTemplateActions(l *schema.Set) map[string]types.CreateExper return attrs } -func expandExperimentTemplateActionsForUpdate(l *schema.Set) map[string]types.UpdateExperimentTemplateActionInputItem { +func expandExperimentTemplateActionsForUpdate(l *schema.Set) map[string]awstypes.UpdateExperimentTemplateActionInputItem { if l.Len() == 0 { return nil } - attrs := make(map[string]types.UpdateExperimentTemplateActionInputItem, l.Len()) + attrs := make(map[string]awstypes.UpdateExperimentTemplateActionInputItem, l.Len()) for _, m := range l.List() { raw := m.(map[string]interface{}) - config := types.UpdateExperimentTemplateActionInputItem{} + config := awstypes.UpdateExperimentTemplateActionInputItem{} if v, ok := raw["action_id"].(string); ok && v != "" { config.ActionId = aws.String(v) @@ -519,16 +550,70 @@ func expandExperimentTemplateActionsForUpdate(l *schema.Set) map[string]types.Up return attrs } -func expandExperimentTemplateStopConditions(l *schema.Set) []types.CreateExperimentTemplateStopConditionInput { +func expandCreateExperimentTemplateExperimentOptionsInput(tfMap []interface{}) *awstypes.CreateExperimentTemplateExperimentOptionsInput { + if len(tfMap) == 0 || tfMap[0] == nil { + return nil + } + + apiObject := &awstypes.CreateExperimentTemplateExperimentOptionsInput{} + + m := tfMap[0].(map[string]interface{}) + + if v, ok := m["account_targeting"].(string); ok { + apiObject.AccountTargeting = awstypes.AccountTargeting(v) + } + + if v, ok := m["empty_target_resolution_mode"].(string); ok { + apiObject.EmptyTargetResolutionMode = awstypes.EmptyTargetResolutionMode(v) + } + + return apiObject +} + +func expandUpdateExperimentTemplateExperimentOptionsInput(tfMap []interface{}) *awstypes.UpdateExperimentTemplateExperimentOptionsInput { + if len(tfMap) == 0 || tfMap[0] == nil { + return nil + } + + m := tfMap[0].(map[string]interface{}) + + apiObject := &awstypes.UpdateExperimentTemplateExperimentOptionsInput{} + + if v, ok := m["empty_target_resolution_mode"].(string); ok { + apiObject.EmptyTargetResolutionMode = awstypes.EmptyTargetResolutionMode(v) + } + + return apiObject +} + +func flattenExperimentTemplateExperimentOptions(apiObject *awstypes.ExperimentTemplateExperimentOptions) []map[string]interface{} { + if apiObject == nil { + return nil + } + + tfMap := make([]map[string]interface{}, 1) + tfMap[0] = make(map[string]interface{}) + if v := apiObject.AccountTargeting; v != "" { + tfMap[0]["account_targeting"] = v + } + + if v := apiObject.EmptyTargetResolutionMode; v != "" { + tfMap[0]["empty_target_resolution_mode"] = v + } + + return tfMap +} + +func expandExperimentTemplateStopConditions(l *schema.Set) []awstypes.CreateExperimentTemplateStopConditionInput { if l.Len() == 0 { return nil } - items := []types.CreateExperimentTemplateStopConditionInput{} + items := []awstypes.CreateExperimentTemplateStopConditionInput{} for _, m := range l.List() { raw := m.(map[string]interface{}) - config := types.CreateExperimentTemplateStopConditionInput{} + config := awstypes.CreateExperimentTemplateStopConditionInput{} if v, ok := raw[names.AttrSource].(string); ok && v != "" { config.Source = aws.String(v) @@ -544,14 +629,14 @@ func expandExperimentTemplateStopConditions(l *schema.Set) []types.CreateExperim return items } -func expandExperimentTemplateLogConfiguration(l []interface{}) *types.CreateExperimentTemplateLogConfigurationInput { +func expandExperimentTemplateLogConfiguration(l []interface{}) *awstypes.CreateExperimentTemplateLogConfigurationInput { if len(l) == 0 { return nil } raw := l[0].(map[string]interface{}) - config := types.CreateExperimentTemplateLogConfigurationInput{ + config := awstypes.CreateExperimentTemplateLogConfigurationInput{ LogSchemaVersion: aws.Int32(int32(raw["log_schema_version"].(int))), } @@ -566,27 +651,27 @@ func expandExperimentTemplateLogConfiguration(l []interface{}) *types.CreateExpe return &config } -func expandExperimentTemplateCloudWatchLogsConfiguration(l []interface{}) *types.ExperimentTemplateCloudWatchLogsLogConfigurationInput { +func expandExperimentTemplateCloudWatchLogsConfiguration(l []interface{}) *awstypes.ExperimentTemplateCloudWatchLogsLogConfigurationInput { if len(l) == 0 { return nil } raw := l[0].(map[string]interface{}) - config := types.ExperimentTemplateCloudWatchLogsLogConfigurationInput{ + config := awstypes.ExperimentTemplateCloudWatchLogsLogConfigurationInput{ LogGroupArn: aws.String(raw["log_group_arn"].(string)), } return &config } -func expandExperimentTemplateS3Configuration(l []interface{}) *types.ExperimentTemplateS3LogConfigurationInput { +func expandExperimentTemplateS3Configuration(l []interface{}) *awstypes.ExperimentTemplateS3LogConfigurationInput { if len(l) == 0 { return nil } raw := l[0].(map[string]interface{}) - config := types.ExperimentTemplateS3LogConfigurationInput{ + config := awstypes.ExperimentTemplateS3LogConfigurationInput{ BucketName: aws.String(raw[names.AttrBucketName].(string)), } if v, ok := raw[names.AttrPrefix].(string); ok && v != "" { @@ -596,16 +681,16 @@ func expandExperimentTemplateS3Configuration(l []interface{}) *types.ExperimentT return &config } -func expandExperimentTemplateStopConditionsForUpdate(l *schema.Set) []types.UpdateExperimentTemplateStopConditionInput { +func expandExperimentTemplateStopConditionsForUpdate(l *schema.Set) []awstypes.UpdateExperimentTemplateStopConditionInput { if l.Len() == 0 { return nil } - items := []types.UpdateExperimentTemplateStopConditionInput{} + items := []awstypes.UpdateExperimentTemplateStopConditionInput{} for _, m := range l.List() { raw := m.(map[string]interface{}) - config := types.UpdateExperimentTemplateStopConditionInput{} + config := awstypes.UpdateExperimentTemplateStopConditionInput{} if v, ok := raw[names.AttrSource].(string); ok && v != "" { config.Source = aws.String(v) @@ -621,17 +706,17 @@ func expandExperimentTemplateStopConditionsForUpdate(l *schema.Set) []types.Upda return items } -func expandExperimentTemplateTargets(l *schema.Set) (map[string]types.CreateExperimentTemplateTargetInput, error) { +func expandExperimentTemplateTargets(l *schema.Set) (map[string]awstypes.CreateExperimentTemplateTargetInput, error) { if l.Len() == 0 { //Even though a template with no targets is valid (eg. containing just aws:fis:wait) and the API reference states that targets is not required, the key still needs to be present. - return map[string]types.CreateExperimentTemplateTargetInput{}, nil + return map[string]awstypes.CreateExperimentTemplateTargetInput{}, nil } - attrs := make(map[string]types.CreateExperimentTemplateTargetInput, l.Len()) + attrs := make(map[string]awstypes.CreateExperimentTemplateTargetInput, l.Len()) for _, m := range l.List() { raw := m.(map[string]interface{}) - config := types.CreateExperimentTemplateTargetInput{} + config := awstypes.CreateExperimentTemplateTargetInput{} var hasSeenResourceArns bool if v, ok := raw[names.AttrFilter].([]interface{}); ok && len(v) > 0 { @@ -672,16 +757,16 @@ func expandExperimentTemplateTargets(l *schema.Set) (map[string]types.CreateExpe return attrs, nil } -func expandExperimentTemplateTargetsForUpdate(l *schema.Set) (map[string]types.UpdateExperimentTemplateTargetInput, error) { +func expandExperimentTemplateTargetsForUpdate(l *schema.Set) (map[string]awstypes.UpdateExperimentTemplateTargetInput, error) { if l.Len() == 0 { return nil, nil } - attrs := make(map[string]types.UpdateExperimentTemplateTargetInput, l.Len()) + attrs := make(map[string]awstypes.UpdateExperimentTemplateTargetInput, l.Len()) for _, m := range l.List() { raw := m.(map[string]interface{}) - config := types.UpdateExperimentTemplateTargetInput{} + config := awstypes.UpdateExperimentTemplateTargetInput{} var hasSeenResourceArns bool if v, ok := raw[names.AttrFilter].([]interface{}); ok && len(v) > 0 { @@ -722,13 +807,13 @@ func expandExperimentTemplateTargetsForUpdate(l *schema.Set) (map[string]types.U return attrs, nil } -func expandExperimentTemplateLogConfigurationForUpdate(l []interface{}) *types.UpdateExperimentTemplateLogConfigurationInput { +func expandExperimentTemplateLogConfigurationForUpdate(l []interface{}) *awstypes.UpdateExperimentTemplateLogConfigurationInput { if len(l) == 0 { - return &types.UpdateExperimentTemplateLogConfigurationInput{} + return &awstypes.UpdateExperimentTemplateLogConfigurationInput{} } raw := l[0].(map[string]interface{}) - config := types.UpdateExperimentTemplateLogConfigurationInput{ + config := awstypes.UpdateExperimentTemplateLogConfigurationInput{ LogSchemaVersion: aws.Int32(int32(raw["log_schema_version"].(int))), } if v, ok := raw["cloudwatch_logs_configuration"].([]interface{}); ok && len(v) > 0 { @@ -776,16 +861,16 @@ func expandExperimentTemplateActionTargets(l []interface{}) map[string]string { return attrs } -func expandExperimentTemplateTargetFilters(l []interface{}) []types.ExperimentTemplateTargetInputFilter { +func expandExperimentTemplateTargetFilters(l []interface{}) []awstypes.ExperimentTemplateTargetInputFilter { if len(l) == 0 || l[0] == nil { return nil } - items := []types.ExperimentTemplateTargetInputFilter{} + items := []awstypes.ExperimentTemplateTargetInputFilter{} for _, m := range l { raw := m.(map[string]interface{}) - config := types.ExperimentTemplateTargetInputFilter{} + config := awstypes.ExperimentTemplateTargetInputFilter{} if v, ok := raw[names.AttrPath].(string); ok && v != "" { config.Path = aws.String(v) @@ -818,7 +903,7 @@ func expandExperimentTemplateTargetResourceTags(l *schema.Set) map[string]string return attrs } -func flattenExperimentTemplateActions(configured map[string]types.ExperimentTemplateAction) []map[string]interface{} { +func flattenExperimentTemplateActions(configured map[string]awstypes.ExperimentTemplateAction) []map[string]interface{} { dataResources := make([]map[string]interface{}, 0, len(configured)) for k, v := range configured { @@ -837,7 +922,7 @@ func flattenExperimentTemplateActions(configured map[string]types.ExperimentTemp return dataResources } -func flattenExperimentTemplateStopConditions(configured []types.ExperimentTemplateStopCondition) []map[string]interface{} { +func flattenExperimentTemplateStopConditions(configured []awstypes.ExperimentTemplateStopCondition) []map[string]interface{} { dataResources := make([]map[string]interface{}, 0, len(configured)) for _, v := range configured { @@ -854,7 +939,7 @@ func flattenExperimentTemplateStopConditions(configured []types.ExperimentTempla return dataResources } -func flattenExperimentTemplateTargets(configured map[string]types.ExperimentTemplateTarget) []map[string]interface{} { +func flattenExperimentTemplateTargets(configured map[string]awstypes.ExperimentTemplateTarget) []map[string]interface{} { dataResources := make([]map[string]interface{}, 0, len(configured)) for k, v := range configured { @@ -874,7 +959,7 @@ func flattenExperimentTemplateTargets(configured map[string]types.ExperimentTemp return dataResources } -func flattenExperimentTemplateLogConfiguration(configured *types.ExperimentTemplateLogConfiguration) []map[string]interface{} { +func flattenExperimentTemplateLogConfiguration(configured *awstypes.ExperimentTemplateLogConfiguration) []map[string]interface{} { if configured == nil { return make([]map[string]interface{}, 0) } @@ -888,7 +973,7 @@ func flattenExperimentTemplateLogConfiguration(configured *types.ExperimentTempl return dataResources } -func flattenCloudWatchLogsConfiguration(configured *types.ExperimentTemplateCloudWatchLogsLogConfiguration) []map[string]interface{} { +func flattenCloudWatchLogsConfiguration(configured *awstypes.ExperimentTemplateCloudWatchLogsLogConfiguration) []map[string]interface{} { if configured == nil { return make([]map[string]interface{}, 0) } @@ -900,7 +985,7 @@ func flattenCloudWatchLogsConfiguration(configured *types.ExperimentTemplateClou return dataResources } -func flattenS3Configuration(configured *types.ExperimentTemplateS3LogConfiguration) []map[string]interface{} { +func flattenS3Configuration(configured *awstypes.ExperimentTemplateS3LogConfiguration) []map[string]interface{} { if configured == nil { return make([]map[string]interface{}, 0) } @@ -942,7 +1027,7 @@ func flattenExperimentTemplateActionTargets(configured map[string]string) []map[ return dataResources } -func flattenExperimentTemplateTargetFilters(configured []types.ExperimentTemplateTargetFilter) []map[string]interface{} { +func flattenExperimentTemplateTargetFilters(configured []awstypes.ExperimentTemplateTargetFilter) []map[string]interface{} { dataResources := make([]map[string]interface{}, 0, len(configured)) for _, v := range configured { diff --git a/internal/service/fis/experiment_template_test.go b/internal/service/fis/experiment_template_test.go index b939b3c8425..b217d93c7e2 100644 --- a/internal/service/fis/experiment_template_test.go +++ b/internal/service/fis/experiment_template_test.go @@ -5,20 +5,18 @@ package fis_test import ( "context" - "errors" "fmt" "testing" - "github.com/aws/aws-sdk-go-v2/aws" "github.com/aws/aws-sdk-go-v2/service/fis" - "github.com/aws/aws-sdk-go-v2/service/fis/types" - "github.com/hashicorp/aws-sdk-go-base/v2/awsv1shim/v2/tfawserr" + 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" 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" ) @@ -26,7 +24,7 @@ func TestAccFISExperimentTemplate_basic(t *testing.T) { ctx := acctest.Context(t) rName := sdkacctest.RandomWithPrefix(acctest.ResourcePrefix) resourceName := "aws_fis_experiment_template.test" - var conf types.ExperimentTemplate + var conf awstypes.ExperimentTemplate resource.ParallelTest(t, resource.TestCase{ PreCheck: func() { acctest.PreCheck(ctx, t) }, @@ -36,13 +34,9 @@ func TestAccFISExperimentTemplate_basic(t *testing.T) { Steps: []resource.TestStep{ { Config: testAccExperimentTemplateConfig_basic(rName, "An experiment template for testing", "test-action-1", "", "aws:ec2:terminate-instances", "Instances", "to-terminate-1", "aws:ec2:instance", "COUNT(1)", "env", "test"), - Check: resource.ComposeTestCheckFunc( + Check: resource.ComposeAggregateTestCheckFunc( testAccExperimentTemplateExists(ctx, resourceName, &conf), - resource.TestCheckResourceAttr(resourceName, names.AttrDescription, "An experiment template for testing"), - resource.TestCheckResourceAttrPair(resourceName, names.AttrRoleARN, "aws_iam_role.test", names.AttrARN), - resource.TestCheckResourceAttr(resourceName, "stop_condition.0.source", "none"), - resource.TestCheckResourceAttr(resourceName, "stop_condition.0.value", ""), - resource.TestCheckResourceAttr(resourceName, "stop_condition.#", acctest.Ct1), + resource.TestCheckResourceAttr(resourceName, "action.#", acctest.Ct1), resource.TestCheckResourceAttr(resourceName, "action.0.name", "test-action-1"), resource.TestCheckResourceAttr(resourceName, "action.0.description", ""), resource.TestCheckResourceAttr(resourceName, "action.0.action_id", "aws:ec2:terminate-instances"), @@ -51,7 +45,13 @@ func TestAccFISExperimentTemplate_basic(t *testing.T) { resource.TestCheckResourceAttr(resourceName, "action.0.target.0.key", "Instances"), resource.TestCheckResourceAttr(resourceName, "action.0.target.0.value", "to-terminate-1"), resource.TestCheckResourceAttr(resourceName, "action.0.target.#", acctest.Ct1), - resource.TestCheckResourceAttr(resourceName, "action.#", acctest.Ct1), + resource.TestCheckResourceAttr(resourceName, names.AttrDescription, "An experiment template for testing"), + resource.TestCheckResourceAttr(resourceName, "experiment_options.#", acctest.Ct1), + resource.TestCheckResourceAttrPair(resourceName, names.AttrRoleARN, "aws_iam_role.test", names.AttrARN), + resource.TestCheckResourceAttr(resourceName, "stop_condition.#", acctest.Ct1), + resource.TestCheckResourceAttr(resourceName, "stop_condition.0.source", "none"), + resource.TestCheckResourceAttr(resourceName, "stop_condition.0.value", ""), + resource.TestCheckResourceAttr(resourceName, "target.#", acctest.Ct1), resource.TestCheckResourceAttr(resourceName, "target.0.name", "to-terminate-1"), resource.TestCheckResourceAttr(resourceName, "target.0.resource_type", "aws:ec2:instance"), resource.TestCheckResourceAttr(resourceName, "target.0.selection_mode", "COUNT(1)"), @@ -60,7 +60,6 @@ func TestAccFISExperimentTemplate_basic(t *testing.T) { resource.TestCheckResourceAttr(resourceName, "target.0.resource_tag.0.key", "env"), resource.TestCheckResourceAttr(resourceName, "target.0.resource_tag.0.value", "test"), resource.TestCheckResourceAttr(resourceName, "target.0.resource_tag.#", acctest.Ct1), - resource.TestCheckResourceAttr(resourceName, "target.#", acctest.Ct1), ), }, { @@ -76,7 +75,7 @@ func TestAccFISExperimentTemplate_disappears(t *testing.T) { ctx := acctest.Context(t) rName := sdkacctest.RandomWithPrefix(acctest.ResourcePrefix) resourceName := "aws_fis_experiment_template.test" - var conf types.ExperimentTemplate + var conf awstypes.ExperimentTemplate resource.ParallelTest(t, resource.TestCase{ PreCheck: func() { acctest.PreCheck(ctx, t) }, @@ -100,7 +99,7 @@ func TestAccFISExperimentTemplate_update(t *testing.T) { ctx := acctest.Context(t) rName := sdkacctest.RandomWithPrefix(acctest.ResourcePrefix) resourceName := "aws_fis_experiment_template.test" - var conf types.ExperimentTemplate + var conf awstypes.ExperimentTemplate resource.ParallelTest(t, resource.TestCase{ PreCheck: func() { acctest.PreCheck(ctx, t) }, @@ -170,7 +169,7 @@ func TestAccFISExperimentTemplate_spot(t *testing.T) { ctx := acctest.Context(t) rName := sdkacctest.RandomWithPrefix(acctest.ResourcePrefix) resourceName := "aws_fis_experiment_template.test" - var conf types.ExperimentTemplate + var conf awstypes.ExperimentTemplate resource.ParallelTest(t, resource.TestCase{ PreCheck: func() { acctest.PreCheck(ctx, t) }, @@ -220,7 +219,7 @@ func TestAccFISExperimentTemplate_eks(t *testing.T) { } rName := sdkacctest.RandomWithPrefix(acctest.ResourcePrefix) resourceName := "aws_fis_experiment_template.test" - var conf types.ExperimentTemplate + var conf awstypes.ExperimentTemplate resource.ParallelTest(t, resource.TestCase{ PreCheck: func() { acctest.PreCheck(ctx, t) }, @@ -276,7 +275,7 @@ func TestAccFISExperimentTemplate_ebs(t *testing.T) { } rName := sdkacctest.RandomWithPrefix(acctest.ResourcePrefix) resourceName := "aws_fis_experiment_template.test" - var conf types.ExperimentTemplate + var conf awstypes.ExperimentTemplate resource.ParallelTest(t, resource.TestCase{ PreCheck: func() { acctest.PreCheck(ctx, t) }, @@ -324,7 +323,7 @@ func TestAccFISExperimentTemplate_ebsParameters(t *testing.T) { } rName := sdkacctest.RandomWithPrefix(acctest.ResourcePrefix) resourceName := "aws_fis_experiment_template.test" - var conf types.ExperimentTemplate + var conf awstypes.ExperimentTemplate resource.ParallelTest(t, resource.TestCase{ PreCheck: func() { acctest.PreCheck(ctx, t) }, @@ -372,7 +371,7 @@ func TestAccFISExperimentTemplate_loggingConfiguration(t *testing.T) { ctx := acctest.Context(t) rName := sdkacctest.RandomWithPrefix(acctest.ResourcePrefix) resourceName := "aws_fis_experiment_template.test" - var conf types.ExperimentTemplate + var conf awstypes.ExperimentTemplate resource.ParallelTest(t, resource.TestCase{ PreCheck: func() { acctest.PreCheck(ctx, t) }, @@ -419,25 +418,60 @@ func TestAccFISExperimentTemplate_loggingConfiguration(t *testing.T) { }) } -func testAccExperimentTemplateExists(ctx context.Context, resourceName string, config *types.ExperimentTemplate) resource.TestCheckFunc { +func TestAccFISExperimentTemplate_updateExperimentOptions(t *testing.T) { + ctx := acctest.Context(t) + rName := sdkacctest.RandomWithPrefix(acctest.ResourcePrefix) + resourceName := "aws_fis_experiment_template.test" + var conf awstypes.ExperimentTemplate + + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { acctest.PreCheck(ctx, t) }, + ErrorCheck: acctest.ErrorCheck(t, fis.ServiceID), + ProtoV5ProviderFactories: acctest.ProtoV5ProviderFactories, + CheckDestroy: testAccCheckExperimentTemplateDestroy(ctx), + Steps: []resource.TestStep{ + { + Config: testAccExperimentTemplateConfig_ExperimentOptions(rName, "skip"), + Check: resource.ComposeTestCheckFunc( + testAccExperimentTemplateExists(ctx, resourceName, &conf), + resource.TestCheckResourceAttr(resourceName, "experiment_options.#", acctest.Ct1), + resource.TestCheckResourceAttr(resourceName, "experiment_options.0.account_targeting", "single-account"), + resource.TestCheckResourceAttr(resourceName, "experiment_options.0.empty_target_resolution_mode", "skip"), + ), + }, + { + Config: testAccExperimentTemplateConfig_ExperimentOptions(rName, "fail"), + Check: resource.ComposeTestCheckFunc( + testAccExperimentTemplateExists(ctx, resourceName, &conf), + resource.TestCheckResourceAttr(resourceName, "experiment_options.#", acctest.Ct1), + resource.TestCheckResourceAttr(resourceName, "experiment_options.0.empty_target_resolution_mode", "fail"), + ), + }, + { + ResourceName: resourceName, + ImportState: true, + ImportStateVerify: true, + }, + }, + }) +} + +func testAccExperimentTemplateExists(ctx context.Context, n string, v *awstypes.ExperimentTemplate) resource.TestCheckFunc { return func(s *terraform.State) error { - rs, ok := s.RootModule().Resources[resourceName] + rs, ok := s.RootModule().Resources[n] if !ok { - return fmt.Errorf("Not found: %s", resourceName) + return fmt.Errorf("Not found: %s", n) } conn := acctest.Provider.Meta().(*conns.AWSClient).FISClient(ctx) - out, err := conn.GetExperimentTemplate(ctx, &fis.GetExperimentTemplateInput{Id: aws.String(rs.Primary.ID)}) - if err != nil { - return fmt.Errorf("Describe Experiment Template error: %v", err) - } + output, err := tffis.FindExperimentTemplateByID(ctx, conn, rs.Primary.ID) - if out.ExperimentTemplate == nil { - return fmt.Errorf("No Experiment Template returned %v in %v", out.ExperimentTemplate, out) + if err != nil { + return err } - *out.ExperimentTemplate = *config + *v = *output return nil } @@ -451,12 +485,17 @@ func testAccCheckExperimentTemplateDestroy(ctx context.Context) resource.TestChe continue } - _, err := conn.GetExperimentTemplate(ctx, &fis.GetExperimentTemplateInput{Id: aws.String(rs.Primary.ID)}) + _, err := tffis.FindExperimentTemplateByID(ctx, conn, rs.Primary.ID) - var nf *types.ResourceNotFoundException - if !tfawserr.ErrStatusCodeEquals(err, tffis.ErrCodeNotFound) && !errors.As(err, &nf) { - return fmt.Errorf("Experiment Template '%s' was not deleted properly", rs.Primary.ID) + if tfresource.NotFound(err) { + continue + } + + if err != nil { + return err } + + return fmt.Errorf("FIS Experiment Template %s still exists", rs.Primary.ID) } return nil @@ -1067,3 +1106,62 @@ resource "aws_fis_experiment_template" "test" { } `, rName, desc, actionName, actionDesc, actionID, actionTargetK, actionTargetV, targetResType, targetSelectMode, targetResTagK, targetResTagV) } + +func testAccExperimentTemplateConfig_ExperimentOptions(rName, mode string) string { + return fmt.Sprintf(` +data "aws_partition" "current" {} + +resource "aws_iam_role" "test" { + name = %[1]q + + assume_role_policy = jsonencode({ + Statement = [{ + Action = "sts:AssumeRole" + Effect = "Allow" + Principal = { + Service = [ + "fis.${data.aws_partition.current.dns_suffix}", + ] + } + }] + Version = "2012-10-17" + }) +} + +resource "aws_fis_experiment_template" "test" { + description = "An experiment template for testing" + role_arn = aws_iam_role.test.arn + + stop_condition { + source = "none" + } + + experiment_options { + account_targeting = "single-account" + empty_target_resolution_mode = %[2]q + } + + action { + name = "test-action-1" + description = "" + action_id = "aws:ec2:terminate-instances" + + target { + key = "Instances" + value = "to-terminate-1" + } + } + + target { + name = "to-terminate-1" + resource_type = "aws:ec2:instance" + selection_mode = "ALL" + + resource_tag { + key = "env2" + value = "test2" + } + } +} +`, rName, mode) +} diff --git a/internal/service/fis/exports_test.go b/internal/service/fis/exports_test.go new file mode 100644 index 00000000000..d22424e77e1 --- /dev/null +++ b/internal/service/fis/exports_test.go @@ -0,0 +1,11 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package fis + +// Exports for use in tests only. +var ( + ResourceExperimentTemplate = resourceExperimentTemplate + + FindExperimentTemplateByID = findExperimentTemplateByID +) diff --git a/internal/service/fis/service_package_gen.go b/internal/service/fis/service_package_gen.go index 20600afce85..b8305e06a87 100644 --- a/internal/service/fis/service_package_gen.go +++ b/internal/service/fis/service_package_gen.go @@ -29,7 +29,7 @@ func (p *servicePackage) SDKDataSources(ctx context.Context) []*types.ServicePac func (p *servicePackage) SDKResources(ctx context.Context) []*types.ServicePackageSDKResource { return []*types.ServicePackageSDKResource{ { - Factory: ResourceExperimentTemplate, + Factory: resourceExperimentTemplate, TypeName: "aws_fis_experiment_template", Name: "Experiment Template", Tags: &types.ServicePackageResourceTags{}, diff --git a/internal/service/fis/sweep.go b/internal/service/fis/sweep.go index 9668fa11d98..e709d14b74f 100644 --- a/internal/service/fis/sweep.go +++ b/internal/service/fis/sweep.go @@ -9,9 +9,9 @@ import ( "github.com/aws/aws-sdk-go-v2/aws" "github.com/aws/aws-sdk-go-v2/service/fis" - "github.com/hashicorp/go-multierror" "github.com/hashicorp/terraform-plugin-testing/helper/resource" "github.com/hashicorp/terraform-provider-aws/internal/sweep" + "github.com/hashicorp/terraform-provider-aws/internal/sweep/awsv2" ) func RegisterSweepers() { @@ -30,24 +30,24 @@ func sweepExperimentTemplates(region string) error { conn := client.FISClient(ctx) input := &fis.ListExperimentTemplatesInput{} sweepResources := make([]sweep.Sweepable, 0) - var sweeperErrs *multierror.Error - pg := fis.NewListExperimentTemplatesPaginator(conn, input) + pages := fis.NewListExperimentTemplatesPaginator(conn, input) + for pages.HasMorePages() { + page, err := pages.NextPage(ctx) - for pg.HasMorePages() { - page, err := pg.NextPage(ctx) + if awsv2.SkipSweepError(err) { + log.Printf("[WARN] Skipping FIS Experiment Template sweep for %s: %s", region, err) + return nil + } if err != nil { - sweeperErr := fmt.Errorf("error listing FIS Experiment Templates: %w", err) - log.Printf("[ERROR] %s", sweeperErr) - sweeperErrs = multierror.Append(sweeperErrs, sweeperErr) - continue + return fmt.Errorf("error listing FIS Experiment Templates (%s): %w", region, err) } - for _, experimentTemplate := range page.ExperimentTemplates { - r := ResourceExperimentTemplate() + for _, v := range page.ExperimentTemplates { + r := resourceExperimentTemplate() d := r.Data(nil) - d.SetId(aws.ToString(experimentTemplate.Id)) + d.SetId(aws.ToString(v.Id)) sweepResources = append(sweepResources, sweep.NewSweepResource(r, d, client)) } diff --git a/website/docs/r/fis_experiment_template.html.markdown b/website/docs/r/fis_experiment_template.html.markdown index e825d3b5212..e6d9fd8130c 100644 --- a/website/docs/r/fis_experiment_template.html.markdown +++ b/website/docs/r/fis_experiment_template.html.markdown @@ -59,10 +59,18 @@ The following arguments are required: The following arguments are optional: +* `experiment_options` - (Optional) The experiment options for the experiment template. See [experiment_options](#experiment_options) below for more details! * `tags` - (Optional) Key-value mapping of tags. If configured with a provider [`default_tags` configuration block](https://registry.terraform.io/providers/hashicorp/aws/latest/docs#default_tags-configuration-block) present, tags with matching keys will overwrite those defined at the provider-level. * `target` - (Optional) Target of an action. See below. * `log_configuration` - (Optional) The configuration for experiment logging. See below. +### experiment_options + +The `experiment_options` block supports the following: + +* `account_targeting` - (Optional) Specifies the account targeting setting for experiment options. Supports `single-account` and `multi-account`. +* `empty_target_resolution_mode` - (Optional) Specifies the empty target resolution mode for experiment options. Supports `fail` and `skip`. + ### `action` * `action_id` - (Required) ID of the action. To find out what actions are supported see [AWS FIS actions reference](https://docs.aws.amazon.com/fis/latest/userguide/fis-actions-reference.html).