diff --git a/.changelog/18640.txt b/.changelog/18640.txt new file mode 100644 index 00000000000..2978fdcdbf6 --- /dev/null +++ b/.changelog/18640.txt @@ -0,0 +1,3 @@ +```release-note:bug +resource/aws_ssm_parameter: Allow `tags` to be applied to resource when `overwrite` is configured +``` diff --git a/aws/resource_aws_ssm_parameter.go b/aws/resource_aws_ssm_parameter.go index 9bcc1e4e321..b89cb43ce01 100644 --- a/aws/resource_aws_ssm_parameter.go +++ b/aws/resource_aws_ssm_parameter.go @@ -8,11 +8,13 @@ import ( "github.com/aws/aws-sdk-go/aws" "github.com/aws/aws-sdk-go/service/ssm" + "github.com/hashicorp/aws-sdk-go-base/tfawserr" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/customdiff" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/validation" "github.com/terraform-providers/terraform-provider-aws/aws/internal/keyvaluetags" + "github.com/terraform-providers/terraform-provider-aws/aws/internal/tfresource" ) const ( @@ -22,9 +24,9 @@ const ( func resourceAwsSsmParameter() *schema.Resource { return &schema.Resource{ - Create: resourceAwsSsmParameterPut, + Create: resourceAwsSsmParameterCreate, Read: resourceAwsSsmParameterRead, - Update: resourceAwsSsmParameterPut, + Update: resourceAwsSsmParameterUpdate, Delete: resourceAwsSsmParameterDelete, Importer: &schema.ResourceImporter{ State: schema.ImportStatePassthrough, @@ -108,12 +110,70 @@ func resourceAwsSsmParameter() *schema.Resource { } } +func resourceAwsSsmParameterCreate(d *schema.ResourceData, meta interface{}) error { + conn := meta.(*AWSClient).ssmconn + + name := d.Get("name").(string) + + paramInput := &ssm.PutParameterInput{ + Name: aws.String(name), + Type: aws.String(d.Get("type").(string)), + Tier: aws.String(d.Get("tier").(string)), + Value: aws.String(d.Get("value").(string)), + Overwrite: aws.Bool(shouldUpdateSsmParameter(d)), + AllowedPattern: aws.String(d.Get("allowed_pattern").(string)), + } + + if v, ok := d.GetOk("data_type"); ok { + paramInput.DataType = aws.String(v.(string)) + } + + if v, ok := d.GetOk("description"); ok { + paramInput.Description = aws.String(v.(string)) + } + + if keyID, ok := d.GetOk("key_id"); ok && d.Get("type").(string) == ssm.ParameterTypeSecureString { + paramInput.SetKeyId(keyID.(string)) + } + + // AWS SSM Service only supports PutParameter requests with Tags + // iff Overwrite is not provided or is false; in this resource's case, + // the Overwrite value is always set in the paramInput so we check for the value + if v, ok := d.GetOk("tags"); ok && !aws.BoolValue(paramInput.Overwrite) { + paramInput.Tags = keyvaluetags.New(v.(map[string]interface{})).IgnoreAws().SsmTags() + } + + _, err := conn.PutParameter(paramInput) + + if tfawserr.ErrMessageContains(err, "ValidationException", "Tier is not supported") { + paramInput.Tier = nil + _, err = conn.PutParameter(paramInput) + } + + if err != nil { + return fmt.Errorf("error creating SSM parameter (%s): %w", name, err) + } + + // Since the AWS SSM Service does not support PutParameter requests with + // Tags and Overwrite set to true, we make an additional API call + // to Update the resource's tags if necessary + if d.HasChange("tags") && paramInput.Tags == nil { + o, n := d.GetChange("tags") + + if err := keyvaluetags.SsmUpdateTags(conn, name, ssm.ResourceTypeForTaggingParameter, o, n); err != nil { + return fmt.Errorf("error updating SSM Parameter (%s) tags: %w", name, err) + } + } + + d.SetId(name) + + return resourceAwsSsmParameterRead(d, meta) +} + func resourceAwsSsmParameterRead(d *schema.ResourceData, meta interface{}) error { - ssmconn := meta.(*AWSClient).ssmconn + conn := meta.(*AWSClient).ssmconn ignoreTagsConfig := meta.(*AWSClient).IgnoreTagsConfig - log.Printf("[DEBUG] Reading SSM Parameter: %s", d.Id()) - input := &ssm.GetParameterInput{ Name: aws.String(d.Id()), WithDecryption: aws.Bool(true), @@ -122,9 +182,9 @@ func resourceAwsSsmParameterRead(d *schema.ResourceData, meta interface{}) error var resp *ssm.GetParameterOutput err := resource.Retry(ssmParameterCreationValidationTimeout, func() *resource.RetryError { var err error - resp, err = ssmconn.GetParameter(input) + resp, err = conn.GetParameter(input) - if isAWSErr(err, ssm.ErrCodeParameterNotFound, "") && d.IsNewResource() && d.Get("data_type").(string) == "aws:ec2:image" { + if tfawserr.ErrCodeEquals(err, ssm.ErrCodeParameterNotFound) && d.IsNewResource() && d.Get("data_type").(string) == "aws:ec2:image" { return resource.RetryableError(fmt.Errorf("error reading SSM Parameter (%s) after creation: this can indicate that the provided parameter value could not be validated by SSM", d.Id())) } @@ -135,11 +195,11 @@ func resourceAwsSsmParameterRead(d *schema.ResourceData, meta interface{}) error return nil }) - if isResourceTimeoutError(err) { - resp, err = ssmconn.GetParameter(input) + if tfresource.TimedOut(err) { + resp, err = conn.GetParameter(input) } - if isAWSErr(err, ssm.ErrCodeParameterNotFound, "") && !d.IsNewResource() { + if tfawserr.ErrCodeEquals(err, ssm.ErrCodeParameterNotFound) && !d.IsNewResource() { log.Printf("[WARN] SSM Parameter (%s) not found, removing from state", d.Id()) d.SetId("") return nil @@ -150,7 +210,7 @@ func resourceAwsSsmParameterRead(d *schema.ResourceData, meta interface{}) error } param := resp.Parameter - name := *param.Name + name := aws.StringValue(param.Name) d.Set("name", name) d.Set("type", param.Type) d.Set("value", param.Value) @@ -165,9 +225,9 @@ func resourceAwsSsmParameterRead(d *schema.ResourceData, meta interface{}) error }, }, } - describeResp, err := ssmconn.DescribeParameters(describeParamsInput) + describeResp, err := conn.DescribeParameters(describeParamsInput) if err != nil { - return fmt.Errorf("error describing SSM parameter: %w", err) + return fmt.Errorf("error describing SSM parameter (%s): %w", d.Id(), err) } if describeResp == nil || len(describeResp.Parameters) == 0 || describeResp.Parameters[0] == nil { @@ -186,7 +246,7 @@ func resourceAwsSsmParameterRead(d *schema.ResourceData, meta interface{}) error d.Set("allowed_pattern", detail.AllowedPattern) d.Set("data_type", detail.DataType) - tags, err := keyvaluetags.SsmListTags(ssmconn, name, ssm.ResourceTypeForTaggingParameter) + tags, err := keyvaluetags.SsmListTags(conn, name, ssm.ResourceTypeForTaggingParameter) if err != nil { return fmt.Errorf("error listing tags for SSM Parameter (%s): %w", name, err) @@ -201,76 +261,70 @@ func resourceAwsSsmParameterRead(d *schema.ResourceData, meta interface{}) error return nil } -func resourceAwsSsmParameterDelete(d *schema.ResourceData, meta interface{}) error { - ssmconn := meta.(*AWSClient).ssmconn +func resourceAwsSsmParameterUpdate(d *schema.ResourceData, meta interface{}) error { + conn := meta.(*AWSClient).ssmconn + + if d.HasChangesExcept("tags") { + paramInput := &ssm.PutParameterInput{ + Name: aws.String(d.Get("name").(string)), + Type: aws.String(d.Get("type").(string)), + Tier: aws.String(d.Get("tier").(string)), + Value: aws.String(d.Get("value").(string)), + Overwrite: aws.Bool(shouldUpdateSsmParameter(d)), + AllowedPattern: aws.String(d.Get("allowed_pattern").(string)), + } - log.Printf("[INFO] Deleting SSM Parameter: %s", d.Id()) + if d.HasChange("data_type") { + paramInput.DataType = aws.String(d.Get("data_type").(string)) + } - _, err := ssmconn.DeleteParameter(&ssm.DeleteParameterInput{ - Name: aws.String(d.Get("name").(string)), - }) - if err != nil { - return fmt.Errorf("error deleting SSM Parameter (%s): %s", d.Id(), err) - } + if d.HasChange("description") { + paramInput.Description = aws.String(d.Get("description").(string)) + } - return nil -} + if d.HasChange("key_id") && d.Get("type").(string) == ssm.ParameterTypeSecureString { + paramInput.SetKeyId(d.Get("key_id").(string)) + } -func resourceAwsSsmParameterPut(d *schema.ResourceData, meta interface{}) error { - ssmconn := meta.(*AWSClient).ssmconn + _, err := conn.PutParameter(paramInput) - name := d.Get("name").(string) - log.Printf("[INFO] Creating SSM Parameter: %s", name) + if tfawserr.ErrMessageContains(err, "ValidationException", "Tier is not supported") { + paramInput.Tier = nil + _, err = conn.PutParameter(paramInput) + } - paramInput := &ssm.PutParameterInput{ - Name: aws.String(name), - Type: aws.String(d.Get("type").(string)), - Tier: aws.String(d.Get("tier").(string)), - Value: aws.String(d.Get("value").(string)), - Overwrite: aws.Bool(shouldUpdateSsmParameter(d)), - AllowedPattern: aws.String(d.Get("allowed_pattern").(string)), + if err != nil { + return fmt.Errorf("error updating SSM parameter (%s): %w", d.Id(), err) + } } - if v, ok := d.GetOk("data_type"); ok { - paramInput.DataType = aws.String(v.(string)) - } + if d.HasChange("tags") { + o, n := d.GetChange("tags") - if d.HasChange("description") { - _, n := d.GetChange("description") - paramInput.Description = aws.String(n.(string)) + if err := keyvaluetags.SsmUpdateTags(conn, d.Id(), ssm.ResourceTypeForTaggingParameter, o, n); err != nil { + return fmt.Errorf("error updating SSM Parameter (%s) tags: %w", d.Id(), err) + } } - if keyID, ok := d.GetOk("key_id"); ok && d.Get("type").(string) == ssm.ParameterTypeSecureString { - paramInput.SetKeyId(keyID.(string)) - } + return resourceAwsSsmParameterRead(d, meta) +} - if v, ok := d.GetOk("tags"); ok && d.IsNewResource() { - paramInput.Tags = keyvaluetags.New(v.(map[string]interface{})).IgnoreAws().SsmTags() - } +func resourceAwsSsmParameterDelete(d *schema.ResourceData, meta interface{}) error { + conn := meta.(*AWSClient).ssmconn - log.Printf("[DEBUG] Waiting for SSM Parameter %v to be updated", d.Get("name")) - _, err := ssmconn.PutParameter(paramInput) + _, err := conn.DeleteParameter(&ssm.DeleteParameterInput{ + Name: aws.String(d.Get("name").(string)), + }) - if isAWSErr(err, "ValidationException", "Tier is not supported") { - paramInput.Tier = nil - _, err = ssmconn.PutParameter(paramInput) + if tfawserr.ErrCodeEquals(err, ssm.ErrCodeParameterNotFound) { + return nil } if err != nil { - return fmt.Errorf("error creating SSM parameter: %w", err) - } - - if !d.IsNewResource() && d.HasChange("tags") { - o, n := d.GetChange("tags") - - if err := keyvaluetags.SsmUpdateTags(ssmconn, name, ssm.ResourceTypeForTaggingParameter, o, n); err != nil { - return fmt.Errorf("error updating SSM Parameter (%s) tags: %w", name, err) - } + return fmt.Errorf("error deleting SSM Parameter (%s): %s", d.Id(), err) } - d.SetId(name) - - return resourceAwsSsmParameterRead(d, meta) + return nil } func shouldUpdateSsmParameter(d *schema.ResourceData) bool { diff --git a/aws/resource_aws_ssm_parameter_test.go b/aws/resource_aws_ssm_parameter_test.go index 9114e3b19ae..9e1ecc56de6 100644 --- a/aws/resource_aws_ssm_parameter_test.go +++ b/aws/resource_aws_ssm_parameter_test.go @@ -239,6 +239,103 @@ func TestAccAWSSSMParameter_overwrite(t *testing.T) { }) } +// Reference: https://github.com/hashicorp/terraform-provider-aws/issues/18550 +func TestAccAWSSSMParameter_overwriteWithTags(t *testing.T) { + var param ssm.Parameter + rName := fmt.Sprintf("%s_%s", t.Name(), acctest.RandString(10)) + resourceName := "aws_ssm_parameter.test" + + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + ErrorCheck: testAccErrorCheck(t, ssm.EndpointsID), + Providers: testAccProviders, + CheckDestroy: testAccCheckAWSSSMParameterDestroy, + Steps: []resource.TestStep{ + { + Config: testAccAWSSSMParameterConfigOverwriteWithTags1(rName, true, "key1", "value1"), + Check: resource.ComposeTestCheckFunc( + testAccCheckAWSSSMParameterExists(resourceName, ¶m), + resource.TestCheckResourceAttr(resourceName, "tags.%", "1"), + resource.TestCheckResourceAttr(resourceName, "tags.key1", "value1"), + ), + }, + { + ResourceName: resourceName, + ImportState: true, + ImportStateVerify: true, + ImportStateVerifyIgnore: []string{"overwrite"}, + }, + }, + }) +} + +// Reference: https://github.com/hashicorp/terraform-provider-aws/issues/18550 +func TestAccAWSSSMParameter_noOverwriteWithTags(t *testing.T) { + var param ssm.Parameter + rName := fmt.Sprintf("%s_%s", t.Name(), acctest.RandString(10)) + resourceName := "aws_ssm_parameter.test" + + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + ErrorCheck: testAccErrorCheck(t, ssm.EndpointsID), + Providers: testAccProviders, + CheckDestroy: testAccCheckAWSSSMParameterDestroy, + Steps: []resource.TestStep{ + { + Config: testAccAWSSSMParameterConfigOverwriteWithTags1(rName, false, "key1", "value1"), + Check: resource.ComposeTestCheckFunc( + testAccCheckAWSSSMParameterExists(resourceName, ¶m), + resource.TestCheckResourceAttr(resourceName, "tags.%", "1"), + resource.TestCheckResourceAttr(resourceName, "tags.key1", "value1"), + ), + }, + { + ResourceName: resourceName, + ImportState: true, + ImportStateVerify: true, + ImportStateVerifyIgnore: []string{"overwrite"}, + }, + }, + }) +} + +// Reference: https://github.com/hashicorp/terraform-provider-aws/issues/18550 +func TestAccAWSSSMParameter_updateToOverwriteWithTags(t *testing.T) { + var param ssm.Parameter + rName := fmt.Sprintf("%s_%s", t.Name(), acctest.RandString(10)) + resourceName := "aws_ssm_parameter.test" + + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + ErrorCheck: testAccErrorCheck(t, ssm.EndpointsID), + Providers: testAccProviders, + CheckDestroy: testAccCheckAWSSSMParameterDestroy, + Steps: []resource.TestStep{ + { + Config: testAccAWSSSMParameterBasicConfigTags1(rName, "key1", "value1"), + Check: resource.ComposeTestCheckFunc( + testAccCheckAWSSSMParameterExists(resourceName, ¶m), + resource.TestCheckResourceAttr(resourceName, "tags.%", "1"), + resource.TestCheckResourceAttr(resourceName, "tags.key1", "value1"), + ), + }, + { + ResourceName: resourceName, + ImportState: true, + ImportStateVerify: true, + }, + { + Config: testAccAWSSSMParameterConfigOverwriteWithTags1(rName, true, "key1", "value2"), + Check: resource.ComposeTestCheckFunc( + testAccCheckAWSSSMParameterExists(resourceName, ¶m), + resource.TestCheckResourceAttr(resourceName, "tags.%", "1"), + resource.TestCheckResourceAttr(resourceName, "tags.key1", "value2"), + ), + }, + }, + }) +} + func TestAccAWSSSMParameter_tags(t *testing.T) { var param ssm.Parameter rName := fmt.Sprintf("%s_%s", t.Name(), acctest.RandString(10)) @@ -706,6 +803,20 @@ resource "aws_ssm_parameter" "test" { `, rName, pType, value) } +func testAccAWSSSMParameterConfigOverwriteWithTags1(rName string, overwrite bool, tagKey1, tagValue1 string) string { + return fmt.Sprintf(` +resource "aws_ssm_parameter" "test" { + name = %[1]q + overwrite = %[2]t + type = "String" + value = %[1]q + tags = { + %[3]q = %[4]q + } +} +`, rName, overwrite, tagKey1, tagValue1) +} + func testAccAWSSSMParameterSecureConfig(rName string, value string) string { return fmt.Sprintf(` resource "aws_ssm_parameter" "secret_test" {