From d4502ea93bd0b41b0309d3f530971a57da0e9c84 Mon Sep 17 00:00:00 2001 From: Benjamin Bennett Date: Tue, 3 May 2022 15:39:53 +0100 Subject: [PATCH] Migrating id resource to framework (#177) --- internal/provider/provider.go | 1 - internal/provider_fm/models.go | 13 +- internal/provider_fm/provider.go | 1 + internal/provider_fm/resource_id.go | 219 ++++++++++++++++++ .../resource_id_test.go | 10 +- internal/provider_fm/resource_uuid.go | 11 +- 6 files changed, 238 insertions(+), 17 deletions(-) create mode 100644 internal/provider_fm/resource_id.go rename internal/{provider => provider_fm}/resource_id_test.go (90%) diff --git a/internal/provider/provider.go b/internal/provider/provider.go index e2cefd790..2366e3670 100644 --- a/internal/provider/provider.go +++ b/internal/provider/provider.go @@ -16,7 +16,6 @@ func New() *schema.Provider { Schema: map[string]*schema.Schema{}, ResourcesMap: map[string]*schema.Resource{ - "random_id": resourceId(), "random_shuffle": resourceShuffle(), "random_pet": resourcePet(), "random_string": resourceString(), diff --git a/internal/provider_fm/models.go b/internal/provider_fm/models.go index 39b92d0da..83fb958d3 100644 --- a/internal/provider_fm/models.go +++ b/internal/provider_fm/models.go @@ -2,8 +2,19 @@ package provider_fm import "github.com/hashicorp/terraform-plugin-framework/types" +type ID 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 UUID struct { ID types.String `tfsdk:"id"` Result types.String `tfsdk:"result"` - Keepers types.Map `tfsdk:"keepers""` + Keepers types.Map `tfsdk:"keepers"` } diff --git a/internal/provider_fm/provider.go b/internal/provider_fm/provider.go index 44601a1da..de5d427fa 100644 --- a/internal/provider_fm/provider.go +++ b/internal/provider_fm/provider.go @@ -24,6 +24,7 @@ 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{}, }, nil } diff --git a/internal/provider_fm/resource_id.go b/internal/provider_fm/resource_id.go new file mode 100644 index 000000000..6677500db --- /dev/null +++ b/internal/provider_fm/resource_id.go @@ -0,0 +1,219 @@ +package provider_fm + +import ( + "context" + "crypto/rand" + "encoding/base64" + hex2 "encoding/hex" + "fmt" + "github.com/hashicorp/terraform-plugin-framework/diag" + "github.com/hashicorp/terraform-plugin-framework/tfsdk" + "github.com/hashicorp/terraform-plugin-framework/types" + "math/big" + "strings" +) + +type resourceIDType struct{} + +func (r resourceIDType) GetSchema(context.Context) (tfsdk.Schema, diag.Diagnostics) { + return tfsdk.Schema{ + Description: ` +The resource ` + "`random_id`" + ` generates random numbers that are intended to be +used as unique identifiers for other resources. + +This resource *does* use a cryptographic random number generator in order +to minimize the chance of collisions, making the results of this resource +when a 16-byte identifier is requested of equivalent uniqueness to a +type-4 UUID. + +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()}, + }, + "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()}, + }, + "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()}, + }, + "b64_url": { + Description: "The generated id presented in base64, using the URL-friendly character set: " + + "case-sensitive letters, digits and the characters `_` and `-`.", + Type: types.StringType, + Computed: true, + }, + "b64_std": { + Description: "The generated id presented in base64 without additional transformations.", + Type: types.StringType, + Computed: true, + }, + "hex": { + Description: "The generated id presented in padded hexadecimal digits. This result will " + + "always be twice as long as the requested byte length.", + Type: types.StringType, + Computed: true, + }, + "dec": { + Description: "The generated id presented in non-padded decimal digits.", + Type: types.StringType, + Computed: true, + }, + "id": { + Description: "The generated id presented in base64 without additional transformations or prefix.", + Type: types.StringType, + Computed: true, + }, + }, + }, nil +} + +func (r resourceIDType) NewResource(_ context.Context, p tfsdk.Provider) (tfsdk.Resource, diag.Diagnostics) { + return resourceID{ + p: *(p.(*provider)), + }, nil +} + +type resourceID struct { + p provider +} + +func (r resourceID) 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 ID + + diags := req.Plan.Get(ctx, &plan) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } + + byteLength := plan.ByteLength.Value + bytes := make([]byte, byteLength) + + n, err := rand.Reader.Read(bytes) + if int64(n) != byteLength { + resp.Diagnostics.AddError( + "generated insufficient random bytes: %s", + fmt.Sprintf("generated insufficient random bytes: %s", err), + ) + return + } + if err != nil { + resp.Diagnostics.AddError( + "error generating random bytes", + fmt.Sprintf("error generating random bytes: %s", err), + ) + return + } + + id := base64.RawURLEncoding.EncodeToString(bytes) + prefix := plan.Prefix.Value + b64Std := base64.StdEncoding.EncodeToString(bytes) + hex := hex2.EncodeToString(bytes) + + bigInt := big.Int{} + bigInt.SetBytes(bytes) + dec := bigInt.String() + + i := ID{ + ID: types.String{Value: id}, + Keepers: plan.Keepers, + ByteLength: types.Int64{Value: plan.ByteLength.Value}, + Prefix: plan.Prefix, + B64URL: types.String{Value: prefix + id}, + B64Std: types.String{Value: prefix + b64Std}, + Hex: types.String{Value: prefix + hex}, + Dec: types.String{Value: prefix + dec}, + } + + diags = resp.State.Set(ctx, i) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } +} + +func (r resourceID) Read(ctx context.Context, req tfsdk.ReadResourceRequest, resp *tfsdk.ReadResourceResponse) { + // Intentionally left blank. +} + +func (r resourceID) Update(ctx context.Context, req tfsdk.UpdateResourceRequest, resp *tfsdk.UpdateResourceResponse) { + // Intentionally left blank. +} + +func (r resourceID) Delete(ctx context.Context, req tfsdk.DeleteResourceRequest, resp *tfsdk.DeleteResourceResponse) { + resp.State.RemoveResource(ctx) +} + +func (r resourceID) ImportState(ctx context.Context, req tfsdk.ImportResourceStateRequest, resp *tfsdk.ImportResourceStateResponse) { + id := req.ID + var prefix string + + sep := strings.LastIndex(id, ",") + if sep != -1 { + prefix = id[:sep] + id = id[sep+1:] + } + + bytes, err := base64.RawURLEncoding.DecodeString(id) + if err != nil { + resp.Diagnostics.AddError( + "error decoding ID", + fmt.Sprintf("error decoding ID: %s", err)) + return + } + + b64Std := base64.StdEncoding.EncodeToString(bytes) + hex := hex2.EncodeToString(bytes) + + bigInt := big.Int{} + bigInt.SetBytes(bytes) + dec := bigInt.String() + + var state ID + + state.ID.Value = id + state.ByteLength.Value = int64(len(bytes)) + state.Keepers.ElemType = types.StringType + state.B64Std.Value = prefix + b64Std + state.B64URL.Value = prefix + id + state.Hex.Value = prefix + hex + state.Dec.Value = prefix + dec + + if prefix == "" { + state.Prefix.Null = true + } else { + state.Prefix.Value = prefix + } + + diags := resp.State.Set(ctx, &state) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } +} diff --git a/internal/provider/resource_id_test.go b/internal/provider_fm/resource_id_test.go similarity index 90% rename from internal/provider/resource_id_test.go rename to internal/provider_fm/resource_id_test.go index d1d03afda..7dda7ec06 100644 --- a/internal/provider/resource_id_test.go +++ b/internal/provider_fm/resource_id_test.go @@ -1,4 +1,4 @@ -package provider +package provider_fm import ( "fmt" @@ -16,8 +16,8 @@ type idLens struct { func TestAccResourceID(t *testing.T) { resource.UnitTest(t, resource.TestCase{ - PreCheck: func() { testAccPreCheck(t) }, - ProviderFactories: testAccProviders, + PreCheck: func() { testAccPreCheck(t) }, + ProtoV6ProviderFactories: testAccProtoV6ProviderFactories, Steps: []resource.TestStep{ { Config: testAccResourceIDConfig, @@ -40,8 +40,8 @@ func TestAccResourceID(t *testing.T) { func TestAccResourceID_importWithPrefix(t *testing.T) { resource.UnitTest(t, resource.TestCase{ - PreCheck: func() { testAccPreCheck(t) }, - ProviderFactories: testAccProviders, + PreCheck: func() { testAccPreCheck(t) }, + ProtoV6ProviderFactories: testAccProtoV6ProviderFactories, Steps: []resource.TestStep{ { Config: testAccResourceIDConfigWithPrefix, diff --git a/internal/provider_fm/resource_uuid.go b/internal/provider_fm/resource_uuid.go index 49666d265..3bd7f7c76 100644 --- a/internal/provider_fm/resource_uuid.go +++ b/internal/provider_fm/resource_uuid.go @@ -7,7 +7,6 @@ import ( "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" ) type resourceUUIDType struct{} @@ -94,13 +93,7 @@ func (r resourceUUID) Read(ctx context.Context, req tfsdk.ReadResourceRequest, r } func (r resourceUUID) Update(ctx context.Context, req tfsdk.UpdateResourceRequest, resp *tfsdk.UpdateResourceResponse) { - var plan UUID - diags := req.Plan.Get(ctx, &plan) - resp.Diagnostics.Append(diags...) - if resp.Diagnostics.HasError() { - return - } - + // Intentionally left blank. } func (r resourceUUID) Delete(ctx context.Context, req tfsdk.DeleteResourceRequest, resp *tfsdk.DeleteResourceResponse) { @@ -108,8 +101,6 @@ func (r resourceUUID) Delete(ctx context.Context, req tfsdk.DeleteResourceReques } func (r resourceUUID) ImportState(ctx context.Context, req tfsdk.ImportResourceStateRequest, resp *tfsdk.ImportResourceStateResponse) { - tfsdk.ResourceImportStatePassthroughID(ctx, tftypes.NewAttributePath().WithAttributeName("id"), req, resp) - bytes, err := uuid.ParseUUID(req.ID) if err != nil { resp.Diagnostics.AddError(