diff --git a/internal/provider/diagnostics.go b/internal/diagnostics/diagnostics.go similarity index 70% rename from internal/provider/diagnostics.go rename to internal/diagnostics/diagnostics.go index ef87d2eb..35ab13da 100644 --- a/internal/provider/diagnostics.go +++ b/internal/diagnostics/diagnostics.go @@ -1,4 +1,4 @@ -package provider +package diagnostics import ( "fmt" @@ -6,27 +6,27 @@ import ( "github.com/hashicorp/terraform-plugin-framework/diag" ) -const retryMsg = "Retry the Terraform operation. If the error still occurs or happens regularly, please contact the provider developer with hardware and operating system information.\n\n" +const RetryMsg = "Retry the Terraform operation. If the error still occurs or happens regularly, please contact the provider developer with hardware and operating system information.\n\n" -func randomReadError(errMsg string) diag.Diagnostics { +func RandomReadError(errMsg string) diag.Diagnostics { var diags diag.Diagnostics diags.AddError( "Random Read Error", "While attempting to generate a random value for this resource, a read error was generated.\n\n"+ - retryMsg+ + RetryMsg+ fmt.Sprintf("Original Error: %s", errMsg), ) return diags } -func hashGenerationError(errMsg string) diag.Diagnostics { +func HashGenerationError(errMsg string) diag.Diagnostics { var diags diag.Diagnostics diags.AddError( "Hash Generation Error", - "While attempting to generate a hash from of the password an error occurred.\n\n"+ + "While attempting to generate a hash from the password an error occurred.\n\n"+ "Verify that the state contains a populated 'result' field, using 'terraform state show', and retry the operation\n\n"+ fmt.Sprintf("Original Error: %s", errMsg), ) diff --git a/internal/provider/models.go b/internal/provider/models.go deleted file mode 100644 index 611bf687..00000000 --- a/internal/provider/models.go +++ /dev/null @@ -1,128 +0,0 @@ -package provider - -import "github.com/hashicorp/terraform-plugin-framework/types" - -type IDModel struct { - ID types.String `tfsdk:"id"` - Keepers types.Map `tfsdk:"keepers"` - ByteLength types.Int64 `tfsdk:"byte_length"` - Prefix types.String `tfsdk:"prefix"` - B64URL types.String `tfsdk:"b64_url"` - B64Std types.String `tfsdk:"b64_std"` - Hex types.String `tfsdk:"hex"` - Dec types.String `tfsdk:"dec"` -} - -type IntegerModel struct { - ID types.String `tfsdk:"id"` - Keepers types.Map `tfsdk:"keepers"` - Min types.Int64 `tfsdk:"min"` - Max types.Int64 `tfsdk:"max"` - Seed types.String `tfsdk:"seed"` - Result types.Int64 `tfsdk:"result"` -} - -type PasswordModelV2 struct { - ID types.String `tfsdk:"id"` - Keepers types.Map `tfsdk:"keepers"` - 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"` -} - -type PasswordModelV1 struct { - ID types.String `tfsdk:"id"` - Keepers types.Map `tfsdk:"keepers"` - Length types.Int64 `tfsdk:"length"` - Special types.Bool `tfsdk:"special"` - Upper types.Bool `tfsdk:"upper"` - Lower types.Bool `tfsdk:"lower"` - Number types.Bool `tfsdk:"number"` - 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"` -} - -type PasswordModelV0 struct { - ID types.String `tfsdk:"id"` - Keepers types.Map `tfsdk:"keepers"` - Length types.Int64 `tfsdk:"length"` - Special types.Bool `tfsdk:"special"` - Upper types.Bool `tfsdk:"upper"` - Lower types.Bool `tfsdk:"lower"` - Number types.Bool `tfsdk:"number"` - 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"` -} - -type PetNameModel struct { - ID types.String `tfsdk:"id"` - Keepers types.Map `tfsdk:"keepers"` - Length types.Int64 `tfsdk:"length"` - Prefix types.String `tfsdk:"prefix"` - Separator types.String `tfsdk:"separator"` -} - -type ShuffleModel struct { - ID types.String `tfsdk:"id"` - Keepers types.Map `tfsdk:"keepers"` - Seed types.String `tfsdk:"seed"` - Input types.List `tfsdk:"input"` - ResultCount types.Int64 `tfsdk:"result_count"` - Result types.List `tfsdk:"result"` -} - -type StringModelV2 struct { - ID types.String `tfsdk:"id"` - Keepers types.Map `tfsdk:"keepers"` - 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"` -} - -type StringModelV1 struct { - ID types.String `tfsdk:"id"` - Keepers types.Map `tfsdk:"keepers"` - Length types.Int64 `tfsdk:"length"` - Special types.Bool `tfsdk:"special"` - Upper types.Bool `tfsdk:"upper"` - Lower types.Bool `tfsdk:"lower"` - Number types.Bool `tfsdk:"number"` - 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"` -} - -type UUIDModel struct { - ID types.String `tfsdk:"id"` - Keepers types.Map `tfsdk:"keepers"` - Result types.String `tfsdk:"result"` -} diff --git a/internal/provider/provider.go b/internal/provider/provider.go index 6986b38e..8d3d39a4 100644 --- a/internal/provider/provider.go +++ b/internal/provider/provider.go @@ -5,34 +5,43 @@ import ( "github.com/hashicorp/terraform-plugin-framework/diag" "github.com/hashicorp/terraform-plugin-framework/tfsdk" -) -type provider struct { -} + "github.com/terraform-providers/terraform-provider-random/internal/resources/id" + "github.com/terraform-providers/terraform-provider-random/internal/resources/integer" + "github.com/terraform-providers/terraform-provider-random/internal/resources/password" + "github.com/terraform-providers/terraform-provider-random/internal/resources/pet" + "github.com/terraform-providers/terraform-provider-random/internal/resources/shuffle" + stringresource "github.com/terraform-providers/terraform-provider-random/internal/resources/string" + "github.com/terraform-providers/terraform-provider-random/internal/resources/uuid" +) -func NewFramework() tfsdk.Provider { +func NewProvider() tfsdk.Provider { return &provider{} } -func (p *provider) GetSchema(ctx context.Context) (tfsdk.Schema, diag.Diagnostics) { +var _ tfsdk.Provider = (*provider)(nil) + +type provider struct{} + +func (p *provider) GetSchema(context.Context) (tfsdk.Schema, diag.Diagnostics) { return tfsdk.Schema{}, nil } -func (p *provider) Configure(ctx context.Context, req tfsdk.ConfigureProviderRequest, resp *tfsdk.ConfigureProviderResponse) { +func (p *provider) Configure(context.Context, tfsdk.ConfigureProviderRequest, *tfsdk.ConfigureProviderResponse) { } -func (p *provider) GetResources(ctx context.Context) (map[string]tfsdk.ResourceType, diag.Diagnostics) { +func (p *provider) GetResources(context.Context) (map[string]tfsdk.ResourceType, diag.Diagnostics) { return map[string]tfsdk.ResourceType{ - "random_id": resourceIDType{}, - "random_integer": resourceIntegerType{}, - "random_password": resourcePasswordType{}, - "random_pet": resourcePetType{}, - "random_shuffle": resourceShuffleType{}, - "random_string": resourceStringType{}, - "random_uuid": resourceUUIDType{}, + "random_id": id.NewResourceType(), + "random_integer": integer.NewResourceType(), + "random_password": password.NewResourceType(), + "random_pet": pet.NewResourceType(), + "random_shuffle": shuffle.NewResourceType(), + "random_string": stringresource.NewResourceType(), + "random_uuid": uuid.NewResourceType(), }, nil } -func (p *provider) GetDataSources(ctx context.Context) (map[string]tfsdk.DataSourceType, diag.Diagnostics) { +func (p *provider) GetDataSources(context.Context) (map[string]tfsdk.DataSourceType, diag.Diagnostics) { return map[string]tfsdk.DataSourceType{}, nil } diff --git a/internal/provider/resource_id_test.go b/internal/provider/provider_resource_id_test.go similarity index 93% rename from internal/provider/resource_id_test.go rename to internal/provider/provider_resource_id_test.go index a1ca08b4..82106148 100644 --- a/internal/provider/resource_id_test.go +++ b/internal/provider/provider_resource_id_test.go @@ -8,7 +8,6 @@ import ( func TestAccResourceID(t *testing.T) { resource.UnitTest(t, resource.TestCase{ - PreCheck: func() { testAccPreCheck(t) }, ProtoV6ProviderFactories: testAccProtoV6ProviderFactories(), Steps: []resource.TestStep{ { @@ -33,7 +32,6 @@ func TestAccResourceID(t *testing.T) { func TestAccResourceID_importWithPrefix(t *testing.T) { resource.UnitTest(t, resource.TestCase{ - PreCheck: func() { testAccPreCheck(t) }, ProtoV6ProviderFactories: testAccProtoV6ProviderFactories(), Steps: []resource.TestStep{ { diff --git a/internal/provider/resource_integer_test.go b/internal/provider/provider_resource_integer_test.go similarity index 92% rename from internal/provider/resource_integer_test.go rename to internal/provider/provider_resource_integer_test.go index 0bbaa24c..9878fd05 100644 --- a/internal/provider/resource_integer_test.go +++ b/internal/provider/provider_resource_integer_test.go @@ -10,7 +10,6 @@ import ( func TestAccResourceIntegerBasic(t *testing.T) { t.Parallel() resource.UnitTest(t, resource.TestCase{ - PreCheck: func() { testAccPreCheck(t) }, ProtoV6ProviderFactories: testAccProtoV6ProviderFactories(), Steps: []resource.TestStep{ { @@ -36,7 +35,6 @@ func TestAccResourceIntegerBasic(t *testing.T) { func TestAccResourceIntegerUpdate(t *testing.T) { t.Parallel() resource.UnitTest(t, resource.TestCase{ - PreCheck: func() { testAccPreCheck(t) }, ProtoV6ProviderFactories: testAccProtoV6ProviderFactories(), Steps: []resource.TestStep{ { @@ -66,7 +64,6 @@ func TestAccResourceIntegerUpdate(t *testing.T) { func TestAccResourceIntegerSeedless_to_seeded(t *testing.T) { t.Parallel() resource.UnitTest(t, resource.TestCase{ - PreCheck: func() { testAccPreCheck(t) }, ProtoV6ProviderFactories: testAccProtoV6ProviderFactories(), Steps: []resource.TestStep{ { @@ -95,7 +92,6 @@ func TestAccResourceIntegerSeedless_to_seeded(t *testing.T) { func TestAccResourceIntegerSeeded_to_seedless(t *testing.T) { t.Parallel() resource.UnitTest(t, resource.TestCase{ - PreCheck: func() { testAccPreCheck(t) }, ProtoV6ProviderFactories: testAccProtoV6ProviderFactories(), Steps: []resource.TestStep{ { @@ -124,7 +120,6 @@ func TestAccResourceIntegerSeeded_to_seedless(t *testing.T) { func TestAccResourceIntegerBig(t *testing.T) { t.Parallel() resource.UnitTest(t, resource.TestCase{ - PreCheck: func() { testAccPreCheck(t) }, ProtoV6ProviderFactories: testAccProtoV6ProviderFactories(), Steps: []resource.TestStep{ { diff --git a/internal/provider/resource_password_test.go b/internal/provider/provider_resource_password_test.go similarity index 77% rename from internal/provider/resource_password_test.go rename to internal/provider/provider_resource_password_test.go index b688a22f..c3f909a5 100644 --- a/internal/provider/resource_password_test.go +++ b/internal/provider/provider_resource_password_test.go @@ -1,23 +1,16 @@ package provider import ( - "context" "fmt" "regexp" "testing" - "github.com/google/go-cmp/cmp" - "github.com/hashicorp/terraform-plugin-framework/tfsdk" - "github.com/hashicorp/terraform-plugin-framework/types" - "github.com/hashicorp/terraform-plugin-go/tftypes" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource" "github.com/hashicorp/terraform-plugin-sdk/v2/terraform" - "golang.org/x/crypto/bcrypt" ) func TestAccResourcePasswordBasic(t *testing.T) { resource.UnitTest(t, resource.TestCase{ - PreCheck: func() { testAccPreCheck(t) }, ProtoV6ProviderFactories: testAccProtoV6ProviderFactories(), Steps: []resource.TestStep{ { @@ -55,7 +48,6 @@ func TestAccResourcePasswordBasic(t *testing.T) { func TestAccResourcePasswordOverride(t *testing.T) { resource.UnitTest(t, resource.TestCase{ - PreCheck: func() { testAccPreCheck(t) }, ProtoV6ProviderFactories: testAccProtoV6ProviderFactories(), Steps: []resource.TestStep{ { @@ -480,6 +472,7 @@ func TestAccResourcePassword_StateUpgrade_V1toV2(t *testing.T) { c.configDuringUpgrade = c.configBeforeUpgrade } + // TODO: Why is resource.Test not being used here resource.UnitTest(t, resource.TestCase{ Steps: []resource.TestStep{ { @@ -503,7 +496,6 @@ func TestAccResourcePassword_StateUpgrade_V1toV2(t *testing.T) { func TestAccResourcePasswordMin(t *testing.T) { resource.UnitTest(t, resource.TestCase{ - PreCheck: func() { testAccPreCheck(t) }, ProtoV6ProviderFactories: testAccProtoV6ProviderFactories(), Steps: []resource.TestStep{ { @@ -526,131 +518,3 @@ func TestAccResourcePasswordMin(t *testing.T) { }, }) } - -func TestMigratePasswordStateV0toV2(t *testing.T) { - raw := tftypes.NewValue(tftypes.Object{}, map[string]tftypes.Value{ - "id": tftypes.NewValue(tftypes.String, "none"), - "keepers": tftypes.NewValue(tftypes.Map{ElementType: tftypes.String}, nil), - "length": tftypes.NewValue(tftypes.Number, 16), - "lower": tftypes.NewValue(tftypes.Bool, true), - "min_lower": tftypes.NewValue(tftypes.Number, 0), - "min_numeric": tftypes.NewValue(tftypes.Number, 0), - "min_special": tftypes.NewValue(tftypes.Number, 0), - "min_upper": tftypes.NewValue(tftypes.Number, 0), - "number": tftypes.NewValue(tftypes.Bool, true), - "override_special": tftypes.NewValue(tftypes.String, "!#$%\u0026*()-_=+[]{}\u003c\u003e:?"), - "result": tftypes.NewValue(tftypes.String, "DZy_3*tnonj%Q%Yx"), - "special": tftypes.NewValue(tftypes.Bool, true), - "upper": tftypes.NewValue(tftypes.Bool, true), - }) - - req := tfsdk.UpgradeResourceStateRequest{ - State: &tfsdk.State{ - Raw: raw, - Schema: passwordSchemaV0(), - }, - } - - resp := &tfsdk.UpgradeResourceStateResponse{ - State: tfsdk.State{ - Schema: passwordSchemaV2(), - }, - } - - upgradePasswordStateV0toV2(context.Background(), req, resp) - - expected := PasswordModelV2{ - ID: types.String{Value: "none"}, - Keepers: types.Map{Null: true, ElemType: types.StringType}, - Length: types.Int64{Value: 16}, - Special: types.Bool{Value: true}, - Upper: types.Bool{Value: true}, - Lower: types.Bool{Value: true}, - Numeric: types.Bool{Value: true}, - MinNumeric: types.Int64{Value: 0}, - MinUpper: types.Int64{Value: 0}, - MinLower: types.Int64{Value: 0}, - MinSpecial: types.Int64{Value: 0}, - OverrideSpecial: types.String{Value: "!#$%\u0026*()-_=+[]{}\u003c\u003e:?"}, - Result: types.String{Value: "DZy_3*tnonj%Q%Yx"}, - } - - actual := PasswordModelV2{} - diags := resp.State.Get(context.Background(), &actual) - if diags.HasError() { - t.Errorf("error getting state: %v", diags) - } - - err := bcrypt.CompareHashAndPassword([]byte(actual.BcryptHash.Value), []byte(actual.Result.Value)) - if err != nil { - t.Errorf("unexpected bcrypt comparison error: %s", err) - } - - // Setting actual.BcryptHash to zero value to allow direct comparison of expected and actual. - actual.BcryptHash = types.String{} - - if !cmp.Equal(expected, actual) { - t.Errorf("expected: %+v, got: %+v", expected, actual) - } -} - -func TestMigratePasswordStateV1toV2(t *testing.T) { - raw := tftypes.NewValue(tftypes.Object{}, map[string]tftypes.Value{ - "id": tftypes.NewValue(tftypes.String, "none"), - "keepers": tftypes.NewValue(tftypes.Map{ElementType: tftypes.String}, nil), - "length": tftypes.NewValue(tftypes.Number, 16), - "lower": tftypes.NewValue(tftypes.Bool, true), - "min_lower": tftypes.NewValue(tftypes.Number, 0), - "min_numeric": tftypes.NewValue(tftypes.Number, 0), - "min_special": tftypes.NewValue(tftypes.Number, 0), - "min_upper": tftypes.NewValue(tftypes.Number, 0), - "number": tftypes.NewValue(tftypes.Bool, true), - "override_special": tftypes.NewValue(tftypes.String, "!#$%\u0026*()-_=+[]{}\u003c\u003e:?"), - "result": tftypes.NewValue(tftypes.String, "DZy_3*tnonj%Q%Yx"), - "special": tftypes.NewValue(tftypes.Bool, true), - "upper": tftypes.NewValue(tftypes.Bool, true), - "bcrypt_hash": tftypes.NewValue(tftypes.String, "bcrypt_hash"), - }) - - req := tfsdk.UpgradeResourceStateRequest{ - State: &tfsdk.State{ - Raw: raw, - Schema: passwordSchemaV1(), - }, - } - - resp := &tfsdk.UpgradeResourceStateResponse{ - State: tfsdk.State{ - Schema: passwordSchemaV2(), - }, - } - - upgradePasswordStateV1toV2(context.Background(), req, resp) - - expected := PasswordModelV2{ - ID: types.String{Value: "none"}, - Keepers: types.Map{Null: true, ElemType: types.StringType}, - Length: types.Int64{Value: 16}, - Special: types.Bool{Value: true}, - Upper: types.Bool{Value: true}, - Lower: types.Bool{Value: true}, - Numeric: types.Bool{Value: true}, - MinNumeric: types.Int64{Value: 0}, - MinUpper: types.Int64{Value: 0}, - MinLower: types.Int64{Value: 0}, - MinSpecial: types.Int64{Value: 0}, - OverrideSpecial: types.String{Value: "!#$%\u0026*()-_=+[]{}\u003c\u003e:?"}, - BcryptHash: types.String{Value: "bcrypt_hash"}, - Result: types.String{Value: "DZy_3*tnonj%Q%Yx"}, - } - - actual := PasswordModelV2{} - diags := resp.State.Get(context.Background(), &actual) - if diags.HasError() { - t.Errorf("error getting state: %v", diags) - } - - if !cmp.Equal(expected, actual) { - t.Errorf("expected: %+v, got: %+v", expected, actual) - } -} diff --git a/internal/provider/resource_pet_test.go b/internal/provider/provider_resource_pet_test.go similarity index 90% rename from internal/provider/resource_pet_test.go rename to internal/provider/provider_resource_pet_test.go index 2a78cffd..a2ab2867 100644 --- a/internal/provider/resource_pet_test.go +++ b/internal/provider/provider_resource_pet_test.go @@ -11,7 +11,6 @@ import ( func TestAccResourcePet_basic(t *testing.T) { resource.UnitTest(t, resource.TestCase{ - PreCheck: func() { testAccPreCheck(t) }, ProtoV6ProviderFactories: testAccProtoV6ProviderFactories(), Steps: []resource.TestStep{ { @@ -27,7 +26,6 @@ func TestAccResourcePet_basic(t *testing.T) { func TestAccResourcePet_length(t *testing.T) { resource.UnitTest(t, resource.TestCase{ - PreCheck: func() { testAccPreCheck(t) }, ProtoV6ProviderFactories: testAccProtoV6ProviderFactories(), Steps: []resource.TestStep{ { @@ -44,7 +42,6 @@ func TestAccResourcePet_length(t *testing.T) { func TestAccResourcePet_prefix(t *testing.T) { resource.UnitTest(t, resource.TestCase{ - PreCheck: func() { testAccPreCheck(t) }, ProtoV6ProviderFactories: testAccProtoV6ProviderFactories(), Steps: []resource.TestStep{ { @@ -62,7 +59,6 @@ func TestAccResourcePet_prefix(t *testing.T) { func TestAccResourcePet_separator(t *testing.T) { resource.UnitTest(t, resource.TestCase{ - PreCheck: func() { testAccPreCheck(t) }, ProtoV6ProviderFactories: testAccProtoV6ProviderFactories(), Steps: []resource.TestStep{ { diff --git a/internal/provider/resource_shuffle_test.go b/internal/provider/provider_resource_shuffle_test.go similarity index 93% rename from internal/provider/resource_shuffle_test.go rename to internal/provider/provider_resource_shuffle_test.go index 14899cf5..79ddfa34 100644 --- a/internal/provider/resource_shuffle_test.go +++ b/internal/provider/provider_resource_shuffle_test.go @@ -20,7 +20,6 @@ import ( // guaranteed consistent across Terraform releases. func TestAccResourceShuffleDefault(t *testing.T) { resource.UnitTest(t, resource.TestCase{ - PreCheck: func() { testAccPreCheck(t) }, ProtoV6ProviderFactories: testAccProtoV6ProviderFactories(), Steps: []resource.TestStep{ { @@ -38,7 +37,6 @@ func TestAccResourceShuffleDefault(t *testing.T) { func TestAccResourceShuffleShorter(t *testing.T) { resource.UnitTest(t, resource.TestCase{ - PreCheck: func() { testAccPreCheck(t) }, ProtoV6ProviderFactories: testAccProtoV6ProviderFactories(), Steps: []resource.TestStep{ { @@ -56,7 +54,6 @@ func TestAccResourceShuffleShorter(t *testing.T) { func TestAccResourceShuffleLonger(t *testing.T) { resource.UnitTest(t, resource.TestCase{ - PreCheck: func() { testAccPreCheck(t) }, ProtoV6ProviderFactories: testAccProtoV6ProviderFactories(), Steps: []resource.TestStep{ { @@ -74,7 +71,6 @@ func TestAccResourceShuffleLonger(t *testing.T) { func TestAccResourceShuffleEmpty(t *testing.T) { resource.UnitTest(t, resource.TestCase{ - PreCheck: func() { testAccPreCheck(t) }, ProtoV6ProviderFactories: testAccProtoV6ProviderFactories(), Steps: []resource.TestStep{ { @@ -92,7 +88,6 @@ func TestAccResourceShuffleEmpty(t *testing.T) { func TestAccResourceShuffleOne(t *testing.T) { resource.UnitTest(t, resource.TestCase{ - PreCheck: func() { testAccPreCheck(t) }, ProtoV6ProviderFactories: testAccProtoV6ProviderFactories(), Steps: []resource.TestStep{ { diff --git a/internal/provider/resource_string_test.go b/internal/provider/provider_resource_string_test.go similarity index 97% rename from internal/provider/resource_string_test.go rename to internal/provider/provider_resource_string_test.go index ac77350c..8bb5c0be 100644 --- a/internal/provider/resource_string_test.go +++ b/internal/provider/provider_resource_string_test.go @@ -10,7 +10,6 @@ import ( func TestAccResourceString(t *testing.T) { resource.UnitTest(t, resource.TestCase{ - PreCheck: func() { testAccPreCheck(t) }, ProtoV6ProviderFactories: testAccProtoV6ProviderFactories(), Steps: []resource.TestStep{ { @@ -32,7 +31,6 @@ func TestAccResourceString(t *testing.T) { func TestAccResourceStringOverride(t *testing.T) { resource.UnitTest(t, resource.TestCase{ - PreCheck: func() { testAccPreCheck(t) }, ProtoV6ProviderFactories: testAccProtoV6ProviderFactories(), Steps: []resource.TestStep{ { @@ -54,7 +52,6 @@ func TestAccResourceStringOverride(t *testing.T) { func TestAccResourceStringMin(t *testing.T) { resource.UnitTest(t, resource.TestCase{ - PreCheck: func() { testAccPreCheck(t) }, ProtoV6ProviderFactories: testAccProtoV6ProviderFactories(), Steps: []resource.TestStep{ { @@ -284,7 +281,6 @@ func TestAccResourceString_StateUpgrade_V1toV2(t *testing.T) { func TestAccResourceStringErrors(t *testing.T) { resource.UnitTest(t, resource.TestCase{ - PreCheck: func() { testAccPreCheck(t) }, ProtoV6ProviderFactories: testAccProtoV6ProviderFactories(), Steps: []resource.TestStep{ { diff --git a/internal/provider/resource_uuid_test.go b/internal/provider/provider_resource_uuid_test.go similarity index 92% rename from internal/provider/resource_uuid_test.go rename to internal/provider/provider_resource_uuid_test.go index 8ddfa1cd..9643d7fd 100644 --- a/internal/provider/resource_uuid_test.go +++ b/internal/provider/provider_resource_uuid_test.go @@ -9,7 +9,6 @@ import ( func TestAccResourceUUID(t *testing.T) { resource.UnitTest(t, resource.TestCase{ - PreCheck: func() { testAccPreCheck(t) }, ProtoV6ProviderFactories: testAccProtoV6ProviderFactories(), Steps: []resource.TestStep{ { diff --git a/internal/provider/provider_test.go b/internal/provider/provider_test.go index bafa1be2..46d1e872 100644 --- a/internal/provider/provider_test.go +++ b/internal/provider/provider_test.go @@ -1,20 +1,15 @@ package provider import ( - "testing" - "github.com/hashicorp/terraform-plugin-framework/providerserver" "github.com/hashicorp/terraform-plugin-go/tfprotov6" ) -func testAccPreCheck(t *testing.T) { -} - //nolint:unparam func testAccProtoV6ProviderFactories() map[string]func() (tfprotov6.ProviderServer, error) { return map[string]func() (tfprotov6.ProviderServer, error){ "random": func() (tfprotov6.ProviderServer, error) { - return providerserver.NewProtocol6(NewFramework())(), nil + return providerserver.NewProtocol6(NewProvider())(), nil }, } } diff --git a/internal/provider/string.go b/internal/random/random.go similarity index 61% rename from internal/provider/string.go rename to internal/random/random.go index 93e911f7..3bf98bc3 100644 --- a/internal/provider/string.go +++ b/internal/random/random.go @@ -1,4 +1,4 @@ -package provider +package random import ( "crypto/rand" @@ -6,52 +6,52 @@ import ( "sort" ) -type randomStringParams struct { - length int64 - upper bool - minUpper int64 - lower bool - minLower int64 - numeric bool - minNumeric int64 - special bool - minSpecial int64 - overrideSpecial string +type RandomStringParams struct { + Length int64 + Upper bool + MinUpper int64 + Lower bool + MinLower int64 + Numeric bool + MinNumeric int64 + Special bool + MinSpecial int64 + OverrideSpecial string } -func createRandomString(input randomStringParams) ([]byte, error) { +func CreateRandomString(input RandomStringParams) ([]byte, error) { const numChars = "0123456789" const lowerChars = "abcdefghijklmnopqrstuvwxyz" const upperChars = "ABCDEFGHIJKLMNOPQRSTUVWXYZ" var specialChars = "!@#$%&*()-_=+[]{}<>:?" var result []byte - if input.overrideSpecial != "" { - specialChars = input.overrideSpecial + if input.OverrideSpecial != "" { + specialChars = input.OverrideSpecial } var chars = "" - if input.upper { + if input.Upper { chars += upperChars } - if input.lower { + if input.Lower { chars += lowerChars } - if input.numeric { + if input.Numeric { chars += numChars } - if input.special { + if input.Special { chars += specialChars } minMapping := map[string]int64{ - numChars: input.minNumeric, - lowerChars: input.minLower, - upperChars: input.minUpper, - specialChars: input.minSpecial, + numChars: input.MinNumeric, + lowerChars: input.MinLower, + upperChars: input.MinUpper, + specialChars: input.MinSpecial, } - result = make([]byte, 0, input.length) + result = make([]byte, 0, input.Length) for k, v := range minMapping { s, err := generateRandomBytes(&k, v) @@ -61,7 +61,7 @@ func createRandomString(input randomStringParams) ([]byte, error) { result = append(result, s...) } - s, err := generateRandomBytes(&chars, input.length-int64(len(result))) + s, err := generateRandomBytes(&chars, input.Length-int64(len(result))) if err != nil { return nil, err } diff --git a/internal/provider/seed.go b/internal/random/seed.go similarity index 96% rename from internal/provider/seed.go rename to internal/random/seed.go index 4d3c5265..b0bcf8bb 100644 --- a/internal/provider/seed.go +++ b/internal/random/seed.go @@ -1,4 +1,4 @@ -package provider +package random import ( "hash/crc64" diff --git a/internal/provider/resource_id.go b/internal/resources/id/resource.go similarity index 73% rename from internal/provider/resource_id.go rename to internal/resources/id/resource.go index 87059669..03d49929 100644 --- a/internal/provider/resource_id.go +++ b/internal/resources/id/resource.go @@ -1,4 +1,4 @@ -package provider +package id import ( "context" @@ -12,11 +12,19 @@ import ( "github.com/hashicorp/terraform-plugin-framework/diag" "github.com/hashicorp/terraform-plugin-framework/tfsdk" "github.com/hashicorp/terraform-plugin-framework/types" + + "github.com/terraform-providers/terraform-provider-random/internal/diagnostics" ) -type resourceIDType struct{} +func NewResourceType() *resourceType { + return &resourceType{} +} + +var _ tfsdk.ResourceType = (*resourceType)(nil) + +type resourceType struct{} -func (r resourceIDType) GetSchema(context.Context) (tfsdk.Schema, diag.Diagnostics) { +func (r *resourceType) GetSchema(context.Context) (tfsdk.Schema, diag.Diagnostics) { return tfsdk.Schema{ Description: ` The resource ` + "`random_id`" + ` generates random numbers that are intended to be @@ -39,22 +47,28 @@ exist concurrently. Type: types.MapType{ ElemType: types.StringType, }, - Optional: true, - PlanModifiers: []tfsdk.AttributePlanModifier{tfsdk.RequiresReplace()}, + Optional: true, + PlanModifiers: []tfsdk.AttributePlanModifier{ + tfsdk.RequiresReplace(), + }, }, "byte_length": { Description: "The number of random bytes to produce. The minimum value is 1, which produces " + "eight bits of randomness.", - Type: types.Int64Type, - Required: true, - PlanModifiers: []tfsdk.AttributePlanModifier{tfsdk.RequiresReplace()}, + Type: types.Int64Type, + Required: true, + PlanModifiers: []tfsdk.AttributePlanModifier{ + tfsdk.RequiresReplace(), + }, }, "prefix": { Description: "Arbitrary string to prefix the output value with. This string is supplied as-is, " + "meaning it is not guaranteed to be URL-safe or base64 encoded.", - Type: types.StringType, - Optional: true, - PlanModifiers: []tfsdk.AttributePlanModifier{tfsdk.RequiresReplace()}, + Type: types.StringType, + Optional: true, + PlanModifiers: []tfsdk.AttributePlanModifier{ + tfsdk.RequiresReplace(), + }, }, "b64_url": { Description: "The generated id presented in base64, using the URL-friendly character set: " + @@ -87,18 +101,19 @@ exist concurrently. }, nil } -func (r resourceIDType) NewResource(_ context.Context, p tfsdk.Provider) (tfsdk.Resource, diag.Diagnostics) { - return resourceID{ - p: *(p.(*provider)), - }, nil +func (r *resourceType) NewResource(_ context.Context, p tfsdk.Provider) (tfsdk.Resource, diag.Diagnostics) { + return &resource{}, nil } -type resourceID struct { - p provider -} +var ( + _ tfsdk.Resource = (*resource)(nil) + _ tfsdk.ResourceWithImportState = (*resource)(nil) +) -func (r resourceID) Create(ctx context.Context, req tfsdk.CreateResourceRequest, resp *tfsdk.CreateResourceResponse) { - var plan IDModel +type resource struct{} + +func (r *resource) Create(ctx context.Context, req tfsdk.CreateResourceRequest, resp *tfsdk.CreateResourceResponse) { + var plan modelV0 diags := req.Plan.Get(ctx, &plan) resp.Diagnostics.Append(diags...) @@ -120,7 +135,7 @@ func (r resourceID) Create(ctx context.Context, req tfsdk.CreateResourceRequest, return } if err != nil { - resp.Diagnostics.Append(randomReadError(err.Error())...) + resp.Diagnostics.Append(diagnostics.RandomReadError(err.Error())...) return } @@ -133,7 +148,7 @@ func (r resourceID) Create(ctx context.Context, req tfsdk.CreateResourceRequest, bigInt.SetBytes(bytes) dec := bigInt.String() - i := IDModel{ + i := modelV0{ ID: types.String{Value: id}, Keepers: plan.Keepers, ByteLength: types.Int64{Value: plan.ByteLength.Value}, @@ -152,21 +167,20 @@ func (r resourceID) Create(ctx context.Context, req tfsdk.CreateResourceRequest, } // Read does not need to perform any operations as the state in ReadResourceResponse is already populated. -func (r resourceID) Read(ctx context.Context, req tfsdk.ReadResourceRequest, resp *tfsdk.ReadResourceResponse) { +func (r *resource) Read(context.Context, tfsdk.ReadResourceRequest, *tfsdk.ReadResourceResponse) { } // Update is intentionally left blank as all required and optional attributes force replacement of the resource // through the RequiresReplace AttributePlanModifier. -func (r resourceID) Update(ctx context.Context, req tfsdk.UpdateResourceRequest, resp *tfsdk.UpdateResourceResponse) { - // Intentionally left blank. +func (r *resource) Update(context.Context, tfsdk.UpdateResourceRequest, *tfsdk.UpdateResourceResponse) { } // 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 resourceID) Delete(ctx context.Context, req tfsdk.DeleteResourceRequest, resp *tfsdk.DeleteResourceResponse) { +func (r *resource) Delete(context.Context, tfsdk.DeleteResourceRequest, *tfsdk.DeleteResourceResponse) { } -func (r resourceID) ImportState(ctx context.Context, req tfsdk.ImportResourceStateRequest, resp *tfsdk.ImportResourceStateResponse) { +func (r *resource) ImportState(ctx context.Context, req tfsdk.ImportResourceStateRequest, resp *tfsdk.ImportResourceStateResponse) { id := req.ID var prefix string @@ -181,7 +195,7 @@ func (r resourceID) ImportState(ctx context.Context, req tfsdk.ImportResourceSta resp.Diagnostics.AddError( "Import Random ID Error", "While attempting to import a random id there was a decoding error.\n\n+"+ - retryMsg+ + diagnostics.RetryMsg+ fmt.Sprintf("Original Error: %s", err), ) return @@ -194,7 +208,7 @@ func (r resourceID) ImportState(ctx context.Context, req tfsdk.ImportResourceSta bigInt.SetBytes(bytes) dec := bigInt.String() - var state IDModel + var state modelV0 state.ID.Value = id state.ByteLength.Value = int64(len(bytes)) @@ -216,3 +230,14 @@ func (r resourceID) ImportState(ctx context.Context, req tfsdk.ImportResourceSta return } } + +type modelV0 struct { + ID types.String `tfsdk:"id"` + Keepers types.Map `tfsdk:"keepers"` + ByteLength types.Int64 `tfsdk:"byte_length"` + Prefix types.String `tfsdk:"prefix"` + B64URL types.String `tfsdk:"b64_url"` + B64Std types.String `tfsdk:"b64_std"` + Hex types.String `tfsdk:"hex"` + Dec types.String `tfsdk:"dec"` +} diff --git a/internal/provider/resource_integer.go b/internal/resources/integer/resource.go similarity index 75% rename from internal/provider/resource_integer.go rename to internal/resources/integer/resource.go index bc6b511b..a81b7f8b 100644 --- a/internal/provider/resource_integer.go +++ b/internal/resources/integer/resource.go @@ -1,4 +1,4 @@ -package provider +package integer import ( "context" @@ -9,11 +9,19 @@ import ( "github.com/hashicorp/terraform-plugin-framework/diag" "github.com/hashicorp/terraform-plugin-framework/tfsdk" "github.com/hashicorp/terraform-plugin-framework/types" + + "github.com/terraform-providers/terraform-provider-random/internal/random" ) -type resourceIntegerType struct{} +func NewResourceType() *resourceType { + return &resourceType{} +} + +var _ tfsdk.ResourceType = (*resourceType)(nil) -func (r resourceIntegerType) GetSchema(context.Context) (tfsdk.Schema, diag.Diagnostics) { +type resourceType struct{} + +func (r *resourceType) GetSchema(context.Context) (tfsdk.Schema, diag.Diagnostics) { return tfsdk.Schema{ Description: "The resource `random_integer` generates random values from a given range, described " + "by the `min` and `max` attributes of a given resource.\n" + @@ -63,18 +71,19 @@ func (r resourceIntegerType) GetSchema(context.Context) (tfsdk.Schema, diag.Diag }, nil } -func (r resourceIntegerType) NewResource(_ context.Context, p tfsdk.Provider) (tfsdk.Resource, diag.Diagnostics) { - return resourceInteger{ - p: *(p.(*provider)), - }, nil +func (r *resourceType) NewResource(_ context.Context, _ tfsdk.Provider) (tfsdk.Resource, diag.Diagnostics) { + return &resource{}, nil } -type resourceInteger struct { - p provider -} +var ( + _ tfsdk.Resource = (*resource)(nil) + _ tfsdk.ResourceWithImportState = (*resource)(nil) +) + +type resource struct{} -func (r resourceInteger) Create(ctx context.Context, req tfsdk.CreateResourceRequest, resp *tfsdk.CreateResourceResponse) { - var plan IntegerModel +func (r *resource) Create(ctx context.Context, req tfsdk.CreateResourceRequest, resp *tfsdk.CreateResourceResponse) { + var plan modelV0 diags := req.Plan.Get(ctx, &plan) resp.Diagnostics.Append(diags...) @@ -94,10 +103,10 @@ func (r resourceInteger) Create(ctx context.Context, req tfsdk.CreateResourceReq return } - rand := NewRand(seed) + rand := random.NewRand(seed) number := rand.Intn((max+1)-min) + min - u := &IntegerModel{ + u := &modelV0{ ID: types.String{Value: strconv.Itoa(number)}, Keepers: plan.Keepers, Min: types.Int64{Value: int64(min)}, @@ -119,20 +128,20 @@ func (r resourceInteger) Create(ctx context.Context, req tfsdk.CreateResourceReq } // Read does not need to perform any operations as the state in ReadResourceResponse is already populated. -func (r resourceInteger) Read(ctx context.Context, req tfsdk.ReadResourceRequest, resp *tfsdk.ReadResourceResponse) { +func (r *resource) Read(ctx context.Context, req tfsdk.ReadResourceRequest, resp *tfsdk.ReadResourceResponse) { } // Update is intentionally left blank as all required and optional attributes force replacement of the resource // through the RequiresReplace AttributePlanModifier. -func (r resourceInteger) Update(ctx context.Context, req tfsdk.UpdateResourceRequest, resp *tfsdk.UpdateResourceResponse) { +func (r *resource) Update(ctx context.Context, req tfsdk.UpdateResourceRequest, resp *tfsdk.UpdateResourceResponse) { } // 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 resourceInteger) Delete(ctx context.Context, req tfsdk.DeleteResourceRequest, resp *tfsdk.DeleteResourceResponse) { +func (r *resource) Delete(ctx context.Context, req tfsdk.DeleteResourceRequest, resp *tfsdk.DeleteResourceResponse) { } -func (r resourceInteger) ImportState(ctx context.Context, req tfsdk.ImportResourceStateRequest, resp *tfsdk.ImportResourceStateResponse) { +func (r *resource) ImportState(ctx context.Context, req tfsdk.ImportResourceStateRequest, resp *tfsdk.ImportResourceStateResponse) { parts := strings.Split(req.ID, ",") if len(parts) != 3 && len(parts) != 4 { resp.Diagnostics.AddError( @@ -172,7 +181,7 @@ func (r resourceInteger) ImportState(ctx context.Context, req tfsdk.ImportResour return } - var state IntegerModel + var state modelV0 state.ID.Value = parts[0] state.Keepers.ElemType = types.StringType @@ -190,3 +199,12 @@ func (r resourceInteger) ImportState(ctx context.Context, req tfsdk.ImportResour return } } + +type modelV0 struct { + ID types.String `tfsdk:"id"` + Keepers types.Map `tfsdk:"keepers"` + Min types.Int64 `tfsdk:"min"` + Max types.Int64 `tfsdk:"max"` + Seed types.String `tfsdk:"seed"` + Result types.Int64 `tfsdk:"result"` +} diff --git a/internal/resources/password/resource.go b/internal/resources/password/resource.go new file mode 100644 index 00000000..f874bbf8 --- /dev/null +++ b/internal/resources/password/resource.go @@ -0,0 +1,277 @@ +package password + +import ( + "context" + + "github.com/hashicorp/terraform-plugin-framework/diag" + "github.com/hashicorp/terraform-plugin-framework/tfsdk" + "github.com/hashicorp/terraform-plugin-framework/types" + "golang.org/x/crypto/bcrypt" + + "github.com/terraform-providers/terraform-provider-random/internal/diagnostics" + "github.com/terraform-providers/terraform-provider-random/internal/random" +) + +var _ tfsdk.ResourceType = (*resourceType)(nil) + +func NewResourceType() *resourceType { + return &resourceType{} +} + +type resourceType struct{} + +func (r *resourceType) GetSchema(context.Context) (tfsdk.Schema, diag.Diagnostics) { + return schemaV2(), nil +} + +func (r *resourceType) NewResource(_ context.Context, _ tfsdk.Provider) (tfsdk.Resource, diag.Diagnostics) { + return &resource{}, nil +} + +var ( + _ tfsdk.Resource = (*resource)(nil) + _ tfsdk.ResourceWithImportState = (*resource)(nil) + _ tfsdk.ResourceWithUpgradeState = (*resource)(nil) +) + +type resource struct{} + +func (r *resource) Create(ctx context.Context, req tfsdk.CreateResourceRequest, resp *tfsdk.CreateResourceResponse) { + var plan modelV2 + + diags := req.Plan.Get(ctx, &plan) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } + + params := random.RandomStringParams{ + Length: plan.Length.Value, + Upper: plan.Upper.Value, + MinUpper: plan.MinUpper.Value, + Lower: plan.Lower.Value, + MinLower: plan.MinLower.Value, + Numeric: plan.Numeric.Value, + MinNumeric: plan.MinNumeric.Value, + Special: plan.Special.Value, + MinSpecial: plan.MinSpecial.Value, + OverrideSpecial: plan.OverrideSpecial.Value, + } + + result, err := random.CreateRandomString(params) + if err != nil { + resp.Diagnostics.Append(diagnostics.RandomReadError(err.Error())...) + return + } + + state := modelV2{ + ID: types.String{Value: "none"}, + Keepers: plan.Keepers, + Length: types.Int64{Value: plan.Length.Value}, + Special: types.Bool{Value: plan.Special.Value}, + Upper: types.Bool{Value: plan.Upper.Value}, + Lower: types.Bool{Value: plan.Lower.Value}, + Numeric: types.Bool{Value: plan.Numeric.Value}, + MinNumeric: types.Int64{Value: plan.MinNumeric.Value}, + MinUpper: types.Int64{Value: plan.MinUpper.Value}, + MinLower: types.Int64{Value: plan.MinLower.Value}, + MinSpecial: types.Int64{Value: plan.MinSpecial.Value}, + OverrideSpecial: types.String{Value: plan.OverrideSpecial.Value}, + Result: types.String{Value: string(result)}, + } + + hash, err := generateHash(plan.Result.Value) + if err != nil { + resp.Diagnostics.Append(diagnostics.HashGenerationError(err.Error())...) + } + + state.BcryptHash = types.String{Value: hash} + + diags = resp.State.Set(ctx, state) + 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 *resource) Read(ctx context.Context, req tfsdk.ReadResourceRequest, resp *tfsdk.ReadResourceResponse) { +} + +// Update is intentionally left blank as all required and optional attributes force replacement of the resource +// through the RequiresReplace AttributePlanModifier. +func (r *resource) Update(ctx context.Context, req tfsdk.UpdateResourceRequest, resp *tfsdk.UpdateResourceResponse) { +} + +// 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 *resource) Delete(ctx context.Context, req tfsdk.DeleteResourceRequest, resp *tfsdk.DeleteResourceResponse) { +} + +func (r *resource) ImportState(ctx context.Context, req tfsdk.ImportResourceStateRequest, resp *tfsdk.ImportResourceStateResponse) { + id := req.ID + + state := modelV2{ + ID: types.String{Value: "none"}, + Result: types.String{Value: id}, + Length: types.Int64{Value: int64(len(id))}, + Special: types.Bool{Value: true}, + Upper: types.Bool{Value: true}, + Lower: types.Bool{Value: true}, + Numeric: types.Bool{Value: true}, + MinSpecial: types.Int64{Value: 0}, + MinUpper: types.Int64{Value: 0}, + MinLower: types.Int64{Value: 0}, + MinNumeric: types.Int64{Value: 0}, + } + + state.Keepers.ElemType = types.StringType + + hash, err := generateHash(id) + if err != nil { + resp.Diagnostics.Append(diagnostics.HashGenerationError(err.Error())...) + } + + state.BcryptHash = types.String{Value: hash} + + diags := resp.State.Set(ctx, &state) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } +} + +func (r *resource) UpgradeState(context.Context) map[int64]tfsdk.ResourceStateUpgrader { + schemaV0 := schemaV0() + schemaV1 := schemaV1() + + return map[int64]tfsdk.ResourceStateUpgrader{ + 0: { + PriorSchema: &schemaV0, + StateUpgrader: upgradePasswordStateV0toV2, + }, + 1: { + PriorSchema: &schemaV1, + StateUpgrader: upgradePasswordStateV1toV2, + }, + } +} + +func upgradePasswordStateV0toV2(ctx context.Context, req tfsdk.UpgradeResourceStateRequest, resp *tfsdk.UpgradeResourceStateResponse) { + type modelV0 struct { + ID types.String `tfsdk:"id"` + Keepers types.Map `tfsdk:"keepers"` + Length types.Int64 `tfsdk:"length"` + Special types.Bool `tfsdk:"special"` + Upper types.Bool `tfsdk:"upper"` + Lower types.Bool `tfsdk:"lower"` + Number types.Bool `tfsdk:"number"` + 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"` + } + + var passwordDataV0 modelV0 + + resp.Diagnostics.Append(req.State.Get(ctx, &passwordDataV0)...) + if resp.Diagnostics.HasError() { + return + } + + passwordDataV2 := modelV2{ + Keepers: passwordDataV0.Keepers, + Length: passwordDataV0.Length, + Special: passwordDataV0.Special, + Upper: passwordDataV0.Upper, + Lower: passwordDataV0.Lower, + Numeric: passwordDataV0.Number, + MinNumeric: passwordDataV0.MinNumeric, + MinLower: passwordDataV0.MinLower, + MinSpecial: passwordDataV0.MinSpecial, + OverrideSpecial: passwordDataV0.OverrideSpecial, + Result: passwordDataV0.Result, + ID: passwordDataV0.ID, + } + + hash, err := generateHash(passwordDataV2.Result.Value) + if err != nil { + resp.Diagnostics.Append(diagnostics.HashGenerationError(err.Error())...) + return + } + + passwordDataV2.BcryptHash.Value = hash + + diags := resp.State.Set(ctx, passwordDataV2) + resp.Diagnostics.Append(diags...) +} + +func upgradePasswordStateV1toV2(ctx context.Context, req tfsdk.UpgradeResourceStateRequest, resp *tfsdk.UpgradeResourceStateResponse) { + type modelV1 struct { + ID types.String `tfsdk:"id"` + Keepers types.Map `tfsdk:"keepers"` + Length types.Int64 `tfsdk:"length"` + Special types.Bool `tfsdk:"special"` + Upper types.Bool `tfsdk:"upper"` + Lower types.Bool `tfsdk:"lower"` + Number types.Bool `tfsdk:"number"` + 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"` + } + + var passwordDataV1 modelV1 + + resp.Diagnostics.Append(req.State.Get(ctx, &passwordDataV1)...) + if resp.Diagnostics.HasError() { + return + } + + passwordDataV2 := modelV2{ + Keepers: passwordDataV1.Keepers, + Length: passwordDataV1.Length, + Special: passwordDataV1.Special, + Upper: passwordDataV1.Upper, + Lower: passwordDataV1.Lower, + Numeric: passwordDataV1.Number, + MinNumeric: passwordDataV1.MinNumeric, + MinLower: passwordDataV1.MinLower, + MinSpecial: passwordDataV1.MinSpecial, + OverrideSpecial: passwordDataV1.OverrideSpecial, + BcryptHash: passwordDataV1.BcryptHash, + Result: passwordDataV1.Result, + ID: passwordDataV1.ID, + } + + diags := resp.State.Set(ctx, passwordDataV2) + resp.Diagnostics.Append(diags...) +} + +func generateHash(toHash string) (string, error) { + hash, err := bcrypt.GenerateFromPassword([]byte(toHash), bcrypt.DefaultCost) + + return string(hash), err +} + +type modelV2 struct { + ID types.String `tfsdk:"id"` + Keepers types.Map `tfsdk:"keepers"` + 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"` +} diff --git a/internal/resources/password/resource_test.go b/internal/resources/password/resource_test.go new file mode 100644 index 00000000..e8637b41 --- /dev/null +++ b/internal/resources/password/resource_test.go @@ -0,0 +1,140 @@ +package password + +import ( + "context" + "testing" + + "github.com/google/go-cmp/cmp" + "github.com/hashicorp/terraform-plugin-framework/tfsdk" + "github.com/hashicorp/terraform-plugin-framework/types" + "github.com/hashicorp/terraform-plugin-go/tftypes" + "golang.org/x/crypto/bcrypt" +) + +func TestUpgradePasswordStateV0toV2(t *testing.T) { + raw := tftypes.NewValue(tftypes.Object{}, map[string]tftypes.Value{ + "id": tftypes.NewValue(tftypes.String, "none"), + "keepers": tftypes.NewValue(tftypes.Map{ElementType: tftypes.String}, nil), + "length": tftypes.NewValue(tftypes.Number, 16), + "lower": tftypes.NewValue(tftypes.Bool, true), + "min_lower": tftypes.NewValue(tftypes.Number, 0), + "min_numeric": tftypes.NewValue(tftypes.Number, 0), + "min_special": tftypes.NewValue(tftypes.Number, 0), + "min_upper": tftypes.NewValue(tftypes.Number, 0), + "number": tftypes.NewValue(tftypes.Bool, true), + "override_special": tftypes.NewValue(tftypes.String, "!#$%\u0026*()-_=+[]{}\u003c\u003e:?"), + "result": tftypes.NewValue(tftypes.String, "DZy_3*tnonj%Q%Yx"), + "special": tftypes.NewValue(tftypes.Bool, true), + "upper": tftypes.NewValue(tftypes.Bool, true), + }) + + req := tfsdk.UpgradeResourceStateRequest{ + State: &tfsdk.State{ + Raw: raw, + Schema: schemaV0(), + }, + } + + resp := &tfsdk.UpgradeResourceStateResponse{ + State: tfsdk.State{ + Schema: schemaV2(), + }, + } + + upgradePasswordStateV0toV2(context.Background(), req, resp) + + expected := modelV2{ + ID: types.String{Value: "none"}, + Keepers: types.Map{Null: true, ElemType: types.StringType}, + Length: types.Int64{Value: 16}, + Special: types.Bool{Value: true}, + Upper: types.Bool{Value: true}, + Lower: types.Bool{Value: true}, + Numeric: types.Bool{Value: true}, + MinNumeric: types.Int64{Value: 0}, + MinUpper: types.Int64{Value: 0}, + MinLower: types.Int64{Value: 0}, + MinSpecial: types.Int64{Value: 0}, + OverrideSpecial: types.String{Value: "!#$%\u0026*()-_=+[]{}\u003c\u003e:?"}, + Result: types.String{Value: "DZy_3*tnonj%Q%Yx"}, + } + + actual := modelV2{} + diags := resp.State.Get(context.Background(), &actual) + if diags.HasError() { + t.Errorf("error getting state: %v", diags) + } + + err := bcrypt.CompareHashAndPassword([]byte(actual.BcryptHash.Value), []byte(actual.Result.Value)) + if err != nil { + t.Errorf("unexpected bcrypt comparison error: %s", err) + } + + // Setting actual.BcryptHash to zero value to allow direct comparison of expected and actual. + actual.BcryptHash = types.String{} + + if !cmp.Equal(expected, actual) { + t.Errorf("expected: %+v, got: %+v", expected, actual) + } +} + +func TestUpgradePasswordStateV1toV2(t *testing.T) { + raw := tftypes.NewValue(tftypes.Object{}, map[string]tftypes.Value{ + "id": tftypes.NewValue(tftypes.String, "none"), + "keepers": tftypes.NewValue(tftypes.Map{ElementType: tftypes.String}, nil), + "length": tftypes.NewValue(tftypes.Number, 16), + "lower": tftypes.NewValue(tftypes.Bool, true), + "min_lower": tftypes.NewValue(tftypes.Number, 0), + "min_numeric": tftypes.NewValue(tftypes.Number, 0), + "min_special": tftypes.NewValue(tftypes.Number, 0), + "min_upper": tftypes.NewValue(tftypes.Number, 0), + "number": tftypes.NewValue(tftypes.Bool, true), + "override_special": tftypes.NewValue(tftypes.String, "!#$%\u0026*()-_=+[]{}\u003c\u003e:?"), + "result": tftypes.NewValue(tftypes.String, "DZy_3*tnonj%Q%Yx"), + "special": tftypes.NewValue(tftypes.Bool, true), + "upper": tftypes.NewValue(tftypes.Bool, true), + "bcrypt_hash": tftypes.NewValue(tftypes.String, "bcrypt_hash"), + }) + + req := tfsdk.UpgradeResourceStateRequest{ + State: &tfsdk.State{ + Raw: raw, + Schema: schemaV1(), + }, + } + + resp := &tfsdk.UpgradeResourceStateResponse{ + State: tfsdk.State{ + Schema: schemaV2(), + }, + } + + upgradePasswordStateV1toV2(context.Background(), req, resp) + + expected := modelV2{ + ID: types.String{Value: "none"}, + Keepers: types.Map{Null: true, ElemType: types.StringType}, + Length: types.Int64{Value: 16}, + Special: types.Bool{Value: true}, + Upper: types.Bool{Value: true}, + Lower: types.Bool{Value: true}, + Numeric: types.Bool{Value: true}, + MinNumeric: types.Int64{Value: 0}, + MinUpper: types.Int64{Value: 0}, + MinLower: types.Int64{Value: 0}, + MinSpecial: types.Int64{Value: 0}, + OverrideSpecial: types.String{Value: "!#$%\u0026*()-_=+[]{}\u003c\u003e:?"}, + BcryptHash: types.String{Value: "bcrypt_hash"}, + Result: types.String{Value: "DZy_3*tnonj%Q%Yx"}, + } + + actual := modelV2{} + diags := resp.State.Get(context.Background(), &actual) + if diags.HasError() { + t.Errorf("error getting state: %v", diags) + } + + if !cmp.Equal(expected, actual) { + t.Errorf("expected: %+v, got: %+v", expected, actual) + } +} diff --git a/internal/provider/resource_password.go b/internal/resources/password/schema.go similarity index 69% rename from internal/provider/resource_password.go rename to internal/resources/password/schema.go index 12a3f626..aeaef2cd 100644 --- a/internal/provider/resource_password.go +++ b/internal/resources/password/schema.go @@ -1,73 +1,16 @@ -package provider +package password import ( - "context" - "github.com/hashicorp/terraform-plugin-framework-validators/int64validator" - "github.com/hashicorp/terraform-plugin-framework/diag" "github.com/hashicorp/terraform-plugin-framework/tfsdk" "github.com/hashicorp/terraform-plugin-framework/types" "github.com/hashicorp/terraform-plugin-go/tftypes" - "golang.org/x/crypto/bcrypt" "github.com/terraform-providers/terraform-provider-random/internal/planmodifiers" + "github.com/terraform-providers/terraform-provider-random/internal/validators" ) -type resourcePasswordType struct{} - -func (r resourcePasswordType) GetSchema(context.Context) (tfsdk.Schema, diag.Diagnostics) { - return passwordSchemaV2(), nil -} - -func (r resourcePasswordType) NewResource(_ context.Context, p tfsdk.Provider) (tfsdk.Resource, diag.Diagnostics) { - return resourcePassword{ - p: *(p.(*provider)), - }, nil -} - -type resourcePassword struct { - p provider -} - -func (r resourcePassword) Create(ctx context.Context, req tfsdk.CreateResourceRequest, resp *tfsdk.CreateResourceResponse) { - createPassword(ctx, req, resp) -} - -// Read does not need to perform any operations as the state in ReadResourceResponse is already populated. -func (r resourcePassword) Read(ctx context.Context, req tfsdk.ReadResourceRequest, resp *tfsdk.ReadResourceResponse) { -} - -// Update is intentionally left blank as all required and optional attributes force replacement of the resource -// through the RequiresReplace AttributePlanModifier. -func (r resourcePassword) Update(ctx context.Context, req tfsdk.UpdateResourceRequest, resp *tfsdk.UpdateResourceResponse) { -} - -// 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 resourcePassword) Delete(ctx context.Context, req tfsdk.DeleteResourceRequest, resp *tfsdk.DeleteResourceResponse) { -} - -func (r resourcePassword) ImportState(ctx context.Context, req tfsdk.ImportResourceStateRequest, resp *tfsdk.ImportResourceStateResponse) { - importPassword(ctx, req, resp) -} - -func (r resourcePassword) UpgradeState(context.Context) map[int64]tfsdk.ResourceStateUpgrader { - schemaV0 := passwordSchemaV0() - schemaV1 := passwordSchemaV1() - - return map[int64]tfsdk.ResourceStateUpgrader{ - 0: { - PriorSchema: &schemaV0, - StateUpgrader: upgradePasswordStateV0toV2, - }, - 1: { - PriorSchema: &schemaV1, - StateUpgrader: upgradePasswordStateV1toV2, - }, - } -} - -func passwordSchemaV2() tfsdk.Schema { +func schemaV2() tfsdk.Schema { return tfsdk.Schema{ Version: 2, Description: "Identical to [random_string](string.html) with the exception that the result is " + @@ -98,7 +41,7 @@ func passwordSchemaV2() tfsdk.Schema { }, Validators: []tfsdk.AttributeValidator{ int64validator.AtLeast(1), - NewIntIsAtLeastSumOfValidator( + validators.NewIntIsAtLeastSumOfValidator( tftypes.NewAttributePath().WithAttributeName("min_upper"), tftypes.NewAttributePath().WithAttributeName("min_lower"), tftypes.NewAttributePath().WithAttributeName("min_numeric"), @@ -230,7 +173,7 @@ func passwordSchemaV2() tfsdk.Schema { } } -func passwordSchemaV1() tfsdk.Schema { +func schemaV1() tfsdk.Schema { return tfsdk.Schema{ Version: 1, Description: "Identical to [random_string](string.html) with the exception that the result is " + @@ -259,7 +202,7 @@ func passwordSchemaV1() tfsdk.Schema { PlanModifiers: []tfsdk.AttributePlanModifier{tfsdk.RequiresReplace()}, Validators: []tfsdk.AttributeValidator{ int64validator.AtLeast(1), - NewIntIsAtLeastSumOfValidator( + validators.NewIntIsAtLeastSumOfValidator( tftypes.NewAttributePath().WithAttributeName("min_upper"), tftypes.NewAttributePath().WithAttributeName("min_lower"), tftypes.NewAttributePath().WithAttributeName("min_numeric"), @@ -391,7 +334,7 @@ func passwordSchemaV1() tfsdk.Schema { } } -func passwordSchemaV0() tfsdk.Schema { +func schemaV0() tfsdk.Schema { return tfsdk.Schema{ 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 " + @@ -417,7 +360,7 @@ func passwordSchemaV0() tfsdk.Schema { PlanModifiers: []tfsdk.AttributePlanModifier{tfsdk.RequiresReplace()}, Validators: []tfsdk.AttributeValidator{ int64validator.AtLeast(1), - NewIntIsAtLeastSumOfValidator( + validators.NewIntIsAtLeastSumOfValidator( tftypes.NewAttributePath().WithAttributeName("min_upper"), tftypes.NewAttributePath().WithAttributeName("min_lower"), tftypes.NewAttributePath().WithAttributeName("min_numeric"), @@ -541,163 +484,3 @@ func passwordSchemaV0() tfsdk.Schema { }, } } - -func createPassword(ctx context.Context, req tfsdk.CreateResourceRequest, resp *tfsdk.CreateResourceResponse) { - var plan PasswordModelV2 - - diags := req.Plan.Get(ctx, &plan) - resp.Diagnostics.Append(diags...) - if resp.Diagnostics.HasError() { - return - } - - params := randomStringParams{ - length: plan.Length.Value, - upper: plan.Upper.Value, - minUpper: plan.MinUpper.Value, - lower: plan.Lower.Value, - minLower: plan.MinLower.Value, - numeric: plan.Numeric.Value, - minNumeric: plan.MinNumeric.Value, - special: plan.Special.Value, - minSpecial: plan.MinSpecial.Value, - overrideSpecial: plan.OverrideSpecial.Value, - } - - result, err := createRandomString(params) - if err != nil { - resp.Diagnostics.Append(randomReadError(err.Error())...) - return - } - - state := PasswordModelV2{ - ID: types.String{Value: "none"}, - Keepers: plan.Keepers, - Length: types.Int64{Value: plan.Length.Value}, - Special: types.Bool{Value: plan.Special.Value}, - Upper: types.Bool{Value: plan.Upper.Value}, - Lower: types.Bool{Value: plan.Lower.Value}, - Numeric: types.Bool{Value: plan.Numeric.Value}, - MinNumeric: types.Int64{Value: plan.MinNumeric.Value}, - MinUpper: types.Int64{Value: plan.MinUpper.Value}, - MinLower: types.Int64{Value: plan.MinLower.Value}, - MinSpecial: types.Int64{Value: plan.MinSpecial.Value}, - OverrideSpecial: types.String{Value: plan.OverrideSpecial.Value}, - Result: types.String{Value: string(result)}, - } - - hash, err := generateHash(plan.Result.Value) - if err != nil { - resp.Diagnostics.Append(hashGenerationError(err.Error())...) - } - - state.BcryptHash = types.String{Value: hash} - - diags = resp.State.Set(ctx, state) - resp.Diagnostics.Append(diags...) - if resp.Diagnostics.HasError() { - return - } -} - -func importPassword(ctx context.Context, req tfsdk.ImportResourceStateRequest, resp *tfsdk.ImportResourceStateResponse) { - id := req.ID - - state := PasswordModelV2{ - ID: types.String{Value: "none"}, - Result: types.String{Value: id}, - Length: types.Int64{Value: int64(len(id))}, - Special: types.Bool{Value: true}, - Upper: types.Bool{Value: true}, - Lower: types.Bool{Value: true}, - Numeric: types.Bool{Value: true}, - MinSpecial: types.Int64{Value: 0}, - MinUpper: types.Int64{Value: 0}, - MinLower: types.Int64{Value: 0}, - MinNumeric: types.Int64{Value: 0}, - } - - state.Keepers.ElemType = types.StringType - - hash, err := generateHash(id) - if err != nil { - resp.Diagnostics.Append(hashGenerationError(err.Error())...) - } - - state.BcryptHash = types.String{Value: hash} - - diags := resp.State.Set(ctx, &state) - resp.Diagnostics.Append(diags...) - if resp.Diagnostics.HasError() { - return - } -} - -func upgradePasswordStateV0toV2(ctx context.Context, req tfsdk.UpgradeResourceStateRequest, resp *tfsdk.UpgradeResourceStateResponse) { - var passwordDataV0 PasswordModelV0 - - resp.Diagnostics.Append(req.State.Get(ctx, &passwordDataV0)...) - if resp.Diagnostics.HasError() { - return - } - - passwordDataV2 := PasswordModelV2{ - Keepers: passwordDataV0.Keepers, - Length: passwordDataV0.Length, - Special: passwordDataV0.Special, - Upper: passwordDataV0.Upper, - Lower: passwordDataV0.Lower, - Numeric: passwordDataV0.Number, - MinNumeric: passwordDataV0.MinNumeric, - MinLower: passwordDataV0.MinLower, - MinSpecial: passwordDataV0.MinSpecial, - OverrideSpecial: passwordDataV0.OverrideSpecial, - Result: passwordDataV0.Result, - ID: passwordDataV0.ID, - } - - hash, err := generateHash(passwordDataV2.Result.Value) - if err != nil { - resp.Diagnostics.Append(hashGenerationError(err.Error())...) - return - } - - passwordDataV2.BcryptHash.Value = hash - - diags := resp.State.Set(ctx, passwordDataV2) - resp.Diagnostics.Append(diags...) -} - -func upgradePasswordStateV1toV2(ctx context.Context, req tfsdk.UpgradeResourceStateRequest, resp *tfsdk.UpgradeResourceStateResponse) { - var passwordDataV1 PasswordModelV1 - - resp.Diagnostics.Append(req.State.Get(ctx, &passwordDataV1)...) - if resp.Diagnostics.HasError() { - return - } - - passwordDataV2 := PasswordModelV2{ - Keepers: passwordDataV1.Keepers, - Length: passwordDataV1.Length, - Special: passwordDataV1.Special, - Upper: passwordDataV1.Upper, - Lower: passwordDataV1.Lower, - Numeric: passwordDataV1.Number, - MinNumeric: passwordDataV1.MinNumeric, - MinLower: passwordDataV1.MinLower, - MinSpecial: passwordDataV1.MinSpecial, - OverrideSpecial: passwordDataV1.OverrideSpecial, - BcryptHash: passwordDataV1.BcryptHash, - Result: passwordDataV1.Result, - ID: passwordDataV1.ID, - } - - diags := resp.State.Set(ctx, passwordDataV2) - resp.Diagnostics.Append(diags...) -} - -func generateHash(toHash string) (string, error) { - hash, err := bcrypt.GenerateFromPassword([]byte(toHash), bcrypt.DefaultCost) - - return string(hash), err -} diff --git a/internal/provider/resource_pet.go b/internal/resources/pet/resource.go similarity index 72% rename from internal/provider/resource_pet.go rename to internal/resources/pet/resource.go index 113ecdb0..69740f0e 100644 --- a/internal/provider/resource_pet.go +++ b/internal/resources/pet/resource.go @@ -1,4 +1,4 @@ -package provider +package pet import ( "context" @@ -13,14 +13,15 @@ import ( "github.com/terraform-providers/terraform-provider-random/internal/planmodifiers" ) -type resourcePetType struct{} +func NewResourceType() *resourceType { + return &resourceType{} +} -func (r resourcePetType) GetSchema(context.Context) (tfsdk.Schema, diag.Diagnostics) { - // This is necessary to ensure each call to petname is properly randomised: - // the library uses `rand.Intn()` and does NOT seed `rand.Seed()` by default, - // so this call takes care of that. - petname.NonDeterministicMode() +var _ tfsdk.ResourceType = (*resourceType)(nil) + +type resourceType struct{} +func (r *resourceType) GetSchema(context.Context) (tfsdk.Schema, diag.Diagnostics) { return tfsdk.Schema{ Description: "The resource `random_pet` generates random pet names that are intended to be used as " + "unique identifiers for other resources.\n" + @@ -35,8 +36,10 @@ func (r resourcePetType) GetSchema(context.Context) (tfsdk.Schema, diag.Diagnost Type: types.MapType{ ElemType: types.StringType, }, - Optional: true, - PlanModifiers: []tfsdk.AttributePlanModifier{tfsdk.RequiresReplace()}, + Optional: true, + PlanModifiers: []tfsdk.AttributePlanModifier{ + tfsdk.RequiresReplace(), + }, }, "length": { Description: "The length (in words) of the pet name. Defaults to 2", @@ -45,7 +48,7 @@ func (r resourcePetType) GetSchema(context.Context) (tfsdk.Schema, diag.Diagnost Computed: true, PlanModifiers: []tfsdk.AttributePlanModifier{ planmodifiers.DefaultValue(types.Int64{Value: 2}), - tfsdk.RequiresReplace(), + planmodifiers.RequiresReplace(), }, }, "prefix": { @@ -61,7 +64,7 @@ func (r resourcePetType) GetSchema(context.Context) (tfsdk.Schema, diag.Diagnost Computed: true, PlanModifiers: []tfsdk.AttributePlanModifier{ planmodifiers.DefaultValue(types.String{Value: "-"}), - tfsdk.RequiresReplace(), + planmodifiers.RequiresReplace(), }, }, "id": { @@ -73,18 +76,21 @@ func (r resourcePetType) GetSchema(context.Context) (tfsdk.Schema, diag.Diagnost }, nil } -func (r resourcePetType) NewResource(_ context.Context, p tfsdk.Provider) (tfsdk.Resource, diag.Diagnostics) { - return resourcePet{ - p: *(p.(*provider)), - }, nil +func (r *resourceType) NewResource(_ context.Context, p tfsdk.Provider) (tfsdk.Resource, diag.Diagnostics) { + return &resource{}, nil } -type resourcePet struct { - p provider -} +var _ tfsdk.Resource = (*resource)(nil) -func (r resourcePet) Create(ctx context.Context, req tfsdk.CreateResourceRequest, resp *tfsdk.CreateResourceResponse) { - var plan PetNameModel +type resource struct{} + +func (r *resource) Create(ctx context.Context, req tfsdk.CreateResourceRequest, resp *tfsdk.CreateResourceResponse) { + // This is necessary to ensure each call to petname is properly randomised: + // the library uses `rand.Intn()` and does NOT seed `rand.Seed()` by default, + // so this call takes care of that. + petname.NonDeterministicMode() + + var plan modelV0 diags := req.Plan.Get(ctx, &plan) resp.Diagnostics.Append(diags...) @@ -98,7 +104,7 @@ func (r resourcePet) Create(ctx context.Context, req tfsdk.CreateResourceRequest pet := strings.ToLower(petname.Generate(int(length), separator)) - pn := PetNameModel{ + pn := modelV0{ Keepers: plan.Keepers, Length: types.Int64{Value: length}, Separator: types.String{Value: separator}, @@ -121,15 +127,23 @@ func (r resourcePet) Create(ctx context.Context, req tfsdk.CreateResourceRequest } // Read does not need to perform any operations as the state in ReadResourceResponse is already populated. -func (r resourcePet) Read(ctx context.Context, req tfsdk.ReadResourceRequest, resp *tfsdk.ReadResourceResponse) { +func (r *resource) Read(ctx context.Context, req tfsdk.ReadResourceRequest, resp *tfsdk.ReadResourceResponse) { } // Update is intentionally left blank as all required and optional attributes force replacement of the resource // through the RequiresReplace AttributePlanModifier. -func (r resourcePet) Update(ctx context.Context, req tfsdk.UpdateResourceRequest, resp *tfsdk.UpdateResourceResponse) { +func (r *resource) Update(ctx context.Context, req tfsdk.UpdateResourceRequest, resp *tfsdk.UpdateResourceResponse) { } // 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 resourcePet) Delete(ctx context.Context, req tfsdk.DeleteResourceRequest, resp *tfsdk.DeleteResourceResponse) { +func (r *resource) Delete(ctx context.Context, req tfsdk.DeleteResourceRequest, resp *tfsdk.DeleteResourceResponse) { +} + +type modelV0 struct { + ID types.String `tfsdk:"id"` + Keepers types.Map `tfsdk:"keepers"` + Length types.Int64 `tfsdk:"length"` + Prefix types.String `tfsdk:"prefix"` + Separator types.String `tfsdk:"separator"` } diff --git a/internal/provider/resource_shuffle.go b/internal/resources/shuffle/resource.go similarity index 66% rename from internal/provider/resource_shuffle.go rename to internal/resources/shuffle/resource.go index 325a194d..fb90aa93 100644 --- a/internal/provider/resource_shuffle.go +++ b/internal/resources/shuffle/resource.go @@ -1,4 +1,4 @@ -package provider +package shuffle import ( "context" @@ -7,11 +7,19 @@ import ( "github.com/hashicorp/terraform-plugin-framework/diag" "github.com/hashicorp/terraform-plugin-framework/tfsdk" "github.com/hashicorp/terraform-plugin-framework/types" + + "github.com/terraform-providers/terraform-provider-random/internal/random" ) -type resourceShuffleType struct{} +func NewResourceType() *resourceType { + return &resourceType{} +} + +var _ tfsdk.ResourceType = (*resourceType)(nil) + +type resourceType struct{} -func (r resourceShuffleType) GetSchema(context.Context) (tfsdk.Schema, diag.Diagnostics) { +func (r *resourceType) GetSchema(context.Context) (tfsdk.Schema, diag.Diagnostics) { return tfsdk.Schema{ Description: "The resource `random_shuffle` generates a random permutation of a list of strings " + "given as an argument.", @@ -22,8 +30,10 @@ func (r resourceShuffleType) GetSchema(context.Context) (tfsdk.Schema, diag.Diag Type: types.MapType{ ElemType: types.StringType, }, - Optional: true, - PlanModifiers: []tfsdk.AttributePlanModifier{tfsdk.RequiresReplace()}, + Optional: true, + PlanModifiers: []tfsdk.AttributePlanModifier{ + tfsdk.RequiresReplace(), + }, }, "seed": { Description: "Arbitrary string with which to seed the random number generator, in order to " + @@ -32,26 +42,32 @@ func (r resourceShuffleType) GetSchema(context.Context) (tfsdk.Schema, diag.Diag "**Important:** Even with an identical seed, it is not guaranteed that the same permutation " + "will be produced across different versions of Terraform. This argument causes the " + "result to be *less volatile*, but not fixed for all time.", - Type: types.StringType, - Optional: true, - PlanModifiers: []tfsdk.AttributePlanModifier{tfsdk.RequiresReplace()}, + Type: types.StringType, + Optional: true, + PlanModifiers: []tfsdk.AttributePlanModifier{ + tfsdk.RequiresReplace(), + }, }, "input": { Description: "The list of strings to shuffle.", Type: types.ListType{ ElemType: types.StringType, }, - Required: true, - PlanModifiers: []tfsdk.AttributePlanModifier{tfsdk.RequiresReplace()}, + Required: true, + PlanModifiers: []tfsdk.AttributePlanModifier{ + tfsdk.RequiresReplace(), + }, }, "result_count": { Description: "The number of results to return. Defaults to the number of items in the " + "`input` list. If fewer items are requested, some elements will be excluded from the " + "result. If more items are requested, items will be repeated in the result but not more " + "frequently than the number of items in the input list.", - Type: types.Int64Type, - Optional: true, - PlanModifiers: []tfsdk.AttributePlanModifier{tfsdk.RequiresReplace()}, + Type: types.Int64Type, + Optional: true, + PlanModifiers: []tfsdk.AttributePlanModifier{ + tfsdk.RequiresReplace(), + }, }, "result": { Description: "Random permutation of the list of strings given in `input`.", @@ -69,18 +85,16 @@ func (r resourceShuffleType) GetSchema(context.Context) (tfsdk.Schema, diag.Diag }, nil } -func (r resourceShuffleType) NewResource(_ context.Context, p tfsdk.Provider) (tfsdk.Resource, diag.Diagnostics) { - return resourceShuffle{ - p: *(p.(*provider)), - }, nil +func (r *resourceType) NewResource(_ context.Context, p tfsdk.Provider) (tfsdk.Resource, diag.Diagnostics) { + return &resource{}, nil } -type resourceShuffle struct { - p provider -} +var _ tfsdk.Resource = (*resource)(nil) -func (r resourceShuffle) Create(ctx context.Context, req tfsdk.CreateResourceRequest, resp *tfsdk.CreateResourceResponse) { - var plan ShuffleModel +type resource struct{} + +func (r *resource) Create(ctx context.Context, req tfsdk.CreateResourceRequest, resp *tfsdk.CreateResourceResponse) { + var plan modelV0 diags := req.Plan.Get(ctx, &plan) resp.Diagnostics.Append(diags...) if resp.Diagnostics.HasError() { @@ -98,7 +112,7 @@ func (r resourceShuffle) Create(ctx context.Context, req tfsdk.CreateResourceReq result := make([]attr.Value, 0, resultCount) if len(input.Elems) > 0 { - rand := NewRand(seed) + rand := random.NewRand(seed) // Keep producing permutations until we fill our result Batches: @@ -115,7 +129,7 @@ func (r resourceShuffle) Create(ctx context.Context, req tfsdk.CreateResourceReq } } - s := ShuffleModel{ + s := modelV0{ ID: types.String{Value: "-"}, Keepers: plan.Keepers, Input: plan.Input, @@ -147,15 +161,24 @@ func (r resourceShuffle) Create(ctx context.Context, req tfsdk.CreateResourceReq } // Read does not need to perform any operations as the state in ReadResourceResponse is already populated. -func (r resourceShuffle) Read(ctx context.Context, req tfsdk.ReadResourceRequest, resp *tfsdk.ReadResourceResponse) { +func (r *resource) Read(ctx context.Context, req tfsdk.ReadResourceRequest, resp *tfsdk.ReadResourceResponse) { } // Update is intentionally left blank as all required and optional attributes force replacement of the resource // through the RequiresReplace AttributePlanModifier. -func (r resourceShuffle) Update(ctx context.Context, req tfsdk.UpdateResourceRequest, resp *tfsdk.UpdateResourceResponse) { +func (r *resource) Update(ctx context.Context, req tfsdk.UpdateResourceRequest, resp *tfsdk.UpdateResourceResponse) { } // 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 resourceShuffle) Delete(ctx context.Context, req tfsdk.DeleteResourceRequest, resp *tfsdk.DeleteResourceResponse) { +func (r *resource) Delete(ctx context.Context, req tfsdk.DeleteResourceRequest, resp *tfsdk.DeleteResourceResponse) { +} + +type modelV0 struct { + ID types.String `tfsdk:"id"` + Keepers types.Map `tfsdk:"keepers"` + Seed types.String `tfsdk:"seed"` + Input types.List `tfsdk:"input"` + ResultCount types.Int64 `tfsdk:"result_count"` + Result types.List `tfsdk:"result"` } diff --git a/internal/provider/resource_string.go b/internal/resources/string/resource.go similarity index 53% rename from internal/provider/resource_string.go rename to internal/resources/string/resource.go index 91329dcc..d56bf199 100644 --- a/internal/provider/resource_string.go +++ b/internal/resources/string/resource.go @@ -1,4 +1,4 @@ -package provider +package string import ( "context" @@ -9,61 +9,130 @@ import ( "github.com/hashicorp/terraform-plugin-framework/types" "github.com/hashicorp/terraform-plugin-go/tftypes" + "github.com/terraform-providers/terraform-provider-random/internal/diagnostics" "github.com/terraform-providers/terraform-provider-random/internal/planmodifiers" + "github.com/terraform-providers/terraform-provider-random/internal/random" + "github.com/terraform-providers/terraform-provider-random/internal/validators" ) -type resourceStringType struct{} - -func (r resourceStringType) GetSchema(context.Context) (tfsdk.Schema, diag.Diagnostics) { - return stringSchemaV2(), nil +func NewResourceType() *resourceType { + return &resourceType{} } -func (r resourceStringType) NewResource(_ context.Context, p tfsdk.Provider) (tfsdk.Resource, diag.Diagnostics) { - return resourceString{ - p: *(p.(*provider)), - }, nil +var _ tfsdk.ResourceType = (*resourceType)(nil) + +type resourceType struct{} + +func (r resourceType) GetSchema(context.Context) (tfsdk.Schema, diag.Diagnostics) { + return schemaV2(), nil } -type resourceString struct { - p provider +func (r resourceType) NewResource(_ context.Context, p tfsdk.Provider) (tfsdk.Resource, diag.Diagnostics) { + return &resource{}, nil } -func (r resourceString) Create(ctx context.Context, req tfsdk.CreateResourceRequest, resp *tfsdk.CreateResourceResponse) { - createString(ctx, req, resp) +var ( + _ tfsdk.Resource = (*resource)(nil) + _ tfsdk.ResourceWithImportState = (*resource)(nil) + _ tfsdk.ResourceWithUpgradeState = (*resource)(nil) +) + +type resource struct{} + +func (r *resource) Create(ctx context.Context, req tfsdk.CreateResourceRequest, resp *tfsdk.CreateResourceResponse) { + var plan modelV2 + + diags := req.Plan.Get(ctx, &plan) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } + + params := random.RandomStringParams{ + Length: plan.Length.Value, + Upper: plan.Upper.Value, + MinUpper: plan.MinUpper.Value, + Lower: plan.Lower.Value, + MinLower: plan.MinLower.Value, + Numeric: plan.Numeric.Value, + MinNumeric: plan.MinNumeric.Value, + Special: plan.Special.Value, + MinSpecial: plan.MinSpecial.Value, + OverrideSpecial: plan.OverrideSpecial.Value, + } + + result, err := random.CreateRandomString(params) + if err != nil { + resp.Diagnostics.Append(diagnostics.RandomReadError(err.Error())...) + return + } + + state := modelV2{ + ID: types.String{Value: string(result)}, + Keepers: plan.Keepers, + Length: types.Int64{Value: plan.Length.Value}, + Special: types.Bool{Value: plan.Special.Value}, + Upper: types.Bool{Value: plan.Upper.Value}, + Lower: types.Bool{Value: plan.Lower.Value}, + Numeric: types.Bool{Value: plan.Numeric.Value}, + MinNumeric: types.Int64{Value: plan.MinNumeric.Value}, + MinUpper: types.Int64{Value: plan.MinUpper.Value}, + MinLower: types.Int64{Value: plan.MinLower.Value}, + MinSpecial: types.Int64{Value: plan.MinSpecial.Value}, + OverrideSpecial: types.String{Value: plan.OverrideSpecial.Value}, + Result: types.String{Value: string(result)}, + } + + diags = resp.State.Set(ctx, state) + 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 resourceString) Read(ctx context.Context, req tfsdk.ReadResourceRequest, resp *tfsdk.ReadResourceResponse) { +func (r *resource) Read(ctx context.Context, req tfsdk.ReadResourceRequest, resp *tfsdk.ReadResourceResponse) { } // Update is intentionally left blank as all required and optional attributes force replacement of the resource // through the RequiresReplace AttributePlanModifier. -func (r resourceString) Update(ctx context.Context, req tfsdk.UpdateResourceRequest, resp *tfsdk.UpdateResourceResponse) { +func (r *resource) Update(ctx context.Context, req tfsdk.UpdateResourceRequest, resp *tfsdk.UpdateResourceResponse) { } // 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 resourceString) Delete(ctx context.Context, req tfsdk.DeleteResourceRequest, resp *tfsdk.DeleteResourceResponse) { +func (r *resource) Delete(ctx context.Context, req tfsdk.DeleteResourceRequest, resp *tfsdk.DeleteResourceResponse) { } -func (r resourceString) ImportState(ctx context.Context, req tfsdk.ImportResourceStateRequest, resp *tfsdk.ImportResourceStateResponse) { - importString(ctx, req, resp) -} +func (r *resource) ImportState(ctx context.Context, req tfsdk.ImportResourceStateRequest, resp *tfsdk.ImportResourceStateResponse) { + id := req.ID -func (r resourceString) UpgradeState(context.Context) map[int64]tfsdk.ResourceStateUpgrader { - schemaV1 := stringSchemaV1() + state := modelV2{ + ID: types.String{Value: id}, + Result: types.String{Value: id}, + Length: types.Int64{Value: int64(len(id))}, + Special: types.Bool{Value: true}, + Upper: types.Bool{Value: true}, + Lower: types.Bool{Value: true}, + Numeric: types.Bool{Value: true}, + MinSpecial: types.Int64{Value: 0}, + MinUpper: types.Int64{Value: 0}, + MinLower: types.Int64{Value: 0}, + MinNumeric: types.Int64{Value: 0}, + } - return map[int64]tfsdk.ResourceStateUpgrader{ - 1: { - PriorSchema: &schemaV1, - StateUpgrader: upgradeStringStateV1toV2, - }, + state.Keepers.ElemType = types.StringType + + diags := resp.State.Set(ctx, &state) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return } } -func stringSchemaV2() tfsdk.Schema { - return tfsdk.Schema{ - Version: 2, +func (r *resource) UpgradeState(context.Context) map[int64]tfsdk.ResourceStateUpgrader { + schemaV1 := tfsdk.Schema{ + Version: 1, Description: "The resource `random_string` generates a random permutation of alphanumeric " + "characters and optionally special characters.\n" + "\n" + @@ -93,161 +162,7 @@ func stringSchemaV2() tfsdk.Schema { PlanModifiers: []tfsdk.AttributePlanModifier{tfsdk.RequiresReplace()}, Validators: []tfsdk.AttributeValidator{ int64validator.AtLeast(1), - NewIntIsAtLeastSumOfValidator( - tftypes.NewAttributePath().WithAttributeName("min_upper"), - tftypes.NewAttributePath().WithAttributeName("min_lower"), - tftypes.NewAttributePath().WithAttributeName("min_numeric"), - tftypes.NewAttributePath().WithAttributeName("min_special"), - ), - }, - }, - - "special": { - Description: "Include special characters in the result. These are `!@#$%&*()-_=+[]{}<>:?`. Default value is `true`.", - Type: types.BoolType, - Optional: true, - Computed: true, - PlanModifiers: []tfsdk.AttributePlanModifier{ - planmodifiers.DefaultValue(types.Bool{Value: true}), - planmodifiers.RequiresReplace(), - }, - }, - - "upper": { - Description: "Include uppercase alphabet characters in the result. Default value is `true`.", - Type: types.BoolType, - Optional: true, - Computed: true, - PlanModifiers: []tfsdk.AttributePlanModifier{ - planmodifiers.DefaultValue(types.Bool{Value: true}), - planmodifiers.RequiresReplace(), - }, - }, - - "lower": { - Description: "Include lowercase alphabet characters in the result. Default value is `true`.", - Type: types.BoolType, - Optional: true, - Computed: true, - PlanModifiers: []tfsdk.AttributePlanModifier{ - planmodifiers.DefaultValue(types.Bool{Value: true}), - planmodifiers.RequiresReplace(), - }, - }, - - "numeric": { - Description: "Include numeric characters in the result. Default value is `true`.", - Type: types.BoolType, - Optional: true, - Computed: true, - PlanModifiers: []tfsdk.AttributePlanModifier{ - planmodifiers.DefaultValue(types.Bool{Value: true}), - planmodifiers.RequiresReplace(), - }, - }, - - "min_numeric": { - Description: "Minimum number of numeric characters in the result. Default value is `0`.", - Type: types.Int64Type, - Optional: true, - Computed: true, - PlanModifiers: []tfsdk.AttributePlanModifier{ - planmodifiers.DefaultValue(types.Int64{Value: 0}), - planmodifiers.RequiresReplace(), - }, - }, - - "min_upper": { - Description: "Minimum number of uppercase alphabet characters in the result. Default value is `0`.", - Type: types.Int64Type, - Optional: true, - Computed: true, - PlanModifiers: []tfsdk.AttributePlanModifier{ - planmodifiers.DefaultValue(types.Int64{Value: 0}), - planmodifiers.RequiresReplace(), - }, - }, - - "min_lower": { - Description: "Minimum number of lowercase alphabet characters in the result. Default value is `0`.", - Type: types.Int64Type, - Optional: true, - Computed: true, - PlanModifiers: []tfsdk.AttributePlanModifier{ - planmodifiers.DefaultValue(types.Int64{Value: 0}), - planmodifiers.RequiresReplace(), - }, - }, - - "min_special": { - Description: "Minimum number of special characters in the result. Default value is `0`.", - Type: types.Int64Type, - Optional: true, - Computed: true, - PlanModifiers: []tfsdk.AttributePlanModifier{ - planmodifiers.DefaultValue(types.Int64{Value: 0}), - planmodifiers.RequiresReplace(), - }, - }, - - "override_special": { - 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.", - Type: types.StringType, - Optional: true, - Computed: true, - PlanModifiers: []tfsdk.AttributePlanModifier{ - tfsdk.RequiresReplace(), - }, - }, - - "result": { - Description: "The generated random string.", - Type: types.StringType, - Computed: true, - }, - - "id": { - Description: "The generated random string.", - Computed: true, - Type: types.StringType, - }, - }, - } -} - -func stringSchemaV1() tfsdk.Schema { - return tfsdk.Schema{ - Version: 1, - Description: "The resource `random_string` generates a random permutation of alphanumeric " + - "characters and optionally special characters.\n" + - "\n" + - "This resource *does* use a cryptographic random number generator.\n" + - "\n" + - "Historically this resource's intended usage has been ambiguous as the original example used " + - "it in a password. For backwards compatibility it will continue to exist. For unique ids please " + - "use [random_id](id.html), for sensitive random values please use [random_password](password.html).", - Attributes: map[string]tfsdk.Attribute{ - "keepers": { - Description: "Arbitrary map of values that, when changed, will trigger recreation of " + - "resource. See [the main provider documentation](../index.html) for more information.", - Type: types.MapType{ - ElemType: types.StringType, - }, - Optional: true, - PlanModifiers: []tfsdk.AttributePlanModifier{tfsdk.RequiresReplace()}, - }, - - "length": { - 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`).", - Type: types.Int64Type, - Required: true, - PlanModifiers: []tfsdk.AttributePlanModifier{tfsdk.RequiresReplace()}, - Validators: []tfsdk.AttributeValidator{ - int64validator.AtLeast(1), - NewIntIsAtLeastSumOfValidator( + validators.NewIntIsAtLeastSumOfValidator( tftypes.NewAttributePath().WithAttributeName("min_upper"), tftypes.NewAttributePath().WithAttributeName("min_lower"), tftypes.NewAttributePath().WithAttributeName("min_numeric"), @@ -369,94 +284,40 @@ func stringSchemaV1() tfsdk.Schema { }, }, } -} - -func createString(ctx context.Context, req tfsdk.CreateResourceRequest, resp *tfsdk.CreateResourceResponse) { - var plan StringModelV2 - - diags := req.Plan.Get(ctx, &plan) - resp.Diagnostics.Append(diags...) - if resp.Diagnostics.HasError() { - return - } - params := randomStringParams{ - length: plan.Length.Value, - upper: plan.Upper.Value, - minUpper: plan.MinUpper.Value, - lower: plan.Lower.Value, - minLower: plan.MinLower.Value, - numeric: plan.Numeric.Value, - minNumeric: plan.MinNumeric.Value, - special: plan.Special.Value, - minSpecial: plan.MinSpecial.Value, - overrideSpecial: plan.OverrideSpecial.Value, - } - - result, err := createRandomString(params) - if err != nil { - resp.Diagnostics.Append(randomReadError(err.Error())...) - return - } - - state := StringModelV2{ - ID: types.String{Value: string(result)}, - Keepers: plan.Keepers, - Length: types.Int64{Value: plan.Length.Value}, - Special: types.Bool{Value: plan.Special.Value}, - Upper: types.Bool{Value: plan.Upper.Value}, - Lower: types.Bool{Value: plan.Lower.Value}, - Numeric: types.Bool{Value: plan.Numeric.Value}, - MinNumeric: types.Int64{Value: plan.MinNumeric.Value}, - MinUpper: types.Int64{Value: plan.MinUpper.Value}, - MinLower: types.Int64{Value: plan.MinLower.Value}, - MinSpecial: types.Int64{Value: plan.MinSpecial.Value}, - OverrideSpecial: types.String{Value: plan.OverrideSpecial.Value}, - Result: types.String{Value: string(result)}, - } - - diags = resp.State.Set(ctx, state) - resp.Diagnostics.Append(diags...) - if resp.Diagnostics.HasError() { - return + return map[int64]tfsdk.ResourceStateUpgrader{ + 1: { + PriorSchema: &schemaV1, + StateUpgrader: upgradeStringStateV1toV2, + }, } } -func importString(ctx context.Context, req tfsdk.ImportResourceStateRequest, resp *tfsdk.ImportResourceStateResponse) { - id := req.ID - - state := StringModelV2{ - ID: types.String{Value: id}, - Result: types.String{Value: id}, - Length: types.Int64{Value: int64(len(id))}, - Special: types.Bool{Value: true}, - Upper: types.Bool{Value: true}, - Lower: types.Bool{Value: true}, - Numeric: types.Bool{Value: true}, - MinSpecial: types.Int64{Value: 0}, - MinUpper: types.Int64{Value: 0}, - MinLower: types.Int64{Value: 0}, - MinNumeric: types.Int64{Value: 0}, - } - - state.Keepers.ElemType = types.StringType - - diags := resp.State.Set(ctx, &state) - resp.Diagnostics.Append(diags...) - if resp.Diagnostics.HasError() { - return +func upgradeStringStateV1toV2(ctx context.Context, req tfsdk.UpgradeResourceStateRequest, resp *tfsdk.UpgradeResourceStateResponse) { + type modelV1 struct { + ID types.String `tfsdk:"id"` + Keepers types.Map `tfsdk:"keepers"` + Length types.Int64 `tfsdk:"length"` + Special types.Bool `tfsdk:"special"` + Upper types.Bool `tfsdk:"upper"` + Lower types.Bool `tfsdk:"lower"` + Number types.Bool `tfsdk:"number"` + 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"` } -} -func upgradeStringStateV1toV2(ctx context.Context, req tfsdk.UpgradeResourceStateRequest, resp *tfsdk.UpgradeResourceStateResponse) { - var stringDataV1 StringModelV1 + var stringDataV1 modelV1 resp.Diagnostics.Append(req.State.Get(ctx, &stringDataV1)...) if resp.Diagnostics.HasError() { return } - stringDataV2 := StringModelV2{ + stringDataV2 := modelV2{ Keepers: stringDataV1.Keepers, Length: stringDataV1.Length, Special: stringDataV1.Special, @@ -474,3 +335,19 @@ func upgradeStringStateV1toV2(ctx context.Context, req tfsdk.UpgradeResourceStat diags := resp.State.Set(ctx, stringDataV2) resp.Diagnostics.Append(diags...) } + +type modelV2 struct { + ID types.String `tfsdk:"id"` + Keepers types.Map `tfsdk:"keepers"` + 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"` +} diff --git a/internal/provider/resource_uuid.go b/internal/resources/uuid/resource_uuid.go similarity index 65% rename from internal/provider/resource_uuid.go rename to internal/resources/uuid/resource_uuid.go index 77bd8832..fe238a27 100644 --- a/internal/provider/resource_uuid.go +++ b/internal/resources/uuid/resource_uuid.go @@ -1,4 +1,4 @@ -package provider +package uuid import ( "context" @@ -8,11 +8,19 @@ import ( "github.com/hashicorp/terraform-plugin-framework/diag" "github.com/hashicorp/terraform-plugin-framework/tfsdk" "github.com/hashicorp/terraform-plugin-framework/types" + + "github.com/terraform-providers/terraform-provider-random/internal/diagnostics" ) -type resourceUUIDType struct{} +func NewResourceType() *resourceType { + return &resourceType{} +} + +var _ tfsdk.ResourceType = (*resourceType)(nil) -func (r resourceUUIDType) GetSchema(context.Context) (tfsdk.Schema, diag.Diagnostics) { +type resourceType struct{} + +func (r *resourceType) GetSchema(context.Context) (tfsdk.Schema, diag.Diagnostics) { return tfsdk.Schema{ Description: "The resource `random_uuid` generates random uuid string that is intended to be " + "used as unique identifiers for other resources.\n" + @@ -26,8 +34,10 @@ func (r resourceUUIDType) GetSchema(context.Context) (tfsdk.Schema, diag.Diagnos Type: types.MapType{ ElemType: types.StringType, }, - Optional: true, - PlanModifiers: []tfsdk.AttributePlanModifier{tfsdk.RequiresReplace()}, + Optional: true, + PlanModifiers: []tfsdk.AttributePlanModifier{ + tfsdk.RequiresReplace(), + }, }, "result": { Description: "The generated uuid presented in string format.", @@ -43,29 +53,31 @@ func (r resourceUUIDType) GetSchema(context.Context) (tfsdk.Schema, diag.Diagnos }, nil } -func (r resourceUUIDType) NewResource(_ context.Context, p tfsdk.Provider) (tfsdk.Resource, diag.Diagnostics) { - return resourceUUID{ - p: *(p.(*provider)), - }, nil +func (r resourceType) NewResource(_ context.Context, p tfsdk.Provider) (tfsdk.Resource, diag.Diagnostics) { + return &resource{}, nil } -type resourceUUID struct { - p provider +var ( + _ tfsdk.Resource = (*resource)(nil) + _ tfsdk.ResourceWithImportState = (*resource)(nil) +) + +type resource struct { } -func (r resourceUUID) Create(ctx context.Context, req tfsdk.CreateResourceRequest, resp *tfsdk.CreateResourceResponse) { +func (r *resource) Create(ctx context.Context, req tfsdk.CreateResourceRequest, resp *tfsdk.CreateResourceResponse) { result, err := uuid.GenerateUUID() if err != nil { resp.Diagnostics.AddError( "Create Random UUID error", "There was an error during generation of a UUID.\n\n"+ - retryMsg+ + diagnostics.RetryMsg+ fmt.Sprintf("Original Error: %s", err), ) return } - var plan UUIDModel + var plan modelV0 diags := req.Plan.Get(ctx, &plan) resp.Diagnostics.Append(diags...) @@ -73,7 +85,7 @@ func (r resourceUUID) Create(ctx context.Context, req tfsdk.CreateResourceReques return } - u := &UUIDModel{ + u := &modelV0{ ID: types.String{Value: result}, Result: types.String{Value: result}, Keepers: plan.Keepers, @@ -87,26 +99,26 @@ func (r resourceUUID) Create(ctx context.Context, req tfsdk.CreateResourceReques } // Read does not need to perform any operations as the state in ReadResourceResponse is already populated. -func (r resourceUUID) Read(ctx context.Context, req tfsdk.ReadResourceRequest, resp *tfsdk.ReadResourceResponse) { +func (r *resource) Read(ctx context.Context, req tfsdk.ReadResourceRequest, resp *tfsdk.ReadResourceResponse) { } // Update is intentionally left blank as all required and optional attributes force replacement of the resource // through the RequiresReplace AttributePlanModifier. -func (r resourceUUID) Update(ctx context.Context, req tfsdk.UpdateResourceRequest, resp *tfsdk.UpdateResourceResponse) { +func (r *resource) Update(ctx context.Context, req tfsdk.UpdateResourceRequest, resp *tfsdk.UpdateResourceResponse) { } // 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 resourceUUID) Delete(ctx context.Context, req tfsdk.DeleteResourceRequest, resp *tfsdk.DeleteResourceResponse) { +func (r *resource) Delete(ctx context.Context, req tfsdk.DeleteResourceRequest, resp *tfsdk.DeleteResourceResponse) { } -func (r resourceUUID) ImportState(ctx context.Context, req tfsdk.ImportResourceStateRequest, resp *tfsdk.ImportResourceStateResponse) { +func (r *resource) ImportState(ctx context.Context, req tfsdk.ImportResourceStateRequest, resp *tfsdk.ImportResourceStateResponse) { bytes, err := uuid.ParseUUID(req.ID) if err != nil { resp.Diagnostics.AddError( "Import Random UUID Error", "There was an error during the parsing of the UUID.\n\n"+ - retryMsg+ + diagnostics.RetryMsg+ fmt.Sprintf("Original Error: %s", err), ) return @@ -117,13 +129,13 @@ func (r resourceUUID) ImportState(ctx context.Context, req tfsdk.ImportResourceS resp.Diagnostics.AddError( "Import Random UUID Error", "There was an error during the formatting of the UUID.\n\n"+ - retryMsg+ + diagnostics.RetryMsg+ fmt.Sprintf("Original Error: %s", err), ) return } - var state UUIDModel + var state modelV0 state.ID.Value = result state.Result.Value = result @@ -135,3 +147,9 @@ func (r resourceUUID) ImportState(ctx context.Context, req tfsdk.ImportResourceS return } } + +type modelV0 struct { + ID types.String `tfsdk:"id"` + Keepers types.Map `tfsdk:"keepers"` + Result types.String `tfsdk:"result"` +} diff --git a/internal/provider/validators.go b/internal/validators/validators.go similarity index 97% rename from internal/provider/validators.go rename to internal/validators/validators.go index 8e0e5a22..2ec1d118 100644 --- a/internal/provider/validators.go +++ b/internal/validators/validators.go @@ -1,4 +1,4 @@ -package provider +package validators import ( "context" @@ -12,7 +12,7 @@ import ( "github.com/hashicorp/terraform-plugin-log/tflog" ) -// intIsAtLeastValidator checks that the value of the attribute in the configuration +// intIsAtLeastSumOfValidator checks that the value of the attribute in the configuration // (i.e., AttributeConfig in ValidateAttributeRequest) is greater than or, equal to the sum of the values of the // attributes in the slice of AttributePath. type intIsAtLeastSumOfValidator struct { diff --git a/internal/provider/validators_test.go b/internal/validators/validators_test.go similarity index 95% rename from internal/provider/validators_test.go rename to internal/validators/validators_test.go index 7981aea8..58d18e0e 100644 --- a/internal/provider/validators_test.go +++ b/internal/validators/validators_test.go @@ -1,4 +1,4 @@ -package provider +package validators import ( "context" @@ -19,7 +19,17 @@ func TestIsAtLeastSumOfValidator_Validate(t *testing.T) { AttributePath: tftypes.NewAttributePath().WithAttributeName("length"), AttributeConfig: types.Int64{Value: 16}, Config: tfsdk.Config{ - Schema: passwordSchemaV1(), + Schema: tfsdk.Schema{ + Attributes: map[string]tfsdk.Attribute{ + "min_upper": { + Type: types.Int64Type, + }, + + "min_lower": { + Type: types.Int64Type, + }, + }, + }, }, } diff --git a/main.go b/main.go index 9dc3fff8..4ac97cb2 100644 --- a/main.go +++ b/main.go @@ -20,7 +20,7 @@ import ( //go:generate go run github.com/hashicorp/terraform-plugin-docs/cmd/tfplugindocs func main() { - err := providerserver.Serve(context.Background(), provider.NewFramework, providerserver.ServeOpts{ + err := providerserver.Serve(context.Background(), provider.NewProvider, providerserver.ServeOpts{ Address: "registry.terraform.io/hashicorp/random", }) if err != nil {