From 0a16063816289494b89c58489b29fb09769764ec Mon Sep 17 00:00:00 2001 From: YanniHu1996 Date: Mon, 15 May 2023 19:58:09 +0800 Subject: [PATCH] refactor: rewrite resource_region with terraform-plugin-framework --- go.mod | 2 + go.sum | 4 + pkg/provider/default.go | 28 ++++ pkg/provider/provider.go | 2 +- pkg/provider/resource_region.go | 249 +++++++++++++++++--------------- pkg/provider/utils.go | 11 ++ pkg/provider/validators.go | 9 ++ 7 files changed, 184 insertions(+), 121 deletions(-) create mode 100644 pkg/provider/default.go diff --git a/go.mod b/go.mod index deaccb94..978c0f9e 100644 --- a/go.mod +++ b/go.mod @@ -9,6 +9,8 @@ require ( github.com/hashicorp/go-cty v1.4.1-0.20200414143053-d3edf31b6320 github.com/hashicorp/terraform-plugin-docs v0.14.2-0.20230330141128-e8f8eb1f6dbb github.com/hashicorp/terraform-plugin-framework v1.2.0 + github.com/hashicorp/terraform-plugin-framework-timeouts v0.3.1 + github.com/hashicorp/terraform-plugin-framework-validators v0.10.0 github.com/hashicorp/terraform-plugin-go v0.14.3 github.com/hashicorp/terraform-plugin-log v0.8.0 github.com/hashicorp/terraform-plugin-mux v0.9.0 diff --git a/go.sum b/go.sum index 3cd45c73..f72458f4 100644 --- a/go.sum +++ b/go.sum @@ -111,6 +111,10 @@ github.com/hashicorp/terraform-plugin-docs v0.14.2-0.20230330141128-e8f8eb1f6dbb github.com/hashicorp/terraform-plugin-docs v0.14.2-0.20230330141128-e8f8eb1f6dbb/go.mod h1:1YAwCOLHQLQUkM+rPf1+tCayEK92kdyLIfzSfEDe6og= github.com/hashicorp/terraform-plugin-framework v1.2.0 h1:MZjFFfULnFq8fh04FqrKPcJ/nGpHOvX4buIygT3MSNY= github.com/hashicorp/terraform-plugin-framework v1.2.0/go.mod h1:nToI62JylqXDq84weLJ/U3umUsBhZAaTmU0HXIVUOcw= +github.com/hashicorp/terraform-plugin-framework-timeouts v0.3.1 h1:5GhozvHUsrqxqku+yd0UIRTkmDLp2QPX5paL1Kq5uUA= +github.com/hashicorp/terraform-plugin-framework-timeouts v0.3.1/go.mod h1:ThtYDU8p6sJ9+SI+TYxXrw28vXxgBwYOpoPv1EojSJI= +github.com/hashicorp/terraform-plugin-framework-validators v0.10.0 h1:4L0tmy/8esP6OcvocVymw52lY0HyQ5OxB7VNl7k4bS0= +github.com/hashicorp/terraform-plugin-framework-validators v0.10.0/go.mod h1:qdQJCdimB9JeX2YwOpItEu+IrfoJjWQ5PhLpAOMDQAE= github.com/hashicorp/terraform-plugin-go v0.14.3 h1:nlnJ1GXKdMwsC8g1Nh05tK2wsC3+3BL/DBBxFEki+j0= github.com/hashicorp/terraform-plugin-go v0.14.3/go.mod h1:7ees7DMZ263q8wQ6E4RdIdR6nHHJtrdt4ogX5lPkX1A= github.com/hashicorp/terraform-plugin-log v0.8.0 h1:pX2VQ/TGKu+UU1rCay0OlzosNKe4Nz1pepLXj95oyy0= diff --git a/pkg/provider/default.go b/pkg/provider/default.go new file mode 100644 index 00000000..1c3b4129 --- /dev/null +++ b/pkg/provider/default.go @@ -0,0 +1,28 @@ +package provider + +import ( + "context" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/defaults" + "github.com/hashicorp/terraform-plugin-framework/types" +) + +type defaultString struct { + Desc string + Default string +} + +func DefaultString(desc string, Default string) *defaultString { + return &defaultString{Desc: desc, Default: Default} +} + +func (d defaultString) Description(_ context.Context) string { + return d.Desc +} + +func (d defaultString) MarkdownDescription(_ context.Context) string { + return d.Desc +} + +func (d defaultString) DefaultString(ctx context.Context, request defaults.StringRequest, response *defaults.StringResponse) { + response.PlanValue = types.StringValue(d.Default) +} diff --git a/pkg/provider/provider.go b/pkg/provider/provider.go index 4325aeb0..3c53f48c 100644 --- a/pkg/provider/provider.go +++ b/pkg/provider/provider.go @@ -59,7 +59,6 @@ func New(version string) func() *schema.Provider { ResourcesMap: map[string]*schema.Resource{ "biganimal_cluster": resourceCluster.Schema(), - "biganimal_region": resourceRegion.Schema(), "biganimal_aws_connection": resourceAWSConnection.Schema(), "biganimal_azure_connection": resourceAzureConnection.Schema(), "biganimal_faraway_replica": resourceFAReplica.Schema(), @@ -195,5 +194,6 @@ func (b bigAnimalProvider) DataSources(ctx context.Context) []func() datasource. func (b bigAnimalProvider) Resources(ctx context.Context) []func() resource.Resource { return []func() resource.Resource{ NewProjectResource, + NewRegionResource, } } diff --git a/pkg/provider/resource_region.go b/pkg/provider/resource_region.go index 722ba053..f52e5834 100644 --- a/pkg/provider/resource_region.go +++ b/pkg/provider/resource_region.go @@ -4,185 +4,194 @@ import ( "context" "errors" "fmt" - "time" - "github.com/EnterpriseDB/terraform-provider-biganimal/pkg/api" - "github.com/EnterpriseDB/terraform-provider-biganimal/pkg/utils" + "github.com/hashicorp/terraform-plugin-framework-timeouts/resource/timeouts" + fdiag "github.com/hashicorp/terraform-plugin-framework/diag" + "github.com/hashicorp/terraform-plugin-framework/resource" + fschema "github.com/hashicorp/terraform-plugin-framework/resource/schema" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/planmodifier" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/stringplanmodifier" + "github.com/hashicorp/terraform-plugin-framework/schema/validator" + "github.com/hashicorp/terraform-plugin-framework/tfsdk" "github.com/hashicorp/terraform-plugin-log/tflog" - "github.com/hashicorp/terraform-plugin-sdk/v2/diag" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/retry" - "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" + "time" ) -// RegionResource is a struct to namespace all the functions -// involved in the Region Resource. When multiple resources and objects -// are in the same pkg/provider, then it's difficult to namespace things well -type RegionResource struct{} - -func NewRegionResource() *RegionResource { - return &RegionResource{} +func NewRegionResource() resource.Resource { + return ®ionResource{} } -func (r *RegionResource) Schema() *schema.Resource { - return &schema.Resource{ - Description: "The region resource is used to manage regions for a given cloud provider. See [Activating regions](https://www.enterprisedb.com/docs/biganimal/latest/getting_started/activating_regions/) for more details.", - - CreateContext: r.Create, - ReadContext: r.Read, - UpdateContext: r.Update, - DeleteContext: r.Delete, +type regionResource struct { + client *api.API +} - Timeouts: &schema.ResourceTimeout{ - Create: schema.DefaultTimeout(60 * time.Minute), - Update: schema.DefaultTimeout(60 * time.Minute), - Delete: schema.DefaultTimeout(60 * time.Minute), +func (r regionResource) Schema(ctx context.Context, req resource.SchemaRequest, resp *resource.SchemaResponse) { + resp.Schema = fschema.Schema{ + MarkdownDescription: "The region resource is used to manage regions for a given cloud provider. See [Activating regions](https://www.enterprisedb.com/docs/biganimal/latest/getting_started/activating_regions/) for more details.", + Blocks: map[string]fschema.Block{ + "timeouts": timeouts.Block(ctx, + timeouts.Opts{Create: true, Delete: true, Update: true}), }, - Schema: map[string]*schema.Schema{ - "cloud_provider": { - Description: "Cloud provider. For example, \"aws\" or \"azure\".", - Type: schema.TypeString, - Required: true, + Attributes: map[string]fschema.Attribute{ + "cloud_provider": fschema.StringAttribute{ + MarkdownDescription: "Cloud provider. For example, \"aws\" or \"azure\".", + Required: true, }, - "project_id": { - Description: "BigAnimal Project ID.", - Type: schema.TypeString, - Required: true, - ValidateDiagFunc: validateProjectId, + "project_id": fschema.StringAttribute{ + MarkdownDescription: "BigAnimal Project ID.", + Required: true, + Validators: []validator.String{ + ProjectIdValidator(), + }, + PlanModifiers: []planmodifier.String{ + stringplanmodifier.UseStateForUnknown(), + }, }, - "region_id": { - Description: "Region ID of the region. For example, \"germanywestcentral\" in the Azure cloud provider or \"eu-west-1\" in the AWS cloud provider.", - Type: schema.TypeString, - Required: true, + "region_id": fschema.StringAttribute{ + MarkdownDescription: "Region ID of the region. For example, \"germanywestcentral\" in the Azure cloud provider or \"eu-west-1\" in the AWS cloud provider.", + Required: true, }, - "name": { - Description: "Region name of the region. For example, \"Germany West Central\" or \"EU West 1\".", - Type: schema.TypeString, - Computed: true, + "name": fschema.StringAttribute{ + MarkdownDescription: "Region name of the region. For example, \"Germany West Central\" or \"EU West 1\".", + Computed: true, + PlanModifiers: []planmodifier.String{ + stringplanmodifier.UseStateForUnknown(), + }, }, - "status": { - Description: "Region status of the region. For example, \"ACTIVE\", \"INACTIVE\", or \"SUSPENDED\".", - Type: schema.TypeString, - Optional: true, - Default: api.REGION_ACTIVE, + "status": fschema.StringAttribute{ + MarkdownDescription: "Region status of the region. For example, \"ACTIVE\", \"INACTIVE\", or \"SUSPENDED\".", + Optional: true, + Default: DefaultString("The default of region desired status", api.REGION_ACTIVE), }, - "continent": { - Description: "Continent that region belongs to. For example, \"Asia\", \"Australia\", or \"Europe\".", - Type: schema.TypeString, - Computed: true, + "continent": fschema.StringAttribute{ + MarkdownDescription: "Continent that region belongs to. For example, \"Asia\", \"Australia\", or \"Europe\".", + Computed: true, + PlanModifiers: []planmodifier.String{ + stringplanmodifier.UseStateForUnknown(), + }, }, }, } } -func (r *RegionResource) Create(ctx context.Context, d *schema.ResourceData, meta any) diag.Diagnostics { - return r.Update(ctx, d, meta) +type Region struct { + ProjectID string `tfsdk:"project_id"` + CloudProvider string `tfsdk:"cloud_provider"` + RegionID string `tfsdk:"region_id"` + Name string `tfsdk:"name"` + Status string `tfsdk:"status"` + Continent string `tfsdk:"continent"` + + Timeouts timeouts.Value `tfsdk:"timeouts"` +} + +func (r regionResource) Metadata(ctx context.Context, req resource.MetadataRequest, resp *resource.MetadataResponse) { + resp.TypeName = req.ProviderTypeName + "_region" } -func (r *RegionResource) Read(ctx context.Context, d *schema.ResourceData, meta any) diag.Diagnostics { - if err := r.read(ctx, d, meta); err != nil { - return fromBigAnimalErr(err) +func (r regionResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) { + var config Region + diags := req.Config.Get(ctx, &config) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return } - return diag.Diagnostics{} + + diags = r.update(ctx, config, resp.State) + resp.Diagnostics.Append(diags...) + return } -func (r *RegionResource) read(ctx context.Context, d *schema.ResourceData, meta any) error { - client := api.BuildAPI(meta).RegionClient() - projectId := d.Get("project_id").(string) - cloud_provider := d.Get("cloud_provider").(string) +func (r regionResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) { + var state Region + diags := req.State.Get(ctx, &state) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } - id := d.Get("region_id").(string) - region, err := client.Read(ctx, projectId, cloud_provider, id) + resp.Diagnostics.Append(r.read(ctx, state, resp.State)...) +} + +func (r regionResource) read(ctx context.Context, region Region, state tfsdk.State) fdiag.Diagnostics { + read, err := r.client.RegionClient().Read(ctx, region.ProjectID, region.CloudProvider, region.RegionID) if err != nil { - return err + return fromErr(err, "Error reading region %v", region.RegionID) } - utils.SetOrPanic(d, "name", region.Name) - utils.SetOrPanic(d, "status", region.Status) - utils.SetOrPanic(d, "continent", region.Continent) - d.SetId(fmt.Sprintf("%s/%s", cloud_provider, id)) - - return nil + region.Name = read.Name + region.Status = read.Status + region.Continent = read.Continent + return state.Set(ctx, ®ion) } -func (r *RegionResource) Update(ctx context.Context, d *schema.ResourceData, meta any) diag.Diagnostics { - client := api.BuildAPI(meta).RegionClient() +func (r regionResource) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) { + var plan Region + diags := req.Plan.Get(ctx, &plan) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } - cloudProvider := d.Get("cloud_provider").(string) - id := d.Get("region_id").(string) - projectId := d.Get("project_id").(string) - desiredState := d.Get("status").(string) + resp.Diagnostics.Append(r.update(ctx, plan, resp.State)...) +} - region, err := client.Read(ctx, projectId, cloudProvider, id) +func (r regionResource) update(ctx context.Context, region Region, state tfsdk.State) fdiag.Diagnostics { + current, err := r.client.RegionClient().Read(ctx, region.ProjectID, region.CloudProvider, region.RegionID) if err != nil { - return fromBigAnimalErr(err) + return fromErr(err, "Error reading region %v", region.RegionID) + } + if current.Status == region.Status { // no change, exit early + return nil } - utils.SetOrPanic(d, "name", region.Name) - utils.SetOrPanic(d, "continent", region.Continent) - utils.SetOrPanic(d, "status", desiredState) - d.SetId(fmt.Sprintf("%s/%s", cloudProvider, id)) + tflog.Debug(ctx, fmt.Sprintf("updating region from %s to %s", current.Status, region.Status)) - if desiredState == region.Status { // no change, exit early - return diag.Diagnostics{} + if err := r.client.RegionClient().Update(ctx, region.Status, region.ProjectID, region.CloudProvider, region.RegionID); err != nil { + return fromErr(err, "Error updating region %v", region.RegionID) } - tflog.Debug(ctx, fmt.Sprintf("updating region from %s to %s", region.Status, desiredState)) - if err = client.Update(ctx, desiredState, projectId, cloudProvider, id); err != nil { - return fromBigAnimalErr(err) + timeout, diagnostics := region.Timeouts.Create(ctx, 60*time.Minute) + if diagnostics != nil { + return diagnostics } - // retry until we get success err = retry.RetryContext( ctx, - d.Timeout(schema.TimeoutCreate)-time.Minute, - r.retryFunc(ctx, d, meta, cloudProvider, id, desiredState)) + timeout-time.Minute, + r.retryFunc(ctx, region)) if err != nil { - return diag.FromErr(err) + return fromErr(err, "") } - return diag.Diagnostics{} -} -func (r *RegionResource) Delete(ctx context.Context, d *schema.ResourceData, meta any) diag.Diagnostics { - client := api.BuildAPI(meta).RegionClient() + return r.read(ctx, region, state) +} - projectId := d.Get("project_id").(string) - cloudProvider := d.Get("cloud_provider").(string) - id := d.Get("region_id").(string) - desiredState := api.REGION_INACTIVE - if err := client.Update(ctx, api.REGION_INACTIVE, projectId, cloudProvider, id); err != nil { - return fromBigAnimalErr(err) - } +func (r regionResource) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) { + //TODO implement me + panic("implement me") +} - // retry until we get success - err := retry.RetryContext( - ctx, - d.Timeout(schema.TimeoutDelete)-time.Minute, - r.retryFunc(ctx, d, meta, cloudProvider, id, desiredState)) - if err != nil { - return diag.FromErr(err) +func (r regionResource) Configure(ctx context.Context, req resource.ConfigureRequest, resp *resource.ConfigureResponse) { + if req.ProviderData == nil { + return } - return diag.Diagnostics{} + r.client = req.ProviderData.(*api.API) } -func (r *RegionResource) retryFunc(ctx context.Context, d *schema.ResourceData, meta any, cloudProvider, regionId, desiredState string) retry.RetryFunc { - client := api.BuildAPI(meta).RegionClient() +func (r regionResource) retryFunc(ctx context.Context, region Region) retry.RetryFunc { return func() *retry.RetryError { - projectId := d.Get("project_id").(string) - region, err := client.Read(ctx, projectId, cloudProvider, regionId) + curr, err := r.client.RegionClient().Read(ctx, region.ProjectID, region.CloudProvider, region.RegionID) if err != nil { return retry.NonRetryableError(fmt.Errorf("error describing instance: %s", err)) } - if region.Status != desiredState { + if curr.Status != region.Status { return retry.RetryableError(errors.New("operation incomplete")) } - - if err := r.read(ctx, d, meta); err != nil { - return retry.NonRetryableError(err) - } - return nil } } diff --git a/pkg/provider/utils.go b/pkg/provider/utils.go index 9275f10e..67504f68 100644 --- a/pkg/provider/utils.go +++ b/pkg/provider/utils.go @@ -2,7 +2,9 @@ package provider import ( "errors" + "fmt" "github.com/EnterpriseDB/terraform-provider-biganimal/pkg/api" + diag2 "github.com/hashicorp/terraform-plugin-framework/diag" "github.com/hashicorp/terraform-plugin-sdk/v2/diag" ) @@ -34,3 +36,12 @@ func unsupportedWarning(message string) diag.Diagnostics { }, } } + +func fromErr(err error, summary string, args ...any) diag2.Diagnostics { + summary = fmt.Sprintf(summary, args...) + return diag2.Diagnostics{ + diag2.NewErrorDiagnostic( + summary, err.Error(), + ), + } +} diff --git a/pkg/provider/validators.go b/pkg/provider/validators.go index c3636fe4..c9e7c526 100644 --- a/pkg/provider/validators.go +++ b/pkg/provider/validators.go @@ -3,6 +3,8 @@ package provider import ( "fmt" "github.com/google/uuid" + "github.com/hashicorp/terraform-plugin-framework-validators/stringvalidator" + "github.com/hashicorp/terraform-plugin-framework/schema/validator" "regexp" "strings" @@ -27,6 +29,13 @@ func validateProjectId(v interface{}, path cty.Path) diag.Diagnostics { return diags } +func ProjectIdValidator() validator.String { + return stringvalidator.RegexMatches( + regexp.MustCompile("^prj_[0-9A-Za-z_]{16}$"), + "Please provide a valid name for the project_id, for example: prj_abcdABCD01234567", + ) +} + func validateARN(v interface{}, _ cty.Path) diag.Diagnostics { a, err := arn.Parse(v.(string)) if err != nil || a.Service != "iam" || !strings.HasPrefix(a.Resource, "role") {