diff --git a/.changelog/40313.txt b/.changelog/40313.txt new file mode 100644 index 00000000000..915aa753474 --- /dev/null +++ b/.changelog/40313.txt @@ -0,0 +1,3 @@ +```release-note:new-ephemeral +aws_ssm_parameter +``` diff --git a/internal/service/ssm/parameter_ephemeral.go b/internal/service/ssm/parameter_ephemeral.go new file mode 100644 index 00000000000..0e619062296 --- /dev/null +++ b/internal/service/ssm/parameter_ephemeral.go @@ -0,0 +1,107 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package ssm + +import ( + "context" + + "github.com/hashicorp/terraform-plugin-framework/ephemeral" + "github.com/hashicorp/terraform-plugin-framework/ephemeral/schema" + "github.com/hashicorp/terraform-plugin-framework/types" + "github.com/hashicorp/terraform-provider-aws/internal/create" + "github.com/hashicorp/terraform-provider-aws/internal/framework" + fwflex "github.com/hashicorp/terraform-provider-aws/internal/framework/flex" + fwtypes "github.com/hashicorp/terraform-provider-aws/internal/framework/types" + "github.com/hashicorp/terraform-provider-aws/names" +) + +const ( + ERNameParameter = "Ephemeral Resource Parameter" +) + +// @EphemeralResource(aws_ssm_parameter, name="Parameter") +func newEphemeralParameter(_ context.Context) (ephemeral.EphemeralResourceWithConfigure, error) { + return &ephemeralParameter{}, nil +} + +type ephemeralParameter struct { + framework.EphemeralResourceWithConfigure +} + +func (e *ephemeralParameter) Metadata(_ context.Context, _ ephemeral.MetadataRequest, response *ephemeral.MetadataResponse) { + response.TypeName = "aws_ssm_parameter" +} + +func (e *ephemeralParameter) Schema(ctx context.Context, _ ephemeral.SchemaRequest, response *ephemeral.SchemaResponse) { + response.Schema = schema.Schema{ + Attributes: map[string]schema.Attribute{ + names.AttrARN: schema.StringAttribute{ + CustomType: fwtypes.ARNType, + Required: true, + }, + names.AttrName: schema.StringAttribute{ + Computed: true, + }, + names.AttrType: schema.StringAttribute{ + Computed: true, + }, + names.AttrValue: schema.StringAttribute{ + Computed: true, + Sensitive: true, + }, + names.AttrVersion: schema.Int64Attribute{ + Computed: true, + }, + "with_decryption": schema.BoolAttribute{ + Optional: true, + Computed: true, + }, + }, + } +} + +func (e *ephemeralParameter) Open(ctx context.Context, request ephemeral.OpenRequest, response *ephemeral.OpenResponse) { + var data epParameterData + conn := e.Meta().SSMClient(ctx) + + response.Diagnostics.Append(request.Config.Get(ctx, &data)...) + if response.Diagnostics.HasError() { + return + } + + // Terraform does not have the notion of planning for ephemeral resources, + // data sources, or providers. As a result, default handlers are not + // implemented for these objects in the Terraform Plugin Framework. + // + // To align with the data source data.aws_ssm_parameter, + // we default `with_decryption`. + if data.WithDecryption.IsNull() { + data.WithDecryption = types.BoolValue(true) + } + + output, err := findParameterByName(ctx, conn, data.ARN.ValueString(), data.WithDecryption.ValueBool()) + if err != nil { + response.Diagnostics.AddError( + create.ProblemStandardMessage(names.SSM, create.ErrActionReading, ERNameParameter, data.ARN.String(), err), + err.Error(), + ) + return + } + + response.Diagnostics.Append(fwflex.Flatten(ctx, output, &data)...) + if response.Diagnostics.HasError() { + return + } + + response.Diagnostics.Append(response.Result.Set(ctx, &data)...) +} + +type epParameterData struct { + ARN fwtypes.ARN `tfsdk:"arn"` + Name types.String `tfsdk:"name"` + Type types.String `tfsdk:"type"` + Value types.String `tfsdk:"value"` + Version types.Int64 `tfsdk:"version"` + WithDecryption types.Bool `tfsdk:"with_decryption"` +} diff --git a/internal/service/ssm/parameter_ephemeral_test.go b/internal/service/ssm/parameter_ephemeral_test.go new file mode 100644 index 00000000000..9440ccce33d --- /dev/null +++ b/internal/service/ssm/parameter_ephemeral_test.go @@ -0,0 +1,324 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package ssm_test + +import ( + "fmt" + "testing" + + sdkacctest "github.com/hashicorp/terraform-plugin-testing/helper/acctest" + "github.com/hashicorp/terraform-plugin-testing/helper/resource" + "github.com/hashicorp/terraform-plugin-testing/knownvalue" + "github.com/hashicorp/terraform-plugin-testing/statecheck" + "github.com/hashicorp/terraform-plugin-testing/tfjsonpath" + "github.com/hashicorp/terraform-plugin-testing/tfversion" + "github.com/hashicorp/terraform-provider-aws/internal/acctest" + "github.com/hashicorp/terraform-provider-aws/names" +) + +func TestAccSSMParameterEphemeral_basic(t *testing.T) { + ctx := acctest.Context(t) + rName := sdkacctest.RandomWithPrefix(acctest.ResourcePrefix) + echoResourceName := "echo.test" + dataPath := tfjsonpath.New("data") + secretString := "super-secret" + + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { acctest.PreCheck(ctx, t) }, + ErrorCheck: acctest.ErrorCheck(t, names.SSMServiceID), + TerraformVersionChecks: []tfversion.TerraformVersionCheck{ + tfversion.SkipBelow(tfversion.Version1_10_0), + }, + ProtoV5ProviderFactories: acctest.ProtoV5ProviderFactories, + ProtoV6ProviderFactories: acctest.ProtoV6ProviderFactories(ctx, acctest.ProviderNameEcho), + CheckDestroy: acctest.CheckDestroyNoop, + Steps: []resource.TestStep{ + { + Config: testAccParameterEphemeralResourceConfig_basic(rName, secretString), + ConfigStateChecks: []statecheck.StateCheck{ + statecheck.ExpectKnownValue(echoResourceName, dataPath.AtMapKey(names.AttrARN), knownvalue.NotNull()), + statecheck.ExpectKnownValue(echoResourceName, dataPath.AtMapKey(names.AttrName), knownvalue.NotNull()), + statecheck.ExpectKnownValue(echoResourceName, dataPath.AtMapKey(names.AttrType), knownvalue.NotNull()), + statecheck.ExpectKnownValue(echoResourceName, dataPath.AtMapKey(names.AttrValue), knownvalue.NotNull()), + statecheck.ExpectKnownValue(echoResourceName, dataPath.AtMapKey(names.AttrVersion), knownvalue.NotNull()), + statecheck.ExpectKnownValue(echoResourceName, dataPath.AtMapKey("with_decryption"), knownvalue.Bool(true)), + statecheck.ExpectKnownValue(echoResourceName, dataPath.AtMapKey(names.AttrValue), knownvalue.StringExact(secretString)), + }, + }, + }, + }) +} + +func TestAccSSMParameterEphemeral_secureString(t *testing.T) { + ctx := acctest.Context(t) + rName := sdkacctest.RandomWithPrefix(acctest.ResourcePrefix) + echoResourceName := "echo.test" + dataPath := tfjsonpath.New("data") + secretString := "super-secret" + + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { acctest.PreCheck(ctx, t) }, + ErrorCheck: acctest.ErrorCheck(t, names.SSMServiceID), + TerraformVersionChecks: []tfversion.TerraformVersionCheck{ + tfversion.SkipBelow(tfversion.Version1_10_0), + }, + ProtoV5ProviderFactories: acctest.ProtoV5ProviderFactories, + ProtoV6ProviderFactories: acctest.ProtoV6ProviderFactories(ctx, acctest.ProviderNameEcho), + CheckDestroy: acctest.CheckDestroyNoop, + Steps: []resource.TestStep{ + { + Config: testAccParameterEphemeralResourceConfig_secureString(rName, secretString), + ConfigStateChecks: []statecheck.StateCheck{ + statecheck.ExpectKnownValue(echoResourceName, dataPath.AtMapKey(names.AttrARN), knownvalue.NotNull()), + statecheck.ExpectKnownValue(echoResourceName, dataPath.AtMapKey(names.AttrName), knownvalue.NotNull()), + statecheck.ExpectKnownValue(echoResourceName, dataPath.AtMapKey(names.AttrType), knownvalue.NotNull()), + statecheck.ExpectKnownValue(echoResourceName, dataPath.AtMapKey(names.AttrValue), knownvalue.NotNull()), + statecheck.ExpectKnownValue(echoResourceName, dataPath.AtMapKey(names.AttrVersion), knownvalue.NotNull()), + statecheck.ExpectKnownValue(echoResourceName, dataPath.AtMapKey("with_decryption"), knownvalue.Bool(true)), + statecheck.ExpectKnownValue(echoResourceName, dataPath.AtMapKey(names.AttrValue), knownvalue.StringExact(secretString)), + }, + }, + }, + }) +} + +func TestAccSSMParameterEphemeral_variable(t *testing.T) { + ctx := acctest.Context(t) + rName := sdkacctest.RandomWithPrefix(acctest.ResourcePrefix) + echoResourceName := "echo.test" + dataPath := tfjsonpath.New("data") + secretString := "super-secret" + + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { acctest.PreCheck(ctx, t) }, + ErrorCheck: acctest.ErrorCheck(t, names.SSMServiceID), + TerraformVersionChecks: []tfversion.TerraformVersionCheck{ + tfversion.SkipBelow(tfversion.Version1_10_0), + }, + ProtoV5ProviderFactories: acctest.ProtoV5ProviderFactories, + ProtoV6ProviderFactories: acctest.ProtoV6ProviderFactories(ctx, acctest.ProviderNameEcho), + CheckDestroy: acctest.CheckDestroyNoop, + Steps: []resource.TestStep{ + { + Config: testAccParameterEphemeralResourceConfig_variable(rName, secretString), + ConfigStateChecks: []statecheck.StateCheck{ + statecheck.ExpectKnownValue(echoResourceName, dataPath.AtMapKey(names.AttrARN), knownvalue.NotNull()), + statecheck.ExpectKnownValue(echoResourceName, dataPath.AtMapKey(names.AttrName), knownvalue.NotNull()), + statecheck.ExpectKnownValue(echoResourceName, dataPath.AtMapKey(names.AttrType), knownvalue.NotNull()), + statecheck.ExpectKnownValue(echoResourceName, dataPath.AtMapKey(names.AttrValue), knownvalue.NotNull()), + statecheck.ExpectKnownValue(echoResourceName, dataPath.AtMapKey(names.AttrVersion), knownvalue.NotNull()), + statecheck.ExpectKnownValue(echoResourceName, dataPath.AtMapKey("with_decryption"), knownvalue.Bool(true)), + statecheck.ExpectKnownValue(echoResourceName, dataPath.AtMapKey(names.AttrValue), knownvalue.StringExact(secretString)), + }, + }, + }, + }) +} + +func TestAccSSMParameterEphemeral_secureStringVariable(t *testing.T) { + ctx := acctest.Context(t) + rName := sdkacctest.RandomWithPrefix(acctest.ResourcePrefix) + echoResourceName := "echo.test" + dataPath := tfjsonpath.New("data") + secretString := "super-secret" + + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { acctest.PreCheck(ctx, t) }, + ErrorCheck: acctest.ErrorCheck(t, names.SSMServiceID), + TerraformVersionChecks: []tfversion.TerraformVersionCheck{ + tfversion.SkipBelow(tfversion.Version1_10_0), + }, + ProtoV5ProviderFactories: acctest.ProtoV5ProviderFactories, + ProtoV6ProviderFactories: acctest.ProtoV6ProviderFactories(ctx, acctest.ProviderNameEcho), + CheckDestroy: acctest.CheckDestroyNoop, + Steps: []resource.TestStep{ + { + Config: testAccParameterEphemeralResourceConfig_secureStringVariable(rName, secretString), + ConfigStateChecks: []statecheck.StateCheck{ + statecheck.ExpectKnownValue(echoResourceName, dataPath.AtMapKey(names.AttrARN), knownvalue.NotNull()), + statecheck.ExpectKnownValue(echoResourceName, dataPath.AtMapKey(names.AttrName), knownvalue.NotNull()), + statecheck.ExpectKnownValue(echoResourceName, dataPath.AtMapKey(names.AttrType), knownvalue.NotNull()), + statecheck.ExpectKnownValue(echoResourceName, dataPath.AtMapKey(names.AttrValue), knownvalue.NotNull()), + statecheck.ExpectKnownValue(echoResourceName, dataPath.AtMapKey(names.AttrVersion), knownvalue.NotNull()), + statecheck.ExpectKnownValue(echoResourceName, dataPath.AtMapKey("with_decryption"), knownvalue.Bool(true)), + statecheck.ExpectKnownValue(echoResourceName, dataPath.AtMapKey(names.AttrValue), knownvalue.StringExact(secretString)), + }, + }, + }, + }) +} + +func TestAccSSMParameterEphemeral_withDecryption(t *testing.T) { + ctx := acctest.Context(t) + rName := sdkacctest.RandomWithPrefix(acctest.ResourcePrefix) + echoResourceName := "echo.test" + dataPath := tfjsonpath.New("data") + secretString := "super-secret" + + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { acctest.PreCheck(ctx, t) }, + ErrorCheck: acctest.ErrorCheck(t, names.SSMServiceID), + TerraformVersionChecks: []tfversion.TerraformVersionCheck{ + tfversion.SkipBelow(tfversion.Version1_10_0), + }, + ProtoV5ProviderFactories: acctest.ProtoV5ProviderFactories, + ProtoV6ProviderFactories: acctest.ProtoV6ProviderFactories(ctx, acctest.ProviderNameEcho), + CheckDestroy: acctest.CheckDestroyNoop, + Steps: []resource.TestStep{ + { + Config: testAccParameterEphemeralResourceConfig_withDecryption(rName, secretString), + ConfigStateChecks: []statecheck.StateCheck{ + statecheck.ExpectKnownValue(echoResourceName, dataPath.AtMapKey(names.AttrARN), knownvalue.NotNull()), + statecheck.ExpectKnownValue(echoResourceName, dataPath.AtMapKey(names.AttrName), knownvalue.NotNull()), + statecheck.ExpectKnownValue(echoResourceName, dataPath.AtMapKey(names.AttrType), knownvalue.NotNull()), + statecheck.ExpectKnownValue(echoResourceName, dataPath.AtMapKey(names.AttrValue), knownvalue.NotNull()), + statecheck.ExpectKnownValue(echoResourceName, dataPath.AtMapKey(names.AttrVersion), knownvalue.NotNull()), + statecheck.ExpectKnownValue(echoResourceName, dataPath.AtMapKey("with_decryption"), knownvalue.Bool(true)), + statecheck.ExpectKnownValue(echoResourceName, dataPath.AtMapKey(names.AttrValue), knownvalue.StringExact(secretString)), + }, + }, + }, + }) +} + +func TestAccSSMParameterEphemeral_withDecryptionFalse(t *testing.T) { + ctx := acctest.Context(t) + rName := sdkacctest.RandomWithPrefix(acctest.ResourcePrefix) + echoResourceName := "echo.test" + dataPath := tfjsonpath.New("data") + secretString := "super-secret" + + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { acctest.PreCheck(ctx, t) }, + ErrorCheck: acctest.ErrorCheck(t, names.SSMServiceID), + TerraformVersionChecks: []tfversion.TerraformVersionCheck{ + tfversion.SkipBelow(tfversion.Version1_10_0), + }, + ProtoV5ProviderFactories: acctest.ProtoV5ProviderFactories, + ProtoV6ProviderFactories: acctest.ProtoV6ProviderFactories(ctx, acctest.ProviderNameEcho), + CheckDestroy: acctest.CheckDestroyNoop, + Steps: []resource.TestStep{ + { + Config: testAccParameterEphemeralResourceConfig_withDecryptionFalse(rName, secretString), + ConfigStateChecks: []statecheck.StateCheck{ + statecheck.ExpectKnownValue(echoResourceName, dataPath.AtMapKey(names.AttrARN), knownvalue.NotNull()), + statecheck.ExpectKnownValue(echoResourceName, dataPath.AtMapKey(names.AttrName), knownvalue.NotNull()), + statecheck.ExpectKnownValue(echoResourceName, dataPath.AtMapKey(names.AttrType), knownvalue.NotNull()), + statecheck.ExpectKnownValue(echoResourceName, dataPath.AtMapKey(names.AttrValue), knownvalue.NotNull()), + statecheck.ExpectKnownValue(echoResourceName, dataPath.AtMapKey(names.AttrVersion), knownvalue.NotNull()), + statecheck.ExpectKnownValue(echoResourceName, dataPath.AtMapKey("with_decryption"), knownvalue.Bool(false)), + statecheck.ExpectKnownValue(echoResourceName, dataPath.AtMapKey(names.AttrValue), knownvalue.StringExact(secretString)), + }, + }, + }, + }) +} + +func testAccParameterEphemeralResourceConfig_basic(rName, secretString string) string { + return acctest.ConfigCompose( + acctest.ConfigWithEchoProvider("ephemeral.aws_ssm_parameter.test"), + fmt.Sprintf(` +resource "aws_ssm_parameter" "test" { + name = %[1]q + type = "String" + value = %[2]q +} + +ephemeral "aws_ssm_parameter" "test" { + arn = aws_ssm_parameter.test.arn +} +`, rName, secretString)) +} + +func testAccParameterEphemeralResourceConfig_secureString(rName, secretString string) string { + return acctest.ConfigCompose( + acctest.ConfigWithEchoProvider("ephemeral.aws_ssm_parameter.test"), + fmt.Sprintf(` +resource "aws_ssm_parameter" "test" { + name = %[1]q + type = "SecureString" + value = %[2]q +} + +ephemeral "aws_ssm_parameter" "test" { + arn = aws_ssm_parameter.test.arn +} +`, rName, secretString)) +} + +func testAccParameterEphemeralResourceConfig_variable(rName, secretString string) string { + return acctest.ConfigCompose( + acctest.ConfigWithEchoProvider("ephemeral.aws_ssm_parameter.test"), + fmt.Sprintf(` +variable "test" { + type = string + default = %[2]q +} + +resource "aws_ssm_parameter" "test" { + name = %[1]q + type = "String" + value = var.test +} + +ephemeral "aws_ssm_parameter" "test" { + arn = aws_ssm_parameter.test.arn +} +`, rName, secretString)) +} + +func testAccParameterEphemeralResourceConfig_secureStringVariable(rName, secretString string) string { + return acctest.ConfigCompose( + acctest.ConfigWithEchoProvider("ephemeral.aws_ssm_parameter.test"), + fmt.Sprintf(` +variable "test" { + type = string + default = %[2]q +} + +resource "aws_ssm_parameter" "test" { + name = %[1]q + type = "SecureString" + value = var.test +} + +ephemeral "aws_ssm_parameter" "test" { + arn = aws_ssm_parameter.test.arn +} +`, rName, secretString)) +} + +func testAccParameterEphemeralResourceConfig_withDecryption(rName, secretString string) string { + return acctest.ConfigCompose( + acctest.ConfigWithEchoProvider("ephemeral.aws_ssm_parameter.test"), + fmt.Sprintf(` +resource "aws_ssm_parameter" "test" { + name = %[1]q + type = "String" + value = %[2]q +} + +ephemeral "aws_ssm_parameter" "test" { + arn = aws_ssm_parameter.test.arn + with_decryption = true +} +`, rName, secretString)) +} + +func testAccParameterEphemeralResourceConfig_withDecryptionFalse(rName, secretString string) string { + return acctest.ConfigCompose( + acctest.ConfigWithEchoProvider("ephemeral.aws_ssm_parameter.test"), + fmt.Sprintf(` +resource "aws_ssm_parameter" "test" { + name = %[1]q + type = "String" + value = %[2]q +} + +ephemeral "aws_ssm_parameter" "test" { + arn = aws_ssm_parameter.test.arn + with_decryption = false +} +`, rName, secretString)) +} diff --git a/internal/service/ssm/service_package_gen.go b/internal/service/ssm/service_package_gen.go index 275f94edf23..c5fb3134a8e 100644 --- a/internal/service/ssm/service_package_gen.go +++ b/internal/service/ssm/service_package_gen.go @@ -14,6 +14,15 @@ import ( type servicePackage struct{} +func (p *servicePackage) EphemeralResources(ctx context.Context) []*types.ServicePackageEphemeralResource { + return []*types.ServicePackageEphemeralResource{ + { + Factory: newEphemeralParameter, + Name: "Parameter", + }, + } +} + func (p *servicePackage) FrameworkDataSources(ctx context.Context) []*types.ServicePackageFrameworkDataSource { return []*types.ServicePackageFrameworkDataSource{ { diff --git a/website/docs/ephemeral-resources/ssm_parameter.html.markdown b/website/docs/ephemeral-resources/ssm_parameter.html.markdown new file mode 100644 index 00000000000..8b8459be27e --- /dev/null +++ b/website/docs/ephemeral-resources/ssm_parameter.html.markdown @@ -0,0 +1,40 @@ +--- +subcategory: "SSM (Systems Manager)" +layout: "aws" +page_title: "AWS: aws_ssm_parameter" +description: |- + Retrieve information about an SSM parameter, including its value. To retrieve parameter metadata, see the `aws_ssm_parameter` data source. +--- + +# Ephemeral: aws_ssm_parameter + +Retrieve information about an SSM parameter, including its value. + +~> **NOTE:** Ephemeral resources are a new feature and may evolve as we continue to explore their most effective uses. [Learn more](https://developer.hashicorp.com/terraform/language/v1.10.x/resources/ephemeral). + +## Example Usage + +### Retrieve an SSM parameter + +By default, this ephemeral resource attempst to return decrypted values for secure string parameters. + +```terraform +ephemeral "aws_ssm_parameter" "example" { + arn = aws_ssm_parameter.example.arn +} +``` + +## Argument Reference + +* `arn` - (Required) The Amazon Resource Name (ARN) of the parameter that you want to query +* `with_decryption` - (Optional) Return decrypted values for a secure string parameter (Defaults to `true`). + +## Attribute Reference + +This resource exports the following attributes in addition to the arguments above: + +* `name` - The name of the parameter. +* `type` - The type of parameter. +* `value` - The parameter value. +* `version` - The parameter version. +* `with_decryption` - Indicates whether the secure string parameters were decrypted.