diff --git a/internal/provider/provider.go b/internal/provider/provider.go index 2366e367..598e77c0 100644 --- a/internal/provider/provider.go +++ b/internal/provider/provider.go @@ -20,7 +20,6 @@ func New() *schema.Provider { "random_pet": resourcePet(), "random_string": resourceString(), "random_password": resourcePassword(), - "random_integer": resourceInteger(), }, } } diff --git a/internal/provider_fm/models.go b/internal/provider_fm/models.go index 83fb958d..fab86258 100644 --- a/internal/provider_fm/models.go +++ b/internal/provider_fm/models.go @@ -13,8 +13,17 @@ type ID struct { Dec types.String `tfsdk:"dec"` } +type Integer 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 UUID struct { ID types.String `tfsdk:"id"` - Result types.String `tfsdk:"result"` Keepers types.Map `tfsdk:"keepers"` + Result types.String `tfsdk:"result"` } diff --git a/internal/provider_fm/provider.go b/internal/provider_fm/provider.go index de5d427f..c7c6f92f 100644 --- a/internal/provider_fm/provider.go +++ b/internal/provider_fm/provider.go @@ -24,8 +24,9 @@ func (p *provider) Configure(ctx context.Context, req tfsdk.ConfigureProviderReq func (p *provider) GetResources(ctx context.Context) (map[string]tfsdk.ResourceType, diag.Diagnostics) { return map[string]tfsdk.ResourceType{ - "random_id": resourceIDType{}, - "random_uuid": resourceUUIDType{}, + "random_id": resourceIDType{}, + "random_integer": resourceIntegerType{}, + "random_uuid": resourceUUIDType{}, }, nil } diff --git a/internal/provider_fm/resource_integer.go b/internal/provider_fm/resource_integer.go new file mode 100644 index 00000000..5f6e52f9 --- /dev/null +++ b/internal/provider_fm/resource_integer.go @@ -0,0 +1,192 @@ +package provider_fm + +import ( + "context" + "fmt" + "github.com/hashicorp/terraform-plugin-framework/diag" + "github.com/hashicorp/terraform-plugin-framework/tfsdk" + "github.com/hashicorp/terraform-plugin-framework/types" + "strconv" + "strings" +) + +type resourceIntegerType struct{} + +func (r resourceIntegerType) 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" + + "\n" + + "This resource can be used in conjunction with resources that have the `create_before_destroy` " + + "lifecycle flag set, to avoid conflicts with unique names during the brief period where both the " + + "old and new resources exist concurrently.", + 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()}, + }, + "min": { + Description: "The minimum inclusive value of the range.", + Type: types.Int64Type, + Required: true, + PlanModifiers: []tfsdk.AttributePlanModifier{tfsdk.RequiresReplace()}, + }, + "max": { + Description: "The maximum inclusive value of the range.", + Type: types.Int64Type, + Required: true, + PlanModifiers: []tfsdk.AttributePlanModifier{tfsdk.RequiresReplace()}, + }, + "seed": { + Description: "A custom seed to always produce the same value.", + Type: types.StringType, + Optional: true, + PlanModifiers: []tfsdk.AttributePlanModifier{tfsdk.RequiresReplace()}, + }, + "result": { + Description: "The random integer result.", + Type: types.Int64Type, + Computed: true, + }, + "id": { + Description: "The generated uuid presented in string format.", + Type: types.StringType, + Computed: true, + }, + }, + }, nil +} + +func (r resourceIntegerType) NewResource(_ context.Context, p tfsdk.Provider) (tfsdk.Resource, diag.Diagnostics) { + return resourceInteger{ + p: *(p.(*provider)), + }, nil +} + +type resourceInteger struct { + p provider +} + +func (r resourceInteger) Create(ctx context.Context, req tfsdk.CreateResourceRequest, resp *tfsdk.CreateResourceResponse) { + if !r.p.configured { + resp.Diagnostics.AddError( + "provider not configured", + "provider not configured", + ) + } + + var plan Integer + diags := req.Plan.Get(ctx, &plan) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } + + max := int(plan.Max.Value) + min := int(plan.Min.Value) + seed := plan.Seed.Value + + if max < min { + resp.Diagnostics.AddError( + "minimum value needs to be smaller than or equal to maximum value", + "minimum value needs to be smaller than or equal to maximum value", + ) + return + } + + rand := NewRand(seed) + number := rand.Intn((max+1)-min) + min + + u := &Integer{ + ID: types.String{Value: strconv.Itoa(number)}, + Keepers: plan.Keepers, + Min: types.Int64{Value: int64(min)}, + Max: types.Int64{Value: int64(max)}, + Result: types.Int64{Value: int64(number)}, + } + + if seed != "" { + u.Seed.Value = seed + } else { + u.Seed.Null = true + } + + diags = resp.State.Set(ctx, u) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } +} + +func (r resourceInteger) Read(ctx context.Context, req tfsdk.ReadResourceRequest, resp *tfsdk.ReadResourceResponse) { + // Intentionally left blank. +} + +func (r resourceInteger) Update(ctx context.Context, req tfsdk.UpdateResourceRequest, resp *tfsdk.UpdateResourceResponse) { + // Intentionally left blank. +} + +func (r resourceInteger) Delete(ctx context.Context, req tfsdk.DeleteResourceRequest, resp *tfsdk.DeleteResourceResponse) { + resp.State.RemoveResource(ctx) +} + +func (r resourceInteger) 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( + "Invalid import usage: expecting {result},{min},{max} or {result},{min},{max},{seed}", + "Invalid import usage: expecting {result},{min},{max} or {result},{min},{max},{seed}", + ) + return + } + + result, err := strconv.ParseInt(parts[0], 10, 64) + if err != nil { + resp.Diagnostics.AddError( + "error parsing result", + fmt.Sprintf("error parsing result: %s", err), + ) + return + } + + min, err := strconv.ParseInt(parts[1], 10, 64) + if err != nil { + resp.Diagnostics.AddError( + "error parsing min", + fmt.Sprintf("error parsing min: %s", err), + ) + return + } + + max, err := strconv.ParseInt(parts[2], 10, 64) + if err != nil { + resp.Diagnostics.AddError( + "error parsing max", + fmt.Sprintf("error parsing max: %s", err), + ) + return + } + + var state Integer + + state.ID.Value = parts[0] + state.Keepers.ElemType = types.StringType + state.Result.Value = int64(result) + state.Min.Value = int64(min) + state.Max.Value = int64(max) + + if len(parts) == 4 { + state.Seed.Value = parts[3] + } + + diags := resp.State.Set(ctx, &state) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } +} diff --git a/internal/provider/resource_integer_test.go b/internal/provider_fm/resource_integer_test.go similarity index 87% rename from internal/provider/resource_integer_test.go rename to internal/provider_fm/resource_integer_test.go index b9ad040a..df2c74e3 100644 --- a/internal/provider/resource_integer_test.go +++ b/internal/provider_fm/resource_integer_test.go @@ -1,4 +1,4 @@ -package provider +package provider_fm import ( "fmt" @@ -11,8 +11,8 @@ import ( func TestAccResourceIntegerBasic(t *testing.T) { t.Parallel() resource.UnitTest(t, resource.TestCase{ - PreCheck: func() { testAccPreCheck(t) }, - ProviderFactories: testAccProviders, + PreCheck: func() { testAccPreCheck(t) }, + ProtoV6ProviderFactories: testAccProtoV6ProviderFactories, Steps: []resource.TestStep{ { Config: testRandomIntegerBasic, @@ -33,8 +33,8 @@ func TestAccResourceIntegerBasic(t *testing.T) { func TestAccResourceIntegerUpdate(t *testing.T) { t.Parallel() resource.UnitTest(t, resource.TestCase{ - PreCheck: func() { testAccPreCheck(t) }, - ProviderFactories: testAccProviders, + PreCheck: func() { testAccPreCheck(t) }, + ProtoV6ProviderFactories: testAccProtoV6ProviderFactories, Steps: []resource.TestStep{ { Config: testRandomIntegerBasic, @@ -55,8 +55,8 @@ func TestAccResourceIntegerUpdate(t *testing.T) { func TestAccResourceIntegerSeedless_to_seeded(t *testing.T) { t.Parallel() resource.UnitTest(t, resource.TestCase{ - PreCheck: func() { testAccPreCheck(t) }, - ProviderFactories: testAccProviders, + PreCheck: func() { testAccPreCheck(t) }, + ProtoV6ProviderFactories: testAccProtoV6ProviderFactories, Steps: []resource.TestStep{ { Config: testRandomIntegerSeedless, @@ -77,8 +77,8 @@ 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) }, - ProviderFactories: testAccProviders, + PreCheck: func() { testAccPreCheck(t) }, + ProtoV6ProviderFactories: testAccProtoV6ProviderFactories, Steps: []resource.TestStep{ { Config: testRandomIntegerBasic, @@ -99,8 +99,8 @@ func TestAccResourceIntegerSeeded_to_seedless(t *testing.T) { func TestAccResourceIntegerBig(t *testing.T) { t.Parallel() resource.UnitTest(t, resource.TestCase{ - PreCheck: func() { testAccPreCheck(t) }, - ProviderFactories: testAccProviders, + PreCheck: func() { testAccPreCheck(t) }, + ProtoV6ProviderFactories: testAccProtoV6ProviderFactories, Steps: []resource.TestStep{ { Config: testRandomIntegerBig, diff --git a/internal/provider_fm/seed.go b/internal/provider_fm/seed.go new file mode 100644 index 00000000..ac0283a4 --- /dev/null +++ b/internal/provider_fm/seed.go @@ -0,0 +1,24 @@ +package provider_fm + +import ( + "hash/crc64" + "math/rand" + "time" +) + +// NewRand returns a seeded random number generator, using a seed derived +// from the provided string. +// +// If the seed string is empty, the current time is used as a seed. +func NewRand(seed string) *rand.Rand { + var seedInt int64 + if seed != "" { + crcTable := crc64.MakeTable(crc64.ISO) + seedInt = int64(crc64.Checksum([]byte(seed), crcTable)) + } else { + seedInt = time.Now().UnixNano() + } + + randSource := rand.NewSource(seedInt) + return rand.New(randSource) +}