From 1fe2e3a3f702d0407560491bb82c0f1c23b6c179 Mon Sep 17 00:00:00 2001 From: Iain Adams Date: Fri, 4 Mar 2022 20:35:08 +0000 Subject: [PATCH 1/5] adds sync_compliance attribute to ssm_association resource, fixes #22945 --- .changelog/23515.txt | 3 ++ internal/service/ssm/association.go | 10 ++++ internal/service/ssm/association_test.go | 50 ++++++++++++++++++++ website/docs/r/ssm_association.html.markdown | 1 + 4 files changed, 64 insertions(+) create mode 100644 .changelog/23515.txt diff --git a/.changelog/23515.txt b/.changelog/23515.txt new file mode 100644 index 000000000000..52aeb23c4f8c --- /dev/null +++ b/.changelog/23515.txt @@ -0,0 +1,3 @@ +```release-note:enhancement +resource/aws_ssm_association: Add `sync_compliance` attribute +``` diff --git a/internal/service/ssm/association.go b/internal/service/ssm/association.go index f620c021b64a..b3a8f702918b 100644 --- a/internal/service/ssm/association.go +++ b/internal/service/ssm/association.go @@ -43,6 +43,11 @@ func ResourceAssociation() *schema.Resource { Default: false, Optional: true, }, + "sync_compliance": { + Type: schema.TypeString, + Optional: true, + ValidateFunc: validation.StringInSlice([]string{"AUTO", "MANUAL"}, false), + }, "association_name": { Type: schema.TypeString, Optional: true, @@ -166,6 +171,10 @@ func resourceAssociationCreate(ctx context.Context, d *schema.ResourceData, meta Name: aws.String(d.Get("name").(string)), } + if v, ok := d.GetOk("sync_compliance"); ok { + associationInput.SyncCompliance = aws.String(v.(string)) + } + if v, ok := d.GetOk("apply_only_at_cron_interval"); ok { associationInput.ApplyOnlyAtCronInterval = aws.Bool(v.(bool)) } @@ -260,6 +269,7 @@ func resourceAssociationRead(ctx context.Context, d *schema.ResourceData, meta i Resource: fmt.Sprintf("association/%s", aws.StringValue(association.AssociationId)), }.String() d.Set("arn", arn) + d.Set("sync_compliance", association.SyncCompliance) d.Set("apply_only_at_cron_interval", association.ApplyOnlyAtCronInterval) d.Set("association_name", association.AssociationName) d.Set("instance_id", association.InstanceId) diff --git a/internal/service/ssm/association_test.go b/internal/service/ssm/association_test.go index 5a776894fbf8..676767698010 100644 --- a/internal/service/ssm/association_test.go +++ b/internal/service/ssm/association_test.go @@ -608,6 +608,32 @@ func TestAccSSMAssociation_rateControl(t *testing.T) { }) } +func TestAccSSMAssociation_syncCompliance(t *testing.T) { + rName := "AWS-RunPatchBaselineAssociation" + resourceName := "aws_ssm_association.test" + + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { acctest.PreCheck(t) }, + ErrorCheck: acctest.ErrorCheck(t, ssm.EndpointsID), + ProtoV5ProviderFactories: acctest.ProtoV5ProviderFactories, + CheckDestroy: testAccCheckAssociationDestroy, + Steps: []resource.TestStep{ + { + Config: testAccAssociationSyncComplianceConfig(rName), + Check: resource.ComposeTestCheckFunc( + testAccCheckAssociationExists(resourceName), + resource.TestCheckResourceAttr(resourceName, "sync_compliance", "MANUAL"), + ), + }, + { + ResourceName: resourceName, + ImportState: true, + ImportStateVerify: true, + }, + }, + }) +} + func testAccCheckAssociationExists(ctx context.Context, n string) resource.TestCheckFunc { return func(s *terraform.State) error { rs, ok := s.RootModule().Resources[n] @@ -1569,6 +1595,30 @@ resource "aws_ssm_association" "test" { `, rName, rate) } +func testAccAssociationSyncComplianceConfig(rName string) string { + return fmt.Sprintf(` +resource "aws_ssm_association" "test" { + name = %[1]q + targets { + key = "InstanceIds" + values = ["*"] + } + apply_only_at_cron_interval = false + sync_compliance = "MANUAL" + parameters = { + Operation = "Scan" + RebootOption = "NoReboot" + } + schedule_expression = "cron(0 6 ? * * *)" + lifecycle { + ignore_changes = [ + parameters["AssociationId"] + ] + } +} +`, rName) +} + func testAccAssociationConfig_outputLocationAndWaitForSuccess(rName string) string { return acctest.ConfigCompose( testAccAssociationWithOutputLocationS3RegionConfigBase(rName), diff --git a/website/docs/r/ssm_association.html.markdown b/website/docs/r/ssm_association.html.markdown index 4e4aca7b1ce1..05642c5bd37c 100644 --- a/website/docs/r/ssm_association.html.markdown +++ b/website/docs/r/ssm_association.html.markdown @@ -84,6 +84,7 @@ resource "aws_ssm_association" "example" { The following arguments are supported: * `name` - (Required) The name of the SSM document to apply. +* `sync_compliance` - (Optional) The mode for generating association compliance. You can specify `AUTO` or `MANUAL`. * `apply_only_at_cron_interval` - (Optional) By default, when you create a new or update associations, the system runs it immediately and then according to the schedule you specified. Enable this option if you do not want an association to run immediately after you create or update it. This parameter is not supported for rate expressions. Default: `false`. * `association_name` - (Optional) The descriptive name for the association. * `document_version` - (Optional) The document version you want to associate with the target(s). Can be a specific version or the default version. From f5f2ad60529997484211f0a0eec403d565d29022 Mon Sep 17 00:00:00 2001 From: Adrian Johnson Date: Fri, 1 Sep 2023 12:27:59 -0500 Subject: [PATCH 2/5] aws_ssm_association: fix test --- internal/service/ssm/association_test.go | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/internal/service/ssm/association_test.go b/internal/service/ssm/association_test.go index 98dc388a6bfd..aac0d1e5e18c 100644 --- a/internal/service/ssm/association_test.go +++ b/internal/service/ssm/association_test.go @@ -612,19 +612,20 @@ func TestAccSSMAssociation_rateControl(t *testing.T) { } func TestAccSSMAssociation_syncCompliance(t *testing.T) { + ctx := acctest.Context(t) rName := "AWS-RunPatchBaselineAssociation" resourceName := "aws_ssm_association.test" resource.ParallelTest(t, resource.TestCase{ - PreCheck: func() { acctest.PreCheck(t) }, + PreCheck: func() { acctest.PreCheck(ctx, t) }, ErrorCheck: acctest.ErrorCheck(t, ssm.EndpointsID), ProtoV5ProviderFactories: acctest.ProtoV5ProviderFactories, - CheckDestroy: testAccCheckAssociationDestroy, + CheckDestroy: testAccCheckAssociationDestroy(ctx), Steps: []resource.TestStep{ { Config: testAccAssociationSyncComplianceConfig(rName), Check: resource.ComposeTestCheckFunc( - testAccCheckAssociationExists(resourceName), + testAccCheckAssociationExists(ctx, resourceName), resource.TestCheckResourceAttr(resourceName, "sync_compliance", "MANUAL"), ), }, From 2e91415a928a481ebde1a516e42e83492384edbc Mon Sep 17 00:00:00 2001 From: Adrian Johnson Date: Fri, 1 Sep 2023 12:34:10 -0500 Subject: [PATCH 3/5] aws_ssm_association: tweak attribute order and allow update --- internal/service/ssm/association.go | 28 ++++++++++++++++------------ 1 file changed, 16 insertions(+), 12 deletions(-) diff --git a/internal/service/ssm/association.go b/internal/service/ssm/association.go index 53c5b3ae03e5..66c2715b10b2 100644 --- a/internal/service/ssm/association.go +++ b/internal/service/ssm/association.go @@ -47,11 +47,6 @@ func ResourceAssociation() *schema.Resource { Default: false, Optional: true, }, - "sync_compliance": { - Type: schema.TypeString, - Optional: true, - ValidateFunc: validation.StringInSlice([]string{"AUTO", "MANUAL"}, false), - }, "association_name": { Type: schema.TypeString, Optional: true, @@ -102,6 +97,11 @@ func ResourceAssociation() *schema.Resource { Optional: true, ValidateFunc: validation.StringLenBetween(1, 256), }, + "sync_compliance": { + Type: schema.TypeString, + Optional: true, + ValidateFunc: validation.StringInSlice(ssm.AssociationSyncCompliance_Values(), false), + }, "output_location": { Type: schema.TypeList, MaxItems: 1, @@ -175,10 +175,6 @@ func resourceAssociationCreate(ctx context.Context, d *schema.ResourceData, meta Name: aws.String(d.Get("name").(string)), } - if v, ok := d.GetOk("sync_compliance"); ok { - associationInput.SyncCompliance = aws.String(v.(string)) - } - if v, ok := d.GetOk("apply_only_at_cron_interval"); ok { associationInput.ApplyOnlyAtCronInterval = aws.Bool(v.(bool)) } @@ -195,12 +191,16 @@ func resourceAssociationCreate(ctx context.Context, d *schema.ResourceData, meta associationInput.DocumentVersion = aws.String(v.(string)) } + if v, ok := d.GetOk("parameters"); ok { + associationInput.Parameters = expandDocumentParameters(v.(map[string]interface{})) + } + if v, ok := d.GetOk("schedule_expression"); ok { associationInput.ScheduleExpression = aws.String(v.(string)) } - if v, ok := d.GetOk("parameters"); ok { - associationInput.Parameters = expandDocumentParameters(v.(map[string]interface{})) + if v, ok := d.GetOk("sync_compliance"); ok { + associationInput.SyncCompliance = aws.String(v.(string)) } if v, ok := d.GetOk("targets"); ok { @@ -273,13 +273,13 @@ func resourceAssociationRead(ctx context.Context, d *schema.ResourceData, meta i Resource: fmt.Sprintf("association/%s", aws.StringValue(association.AssociationId)), }.String() d.Set("arn", arn) - d.Set("sync_compliance", association.SyncCompliance) d.Set("apply_only_at_cron_interval", association.ApplyOnlyAtCronInterval) d.Set("association_name", association.AssociationName) d.Set("instance_id", association.InstanceId) d.Set("name", association.Name) d.Set("association_id", association.AssociationId) d.Set("schedule_expression", association.ScheduleExpression) + d.Set("sync_compliance", association.SyncCompliance) d.Set("document_version", association.DocumentVersion) d.Set("compliance_severity", association.ComplianceSeverity) d.Set("max_concurrency", association.MaxConcurrency) @@ -328,6 +328,10 @@ func resourceAssociationUpdate(ctx context.Context, d *schema.ResourceData, meta associationInput.ScheduleExpression = aws.String(v.(string)) } + if d.HasChange("sync_compliance") { + associationInput.SyncCompliance = aws.String(d.Get("sync_compliance").(string)) + } + if v, ok := d.GetOk("parameters"); ok { associationInput.Parameters = expandDocumentParameters(v.(map[string]interface{})) } From a2e498aa4e877b57dfe7564b5ba4d105d54aac73 Mon Sep 17 00:00:00 2001 From: Adrian Johnson Date: Fri, 1 Sep 2023 12:40:58 -0500 Subject: [PATCH 4/5] aws_ssm_association: reorder documentation --- website/docs/r/ssm_association.html.markdown | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/website/docs/r/ssm_association.html.markdown b/website/docs/r/ssm_association.html.markdown index 3ed517575744..670d42a13e6f 100644 --- a/website/docs/r/ssm_association.html.markdown +++ b/website/docs/r/ssm_association.html.markdown @@ -84,19 +84,19 @@ resource "aws_ssm_association" "example" { This resource supports the following arguments: * `name` - (Required) The name of the SSM document to apply. -* `sync_compliance` - (Optional) The mode for generating association compliance. You can specify `AUTO` or `MANUAL`. * `apply_only_at_cron_interval` - (Optional) By default, when you create a new or update associations, the system runs it immediately and then according to the schedule you specified. Enable this option if you do not want an association to run immediately after you create or update it. This parameter is not supported for rate expressions. Default: `false`. * `association_name` - (Optional) The descriptive name for the association. +* `automation_target_parameter_name` - (Optional) Specify the target for the association. This target is required for associations that use an `Automation` document and target resources by using rate controls. This should be set to the SSM document `parameter` that will define how your automation will branch out. +* `compliance_severity` - (Optional) The compliance severity for the association. Can be one of the following: `UNSPECIFIED`, `LOW`, `MEDIUM`, `HIGH` or `CRITICAL` * `document_version` - (Optional) The document version you want to associate with the target(s). Can be a specific version or the default version. * `instance_id` - (Optional, **Deprecated**) The instance ID to apply an SSM document to. Use `targets` with key `InstanceIds` for document schema versions 2.0 and above. Use the `targets` attribute instead. +* `max_concurrency` - (Optional) The maximum number of targets allowed to run the association at the same time. You can specify a number, for example 10, or a percentage of the target set, for example 10%. +* `max_errors` - (Optional) The number of errors that are allowed before the system stops sending requests to run the association on additional targets. You can specify a number, for example 10, or a percentage of the target set, for example 10%. If you specify a threshold of 3, the stop command is sent when the fourth error is returned. If you specify a threshold of 10% for 50 associations, the stop command is sent when the sixth error is returned. * `output_location` - (Optional) An output location block. Output Location is documented below. * `parameters` - (Optional) A block of arbitrary string parameters to pass to the SSM document. * `schedule_expression` - (Optional) A [cron or rate expression](https://docs.aws.amazon.com/systems-manager/latest/userguide/reference-cron-and-rate-expressions.html) that specifies when the association runs. +* `sync_compliance` - (Optional) The mode for generating association compliance. You can specify `AUTO` or `MANUAL`. * `targets` - (Optional) A block containing the targets of the SSM association. Targets are documented below. AWS currently supports a maximum of 5 targets. -* `compliance_severity` - (Optional) The compliance severity for the association. Can be one of the following: `UNSPECIFIED`, `LOW`, `MEDIUM`, `HIGH` or `CRITICAL` -* `max_concurrency` - (Optional) The maximum number of targets allowed to run the association at the same time. You can specify a number, for example 10, or a percentage of the target set, for example 10%. -* `max_errors` - (Optional) The number of errors that are allowed before the system stops sending requests to run the association on additional targets. You can specify a number, for example 10, or a percentage of the target set, for example 10%. If you specify a threshold of 3, the stop command is sent when the fourth error is returned. If you specify a threshold of 10% for 50 associations, the stop command is sent when the sixth error is returned. -* `automation_target_parameter_name` - (Optional) Specify the target for the association. This target is required for associations that use an `Automation` document and target resources by using rate controls. This should be set to the SSM document `parameter` that will define how your automation will branch out. * `wait_for_success_timeout_seconds` - (Optional) The number of seconds to wait for the association status to be `Success`. If `Success` status is not reached within the given time, create opration will fail. Output Location (`output_location`) is an S3 bucket where you want to store the results of this association: From dd31ce79f0141665ec3e18602c415fc80b633163 Mon Sep 17 00:00:00 2001 From: Adrian Johnson Date: Fri, 1 Sep 2023 13:00:17 -0500 Subject: [PATCH 5/5] aws_ssm_association: update test --- internal/service/ssm/association_test.go | 16 +++++++++------- 1 file changed, 9 insertions(+), 7 deletions(-) diff --git a/internal/service/ssm/association_test.go b/internal/service/ssm/association_test.go index aac0d1e5e18c..254278f61f7b 100644 --- a/internal/service/ssm/association_test.go +++ b/internal/service/ssm/association_test.go @@ -623,16 +623,18 @@ func TestAccSSMAssociation_syncCompliance(t *testing.T) { CheckDestroy: testAccCheckAssociationDestroy(ctx), Steps: []resource.TestStep{ { - Config: testAccAssociationSyncComplianceConfig(rName), + Config: testAccAssociationSyncComplianceConfig(rName, "MANUAL"), Check: resource.ComposeTestCheckFunc( testAccCheckAssociationExists(ctx, resourceName), resource.TestCheckResourceAttr(resourceName, "sync_compliance", "MANUAL"), ), }, { - ResourceName: resourceName, - ImportState: true, - ImportStateVerify: true, + Config: testAccAssociationSyncComplianceConfig(rName, "AUTO"), + Check: resource.ComposeTestCheckFunc( + testAccCheckAssociationExists(ctx, resourceName), + resource.TestCheckResourceAttr(resourceName, "sync_compliance", "AUTO"), + ), }, }, }) @@ -1599,7 +1601,7 @@ resource "aws_ssm_association" "test" { `, rName, rate) } -func testAccAssociationSyncComplianceConfig(rName string) string { +func testAccAssociationSyncComplianceConfig(rName, syncCompliance string) string { return fmt.Sprintf(` resource "aws_ssm_association" "test" { name = %[1]q @@ -1608,7 +1610,7 @@ resource "aws_ssm_association" "test" { values = ["*"] } apply_only_at_cron_interval = false - sync_compliance = "MANUAL" + sync_compliance = %[2]q parameters = { Operation = "Scan" RebootOption = "NoReboot" @@ -1620,7 +1622,7 @@ resource "aws_ssm_association" "test" { ] } } -`, rName) +`, rName, syncCompliance) } func testAccAssociationConfig_outputLocationAndWaitForSuccess(rName string) string {