// Copyright IBM Corp. 2014, 2026 // SPDX-License-Identifier: MPL-2.0 // DONOTCOPY: Copying old resources spreads bad habits. Use skaff instead. package ssm import ( "context" "log" "strings" "time" "github.com/aws/aws-sdk-go-v2/aws" "github.com/aws/aws-sdk-go-v2/aws/arn" "github.com/aws/aws-sdk-go-v2/service/ssm" awstypes "github.com/aws/aws-sdk-go-v2/service/ssm/types" "github.com/hashicorp/aws-sdk-go-base/v2/tfawserr" "github.com/hashicorp/go-cty/cty" "github.com/hashicorp/terraform-plugin-sdk/v2/diag" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/customdiff" "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/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" "github.com/hashicorp/terraform-provider-aws/internal/provider/sdkv2/importer" "github.com/hashicorp/terraform-provider-aws/internal/retry" tftags "github.com/hashicorp/terraform-provider-aws/internal/tags" "github.com/hashicorp/terraform-provider-aws/internal/tfresource" "github.com/hashicorp/terraform-provider-aws/names" ) // @SDKResource("aws_ssm_parameter", name="Parameter") // @Tags(identifierAttribute="name", resourceType="Parameter") // @Testing(existsType="github.com/aws/aws-sdk-go-v2/service/ssm/types;awstypes;awstypes.Parameter") // @IdentityAttribute("name") // @Testing(idAttrDuplicates="name") // @Testing(preIdentityVersion="v6.7.0") // @Testing(plannableImportAction="NoOp") // @CustomImport // @Testing(identityTestCases="basic;leadingslash;path") func resourceParameter() *schema.Resource { return &schema.Resource{ CreateWithoutTimeout: resourceParameterCreate, ReadWithoutTimeout: resourceParameterRead, UpdateWithoutTimeout: resourceParameterUpdate, DeleteWithoutTimeout: resourceParameterDelete, Importer: &schema.ResourceImporter{ StateContext: func(ctx context.Context, d *schema.ResourceData, meta any) ([]*schema.ResourceData, error) { // We allow importing by ARN and versioned parameter in addition to named parameter if id := d.Id(); id != "" { if arn.IsARN(id) { paramARN, err := arn.Parse(id) if err != nil { return nil, err } id = paramARN.Resource // The resource part of the ARN is "parameter/", where "" is either a bare string // or a path with leading slash. If there are any slashes in "" (after the separating slash), // we need to keep the leading slash, otherwise we must remove it. id = strings.TrimPrefix(id, names.AttrParameter) if !strings.Contains(id[1:], "/") { id = strings.TrimPrefix(id, "/") } d.SetId(id) } } if err := importer.Import(ctx, d, meta); err != nil { return nil, err } return []*schema.ResourceData{d}, nil }, }, SchemaFunc: func() map[string]*schema.Schema { return map[string]*schema.Schema{ "allowed_pattern": { Type: schema.TypeString, Optional: true, ValidateFunc: validation.StringLenBetween(0, 1024), }, names.AttrARN: { Type: schema.TypeString, Optional: true, Computed: true, }, "data_type": { Type: schema.TypeString, Optional: true, Computed: true, ValidateFunc: validation.StringInSlice([]string{ "aws:ec2:image", "aws:ssm:integration", "text", }, false), ForceNew: true, }, names.AttrDescription: { Type: schema.TypeString, Optional: true, ValidateFunc: validation.StringLenBetween(0, 1024), }, "has_value_wo": { Type: schema.TypeBool, Computed: true, }, "insecure_value": { Type: schema.TypeString, Optional: true, Computed: true, ExactlyOneOf: []string{"value_wo", "insecure_value", names.AttrValue}, }, names.AttrKeyID: { Type: schema.TypeString, Optional: true, Computed: true, }, names.AttrName: { Type: schema.TypeString, Required: true, ForceNew: true, ValidateFunc: validation.StringLenBetween(1, 2048), }, "overwrite": { Type: schema.TypeBool, Optional: true, }, names.AttrTags: tftags.TagsSchema(), names.AttrTagsAll: tftags.TagsSchemaComputed(), "tier": { Type: schema.TypeString, Optional: true, Computed: true, ValidateDiagFunc: enum.Validate[awstypes.ParameterTier](), DiffSuppressFunc: func(k, old, new string, d *schema.ResourceData) bool { if old != "" { return awstypes.ParameterTier(new) == awstypes.ParameterTierIntelligentTiering } return false }, }, names.AttrType: { Type: schema.TypeString, Required: true, ValidateDiagFunc: enum.Validate[awstypes.ParameterType](), }, names.AttrValue: { Type: schema.TypeString, Optional: true, Sensitive: true, Computed: true, ExactlyOneOf: []string{"value_wo", "insecure_value", names.AttrValue}, }, "value_wo": { Type: schema.TypeString, Optional: true, Sensitive: true, WriteOnly: true, ExactlyOneOf: []string{"value_wo", "insecure_value", names.AttrValue}, RequiredWith: []string{"value_wo_version"}, }, "value_wo_version": { Type: schema.TypeInt, Optional: true, RequiredWith: []string{"value_wo"}, }, names.AttrVersion: { Type: schema.TypeInt, Computed: true, }, } }, CustomizeDiff: customdiff.Sequence( // Prevent the following error during tier update from Advanced to Standard: // ValidationException: This parameter uses the advanced-parameter tier. You can't downgrade a parameter from the advanced-parameter tier to the standard-parameter tier. If necessary, you can delete the advanced parameter and recreate it as a standard parameter. customdiff.ForceNewIfChange("tier", func(_ context.Context, old, new, meta any) bool { return awstypes.ParameterTier(old.(string)) == awstypes.ParameterTierAdvanced && awstypes.ParameterTier(new.(string)) == awstypes.ParameterTierStandard }), customdiff.ComputedIf(names.AttrVersion, func(_ context.Context, diff *schema.ResourceDiff, meta any) bool { return diff.HasChange(names.AttrValue) || !diff.NewValueKnown(names.AttrValue) || diff.HasChange(names.AttrDescription) }), customdiff.ComputedIf(names.AttrValue, func(_ context.Context, diff *schema.ResourceDiff, meta any) bool { return diff.HasChange("insecure_value") }), customdiff.ComputedIf("insecure_value", func(_ context.Context, diff *schema.ResourceDiff, meta any) bool { if diff.NewValueKnown("insecure_value") { return false } return diff.HasChange(names.AttrValue) || !diff.NewValueKnown(names.AttrValue) }), customdiff.ComputedIf("has_value_wo", func(_ context.Context, diff *schema.ResourceDiff, meta any) bool { return diff.HasChange("value_wo_version") || !diff.NewValueKnown("value_wo_version") }), ), } } func resourceParameterCreate(ctx context.Context, d *schema.ResourceData, meta any) diag.Diagnostics { var diags diag.Diagnostics conn := meta.(*conns.AWSClient).SSMClient(ctx) name := d.Get(names.AttrName).(string) typ := awstypes.ParameterType(d.Get(names.AttrType).(string)) value := d.Get(names.AttrValue).(string) if v, ok := d.Get("insecure_value").(string); ok && v != "" { value = v } valueWO, di := flex.GetWriteOnlyStringValue(d, cty.GetAttrPath("value_wo")) diags = append(diags, di...) if diags.HasError() { return diags } if valueWO != "" { value = valueWO } input := &ssm.PutParameterInput{ AllowedPattern: aws.String(d.Get("allowed_pattern").(string)), Name: aws.String(name), Overwrite: aws.Bool(shouldUpdateParameter(d)), Type: typ, Value: aws.String(value), } if v, ok := d.GetOk("data_type"); ok { input.DataType = aws.String(v.(string)) } if v, ok := d.GetOk(names.AttrDescription); ok { input.Description = aws.String(v.(string)) } if v, ok := d.GetOk(names.AttrKeyID); ok && typ == awstypes.ParameterTypeSecureString { input.KeyId = aws.String(v.(string)) } if v, ok := d.GetOk("tier"); ok { input.Tier = awstypes.ParameterTier(v.(string)) } // AWS SSM Service only supports PutParameter requests with Tags // if 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 tags := getTagsIn(ctx) if !aws.ToBool(input.Overwrite) { input.Tags = tags } _, err := conn.PutParameter(ctx, input) if tfawserr.ErrMessageContains(err, errCodeValidationException, "Tier is not supported") { log.Printf("[WARN] Creating SSM Parameter (%s): tier %q not supported, using default", name, d.Get("tier").(string)) input.Tier = "" _, err = conn.PutParameter(ctx, input) } if err != nil { return sdkdiag.AppendErrorf(diags, "creating SSM Parameter (%s): %s", 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 len(tags) > 0 && input.Tags == nil { if err := createTags(ctx, conn, name, string(awstypes.ResourceTypeForTaggingParameter), tags); err != nil { return sdkdiag.AppendErrorf(diags, "setting SSM Parameter (%s) tags: %s", name, err) } } d.SetId(name) return append(diags, resourceParameterRead(ctx, d, meta)...) } func resourceParameterRead(ctx context.Context, d *schema.ResourceData, meta any) diag.Diagnostics { var diags diag.Diagnostics conn := meta.(*conns.AWSClient).SSMClient(ctx) const ( // Maximum amount of time to wait for asynchronous validation on SSM Parameter creation. timeout = 2 * time.Minute ) outputRaw, err := tfresource.RetryWhen(ctx, timeout, func(ctx context.Context) (any, error) { return findParameterByName(ctx, conn, d.Id(), true) }, func(err error) (bool, error) { if d.IsNewResource() && retry.NotFound(err) && d.Get("data_type").(string) == "aws:ec2:image" { return true, err } return false, err }, ) if !d.IsNewResource() && retry.NotFound(err) { log.Printf("[WARN] SSM Parameter %s not found, removing from state", d.Id()) d.SetId("") return diags } if err != nil { return sdkdiag.AppendErrorf(diags, "reading SSM Parameter (%s): %s", d.Id(), err) } param := outputRaw.(*awstypes.Parameter) paramMetadata, err := findParameterMetadataByName(ctx, conn, aws.ToString(param.Name)) if !d.IsNewResource() && retry.NotFound(err) { log.Printf("[WARN] SSM Parameter %s not found, removing from state", d.Id()) d.SetId("") return diags } if err != nil { return sdkdiag.AppendErrorf(diags, "reading SSM Parameter metadata (%s): %s", d.Id(), err) } resourceParameterFlatten(d, param, paramMetadata) hasWriteOnly := d.Get("has_value_wo").(bool) rawConfig := d.GetRawConfig() if !rawConfig.IsNull() { valueWO, di := flex.GetWriteOnlyStringValue(d, cty.GetAttrPath("value_wo")) diags = append(diags, di...) if diags.HasError() { return diags } if valueWO != "" { hasWriteOnly = true } else { hasWriteOnly = false } } if hasWriteOnly { d.Set("has_value_wo", true) d.Set(names.AttrValue, nil) } else { d.Set("has_value_wo", nil) if _, ok := d.GetOk("insecure_value"); ok && param.Type != awstypes.ParameterTypeSecureString { d.Set("insecure_value", param.Value) } else { d.Set(names.AttrValue, param.Value) } } if param.Type == awstypes.ParameterTypeSecureString && d.Get("insecure_value").(string) != "" { return sdkdiag.AppendErrorf(diags, "invalid configuration, cannot set type = %s and insecure_value", param.Type) } return diags } func resourceParameterFlatten(d *schema.ResourceData, param *awstypes.Parameter, paramMetadata *awstypes.ParameterMetadata) { d.Set(names.AttrARN, param.ARN) d.Set(names.AttrName, param.Name) d.Set(names.AttrType, param.Type) d.Set(names.AttrVersion, param.Version) d.Set("allowed_pattern", paramMetadata.AllowedPattern) d.Set("data_type", paramMetadata.DataType) d.Set(names.AttrDescription, paramMetadata.Description) d.Set(names.AttrKeyID, paramMetadata.KeyId) d.Set("tier", paramMetadata.Tier) } func resourceParameterUpdate(ctx context.Context, d *schema.ResourceData, meta any) diag.Diagnostics { var diags diag.Diagnostics conn := meta.(*conns.AWSClient).SSMClient(ctx) if d.HasChangesExcept("overwrite", names.AttrTags, names.AttrTagsAll) { typ := awstypes.ParameterType(d.Get(names.AttrType).(string)) value := d.Get(names.AttrValue).(string) if v, ok := d.Get("insecure_value").(string); ok && v != "" { value = v } if d.HasChanges("value_wo_version") { valueWO, di := flex.GetWriteOnlyStringValue(d, cty.GetAttrPath("value_wo")) diags = append(diags, di...) if diags.HasError() { return diags } if valueWO != "" { value = valueWO } } input := &ssm.PutParameterInput{ AllowedPattern: aws.String(d.Get("allowed_pattern").(string)), Name: aws.String(d.Id()), Overwrite: aws.Bool(shouldUpdateParameter(d)), Tier: awstypes.ParameterTier(d.Get("tier").(string)), Type: typ, Value: aws.String(value), } if d.HasChange("data_type") { input.DataType = aws.String(d.Get("data_type").(string)) } if d.HasChange(names.AttrDescription) { input.Description = aws.String(d.Get(names.AttrDescription).(string)) } if d.HasChange(names.AttrKeyID) && typ == awstypes.ParameterTypeSecureString { input.KeyId = aws.String(d.Get(names.AttrKeyID).(string)) } // Retrieve the value set in the config directly to counteract the DiffSuppressFunc above. if v := d.GetRawConfig().GetAttr("tier"); v.IsKnown() && !v.IsNull() { input.Tier = awstypes.ParameterTier(v.AsString()) } _, err := conn.PutParameter(ctx, input) if tfawserr.ErrMessageContains(err, errCodeValidationException, "Tier is not supported") { log.Printf("[WARN] Creating SSM Parameter (%s): tier %q not supported, using default", d.Id(), d.Get("tier").(string)) input.Tier = "" _, err = conn.PutParameter(ctx, input) } if err != nil { return sdkdiag.AppendErrorf(diags, "updating SSM Parameter (%s): %s", d.Id(), err) } } return append(diags, resourceParameterRead(ctx, d, meta)...) } func resourceParameterDelete(ctx context.Context, d *schema.ResourceData, meta any) diag.Diagnostics { var diags diag.Diagnostics conn := meta.(*conns.AWSClient).SSMClient(ctx) log.Printf("[DEBUG] Deleting SSM Parameter: %s", d.Id()) _, err := conn.DeleteParameter(ctx, &ssm.DeleteParameterInput{ // Use "name" instead of "id" in case the resource was imported by ARN. Name: aws.String(d.Get(names.AttrName).(string)), }) if errs.IsA[*awstypes.ParameterNotFound](err) { return diags } if err != nil { return sdkdiag.AppendErrorf(diags, "deleting SSM Parameter (%s): %s", d.Id(), err) } return diags } func findParameterByName(ctx context.Context, conn *ssm.Client, name string, withDecryption bool) (*awstypes.Parameter, error) { input := &ssm.GetParameterInput{ Name: aws.String(name), WithDecryption: aws.Bool(withDecryption), } output, err := conn.GetParameter(ctx, input) if errs.IsA[*awstypes.ParameterNotFound](err) { return nil, &retry.NotFoundError{ LastError: err, } } if err != nil { return nil, err } if output == nil || output.Parameter == nil { return nil, tfresource.NewEmptyResultError() } return output.Parameter, nil } func findParameterMetadataByName(ctx context.Context, conn *ssm.Client, name string) (*awstypes.ParameterMetadata, error) { input := &ssm.DescribeParametersInput{ ParameterFilters: []awstypes.ParameterStringFilter{ { Key: aws.String("Name"), Option: aws.String("Equals"), Values: []string{name}, }, }, } return findParameterMetadata(ctx, conn, input) } func findParameterMetadata(ctx context.Context, conn *ssm.Client, input *ssm.DescribeParametersInput) (*awstypes.ParameterMetadata, error) { output, err := findParametersMetadata(ctx, conn, input) if err != nil { return nil, err } return tfresource.AssertSingleValueResult(output) } func findParametersMetadata(ctx context.Context, conn *ssm.Client, input *ssm.DescribeParametersInput) ([]awstypes.ParameterMetadata, error) { var output []awstypes.ParameterMetadata pages := ssm.NewDescribeParametersPaginator(conn, input) for pages.HasMorePages() { page, err := pages.NextPage(ctx) if err != nil { return nil, err } output = append(output, page.Parameters...) } return output, nil } func shouldUpdateParameter(d *schema.ResourceData) bool { // If the user has specified a preference, return their preference. if v := d.GetRawConfig().GetAttr("overwrite"); v.IsKnown() && !v.IsNull() { return v.True() } // Since the user has not specified a preference, obey lifecycle rules // if it is not a new resource, otherwise overwrite should be set to false. return !d.IsNewResource() }