diff --git a/internal/provider/products_data_source.go b/internal/provider/datasource/products/products_data_source.go similarity index 99% rename from internal/provider/products_data_source.go rename to internal/provider/datasource/products/products_data_source.go index b706cc5..412f7c9 100644 --- a/internal/provider/products_data_source.go +++ b/internal/provider/datasource/products/products_data_source.go @@ -1,4 +1,4 @@ -package provider +package products import ( "context" diff --git a/internal/provider/products_data_source_test.go b/internal/provider/datasource/products/products_data_source_test.go similarity index 68% rename from internal/provider/products_data_source_test.go rename to internal/provider/datasource/products/products_data_source_test.go index 9de6ba0..fc75d7f 100644 --- a/internal/provider/products_data_source_test.go +++ b/internal/provider/datasource/products/products_data_source_test.go @@ -1,15 +1,16 @@ -package provider +package products_test import ( "testing" "github.com/hashicorp/terraform-plugin-testing/helper/resource" + "github.com/timescale/terraform-provider-timescale/internal/test" ) func TestProductDataSource(t *testing.T) { resource.Test(t, resource.TestCase{ - ProtoV6ProviderFactories: testAccProtoV6ProviderFactories, - PreCheck: func() { testAccPreCheck(t) }, + ProtoV6ProviderFactories: test.TestAccProtoV6ProviderFactories, + PreCheck: func() { test.TestAccPreCheck(t) }, Steps: []resource.TestStep{ // Read datasource { @@ -24,7 +25,7 @@ func TestProductDataSource(t *testing.T) { } func newProductsConfig() string { - return providerConfig + ` + return test.ProviderConfig + ` data "timescale_products" "products" { }` } diff --git a/internal/provider/datasource/service/service_data_source.go b/internal/provider/datasource/service/service_data_source.go new file mode 100644 index 0000000..3ecfbfa --- /dev/null +++ b/internal/provider/datasource/service/service_data_source.go @@ -0,0 +1,83 @@ +package service + +import ( + "context" + "fmt" + + "github.com/hashicorp/terraform-plugin-framework/datasource" + "github.com/hashicorp/terraform-plugin-framework/datasource/schema" + "github.com/hashicorp/terraform-plugin-framework/path" + "github.com/hashicorp/terraform-plugin-log/tflog" + + tsClient "github.com/timescale/terraform-provider-timescale/internal/client" + sc "github.com/timescale/terraform-provider-timescale/internal/schema" +) + +// Ensure provider defined types fully satisfy framework interfaces. +var _ datasource.DataSource = &ServiceDataSource{} +var _ datasource.DataSourceWithConfigure = &ServiceDataSource{} + +func NewServiceDataSource() datasource.DataSource { + return &ServiceDataSource{} +} + +// ServiceDataSource defines the data source implementation. +type ServiceDataSource struct { + client *tsClient.Client +} + +func (d *ServiceDataSource) Metadata(ctx context.Context, req datasource.MetadataRequest, resp *datasource.MetadataResponse) { + resp.TypeName = req.ProviderTypeName + "_service" +} + +func (d *ServiceDataSource) Schema(ctx context.Context, req datasource.SchemaRequest, resp *datasource.SchemaResponse) { + resp.Schema = schema.Schema{ + // This description is used by the documentation generator and the language server. + MarkdownDescription: "Service data source", + Attributes: sc.Converter(sc.Service()), + } +} + +func (d *ServiceDataSource) Configure(ctx context.Context, req datasource.ConfigureRequest, resp *datasource.ConfigureResponse) { + tflog.Trace(ctx, "ServiceDataSource.Configure") + + if req.ProviderData == nil { + return + } + client, ok := req.ProviderData.(*tsClient.Client) + + if !ok { + resp.Diagnostics.AddError( + "Unexpected Client Type", + fmt.Sprintf("Expected *tsClient.Client, got: %T. Please report this issue to the provider developers.", req.ProviderData), + ) + return + } + + d.client = client +} + +func (d *ServiceDataSource) Read(ctx context.Context, req datasource.ReadRequest, resp *datasource.ReadResponse) { + tflog.Trace(ctx, "ServiceDataSource.Read") + + var id string + resp.Diagnostics.Append(req.Config.GetAttribute(ctx, path.Root("id"), &id)...) + + if resp.Diagnostics.HasError() { + tflog.Error(ctx, fmt.Sprintf("error reading terraform plan %v", resp.Diagnostics.Errors())) + return + } + + tflog.Info(ctx, "Getting Service: "+id) + service, err := d.client.GetService(ctx, id) + if err != nil { + resp.Diagnostics.AddError("Client Error", fmt.Sprintf("Unable to read service, got error: %s", err)) + return + } + state := sc.NewService(ctx, service, &resp.Diagnostics, nil) + resp.Diagnostics.Append(resp.State.Set(ctx, state)...) + if resp.Diagnostics.HasError() { + tflog.Error(ctx, fmt.Sprintf("error updating terraform state %v", resp.Diagnostics.Errors())) + return + } +} diff --git a/internal/provider/service_data_source_test.go b/internal/provider/datasource/service/service_data_source_test.go similarity index 89% rename from internal/provider/service_data_source_test.go rename to internal/provider/datasource/service/service_data_source_test.go index 4d5d0cc..139340f 100644 --- a/internal/provider/service_data_source_test.go +++ b/internal/provider/datasource/service/service_data_source_test.go @@ -1,14 +1,15 @@ -package provider +package service_test import ( "testing" "github.com/hashicorp/terraform-plugin-testing/helper/resource" + "github.com/timescale/terraform-provider-timescale/internal/test" ) func TestServiceDataSource(t *testing.T) { resource.ParallelTest(t, resource.TestCase{ - ProtoV6ProviderFactories: testAccProtoV6ProviderFactories, + ProtoV6ProviderFactories: test.TestAccProtoV6ProviderFactories, Steps: []resource.TestStep{ // Read testing { @@ -31,7 +32,7 @@ func TestServiceDataSource(t *testing.T) { } func newServiceDataSource() string { - return providerConfig + ` + return test.ProviderConfig + ` resource "timescale_service" "resource" { name = "newServiceDataSource test" } diff --git a/internal/provider/vpcs_data_source.go b/internal/provider/datasource/vpc/vpcs_data_source.go similarity index 100% rename from internal/provider/vpcs_data_source.go rename to internal/provider/datasource/vpc/vpcs_data_source.go diff --git a/internal/provider/provider.go b/internal/provider/provider.go index 24fe2a4..ac22be6 100644 --- a/internal/provider/provider.go +++ b/internal/provider/provider.go @@ -14,6 +14,10 @@ import ( "github.com/hashicorp/terraform-plugin-log/tflog" tsClient "github.com/timescale/terraform-provider-timescale/internal/client" + d_products "github.com/timescale/terraform-provider-timescale/internal/provider/datasource/products" + d_service "github.com/timescale/terraform-provider-timescale/internal/provider/datasource/service" + d_vpc "github.com/timescale/terraform-provider-timescale/internal/provider/datasource/vpc" + r_service "github.com/timescale/terraform-provider-timescale/internal/provider/resource/service" ) // Ensure TimescaleProvider satisfies various provider interfaces. @@ -120,7 +124,7 @@ func (p *TimescaleProvider) Configure(ctx context.Context, req provider.Configur func (p *TimescaleProvider) Resources(ctx context.Context) []func() resource.Resource { tflog.Trace(ctx, "TimescaleProvider.Resources") return []func() resource.Resource{ - NewServiceResource, + r_service.NewServiceResource, } } @@ -128,9 +132,9 @@ func (p *TimescaleProvider) Resources(ctx context.Context) []func() resource.Res func (p *TimescaleProvider) DataSources(ctx context.Context) []func() datasource.DataSource { tflog.Trace(ctx, "TimescaleProvider.DataSources") return []func() datasource.DataSource{ - NewProductsDataSource, - NewServiceDataSource, - NewVpcsDataSource, + d_products.NewProductsDataSource, + d_service.NewServiceDataSource, + d_vpc.NewVpcsDataSource, } } diff --git a/internal/provider/service_resource.go b/internal/provider/resource/service/service_resource.go similarity index 60% rename from internal/provider/service_resource.go rename to internal/provider/resource/service/service_resource.go index d01a9c2..47d667a 100644 --- a/internal/provider/service_resource.go +++ b/internal/provider/resource/service/service_resource.go @@ -1,4 +1,4 @@ -package provider +package service import ( "context" @@ -7,43 +7,25 @@ import ( "time" "github.com/hashicorp/terraform-plugin-framework-timeouts/resource/timeouts" - "github.com/hashicorp/terraform-plugin-framework-validators/int64validator" - "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/booldefault" - "github.com/hashicorp/terraform-plugin-framework/resource/schema/int64default" - "github.com/hashicorp/terraform-plugin-framework/resource/schema/int64planmodifier" - "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/types" "github.com/hashicorp/terraform-plugin-log/tflog" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/retry" tsClient "github.com/timescale/terraform-provider-timescale/internal/client" - multiplyvalidator "github.com/timescale/terraform-provider-timescale/internal/utils" + sc "github.com/timescale/terraform-provider-timescale/internal/schema" ) // Ensure provider defined types fully satisfy framework interfaces var _ resource.Resource = &ServiceResource{} var _ resource.ResourceWithImportState = &ServiceResource{} -const ( +var ( ErrCreateTimeout = "Error waiting for service creation" ErrUpdateService = "Error updating service" ErrInvalidAttribute = "Invalid Attribute Value" - - DefaultMilliCPU = 500 - DefaultMemoryGB = 2 - - DefaultEnableHAReplica = false -) - -var ( - memorySizes = []int64{2, 4, 8, 16, 32, 64, 128} - milliCPUSizes = []int64{500, 1000, 2000, 4000, 8000, 16000, 32000} ) func NewServiceResource() resource.Resource { @@ -55,23 +37,6 @@ type ServiceResource struct { client *tsClient.Client } -// serviceResourceModel maps the resource schema data. -type serviceResourceModel struct { - ID types.String `tfsdk:"id"` - Name types.String `tfsdk:"name"` - Timeouts timeouts.Value `tfsdk:"timeouts"` - MilliCPU types.Int64 `tfsdk:"milli_cpu"` - StorageGB types.Int64 `tfsdk:"storage_gb"` - MemoryGB types.Int64 `tfsdk:"memory_gb"` - Password types.String `tfsdk:"password"` - Hostname types.String `tfsdk:"hostname"` - Port types.Int64 `tfsdk:"port"` - Username types.String `tfsdk:"username"` - RegionCode types.String `tfsdk:"region_code"` - EnableHAReplica types.Bool `tfsdk:"enable_ha_replica"` - VpcId types.Int64 `tfsdk:"vpc_id"` -} - func (r *ServiceResource) Metadata(ctx context.Context, req resource.MetadataRequest, resp *resource.MetadataResponse) { tflog.Trace(ctx, "ServiceResource.Metadata") resp.TypeName = req.ProviderTypeName + "_service" @@ -86,104 +51,7 @@ func (r *ServiceResource) Schema(ctx context.Context, req resource.SchemaRequest Please note that when updating the vpc_id attribute, it is possible to encounter a "no Endpoint for that service id exists" error. The change has been taken into account but must still be propagated. You can run "terraform refresh" shortly to get the updated data.`, - Attributes: map[string]schema.Attribute{ - "id": schema.StringAttribute{ - MarkdownDescription: "Service ID is the unique identifier for this service.", - Description: "service id", - Computed: true, - PlanModifiers: []planmodifier.String{ - stringplanmodifier.UseStateForUnknown(), - }, - }, - "name": schema.StringAttribute{ - MarkdownDescription: "Service Name is the configurable name assigned to this resource. If none is provided, a default will be generated by the provider.", - Description: "service name", - Optional: true, - // If the name attribute is absent, the provider will generate a default. - Computed: true, - }, - "milli_cpu": schema.Int64Attribute{ - MarkdownDescription: "Milli CPU", - Description: "Milli CPU", - Optional: true, - Computed: true, - Default: int64default.StaticInt64(DefaultMilliCPU), - Validators: []validator.Int64{ - int64validator.OneOf(milliCPUSizes...), - multiplyvalidator.EqualToMultipleOf(250, path.Expressions{ - path.MatchRoot("memory_gb"), - }...), - }, - }, - "enable_ha_replica": schema.BoolAttribute{ - MarkdownDescription: "Enable HA Replica", - Description: "Enable HA Replica", - Optional: true, - Computed: true, - Default: booldefault.StaticBool(DefaultEnableHAReplica), - }, - "storage_gb": schema.Int64Attribute{ - MarkdownDescription: "Deprecated: Storage GB", - Description: "Deprecated: Storage GB", - Optional: true, - DeprecationMessage: "This field is ignored. With the new usage-based storage Timescale automatically allocates the disk space needed by your service and you only pay for the disk space you use.", - }, - "memory_gb": schema.Int64Attribute{ - MarkdownDescription: "Memory GB", - Description: "Memory GB", - Optional: true, - Computed: true, - Default: int64default.StaticInt64(DefaultMemoryGB), - Validators: []validator.Int64{int64validator.OneOf(memorySizes...)}, - }, - "timeouts": timeouts.Attributes(ctx, timeouts.Opts{ - Create: true, - }), - "password": schema.StringAttribute{ - Description: "The Postgres password for this service. The password is provided once during service creation", - MarkdownDescription: "The Postgres password for this service. The password is provided once during service creation", - Computed: true, - Sensitive: true, - PlanModifiers: []planmodifier.String{ - stringplanmodifier.UseStateForUnknown(), - }, - }, - "hostname": schema.StringAttribute{ - Description: "The hostname for this service", - MarkdownDescription: "The hostname for this service", - Computed: true, - }, - "port": schema.Int64Attribute{ - Description: "The port for this service", - MarkdownDescription: "The port for this service", - Computed: true, - }, - "username": schema.StringAttribute{ - Description: "The Postgres user for this service", - MarkdownDescription: "The Postgres user for this service", - Computed: true, - PlanModifiers: []planmodifier.String{ - stringplanmodifier.UseStateForUnknown(), - }, - }, - "region_code": schema.StringAttribute{ - Description: `The region for this service`, - MarkdownDescription: "The region for this service.", - Optional: true, - Computed: true, - PlanModifiers: []planmodifier.String{ - stringplanmodifier.UseStateForUnknown(), - }, - }, - "vpc_id": schema.Int64Attribute{ - Description: `The VpcID this service is tied to.`, - MarkdownDescription: `The VpcID this service is tied to.`, - Optional: true, - PlanModifiers: []planmodifier.Int64{ - int64planmodifier.UseStateForUnknown(), - }, - }, - }, + Attributes: sc.Service(), } } @@ -210,7 +78,7 @@ func (r *ServiceResource) Configure(ctx context.Context, req resource.ConfigureR func (r *ServiceResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) { tflog.Trace(ctx, "ServiceResource.Create") - var plan serviceResourceModel + var plan sc.ServiceModel // Read Terraform plan data into the model resp.Diagnostics.Append(req.Plan.Get(ctx, &plan)...) @@ -252,7 +120,7 @@ func (r *ServiceResource) Create(ctx context.Context, req resource.CreateRequest } return } - resourceModel := serviceToResource(resp.Diagnostics, service, plan) + resourceModel := sc.NewService(ctx, service, &resp.Diagnostics, &plan) resp.Diagnostics.Append(resp.State.Set(ctx, resourceModel)...) if resp.Diagnostics.HasError() { tflog.Error(ctx, fmt.Sprintf("error updating terraform state %v", resp.Diagnostics.Errors())) @@ -298,7 +166,7 @@ func (r *ServiceResource) waitForServiceReadiness(ctx context.Context, ID string func (r *ServiceResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) { tflog.Trace(ctx, "ServiceResource.Read") - var state serviceResourceModel + var state sc.ServiceModel // Read Terraform prior state plan into the model resp.Diagnostics.Append(req.State.Get(ctx, &state)...) @@ -313,7 +181,7 @@ func (r *ServiceResource) Read(ctx context.Context, req resource.ReadRequest, re resp.Diagnostics.AddError("Client Error", fmt.Sprintf("Unable to read service, got error: %s", err)) return } - resourceModel := serviceToResource(resp.Diagnostics, service, state) + resourceModel := sc.NewService(ctx, service, &resp.Diagnostics, &state) // Save updated plan into Terraform state resp.Diagnostics.Append(resp.State.Set(ctx, resourceModel)...) if resp.Diagnostics.HasError() { @@ -324,7 +192,7 @@ func (r *ServiceResource) Read(ctx context.Context, req resource.ReadRequest, re func (r *ServiceResource) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) { tflog.Trace(ctx, "ServiceResource.Update") - var plan, state serviceResourceModel + var plan, state sc.ServiceModel // Read Terraform plan data into the model resp.Diagnostics.Append(req.Plan.Get(ctx, &plan)...) resp.Diagnostics.Append(req.State.Get(ctx, &state)...) @@ -420,7 +288,7 @@ func (r *ServiceResource) Update(ctx context.Context, req resource.UpdateRequest resp.Diagnostics.AddError(ErrCreateTimeout, fmt.Sprintf("error occured while waiting for service reconfiguration, got error: %s", err)) return } - resources := serviceToResource(resp.Diagnostics, service, plan) + resources := sc.NewService(ctx, service, &resp.Diagnostics, &plan) resp.Diagnostics.Append(resp.State.Set(ctx, resources)...) if resp.Diagnostics.HasError() { @@ -432,7 +300,7 @@ func (r *ServiceResource) Update(ctx context.Context, req resource.UpdateRequest func (r *ServiceResource) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) { tflog.Trace(ctx, "ServiceResource.Delete") - var data serviceResourceModel + var data sc.ServiceModel // Read Terraform prior state data into the model resp.Diagnostics.Append(req.State.Get(ctx, &data)...) @@ -456,30 +324,3 @@ func (r *ServiceResource) Delete(ctx context.Context, req resource.DeleteRequest func (r *ServiceResource) ImportState(ctx context.Context, req resource.ImportStateRequest, resp *resource.ImportStateResponse) { resource.ImportStatePassthroughID(ctx, path.Root("id"), req, resp) } - -func serviceToResource(diag diag.Diagnostics, s *tsClient.Service, state serviceResourceModel) serviceResourceModel { - model := serviceResourceModel{ - ID: types.StringValue(s.ID), - Password: state.Password, - Name: types.StringValue(s.Name), - MilliCPU: types.Int64Value(s.Resources[0].Spec.MilliCPU), - MemoryGB: types.Int64Value(s.Resources[0].Spec.MemoryGB), - Hostname: types.StringValue(s.ServiceSpec.Hostname), - Username: types.StringValue(s.ServiceSpec.Username), - Port: types.Int64Value(s.ServiceSpec.Port), - RegionCode: types.StringValue(s.RegionCode), - Timeouts: state.Timeouts, - EnableHAReplica: types.BoolValue(s.ReplicaStatus != ""), - } - if s.VpcEndpoint != nil { - if vpcId, err := strconv.ParseInt(s.VpcEndpoint.VpcId, 10, 64); err != nil { - diag.AddError("Parse Error", "could not parse vpcID") - } else { - model.VpcId = types.Int64Value(vpcId) - } - model.Hostname = types.StringValue(s.VpcEndpoint.Host) - model.Port = types.Int64Value(s.VpcEndpoint.Port) - } - - return model -} diff --git a/internal/provider/service_resource_test.go b/internal/provider/resource/service/service_resource_test.go similarity index 85% rename from internal/provider/service_resource_test.go rename to internal/provider/resource/service/service_resource_test.go index 3bd65e0..46fc78a 100644 --- a/internal/provider/service_resource_test.go +++ b/internal/provider/resource/service/service_resource_test.go @@ -1,4 +1,4 @@ -package provider +package service_test import ( "errors" @@ -8,15 +8,31 @@ import ( "github.com/hashicorp/terraform-plugin-testing/helper/resource" "github.com/hashicorp/terraform-plugin-testing/terraform" + "github.com/timescale/terraform-provider-timescale/internal/provider/resource/service" + "github.com/timescale/terraform-provider-timescale/internal/test" ) const DEFAULT_VPC_ID = 2074 // Default vpc id for test acc +type Config struct { + Name string + Timeouts Timeouts + MilliCPU int64 + MemoryGB int64 + RegionCode string + EnableHAReplica bool + VpcID int64 +} + +type Timeouts struct { + Create string +} + func TestServiceResource_Default_Success(t *testing.T) { // Test resource creation succeeds and update is not allowed resource.ParallelTest(t, resource.TestCase{ - ProtoV6ProviderFactories: testAccProtoV6ProviderFactories, - PreCheck: func() { testAccPreCheck(t) }, + ProtoV6ProviderFactories: test.TestAccProtoV6ProviderFactories, + PreCheck: func() { test.TestAccPreCheck(t) }, Steps: []resource.TestStep{ // Create default and Read testing { @@ -91,8 +107,8 @@ func TestServiceResource_Default_Success(t *testing.T) { func TestServiceResource_Timeout(t *testing.T) { resource.Test(t, resource.TestCase{ - ProtoV6ProviderFactories: testAccProtoV6ProviderFactories, - PreCheck: func() { testAccPreCheck(t) }, + ProtoV6ProviderFactories: test.TestAccProtoV6ProviderFactories, + PreCheck: func() { test.TestAccPreCheck(t) }, Steps: []resource.TestStep{ { Config: newServiceConfig(Config{ @@ -101,7 +117,7 @@ func TestServiceResource_Timeout(t *testing.T) { Create: "1s", }, }), - ExpectError: regexp.MustCompile(ErrCreateTimeout), + ExpectError: regexp.MustCompile(service.ErrCreateTimeout), }, }, }) @@ -110,8 +126,8 @@ func TestServiceResource_Timeout(t *testing.T) { func TestServiceResource_CustomConf(t *testing.T) { // Test resource creation succeeds and update is not allowed resource.ParallelTest(t, resource.TestCase{ - ProtoV6ProviderFactories: testAccProtoV6ProviderFactories, - PreCheck: func() { testAccPreCheck(t) }, + ProtoV6ProviderFactories: test.TestAccProtoV6ProviderFactories, + PreCheck: func() { test.TestAccPreCheck(t) }, Steps: []resource.TestStep{ // Invalid conf millicpu & memory invalid ratio { @@ -120,7 +136,7 @@ func TestServiceResource_CustomConf(t *testing.T) { MilliCPU: 2000, MemoryGB: 2, }), - ExpectError: regexp.MustCompile(ErrInvalidAttribute), + ExpectError: regexp.MustCompile(service.ErrInvalidAttribute), }, // Invalid conf storage invalid value { @@ -129,14 +145,14 @@ func TestServiceResource_CustomConf(t *testing.T) { MilliCPU: 500, MemoryGB: 3, }), - ExpectError: regexp.MustCompile(ErrInvalidAttribute), + ExpectError: regexp.MustCompile(service.ErrInvalidAttribute), }, // Invalid conf storage invalid region { Config: newServiceCustomConfig("invalid", Config{ RegionCode: "test-invalid-region", }), - ExpectError: regexp.MustCompile(ErrInvalidAttribute), + ExpectError: regexp.MustCompile(service.ErrInvalidAttribute), }, // Create with custom conf and region { @@ -175,8 +191,8 @@ func TestServiceResource_CustomConf(t *testing.T) { func TestServiceResource_Import(t *testing.T) { config := newServiceConfig(Config{Name: "import test"}) resource.Test(t, resource.TestCase{ - ProtoV6ProviderFactories: testAccProtoV6ProviderFactories, - PreCheck: func() { testAccPreCheck(t) }, + ProtoV6ProviderFactories: test.TestAccProtoV6ProviderFactories, + PreCheck: func() { test.TestAccPreCheck(t) }, Steps: []resource.TestStep{ // Create the service to import { @@ -210,7 +226,7 @@ func newServiceConfig(config Config) string { if config.Timeouts.Create == "" { config.Timeouts.Create = "10m" } - return providerConfig + fmt.Sprintf(` + return test.ProviderConfig + fmt.Sprintf(` resource "timescale_service" "resource" { name = %q timeouts = { @@ -223,7 +239,7 @@ func newServiceComputeResizeConfig(config Config) string { if config.Timeouts.Create == "" { config.Timeouts.Create = "10m" } - return providerConfig + fmt.Sprintf(` + return test.ProviderConfig + fmt.Sprintf(` resource "timescale_service" "resource" { name = %q milli_cpu = %d @@ -238,7 +254,7 @@ func newServiceAddVpc(config Config) string { if config.Timeouts.Create == "" { config.Timeouts.Create = "10m" } - return providerConfig + fmt.Sprintf(` + return test.ProviderConfig + fmt.Sprintf(` resource "timescale_service" "resource" { name = %q milli_cpu = %d @@ -254,7 +270,7 @@ func newServiceAddHAReplica(config Config) string { if config.Timeouts.Create == "" { config.Timeouts.Create = "10m" } - return providerConfig + fmt.Sprintf(` + return test.ProviderConfig + fmt.Sprintf(` resource "timescale_service" "resource" { name = %q milli_cpu = %d @@ -270,7 +286,7 @@ func newServiceCustomConfig(resourceName string, config Config) string { if config.Timeouts.Create == "" { config.Timeouts.Create = "30m" } - return providerConfig + fmt.Sprintf(` + return test.ProviderConfig + fmt.Sprintf(` resource "timescale_service" "%s" { name = %q timeouts = { @@ -287,7 +303,7 @@ func newServiceCustomVpcConfig(resourceName string, config Config) string { if config.Timeouts.Create == "" { config.Timeouts.Create = "30m" } - return providerConfig + fmt.Sprintf(` + return test.ProviderConfig + fmt.Sprintf(` resource "timescale_service" "%s" { name = %q timeouts = { diff --git a/internal/provider/service_data_source.go b/internal/provider/service_data_source.go deleted file mode 100644 index 0378bb3..0000000 --- a/internal/provider/service_data_source.go +++ /dev/null @@ -1,224 +0,0 @@ -package provider - -import ( - "context" - "fmt" - "strconv" - - "github.com/hashicorp/terraform-plugin-framework/datasource" - "github.com/hashicorp/terraform-plugin-framework/datasource/schema" - "github.com/hashicorp/terraform-plugin-framework/diag" - "github.com/hashicorp/terraform-plugin-framework/path" - "github.com/hashicorp/terraform-plugin-framework/types" - "github.com/hashicorp/terraform-plugin-log/tflog" - - tsClient "github.com/timescale/terraform-provider-timescale/internal/client" -) - -// Ensure provider defined types fully satisfy framework interfaces. -var _ datasource.DataSource = &ServiceDataSource{} -var _ datasource.DataSourceWithConfigure = &ServiceDataSource{} - -func NewServiceDataSource() datasource.DataSource { - return &ServiceDataSource{} -} - -// ServiceDataSource defines the data source implementation. -type ServiceDataSource struct { - client *tsClient.Client -} - -// ServiceDataSourceModel describes the data source data model. -type ServiceDataSourceModel struct { - ID types.String `tfsdk:"id"` - Name types.String `tfsdk:"name"` - RegionCode types.String `tfsdk:"region_code"` - Spec SpecModel `tfsdk:"spec"` - Resources []ResourceModel `tfsdk:"resources"` - Created types.String `tfsdk:"created"` - VpcId types.Int64 `tfsdk:"vpc_id"` -} - -type SpecModel struct { - Hostname types.String `tfsdk:"hostname"` - Username types.String `tfsdk:"username"` - Port types.Int64 `tfsdk:"port"` -} - -type ResourceModel struct { - ID types.String `tfsdk:"id"` - Spec ResourceSpecModel `tfsdk:"spec"` -} - -type ResourceSpecModel struct { - MilliCPU types.Int64 `tfsdk:"milli_cpu"` - MemoryGB types.Int64 `tfsdk:"memory_gb"` - EnableHAReplica types.Bool `tfsdk:"enable_ha_replica"` -} - -func (d *ServiceDataSource) Metadata(ctx context.Context, req datasource.MetadataRequest, resp *datasource.MetadataResponse) { - resp.TypeName = req.ProviderTypeName + "_service" -} - -func (d *ServiceDataSource) Schema(ctx context.Context, req datasource.SchemaRequest, resp *datasource.SchemaResponse) { - resp.Schema = schema.Schema{ - // This description is used by the documentation generator and the language server. - MarkdownDescription: "Service data source", - - Attributes: map[string]schema.Attribute{ - "id": schema.StringAttribute{ - MarkdownDescription: "Service ID is the unique identifier for this service", - Description: "service id", - Required: true, - }, - "name": schema.StringAttribute{ - MarkdownDescription: "Service Name is the configurable name assigned to this resource. If none is provided, a default will be generated by the provider.", - Description: "service name", - Computed: true, - }, - "region_code": schema.StringAttribute{ - MarkdownDescription: "Region Code is the physical data center where this service is located.", - Computed: true, - }, - "spec": schema.SingleNestedAttribute{ - Attributes: map[string]schema.Attribute{ - "hostname": schema.StringAttribute{ - MarkdownDescription: "Hostname is the hostname of this service.", - Description: "hostname is the hostname of this service", - Computed: true, - }, - "username": schema.StringAttribute{ - MarkdownDescription: "Username is the Postgres username.", - Description: "username is the Postgres username", - Computed: true, - }, - "port": schema.Int64Attribute{ - MarkdownDescription: "Port is the port assigned to this service.", - Description: "port is the port assigned to this service", - Computed: true, - }, - }, - Computed: true, - }, - "resources": schema.ListNestedAttribute{ - Computed: true, - NestedObject: schema.NestedAttributeObject{ - Attributes: map[string]schema.Attribute{ - "id": schema.StringAttribute{ - Computed: true, - }, - "spec": schema.SingleNestedAttribute{ - Computed: true, - Attributes: map[string]schema.Attribute{ - "milli_cpu": schema.Int64Attribute{ - MarkdownDescription: "MilliCPU is the cpu allocated for this service.", - Description: "MilliCPU is the cpu allocated for this service.", - Computed: true, - }, - "memory_gb": schema.Int64Attribute{ - MarkdownDescription: "MemoryGB is the memory allocated for this service.", - Description: "MemoryGB is the memory allocated for this service.", - Computed: true, - }, - "enable_ha_replica": schema.BoolAttribute{ - MarkdownDescription: "EnableHAReplica defines if a replica will be provisioned for this service.", - Description: "EnableHAReplica defines if a replica will be provisioned for this service.", - Computed: true, - }, - }, - }, - }, - }, - }, - "created": schema.StringAttribute{ - MarkdownDescription: "Created is the time this service was created.", - Description: "Created is the time this service was created.", - Computed: true, - }, - "vpc_id": schema.Int64Attribute{ - MarkdownDescription: "VPC ID this service is linked to.", - Description: "VPC ID this service is linked to.", - Optional: true, - Computed: true, - }, - }, - } -} - -func (d *ServiceDataSource) Configure(ctx context.Context, req datasource.ConfigureRequest, resp *datasource.ConfigureResponse) { - tflog.Trace(ctx, "ServiceDataSource.Configure") - - if req.ProviderData == nil { - return - } - client, ok := req.ProviderData.(*tsClient.Client) - - if !ok { - resp.Diagnostics.AddError( - "Unexpected Client Type", - fmt.Sprintf("Expected *tsClient.Client, got: %T. Please report this issue to the provider developers.", req.ProviderData), - ) - return - } - - d.client = client -} - -func (d *ServiceDataSource) Read(ctx context.Context, req datasource.ReadRequest, resp *datasource.ReadResponse) { - tflog.Trace(ctx, "ServiceDataSource.Read") - - var id string - resp.Diagnostics.Append(req.Config.GetAttribute(ctx, path.Root("id"), &id)...) - - if resp.Diagnostics.HasError() { - tflog.Error(ctx, fmt.Sprintf("error reading terraform plan %v", resp.Diagnostics.Errors())) - return - } - - tflog.Info(ctx, "Getting Service: "+id) - service, err := d.client.GetService(ctx, id) - if err != nil { - resp.Diagnostics.AddError("Client Error", fmt.Sprintf("Unable to read service, got error: %s", err)) - return - } - state := serviceToDataModel(resp.Diagnostics, service) - resp.Diagnostics.Append(resp.State.Set(ctx, state)...) - if resp.Diagnostics.HasError() { - tflog.Error(ctx, fmt.Sprintf("error updating terraform state %v", resp.Diagnostics.Errors())) - return - } -} - -func serviceToDataModel(diag diag.Diagnostics, s *tsClient.Service) ServiceDataSourceModel { - serviceModel := ServiceDataSourceModel{ - ID: types.StringValue(s.ID), - Name: types.StringValue(s.Name), - RegionCode: types.StringValue(s.RegionCode), - Spec: SpecModel{ - Hostname: types.StringValue(s.ServiceSpec.Hostname), - Username: types.StringValue(s.ServiceSpec.Username), - Port: types.Int64Value(s.ServiceSpec.Port), - }, - Created: types.StringValue(s.Created), - } - if s.VpcEndpoint != nil { - if vpcId, err := strconv.ParseInt(s.VpcEndpoint.VpcId, 10, 64); err != nil { - diag.AddError("Parse Error", "could not parse vpcID") - } else { - serviceModel.VpcId = types.Int64Value(vpcId) - } - serviceModel.Spec.Hostname = types.StringValue(s.VpcEndpoint.Host) - serviceModel.Spec.Port = types.Int64Value(s.VpcEndpoint.Port) - } - for _, resource := range s.Resources { - serviceModel.Resources = append(serviceModel.Resources, ResourceModel{ - ID: types.StringValue(resource.ID), - Spec: ResourceSpecModel{ - MilliCPU: types.Int64Value(resource.Spec.MilliCPU), - MemoryGB: types.Int64Value(resource.Spec.MemoryGB), - EnableHAReplica: types.BoolValue(s.ReplicaStatus != ""), - }, - }) - } - return serviceModel -} diff --git a/internal/schema/service.go b/internal/schema/service.go new file mode 100644 index 0000000..decddbb --- /dev/null +++ b/internal/schema/service.go @@ -0,0 +1,212 @@ +package schema + +import ( + "context" + "strconv" + + "github.com/hashicorp/terraform-plugin-framework-timeouts/resource/timeouts" + "github.com/hashicorp/terraform-plugin-framework-validators/int64validator" + "github.com/hashicorp/terraform-plugin-framework/attr" + "github.com/hashicorp/terraform-plugin-framework/diag" + "github.com/hashicorp/terraform-plugin-framework/path" + "github.com/hashicorp/terraform-plugin-framework/resource/schema" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/booldefault" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/int64default" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/int64planmodifier" + "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-framework/types" + "github.com/timescale/terraform-provider-timescale/internal/client" + multiplyvalidator "github.com/timescale/terraform-provider-timescale/internal/utils" +) + +const ( + DefaultMilliCPU = 500 + DefaultMemoryGB = 2 + + DefaultEnableHAReplica = false +) + +var ( + memorySizes = []int64{2, 4, 8, 16, 32, 64, 128} + milliCPUSizes = []int64{500, 1000, 2000, 4000, 8000, 16000, 32000} + + ServiceAttrType = types.ObjectType{ + AttrTypes: map[string]attr.Type{ + "id": types.StringType, + "name": types.StringType, + "milli_cpu": types.Int64Type, + "storage_gb": types.Int64Type, + "memory_gb": types.Int64Type, + "password": types.StringType, + "hostname": types.StringType, + "port": types.Int64Type, + "username": types.StringType, + "region_code": types.StringType, + "enable_ha_replica": types.BoolType, + "vpc_id": types.Int64Type, + "created": types.StringType, + }, + } +) + +type ServiceModel struct { + ID types.String `tfsdk:"id"` + Name types.String `tfsdk:"name"` + Timeouts timeouts.Value `tfsdk:"timeouts"` + MilliCPU types.Int64 `tfsdk:"milli_cpu"` + StorageGB types.Int64 `tfsdk:"storage_gb"` + MemoryGB types.Int64 `tfsdk:"memory_gb"` + Password types.String `tfsdk:"password"` + Hostname types.String `tfsdk:"hostname"` + Port types.Int64 `tfsdk:"port"` + Username types.String `tfsdk:"username"` + RegionCode types.String `tfsdk:"region_code"` + EnableHAReplica types.Bool `tfsdk:"enable_ha_replica"` + VpcId types.Int64 `tfsdk:"vpc_id"` + Created types.String `tfsdk:"created"` +} + +func Service() map[string]schema.Attribute { + return map[string]schema.Attribute{ + "id": schema.StringAttribute{ + MarkdownDescription: "Service ID is the unique identifier for this service.", + Description: "service id", + Computed: true, + PlanModifiers: []planmodifier.String{ + stringplanmodifier.UseStateForUnknown(), + }, + }, + "name": schema.StringAttribute{ + MarkdownDescription: "Service Name is the configurable name assigned to this resource. If none is provided, a default will be generated by the provider.", + Description: "service name", + Optional: true, + // If the name attribute is absent, the provider will generate a default. + Computed: true, + }, + "timeouts": timeouts.Attributes(context.Background(), timeouts.Opts{ + Create: true, + }), + "milli_cpu": schema.Int64Attribute{ + MarkdownDescription: "Milli CPU", + Description: "Milli CPU", + Optional: true, + Computed: true, + Default: int64default.StaticInt64(DefaultMilliCPU), + Validators: []validator.Int64{ + int64validator.OneOf(milliCPUSizes...), + multiplyvalidator.EqualToMultipleOf(250, path.Expressions{ + path.MatchRoot("memory_gb"), + }...), + }, + }, + "storage_gb": schema.Int64Attribute{ + MarkdownDescription: "Deprecated: Storage GB", + Description: "Deprecated: Storage GB", + Optional: true, + DeprecationMessage: "This field is ignored. With the new usage-based storage Timescale automatically allocates the disk space needed by your service and you only pay for the disk space you use.", + }, + "memory_gb": schema.Int64Attribute{ + MarkdownDescription: "Memory GB", + Description: "Memory GB", + Optional: true, + Computed: true, + Default: int64default.StaticInt64(DefaultMemoryGB), + Validators: []validator.Int64{int64validator.OneOf(memorySizes...)}, + }, + "password": schema.StringAttribute{ + Description: "The Postgres password for this service. The password is provided once during service creation", + MarkdownDescription: "The Postgres password for this service. The password is provided once during service creation", + Computed: true, + Sensitive: true, + PlanModifiers: []planmodifier.String{ + stringplanmodifier.UseStateForUnknown(), + }, + }, + "hostname": schema.StringAttribute{ + Description: "The hostname for this service", + MarkdownDescription: "The hostname for this service", + Computed: true, + }, + "port": schema.Int64Attribute{ + Description: "The port for this service", + MarkdownDescription: "The port for this service", + Computed: true, + }, + "username": schema.StringAttribute{ + Description: "The Postgres user for this service", + MarkdownDescription: "The Postgres user for this service", + Computed: true, + PlanModifiers: []planmodifier.String{ + stringplanmodifier.UseStateForUnknown(), + }, + }, + "region_code": schema.StringAttribute{ + Description: `The region for this service`, + MarkdownDescription: "The region for this service.", + Optional: true, + Computed: true, + PlanModifiers: []planmodifier.String{ + stringplanmodifier.UseStateForUnknown(), + }, + }, + "enable_ha_replica": schema.BoolAttribute{ + MarkdownDescription: "Enable HA Replica", + Description: "Enable HA Replica", + Optional: true, + Computed: true, + Default: booldefault.StaticBool(DefaultEnableHAReplica), + }, + "vpc_id": schema.Int64Attribute{ + Description: `The VpcID this service is tied to.`, + MarkdownDescription: `The VpcID this service is tied to.`, + Optional: true, + PlanModifiers: []planmodifier.Int64{ + int64planmodifier.UseStateForUnknown(), + }, + }, + "created": schema.StringAttribute{ + MarkdownDescription: "Created is the time this service was created.", + Description: "Created is the time this service was created.", + Computed: true, + }, + } +} + +func NewService(ctx context.Context, s *client.Service, diags *diag.Diagnostics, state *ServiceModel) attr.Value { + res := ServiceModel{ + ID: types.StringValue(s.ID), + Name: types.StringValue(s.Name), + RegionCode: types.StringValue(s.RegionCode), + Hostname: types.StringValue(s.ServiceSpec.Hostname), + Username: types.StringValue(s.ServiceSpec.Username), + Port: types.Int64Value(s.ServiceSpec.Port), + Created: types.StringValue(s.Created), + MilliCPU: types.Int64Value(s.Resources[0].Spec.MilliCPU), + MemoryGB: types.Int64Value(s.Resources[0].Spec.MemoryGB), + EnableHAReplica: types.BoolValue(s.ReplicaStatus != ""), + } + if state != nil { + res.Password = state.Password + res.Timeouts = state.Timeouts + } + if s.VpcEndpoint != nil { + if vpcId, err := strconv.ParseInt(s.VpcEndpoint.VpcId, 10, 64); err != nil { + diags.AddError("Parse Error", "could not parse vpcID") + } else { + res.VpcId = types.Int64Value(vpcId) + } + res.Hostname = types.StringValue(s.VpcEndpoint.Host) + res.Port = types.Int64Value(s.VpcEndpoint.Port) + } + var value attr.Value + diags.Append(tfsdk.ValueFrom( + ctx, + res, + ServiceAttrType, + &value, + )...) + return value +} diff --git a/internal/schema/utils.go b/internal/schema/utils.go new file mode 100644 index 0000000..d84800b --- /dev/null +++ b/internal/schema/utils.go @@ -0,0 +1,119 @@ +package schema + +import ( + "fmt" + + ds_schema "github.com/hashicorp/terraform-plugin-framework/datasource/schema" + r_schema "github.com/hashicorp/terraform-plugin-framework/resource/schema" +) + +func Converter(rSchema map[string]r_schema.Attribute) map[string]ds_schema.Attribute { + dSchema := make(map[string]ds_schema.Attribute) + for name, fromAttr := range rSchema { + + // required := fromAttr.IsRequired() + // computed := fromAttr.IsComputed() + // optional := fromAttr.IsOptional() + + // for a datasource, all attributes are computed and the required attrs + // are on the container / outside. + required := false + computed := true + optional := false + + if !(required && computed && optional) { + computed = true + } + + switch fromAttrType := fromAttr.(type) { + case r_schema.StringAttribute: + dSchema[name] = ds_schema.StringAttribute{ + Validators: fromAttrType.Validators, + Description: fromAttrType.Description, + MarkdownDescription: fromAttrType.MarkdownDescription, + CustomType: fromAttrType.CustomType, + Sensitive: fromAttrType.Sensitive, + Optional: optional, + Computed: computed, + Required: required, + } + case r_schema.BoolAttribute: + dSchema[name] = ds_schema.BoolAttribute{ + Validators: fromAttrType.Validators, + Description: fromAttrType.Description, + MarkdownDescription: fromAttrType.MarkdownDescription, + CustomType: fromAttrType.CustomType, + Sensitive: fromAttrType.Sensitive, + Optional: optional, + Computed: computed, + Required: required, + } + case r_schema.Int64Attribute: + dSchema[name] = ds_schema.Int64Attribute{ + Validators: fromAttrType.Validators, + Description: fromAttrType.Description, + MarkdownDescription: fromAttrType.MarkdownDescription, + CustomType: fromAttrType.CustomType, + Sensitive: fromAttrType.Sensitive, + Optional: optional, + Computed: computed, + Required: required, + } + case r_schema.ListAttribute: + dSchema[name] = ds_schema.ListAttribute{ + Validators: fromAttrType.Validators, + Description: fromAttrType.Description, + MarkdownDescription: fromAttrType.MarkdownDescription, + CustomType: fromAttrType.CustomType, + Sensitive: fromAttrType.Sensitive, + ElementType: fromAttrType.ElementType, + Optional: optional, + Computed: computed, + Required: required, + } + case r_schema.ListNestedAttribute: + dSchema[name] = ds_schema.ListNestedAttribute{ + Validators: fromAttrType.Validators, + Description: fromAttrType.Description, + MarkdownDescription: fromAttrType.MarkdownDescription, + Sensitive: fromAttrType.Sensitive, + NestedObject: ds_schema.NestedAttributeObject{ + Attributes: Converter(fromAttrType.NestedObject.Attributes), + }, + Optional: optional, + Computed: computed, + Required: required, + } + case r_schema.SetNestedAttribute: + dSchema[name] = ds_schema.SetNestedAttribute{ + Validators: fromAttrType.Validators, + Description: fromAttrType.Description, + MarkdownDescription: fromAttrType.MarkdownDescription, + CustomType: fromAttrType.CustomType, + Sensitive: fromAttrType.Sensitive, + NestedObject: ds_schema.NestedAttributeObject{ + Attributes: Converter(fromAttrType.NestedObject.Attributes), + }, + Optional: optional, + Computed: computed, + Required: required, + } + case r_schema.SetAttribute: + dSchema[name] = ds_schema.SetAttribute{ + Validators: fromAttrType.Validators, + Description: fromAttrType.Description, + MarkdownDescription: fromAttrType.MarkdownDescription, + CustomType: fromAttrType.CustomType, + Sensitive: fromAttrType.Sensitive, + ElementType: fromAttrType.ElementType, + Optional: optional, + Computed: computed, + Required: required, + } + default: + msg := fmt.Sprintf("unknown attribute type: %v", fromAttr.GetType().String()) + panic(msg) + } + } + return dSchema +} diff --git a/internal/provider/provider_test.go b/internal/test/config.go similarity index 68% rename from internal/provider/provider_test.go rename to internal/test/config.go index eff5a1c..10ffd25 100644 --- a/internal/provider/provider_test.go +++ b/internal/test/config.go @@ -1,4 +1,4 @@ -package provider +package test import ( "os" @@ -6,12 +6,14 @@ import ( "github.com/hashicorp/terraform-plugin-framework/providerserver" "github.com/hashicorp/terraform-plugin-go/tfprotov6" + + "github.com/timescale/terraform-provider-timescale/internal/provider" ) const ( - // providerConfig is a shared configuration to combine with the actual + // ProviderConfig is a shared configuration to combine with the actual // test configuration so the Timescale client is properly configured. - providerConfig = ` + ProviderConfig = ` variable "ts_access_key" { type = string } @@ -32,29 +34,7 @@ provider "timescale" { ` ) -// testAccProtoV6ProviderFactories are used to instantiate a provider during -// acceptance testing. The factory function will be invoked for every Terraform -// CLI command executed to create a provider server to which the CLI can -// reattach. -var testAccProtoV6ProviderFactories = map[string]func() (tfprotov6.ProviderServer, error){ - "timescale": providerserver.NewProtocol6WithError(New("test")()), -} - -type Config struct { - Name string - Timeouts Timeouts - MilliCPU int64 - MemoryGB int64 - RegionCode string - EnableHAReplica bool - VpcID int64 -} - -type Timeouts struct { - Create string -} - -func testAccPreCheck(t *testing.T) { +func TestAccPreCheck(t *testing.T) { _, ok := os.LookupEnv("TF_VAR_ts_access_key") if !ok { t.Fatal("environment variable TF_VAR_ts_access_key not set") @@ -72,3 +52,11 @@ func testAccPreCheck(t *testing.T) { t.Fatal("environment variable TIMESCALE_DEV_URL not set") } } + +// TestAccProtoV6ProviderFactories are used to instantiate a provider during +// acceptance testing. The factory function will be invoked for every Terraform +// CLI command executed to create a provider server to which the CLI can +// reattach. +var TestAccProtoV6ProviderFactories = map[string]func() (tfprotov6.ProviderServer, error){ + "timescale": providerserver.NewProtocol6WithError(provider.New("test")()), +}