Skip to content

Commit

Permalink
refactor: rewrite resource_region with terraform-plugin-framework
Browse files Browse the repository at this point in the history
  • Loading branch information
YanniHu1996 committed May 31, 2023
1 parent 76efdce commit 4a37c93
Show file tree
Hide file tree
Showing 7 changed files with 184 additions and 121 deletions.
2 changes: 2 additions & 0 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
4 changes: 4 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -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=
Expand Down
28 changes: 28 additions & 0 deletions pkg/provider/default.go
Original file line number Diff line number Diff line change
@@ -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)
}
2 changes: 1 addition & 1 deletion pkg/provider/provider.go
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,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(),
Expand Down Expand Up @@ -194,5 +193,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,
}
}
249 changes: 129 additions & 120 deletions pkg/provider/resource_region.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 &regionResource{}
}

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, &region)
}

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
}
}
Loading

0 comments on commit 4a37c93

Please sign in to comment.