diff --git a/.changelog/32102.txt b/.changelog/32102.txt new file mode 100644 index 00000000000..9cf866f750d --- /dev/null +++ b/.changelog/32102.txt @@ -0,0 +1,3 @@ +```release-note:enhancement +resource/aws_fis_experiment_template: Add `log_configuration` configuration block +``` \ No newline at end of file diff --git a/internal/service/fis/experiment_template.go b/internal/service/fis/experiment_template.go index 3735ee8ef01..304986e5d16 100644 --- a/internal/service/fis/experiment_template.go +++ b/internal/service/fis/experiment_template.go @@ -126,6 +126,49 @@ func ResourceExperimentTemplate() *schema.Resource { Required: true, ValidateFunc: validation.StringLenBetween(0, 512), }, + "log_configuration": { + Type: schema.TypeList, + Optional: true, + MaxItems: 1, + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "cloudwatch_logs_configuration": { + Type: schema.TypeList, + Optional: true, + MaxItems: 1, + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "log_group_arn": { + Type: schema.TypeString, + Required: true, + }, + }, + }, + }, + "log_schema_version": { + Type: schema.TypeInt, + Required: true, + }, + "s3_configuration": { + Type: schema.TypeList, + Optional: true, + MaxItems: 1, + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "bucket_name": { + Type: schema.TypeString, + Required: true, + }, + "prefix": { + Type: schema.TypeString, + Optional: true, + }, + }, + }, + }, + }, + }, + }, "role_arn": { Type: schema.TypeString, Required: true, @@ -236,12 +279,13 @@ func resourceExperimentTemplateCreate(ctx context.Context, d *schema.ResourceDat conn := meta.(*conns.AWSClient).FISClient(ctx) input := &fis.CreateExperimentTemplateInput{ - Actions: expandExperimentTemplateActions(d.Get("action").(*schema.Set)), - ClientToken: aws.String(id.UniqueId()), - Description: aws.String(d.Get("description").(string)), - RoleArn: aws.String(d.Get("role_arn").(string)), - StopConditions: expandExperimentTemplateStopConditions(d.Get("stop_condition").(*schema.Set)), - Tags: getTagsIn(ctx), + Actions: expandExperimentTemplateActions(d.Get("action").(*schema.Set)), + ClientToken: aws.String(id.UniqueId()), + Description: aws.String(d.Get("description").(string)), + LogConfiguration: expandExperimentTemplateLogConfiguration(d.Get("log_configuration").([]interface{})), + RoleArn: aws.String(d.Get("role_arn").(string)), + StopConditions: expandExperimentTemplateStopConditions(d.Get("stop_condition").(*schema.Set)), + Tags: getTagsIn(ctx), } targets, err := expandExperimentTemplateTargets(d.Get("target").(*schema.Set)) @@ -296,6 +340,10 @@ func resourceExperimentTemplateRead(ctx context.Context, d *schema.ResourceData, return create.DiagSettingError(names.FIS, ResNameExperimentTemplate, d.Id(), "action", err) } + if err := d.Set("log_configuration", flattenExperimentTemplateLogConfiguration(experimentTemplate.LogConfiguration)); err != nil { + return create.DiagSettingError(names.FIS, ResNameExperimentTemplate, d.Id(), "log_configuration", err) + } + if err := d.Set("stop_condition", flattenExperimentTemplateStopConditions(experimentTemplate.StopConditions)); err != nil { return create.DiagSettingError(names.FIS, ResNameExperimentTemplate, d.Id(), "stop_condition", err) } @@ -312,37 +360,44 @@ func resourceExperimentTemplateRead(ctx context.Context, d *schema.ResourceData, func resourceExperimentTemplateUpdate(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics { conn := meta.(*conns.AWSClient).FISClient(ctx) - input := &fis.UpdateExperimentTemplateInput{ - Id: aws.String(d.Id()), - } + if d.HasChangesExcept("tags", "tags_all") { + input := &fis.UpdateExperimentTemplateInput{ + Id: aws.String(d.Id()), + } - if d.HasChange("action") { - input.Actions = expandExperimentTemplateActionsForUpdate(d.Get("action").(*schema.Set)) - } + if d.HasChange("action") { + input.Actions = expandExperimentTemplateActionsForUpdate(d.Get("action").(*schema.Set)) + } - if d.HasChange("description") { - input.Description = aws.String(d.Get("description").(string)) - } + if d.HasChange("description") { + input.Description = aws.String(d.Get("description").(string)) + } - if d.HasChange("role_arn") { - input.RoleArn = aws.String(d.Get("role_arn").(string)) - } + if d.HasChange("log_configuration") { + config := expandExperimentTemplateLogConfigurationForUpdate(d.Get("log_configuration").([]interface{})) + input.LogConfiguration = config + } - if d.HasChange("stop_condition") { - input.StopConditions = expandExperimentTemplateStopConditionsForUpdate(d.Get("stop_condition").(*schema.Set)) - } + if d.HasChange("role_arn") { + input.RoleArn = aws.String(d.Get("role_arn").(string)) + } + + if d.HasChange("stop_condition") { + input.StopConditions = expandExperimentTemplateStopConditionsForUpdate(d.Get("stop_condition").(*schema.Set)) + } - if d.HasChange("target") { - targets, err := expandExperimentTemplateTargetsForUpdate(d.Get("target").(*schema.Set)) + if d.HasChange("target") { + targets, err := expandExperimentTemplateTargetsForUpdate(d.Get("target").(*schema.Set)) + if err != nil { + return create.DiagError(names.FIS, create.ErrActionUpdating, ResNameExperimentTemplate, d.Id(), err) + } + input.Targets = targets + } + + _, err := conn.UpdateExperimentTemplate(ctx, input) if err != nil { return create.DiagError(names.FIS, create.ErrActionUpdating, ResNameExperimentTemplate, d.Id(), err) } - input.Targets = targets - } - - _, err := conn.UpdateExperimentTemplate(ctx, input) - if err != nil { - return create.DiagError(names.FIS, create.ErrActionUpdating, ResNameExperimentTemplate, d.Id(), err) } return resourceExperimentTemplateRead(ctx, d, meta) @@ -473,6 +528,58 @@ func expandExperimentTemplateStopConditions(l *schema.Set) []types.CreateExperim return items } +func expandExperimentTemplateLogConfiguration(l []interface{}) *types.CreateExperimentTemplateLogConfigurationInput { + if len(l) == 0 { + return nil + } + + raw := l[0].(map[string]interface{}) + + config := types.CreateExperimentTemplateLogConfigurationInput{ + LogSchemaVersion: aws.Int32(int32(raw["log_schema_version"].(int))), + } + + if v, ok := raw["cloudwatch_logs_configuration"].([]interface{}); ok && len(v) > 0 { + config.CloudWatchLogsConfiguration = expandExperimentTemplateCloudWatchLogsConfiguration(v) + } + + if v, ok := raw["s3_configuration"].([]interface{}); ok && len(v) > 0 { + config.S3Configuration = expandExperimentTemplateS3Configuration(v) + } + + return &config +} + +func expandExperimentTemplateCloudWatchLogsConfiguration(l []interface{}) *types.ExperimentTemplateCloudWatchLogsLogConfigurationInput { + if len(l) == 0 { + return nil + } + + raw := l[0].(map[string]interface{}) + + config := types.ExperimentTemplateCloudWatchLogsLogConfigurationInput{ + LogGroupArn: aws.String(raw["log_group_arn"].(string)), + } + return &config +} + +func expandExperimentTemplateS3Configuration(l []interface{}) *types.ExperimentTemplateS3LogConfigurationInput { + if len(l) == 0 { + return nil + } + + raw := l[0].(map[string]interface{}) + + config := types.ExperimentTemplateS3LogConfigurationInput{ + BucketName: aws.String(raw["bucket_name"].(string)), + } + if v, ok := raw["prefix"].(string); ok && v != "" { + config.Prefix = aws.String(v) + } + + return &config +} + func expandExperimentTemplateStopConditionsForUpdate(l *schema.Set) []types.UpdateExperimentTemplateStopConditionInput { if l.Len() == 0 { return nil @@ -603,6 +710,26 @@ func expandExperimentTemplateTargetsForUpdate(l *schema.Set) (map[string]types.U return attrs, nil } +func expandExperimentTemplateLogConfigurationForUpdate(l []interface{}) *types.UpdateExperimentTemplateLogConfigurationInput { + if len(l) == 0 { + return &types.UpdateExperimentTemplateLogConfigurationInput{} + } + + raw := l[0].(map[string]interface{}) + config := types.UpdateExperimentTemplateLogConfigurationInput{ + LogSchemaVersion: aws.Int32(int32(raw["log_schema_version"].(int))), + } + if v, ok := raw["cloudwatch_logs_configuration"].([]interface{}); ok && len(v) > 0 { + config.CloudWatchLogsConfiguration = expandExperimentTemplateCloudWatchLogsConfiguration(v) + } + + if v, ok := raw["s3_configuration"].([]interface{}); ok && len(v) > 0 { + config.S3Configuration = expandExperimentTemplateS3Configuration(v) + } + + return &config +} + func expandExperimentTemplateActionParameteres(l *schema.Set) map[string]string { if l.Len() == 0 { return nil @@ -734,6 +861,47 @@ func flattenExperimentTemplateTargets(configured map[string]types.ExperimentTemp return dataResources } +func flattenExperimentTemplateLogConfiguration(configured *types.ExperimentTemplateLogConfiguration) []map[string]interface{} { + if configured == nil { + return make([]map[string]interface{}, 0) + } + + dataResources := make([]map[string]interface{}, 1) + dataResources[0] = make(map[string]interface{}) + dataResources[0]["log_schema_version"] = configured.LogSchemaVersion + dataResources[0]["cloudwatch_logs_configuration"] = flattenCloudWatchLogsConfiguration(configured.CloudWatchLogsConfiguration) + dataResources[0]["s3_configuration"] = flattenS3Configuration(configured.S3Configuration) + + return dataResources +} + +func flattenCloudWatchLogsConfiguration(configured *types.ExperimentTemplateCloudWatchLogsLogConfiguration) []map[string]interface{} { + if configured == nil { + return make([]map[string]interface{}, 0) + } + + dataResources := make([]map[string]interface{}, 1) + dataResources[0] = make(map[string]interface{}) + dataResources[0]["log_group_arn"] = configured.LogGroupArn + + return dataResources +} + +func flattenS3Configuration(configured *types.ExperimentTemplateS3LogConfiguration) []map[string]interface{} { + if configured == nil { + return make([]map[string]interface{}, 0) + } + + dataResources := make([]map[string]interface{}, 1) + dataResources[0] = make(map[string]interface{}) + dataResources[0]["bucket_name"] = configured.BucketName + if aws.ToString(configured.Prefix) != "" { + dataResources[0]["prefix"] = configured.Prefix + } + + return dataResources +} + func flattenExperimentTemplateActionParameters(configured map[string]string) []map[string]interface{} { dataResources := make([]map[string]interface{}, 0, len(configured)) diff --git a/internal/service/fis/experiment_template_test.go b/internal/service/fis/experiment_template_test.go index 24ce85f944f..a51eb2cec1f 100644 --- a/internal/service/fis/experiment_template_test.go +++ b/internal/service/fis/experiment_template_test.go @@ -313,6 +313,57 @@ func TestAccFISExperimentTemplate_ebs(t *testing.T) { }) } +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 + + 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{ + // Cloudwatch Logging + { + Config: testAccExperimentTemplateConfig_logConfigCloudWatch(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( + testAccExperimentTemplateExists(ctx, resourceName, &conf), + resource.TestCheckResourceAttr(resourceName, "log_configuration.0.log_schema_version", "2"), + acctest.CheckResourceAttrRegionalARN(resourceName, "log_configuration.0.cloudwatch_logs_configuration.0.log_group_arn", "logs", fmt.Sprintf("log-group:%s:*", rName)), + ), + }, + // Delete Logging + { + 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( + testAccExperimentTemplateExists(ctx, resourceName, &conf), + ), + }, + // S3 Logging + { + Config: testAccExperimentTemplateConfig_logConfigS3(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( + testAccExperimentTemplateExists(ctx, resourceName, &conf), + resource.TestCheckResourceAttr(resourceName, "log_configuration.0.log_schema_version", "2"), + resource.TestCheckResourceAttr(resourceName, "log_configuration.0.s3_configuration.0.bucket_name", rName), + resource.TestCheckResourceAttr(resourceName, "log_configuration.0.s3_configuration.0.prefix", ""), + ), + }, + { + Config: testAccExperimentTemplateConfig_logConfigS3Prefix(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( + testAccExperimentTemplateExists(ctx, resourceName, &conf), + resource.TestCheckResourceAttr(resourceName, "log_configuration.0.log_schema_version", "2"), + resource.TestCheckResourceAttr(resourceName, "log_configuration.0.s3_configuration.0.bucket_name", rName), + resource.TestCheckResourceAttr(resourceName, "log_configuration.0.s3_configuration.0.prefix", "test"), + ), + }, + }, + }) +} + func testAccExperimentTemplateExists(ctx context.Context, resourceName string, config *types.ExperimentTemplate) resource.TestCheckFunc { return func(s *terraform.State) error { rs, ok := s.RootModule().Resources[resourceName] @@ -695,3 +746,214 @@ resource "aws_fis_experiment_template" "test" { } `, rName+"-fis", desc, actionName, actionDesc, actionID, actionTargetK, actionTargetV, paramK1, paramV1, targetResType, targetSelectMode, targetResTagK, targetResTagV)) } + +func testAccExperimentTemplateConfig_logConfigCloudWatch(rName, desc, actionName, actionDesc, actionID, actionTargetK, actionTargetV, targetResType, targetSelectMode, targetResTagK, targetResTagV 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_cloudwatch_log_group" "test" { + name = %[1]q +} + +resource "aws_fis_experiment_template" "test" { + description = %[2]q + role_arn = aws_iam_role.test.arn + + stop_condition { + source = "none" + } + + action { + name = %[3]q + description = %[4]q + action_id = %[5]q + + target { + key = %[6]q + value = %[7]q + } + } + + target { + name = %[7]q + resource_type = %[8]q + selection_mode = %[9]q + + resource_tag { + key = %[10]q + value = %[11]q + } + } + + log_configuration { + log_schema_version = 2 + + cloudwatch_logs_configuration { + log_group_arn = "${aws_cloudwatch_log_group.test.arn}:*" + } + } + + tags = { + Name = %[1]q + } +} +`, rName, desc, actionName, actionDesc, actionID, actionTargetK, actionTargetV, targetResType, targetSelectMode, targetResTagK, targetResTagV) +} + +func testAccExperimentTemplateConfig_logConfigS3(rName, desc, actionName, actionDesc, actionID, actionTargetK, actionTargetV, targetResType, targetSelectMode, targetResTagK, targetResTagV 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_s3_bucket" "test" { + bucket = %[1]q +} + +resource "aws_fis_experiment_template" "test" { + description = %[2]q + role_arn = aws_iam_role.test.arn + + stop_condition { + source = "none" + } + + action { + name = %[3]q + description = %[4]q + action_id = %[5]q + + target { + key = %[6]q + value = %[7]q + } + } + + target { + name = %[7]q + resource_type = %[8]q + selection_mode = %[9]q + + resource_tag { + key = %[10]q + value = %[11]q + } + } + + log_configuration { + log_schema_version = 2 + + s3_configuration { + bucket_name = aws_s3_bucket.test.bucket + } + } + + tags = { + Name = %[1]q + } +} +`, rName, desc, actionName, actionDesc, actionID, actionTargetK, actionTargetV, targetResType, targetSelectMode, targetResTagK, targetResTagV) +} + +func testAccExperimentTemplateConfig_logConfigS3Prefix(rName, desc, actionName, actionDesc, actionID, actionTargetK, actionTargetV, targetResType, targetSelectMode, targetResTagK, targetResTagV 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_s3_bucket" "test" { + bucket = %[1]q +} + +resource "aws_fis_experiment_template" "test" { + description = %[2]q + role_arn = aws_iam_role.test.arn + + stop_condition { + source = "none" + } + + action { + name = %[3]q + description = %[4]q + action_id = %[5]q + + target { + key = %[6]q + value = %[7]q + } + } + + target { + name = %[7]q + resource_type = %[8]q + selection_mode = %[9]q + + resource_tag { + key = %[10]q + value = %[11]q + } + } + + log_configuration { + log_schema_version = 2 + + s3_configuration { + bucket_name = aws_s3_bucket.test.bucket + prefix = "test" + } + } + + tags = { + Name = %[1]q + } +} +`, rName, desc, actionName, actionDesc, actionID, actionTargetK, actionTargetV, targetResType, targetSelectMode, targetResTagK, targetResTagV) +} diff --git a/website/docs/r/fis_experiment_template.html.markdown b/website/docs/r/fis_experiment_template.html.markdown index b508c6442f5..fd1e7765830 100644 --- a/website/docs/r/fis_experiment_template.html.markdown +++ b/website/docs/r/fis_experiment_template.html.markdown @@ -61,6 +61,7 @@ The following arguments are optional: * `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. ### `action` @@ -111,6 +112,21 @@ For a list of parameters supported by each action, see [AWS FIS actions referenc * `key` - (Required) Tag key. * `value` - (Required) Tag value. +### `log_configuration` + +* `log_schema_version` - (Required) The schema version. See [documentation](https://docs.aws.amazon.com/fis/latest/userguide/monitoring-logging.html#experiment-log-schema) for the list of schema versions. +* `cloudwatch_logs_configuration` - (Optional) The configuration for experiment logging to Amazon CloudWatch Logs. See below. +* `s3_configuration` - (Optional) The configuration for experiment logging to Amazon S3. See below. + +#### `cloudwatch_logs_configuration` + +* `log_group_arn` - (Required) The Amazon Resource Name (ARN) of the destination Amazon CloudWatch Logs log group. + +#### `s3_configuration` + +* `bucket_name` - (Required) The name of the destination bucket. +* `prefix` - (Optional) The bucket prefix. + ## Attributes Reference In addition to all arguments above, the following attributes are exported: