diff --git a/.changes/unreleased/FEATURES-20250110-172233.yaml b/.changes/unreleased/FEATURES-20250110-172233.yaml new file mode 100644 index 00000000..3b96901c --- /dev/null +++ b/.changes/unreleased/FEATURES-20250110-172233.yaml @@ -0,0 +1,7 @@ +kind: FEATURES +body: 'ephemeral/random_password: New ephemeral resource that generates a password + string. When used in combination with a managed resource write-only attribute, Terraform + will not store the password in the plan or state file.' +time: 2025-01-10T17:22:33.1113-05:00 +custom: + Issue: "625" diff --git a/.changes/unreleased/NOTES-20250110-172526.yaml b/.changes/unreleased/NOTES-20250110-172526.yaml new file mode 100644 index 00000000..d2c460e4 --- /dev/null +++ b/.changes/unreleased/NOTES-20250110-172526.yaml @@ -0,0 +1,6 @@ +kind: NOTES +body: New [ephemeral resource](https://developer.hashicorp.com/terraform/language/resources/ephemeral) + `random_password` now supports [ephemeral values](https://developer.hashicorp.com/terraform/language/values/variables#exclude-values-from-state). +time: 2025-01-10T17:25:26.145298-05:00 +custom: + Issue: "625" diff --git a/.gitignore b/.gitignore index 5982d2c6..ef663907 100644 --- a/.gitignore +++ b/.gitignore @@ -23,6 +23,7 @@ website/node_modules *.iml *.test *.iml +.vscode website/vendor diff --git a/docs/ephemeral-resources/password.md b/docs/ephemeral-resources/password.md new file mode 100644 index 00000000..de5ac414 --- /dev/null +++ b/docs/ephemeral-resources/password.md @@ -0,0 +1,51 @@ +--- +# generated by https://github.com/hashicorp/terraform-plugin-docs +page_title: "random_password Ephemeral Resource - terraform-provider-random" +subcategory: "" +description: |- + -> If the managed resource doesn't have a write-only attribute available for the password (first introduced in Terraform 1.11), then the password can only be created with the managed resource variant of random_password https://registry.terraform.io/providers/hashicorp/random/latest/docs/resources/password. + Generates an ephemeral password string using a cryptographic random number generator. + The primary use-case for generating an ephemeral random password is to be used in combination with a write-only attribute in a managed resource, which will avoid Terraform storing the password string in the plan or state file. +--- + +# random_password (Ephemeral Resource) + +-> If the managed resource doesn't have a write-only attribute available for the password (first introduced in Terraform 1.11), then the password can only be created with the managed resource variant of [`random_password`](https://registry.terraform.io/providers/hashicorp/random/latest/docs/resources/password). + +Generates an ephemeral password string using a cryptographic random number generator. + +The primary use-case for generating an ephemeral random password is to be used in combination with a write-only attribute in a managed resource, which will avoid Terraform storing the password string in the plan or state file. + +## Example Usage + +```terraform +ephemeral "random_password" "password" { + length = 16 + special = true + override_special = "!#$%&*()-_=+[]{}<>:?" +} +``` + + +## Schema + +### Required + +- `length` (Number) The length of the string desired. The minimum value for length is 1 and, length must also be >= (`min_upper` + `min_lower` + `min_numeric` + `min_special`). + +### Optional + +- `lower` (Boolean) Include lowercase alphabet characters in the result. Default value is `true`. +- `min_lower` (Number) Minimum number of lowercase alphabet characters in the result. Default value is `0`. +- `min_numeric` (Number) Minimum number of numeric characters in the result. Default value is `0`. +- `min_special` (Number) Minimum number of special characters in the result. Default value is `0`. +- `min_upper` (Number) Minimum number of uppercase alphabet characters in the result. Default value is `0`. +- `numeric` (Boolean) Include numeric characters in the result. Default value is `true`. If `numeric`, `upper`, `lower`, and `special` are all configured, at least one of them must be set to `true`. +- `override_special` (String) Supply your own list of special characters to use for string generation. This overrides the default character list in the special argument. The `special` argument must still be set to true for any overwritten characters to be used in generation. +- `special` (Boolean) Include special characters in the result. These are `!@#$%&*()-_=+[]{}<>:?`. Default value is `true`. +- `upper` (Boolean) Include uppercase alphabet characters in the result. Default value is `true`. + +### Read-Only + +- `bcrypt_hash` (String, Sensitive) A bcrypt hash of the generated random string. **NOTE**: If the generated random string is greater than 72 bytes in length, `bcrypt_hash` will contain a hash of the first 72 bytes. +- `result` (String, Sensitive) The generated random string. diff --git a/docs/resources/password.md b/docs/resources/password.md index c2c98a06..0cbaf4d4 100644 --- a/docs/resources/password.md +++ b/docs/resources/password.md @@ -2,13 +2,16 @@ page_title: "random_password Resource - terraform-provider-random" subcategory: "" description: |- - Identical to random_string string.html with the exception that the result is treated as sensitive and, thus, not displayed in console output. Read more about sensitive data handling in the Terraform documentation https://www.terraform.io/docs/language/state/sensitive-data.html. + -> If the managed resource supports a write-only attribute for the password (first introduced in Terraform 1.11), then the ephemeral variant of random_password https://registry.terraform.io/providers/hashicorp/random/latest/docs/ephemeral-resources/password should be used, when possible, to avoid storing the password in the plan or state file. + Identical to random_string https://registry.terraform.io/providers/hashicorp/random/latest/docs/resources/string with the exception that the result is treated as sensitive and, thus, not displayed in console output. Read more about sensitive data handling in the Terraform documentation https://developer.hashicorp.com/terraform/language/state/sensitive-data. This resource does use a cryptographic random number generator. --- # random_password (Resource) -Identical to [random_string](string.html) with the exception that the result is treated as sensitive and, thus, _not_ displayed in console output. Read more about sensitive data handling in the [Terraform documentation](https://www.terraform.io/docs/language/state/sensitive-data.html). +-> If the managed resource supports a write-only attribute for the password (first introduced in Terraform 1.11), then the ephemeral variant of [`random_password`](https://registry.terraform.io/providers/hashicorp/random/latest/docs/ephemeral-resources/password) should be used, when possible, to avoid storing the password in the plan or state file. + +Identical to [`random_string`](https://registry.terraform.io/providers/hashicorp/random/latest/docs/resources/string) with the exception that the result is treated as sensitive and, thus, _not_ displayed in console output. Read more about sensitive data handling in the [Terraform documentation](https://developer.hashicorp.com/terraform/language/state/sensitive-data). This resource *does* use a cryptographic random number generator. diff --git a/examples/ephemeral-resources/random_password/ephemeral-resource.tf b/examples/ephemeral-resources/random_password/ephemeral-resource.tf new file mode 100644 index 00000000..eaf8c1cc --- /dev/null +++ b/examples/ephemeral-resources/random_password/ephemeral-resource.tf @@ -0,0 +1,5 @@ +ephemeral "random_password" "password" { + length = 16 + special = true + override_special = "!#$%&*()-_=+[]{}<>:?" +} diff --git a/internal/provider/ephemeral_password.go b/internal/provider/ephemeral_password.go new file mode 100644 index 00000000..be1cc5d8 --- /dev/null +++ b/internal/provider/ephemeral_password.go @@ -0,0 +1,211 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package provider + +import ( + "context" + + "github.com/hashicorp/terraform-plugin-framework-validators/int64validator" + "github.com/hashicorp/terraform-plugin-framework/ephemeral" + "github.com/hashicorp/terraform-plugin-framework/ephemeral/schema" + "github.com/hashicorp/terraform-plugin-framework/path" + "github.com/hashicorp/terraform-plugin-framework/schema/validator" + "github.com/hashicorp/terraform-plugin-framework/types" + "github.com/terraform-providers/terraform-provider-random/internal/diagnostics" + "github.com/terraform-providers/terraform-provider-random/internal/random" + "github.com/terraform-providers/terraform-provider-random/internal/validators" +) + +var ( + _ ephemeral.EphemeralResource = (*passwordEphemeralResource)(nil) +) + +func NewPasswordEphemeralResource() ephemeral.EphemeralResource { + return &passwordEphemeralResource{} +} + +type passwordEphemeralResource struct{} + +type ephemeralPasswordModel struct { + Length types.Int64 `tfsdk:"length"` + Special types.Bool `tfsdk:"special"` + Upper types.Bool `tfsdk:"upper"` + Lower types.Bool `tfsdk:"lower"` + Numeric types.Bool `tfsdk:"numeric"` + MinNumeric types.Int64 `tfsdk:"min_numeric"` + MinUpper types.Int64 `tfsdk:"min_upper"` + MinLower types.Int64 `tfsdk:"min_lower"` + MinSpecial types.Int64 `tfsdk:"min_special"` + OverrideSpecial types.String `tfsdk:"override_special"` + Result types.String `tfsdk:"result"` + BcryptHash types.String `tfsdk:"bcrypt_hash"` +} + +func (e *passwordEphemeralResource) Metadata(ctx context.Context, req ephemeral.MetadataRequest, resp *ephemeral.MetadataResponse) { + resp.TypeName = req.ProviderTypeName + "_password" +} + +func (e *passwordEphemeralResource) Schema(ctx context.Context, req ephemeral.SchemaRequest, resp *ephemeral.SchemaResponse) { + resp.Schema = schema.Schema{ + Description: "-> If the managed resource doesn't have a write-only attribute available for the password (first introduced in Terraform 1.11), then the " + + "password can only be created with the managed resource variant of [`random_password`](https://registry.terraform.io/providers/hashicorp/random/latest/docs/resources/password).\n" + + "\n" + + "Generates an ephemeral password string using a cryptographic random number generator.\n" + + "\n" + + "The primary use-case for generating an ephemeral random password is to be used in combination with a write-only attribute " + + "in a managed resource, which will avoid Terraform storing the password string in the plan or state file.", + Attributes: map[string]schema.Attribute{ + "length": schema.Int64Attribute{ + Description: "The length of the string desired. The minimum value for length is 1 and, length " + + "must also be >= (`min_upper` + `min_lower` + `min_numeric` + `min_special`).", + Required: true, + Validators: []validator.Int64{ + int64validator.AtLeast(1), + int64validator.AtLeastSumOf( + path.MatchRoot("min_upper"), + path.MatchRoot("min_lower"), + path.MatchRoot("min_numeric"), + path.MatchRoot("min_special"), + ), + }, + }, + "special": schema.BoolAttribute{ + Description: "Include special characters in the result. These are `!@#$%&*()-_=+[]{}<>:?`. Default value is `true`.", + Optional: true, + Computed: true, + }, + "upper": schema.BoolAttribute{ + Description: "Include uppercase alphabet characters in the result. Default value is `true`.", + Optional: true, + Computed: true, + }, + "lower": schema.BoolAttribute{ + Description: "Include lowercase alphabet characters in the result. Default value is `true`.", + Optional: true, + Computed: true, + }, + "numeric": schema.BoolAttribute{ + Description: "Include numeric characters in the result. Default value is `true`. " + + "If `numeric`, `upper`, `lower`, and `special` are all configured, at least one " + + "of them must be set to `true`.", + Optional: true, + Computed: true, + Validators: []validator.Bool{ + validators.AtLeastOneOfTrue( + path.MatchRoot("special"), + path.MatchRoot("upper"), + path.MatchRoot("lower"), + ), + }, + }, + "min_numeric": schema.Int64Attribute{ + Description: "Minimum number of numeric characters in the result. Default value is `0`.", + Optional: true, + Computed: true, + }, + + "min_upper": schema.Int64Attribute{ + Description: "Minimum number of uppercase alphabet characters in the result. Default value is `0`.", + Optional: true, + Computed: true, + }, + "min_lower": schema.Int64Attribute{ + Description: "Minimum number of lowercase alphabet characters in the result. Default value is `0`.", + Optional: true, + Computed: true, + }, + "min_special": schema.Int64Attribute{ + Description: "Minimum number of special characters in the result. Default value is `0`.", + Optional: true, + Computed: true, + }, + "override_special": schema.StringAttribute{ + Description: "Supply your own list of special characters to use for string generation. This " + + "overrides the default character list in the special argument. The `special` argument must " + + "still be set to true for any overwritten characters to be used in generation.", + Optional: true, + }, + "result": schema.StringAttribute{ + Description: "The generated random string.", + Computed: true, + Sensitive: true, + }, + "bcrypt_hash": schema.StringAttribute{ + Description: "A bcrypt hash of the generated random string. " + + "**NOTE**: If the generated random string is greater than 72 bytes in length, " + + "`bcrypt_hash` will contain a hash of the first 72 bytes.", + Computed: true, + Sensitive: true, + }, + }, + } +} + +func (e *passwordEphemeralResource) Open(ctx context.Context, req ephemeral.OpenRequest, resp *ephemeral.OpenResponse) { + var data ephemeralPasswordModel + + resp.Diagnostics.Append(req.Config.Get(ctx, &data)...) + if resp.Diagnostics.HasError() { + return + } + + applyDefaultPasswordParameters(&data) + + params := random.StringParams{ + Length: data.Length.ValueInt64(), + Upper: data.Upper.ValueBool(), + MinUpper: data.MinUpper.ValueInt64(), + Lower: data.Lower.ValueBool(), + MinLower: data.MinLower.ValueInt64(), + Numeric: data.Numeric.ValueBool(), + MinNumeric: data.MinNumeric.ValueInt64(), + Special: data.Special.ValueBool(), + MinSpecial: data.MinSpecial.ValueInt64(), + OverrideSpecial: data.OverrideSpecial.ValueString(), + } + + result, err := random.CreateString(params) + if err != nil { + resp.Diagnostics.Append(diagnostics.RandomReadError(err.Error())...) + return + } + + hash, err := generateHash(string(result)) + if err != nil { + resp.Diagnostics.Append(diagnostics.HashGenerationError(err.Error())...) + } + + data.BcryptHash = types.StringValue(hash) + data.Result = types.StringValue(string(result)) + + resp.Diagnostics.Append(resp.Result.Set(ctx, data)...) +} + +func applyDefaultPasswordParameters(data *ephemeralPasswordModel) { + if data.Special.IsNull() { + data.Special = types.BoolValue(true) + } + if data.Upper.IsNull() { + data.Upper = types.BoolValue(true) + } + if data.Lower.IsNull() { + data.Lower = types.BoolValue(true) + } + if data.Numeric.IsNull() { + data.Numeric = types.BoolValue(true) + } + + if data.MinNumeric.IsNull() { + data.MinNumeric = types.Int64Value(0) + } + if data.MinUpper.IsNull() { + data.MinUpper = types.Int64Value(0) + } + if data.MinLower.IsNull() { + data.MinLower = types.Int64Value(0) + } + if data.MinSpecial.IsNull() { + data.MinSpecial = types.Int64Value(0) + } +} diff --git a/internal/provider/ephemeral_password_test.go b/internal/provider/ephemeral_password_test.go new file mode 100644 index 00000000..2cb23778 --- /dev/null +++ b/internal/provider/ephemeral_password_test.go @@ -0,0 +1,233 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package provider + +import ( + "fmt" + "regexp" + "testing" + + "github.com/hashicorp/terraform-plugin-go/tfprotov6" + "github.com/hashicorp/terraform-plugin-testing/echoprovider" + "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/terraform-providers/terraform-provider-random/internal/randomtest" +) + +func TestAccEphemeralResourcePassword_basic(t *testing.T) { + t.Parallel() + + resource.UnitTest(t, resource.TestCase{ + // Ephemeral resources are only available in 1.10 and later + TerraformVersionChecks: []tfversion.TerraformVersionCheck{ + tfversion.SkipBelow(tfversion.Version1_10_0), + }, + ProtoV5ProviderFactories: protoV5ProviderFactories(), + ProtoV6ProviderFactories: map[string]func() (tfprotov6.ProviderServer, error){ + "echo": echoprovider.NewProviderServer(), + }, + Steps: []resource.TestStep{ + { + Config: addEchoConfig(`ephemeral "random_password" "test" { + length = 20 + }`), + ConfigStateChecks: []statecheck.StateCheck{ + statecheck.ExpectKnownValue("echo.password_test", tfjsonpath.New("data").AtMapKey("result"), randomtest.StringLengthExact(20)), + statecheck.ExpectKnownValue("echo.password_test", tfjsonpath.New("data").AtMapKey("special"), knownvalue.Bool(true)), + statecheck.ExpectKnownValue("echo.password_test", tfjsonpath.New("data").AtMapKey("upper"), knownvalue.Bool(true)), + statecheck.ExpectKnownValue("echo.password_test", tfjsonpath.New("data").AtMapKey("lower"), knownvalue.Bool(true)), + statecheck.ExpectKnownValue("echo.password_test", tfjsonpath.New("data").AtMapKey("numeric"), knownvalue.Bool(true)), + statecheck.ExpectKnownValue("echo.password_test", tfjsonpath.New("data").AtMapKey("min_numeric"), knownvalue.Int64Exact(0)), + statecheck.ExpectKnownValue("echo.password_test", tfjsonpath.New("data").AtMapKey("min_upper"), knownvalue.Int64Exact(0)), + statecheck.ExpectKnownValue("echo.password_test", tfjsonpath.New("data").AtMapKey("min_lower"), knownvalue.Int64Exact(0)), + statecheck.ExpectKnownValue("echo.password_test", tfjsonpath.New("data").AtMapKey("min_special"), knownvalue.Int64Exact(0)), + }, + }, + }, + }) +} + +func TestAccEphemeralResourcePassword_BcryptHash(t *testing.T) { + t.Parallel() + + resource.UnitTest(t, resource.TestCase{ + // Ephemeral resources are only available in 1.10 and later + TerraformVersionChecks: []tfversion.TerraformVersionCheck{ + tfversion.SkipBelow(tfversion.Version1_10_0), + }, + ProtoV5ProviderFactories: protoV5ProviderFactories(), + ProtoV6ProviderFactories: map[string]func() (tfprotov6.ProviderServer, error){ + "echo": echoprovider.NewProviderServer(), + }, + Steps: []resource.TestStep{ + { + Config: addEchoConfig(`ephemeral "random_password" "test" { + length = 73 + }`), + ConfigStateChecks: []statecheck.StateCheck{ + statecheck.CompareValuePairs( + "echo.password_test", tfjsonpath.New("data").AtMapKey("bcrypt_hash"), + "echo.password_test", tfjsonpath.New("data").AtMapKey("result"), + randomtest.BcryptHashMatch(), + ), + }, + }, + }, + }) +} + +func TestAccEphemeralResourcePassword_Override(t *testing.T) { + t.Parallel() + + resource.UnitTest(t, resource.TestCase{ + // Ephemeral resources are only available in 1.10 and later + TerraformVersionChecks: []tfversion.TerraformVersionCheck{ + tfversion.SkipBelow(tfversion.Version1_10_0), + }, + ProtoV5ProviderFactories: protoV5ProviderFactories(), + ProtoV6ProviderFactories: map[string]func() (tfprotov6.ProviderServer, error){ + "echo": echoprovider.NewProviderServer(), + }, + Steps: []resource.TestStep{ + { + Config: addEchoConfig(`ephemeral "random_password" "test" { + length = 4 + override_special = "!" + lower = false + upper = false + numeric = false + }`), + ConfigStateChecks: []statecheck.StateCheck{ + statecheck.ExpectKnownValue("echo.password_test", tfjsonpath.New("data").AtMapKey("result"), randomtest.StringLengthExact(4)), + statecheck.ExpectKnownValue("echo.password_test", tfjsonpath.New("data").AtMapKey("result"), knownvalue.StringExact("!!!!")), + }, + }, + }, + }) +} + +func TestAccEphemeralResourcePassword_Min(t *testing.T) { + t.Parallel() + + resource.UnitTest(t, resource.TestCase{ + // Ephemeral resources are only available in 1.10 and later + TerraformVersionChecks: []tfversion.TerraformVersionCheck{ + tfversion.SkipBelow(tfversion.Version1_10_0), + }, + ProtoV5ProviderFactories: protoV5ProviderFactories(), + ProtoV6ProviderFactories: map[string]func() (tfprotov6.ProviderServer, error){ + "echo": echoprovider.NewProviderServer(), + }, + Steps: []resource.TestStep{ + { + Config: addEchoConfig(`ephemeral "random_password" "test" { + length = 12 + override_special = "!#@" + min_lower = 2 + min_upper = 3 + min_special = 1 + min_numeric = 4 + }`), + ConfigStateChecks: []statecheck.StateCheck{ + statecheck.ExpectKnownValue("echo.password_test", tfjsonpath.New("data").AtMapKey("result"), randomtest.StringLengthExact(12)), + statecheck.ExpectKnownValue("echo.password_test", tfjsonpath.New("data").AtMapKey("result"), knownvalue.StringRegexp(regexp.MustCompile(`([a-z].*){2,}`))), + statecheck.ExpectKnownValue("echo.password_test", tfjsonpath.New("data").AtMapKey("result"), knownvalue.StringRegexp(regexp.MustCompile(`([A-Z].*){3,}`))), + statecheck.ExpectKnownValue("echo.password_test", tfjsonpath.New("data").AtMapKey("result"), knownvalue.StringRegexp(regexp.MustCompile(`([0-9].*){4,}`))), + statecheck.ExpectKnownValue("echo.password_test", tfjsonpath.New("data").AtMapKey("result"), knownvalue.StringRegexp(regexp.MustCompile(`([!#@])`))), + }, + }, + }, + }) +} + +func TestAccEphemeralResourcePassword_Numeric_ValidationError(t *testing.T) { + t.Parallel() + + resource.UnitTest(t, resource.TestCase{ + // Ephemeral resources are only available in 1.10 and later + TerraformVersionChecks: []tfversion.TerraformVersionCheck{ + tfversion.SkipBelow(tfversion.Version1_10_0), + }, + ProtoV5ProviderFactories: protoV5ProviderFactories(), + ProtoV6ProviderFactories: map[string]func() (tfprotov6.ProviderServer, error){ + "echo": echoprovider.NewProviderServer(), + }, + Steps: []resource.TestStep{ + { + Config: addEchoConfig(`ephemeral "random_password" "test" { + length = 12 + special = false + upper = false + lower = false + numeric = false + }`), + ExpectError: regexp.MustCompile(`At least one attribute out of \[special,upper,lower,numeric\] must be specified`), + }, + }, + }) +} + +func TestAccEphemeralResourcePassword_Length_ValidationError_SumOf(t *testing.T) { + t.Parallel() + + resource.UnitTest(t, resource.TestCase{ + // Ephemeral resources are only available in 1.10 and later + TerraformVersionChecks: []tfversion.TerraformVersionCheck{ + tfversion.SkipBelow(tfversion.Version1_10_0), + }, + ProtoV5ProviderFactories: protoV5ProviderFactories(), + ProtoV6ProviderFactories: map[string]func() (tfprotov6.ProviderServer, error){ + "echo": echoprovider.NewProviderServer(), + }, + Steps: []resource.TestStep{ + { + Config: addEchoConfig(`ephemeral "random_password" "test" { + length = 11 + min_upper = 3 + min_lower = 3 + min_numeric = 3 + min_special = 3 + }`), + ExpectError: regexp.MustCompile(`Attribute length value must be at least sum of`), + }, + }, + }) +} + +func TestAccEphemeralResourcePassword_Length_ValidationError_AtLeast(t *testing.T) { + t.Parallel() + + resource.UnitTest(t, resource.TestCase{ + // Ephemeral resources are only available in 1.10 and later + TerraformVersionChecks: []tfversion.TerraformVersionCheck{ + tfversion.SkipBelow(tfversion.Version1_10_0), + }, + ProtoV5ProviderFactories: protoV5ProviderFactories(), + ProtoV6ProviderFactories: map[string]func() (tfprotov6.ProviderServer, error){ + "echo": echoprovider.NewProviderServer(), + }, + Steps: []resource.TestStep{ + { + Config: addEchoConfig(`ephemeral "random_password" "test" { + length = 0 + }`), + ExpectError: regexp.MustCompile(`Attribute length value must be at least 1, got: 0`), + }, + }, + }) +} + +// Adds the test echo provider to enable using state checks with ephemeral resources. +func addEchoConfig(cfg string) string { + return fmt.Sprintf(` + %s + provider "echo" { + data = ephemeral.random_password.test + } + resource "echo" "password_test" {} + `, cfg) +} diff --git a/internal/provider/provider.go b/internal/provider/provider.go index 1843c9ff..fd1c340d 100644 --- a/internal/provider/provider.go +++ b/internal/provider/provider.go @@ -7,6 +7,7 @@ import ( "context" "github.com/hashicorp/terraform-plugin-framework/datasource" + "github.com/hashicorp/terraform-plugin-framework/ephemeral" "github.com/hashicorp/terraform-plugin-framework/provider" "github.com/hashicorp/terraform-plugin-framework/resource" ) @@ -16,6 +17,7 @@ func New() provider.Provider { } var _ provider.Provider = (*randomProvider)(nil) +var _ provider.ProviderWithEphemeralResources = (*randomProvider)(nil) type randomProvider struct{} @@ -45,3 +47,9 @@ func (p *randomProvider) Resources(context.Context) []func() resource.Resource { func (p *randomProvider) DataSources(context.Context) []func() datasource.DataSource { return nil } + +func (p *randomProvider) EphemeralResources(context.Context) []func() ephemeral.EphemeralResource { + return []func() ephemeral.EphemeralResource{ + NewPasswordEphemeralResource, + } +} diff --git a/internal/provider/resource_password.go b/internal/provider/resource_password.go index c2dc9528..8c325d91 100644 --- a/internal/provider/resource_password.go +++ b/internal/provider/resource_password.go @@ -558,10 +558,14 @@ func generateHash(toHash string) (string, error) { func passwordSchemaV3() schema.Schema { return schema.Schema{ Version: 3, - Description: "Identical to [random_string](string.html) with the exception that the result is " + - "treated as sensitive and, thus, _not_ displayed in console output. Read more about sensitive " + - "data handling in the " + - "[Terraform documentation](https://www.terraform.io/docs/language/state/sensitive-data.html).\n\n" + + Description: "-> If the managed resource supports a write-only attribute for the password (first introduced in Terraform 1.11), then the " + + "ephemeral variant of [`random_password`](https://registry.terraform.io/providers/hashicorp/random/latest/docs/ephemeral-resources/password) " + + "should be used, when possible, to avoid storing the password in the plan or state file.\n" + + "\n" + + "Identical to [`random_string`](https://registry.terraform.io/providers/hashicorp/random/latest/docs/resources/string) with the exception that the result is " + + "treated as sensitive and, thus, _not_ displayed in console output. Read more about sensitive data handling in the " + + "[Terraform documentation](https://developer.hashicorp.com/terraform/language/state/sensitive-data).\n" + + "\n" + "This resource *does* use a cryptographic random number generator.", Attributes: map[string]schema.Attribute{ "keepers": schema.MapAttribute{