diff --git a/.changes/unreleased/FEATURES-20230213-213454.yaml b/.changes/unreleased/FEATURES-20230213-213454.yaml new file mode 100644 index 00000000..16dfb0bc --- /dev/null +++ b/.changes/unreleased/FEATURES-20230213-213454.yaml @@ -0,0 +1,6 @@ +kind: FEATURES +body: 'resource/random_bytes: New resource that generates an array of random bytes + intended to be used as key or secret' +time: 2023-02-13T21:34:54.806043106-05:00 +custom: + Issue: "272" diff --git a/docs/resources/bytes.md b/docs/resources/bytes.md new file mode 100644 index 00000000..79ddbccb --- /dev/null +++ b/docs/resources/bytes.md @@ -0,0 +1,51 @@ +--- +# generated by https://github.com/hashicorp/terraform-plugin-docs +page_title: "random_bytes Resource - terraform-provider-random" +subcategory: "" +description: |- + The resource random_bytes generates random bytes that are intended to be used as secret or keys. +--- + +# random_bytes (Resource) + +The resource `random_bytes` generates random bytes that are intended to be used as secret or keys. + +## Example Usage + +```terraform +resource "random_bytes" "jwt_secret" { + length = 64 +} + +resource "azurerm_key_vault_secret" "jwt_secret" { + key_vault_id = "some-azure-key-vault-id" + name = "JwtSecret" + value = random_bytes.jwt_secret.result_base64 +} +``` + + +## Schema + +### Required + +- `length` (Number) The number of bytes requested. The minimum value for length is 1. + +### Optional + +- `keepers` (Map of String) Arbitrary map of values that, when changed, will trigger recreation of resource. See [the main provider documentation](../index.html) for more information. + +### Read-Only + +- `id` (String) A static value used internally by Terraform, this should not be referenced in configurations. +- `result_base64` (String, Sensitive) The generated bytes presented in base64 string format. +- `result_hex` (String, Sensitive) The generated bytes presented in hex string format. + +## Import + +Import is supported using the following syntax: + +```shell +# Random bytes can be imported by specifying the value as base64 string. +terraform import random_bytes.basic 8/fu3q+2DcgSJ19i0jZ5Cw== +``` diff --git a/examples/resources/random_bytes/import.sh b/examples/resources/random_bytes/import.sh new file mode 100644 index 00000000..faa352f3 --- /dev/null +++ b/examples/resources/random_bytes/import.sh @@ -0,0 +1,2 @@ +# Random bytes can be imported by specifying the value as base64 string. +terraform import random_bytes.basic 8/fu3q+2DcgSJ19i0jZ5Cw== diff --git a/examples/resources/random_bytes/resource.tf b/examples/resources/random_bytes/resource.tf new file mode 100644 index 00000000..014a52e6 --- /dev/null +++ b/examples/resources/random_bytes/resource.tf @@ -0,0 +1,9 @@ +resource "random_bytes" "jwt_secret" { + length = 64 +} + +resource "azurerm_key_vault_secret" "jwt_secret" { + key_vault_id = "some-azure-key-vault-id" + name = "JwtSecret" + value = random_bytes.jwt_secret.result_base64 +} diff --git a/internal/provider/provider.go b/internal/provider/provider.go index 9e023655..c70c8b34 100644 --- a/internal/provider/provider.go +++ b/internal/provider/provider.go @@ -29,6 +29,7 @@ func (p *randomProvider) Configure(context.Context, provider.ConfigureRequest, * func (p *randomProvider) Resources(context.Context) []func() resource.Resource { return []func() resource.Resource{ NewIdResource, + NewBytesResource, NewIntegerResource, NewPasswordResource, NewPetResource, diff --git a/internal/provider/resource_bytes.go b/internal/provider/resource_bytes.go new file mode 100644 index 00000000..c35336e4 --- /dev/null +++ b/internal/provider/resource_bytes.go @@ -0,0 +1,185 @@ +package provider + +import ( + "context" + "crypto/rand" + "encoding/base64" + "encoding/hex" + "fmt" + "github.com/hashicorp/terraform-plugin-framework-validators/int64validator" + "github.com/hashicorp/terraform-plugin-framework/resource" + "github.com/hashicorp/terraform-plugin-framework/resource/schema" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/int64planmodifier" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/planmodifier" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/stringplanmodifier" + "github.com/hashicorp/terraform-plugin-framework/schema/validator" + "github.com/hashicorp/terraform-plugin-framework/types" + "github.com/terraform-providers/terraform-provider-random/internal/diagnostics" + mapplanmodifiers "github.com/terraform-providers/terraform-provider-random/internal/planmodifiers/map" +) + +var ( + _ resource.Resource = (*bytesResource)(nil) + _ resource.ResourceWithImportState = (*bytesResource)(nil) +) + +func NewBytesResource() resource.Resource { + return &bytesResource{} +} + +type bytesResource struct { +} + +func (r *bytesResource) Metadata(_ context.Context, req resource.MetadataRequest, resp *resource.MetadataResponse) { + resp.TypeName = req.ProviderTypeName + "_bytes" +} + +func (r *bytesResource) Schema(ctx context.Context, req resource.SchemaRequest, resp *resource.SchemaResponse) { + resp.Schema = bytesSchemaV0() +} + +func (r *bytesResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) { + var plan bytesModelV0 + + diags := req.Plan.Get(ctx, &plan) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } + + bytes := make([]byte, plan.Length.ValueInt64()) + _, err := rand.Read(bytes) + if err != nil { + resp.Diagnostics.AddError( + "Create Random bytes error", + "There was an error during random generation.\n\n"+ + diagnostics.RetryMsg+ + fmt.Sprintf("Original Error: %s", err), + ) + return + } + + u := &bytesModelV0{ + ID: types.StringValue("none"), + Length: plan.Length, + ResultBase64: types.StringValue(base64.StdEncoding.EncodeToString(bytes)), + ResultHex: types.StringValue(hex.EncodeToString(bytes)), + Keepers: plan.Keepers, + } + + diags = resp.State.Set(ctx, u) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } +} + +// Read does not need to perform any operations as the state in ReadResourceResponse is already populated. +func (r *bytesResource) Read(context.Context, resource.ReadRequest, *resource.ReadResponse) { +} + +func (r *bytesResource) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) { + var model bytesModelV0 + + resp.Diagnostics.Append(req.Plan.Get(ctx, &model)...) + if resp.Diagnostics.HasError() { + return + } + resp.Diagnostics.Append(resp.State.Set(ctx, &model)...) +} + +// Delete does not need to explicitly call resp.State.RemoveResource() as this is automatically handled by the +// [framework](https://github.com/hashicorp/terraform-plugin-framework/pull/301). +func (r *bytesResource) Delete(context.Context, resource.DeleteRequest, *resource.DeleteResponse) { +} + +func (r *bytesResource) ImportState(ctx context.Context, req resource.ImportStateRequest, resp *resource.ImportStateResponse) { + bytes, err := base64.StdEncoding.DecodeString(req.ID) + if err != nil { + resp.Diagnostics.AddError( + "Import Random bytes Error", + "There was an error during the parsing of the base64 string.\n\n"+ + diagnostics.RetryMsg+ + fmt.Sprintf("Original Error: %s", err), + ) + return + } + + var state bytesModelV0 + + state.ID = types.StringValue("none") + state.Length = types.Int64Value(int64(len(bytes))) + state.ResultBase64 = types.StringValue(req.ID) + state.ResultHex = types.StringValue(hex.EncodeToString(bytes)) + state.Keepers = types.MapNull(types.StringType) + + diags := resp.State.Set(ctx, &state) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } +} + +type bytesModelV0 struct { + ID types.String `tfsdk:"id"` + Length types.Int64 `tfsdk:"length"` + Keepers types.Map `tfsdk:"keepers"` + ResultBase64 types.String `tfsdk:"result_base64"` + ResultHex types.String `tfsdk:"result_hex"` +} + +func bytesSchemaV0() schema.Schema { + return schema.Schema{ + Version: 0, + Description: "The resource `random_bytes` generates random bytes that are intended to be " + + "used as secret or keys.", + Attributes: map[string]schema.Attribute{ + "keepers": schema.MapAttribute{ + Description: "Arbitrary map of values that, when changed, will trigger recreation of " + + "resource. See [the main provider documentation](../index.html) for more information.", + ElementType: types.StringType, + Optional: true, + PlanModifiers: []planmodifier.Map{ + // mapplanmodifiers.RequiresReplaceIfValuesNotNull() has been used for consistency with other + // resources but mapplanmodifier.RequiresReplace() could have been used as there shouldn't be any + // prior state storage from a terraform-plugin-sdk based resource which would've collapsed a map + // of null values into a null map. + mapplanmodifiers.RequiresReplaceIfValuesNotNull(), + }, + }, + "length": schema.Int64Attribute{ + Description: "The number of bytes requested. The minimum value for length is 1.", + Required: true, + PlanModifiers: []planmodifier.Int64{ + int64planmodifier.RequiresReplace(), + }, + Validators: []validator.Int64{ + int64validator.AtLeast(1), + }, + }, + "result_base64": schema.StringAttribute{ + Description: "The generated bytes presented in base64 string format.", + Computed: true, + Sensitive: true, + PlanModifiers: []planmodifier.String{ + stringplanmodifier.UseStateForUnknown(), + }, + }, + "result_hex": schema.StringAttribute{ + Description: "The generated bytes presented in hex string format.", + Computed: true, + Sensitive: true, + PlanModifiers: []planmodifier.String{ + stringplanmodifier.UseStateForUnknown(), + }, + }, + "id": schema.StringAttribute{ + Description: "A static value used internally by Terraform, this should not be referenced in configurations.", + Computed: true, + PlanModifiers: []planmodifier.String{ + stringplanmodifier.UseStateForUnknown(), + }, + }, + }, + } +} diff --git a/internal/provider/resource_bytes_test.go b/internal/provider/resource_bytes_test.go new file mode 100644 index 00000000..a679bd94 --- /dev/null +++ b/internal/provider/resource_bytes_test.go @@ -0,0 +1,640 @@ +package provider + +import ( + "fmt" + "github.com/hashicorp/terraform-plugin-testing/terraform" + "regexp" + "testing" + + "github.com/hashicorp/terraform-plugin-testing/helper/resource" +) + +func TestAccResourceBytes(t *testing.T) { + resource.UnitTest(t, resource.TestCase{ + ProtoV5ProviderFactories: protoV5ProviderFactories(), + Steps: []resource.TestStep{ + { + Config: `resource "random_bytes" "basic" { + length = 32 + }`, + Check: resource.ComposeTestCheckFunc( + resource.TestMatchResourceAttr("random_bytes.basic", "result_base64", regexp.MustCompile(`^[A-Za-z/+\d]{43}=$`)), + resource.TestMatchResourceAttr("random_bytes.basic", "result_hex", regexp.MustCompile(`^[a-f\d]{64}$`)), + resource.TestCheckResourceAttr("random_bytes.basic", "length", "32"), + ), + }, + { + // Usage of ImportStateIdFunc is required as the value passed to the `terraform import` command needs + // to be the bytes encoded with base64, as the bytes resource sets ID to "none" + ImportStateIdFunc: func(s *terraform.State) (string, error) { + id := "random_bytes.basic" + rs, ok := s.RootModule().Resources[id] + if !ok { + return "", fmt.Errorf("not found: %s", id) + } + if rs.Primary.ID == "" { + return "", fmt.Errorf("no ID is set") + } + + return rs.Primary.Attributes["result_base64"], nil + }, + ResourceName: "random_bytes.basic", + ImportState: true, + ImportStateVerify: true, + }, + }, + }) +} + +func TestAccResourceBytes_ImportWithoutKeepersThenUpdateShouldNotTriggerChange(t *testing.T) { + resource.UnitTest(t, resource.TestCase{ + ProtoV5ProviderFactories: protoV5ProviderFactories(), + Steps: []resource.TestStep{ + { + ImportState: true, + ImportStateId: "hkvbcU5f8qGysTFhkI4gzf3yRWC1jXW3aRLCNQFOtNw=", + ImportStatePersist: true, + ResourceName: "random_bytes.basic", + Config: `resource "random_bytes" "basic" { + length = 32 + }`, + Check: resource.ComposeTestCheckFunc( + resource.TestCheckResourceAttr("random_bytes.basic", "result_base64", "hkvbcU5f8qGysTFhkI4gzf3yRWC1jXW3aRLCNQFOtNw="), + resource.TestCheckResourceAttr("random_bytes.basic", "result_hex", "864bdb714e5ff2a1b2b13161908e20cdfdf24560b58d75b76912c235014eb4dc"), + resource.TestCheckResourceAttr("random_bytes.basic", "length", "32"), + ), + }, + { + Config: `resource "random_bytes" "basic" { + length = 32 + }`, + PlanOnly: true, + }, + }, + }) +} + +func TestAccResourceBytes_LengthErrors(t *testing.T) { + resource.UnitTest(t, resource.TestCase{ + ProtoV5ProviderFactories: protoV5ProviderFactories(), + Steps: []resource.TestStep{ + { + Config: `resource "random_bytes" "invalid_length" { + length = 0 + }`, + ExpectError: regexp.MustCompile(`.*Attribute length value must be at least 1, got: 0`), + }, + }, + }) +} + +func TestAccResourceBytes_Length_ForceReplacement(t *testing.T) { + var bytes1, bytes2 string + + resource.ParallelTest(t, resource.TestCase{ + Steps: []resource.TestStep{ + { + ProtoV5ProviderFactories: protoV5ProviderFactories(), + Config: `resource "random_bytes" "test" { + length = 1 + }`, + Check: resource.ComposeTestCheckFunc( + resource.TestCheckResourceAttr("random_bytes.test", "length", "1"), + testExtractResourceAttr("random_bytes.test", "result_base64", &bytes1), + resource.TestCheckResourceAttrWith("random_bytes.test", "result_hex", testCheckLen(2)), + ), + }, + { + ProtoV5ProviderFactories: protoV5ProviderFactories(), + Config: `resource "random_bytes" "test" { + length = 2 + }`, + Check: resource.ComposeTestCheckFunc( + resource.TestCheckResourceAttr("random_bytes.test", "length", "2"), + testExtractResourceAttr("random_bytes.test", "result_base64", &bytes2), + resource.TestCheckResourceAttrWith("random_bytes.test", "result_hex", testCheckLen(4)), + testCheckAttributeValuesDiffer(&bytes1, &bytes2), + ), + }, + }, + }) +} + +func TestAccResourceBytes_Keepers_Keep_EmptyMap(t *testing.T) { + var result1, result2 string + + resource.ParallelTest(t, resource.TestCase{ + Steps: []resource.TestStep{ + { + ProtoV5ProviderFactories: protoV5ProviderFactories(), + Config: `resource "random_bytes" "test" { + length = 12 + keepers = {} + }`, + Check: resource.ComposeTestCheckFunc( + testExtractResourceAttr("random_bytes.test", "result_hex", &result1), + resource.TestCheckResourceAttr("random_bytes.test", "keepers.%", "0"), + ), + }, + { + ProtoV5ProviderFactories: protoV5ProviderFactories(), + Config: `resource "random_bytes" "test" { + length = 12 + keepers = {} + }`, + Check: resource.ComposeTestCheckFunc( + testExtractResourceAttr("random_bytes.test", "result_hex", &result2), + testCheckAttributeValuesEqual(&result1, &result2), + resource.TestCheckResourceAttr("random_bytes.test", "keepers.%", "0"), + ), + }, + }, + }) +} + +func TestAccResourceBytes_Keepers_Keep_EmptyMapToNullValue(t *testing.T) { + var result1, result2 string + + resource.ParallelTest(t, resource.TestCase{ + Steps: []resource.TestStep{ + { + ProtoV5ProviderFactories: protoV5ProviderFactories(), + Config: `resource "random_bytes" "test" { + length = 12 + keepers = {} + }`, + Check: resource.ComposeTestCheckFunc( + testExtractResourceAttr("random_bytes.test", "result_hex", &result1), + resource.TestCheckResourceAttr("random_bytes.test", "keepers.%", "0"), + ), + }, + { + ProtoV5ProviderFactories: protoV5ProviderFactories(), + Config: `resource "random_bytes" "test" { + length = 12 + keepers = { + "key" = null + } + }`, + Check: resource.ComposeTestCheckFunc( + testExtractResourceAttr("random_bytes.test", "result_hex", &result2), + testCheckAttributeValuesEqual(&result1, &result2), + resource.TestCheckResourceAttr("random_bytes.test", "keepers.%", "1"), + ), + }, + }, + }) +} + +func TestAccResourceBytes_Keepers_Keep_NullMap(t *testing.T) { + var result1, result2 string + + resource.ParallelTest(t, resource.TestCase{ + Steps: []resource.TestStep{ + { + ProtoV5ProviderFactories: protoV5ProviderFactories(), + Config: `resource "random_bytes" "test" { + length = 12 + }`, + Check: resource.ComposeTestCheckFunc( + testExtractResourceAttr("random_bytes.test", "result_hex", &result1), + resource.TestCheckResourceAttr("random_bytes.test", "keepers.%", "0"), + ), + }, + { + ProtoV5ProviderFactories: protoV5ProviderFactories(), + Config: `resource "random_bytes" "test" { + length = 12 + }`, + Check: resource.ComposeTestCheckFunc( + testExtractResourceAttr("random_bytes.test", "result_hex", &result2), + testCheckAttributeValuesEqual(&result1, &result2), + resource.TestCheckResourceAttr("random_bytes.test", "keepers.%", "0"), + ), + }, + }, + }) +} + +func TestAccResourceBytes_Keepers_Keep_NullMapToNullValue(t *testing.T) { + var result1, result2 string + + resource.ParallelTest(t, resource.TestCase{ + Steps: []resource.TestStep{ + { + ProtoV5ProviderFactories: protoV5ProviderFactories(), + Config: `resource "random_bytes" "test" { + length = 12 + }`, + Check: resource.ComposeTestCheckFunc( + testExtractResourceAttr("random_bytes.test", "result_hex", &result1), + resource.TestCheckResourceAttr("random_bytes.test", "keepers.%", "0"), + ), + }, + { + ProtoV5ProviderFactories: protoV5ProviderFactories(), + Config: `resource "random_bytes" "test" { + length = 12 + keepers = { + "key" = null + } + }`, + Check: resource.ComposeTestCheckFunc( + testExtractResourceAttr("random_bytes.test", "result_hex", &result2), + testCheckAttributeValuesEqual(&result1, &result2), + resource.TestCheckResourceAttr("random_bytes.test", "keepers.%", "1"), + ), + }, + }, + }) +} + +func TestAccResourceBytes_Keepers_Keep_NullValue(t *testing.T) { + var result1, result2 string + + resource.ParallelTest(t, resource.TestCase{ + Steps: []resource.TestStep{ + { + ProtoV5ProviderFactories: protoV5ProviderFactories(), + Config: `resource "random_bytes" "test" { + length = 12 + keepers = { + "key" = null + } + }`, + Check: resource.ComposeTestCheckFunc( + testExtractResourceAttr("random_bytes.test", "result_hex", &result1), + resource.TestCheckResourceAttr("random_bytes.test", "keepers.%", "1"), + ), + }, + { + ProtoV5ProviderFactories: protoV5ProviderFactories(), + Config: `resource "random_bytes" "test" { + length = 12 + keepers = { + "key" = null + } + }`, + Check: resource.ComposeTestCheckFunc( + testExtractResourceAttr("random_bytes.test", "result_hex", &result2), + testCheckAttributeValuesEqual(&result1, &result2), + resource.TestCheckResourceAttr("random_bytes.test", "keepers.%", "1"), + ), + }, + }, + }) +} + +func TestAccResourceBytes_Keepers_Keep_NullValues(t *testing.T) { + var result1, result2 string + + resource.ParallelTest(t, resource.TestCase{ + Steps: []resource.TestStep{ + { + ProtoV5ProviderFactories: protoV5ProviderFactories(), + Config: `resource "random_bytes" "test" { + length = 12 + keepers = { + "key1" = null + "key2" = null + } + }`, + Check: resource.ComposeTestCheckFunc( + testExtractResourceAttr("random_bytes.test", "result_hex", &result1), + resource.TestCheckResourceAttr("random_bytes.test", "keepers.%", "2"), + ), + }, + { + ProtoV5ProviderFactories: protoV5ProviderFactories(), + Config: `resource "random_bytes" "test" { + length = 12 + keepers = { + "key1" = null + "key2" = null + } + }`, + Check: resource.ComposeTestCheckFunc( + testExtractResourceAttr("random_bytes.test", "result_hex", &result2), + testCheckAttributeValuesEqual(&result1, &result2), + resource.TestCheckResourceAttr("random_bytes.test", "keepers.%", "2"), + ), + }, + }, + }) +} + +func TestAccResourceBytes_Keepers_Keep_Value(t *testing.T) { + var result1, result2 string + + resource.ParallelTest(t, resource.TestCase{ + Steps: []resource.TestStep{ + { + ProtoV5ProviderFactories: protoV5ProviderFactories(), + Config: `resource "random_bytes" "test" { + length = 12 + keepers = { + "key" = "123" + } + }`, + Check: resource.ComposeTestCheckFunc( + testExtractResourceAttr("random_bytes.test", "result_hex", &result1), + resource.TestCheckResourceAttr("random_bytes.test", "keepers.%", "1"), + ), + }, + { + ProtoV5ProviderFactories: protoV5ProviderFactories(), + Config: `resource "random_bytes" "test" { + length = 12 + keepers = { + "key" = "123" + } + }`, + Check: resource.ComposeTestCheckFunc( + testExtractResourceAttr("random_bytes.test", "result_hex", &result2), + testCheckAttributeValuesEqual(&result1, &result2), + resource.TestCheckResourceAttr("random_bytes.test", "keepers.%", "1"), + ), + }, + }, + }) +} + +func TestAccResourceBytes_Keepers_Keep_Values(t *testing.T) { + var result1, result2 string + + resource.ParallelTest(t, resource.TestCase{ + Steps: []resource.TestStep{ + { + ProtoV5ProviderFactories: protoV5ProviderFactories(), + Config: `resource "random_bytes" "test" { + length = 12 + keepers = { + "key1" = "123" + "key2" = "456" + } + }`, + Check: resource.ComposeTestCheckFunc( + testExtractResourceAttr("random_bytes.test", "result_hex", &result1), + resource.TestCheckResourceAttr("random_bytes.test", "keepers.%", "2"), + ), + }, + { + ProtoV5ProviderFactories: protoV5ProviderFactories(), + Config: `resource "random_bytes" "test" { + length = 12 + keepers = { + "key1" = "123" + "key2" = "456" + } + }`, + Check: resource.ComposeTestCheckFunc( + testExtractResourceAttr("random_bytes.test", "result_hex", &result2), + testCheckAttributeValuesEqual(&result1, &result2), + resource.TestCheckResourceAttr("random_bytes.test", "keepers.%", "2"), + ), + }, + }, + }) +} + +func TestAccResourceBytes_Keepers_Replace_EmptyMapToValue(t *testing.T) { + var result1, result2 string + + resource.ParallelTest(t, resource.TestCase{ + Steps: []resource.TestStep{ + { + ProtoV5ProviderFactories: protoV5ProviderFactories(), + Config: `resource "random_bytes" "test" { + length = 12 + keepers = {} + }`, + Check: resource.ComposeTestCheckFunc( + testExtractResourceAttr("random_bytes.test", "result_hex", &result1), + resource.TestCheckResourceAttr("random_bytes.test", "keepers.%", "0"), + ), + }, + { + ProtoV5ProviderFactories: protoV5ProviderFactories(), + Config: `resource "random_bytes" "test" { + length = 12 + keepers = { + "key" = "123" + } + }`, + Check: resource.ComposeTestCheckFunc( + testExtractResourceAttr("random_bytes.test", "result_hex", &result2), + testCheckAttributeValuesDiffer(&result1, &result2), + resource.TestCheckResourceAttr("random_bytes.test", "keepers.%", "1"), + ), + }, + }, + }) +} + +func TestAccResourceBytes_Keepers_Replace_NullMapToValue(t *testing.T) { + var result1, result2 string + + resource.ParallelTest(t, resource.TestCase{ + Steps: []resource.TestStep{ + { + ProtoV5ProviderFactories: protoV5ProviderFactories(), + Config: `resource "random_bytes" "test" { + length = 12 + }`, + Check: resource.ComposeTestCheckFunc( + testExtractResourceAttr("random_bytes.test", "result_hex", &result1), + resource.TestCheckResourceAttr("random_bytes.test", "keepers.%", "0"), + ), + }, + { + ProtoV5ProviderFactories: protoV5ProviderFactories(), + Config: `resource "random_bytes" "test" { + length = 12 + keepers = { + "key" = "123" + } + }`, + Check: resource.ComposeTestCheckFunc( + testExtractResourceAttr("random_bytes.test", "result_hex", &result2), + testCheckAttributeValuesDiffer(&result1, &result2), + resource.TestCheckResourceAttr("random_bytes.test", "keepers.%", "1"), + ), + }, + }, + }) +} + +func TestAccResourceBytes_Keepers_Replace_NullValueToValue(t *testing.T) { + var result1, result2 string + + resource.ParallelTest(t, resource.TestCase{ + Steps: []resource.TestStep{ + { + ProtoV5ProviderFactories: protoV5ProviderFactories(), + Config: `resource "random_bytes" "test" { + length = 12 + keepers = { + "key" = null + } + }`, + Check: resource.ComposeTestCheckFunc( + testExtractResourceAttr("random_bytes.test", "result_hex", &result1), + resource.TestCheckResourceAttr("random_bytes.test", "keepers.%", "1"), + ), + }, + { + ProtoV5ProviderFactories: protoV5ProviderFactories(), + Config: `resource "random_bytes" "test" { + length = 12 + keepers = { + "key" = "123" + } + }`, + Check: resource.ComposeTestCheckFunc( + testExtractResourceAttr("random_bytes.test", "result_hex", &result2), + testCheckAttributeValuesDiffer(&result1, &result2), + resource.TestCheckResourceAttr("random_bytes.test", "keepers.%", "1"), + ), + }, + }, + }) +} + +func TestAccResourceBytes_Keepers_Replace_ValueToEmptyMap(t *testing.T) { + var result1, result2 string + + resource.ParallelTest(t, resource.TestCase{ + Steps: []resource.TestStep{ + { + ProtoV5ProviderFactories: protoV5ProviderFactories(), + Config: `resource "random_bytes" "test" { + length = 12 + keepers = { + "key" = "123" + } + }`, + Check: resource.ComposeTestCheckFunc( + testExtractResourceAttr("random_bytes.test", "result_hex", &result1), + resource.TestCheckResourceAttr("random_bytes.test", "keepers.%", "1"), + ), + }, + { + ProtoV5ProviderFactories: protoV5ProviderFactories(), + Config: `resource "random_bytes" "test" { + length = 12 + keepers = {} + }`, + Check: resource.ComposeTestCheckFunc( + testExtractResourceAttr("random_bytes.test", "result_hex", &result2), + testCheckAttributeValuesDiffer(&result1, &result2), + resource.TestCheckResourceAttr("random_bytes.test", "keepers.%", "0"), + ), + }, + }, + }) +} + +func TestAccResourceBytes_Keepers_Replace_ValueToNullMap(t *testing.T) { + var result1, result2 string + + resource.ParallelTest(t, resource.TestCase{ + Steps: []resource.TestStep{ + { + ProtoV5ProviderFactories: protoV5ProviderFactories(), + Config: `resource "random_bytes" "test" { + length = 12 + keepers = { + "key" = "123" + } + }`, + Check: resource.ComposeTestCheckFunc( + testExtractResourceAttr("random_bytes.test", "result_hex", &result1), + resource.TestCheckResourceAttr("random_bytes.test", "keepers.%", "1"), + ), + }, + { + ProtoV5ProviderFactories: protoV5ProviderFactories(), + Config: `resource "random_bytes" "test" { + length = 12 + }`, + Check: resource.ComposeTestCheckFunc( + testExtractResourceAttr("random_bytes.test", "result_hex", &result2), + testCheckAttributeValuesDiffer(&result1, &result2), + resource.TestCheckResourceAttr("random_bytes.test", "keepers.%", "0"), + ), + }, + }, + }) +} + +func TestAccResourceBytes_Keepers_Replace_ValueToNullValue(t *testing.T) { + var result1, result2 string + + resource.ParallelTest(t, resource.TestCase{ + Steps: []resource.TestStep{ + { + ProtoV5ProviderFactories: protoV5ProviderFactories(), + Config: `resource "random_bytes" "test" { + length = 12 + keepers = { + "key" = "123" + } + }`, + Check: resource.ComposeTestCheckFunc( + testExtractResourceAttr("random_bytes.test", "result_hex", &result1), + resource.TestCheckResourceAttr("random_bytes.test", "keepers.%", "1"), + ), + }, + { + ProtoV5ProviderFactories: protoV5ProviderFactories(), + Config: `resource "random_bytes" "test" { + length = 12 + keepers = { + "key" = null + } + }`, + Check: resource.ComposeTestCheckFunc( + testExtractResourceAttr("random_bytes.test", "result_hex", &result2), + testCheckAttributeValuesDiffer(&result1, &result2), + resource.TestCheckResourceAttr("random_bytes.test", "keepers.%", "1"), + ), + }, + }, + }) +} + +func TestAccResourceBytes_Keepers_Replace_ValueToNewValue(t *testing.T) { + var result1, result2 string + + resource.ParallelTest(t, resource.TestCase{ + Steps: []resource.TestStep{ + { + ProtoV5ProviderFactories: protoV5ProviderFactories(), + Config: `resource "random_bytes" "test" { + length = 12 + keepers = { + "key" = "123" + } + }`, + Check: resource.ComposeTestCheckFunc( + testExtractResourceAttr("random_bytes.test", "result_hex", &result1), + resource.TestCheckResourceAttr("random_bytes.test", "keepers.%", "1"), + ), + }, + { + ProtoV5ProviderFactories: protoV5ProviderFactories(), + Config: `resource "random_bytes" "test" { + length = 12 + keepers = { + "key" = "456" + } + }`, + Check: resource.ComposeTestCheckFunc( + testExtractResourceAttr("random_bytes.test", "result_hex", &result2), + testCheckAttributeValuesDiffer(&result1, &result2), + resource.TestCheckResourceAttr("random_bytes.test", "keepers.%", "1"), + ), + }, + }, + }) +}