diff --git a/.env.example b/.env.example index 95ae07ec..97597b9a 100644 --- a/.env.example +++ b/.env.example @@ -17,3 +17,8 @@ BA_TF_ACC_VAR_region_region_id= # data_source_pgd BA_TF_ACC_VAR_pgd_project_id= BA_TF_ACC_VAR_pgd_name= + +# data_source_region +BA_TF_ACC_VAR_ds_region_project_id= +BA_TF_ACC_VAR_ds_region_provider= +BA_TF_ACC_VAR_ds_region_region_id= diff --git a/docs/data-sources/region.md b/docs/data-sources/region.md index fa2537e5..d02de644 100644 --- a/docs/data-sources/region.md +++ b/docs/data-sources/region.md @@ -1,5 +1,5 @@ # biganimal_region (Data Source) -The region data source shows the available regions within a cloud provider. + ## Example Usage ```terraform @@ -8,8 +8,8 @@ variable "cloud_provider" { description = "Cloud Provider" validation { - condition = contains(["aws", "azure"], var.cloud_provider) - error_message = "Please select one of the supported regions: aws, azure." + condition = contains(["aws", "azure", "bah:aws"], var.cloud_provider) + error_message = "Please select one of the supported regions: aws, azure or bah:aws." } } @@ -40,7 +40,7 @@ output "cloud_provider_id" { ### Required -- `cloud_provider` (String) Cloud provider to list the regions. For example, "aws" or "azure". +- `cloud_provider` (String) Cloud provider to list the regions. For example, "aws", "azure" or "bah:aws". - `project_id` (String) BigAnimal Project ID. ### Optional @@ -50,15 +50,15 @@ output "cloud_provider_id" { ### Read-Only -- `id` (String) The ID of this resource. -- `regions` (List of Object) Region information. (see [below for nested schema](#nestedatt--regions)) +- `id` (String) Datasource ID. +- `regions` (Attributes List) Region information. (see [below for nested schema](#nestedatt--regions)) ### Nested Schema for `regions` Read-Only: -- `continent` (String) -- `name` (String) -- `region_id` (String) -- `status` (String) +- `continent` (String) Continent that region belongs to. +- `name` (String) Region name of the region. +- `region_id` (String) Region ID of the region. +- `status` (String) Region status of the region. diff --git a/docs/resources/region.md b/docs/resources/region.md index 26b333d2..af081226 100644 --- a/docs/resources/region.md +++ b/docs/resources/region.md @@ -43,7 +43,7 @@ output "region_continent" { ### Required -- `cloud_provider` (String) Cloud provider. For example, "aws" or "azure". +- `cloud_provider` (String) Cloud provider. For example, "aws", "azure" or "bah:aws". - `project_id` (String) BigAnimal Project ID. - `region_id` (String) Region ID of the region. For example, "germanywestcentral" in the Azure cloud provider or "eu-west-1" in the AWS cloud provider. @@ -55,7 +55,7 @@ output "region_continent" { ### Read-Only - `continent` (String) Continent that region belongs to. For example, "Asia", "Australia", or "Europe". -- `id` (String) The ID of this resource. +- `id` (String) Resource ID of the region. - `name` (String) Region name of the region. For example, "Germany West Central" or "EU West 1". @@ -66,3 +66,12 @@ Optional: - `create` (String) - `delete` (String) - `update` (String) + +## Import + +Import is supported using the following syntax: + +```shell +# terraform import biganimal_project. // +terraform import biganimal_region.this prj_deadbeef01234567/aws/eu-west-1 +``` diff --git a/examples/data-sources/biganimal_region/data-source.tf b/examples/data-sources/biganimal_region/data-source.tf index c8a724f4..61236156 100644 --- a/examples/data-sources/biganimal_region/data-source.tf +++ b/examples/data-sources/biganimal_region/data-source.tf @@ -3,8 +3,8 @@ variable "cloud_provider" { description = "Cloud Provider" validation { - condition = contains(["aws", "azure"], var.cloud_provider) - error_message = "Please select one of the supported regions: aws, azure." + condition = contains(["aws", "azure", "bah:aws"], var.cloud_provider) + error_message = "Please select one of the supported regions: aws, azure or bah:aws." } } diff --git a/examples/resources/biganimal_region/import.sh b/examples/resources/biganimal_region/import.sh new file mode 100644 index 00000000..ae6a01a2 --- /dev/null +++ b/examples/resources/biganimal_region/import.sh @@ -0,0 +1,2 @@ +# terraform import biganimal_project. // +terraform import biganimal_region.this prj_deadbeef01234567/aws/eu-west-1 diff --git a/go.mod b/go.mod index 52adfbf8..76fcf8fa 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.15.0 github.com/hashicorp/terraform-plugin-framework v1.3.1 + 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.16.0 github.com/hashicorp/terraform-plugin-log v0.9.0 github.com/hashicorp/terraform-plugin-mux v0.10.0 diff --git a/go.sum b/go.sum index efbef7b6..b1c5e8f5 100644 --- a/go.sum +++ b/go.sum @@ -95,6 +95,10 @@ github.com/hashicorp/terraform-plugin-docs v0.15.0 h1:W5xYB5kCUBqO7lyjE2UMmUBh95 github.com/hashicorp/terraform-plugin-docs v0.15.0/go.mod h1:K5Taof1Y7sL4dw6Ie0qMFyQnHN0W+RSVMD0iIyFDFJc= github.com/hashicorp/terraform-plugin-framework v1.3.1 h1:uhd+SuyuDq3oh5VB2Toq5IPyaC5XFAUf9vUFKBmNNOk= github.com/hashicorp/terraform-plugin-framework v1.3.1/go.mod h1:A1WD3Ry7FhrThViUTbkx4ZDsMq9oaAv4U9oTI8bBzCU= +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.16.0 h1:DSOQ0rz5FUiVO4NUzMs8ln9gsPgHMTsfns7Nk+6gPuE= github.com/hashicorp/terraform-plugin-go v0.16.0/go.mod h1:4sn8bFuDbt+2+Yztt35IbOrvZc0zyEi87gJzsTgCES8= github.com/hashicorp/terraform-plugin-log v0.9.0 h1:i7hOA+vdAItN1/7UrfBqBwvYPQ9TFvymaRGZED3FCV0= diff --git a/main.go b/main.go index d486ad59..54df32a7 100644 --- a/main.go +++ b/main.go @@ -3,6 +3,8 @@ package main import ( "context" "flag" + "log" + "github.com/EnterpriseDB/terraform-provider-biganimal/pkg/provider" "github.com/hashicorp/terraform-plugin-framework/providerserver" "github.com/hashicorp/terraform-plugin-go/tfprotov5" @@ -11,7 +13,6 @@ import ( "github.com/hashicorp/terraform-plugin-mux/tf5to6server" "github.com/hashicorp/terraform-plugin-mux/tf6muxserver" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" - "log" ) // Run "go generate" to format example terraform files and generate the docs for the registry/website diff --git a/pkg/models/region.go b/pkg/models/region.go index 4ee7d740..14ce0c81 100644 --- a/pkg/models/region.go +++ b/pkg/models/region.go @@ -1,10 +1,10 @@ package models type Region struct { - Id string `json:"regionId,omitempty" mapstructure:"region_id"` - Name string `json:"regionName,omitempty" mapstructure:"name,omitempty"` - Status string `json:"status,omitempty" mapstructure:"status,omitempty"` - Continent string `json:"continent,omitempty" mapstructure:"continent,omitempty"` + Id string `json:"regionId,omitempty" tfsdk:"region_id"` + Name string `json:"regionName,omitempty" tfsdk:"name"` + Status string `json:"status,omitempty" tfsdk:"status"` + Continent string `json:"continent,omitempty" tfsdk:"continent"` } func (r Region) String() string { diff --git a/pkg/provider/data_source_region.go b/pkg/provider/data_source_region.go index 397710a7..8a58a55e 100644 --- a/pkg/provider/data_source_region.go +++ b/pkg/provider/data_source_region.go @@ -2,114 +2,141 @@ package provider import ( "context" - "errors" "fmt" + "strconv" + "time" "github.com/EnterpriseDB/terraform-provider-biganimal/pkg/api" - "github.com/EnterpriseDB/terraform-provider-biganimal/pkg/utils" - "github.com/hashicorp/terraform-plugin-sdk/v2/diag" - "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" + "github.com/EnterpriseDB/terraform-provider-biganimal/pkg/models" + "github.com/hashicorp/terraform-plugin-framework/datasource" + "github.com/hashicorp/terraform-plugin-framework/datasource/schema" + "github.com/hashicorp/terraform-plugin-framework/schema/validator" + "github.com/hashicorp/terraform-plugin-framework/types" ) -// 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 RegionData struct{} +var _ datasource.DataSourceWithConfigure = ®ionsDataSource{} -func NewRegionData() *RegionData { - return &RegionData{} +// NewRegionsDataSource is a helper function to simplify the provider implementation. +func NewRegionsDataSource() datasource.DataSource { + return ®ionsDataSource{} } -func (r *RegionData) Schema() *schema.Resource { - return &schema.Resource{ - Description: "The region data source shows the available regions within a cloud provider.", - ReadContext: r.Read, +// regionsDataSource is the data source implementation. +type regionsDataSource struct { + client *api.RegionClient +} + +// Configure adds the provider configured client to the data source. +func (r *regionsDataSource) Configure(_ context.Context, req datasource.ConfigureRequest, resp *datasource.ConfigureResponse) { + if req.ProviderData == nil { + return + } + + r.client = req.ProviderData.(*api.API).RegionClient() +} + +func (r *regionsDataSource) Metadata(ctx context.Context, req datasource.MetadataRequest, resp *datasource.MetadataResponse) { + resp.TypeName = req.ProviderTypeName + "_region" +} - //{ - // "regionId": "eu-west-1", - // "regionName": "EU West 1", - // "status": "ACTIVE", - // "continent": "Europe" - //} - Schema: map[string]*schema.Schema{ - "regions": { +func (r *regionsDataSource) Schema(ctx context.Context, req datasource.SchemaRequest, resp *datasource.SchemaResponse) { + resp.Schema = schema.Schema{ + Attributes: map[string]schema.Attribute{ + "id": schema.StringAttribute{ + Description: "Datasource ID.", + Computed: true, + }, + "regions": schema.ListNestedAttribute{ Description: "Region information.", - Type: schema.TypeList, Computed: true, - Elem: &schema.Resource{ - Schema: map[string]*schema.Schema{ - "region_id": { + NestedObject: schema.NestedAttributeObject{ + Attributes: map[string]schema.Attribute{ + "region_id": schema.StringAttribute{ Description: "Region ID of the region.", - Type: schema.TypeString, Computed: true, }, - "name": { + "name": schema.StringAttribute{ Description: "Region name of the region.", - Type: schema.TypeString, Computed: true, }, - "status": { + "status": schema.StringAttribute{ Description: "Region status of the region.", - Type: schema.TypeString, Computed: true, }, - "continent": { + "continent": schema.StringAttribute{ Description: "Continent that region belongs to.", - Type: schema.TypeString, Computed: true, }, }, }, }, - "cloud_provider": { - Description: "Cloud provider to list the regions. For example, \"aws\" or \"azure\".", - Type: schema.TypeString, + + "cloud_provider": schema.StringAttribute{ + Description: "Cloud provider to list the regions. For example, \"aws\", \"azure\" or \"bah:aws\".", Required: true, }, - "project_id": { - Description: "BigAnimal Project ID.", - Type: schema.TypeString, - Required: true, - ValidateDiagFunc: validateProjectId, + "project_id": schema.StringAttribute{ + Description: "BigAnimal Project ID.", + Required: true, + Validators: []validator.String{ + ProjectIdValidator(), + }, }, - "query": { + "query": schema.StringAttribute{ Description: "Query to filter region list.", - Type: schema.TypeString, Optional: true, }, - "region_id": { + "region_id": schema.StringAttribute{ Description: "Unique region ID. For example, \"germanywestcentral\" in the Azure cloud provider, \"eu-west-1\" in the AWS cloud provider.", - Type: schema.TypeString, Optional: true, }, }, } } -func (r *RegionData) Read(ctx context.Context, d *schema.ResourceData, meta any) diag.Diagnostics { - diags := diag.Diagnostics{} - client := api.BuildAPI(meta).RegionClient() - cloud_provider := d.Get("cloud_provider").(string) - projectId := d.Get("project_id").(string) - - query := d.Get("query").(string) +type regionsDataSourceModel struct { + ID *string `tfsdk:"id"` + ProjectId *string `tfsdk:"project_id"` + CloudProvider *string `tfsdk:"cloud_provider"` + RegionId *string `tfsdk:"region_id"` + Query types.String `tfsdk:"query"` + Regions []*models.Region `tfsdk:"regions"` +} - id, ok := d.Get("region_id").(string) - if ok { - query = id +func (r *regionsDataSource) Read(ctx context.Context, req datasource.ReadRequest, resp *datasource.ReadResponse) { + var cfg regionsDataSourceModel + diags := req.Config.Get(ctx, &cfg) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return } - regions, err := client.List(ctx, projectId, cloud_provider, query) - if err != nil { - return fromBigAnimalErr(err) - } + regions := []*models.Region{} + if cfg.RegionId != nil { + region, err := r.client.Read(ctx, *cfg.ProjectId, *cfg.CloudProvider, *cfg.RegionId) + if err != nil { + if appendDiagFromBAErr(err, &diags) { + return + } + diags.AddError(fmt.Sprintf("Error reading region by id: %q", *cfg.RegionId), err.Error()) + return + } + regions = append(regions, region) - if id != "" && len(regions) != 1 { - return diag.FromErr(errors.New("unable to find a unique region")) + } else { + respRegions, err := r.client.List(ctx, *cfg.ProjectId, *cfg.CloudProvider, cfg.Query.ValueString()) + if err != nil { + if appendDiagFromBAErr(err, &diags) { + return + } + diags.AddError(fmt.Sprintf("Error reading region by query: %q", cfg.Query.ValueString()), err.Error()) + return + } + regions = respRegions } - utils.SetOrPanic(d, "regions", regions) - d.SetId(fmt.Sprintf("%s/%s", cloud_provider, query)) - - return diags + cfg.Regions = append(cfg.Regions, regions...) + resourceID := strconv.FormatInt(time.Now().Unix(), 10) + cfg.ID = &resourceID + resp.Diagnostics.Append(resp.State.Set(ctx, &cfg)...) } diff --git a/pkg/provider/data_source_region_test.go b/pkg/provider/data_source_region_test.go new file mode 100644 index 00000000..e41c97ca --- /dev/null +++ b/pkg/provider/data_source_region_test.go @@ -0,0 +1,59 @@ +package provider + +import ( + "fmt" + "os" + "testing" + + "github.com/hashicorp/terraform-plugin-testing/helper/resource" +) + +func TestAccDataSourceRegion_basic(t *testing.T) { + var ( + acc_env_vars_checklist = []string{ + "BA_TF_ACC_VAR_ds_region_project_id", + "BA_TF_ACC_VAR_ds_region_provider", + "BA_TF_ACC_VAR_ds_region_region_id", + } + projectId = os.Getenv("BA_TF_ACC_VAR_ds_region_project_id") + provider = os.Getenv("BA_TF_ACC_VAR_ds_region_provider") + regionId = os.Getenv("BA_TF_ACC_VAR_ds_region_region_id") + ) + + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { + testAccPreCheck(t) + testAccResourcePreCheck(t, "datasource regions", acc_env_vars_checklist) + }, + ProtoV6ProviderFactories: testAccProtoV6ProviderFactories, + Steps: []resource.TestStep{ + { + Config: regionsDataSourceConfig(projectId, provider, regionId), + Check: resource.ComposeTestCheckFunc( + resource.TestCheckResourceAttrPair("data.biganimal_region.test", "regions.0.name", "biganimal_region.test", "name"), + resource.TestCheckResourceAttrPair("data.biganimal_region.test", "regions.0.status", "biganimal_region.test", "status"), + ), + }, + }, + }) +} + +func regionsDataSourceConfig(projectId, provider, regionId string) string { + return fmt.Sprintf(`data "biganimal_region" "test" { + project_id = "%[1]s" + cloud_provider = "%[2]s" + region_id = "%[3]s" +} + +resource "biganimal_region" "test" { + project_id = "%[1]s" + cloud_provider = "%[2]s" + region_id = "%[3]s" + # no need to active region actually for saving time' + status = "INACTIVE" +} + + + +`, projectId, provider, regionId) +} diff --git a/pkg/provider/provider.go b/pkg/provider/provider.go index 374d0dc5..176c7766 100644 --- a/pkg/provider/provider.go +++ b/pkg/provider/provider.go @@ -17,13 +17,11 @@ import ( const DefaultAPIURL = "https://portal.biganimal.com/api/v3" var ( - resourceRegion = NewRegionResource() resourceCluster = NewClusterResource() resourceAWSConnection = NewAWSConnectionResource() resourceAzureConnection = NewAzureConnectionResource() resourceFAReplica = NewFAReplicaResource() - dataRegion = NewRegionData() dataCluster = NewClusterData() dataAWSConnection = NewAWSConnectionData() dataFaReplica = NewFAReplicaData() @@ -53,14 +51,12 @@ func NewSDKProvider(version string) func() *sdkschema.Provider { }, DataSourcesMap: map[string]*sdkschema.Resource{ "biganimal_cluster": dataCluster.Schema(), - "biganimal_region": dataRegion.Schema(), "biganimal_faraway_replica": dataFaReplica.Schema(), "biganimal_aws_connection": dataAWSConnection.Schema(), }, ResourcesMap: map[string]*sdkschema.Resource{ "biganimal_cluster": resourceCluster.Schema(), - "biganimal_region": resourceRegion.Schema(), "biganimal_aws_connection": resourceAWSConnection.Schema(), "biganimal_azure_connection": resourceAzureConnection.Schema(), "biganimal_faraway_replica": resourceFAReplica.Schema(), @@ -181,11 +177,13 @@ func (b bigAnimalProvider) DataSources(ctx context.Context) []func() datasource. return []func() datasource.DataSource{ NewProjectsDataSource, NewPgdDataSource, + NewRegionsDataSource, } } func (b bigAnimalProvider) Resources(ctx context.Context) []func() resource.Resource { return []func() resource.Resource{ NewProjectResource, + NewRegionResource, } } diff --git a/pkg/provider/resource_project.go b/pkg/provider/resource_project.go index 7f869968..db9ef614 100644 --- a/pkg/provider/resource_project.go +++ b/pkg/provider/resource_project.go @@ -2,6 +2,7 @@ package provider import ( "context" + "github.com/hashicorp/terraform-plugin-framework/path" "github.com/EnterpriseDB/terraform-provider-biganimal/pkg/api" diff --git a/pkg/provider/resource_region.go b/pkg/provider/resource_region.go index 722ba053..8bed744e 100644 --- a/pkg/provider/resource_region.go +++ b/pkg/provider/resource_region.go @@ -4,185 +4,244 @@ import ( "context" "errors" "fmt" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/planmodifier" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/stringplanmodifier" + "github.com/hashicorp/terraform-plugin-framework/tfsdk" + "strings" "time" "github.com/EnterpriseDB/terraform-provider-biganimal/pkg/api" - "github.com/EnterpriseDB/terraform-provider-biganimal/pkg/utils" - "github.com/hashicorp/terraform-plugin-log/tflog" - "github.com/hashicorp/terraform-plugin-sdk/v2/diag" + "github.com/hashicorp/terraform-plugin-framework-timeouts/resource/timeouts" + frameworkdiag "github.com/hashicorp/terraform-plugin-framework/diag" + "github.com/hashicorp/terraform-plugin-framework/path" + "github.com/hashicorp/terraform-plugin-framework/resource" + "github.com/hashicorp/terraform-plugin-framework/resource/schema" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/stringdefault" + "github.com/hashicorp/terraform-plugin-framework/schema/validator" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/retry" - "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" ) -// 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{} +type regionResource struct { + client *api.RegionClient } -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, +func (r regionResource) Metadata(ctx context.Context, req resource.MetadataRequest, resp *resource.MetadataResponse) { + resp.TypeName = req.ProviderTypeName + "_region" +} - 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 = schema.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]schema.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]schema.Attribute{ + "id": schema.StringAttribute{ + MarkdownDescription: "Resource ID of the region.", + Computed: true, + PlanModifiers: []planmodifier.String{ + stringplanmodifier.UseStateForUnknown(), + }, }, - "project_id": { - Description: "BigAnimal Project ID.", - Type: schema.TypeString, - Required: true, - ValidateDiagFunc: validateProjectId, + "cloud_provider": schema.StringAttribute{ + MarkdownDescription: "Cloud provider. For example, \"aws\", \"azure\" or \"bah:aws\".", + Required: true, }, - "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, + "project_id": schema.StringAttribute{ + MarkdownDescription: "BigAnimal Project ID.", + Required: true, + Validators: []validator.String{ + ProjectIdValidator(), + }, }, - "name": { - Description: "Region name of the region. For example, \"Germany West Central\" or \"EU West 1\".", - Type: schema.TypeString, - Computed: true, + "region_id": schema.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, + PlanModifiers: []planmodifier.String{ + stringplanmodifier.RequiresReplace(), + }, }, - "status": { - Description: "Region status of the region. For example, \"ACTIVE\", \"INACTIVE\", or \"SUSPENDED\".", - Type: schema.TypeString, - Optional: true, - Default: api.REGION_ACTIVE, + "name": schema.StringAttribute{ + MarkdownDescription: "Region name of the region. For example, \"Germany West Central\" or \"EU West 1\".", + Computed: true, + PlanModifiers: []planmodifier.String{ + stringplanmodifier.UseStateForUnknown(), + }, }, - "continent": { - Description: "Continent that region belongs to. For example, \"Asia\", \"Australia\", or \"Europe\".", - Type: schema.TypeString, - Computed: true, + "status": schema.StringAttribute{ + MarkdownDescription: "Region status of the region. For example, \"ACTIVE\", \"INACTIVE\", or \"SUSPENDED\".", + Optional: true, + Computed: true, + Default: stringdefault.StaticString(api.REGION_ACTIVE), + }, + "continent": schema.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) -} - -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) Configure(_ context.Context, req resource.ConfigureRequest, _ *resource.ConfigureResponse) { + if req.ProviderData == nil { + return } - return diag.Diagnostics{} + + r.client = req.ProviderData.(*api.API).RegionClient() } -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) +type Region struct { + ProjectID *string `tfsdk:"project_id"` + CloudProvider *string `tfsdk:"cloud_provider"` + RegionID *string `tfsdk:"region_id"` + ID *string `tfsdk:"id"` + Name *string `tfsdk:"name"` + Continent *string `tfsdk:"continent"` + Status *string `tfsdk:"status"` - id := d.Get("region_id").(string) - region, err := client.Read(ctx, projectId, cloud_provider, id) - if err != nil { - return err + Timeouts timeouts.Value `tfsdk:"timeouts"` +} + +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 } - 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)) + resp.Diagnostics.Append(r.ensureStatueUpdated(ctx, config)...) + if resp.Diagnostics.HasError() { + return + } - return nil + resp.Diagnostics.Append(r.writeState(ctx, config, &resp.State)...) } -func (r *RegionResource) Update(ctx context.Context, d *schema.ResourceData, meta any) diag.Diagnostics { - client := api.BuildAPI(meta).RegionClient() - - cloudProvider := d.Get("cloud_provider").(string) - id := d.Get("region_id").(string) - projectId := d.Get("project_id").(string) - desiredState := d.Get("status").(string) - - region, err := client.Read(ctx, projectId, cloudProvider, id) - if err != nil { - return fromBigAnimalErr(err) +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 } - 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)) + resp.Diagnostics.Append(r.writeState(ctx, state, &resp.State)...) +} - if desiredState == region.Status { // no change, exit early - return diag.Diagnostics{} +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 } - 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) + resp.Diagnostics.Append(r.ensureStatueUpdated(ctx, plan)...) + if resp.Diagnostics.HasError() { + return } - // retry until we get success - err = retry.RetryContext( - ctx, - d.Timeout(schema.TimeoutCreate)-time.Minute, - r.retryFunc(ctx, d, meta, cloudProvider, id, desiredState)) - if err != nil { - return diag.FromErr(err) + resp.Diagnostics.Append(r.writeState(ctx, plan, &resp.State)...) +} + +func (r *regionResource) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) { + var state Region + diags := req.State.Get(ctx, &state) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return } - return diag.Diagnostics{} + + *state.Status = api.REGION_INACTIVE + resp.Diagnostics.Append(r.ensureStatueUpdated(ctx, state)...) } -func (r *RegionResource) Delete(ctx context.Context, d *schema.ResourceData, meta any) diag.Diagnostics { - client := api.BuildAPI(meta).RegionClient() +func (r *regionResource) ensureStatueUpdated(ctx context.Context, region Region) frameworkdiag.Diagnostics { + if region.Status == nil { + status := api.REGION_ACTIVE + region.Status = &status + } - 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) + diags := frameworkdiag.Diagnostics{} + if err := r.client.Update(ctx, *region.Status, *region.ProjectID, *region.CloudProvider, *region.RegionID); err != nil { + if appendDiagFromBAErr(err, &diags) { + return diags + } + diags.AddError(fmt.Sprintf("Error turning region %q into %q status", *region.RegionID, *region.Status), err.Error()) + return diags + } + + 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.TimeoutDelete)-time.Minute, - r.retryFunc(ctx, d, meta, cloudProvider, id, desiredState)) + timeout-time.Minute, + r.retryFunc(ctx, region)) if err != nil { - return diag.FromErr(err) + if appendDiagFromBAErr(err, &diags) { + return diags + } + diags.AddError(fmt.Sprintf("Error reading region %s", *region.RegionID), err.Error()) } + return diags +} - return diag.Diagnostics{} +func (r *regionResource) writeState(ctx context.Context, region Region, state *tfsdk.State) frameworkdiag.Diagnostics { + read, err := r.client.Read(ctx, *region.ProjectID, *region.CloudProvider, *region.RegionID) + if err != nil { + diags := frameworkdiag.Diagnostics{} + if appendDiagFromBAErr(err, &diags) { + return diags + } + diags.AddError(fmt.Sprintf("Error reading region %s", *region.RegionID), err.Error()) + return diags + } + id := fmt.Sprintf("%s/%s/%s", *region.ProjectID, *region.CloudProvider, *region.RegionID) + region.ID = &id + region.Name = &read.Name + region.Status = &read.Status + region.Continent = &read.Continent + return state.Set(ctx, ®ion) } -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.Read(ctx, *region.ProjectID, *region.CloudProvider, *region.RegionID) if err != nil { - return retry.NonRetryableError(fmt.Errorf("error describing instance: %s", err)) + return retry.NonRetryableError(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 } } + +func (r regionResource) ImportState(ctx context.Context, req resource.ImportStateRequest, resp *resource.ImportStateResponse) { + idParts := strings.Split(req.ID, "/") + if len(idParts) != 3 || idParts[0] == "" || idParts[1] == "" || idParts[2] == "" { + resp.Diagnostics.AddError( + "Unexpected Import Identifier", + fmt.Sprintf("Expected import identifier with format: project_id/cloud_provider/region_id. Got: %q", req.ID), + ) + return + } + + resp.Diagnostics.Append(resp.State.SetAttribute(ctx, path.Root("project_id"), idParts[0])...) + resp.Diagnostics.Append(resp.State.SetAttribute(ctx, path.Root("cloud_provider"), idParts[1])...) + resp.Diagnostics.Append(resp.State.SetAttribute(ctx, path.Root("region_id"), idParts[2])...) +} + +func NewRegionResource() resource.Resource { + return ®ionResource{} +} diff --git a/pkg/provider/resource_region_test.go b/pkg/provider/resource_region_test.go index 30303658..6ef15c68 100644 --- a/pkg/provider/resource_region_test.go +++ b/pkg/provider/resource_region_test.go @@ -21,6 +21,11 @@ func TestAccResourceRegion_basic(t *testing.T) { provider = os.Getenv("BA_TF_ACC_VAR_region_provider") regionID = os.Getenv("BA_TF_ACC_VAR_region_region_id") + regionConfigWithDefault = `resource "biganimal_region" "this" { + project_id = "%s" + cloud_provider = "%s" + region_id = "%s" +}` regionConfig = `resource "biganimal_region" "this" { status = "%s" project_id = "%s" @@ -34,10 +39,10 @@ func TestAccResourceRegion_basic(t *testing.T) { testAccPreCheck(t) testAccResourcePreCheck(t, "region", acc_env_vars_checklist) }, - ProviderFactories: testAccProviderFactories, + ProtoV6ProviderFactories: testAccProtoV6ProviderFactories, Steps: []resource.TestStep{ { - Config: fmt.Sprintf(regionConfig, api.REGION_ACTIVE, projectID, provider, regionID), + Config: fmt.Sprintf(regionConfigWithDefault, projectID, provider, regionID), Check: resource.ComposeTestCheckFunc( resource.TestCheckResourceAttr("biganimal_region.this", "status", api.REGION_ACTIVE), ), diff --git a/pkg/provider/utils.go b/pkg/provider/utils.go index 583fb65d..94388afc 100644 --- a/pkg/provider/utils.go +++ b/pkg/provider/utils.go @@ -4,34 +4,58 @@ import ( "errors" "github.com/EnterpriseDB/terraform-provider-biganimal/pkg/api" + frameworkdiag "github.com/hashicorp/terraform-plugin-framework/diag" - "github.com/hashicorp/terraform-plugin-sdk/v2/diag" + sdkdiag "github.com/hashicorp/terraform-plugin-sdk/v2/diag" ) -func fromBigAnimalErr(err error) diag.Diagnostics { +func fromBigAnimalErr(err error) sdkdiag.Diagnostics { if err == nil { return nil } var baError *api.BigAnimalError if errors.As(err, &baError) { - return diag.Diagnostics{ - diag.Diagnostic{ - Severity: diag.Error, + return sdkdiag.Diagnostics{ + sdkdiag.Diagnostic{ + Severity: sdkdiag.Error, Summary: baError.Error(), Detail: baError.GetDetails(), }, } } - return diag.FromErr(err) + return sdkdiag.FromErr(err) } -func unsupportedWarning(message string) diag.Diagnostics { - return diag.Diagnostics{ - diag.Diagnostic{ - Severity: diag.Warning, +func unsupportedWarning(message string) sdkdiag.Diagnostics { + return sdkdiag.Diagnostics{ + sdkdiag.Diagnostic{ + Severity: sdkdiag.Warning, Summary: "Unsupported", Detail: message, }, } } + +/* +Please use this function for error check after client API calls. +This function returns a boolean representing if passed error is as *api.BigAnimalError, for example: + + r.client.Read(ctx, ...) + if err != nil { + if appendDiagFromBAErr(err){ + return + } + + resp.Diagnostics.AddError(summary, detail) + return + } +*/ +func appendDiagFromBAErr(err error, diags *frameworkdiag.Diagnostics) bool { + var baError *api.BigAnimalError + if errors.As(err, &baError) { + diags.AddError(baError.Error(), baError.GetDetails()) + return true + } + return false +} diff --git a/pkg/provider/validators.go b/pkg/provider/validators.go index d773c4aa..f59c1631 100644 --- a/pkg/provider/validators.go +++ b/pkg/provider/validators.go @@ -5,10 +5,11 @@ import ( "regexp" "strings" - "github.com/google/uuid" - "github.com/aws/aws-sdk-go/aws/arn" + "github.com/google/uuid" "github.com/hashicorp/go-cty/cty" + "github.com/hashicorp/terraform-plugin-framework-validators/stringvalidator" + "github.com/hashicorp/terraform-plugin-framework/schema/validator" "github.com/hashicorp/terraform-plugin-sdk/v2/diag" ) @@ -28,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") {