diff --git a/go.mod b/go.mod index deaccb94..2045e3fd 100644 --- a/go.mod +++ b/go.mod @@ -50,6 +50,8 @@ require ( github.com/hashicorp/logutils v1.0.0 // indirect github.com/hashicorp/terraform-exec v0.18.1 // indirect github.com/hashicorp/terraform-json v0.16.0 // indirect + github.com/hashicorp/terraform-plugin-framework-timeouts v0.3.1 // indirect + github.com/hashicorp/terraform-plugin-framework-validators v0.10.0 // indirect github.com/hashicorp/terraform-registry-address v0.1.0 // indirect github.com/hashicorp/terraform-svchost v0.1.0 // indirect github.com/hashicorp/yamux v0.1.1 // indirect 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/resource_region.go b/pkg/provider/resource_region.go index 722ba053..a09151c3 100644 --- a/pkg/provider/resource_region.go +++ b/pkg/provider/resource_region.go @@ -4,14 +4,22 @@ 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 @@ -186,3 +194,177 @@ func (r *RegionResource) retryFunc(ctx context.Context, d *schema.ResourceData, return nil } } + +type regionResource struct { + client *api.API +} + +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}), + }, + + Attributes: map[string]fschema.Attribute{ + "cloud_provider": fschema.StringAttribute{ + MarkdownDescription: "Cloud provider. For example, \"aws\" or \"azure\".", + Required: true, + }, + "project_id": fschema.StringAttribute{ + MarkdownDescription: "BigAnimal Project ID.", + Required: true, + Validators: []validator.String{ + ProjectIdValidator(), + }, + PlanModifiers: []planmodifier.String{ + stringplanmodifier.UseStateForUnknown(), + }, + }, + "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": 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": 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": fschema.StringAttribute{ + MarkdownDescription: "Continent that region belongs to. For example, \"Asia\", \"Australia\", or \"Europe\".", + Computed: true, + PlanModifiers: []planmodifier.String{ + stringplanmodifier.UseStateForUnknown(), + }, + }, + }, + } +} + +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) 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 + } + + diags = r.update(ctx, config, resp.State) + resp.Diagnostics.Append(diags...) + return +} + +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 + } + + 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 fromErr(err, "Error reading region %v", region.RegionID) + } + + region.Name = read.Name + region.Status = read.Status + region.Continent = read.Continent + return state.Set(ctx, ®ion) +} + +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 + } + + resp.Diagnostics.Append(r.update(ctx, plan, resp.State)...) +} + +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 fromErr(err, "Error reading region %v", region.RegionID) + } + if current.Status == region.Status { // no change, exit early + return nil + } + + tflog.Debug(ctx, fmt.Sprintf("updating region from %s to %s", current.Status, region.Status)) + + 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) + } + + timeout, diagnostics := region.Timeouts.Create(ctx, 60*time.Minute) + if diagnostics != nil { + return diagnostics + } + + err = retry.RetryContext( + ctx, + timeout-time.Minute, + r.retryFunc(ctx, region)) + if err != nil { + return fromErr(err, "") + } + + return r.read(ctx, region, state) +} + +func (r regionResource) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) { + //TODO implement me + panic("implement me") +} + +func (r regionResource) Configure(ctx context.Context, req resource.ConfigureRequest, resp *resource.ConfigureResponse) { + if req.ProviderData == nil { + return + } + + r.client = req.ProviderData.(*api.API) +} + +func (r regionResource) retryFunc(ctx context.Context, region Region) retry.RetryFunc { + return func() *retry.RetryError { + 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 curr.Status != region.Status { + return retry.RetryableError(errors.New("operation incomplete")) + } + 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") {