From af494f8f98cd976cfcf35eeda11dae1bee19bab1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fa=CC=81bio=20Matavelli?= Date: Tue, 16 Apr 2024 22:44:20 +0100 Subject: [PATCH 1/7] feat: add provider v6 --- cloudstack/provider.go | 4 +- cloudstack/provider_test.go | 58 +++++++++++--- cloudstack/provider_v6.go | 155 ++++++++++++++++++++++++++++++++++++ go.mod | 8 +- go.sum | 16 ++-- main.go | 32 +++++--- 6 files changed, 243 insertions(+), 30 deletions(-) create mode 100644 cloudstack/provider_v6.go diff --git a/cloudstack/provider.go b/cloudstack/provider.go index 6d7b7b53..5aad7c55 100644 --- a/cloudstack/provider.go +++ b/cloudstack/provider.go @@ -26,7 +26,7 @@ import ( "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" ) -func New() *schema.Provider { +func Provider() *schema.Provider { return &schema.Provider{ Schema: map[string]*schema.Schema{ "api_url": { @@ -41,6 +41,7 @@ func New() *schema.Provider { Optional: true, DefaultFunc: schema.EnvDefaultFunc("CLOUDSTACK_API_KEY", nil), ConflictsWith: []string{"config", "profile"}, + Sensitive: true, }, "secret_key": { @@ -48,6 +49,7 @@ func New() *schema.Provider { Optional: true, DefaultFunc: schema.EnvDefaultFunc("CLOUDSTACK_SECRET_KEY", nil), ConflictsWith: []string{"config", "profile"}, + Sensitive: true, }, "config": { diff --git a/cloudstack/provider_test.go b/cloudstack/provider_test.go index 0a3acf02..927512aa 100644 --- a/cloudstack/provider_test.go +++ b/cloudstack/provider_test.go @@ -22,10 +22,13 @@ package cloudstack import ( "context" "os" + "regexp" "testing" - "github.com/hashicorp/terraform-plugin-go/tfprotov5" - "github.com/hashicorp/terraform-plugin-mux/tf5muxserver" + "github.com/hashicorp/terraform-plugin-framework/providerserver" + "github.com/hashicorp/terraform-plugin-go/tfprotov6" + "github.com/hashicorp/terraform-plugin-mux/tf5to6server" + "github.com/hashicorp/terraform-plugin-mux/tf6muxserver" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" "github.com/hashicorp/terraform-plugin-testing/helper/resource" ) @@ -36,32 +39,45 @@ var testAccProvider *schema.Provider var cloudStackTemplateURL = os.Getenv("CLOUDSTACK_TEMPLATE_URL") func init() { - testAccProvider = New() + testAccProvider = Provider() testAccProviders = map[string]*schema.Provider{ "cloudstack": testAccProvider, } } func TestProvider(t *testing.T) { - if err := New().InternalValidate(); err != nil { + if err := Provider().InternalValidate(); err != nil { t.Fatalf("err: %s", err) } } func TestProvider_impl(t *testing.T) { - var _ *schema.Provider = New() + var _ *schema.Provider = Provider() } func TestMuxServer(t *testing.T) { resource.Test(t, resource.TestCase{ - ProtoV5ProviderFactories: map[string]func() (tfprotov5.ProviderServer, error){ - "cloudstack": func() (tfprotov5.ProviderServer, error) { + ProtoV6ProviderFactories: map[string]func() (tfprotov6.ProviderServer, error){ + "cloudstack": func() (tfprotov6.ProviderServer, error) { ctx := context.Background() - providers := []func() tfprotov5.ProviderServer{ - New().GRPCProvider, + + upgradedSdkServer, err := tf5to6server.UpgradeServer( + ctx, + Provider().GRPCProvider, + ) + + if err != nil { + return nil, err } - muxServer, err := tf5muxserver.NewMuxServer(ctx, providers...) + providers := []func() tfprotov6.ProviderServer{ + providerserver.NewProtocol6(New()), + func() tfprotov6.ProviderServer { + return upgradedSdkServer + }, + } + + muxServer, err := tf6muxserver.NewMuxServer(ctx, providers...) if err != nil { return nil, err @@ -71,6 +87,10 @@ func TestMuxServer(t *testing.T) { }, }, Steps: []resource.TestStep{ + { + Config: testMuxServerConfig_conflict, + ExpectError: regexp.MustCompile("Invalid Attribute Combination"), + }, { Config: testMuxServerConfig_basic, }, @@ -80,7 +100,7 @@ func TestMuxServer(t *testing.T) { const testMuxServerConfig_basic = ` resource "cloudstack_zone" "zone_resource"{ - name = "TestZone" + name = "TestZone1" dns1 = "8.8.8.8" internal_dns1 = "172.20.0.1" network_type = "Advanced" @@ -94,6 +114,22 @@ resource "cloudstack_zone" "zone_resource"{ } ` +const testMuxServerConfig_conflict = ` +provider "cloudstack" { + api_url = "http://localhost:8080/client/api" + api_key = "xxxxx" + secret_key = "xxxxx" + config = "cloudstack.ini" +} + +data "cloudstack_zone" "zone_data_source"{ + filter{ + name = "name" + value = "test" + } +} + ` + func testAccPreCheck(t *testing.T) { if v := os.Getenv("CLOUDSTACK_API_URL"); v == "" { t.Fatal("CLOUDSTACK_API_URL must be set for acceptance tests") diff --git a/cloudstack/provider_v6.go b/cloudstack/provider_v6.go new file mode 100644 index 00000000..339e4b37 --- /dev/null +++ b/cloudstack/provider_v6.go @@ -0,0 +1,155 @@ +package cloudstack + +import ( + "context" + "fmt" + "os" + "strconv" + + "github.com/hashicorp/terraform-plugin-framework-validators/providervalidator" + "github.com/hashicorp/terraform-plugin-framework/datasource" + "github.com/hashicorp/terraform-plugin-framework/path" + "github.com/hashicorp/terraform-plugin-framework/provider" + "github.com/hashicorp/terraform-plugin-framework/provider/schema" + "github.com/hashicorp/terraform-plugin-framework/resource" + "github.com/hashicorp/terraform-plugin-framework/types" +) + +type CloudstackProvider struct{} + +type CloudstackProviderModel struct { + ApiUrl types.String `tfsdk:"api_url"` + ApiKey types.String `tfsdk:"api_key"` + SecretKey types.String `tfsdk:"secret_key"` + Config types.String `tfsdk:"config"` + Profile types.String `tfsdk:"profile"` + HttpGetOnly types.Bool `tfsdk:"http_get_only"` + Timeout types.Int64 `tfsdk:"timeout"` +} + +var _ provider.Provider = (*CloudstackProvider)(nil) + +func New() provider.Provider { + return &CloudstackProvider{} +} + +func (p *CloudstackProvider) Metadata(ctx context.Context, req provider.MetadataRequest, resp *provider.MetadataResponse) { + resp.TypeName = "cloudstack" +} + +func (p *CloudstackProvider) Schema(ctx context.Context, req provider.SchemaRequest, resp *provider.SchemaResponse) { + resp.Schema = schema.Schema{ + Attributes: map[string]schema.Attribute{ + "api_url": schema.StringAttribute{ + Optional: true, + }, + "api_key": schema.StringAttribute{ + Optional: true, + Sensitive: true, + }, + "secret_key": schema.StringAttribute{ + Optional: true, + Sensitive: true, + }, + "config": schema.StringAttribute{ + Optional: true, + }, + "profile": schema.StringAttribute{ + Optional: true, + }, + "http_get_only": schema.BoolAttribute{ + Optional: true, + }, + "timeout": schema.Int64Attribute{ + Optional: true, + }, + }, + } +} + +func (p *CloudstackProvider) Configure(ctx context.Context, req provider.ConfigureRequest, resp *provider.ConfigureResponse) { + apiUrl := os.Getenv("CLOUDSTACK_API_URL") + apiKey := os.Getenv("CLOUDSTACK_API_KEY") + secretKey := os.Getenv("CLOUDSTACK_SECRET_KEY") + httpGetOnly, _ := strconv.ParseBool(os.Getenv("CLOUDSTACK_HTTP_GET_ONLY")) + timeout, _ := strconv.ParseInt(os.Getenv("CLOUDSTACK_TIMEOUT"), 2, 64) + + var data CloudstackProviderModel + + resp.Diagnostics.Append(req.Config.Get(ctx, &data)...) + + if data.ApiUrl.ValueString() != "" { + apiUrl = data.ApiUrl.ValueString() + } + + if data.ApiKey.ValueString() != "" { + apiKey = data.ApiKey.ValueString() + } + + if data.SecretKey.ValueString() != "" { + secretKey = data.SecretKey.ValueString() + } + + if data.HttpGetOnly.ValueBool() { + httpGetOnly = true + } + + if data.Timeout.ValueInt64() != 0 { + timeout = data.Timeout.ValueInt64() + } + + cfg := Config{ + APIURL: apiUrl, + APIKey: apiKey, + SecretKey: secretKey, + HTTPGETOnly: httpGetOnly, + Timeout: timeout, + } + + client, err := cfg.NewClient() + + if err != nil { + resp.Diagnostics.AddError("cloudstack", fmt.Sprintf("failed to create client: %T", err)) + return + } + + resp.ResourceData = client + resp.DataSourceData = client +} + +func (p *CloudstackProvider) ConfigValidators(ctx context.Context) []provider.ConfigValidator { + return []provider.ConfigValidator{ + providervalidator.Conflicting( + path.MatchRoot("api_url"), + path.MatchRoot("config"), + ), + providervalidator.Conflicting( + path.MatchRoot("api_url"), + path.MatchRoot("profile"), + ), + providervalidator.Conflicting( + path.MatchRoot("api_key"), + path.MatchRoot("config"), + ), + providervalidator.Conflicting( + path.MatchRoot("api_key"), + path.MatchRoot("profile"), + ), + providervalidator.Conflicting( + path.MatchRoot("secret_key"), + path.MatchRoot("config"), + ), + providervalidator.Conflicting( + path.MatchRoot("secret_key"), + path.MatchRoot("profile"), + ), + } +} + +func (p *CloudstackProvider) Resources(ctx context.Context) []func() resource.Resource { + return []func() resource.Resource{} +} + +func (p *CloudstackProvider) DataSources(ctx context.Context) []func() datasource.DataSource { + return []func() datasource.DataSource{} +} diff --git a/go.mod b/go.mod index 01b48a33..061e53d9 100644 --- a/go.mod +++ b/go.mod @@ -4,7 +4,9 @@ require ( github.com/apache/cloudstack-go/v2 v2.16.0 github.com/go-ini/ini v1.67.0 github.com/hashicorp/go-multierror v1.1.1 - github.com/hashicorp/terraform-plugin-go v0.22.0 + github.com/hashicorp/terraform-plugin-framework v1.7.0 + github.com/hashicorp/terraform-plugin-framework-validators v0.12.0 + github.com/hashicorp/terraform-plugin-go v0.22.1 github.com/hashicorp/terraform-plugin-mux v0.15.0 github.com/hashicorp/terraform-plugin-sdk/v2 v2.33.0 github.com/hashicorp/terraform-plugin-testing v1.7.0 @@ -59,8 +61,8 @@ require ( golang.org/x/tools v0.13.0 // indirect google.golang.org/appengine v1.6.8 // indirect google.golang.org/genproto/googleapis/rpc v0.0.0-20240123012728-ef4313101c80 // indirect - google.golang.org/grpc v1.62.0 // indirect - google.golang.org/protobuf v1.32.0 // indirect + google.golang.org/grpc v1.62.1 // indirect + google.golang.org/protobuf v1.33.0 // indirect gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c // indirect ) diff --git a/go.sum b/go.sum index 05e3e588..89a463d8 100644 --- a/go.sum +++ b/go.sum @@ -79,8 +79,12 @@ github.com/hashicorp/terraform-exec v0.20.0 h1:DIZnPsqzPGuUnq6cH8jWcPunBfY+C+M8J github.com/hashicorp/terraform-exec v0.20.0/go.mod h1:ckKGkJWbsNqFKV1itgMnE0hY9IYf1HoiekpuN0eWoDw= github.com/hashicorp/terraform-json v0.21.0 h1:9NQxbLNqPbEMze+S6+YluEdXgJmhQykRyRNd+zTI05U= github.com/hashicorp/terraform-json v0.21.0/go.mod h1:qdeBs11ovMzo5puhrRibdD6d2Dq6TyE/28JiU4tIQxk= -github.com/hashicorp/terraform-plugin-go v0.22.0 h1:1OS1Jk5mO0f5hrziWJGXXIxBrMe2j/B8E+DVGw43Xmc= -github.com/hashicorp/terraform-plugin-go v0.22.0/go.mod h1:mPULV91VKss7sik6KFEcEu7HuTogMLLO/EvWCuFkRVE= +github.com/hashicorp/terraform-plugin-framework v1.7.0 h1:wOULbVmfONnJo9iq7/q+iBOBJul5vRovaYJIu2cY/Pw= +github.com/hashicorp/terraform-plugin-framework v1.7.0/go.mod h1:jY9Id+3KbZ17OMpulgnWLSfwxNVYSoYBQFTgsx044CI= +github.com/hashicorp/terraform-plugin-framework-validators v0.12.0 h1:HOjBuMbOEzl7snOdOoUfE2Jgeto6JOjLVQ39Ls2nksc= +github.com/hashicorp/terraform-plugin-framework-validators v0.12.0/go.mod h1:jfHGE/gzjxYz6XoUwi/aYiiKrJDeutQNUtGQXkaHklg= +github.com/hashicorp/terraform-plugin-go v0.22.1 h1:iTS7WHNVrn7uhe3cojtvWWn83cm2Z6ryIUDTRO0EV7w= +github.com/hashicorp/terraform-plugin-go v0.22.1/go.mod h1:qrjnqRghvQ6KnDbB12XeZ4FluclYwptntoWCr9QaXTI= github.com/hashicorp/terraform-plugin-log v0.9.0 h1:i7hOA+vdAItN1/7UrfBqBwvYPQ9TFvymaRGZED3FCV0= github.com/hashicorp/terraform-plugin-log v0.9.0/go.mod h1:rKL8egZQ/eXSyDqzLUuwUYLVdlYeamldAHSxjUFADow= github.com/hashicorp/terraform-plugin-mux v0.15.0 h1:+/+lDx0WUsIOpkAmdwBIoFU8UP9o2eZASoOnLsWbKME= @@ -224,12 +228,12 @@ google.golang.org/appengine v1.6.8 h1:IhEN5q69dyKagZPYMSdIjS2HqprW324FRQZJcGqPAs google.golang.org/appengine v1.6.8/go.mod h1:1jJ3jBArFh5pcgW8gCtRJnepW8FzD1V44FJffLiz/Ds= google.golang.org/genproto/googleapis/rpc v0.0.0-20240123012728-ef4313101c80 h1:AjyfHzEPEFp/NpvfN5g+KDla3EMojjhRVZc1i7cj+oM= google.golang.org/genproto/googleapis/rpc v0.0.0-20240123012728-ef4313101c80/go.mod h1:PAREbraiVEVGVdTZsVWjSbbTtSyGbAgIIvni8a8CD5s= -google.golang.org/grpc v1.62.0 h1:HQKZ/fa1bXkX1oFOvSjmZEUL8wLSaZTjCcLAlmZRtdk= -google.golang.org/grpc v1.62.0/go.mod h1:IWTG0VlJLCh1SkC58F7np9ka9mx/WNkjl4PGJaiq+QE= +google.golang.org/grpc v1.62.1 h1:B4n+nfKzOICUXMgyrNd19h/I9oH0L1pizfk1d4zSgTk= +google.golang.org/grpc v1.62.1/go.mod h1:IWTG0VlJLCh1SkC58F7np9ka9mx/WNkjl4PGJaiq+QE= google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw= google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= -google.golang.org/protobuf v1.32.0 h1:pPC6BG5ex8PDFnkbrGU3EixyhKcQ2aDuBS36lqK/C7I= -google.golang.org/protobuf v1.32.0/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos= +google.golang.org/protobuf v1.33.0 h1:uNO2rsAINq/JlFpSdYEKIZ0uKD/R9cpdv0T+yoGwGmI= +google.golang.org/protobuf v1.33.0/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= diff --git a/main.go b/main.go index 0ec51629..b018e6ae 100644 --- a/main.go +++ b/main.go @@ -24,10 +24,12 @@ import ( "flag" "log" - "github.com/hashicorp/terraform-plugin-go/tfprotov5" - "github.com/hashicorp/terraform-plugin-go/tfprotov5/tf5server" - "github.com/hashicorp/terraform-plugin-mux/tf5muxserver" + "github.com/hashicorp/terraform-plugin-go/tfprotov6" + "github.com/hashicorp/terraform-plugin-go/tfprotov6/tf6server" + "github.com/hashicorp/terraform-plugin-mux/tf6muxserver" + "github.com/hashicorp/terraform-plugin-framework/providerserver" + "github.com/hashicorp/terraform-plugin-mux/tf5to6server" "github.com/terraform-providers/terraform-provider-cloudstack/cloudstack" ) @@ -39,23 +41,35 @@ func main() { flag.BoolVar(&debug, "debug", false, "set to true to run the provider with support for debuggers like delve") flag.Parse() - providers := []func() tfprotov5.ProviderServer{ - cloudstack.New().GRPCProvider, + updatedSdkServer, err := tf5to6server.UpgradeServer( + ctx, + cloudstack.Provider().GRPCProvider, + ) + + if err != nil { + log.Fatal(err) + } + + providers := []func() tfprotov6.ProviderServer{ + providerserver.NewProtocol6(cloudstack.New()), + func() tfprotov6.ProviderServer { + return updatedSdkServer + }, } - muxServer, err := tf5muxserver.NewMuxServer(ctx, providers...) + muxServer, err := tf6muxserver.NewMuxServer(ctx, providers...) if err != nil { log.Fatal(err) } - var serveOpts []tf5server.ServeOpt + var serveOpts []tf6server.ServeOpt if debug { - serveOpts = append(serveOpts, tf5server.WithManagedDebug()) + serveOpts = append(serveOpts, tf6server.WithManagedDebug()) } - err = tf5server.Serve( + err = tf6server.Serve( "registry.terraform.io/cloudstack/cloudstack", muxServer.ProviderServer, serveOpts..., From 0efd72d21017b3e0d0aff5a0526ac3efa6df61c8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fa=CC=81bio=20Matavelli?= Date: Sat, 20 Apr 2024 19:41:45 +0100 Subject: [PATCH 2/7] feat: add provider v6 --- cloudstack/provider_test.go | 8 ++++++++ cloudstack/provider_v6.go | 4 +++- 2 files changed, 11 insertions(+), 1 deletion(-) diff --git a/cloudstack/provider_test.go b/cloudstack/provider_test.go index 927512aa..ddce64ad 100644 --- a/cloudstack/provider_test.go +++ b/cloudstack/provider_test.go @@ -36,6 +36,9 @@ import ( var testAccProviders map[string]*schema.Provider var testAccProvider *schema.Provider +var testAccProvidersV6 map[string]func() (tfprotov6.ProviderServer, error) +var testAccProviderV6 func() (tfprotov6.ProviderServer, error) + var cloudStackTemplateURL = os.Getenv("CLOUDSTACK_TEMPLATE_URL") func init() { @@ -43,6 +46,11 @@ func init() { testAccProviders = map[string]*schema.Provider{ "cloudstack": testAccProvider, } + + testAccProviderV6 = providerserver.NewProtocol6WithError(New()) + testAccProvidersV6 = map[string]func() (tfprotov6.ProviderServer, error){ + "cloudstack": testAccProviderV6, + } } func TestProvider(t *testing.T) { diff --git a/cloudstack/provider_v6.go b/cloudstack/provider_v6.go index 339e4b37..7f5868bb 100644 --- a/cloudstack/provider_v6.go +++ b/cloudstack/provider_v6.go @@ -147,7 +147,9 @@ func (p *CloudstackProvider) ConfigValidators(ctx context.Context) []provider.Co } func (p *CloudstackProvider) Resources(ctx context.Context) []func() resource.Resource { - return []func() resource.Resource{} + return []func() resource.Resource{ + NewCloudstackServiceOfferingResource, + } } func (p *CloudstackProvider) DataSources(ctx context.Context) []func() datasource.DataSource { From 61ad0bfc84802de0ce9785d6540334cd1f43c1ce Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fa=CC=81bio=20Matavelli?= Date: Sat, 20 Apr 2024 19:42:08 +0100 Subject: [PATCH 3/7] feat: migrate service offering to tf plugin framework --- cloudstack/provider.go | 1 - .../resource_cloudstack_service_offering.go | 807 ++++++++++++++---- ...source_cloudstack_service_offering_test.go | 102 ++- cloudstack/resources.go | 23 + 4 files changed, 726 insertions(+), 207 deletions(-) diff --git a/cloudstack/provider.go b/cloudstack/provider.go index 5aad7c55..85a68692 100644 --- a/cloudstack/provider.go +++ b/cloudstack/provider.go @@ -126,7 +126,6 @@ func Provider() *schema.Provider { "cloudstack_disk_offering": resourceCloudStackDiskOffering(), "cloudstack_volume": resourceCloudStackVolume(), "cloudstack_zone": resourceCloudStackZone(), - "cloudstack_service_offering": resourceCloudStackServiceOffering(), "cloudstack_account": resourceCloudStackAccount(), "cloudstack_user": resourceCloudStackUser(), "cloudstack_domain": resourceCloudStackDomain(), diff --git a/cloudstack/resource_cloudstack_service_offering.go b/cloudstack/resource_cloudstack_service_offering.go index 170e05e0..b81668c2 100644 --- a/cloudstack/resource_cloudstack_service_offering.go +++ b/cloudstack/resource_cloudstack_service_offering.go @@ -20,246 +20,735 @@ package cloudstack import ( + "context" "fmt" - "log" - "github.com/apache/cloudstack-go/v2/cloudstack" - "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" + "github.com/hashicorp/terraform-plugin-framework-validators/boolvalidator" + "github.com/hashicorp/terraform-plugin-framework-validators/int64validator" + "github.com/hashicorp/terraform-plugin-framework-validators/stringvalidator" + "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/boolplanmodifier" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/int64planmodifier" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/listplanmodifier" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/planmodifier" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/stringdefault" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/stringplanmodifier" + "github.com/hashicorp/terraform-plugin-framework/schema/validator" + "github.com/hashicorp/terraform-plugin-framework/types" ) -func resourceCloudStackServiceOffering() *schema.Resource { - return &schema.Resource{ - Create: resourceCloudStackServiceOfferingCreate, - Read: resourceCloudStackServiceOfferingRead, - Update: resourceCloudStackServiceOfferingUpdate, - Delete: resourceCloudStackServiceOfferingDelete, - Schema: map[string]*schema.Schema{ - "name": { - Type: schema.TypeString, - Required: true, - }, - "display_text": { - Type: schema.TypeString, - Required: true, - }, - "cpu_number": { +func NewCloudstackServiceOfferingResource() resource.Resource { + return &resourceCloudstackServiceOffering{} +} + +type resourceCloudstackServiceOffering struct { + ResourceWithConfigure +} + +type resourceCloudStackServiceOfferingModel struct { + Name types.String `tfsdk:"name"` + DisplayText types.String `tfsdk:"display_text"` + Customized types.Bool `tfsdk:"customized"` + CpuNumber types.Int64 `tfsdk:"cpu_number"` + CpuNumberMin types.Int64 `tfsdk:"cpu_number_min"` + CpuNumberMax types.Int64 `tfsdk:"cpu_number_max"` + CpuSpeed types.Int64 `tfsdk:"cpu_speed"` + Memory types.Int64 `tfsdk:"memory"` + MemoryMin types.Int64 `tfsdk:"memory_min"` + MemoryMax types.Int64 `tfsdk:"memory_max"` + HostTags types.String `tfsdk:"host_tags"` + NetworkRate types.Int64 `tfsdk:"network_rate"` + OfferHa types.Bool `tfsdk:"offer_ha"` + DynamicScaling types.Bool `tfsdk:"dynamic_scaling"` + LimitCpuUse types.Bool `tfsdk:"limit_cpu_use"` + Volatile types.Bool `tfsdk:"volatile"` + DeploymentPlanner types.String `tfsdk:"deployment_planner"` + ZoneId types.List `tfsdk:"zone_id"` + DiskOfferingId types.String `tfsdk:"disk_offering_id"` + StorageType types.String `tfsdk:"storage_type"` + ProvisioningType types.String `tfsdk:"provisioning_type"` + WriteCacheType types.String `tfsdk:"write_cache_type"` + QosType types.String `tfsdk:"qos_type"` + DiskReadRateBps types.Int64 `tfsdk:"disk_read_rate_bps"` + DiskWriteRateBps types.Int64 `tfsdk:"disk_write_rate_bps"` + DiskReadRateIops types.Int64 `tfsdk:"disk_read_rate_iops"` + DiskWriteRateIops types.Int64 `tfsdk:"disk_write_rate_iops"` + CustomIops types.Bool `tfsdk:"custom_iops"` + MinIops types.Int64 `tfsdk:"min_iops"` + MaxIops types.Int64 `tfsdk:"max_iops"` + HypervisorSnapshotReserve types.Int64 `tfsdk:"hypervisor_snapshot_reserve"` + RootDiskSize types.Int64 `tfsdk:"root_disk_size"` + StorageTags types.String `tfsdk:"storage_tags"` + Encrypt types.Bool `tfsdk:"encrypt"` + DiskOfferingStrictness types.Bool `tfsdk:"disk_offering_strictness"` + Id types.String `tfsdk:"id"` +} + +func (r *resourceCloudstackServiceOffering) Metadata(ctx context.Context, req resource.MetadataRequest, resp *resource.MetadataResponse) { + resp.TypeName = fmt.Sprintf("%s_service_offering", req.ProviderTypeName) +} + +func (r *resourceCloudstackServiceOffering) Schema(ctx context.Context, req resource.SchemaRequest, resp *resource.SchemaResponse) { + resp.Schema = schema.Schema{ + Attributes: map[string]schema.Attribute{ + "name": schema.StringAttribute{ + Description: "The name of the service offering", + Required: true, + }, + "display_text": schema.StringAttribute{ + Description: "The display text of the service offering", + Required: true, + }, + "customized": schema.BoolAttribute{ + Description: "Is the service offering customized", + Optional: true, + Computed: true, + PlanModifiers: []planmodifier.Bool{ + boolplanmodifier.RequiresReplace(), + }, + Default: booldefault.StaticBool(false), + Validators: []validator.Bool{ + boolvalidator.ConflictsWith(path.MatchRoot("cpu_number"), path.MatchRoot("memory")), + }, + }, + "cpu_number": schema.Int64Attribute{ Description: "Number of CPU cores", - Type: schema.TypeInt, Optional: true, - ForceNew: true, + Computed: true, + PlanModifiers: []planmodifier.Int64{ + int64planmodifier.RequiresReplace(), + }, + Validators: []validator.Int64{ + int64validator.ConflictsWith(path.MatchRoot("cpu_number_min"), path.MatchRoot("cpu_number_max")), + }, + }, + "cpu_number_min": schema.Int64Attribute{ + Description: "Minimum number of CPU cores", + Optional: true, + Computed: true, + PlanModifiers: []planmodifier.Int64{ + int64planmodifier.RequiresReplace(), + }, + Validators: []validator.Int64{ + int64validator.AlsoRequires(path.MatchRoot("cpu_number_max")), + int64validator.AtMostSumOf(path.MatchRoot("cpu_number_max")), + int64validator.ConflictsWith(path.MatchRoot("cpu_number")), + }, + }, + "cpu_number_max": schema.Int64Attribute{ + Description: "Maximum number of CPU cores", + Optional: true, + Computed: true, + PlanModifiers: []planmodifier.Int64{ + int64planmodifier.RequiresReplace(), + }, + Validators: []validator.Int64{ + int64validator.AlsoRequires(path.MatchRoot("cpu_number_min")), + int64validator.AtLeastSumOf(path.MatchRoot("cpu_number_min")), + int64validator.ConflictsWith(path.MatchRoot("cpu_number")), + }, }, - "cpu_speed": { + "cpu_speed": schema.Int64Attribute{ Description: "Speed of CPU in Mhz", - Type: schema.TypeInt, + Required: true, + PlanModifiers: []planmodifier.Int64{ + int64planmodifier.RequiresReplace(), + }, + }, + "memory": schema.Int64Attribute{ + Description: "The total memory of the service offering in MB", Optional: true, - ForceNew: true, + Computed: true, + PlanModifiers: []planmodifier.Int64{ + int64planmodifier.RequiresReplace(), + }, + Validators: []validator.Int64{ + int64validator.ConflictsWith(path.MatchRoot("memory_min"), path.MatchRoot("memory_max")), + }, }, - "host_tags": { + "memory_min": schema.Int64Attribute{ + Description: "Minimum memory of the service offering in MB", + Optional: true, + Computed: true, + PlanModifiers: []planmodifier.Int64{ + int64planmodifier.RequiresReplace(), + }, + Validators: []validator.Int64{ + int64validator.AlsoRequires(path.MatchRoot("memory_max")), + int64validator.AtMostSumOf(path.MatchRoot("memory_max")), + int64validator.ConflictsWith(path.MatchRoot("memory")), + }, + }, + "memory_max": schema.Int64Attribute{ + Description: "Maximum memory of the service offering in MB", + Optional: true, + Computed: true, + PlanModifiers: []planmodifier.Int64{ + int64planmodifier.RequiresReplace(), + }, + Validators: []validator.Int64{ + int64validator.AlsoRequires(path.MatchRoot("memory_min")), + int64validator.AtLeastSumOf(path.MatchRoot("memory_min")), + int64validator.ConflictsWith(path.MatchRoot("memory")), + }, + }, + "host_tags": schema.StringAttribute{ Description: "The host tag for this service offering", - Type: schema.TypeString, Optional: true, + Computed: true, + }, + "network_rate": schema.Int64Attribute{ + Description: "Data transfer rate in megabits per second", + Optional: true, + Computed: true, + PlanModifiers: []planmodifier.Int64{ + int64planmodifier.RequiresReplace(), + }, + }, + "offer_ha": schema.BoolAttribute{ + Description: "The HA for the service offering", + Optional: true, + Computed: true, + PlanModifiers: []planmodifier.Bool{ + boolplanmodifier.RequiresReplace(), + }, + Default: booldefault.StaticBool(false), + }, + "dynamic_scaling": schema.BoolAttribute{ + Description: "Enable dynamic scaling of the service offering", + Optional: true, + Computed: true, + PlanModifiers: []planmodifier.Bool{ + boolplanmodifier.RequiresReplace(), + }, + Default: booldefault.StaticBool(false), }, - "limit_cpu_use": { + "limit_cpu_use": schema.BoolAttribute{ Description: "Restrict the CPU usage to committed service offering", - Type: schema.TypeBool, Optional: true, - ForceNew: true, - Default: false, + Computed: true, + PlanModifiers: []planmodifier.Bool{ + boolplanmodifier.RequiresReplace(), + }, + Default: booldefault.StaticBool(false), }, - "memory": { - Description: "The total memory of the service offering in MB", - Type: schema.TypeInt, + "volatile": schema.BoolAttribute{ + Description: "Service offering is volatile", Optional: true, - ForceNew: true, + Computed: true, + PlanModifiers: []planmodifier.Bool{ + boolplanmodifier.RequiresReplace(), + }, + Default: booldefault.StaticBool(false), }, - "offer_ha": { - Description: "The HA for the service offering", - Type: schema.TypeBool, + "deployment_planner": schema.StringAttribute{ + Description: "The deployment planner for the service offering", Optional: true, - ForceNew: true, - Default: false, + Computed: true, + PlanModifiers: []planmodifier.String{ + stringplanmodifier.RequiresReplace(), + }, + Validators: []validator.String{ + stringvalidator.OneOf("FirstFitPlanner", "UserDispersingPlanner", "UserConcentratedPodPlanner", "ImplicitDedicationPlanner", "BareMetalPlanner"), + }, }, - "storage_type": { + "zone_id": schema.ListAttribute{ + Description: "The ID of the zone(s)", + Optional: true, + Computed: true, + ElementType: types.StringType, + PlanModifiers: []planmodifier.List{ + listplanmodifier.RequiresReplace(), + }, + }, + "disk_offering_id": schema.StringAttribute{ + Description: "The ID of the disk offering", + Optional: true, + Computed: true, + PlanModifiers: []planmodifier.String{ + stringplanmodifier.RequiresReplace(), + }, + }, + "storage_type": schema.StringAttribute{ Description: "The storage type of the service offering. Values are local and shared", - Type: schema.TypeString, Optional: true, - ForceNew: true, - Default: "shared", - ValidateFunc: func(val interface{}, key string) (warns []string, errs []error) { - v := val.(string) - - if v == "local" || v == "shared" { - return - } - - errs = append(errs, fmt.Errorf("storage type should be either local or shared, got %s", v)) - - return + Computed: true, + PlanModifiers: []planmodifier.String{ + stringplanmodifier.RequiresReplace(), + }, + Validators: []validator.String{ + stringvalidator.OneOf("local", "shared"), + stringvalidator.ConflictsWith(path.MatchRoot("disk_offering_id")), + }, + Default: stringdefault.StaticString("shared"), + }, + "provisioning_type": schema.StringAttribute{ + Description: "The provisioning type of the service offering", + Optional: true, + Computed: true, + PlanModifiers: []planmodifier.String{ + stringplanmodifier.RequiresReplace(), + }, + Validators: []validator.String{ + stringvalidator.OneOf("thin", "sparse", "fat"), + stringvalidator.ConflictsWith(path.MatchRoot("disk_offering_id")), + }, + Default: stringdefault.StaticString("thin"), + }, + "write_cache_type": schema.StringAttribute{ + Description: "The write cache type of the service offering", + Optional: true, + Computed: true, + PlanModifiers: []planmodifier.String{ + stringplanmodifier.RequiresReplace(), + }, + Validators: []validator.String{ + stringvalidator.OneOf("none", "writeback", "writethrough"), + stringvalidator.ConflictsWith(path.MatchRoot("disk_offering_id")), + }, + Default: stringdefault.StaticString("none"), + }, + "qos_type": schema.StringAttribute{ + Description: "The QoS type of the service offering", + Optional: true, + Computed: true, + PlanModifiers: []planmodifier.String{ + stringplanmodifier.RequiresReplace(), + }, + Validators: []validator.String{ + stringvalidator.OneOf("none", "hypervisor", "storage"), + stringvalidator.ConflictsWith(path.MatchRoot("disk_offering_id")), + }, + Default: stringdefault.StaticString("none"), + }, + "disk_read_rate_bps": schema.Int64Attribute{ + Description: "The read rate of the service offering", + Optional: true, + Computed: true, + PlanModifiers: []planmodifier.Int64{ + int64planmodifier.RequiresReplace(), + }, + Validators: []validator.Int64{ + int64validator.ConflictsWith(path.MatchRoot("disk_offering_id")), + }, + }, + "disk_write_rate_bps": schema.Int64Attribute{ + Description: "The write rate of the service offering", + Optional: true, + Computed: true, + PlanModifiers: []planmodifier.Int64{ + int64planmodifier.RequiresReplace(), + }, + Validators: []validator.Int64{ + int64validator.ConflictsWith(path.MatchRoot("disk_offering_id")), + }, + }, + "disk_read_rate_iops": schema.Int64Attribute{ + Description: "The read rate of the service offering in IOPS", + Optional: true, + Computed: true, + PlanModifiers: []planmodifier.Int64{ + int64planmodifier.RequiresReplace(), + }, + Validators: []validator.Int64{ + int64validator.ConflictsWith(path.MatchRoot("disk_offering_id")), + }, + }, + "disk_write_rate_iops": schema.Int64Attribute{ + Description: "The write rate of the service offering in IOPS", + Optional: true, + Computed: true, + PlanModifiers: []planmodifier.Int64{ + int64planmodifier.RequiresReplace(), + }, + Validators: []validator.Int64{ + int64validator.ConflictsWith(path.MatchRoot("disk_offering_id")), + }, + }, + "custom_iops": schema.BoolAttribute{ + Description: "Custom IOPS", + Optional: true, + Computed: true, + PlanModifiers: []planmodifier.Bool{ + boolplanmodifier.RequiresReplace(), + }, + Default: booldefault.StaticBool(false), + Validators: []validator.Bool{ + boolvalidator.ConflictsWith(path.MatchRoot("disk_offering_id")), + boolvalidator.ConflictsWith(path.MatchRoot("disk_read_rate_iops"), path.MatchRoot("disk_write_rate_iops")), + boolvalidator.ConflictsWith(path.MatchRoot("disk_read_rate_bps"), path.MatchRoot("disk_write_rate_bps")), + }, + }, + "min_iops": schema.Int64Attribute{ + Description: "The minimum IOPS of the service offering", + Optional: true, + Computed: true, + PlanModifiers: []planmodifier.Int64{ + int64planmodifier.RequiresReplace(), + }, + Validators: []validator.Int64{ + int64validator.ConflictsWith(path.MatchRoot("disk_offering_id")), + int64validator.ConflictsWith(path.MatchRoot("disk_read_rate_iops"), path.MatchRoot("disk_write_rate_iops")), + int64validator.ConflictsWith(path.MatchRoot("disk_read_rate_bps"), path.MatchRoot("disk_write_rate_bps")), + int64validator.ConflictsWith(path.MatchRoot("custom_iops")), + }, + }, + "max_iops": schema.Int64Attribute{ + Description: "The maximum IOPS of the service offering", + Optional: true, + Computed: true, + PlanModifiers: []planmodifier.Int64{ + int64planmodifier.RequiresReplace(), + }, + Validators: []validator.Int64{ + int64validator.ConflictsWith(path.MatchRoot("disk_offering_id")), + int64validator.ConflictsWith(path.MatchRoot("disk_read_rate_iops"), path.MatchRoot("disk_write_rate_iops")), + int64validator.ConflictsWith(path.MatchRoot("disk_read_rate_bps"), path.MatchRoot("disk_write_rate_bps")), + int64validator.ConflictsWith(path.MatchRoot("custom_iops")), + }, + }, + "hypervisor_snapshot_reserve": schema.Int64Attribute{ + Description: "The hypervisor snapshot reserve of the service offering", + Optional: true, + Computed: true, + PlanModifiers: []planmodifier.Int64{ + int64planmodifier.RequiresReplace(), + }, + Validators: []validator.Int64{ + int64validator.ConflictsWith(path.MatchRoot("disk_offering_id")), + int64validator.ConflictsWith(path.MatchRoot("disk_read_rate_iops"), path.MatchRoot("disk_write_rate_iops")), + int64validator.ConflictsWith(path.MatchRoot("disk_read_rate_bps"), path.MatchRoot("disk_write_rate_bps")), + }, + }, + "root_disk_size": schema.Int64Attribute{ + Description: "The size of the root disk in GB", + Optional: true, + Computed: true, + PlanModifiers: []planmodifier.Int64{ + int64planmodifier.RequiresReplace(), + }, + Validators: []validator.Int64{ + int64validator.ConflictsWith(path.MatchRoot("disk_offering_id")), + }, + }, + "storage_tags": schema.StringAttribute{ + Description: "The storage tags of the service offering", + Optional: true, + Computed: true, + PlanModifiers: []planmodifier.String{ + stringplanmodifier.RequiresReplace(), + }, + Validators: []validator.String{ + stringvalidator.ConflictsWith(path.MatchRoot("disk_offering_id")), + }, + }, + "encrypt": schema.BoolAttribute{ + Description: "Encrypt the service offering storage", + Optional: true, + Computed: true, + PlanModifiers: []planmodifier.Bool{ + boolplanmodifier.RequiresReplace(), + }, + Default: booldefault.StaticBool(false), + Validators: []validator.Bool{ + boolvalidator.ConflictsWith(path.MatchRoot("disk_offering_id")), + }, + }, + "disk_offering_strictness": schema.BoolAttribute{ + Description: "Disk offering strictness", + Optional: true, + Computed: true, + PlanModifiers: []planmodifier.Bool{ + boolplanmodifier.RequiresReplace(), + }, + Default: booldefault.StaticBool(false), + }, + "id": schema.StringAttribute{ + Description: "The ID of the service offering", + Computed: true, + PlanModifiers: []planmodifier.String{ + stringplanmodifier.UseStateForUnknown(), }, }, }, } } -func resourceCloudStackServiceOfferingCreate(d *schema.ResourceData, meta interface{}) error { - cs := meta.(*cloudstack.CloudStackClient) - name := d.Get("name").(string) - display_text := d.Get("display_text").(string) +func (r *resourceCloudstackServiceOffering) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) { + var data resourceCloudStackServiceOfferingModel + + resp.Diagnostics.Append(req.Plan.Get(ctx, &data)...) + + if resp.Diagnostics.HasError() { + return + } + + p := r.client.ServiceOffering.NewCreateServiceOfferingParams(data.DisplayText.ValueString(), data.Name.ValueString()) + + if !data.Customized.ValueBool() { + if !data.CpuNumber.IsNull() && !data.CpuNumber.IsUnknown() { + p.SetCpunumber(int(data.CpuNumber.ValueInt64())) + } + + if !data.Memory.IsNull() && !data.Memory.IsUnknown() { + p.SetMemory(int(data.Memory.ValueInt64())) + } - // Create a new parameter struct - p := cs.ServiceOffering.NewCreateServiceOfferingParams(display_text, name) - if v, ok := d.GetOk("cpu_number"); ok { - p.SetCpunumber(v.(int)) + data.CpuNumberMax = types.Int64Null() + data.CpuNumberMin = types.Int64Null() + data.MemoryMax = types.Int64Null() + data.MemoryMin = types.Int64Null() + } else { + if !data.CpuNumberMin.IsNull() && !data.CpuNumberMin.IsUnknown() { + p.SetMincpunumber(int(data.CpuNumberMin.ValueInt64())) + } + + if !data.CpuNumberMax.IsNull() && !data.CpuNumberMax.IsUnknown() { + p.SetMaxcpunumber(int(data.CpuNumberMax.ValueInt64())) + } + + if !data.MemoryMin.IsNull() && !data.MemoryMin.IsUnknown() { + p.SetMinmemory(int(data.MemoryMin.ValueInt64())) + } + + if !data.MemoryMax.IsNull() && !data.MemoryMax.IsUnknown() { + p.SetMaxmemory(int(data.MemoryMax.ValueInt64())) + } + + data.CpuNumber = types.Int64Null() + data.Memory = types.Int64Null() } - if v, ok := d.GetOk("cpu_speed"); ok { - p.SetCpuspeed(v.(int)) + if !data.CpuSpeed.IsNull() && !data.CpuSpeed.IsUnknown() { + p.SetCpuspeed(int(data.CpuSpeed.ValueInt64())) } - if v, ok := d.GetOk("host_tags"); ok { - p.SetHosttags(v.(string)) + if !data.HostTags.IsNull() && !data.HostTags.IsUnknown() { + p.SetHosttags(data.HostTags.ValueString()) + } else { + data.HostTags = types.StringNull() } - if v, ok := d.GetOk("limit_cpu_use"); ok { - p.SetLimitcpuuse(v.(bool)) + if !data.NetworkRate.IsNull() && !data.NetworkRate.IsUnknown() { + p.SetNetworkrate(int(data.NetworkRate.ValueInt64())) + } else { + data.NetworkRate = types.Int64Null() } - if v, ok := d.GetOk("memory"); ok { - p.SetMemory(v.(int)) + if !data.OfferHa.IsNull() && !data.OfferHa.IsUnknown() { + p.SetOfferha(data.OfferHa.ValueBool()) } - if v, ok := d.GetOk("offer_ha"); ok { - p.SetOfferha(v.(bool)) + if !data.DynamicScaling.IsNull() && !data.DynamicScaling.IsUnknown() { + p.SetDynamicscalingenabled(data.DynamicScaling.ValueBool()) } - if v, ok := d.GetOk("storage_type"); ok { - p.SetStoragetype(v.(string)) + if !data.LimitCpuUse.IsNull() && !data.LimitCpuUse.IsUnknown() { + p.SetLimitcpuuse(data.LimitCpuUse.ValueBool()) } - log.Printf("[DEBUG] Creating Service Offering %s", name) - s, err := cs.ServiceOffering.CreateServiceOffering(p) + if !data.Volatile.IsNull() && !data.Volatile.IsUnknown() { + p.SetIsvolatile(data.Volatile.ValueBool()) + } - if err != nil { - return err + if !data.DeploymentPlanner.IsNull() && !data.DeploymentPlanner.IsUnknown() { + p.SetDeploymentplanner(data.DeploymentPlanner.ValueString()) + } else { + data.DeploymentPlanner = types.StringNull() } - log.Printf("[DEBUG] Service Offering %s successfully created", name) - d.SetId(s.Id) + if !data.ZoneId.IsNull() && !data.ZoneId.IsUnknown() { + zoneIds := make([]string, 0, len(data.ZoneId.Elements())) + diags := data.ZoneId.ElementsAs(ctx, &zoneIds, false) + if diags.HasError() { + resp.Diagnostics.Append(diags...) + return + } + p.SetZoneid(zoneIds) + } else { + data.ZoneId = types.ListNull(types.StringType) + } - return resourceCloudStackServiceOfferingRead(d, meta) -} + if !data.DiskOfferingId.IsNull() && !data.DiskOfferingId.IsUnknown() { + p.SetDiskofferingid(data.DiskOfferingId.ValueString()) + } else { + p.SetDiskofferingid("") + data.DiskOfferingId = types.StringNull() + + if !data.StorageType.IsNull() && !data.StorageType.IsUnknown() { + p.SetStoragetype(data.StorageType.ValueString()) + } else { + data.StorageType = types.StringNull() + } -func resourceCloudStackServiceOfferingRead(d *schema.ResourceData, meta interface{}) error { - cs := meta.(*cloudstack.CloudStackClient) - log.Printf("[DEBUG] Retrieving Service Offering %s", d.Get("name").(string)) + if !data.ProvisioningType.IsNull() && !data.ProvisioningType.IsUnknown() { + p.SetProvisioningtype(data.ProvisioningType.ValueString()) + } - // Get the Service Offering details - s, count, err := cs.ServiceOffering.GetServiceOfferingByName(d.Get("name").(string)) + if !data.WriteCacheType.IsNull() && !data.WriteCacheType.IsUnknown() { + p.SetCachemode(data.WriteCacheType.ValueString()) + } - if err != nil { - if count == 0 { - log.Printf("[DEBUG] Service Offering %s does no longer exist", d.Get("name").(string)) - d.SetId("") - return nil + if data.QosType.ValueString() == "hypervisor" { + if !data.DiskReadRateBps.IsNull() && !data.DiskReadRateBps.IsUnknown() { + p.SetBytesreadrate(data.DiskReadRateBps.ValueInt64()) + } + + if !data.DiskWriteRateBps.IsNull() && !data.DiskWriteRateBps.IsUnknown() { + p.SetByteswriterate(data.DiskWriteRateBps.ValueInt64()) + } + + if !data.DiskReadRateIops.IsNull() && !data.DiskReadRateIops.IsUnknown() { + p.SetIopsreadrate(data.DiskReadRateIops.ValueInt64()) + } + + if !data.DiskWriteRateIops.IsNull() && !data.DiskWriteRateIops.IsUnknown() { + p.SetIopswriterate(data.DiskWriteRateIops.ValueInt64()) + } + } else if data.QosType.ValueString() == "storage" { + p.SetCustomizediops(data.CustomIops.ValueBool()) + + if !data.CustomIops.ValueBool() { + if !data.MinIops.IsNull() && !data.MinIops.IsUnknown() { + p.SetMiniops(data.MinIops.ValueInt64()) + } + + if !data.MaxIops.IsNull() && !data.MaxIops.IsUnknown() { + p.SetMaxiops(data.MaxIops.ValueInt64()) + } + } else { + p.SetMiniops(0) + p.SetMaxiops(0) + } + + if !data.HypervisorSnapshotReserve.IsNull() && !data.HypervisorSnapshotReserve.IsUnknown() { + p.SetHypervisorsnapshotreserve(int(data.HypervisorSnapshotReserve.ValueInt64())) + } + } else { + data.DiskReadRateBps = types.Int64Null() + data.DiskWriteRateBps = types.Int64Null() + data.DiskReadRateIops = types.Int64Null() + data.DiskWriteRateIops = types.Int64Null() + data.MinIops = types.Int64Null() + data.MaxIops = types.Int64Null() + data.HypervisorSnapshotReserve = types.Int64Null() } - return err } - d.SetId(s.Id) - - fields := map[string]interface{}{ - "name": s.Name, - "display_text": s.Displaytext, - "cpu_number": s.Cpunumber, - "cpu_speed": s.Cpuspeed, - "host_tags": s.Hosttags, - "limit_cpu_use": s.Limitcpuuse, - "memory": s.Memory, - "offer_ha": s.Offerha, - "storage_type": s.Storagetype, + if !data.RootDiskSize.IsNull() && !data.RootDiskSize.IsUnknown() { + p.SetRootdisksize(data.RootDiskSize.ValueInt64()) + } else { + data.RootDiskSize = types.Int64Null() } - for k, v := range fields { - d.Set(k, v) + if !data.StorageTags.IsNull() && !data.StorageTags.IsUnknown() { + p.SetTags(data.StorageTags.ValueString()) + } else { + data.StorageTags = types.StringNull() } - return nil -} + if !data.Encrypt.IsNull() && !data.Encrypt.IsUnknown() { + p.SetEncryptroot(data.Encrypt.ValueBool()) + } -func resourceCloudStackServiceOfferingUpdate(d *schema.ResourceData, meta interface{}) error { - cs := meta.(*cloudstack.CloudStackClient) + if !data.DiskOfferingStrictness.IsNull() && !data.DiskOfferingStrictness.IsUnknown() { + p.SetDiskofferingstrictness(data.DiskOfferingStrictness.ValueBool()) + } - name := d.Get("name").(string) + s, err := r.client.ServiceOffering.CreateServiceOffering(p) - // Check if the name is changed and if so, update the service offering - if d.HasChange("name") { - log.Printf("[DEBUG] Name changed for %s, starting update", name) + if err != nil { + resp.Diagnostics.AddError( + "Error creating service offering", + fmt.Sprintf("Error while trying to create service offering: %s", err), + ) + return + } - // Create a new parameter struct - p := cs.ServiceOffering.NewUpdateServiceOfferingParams(d.Id()) + data.Id = types.StringValue(s.Id) - // Set the new name - p.SetName(d.Get("name").(string)) + resp.Diagnostics.Append(resp.State.Set(ctx, &data)...) +} - // Update the name - _, err := cs.ServiceOffering.UpdateServiceOffering(p) - if err != nil { - return fmt.Errorf( - "Error updating the name for service offering %s: %s", name, err) - } +func (r *resourceCloudstackServiceOffering) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) { + var data resourceCloudStackServiceOfferingModel - } + resp.Diagnostics.Append(req.State.Get(ctx, &data)...) - // Check if the display text is changed and if so, update seervice offering - if d.HasChange("display_text") { - log.Printf("[DEBUG] Display text changed for %s, starting update", name) + s, count, err := r.client.ServiceOffering.GetServiceOfferingByID(data.Id.ValueString()) - // Create a new parameter struct - p := cs.ServiceOffering.NewUpdateServiceOfferingParams(d.Id()) + if count == 0 { + resp.State.RemoveResource(ctx) + return + } - // Set the new display text - p.SetName(d.Get("display_text").(string)) + if err != nil { + resp.Diagnostics.AddError( + "Error reading service offering", + fmt.Sprintf("Error while trying to read service offering: %s", err), + ) + return + } - // Update the display text - _, err := cs.ServiceOffering.UpdateServiceOffering(p) - if err != nil { - return fmt.Errorf( - "Error updating the display text for service offering %s: %s", name, err) - } + data.Id = types.StringValue(s.Id) + data.Name = types.StringValue(s.Name) + data.DisplayText = types.StringValue(s.Displaytext) + data.CpuNumber = types.Int64Value(int64(s.Cpunumber)) + data.CpuSpeed = types.Int64Value(int64(s.Cpuspeed)) + data.HostTags = types.StringValue(s.Hosttags) + data.LimitCpuUse = types.BoolValue(s.Limitcpuuse) + data.Memory = types.Int64Value(int64(s.Memory)) + data.OfferHa = types.BoolValue(s.Offerha) + data.StorageType = types.StringValue(s.Storagetype) + + resp.Diagnostics.Append(resp.State.Set(ctx, &data)...) +} - } +func (r *resourceCloudstackServiceOffering) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) { + var data resourceCloudStackServiceOfferingModel - if d.HasChange("host_tags") { - log.Printf("[DEBUG] Host tags changed for %s, starting update", name) + resp.Diagnostics.Append(req.Plan.Get(ctx, &data)...) - // Create a new parameter struct - p := cs.ServiceOffering.NewUpdateServiceOfferingParams(d.Id()) + p := r.client.ServiceOffering.NewUpdateServiceOfferingParams(data.Id.ValueString()) - // Set the new host tags - p.SetHosttags(d.Get("host_tags").(string)) + p.SetName(data.Name.ValueString()) + p.SetDisplaytext(data.DisplayText.ValueString()) + p.SetHosttags(data.HostTags.ValueString()) + p.SetStoragetags(data.HostTags.ValueString()) - // Update the host tags - _, err := cs.ServiceOffering.UpdateServiceOffering(p) - if err != nil { - return fmt.Errorf( - "Error updating the host tags for service offering %s: %s", name, err) - } + _, err := r.client.ServiceOffering.UpdateServiceOffering(p) + if err != nil { + resp.Diagnostics.AddError( + "Error updating service offering", + fmt.Sprintf("Error while trying to update service offering: %s", err), + ) + return } - return resourceCloudStackServiceOfferingRead(d, meta) + resp.Diagnostics.Append(resp.State.Set(ctx, &data)...) } -func resourceCloudStackServiceOfferingDelete(d *schema.ResourceData, meta interface{}) error { - cs := meta.(*cloudstack.CloudStackClient) +func (r *resourceCloudstackServiceOffering) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) { + var data resourceCloudStackServiceOfferingModel - // Create a new parameter struct - p := cs.ServiceOffering.NewDeleteServiceOfferingParams(d.Id()) - _, err := cs.ServiceOffering.DeleteServiceOffering(p) + resp.Diagnostics.Append(req.State.Get(ctx, &data)...) + + p := r.client.ServiceOffering.NewDeleteServiceOfferingParams(data.Id.ValueString()) + _, err := r.client.ServiceOffering.DeleteServiceOffering(p) if err != nil { - return fmt.Errorf("Error deleting Service Offering: %s", err) + resp.Diagnostics.AddError( + "Error deleting service offering", + fmt.Sprintf("Error while trying to delete service offering: %s", err), + ) + return } +} - return nil +func (r *resourceCloudstackServiceOffering) ImportState(ctx context.Context, req resource.ImportStateRequest, resp *resource.ImportStateResponse) { + resource.ImportStatePassthroughID(ctx, path.Root("id"), req, resp) } diff --git a/cloudstack/resource_cloudstack_service_offering_test.go b/cloudstack/resource_cloudstack_service_offering_test.go index fc29628c..ae229f1b 100644 --- a/cloudstack/resource_cloudstack_service_offering_test.go +++ b/cloudstack/resource_cloudstack_service_offering_test.go @@ -23,63 +23,71 @@ import ( "fmt" "testing" - "github.com/apache/cloudstack-go/v2/cloudstack" + "github.com/hashicorp/terraform-plugin-testing/helper/acctest" "github.com/hashicorp/terraform-plugin-testing/helper/resource" - "github.com/hashicorp/terraform-plugin-testing/terraform" ) func TestAccCloudStackServiceOffering_basic(t *testing.T) { - var so cloudstack.ServiceOffering + // ctx := context.Background() + rName := acctest.RandomWithPrefix("service_offering") + + // var so cloudstack.ServiceOffering resource.Test(t, resource.TestCase{ - PreCheck: func() { testAccPreCheck(t) }, - Providers: testAccProviders, + ProtoV6ProviderFactories: testAccProvidersV6, Steps: []resource.TestStep{ { - Config: testAccCloudStackServiceOffering_basic, + Config: fmt.Sprintf(` + resource "cloudstack_service_offering" "test" { + name = "%s" + display_text = "Test" + cpu_number = 2 + cpu_speed = 2200 + memory = 8096 + }`, rName), + Check: resource.ComposeTestCheckFunc( + resource.TestCheckResourceAttr("cloudstack_service_offering.test", "cpu_number", "2"), + resource.TestCheckResourceAttr("cloudstack_service_offering.test", "cpu_speed", "2200"), + resource.TestCheckResourceAttr("cloudstack_service_offering.test", "memory", "8096"), + ), + }, + { + ResourceName: "cloudstack_service_offering.test", + ImportState: true, + ImportStateVerify: true, + ImportStateVerifyIgnore: []string{ + "custom_iops", + "customized", + "disk_offering_strictness", + "dynamic_scaling", + "encrypt", + "host_tags", + "provisioning_type", + "qos_type", + "volatile", + "write_cache_type", + }, + }, + { + Config: fmt.Sprintf(` + resource "cloudstack_service_offering" "test2" { + name = "%s" + display_text = "Test" + customized = true + cpu_number_min = 2 + cpu_number_max = 4 + memory_min = 8096 + memory_max = 16384 + cpu_speed = 2200 + }`, rName), Check: resource.ComposeTestCheckFunc( - testAccCheckCloudStackServiceOfferingExists("cloudstack_service_offering.test1", &so), - resource.TestCheckResourceAttr("cloudstack_service_offering.test1", "cpu_number", "2"), - resource.TestCheckResourceAttr("cloudstack_service_offering.test1", "cpu_speed", "2200"), - resource.TestCheckResourceAttr("cloudstack_service_offering.test1", "memory", "8096"), + // testAccCheckCloudStackServiceOfferingExists("cloudstack_service_offering.test2", &so), + resource.TestCheckResourceAttr("cloudstack_service_offering.test2", "cpu_number_min", "2"), + resource.TestCheckResourceAttr("cloudstack_service_offering.test2", "cpu_number_max", "4"), + resource.TestCheckResourceAttr("cloudstack_service_offering.test2", "memory_min", "8096"), + resource.TestCheckResourceAttr("cloudstack_service_offering.test2", "memory_max", "16384"), + resource.TestCheckResourceAttr("cloudstack_service_offering.test2", "cpu_speed", "2200"), ), }, }, }) } - -const testAccCloudStackServiceOffering_basic = ` -resource "cloudstack_service_offering" "test1" { - name = "service_offering_1" - display_text = "Test" - cpu_number = 2 - cpu_speed = 2200 - memory = 8096 -} -` - -func testAccCheckCloudStackServiceOfferingExists(n string, so *cloudstack.ServiceOffering) resource.TestCheckFunc { - return func(s *terraform.State) error { - rs, ok := s.RootModule().Resources[n] - if !ok { - return fmt.Errorf("Not found: %s", n) - } - - if rs.Primary.ID == "" { - return fmt.Errorf("No service offering ID is set") - } - - cs := testAccProvider.Meta().(*cloudstack.CloudStackClient) - resp, _, err := cs.ServiceOffering.GetServiceOfferingByID(rs.Primary.ID) - if err != nil { - return err - } - - if resp.Id != rs.Primary.ID { - return fmt.Errorf("Service offering not found") - } - - *so = *resp - - return nil - } -} diff --git a/cloudstack/resources.go b/cloudstack/resources.go index 0fedb70c..22b2adcc 100644 --- a/cloudstack/resources.go +++ b/cloudstack/resources.go @@ -20,6 +20,7 @@ package cloudstack import ( + "context" "fmt" "log" "regexp" @@ -27,6 +28,7 @@ import ( "time" "github.com/apache/cloudstack-go/v2/cloudstack" + "github.com/hashicorp/terraform-plugin-framework/resource" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" ) @@ -170,3 +172,24 @@ func importStatePassthrough(d *schema.ResourceData, meta interface{}) ([]*schema return []*schema.ResourceData{d}, nil } + +type ResourceWithConfigure struct { + client *cloudstack.CloudStackClient +} + +func (r *ResourceWithConfigure) Configure(ctx context.Context, req resource.ConfigureRequest, resp *resource.ConfigureResponse) { + if req.ProviderData == nil { + return + } + + client, ok := req.ProviderData.(*cloudstack.CloudStackClient) + + if !ok { + resp.Diagnostics.AddError( + "Unexpected Resource Configure Type", + fmt.Sprintf("Expected *cloudstack.CloudStackClient, got %T", req.ProviderData), + ) + } + + r.client = client +} From ed7abd8e916dd6ca417b5ba4c5ac553d07e44793 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fa=CC=81bio=20Matavelli?= Date: Sun, 23 Jun 2024 14:22:38 +0100 Subject: [PATCH 4/7] chore: use muxed server to test v6 --- ...source_cloudstack_service_offering_test.go | 7 +- cloudstack/provider_test.go | 64 +++++++++---------- ...source_cloudstack_service_offering_test.go | 2 +- 3 files changed, 36 insertions(+), 37 deletions(-) diff --git a/cloudstack/data_source_cloudstack_service_offering_test.go b/cloudstack/data_source_cloudstack_service_offering_test.go index afe94d5e..81574849 100644 --- a/cloudstack/data_source_cloudstack_service_offering_test.go +++ b/cloudstack/data_source_cloudstack_service_offering_test.go @@ -30,8 +30,8 @@ func TestAccServiceOfferingDataSource_basic(t *testing.T) { datasourceName := "data.cloudstack_service_offering.service-offering-data-source" resource.Test(t, resource.TestCase{ - PreCheck: func() { testAccPreCheck(t) }, - Providers: testAccProviders, + PreCheck: func() { testAccPreCheck(t) }, + ProtoV6ProviderFactories: testAccMuxProvider, Steps: []resource.TestStep{ { Config: testServiceOfferingDataSourceConfig_basic, @@ -47,6 +47,9 @@ const testServiceOfferingDataSourceConfig_basic = ` resource "cloudstack_service_offering" "service-offering-resource" { name = "TestServiceUpdate" display_text = "DisplayService" + cpu_number = 2 + cpu_speed = 2200 + memory = 8096 } data "cloudstack_service_offering" "service-offering-data-source" { diff --git a/cloudstack/provider_test.go b/cloudstack/provider_test.go index ddce64ad..6792df5a 100644 --- a/cloudstack/provider_test.go +++ b/cloudstack/provider_test.go @@ -36,8 +36,7 @@ import ( var testAccProviders map[string]*schema.Provider var testAccProvider *schema.Provider -var testAccProvidersV6 map[string]func() (tfprotov6.ProviderServer, error) -var testAccProviderV6 func() (tfprotov6.ProviderServer, error) +var testAccMuxProvider map[string]func() (tfprotov6.ProviderServer, error) var cloudStackTemplateURL = os.Getenv("CLOUDSTACK_TEMPLATE_URL") @@ -47,9 +46,34 @@ func init() { "cloudstack": testAccProvider, } - testAccProviderV6 = providerserver.NewProtocol6WithError(New()) - testAccProvidersV6 = map[string]func() (tfprotov6.ProviderServer, error){ - "cloudstack": testAccProviderV6, + testAccMuxProvider = map[string]func() (tfprotov6.ProviderServer, error){ + "cloudstack": func() (tfprotov6.ProviderServer, error) { + ctx := context.Background() + + upgradedSdkServer, err := tf5to6server.UpgradeServer( + ctx, + Provider().GRPCProvider, + ) + + if err != nil { + return nil, err + } + + providers := []func() tfprotov6.ProviderServer{ + providerserver.NewProtocol6(New()), + func() tfprotov6.ProviderServer { + return upgradedSdkServer + }, + } + + muxServer, err := tf6muxserver.NewMuxServer(ctx, providers...) + + if err != nil { + return nil, err + } + + return muxServer.ProviderServer(), nil + }, } } @@ -65,35 +89,7 @@ func TestProvider_impl(t *testing.T) { func TestMuxServer(t *testing.T) { resource.Test(t, resource.TestCase{ - ProtoV6ProviderFactories: map[string]func() (tfprotov6.ProviderServer, error){ - "cloudstack": func() (tfprotov6.ProviderServer, error) { - ctx := context.Background() - - upgradedSdkServer, err := tf5to6server.UpgradeServer( - ctx, - Provider().GRPCProvider, - ) - - if err != nil { - return nil, err - } - - providers := []func() tfprotov6.ProviderServer{ - providerserver.NewProtocol6(New()), - func() tfprotov6.ProviderServer { - return upgradedSdkServer - }, - } - - muxServer, err := tf6muxserver.NewMuxServer(ctx, providers...) - - if err != nil { - return nil, err - } - - return muxServer.ProviderServer(), nil - }, - }, + ProtoV6ProviderFactories: testAccMuxProvider, Steps: []resource.TestStep{ { Config: testMuxServerConfig_conflict, diff --git a/cloudstack/resource_cloudstack_service_offering_test.go b/cloudstack/resource_cloudstack_service_offering_test.go index ae229f1b..b54de766 100644 --- a/cloudstack/resource_cloudstack_service_offering_test.go +++ b/cloudstack/resource_cloudstack_service_offering_test.go @@ -33,7 +33,7 @@ func TestAccCloudStackServiceOffering_basic(t *testing.T) { // var so cloudstack.ServiceOffering resource.Test(t, resource.TestCase{ - ProtoV6ProviderFactories: testAccProvidersV6, + ProtoV6ProviderFactories: testAccMuxProvider, Steps: []resource.TestStep{ { Config: fmt.Sprintf(` From 30f1b3f95f30cf1bfae5751693403a5d52a374b6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fa=CC=81bio=20Matavelli?= Date: Tue, 2 Jul 2024 18:43:03 +0100 Subject: [PATCH 5/7] chore: removes default iops --- cloudstack/resource_cloudstack_service_offering.go | 3 --- 1 file changed, 3 deletions(-) diff --git a/cloudstack/resource_cloudstack_service_offering.go b/cloudstack/resource_cloudstack_service_offering.go index b81668c2..c1aa90d4 100644 --- a/cloudstack/resource_cloudstack_service_offering.go +++ b/cloudstack/resource_cloudstack_service_offering.go @@ -619,9 +619,6 @@ func (r *resourceCloudstackServiceOffering) Create(ctx context.Context, req reso if !data.MaxIops.IsNull() && !data.MaxIops.IsUnknown() { p.SetMaxiops(data.MaxIops.ValueInt64()) } - } else { - p.SetMiniops(0) - p.SetMaxiops(0) } if !data.HypervisorSnapshotReserve.IsNull() && !data.HypervisorSnapshotReserve.IsUnknown() { From d5cd146f2e9dc7869e57ae1d54aaa6187c373b29 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fa=CC=81bio=20Matavelli?= Date: Thu, 4 Jul 2024 08:09:20 +0100 Subject: [PATCH 6/7] chore: revert service offerings --- cloudstack/provider.go | 1 + cloudstack/provider_v6.go | 4 +- .../resource_cloudstack_service_offering.go | 804 ++++-------------- ...source_cloudstack_service_offering_test.go | 104 ++- 4 files changed, 209 insertions(+), 704 deletions(-) diff --git a/cloudstack/provider.go b/cloudstack/provider.go index 85a68692..5aad7c55 100644 --- a/cloudstack/provider.go +++ b/cloudstack/provider.go @@ -126,6 +126,7 @@ func Provider() *schema.Provider { "cloudstack_disk_offering": resourceCloudStackDiskOffering(), "cloudstack_volume": resourceCloudStackVolume(), "cloudstack_zone": resourceCloudStackZone(), + "cloudstack_service_offering": resourceCloudStackServiceOffering(), "cloudstack_account": resourceCloudStackAccount(), "cloudstack_user": resourceCloudStackUser(), "cloudstack_domain": resourceCloudStackDomain(), diff --git a/cloudstack/provider_v6.go b/cloudstack/provider_v6.go index 7f5868bb..339e4b37 100644 --- a/cloudstack/provider_v6.go +++ b/cloudstack/provider_v6.go @@ -147,9 +147,7 @@ func (p *CloudstackProvider) ConfigValidators(ctx context.Context) []provider.Co } func (p *CloudstackProvider) Resources(ctx context.Context) []func() resource.Resource { - return []func() resource.Resource{ - NewCloudstackServiceOfferingResource, - } + return []func() resource.Resource{} } func (p *CloudstackProvider) DataSources(ctx context.Context) []func() datasource.DataSource { diff --git a/cloudstack/resource_cloudstack_service_offering.go b/cloudstack/resource_cloudstack_service_offering.go index c1aa90d4..170e05e0 100644 --- a/cloudstack/resource_cloudstack_service_offering.go +++ b/cloudstack/resource_cloudstack_service_offering.go @@ -20,732 +20,246 @@ package cloudstack import ( - "context" "fmt" + "log" - "github.com/hashicorp/terraform-plugin-framework-validators/boolvalidator" - "github.com/hashicorp/terraform-plugin-framework-validators/int64validator" - "github.com/hashicorp/terraform-plugin-framework-validators/stringvalidator" - "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/boolplanmodifier" - "github.com/hashicorp/terraform-plugin-framework/resource/schema/int64planmodifier" - "github.com/hashicorp/terraform-plugin-framework/resource/schema/listplanmodifier" - "github.com/hashicorp/terraform-plugin-framework/resource/schema/planmodifier" - "github.com/hashicorp/terraform-plugin-framework/resource/schema/stringdefault" - "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/apache/cloudstack-go/v2/cloudstack" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" ) -func NewCloudstackServiceOfferingResource() resource.Resource { - return &resourceCloudstackServiceOffering{} -} - -type resourceCloudstackServiceOffering struct { - ResourceWithConfigure -} - -type resourceCloudStackServiceOfferingModel struct { - Name types.String `tfsdk:"name"` - DisplayText types.String `tfsdk:"display_text"` - Customized types.Bool `tfsdk:"customized"` - CpuNumber types.Int64 `tfsdk:"cpu_number"` - CpuNumberMin types.Int64 `tfsdk:"cpu_number_min"` - CpuNumberMax types.Int64 `tfsdk:"cpu_number_max"` - CpuSpeed types.Int64 `tfsdk:"cpu_speed"` - Memory types.Int64 `tfsdk:"memory"` - MemoryMin types.Int64 `tfsdk:"memory_min"` - MemoryMax types.Int64 `tfsdk:"memory_max"` - HostTags types.String `tfsdk:"host_tags"` - NetworkRate types.Int64 `tfsdk:"network_rate"` - OfferHa types.Bool `tfsdk:"offer_ha"` - DynamicScaling types.Bool `tfsdk:"dynamic_scaling"` - LimitCpuUse types.Bool `tfsdk:"limit_cpu_use"` - Volatile types.Bool `tfsdk:"volatile"` - DeploymentPlanner types.String `tfsdk:"deployment_planner"` - ZoneId types.List `tfsdk:"zone_id"` - DiskOfferingId types.String `tfsdk:"disk_offering_id"` - StorageType types.String `tfsdk:"storage_type"` - ProvisioningType types.String `tfsdk:"provisioning_type"` - WriteCacheType types.String `tfsdk:"write_cache_type"` - QosType types.String `tfsdk:"qos_type"` - DiskReadRateBps types.Int64 `tfsdk:"disk_read_rate_bps"` - DiskWriteRateBps types.Int64 `tfsdk:"disk_write_rate_bps"` - DiskReadRateIops types.Int64 `tfsdk:"disk_read_rate_iops"` - DiskWriteRateIops types.Int64 `tfsdk:"disk_write_rate_iops"` - CustomIops types.Bool `tfsdk:"custom_iops"` - MinIops types.Int64 `tfsdk:"min_iops"` - MaxIops types.Int64 `tfsdk:"max_iops"` - HypervisorSnapshotReserve types.Int64 `tfsdk:"hypervisor_snapshot_reserve"` - RootDiskSize types.Int64 `tfsdk:"root_disk_size"` - StorageTags types.String `tfsdk:"storage_tags"` - Encrypt types.Bool `tfsdk:"encrypt"` - DiskOfferingStrictness types.Bool `tfsdk:"disk_offering_strictness"` - Id types.String `tfsdk:"id"` -} - -func (r *resourceCloudstackServiceOffering) Metadata(ctx context.Context, req resource.MetadataRequest, resp *resource.MetadataResponse) { - resp.TypeName = fmt.Sprintf("%s_service_offering", req.ProviderTypeName) -} - -func (r *resourceCloudstackServiceOffering) Schema(ctx context.Context, req resource.SchemaRequest, resp *resource.SchemaResponse) { - resp.Schema = schema.Schema{ - Attributes: map[string]schema.Attribute{ - "name": schema.StringAttribute{ - Description: "The name of the service offering", - Required: true, - }, - "display_text": schema.StringAttribute{ - Description: "The display text of the service offering", - Required: true, - }, - "customized": schema.BoolAttribute{ - Description: "Is the service offering customized", - Optional: true, - Computed: true, - PlanModifiers: []planmodifier.Bool{ - boolplanmodifier.RequiresReplace(), - }, - Default: booldefault.StaticBool(false), - Validators: []validator.Bool{ - boolvalidator.ConflictsWith(path.MatchRoot("cpu_number"), path.MatchRoot("memory")), - }, - }, - "cpu_number": schema.Int64Attribute{ +func resourceCloudStackServiceOffering() *schema.Resource { + return &schema.Resource{ + Create: resourceCloudStackServiceOfferingCreate, + Read: resourceCloudStackServiceOfferingRead, + Update: resourceCloudStackServiceOfferingUpdate, + Delete: resourceCloudStackServiceOfferingDelete, + Schema: map[string]*schema.Schema{ + "name": { + Type: schema.TypeString, + Required: true, + }, + "display_text": { + Type: schema.TypeString, + Required: true, + }, + "cpu_number": { Description: "Number of CPU cores", + Type: schema.TypeInt, Optional: true, - Computed: true, - PlanModifiers: []planmodifier.Int64{ - int64planmodifier.RequiresReplace(), - }, - Validators: []validator.Int64{ - int64validator.ConflictsWith(path.MatchRoot("cpu_number_min"), path.MatchRoot("cpu_number_max")), - }, - }, - "cpu_number_min": schema.Int64Attribute{ - Description: "Minimum number of CPU cores", - Optional: true, - Computed: true, - PlanModifiers: []planmodifier.Int64{ - int64planmodifier.RequiresReplace(), - }, - Validators: []validator.Int64{ - int64validator.AlsoRequires(path.MatchRoot("cpu_number_max")), - int64validator.AtMostSumOf(path.MatchRoot("cpu_number_max")), - int64validator.ConflictsWith(path.MatchRoot("cpu_number")), - }, - }, - "cpu_number_max": schema.Int64Attribute{ - Description: "Maximum number of CPU cores", - Optional: true, - Computed: true, - PlanModifiers: []planmodifier.Int64{ - int64planmodifier.RequiresReplace(), - }, - Validators: []validator.Int64{ - int64validator.AlsoRequires(path.MatchRoot("cpu_number_min")), - int64validator.AtLeastSumOf(path.MatchRoot("cpu_number_min")), - int64validator.ConflictsWith(path.MatchRoot("cpu_number")), - }, + ForceNew: true, }, - "cpu_speed": schema.Int64Attribute{ + "cpu_speed": { Description: "Speed of CPU in Mhz", - Required: true, - PlanModifiers: []planmodifier.Int64{ - int64planmodifier.RequiresReplace(), - }, - }, - "memory": schema.Int64Attribute{ - Description: "The total memory of the service offering in MB", + Type: schema.TypeInt, Optional: true, - Computed: true, - PlanModifiers: []planmodifier.Int64{ - int64planmodifier.RequiresReplace(), - }, - Validators: []validator.Int64{ - int64validator.ConflictsWith(path.MatchRoot("memory_min"), path.MatchRoot("memory_max")), - }, + ForceNew: true, }, - "memory_min": schema.Int64Attribute{ - Description: "Minimum memory of the service offering in MB", - Optional: true, - Computed: true, - PlanModifiers: []planmodifier.Int64{ - int64planmodifier.RequiresReplace(), - }, - Validators: []validator.Int64{ - int64validator.AlsoRequires(path.MatchRoot("memory_max")), - int64validator.AtMostSumOf(path.MatchRoot("memory_max")), - int64validator.ConflictsWith(path.MatchRoot("memory")), - }, - }, - "memory_max": schema.Int64Attribute{ - Description: "Maximum memory of the service offering in MB", - Optional: true, - Computed: true, - PlanModifiers: []planmodifier.Int64{ - int64planmodifier.RequiresReplace(), - }, - Validators: []validator.Int64{ - int64validator.AlsoRequires(path.MatchRoot("memory_min")), - int64validator.AtLeastSumOf(path.MatchRoot("memory_min")), - int64validator.ConflictsWith(path.MatchRoot("memory")), - }, - }, - "host_tags": schema.StringAttribute{ + "host_tags": { Description: "The host tag for this service offering", + Type: schema.TypeString, Optional: true, - Computed: true, - }, - "network_rate": schema.Int64Attribute{ - Description: "Data transfer rate in megabits per second", - Optional: true, - Computed: true, - PlanModifiers: []planmodifier.Int64{ - int64planmodifier.RequiresReplace(), - }, - }, - "offer_ha": schema.BoolAttribute{ - Description: "The HA for the service offering", - Optional: true, - Computed: true, - PlanModifiers: []planmodifier.Bool{ - boolplanmodifier.RequiresReplace(), - }, - Default: booldefault.StaticBool(false), - }, - "dynamic_scaling": schema.BoolAttribute{ - Description: "Enable dynamic scaling of the service offering", - Optional: true, - Computed: true, - PlanModifiers: []planmodifier.Bool{ - boolplanmodifier.RequiresReplace(), - }, - Default: booldefault.StaticBool(false), }, - "limit_cpu_use": schema.BoolAttribute{ + "limit_cpu_use": { Description: "Restrict the CPU usage to committed service offering", + Type: schema.TypeBool, Optional: true, - Computed: true, - PlanModifiers: []planmodifier.Bool{ - boolplanmodifier.RequiresReplace(), - }, - Default: booldefault.StaticBool(false), + ForceNew: true, + Default: false, }, - "volatile": schema.BoolAttribute{ - Description: "Service offering is volatile", - Optional: true, - Computed: true, - PlanModifiers: []planmodifier.Bool{ - boolplanmodifier.RequiresReplace(), - }, - Default: booldefault.StaticBool(false), - }, - "deployment_planner": schema.StringAttribute{ - Description: "The deployment planner for the service offering", - Optional: true, - Computed: true, - PlanModifiers: []planmodifier.String{ - stringplanmodifier.RequiresReplace(), - }, - Validators: []validator.String{ - stringvalidator.OneOf("FirstFitPlanner", "UserDispersingPlanner", "UserConcentratedPodPlanner", "ImplicitDedicationPlanner", "BareMetalPlanner"), - }, - }, - "zone_id": schema.ListAttribute{ - Description: "The ID of the zone(s)", + "memory": { + Description: "The total memory of the service offering in MB", + Type: schema.TypeInt, Optional: true, - Computed: true, - ElementType: types.StringType, - PlanModifiers: []planmodifier.List{ - listplanmodifier.RequiresReplace(), - }, + ForceNew: true, }, - "disk_offering_id": schema.StringAttribute{ - Description: "The ID of the disk offering", + "offer_ha": { + Description: "The HA for the service offering", + Type: schema.TypeBool, Optional: true, - Computed: true, - PlanModifiers: []planmodifier.String{ - stringplanmodifier.RequiresReplace(), - }, + ForceNew: true, + Default: false, }, - "storage_type": schema.StringAttribute{ + "storage_type": { Description: "The storage type of the service offering. Values are local and shared", + Type: schema.TypeString, Optional: true, - Computed: true, - PlanModifiers: []planmodifier.String{ - stringplanmodifier.RequiresReplace(), - }, - Validators: []validator.String{ - stringvalidator.OneOf("local", "shared"), - stringvalidator.ConflictsWith(path.MatchRoot("disk_offering_id")), - }, - Default: stringdefault.StaticString("shared"), - }, - "provisioning_type": schema.StringAttribute{ - Description: "The provisioning type of the service offering", - Optional: true, - Computed: true, - PlanModifiers: []planmodifier.String{ - stringplanmodifier.RequiresReplace(), - }, - Validators: []validator.String{ - stringvalidator.OneOf("thin", "sparse", "fat"), - stringvalidator.ConflictsWith(path.MatchRoot("disk_offering_id")), - }, - Default: stringdefault.StaticString("thin"), - }, - "write_cache_type": schema.StringAttribute{ - Description: "The write cache type of the service offering", - Optional: true, - Computed: true, - PlanModifiers: []planmodifier.String{ - stringplanmodifier.RequiresReplace(), - }, - Validators: []validator.String{ - stringvalidator.OneOf("none", "writeback", "writethrough"), - stringvalidator.ConflictsWith(path.MatchRoot("disk_offering_id")), - }, - Default: stringdefault.StaticString("none"), - }, - "qos_type": schema.StringAttribute{ - Description: "The QoS type of the service offering", - Optional: true, - Computed: true, - PlanModifiers: []planmodifier.String{ - stringplanmodifier.RequiresReplace(), - }, - Validators: []validator.String{ - stringvalidator.OneOf("none", "hypervisor", "storage"), - stringvalidator.ConflictsWith(path.MatchRoot("disk_offering_id")), - }, - Default: stringdefault.StaticString("none"), - }, - "disk_read_rate_bps": schema.Int64Attribute{ - Description: "The read rate of the service offering", - Optional: true, - Computed: true, - PlanModifiers: []planmodifier.Int64{ - int64planmodifier.RequiresReplace(), - }, - Validators: []validator.Int64{ - int64validator.ConflictsWith(path.MatchRoot("disk_offering_id")), - }, - }, - "disk_write_rate_bps": schema.Int64Attribute{ - Description: "The write rate of the service offering", - Optional: true, - Computed: true, - PlanModifiers: []planmodifier.Int64{ - int64planmodifier.RequiresReplace(), - }, - Validators: []validator.Int64{ - int64validator.ConflictsWith(path.MatchRoot("disk_offering_id")), - }, - }, - "disk_read_rate_iops": schema.Int64Attribute{ - Description: "The read rate of the service offering in IOPS", - Optional: true, - Computed: true, - PlanModifiers: []planmodifier.Int64{ - int64planmodifier.RequiresReplace(), - }, - Validators: []validator.Int64{ - int64validator.ConflictsWith(path.MatchRoot("disk_offering_id")), - }, - }, - "disk_write_rate_iops": schema.Int64Attribute{ - Description: "The write rate of the service offering in IOPS", - Optional: true, - Computed: true, - PlanModifiers: []planmodifier.Int64{ - int64planmodifier.RequiresReplace(), - }, - Validators: []validator.Int64{ - int64validator.ConflictsWith(path.MatchRoot("disk_offering_id")), - }, - }, - "custom_iops": schema.BoolAttribute{ - Description: "Custom IOPS", - Optional: true, - Computed: true, - PlanModifiers: []planmodifier.Bool{ - boolplanmodifier.RequiresReplace(), - }, - Default: booldefault.StaticBool(false), - Validators: []validator.Bool{ - boolvalidator.ConflictsWith(path.MatchRoot("disk_offering_id")), - boolvalidator.ConflictsWith(path.MatchRoot("disk_read_rate_iops"), path.MatchRoot("disk_write_rate_iops")), - boolvalidator.ConflictsWith(path.MatchRoot("disk_read_rate_bps"), path.MatchRoot("disk_write_rate_bps")), - }, - }, - "min_iops": schema.Int64Attribute{ - Description: "The minimum IOPS of the service offering", - Optional: true, - Computed: true, - PlanModifiers: []planmodifier.Int64{ - int64planmodifier.RequiresReplace(), - }, - Validators: []validator.Int64{ - int64validator.ConflictsWith(path.MatchRoot("disk_offering_id")), - int64validator.ConflictsWith(path.MatchRoot("disk_read_rate_iops"), path.MatchRoot("disk_write_rate_iops")), - int64validator.ConflictsWith(path.MatchRoot("disk_read_rate_bps"), path.MatchRoot("disk_write_rate_bps")), - int64validator.ConflictsWith(path.MatchRoot("custom_iops")), - }, - }, - "max_iops": schema.Int64Attribute{ - Description: "The maximum IOPS of the service offering", - Optional: true, - Computed: true, - PlanModifiers: []planmodifier.Int64{ - int64planmodifier.RequiresReplace(), - }, - Validators: []validator.Int64{ - int64validator.ConflictsWith(path.MatchRoot("disk_offering_id")), - int64validator.ConflictsWith(path.MatchRoot("disk_read_rate_iops"), path.MatchRoot("disk_write_rate_iops")), - int64validator.ConflictsWith(path.MatchRoot("disk_read_rate_bps"), path.MatchRoot("disk_write_rate_bps")), - int64validator.ConflictsWith(path.MatchRoot("custom_iops")), - }, - }, - "hypervisor_snapshot_reserve": schema.Int64Attribute{ - Description: "The hypervisor snapshot reserve of the service offering", - Optional: true, - Computed: true, - PlanModifiers: []planmodifier.Int64{ - int64planmodifier.RequiresReplace(), - }, - Validators: []validator.Int64{ - int64validator.ConflictsWith(path.MatchRoot("disk_offering_id")), - int64validator.ConflictsWith(path.MatchRoot("disk_read_rate_iops"), path.MatchRoot("disk_write_rate_iops")), - int64validator.ConflictsWith(path.MatchRoot("disk_read_rate_bps"), path.MatchRoot("disk_write_rate_bps")), - }, - }, - "root_disk_size": schema.Int64Attribute{ - Description: "The size of the root disk in GB", - Optional: true, - Computed: true, - PlanModifiers: []planmodifier.Int64{ - int64planmodifier.RequiresReplace(), - }, - Validators: []validator.Int64{ - int64validator.ConflictsWith(path.MatchRoot("disk_offering_id")), - }, - }, - "storage_tags": schema.StringAttribute{ - Description: "The storage tags of the service offering", - Optional: true, - Computed: true, - PlanModifiers: []planmodifier.String{ - stringplanmodifier.RequiresReplace(), - }, - Validators: []validator.String{ - stringvalidator.ConflictsWith(path.MatchRoot("disk_offering_id")), - }, - }, - "encrypt": schema.BoolAttribute{ - Description: "Encrypt the service offering storage", - Optional: true, - Computed: true, - PlanModifiers: []planmodifier.Bool{ - boolplanmodifier.RequiresReplace(), - }, - Default: booldefault.StaticBool(false), - Validators: []validator.Bool{ - boolvalidator.ConflictsWith(path.MatchRoot("disk_offering_id")), - }, - }, - "disk_offering_strictness": schema.BoolAttribute{ - Description: "Disk offering strictness", - Optional: true, - Computed: true, - PlanModifiers: []planmodifier.Bool{ - boolplanmodifier.RequiresReplace(), - }, - Default: booldefault.StaticBool(false), - }, - "id": schema.StringAttribute{ - Description: "The ID of the service offering", - Computed: true, - PlanModifiers: []planmodifier.String{ - stringplanmodifier.UseStateForUnknown(), + ForceNew: true, + Default: "shared", + ValidateFunc: func(val interface{}, key string) (warns []string, errs []error) { + v := val.(string) + + if v == "local" || v == "shared" { + return + } + + errs = append(errs, fmt.Errorf("storage type should be either local or shared, got %s", v)) + + return }, }, }, } } -func (r *resourceCloudstackServiceOffering) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) { - var data resourceCloudStackServiceOfferingModel - - resp.Diagnostics.Append(req.Plan.Get(ctx, &data)...) - - if resp.Diagnostics.HasError() { - return - } - - p := r.client.ServiceOffering.NewCreateServiceOfferingParams(data.DisplayText.ValueString(), data.Name.ValueString()) - - if !data.Customized.ValueBool() { - if !data.CpuNumber.IsNull() && !data.CpuNumber.IsUnknown() { - p.SetCpunumber(int(data.CpuNumber.ValueInt64())) - } - - if !data.Memory.IsNull() && !data.Memory.IsUnknown() { - p.SetMemory(int(data.Memory.ValueInt64())) - } +func resourceCloudStackServiceOfferingCreate(d *schema.ResourceData, meta interface{}) error { + cs := meta.(*cloudstack.CloudStackClient) + name := d.Get("name").(string) + display_text := d.Get("display_text").(string) - data.CpuNumberMax = types.Int64Null() - data.CpuNumberMin = types.Int64Null() - data.MemoryMax = types.Int64Null() - data.MemoryMin = types.Int64Null() - } else { - if !data.CpuNumberMin.IsNull() && !data.CpuNumberMin.IsUnknown() { - p.SetMincpunumber(int(data.CpuNumberMin.ValueInt64())) - } - - if !data.CpuNumberMax.IsNull() && !data.CpuNumberMax.IsUnknown() { - p.SetMaxcpunumber(int(data.CpuNumberMax.ValueInt64())) - } - - if !data.MemoryMin.IsNull() && !data.MemoryMin.IsUnknown() { - p.SetMinmemory(int(data.MemoryMin.ValueInt64())) - } - - if !data.MemoryMax.IsNull() && !data.MemoryMax.IsUnknown() { - p.SetMaxmemory(int(data.MemoryMax.ValueInt64())) - } - - data.CpuNumber = types.Int64Null() - data.Memory = types.Int64Null() - } - - if !data.CpuSpeed.IsNull() && !data.CpuSpeed.IsUnknown() { - p.SetCpuspeed(int(data.CpuSpeed.ValueInt64())) + // Create a new parameter struct + p := cs.ServiceOffering.NewCreateServiceOfferingParams(display_text, name) + if v, ok := d.GetOk("cpu_number"); ok { + p.SetCpunumber(v.(int)) } - if !data.HostTags.IsNull() && !data.HostTags.IsUnknown() { - p.SetHosttags(data.HostTags.ValueString()) - } else { - data.HostTags = types.StringNull() + if v, ok := d.GetOk("cpu_speed"); ok { + p.SetCpuspeed(v.(int)) } - if !data.NetworkRate.IsNull() && !data.NetworkRate.IsUnknown() { - p.SetNetworkrate(int(data.NetworkRate.ValueInt64())) - } else { - data.NetworkRate = types.Int64Null() + if v, ok := d.GetOk("host_tags"); ok { + p.SetHosttags(v.(string)) } - if !data.OfferHa.IsNull() && !data.OfferHa.IsUnknown() { - p.SetOfferha(data.OfferHa.ValueBool()) + if v, ok := d.GetOk("limit_cpu_use"); ok { + p.SetLimitcpuuse(v.(bool)) } - if !data.DynamicScaling.IsNull() && !data.DynamicScaling.IsUnknown() { - p.SetDynamicscalingenabled(data.DynamicScaling.ValueBool()) + if v, ok := d.GetOk("memory"); ok { + p.SetMemory(v.(int)) } - if !data.LimitCpuUse.IsNull() && !data.LimitCpuUse.IsUnknown() { - p.SetLimitcpuuse(data.LimitCpuUse.ValueBool()) + if v, ok := d.GetOk("offer_ha"); ok { + p.SetOfferha(v.(bool)) } - if !data.Volatile.IsNull() && !data.Volatile.IsUnknown() { - p.SetIsvolatile(data.Volatile.ValueBool()) + if v, ok := d.GetOk("storage_type"); ok { + p.SetStoragetype(v.(string)) } - if !data.DeploymentPlanner.IsNull() && !data.DeploymentPlanner.IsUnknown() { - p.SetDeploymentplanner(data.DeploymentPlanner.ValueString()) - } else { - data.DeploymentPlanner = types.StringNull() - } + log.Printf("[DEBUG] Creating Service Offering %s", name) + s, err := cs.ServiceOffering.CreateServiceOffering(p) - if !data.ZoneId.IsNull() && !data.ZoneId.IsUnknown() { - zoneIds := make([]string, 0, len(data.ZoneId.Elements())) - diags := data.ZoneId.ElementsAs(ctx, &zoneIds, false) - if diags.HasError() { - resp.Diagnostics.Append(diags...) - return - } - p.SetZoneid(zoneIds) - } else { - data.ZoneId = types.ListNull(types.StringType) + if err != nil { + return err } - if !data.DiskOfferingId.IsNull() && !data.DiskOfferingId.IsUnknown() { - p.SetDiskofferingid(data.DiskOfferingId.ValueString()) - } else { - p.SetDiskofferingid("") - data.DiskOfferingId = types.StringNull() + log.Printf("[DEBUG] Service Offering %s successfully created", name) + d.SetId(s.Id) - if !data.StorageType.IsNull() && !data.StorageType.IsUnknown() { - p.SetStoragetype(data.StorageType.ValueString()) - } else { - data.StorageType = types.StringNull() - } + return resourceCloudStackServiceOfferingRead(d, meta) +} - if !data.ProvisioningType.IsNull() && !data.ProvisioningType.IsUnknown() { - p.SetProvisioningtype(data.ProvisioningType.ValueString()) - } +func resourceCloudStackServiceOfferingRead(d *schema.ResourceData, meta interface{}) error { + cs := meta.(*cloudstack.CloudStackClient) + log.Printf("[DEBUG] Retrieving Service Offering %s", d.Get("name").(string)) - if !data.WriteCacheType.IsNull() && !data.WriteCacheType.IsUnknown() { - p.SetCachemode(data.WriteCacheType.ValueString()) - } + // Get the Service Offering details + s, count, err := cs.ServiceOffering.GetServiceOfferingByName(d.Get("name").(string)) - if data.QosType.ValueString() == "hypervisor" { - if !data.DiskReadRateBps.IsNull() && !data.DiskReadRateBps.IsUnknown() { - p.SetBytesreadrate(data.DiskReadRateBps.ValueInt64()) - } - - if !data.DiskWriteRateBps.IsNull() && !data.DiskWriteRateBps.IsUnknown() { - p.SetByteswriterate(data.DiskWriteRateBps.ValueInt64()) - } - - if !data.DiskReadRateIops.IsNull() && !data.DiskReadRateIops.IsUnknown() { - p.SetIopsreadrate(data.DiskReadRateIops.ValueInt64()) - } - - if !data.DiskWriteRateIops.IsNull() && !data.DiskWriteRateIops.IsUnknown() { - p.SetIopswriterate(data.DiskWriteRateIops.ValueInt64()) - } - } else if data.QosType.ValueString() == "storage" { - p.SetCustomizediops(data.CustomIops.ValueBool()) - - if !data.CustomIops.ValueBool() { - if !data.MinIops.IsNull() && !data.MinIops.IsUnknown() { - p.SetMiniops(data.MinIops.ValueInt64()) - } - - if !data.MaxIops.IsNull() && !data.MaxIops.IsUnknown() { - p.SetMaxiops(data.MaxIops.ValueInt64()) - } - } - - if !data.HypervisorSnapshotReserve.IsNull() && !data.HypervisorSnapshotReserve.IsUnknown() { - p.SetHypervisorsnapshotreserve(int(data.HypervisorSnapshotReserve.ValueInt64())) - } - } else { - data.DiskReadRateBps = types.Int64Null() - data.DiskWriteRateBps = types.Int64Null() - data.DiskReadRateIops = types.Int64Null() - data.DiskWriteRateIops = types.Int64Null() - data.MinIops = types.Int64Null() - data.MaxIops = types.Int64Null() - data.HypervisorSnapshotReserve = types.Int64Null() + if err != nil { + if count == 0 { + log.Printf("[DEBUG] Service Offering %s does no longer exist", d.Get("name").(string)) + d.SetId("") + return nil } + return err } - if !data.RootDiskSize.IsNull() && !data.RootDiskSize.IsUnknown() { - p.SetRootdisksize(data.RootDiskSize.ValueInt64()) - } else { - data.RootDiskSize = types.Int64Null() + d.SetId(s.Id) + + fields := map[string]interface{}{ + "name": s.Name, + "display_text": s.Displaytext, + "cpu_number": s.Cpunumber, + "cpu_speed": s.Cpuspeed, + "host_tags": s.Hosttags, + "limit_cpu_use": s.Limitcpuuse, + "memory": s.Memory, + "offer_ha": s.Offerha, + "storage_type": s.Storagetype, } - if !data.StorageTags.IsNull() && !data.StorageTags.IsUnknown() { - p.SetTags(data.StorageTags.ValueString()) - } else { - data.StorageTags = types.StringNull() + for k, v := range fields { + d.Set(k, v) } - if !data.Encrypt.IsNull() && !data.Encrypt.IsUnknown() { - p.SetEncryptroot(data.Encrypt.ValueBool()) - } + return nil +} - if !data.DiskOfferingStrictness.IsNull() && !data.DiskOfferingStrictness.IsUnknown() { - p.SetDiskofferingstrictness(data.DiskOfferingStrictness.ValueBool()) - } +func resourceCloudStackServiceOfferingUpdate(d *schema.ResourceData, meta interface{}) error { + cs := meta.(*cloudstack.CloudStackClient) - s, err := r.client.ServiceOffering.CreateServiceOffering(p) + name := d.Get("name").(string) - if err != nil { - resp.Diagnostics.AddError( - "Error creating service offering", - fmt.Sprintf("Error while trying to create service offering: %s", err), - ) - return - } + // Check if the name is changed and if so, update the service offering + if d.HasChange("name") { + log.Printf("[DEBUG] Name changed for %s, starting update", name) - data.Id = types.StringValue(s.Id) + // Create a new parameter struct + p := cs.ServiceOffering.NewUpdateServiceOfferingParams(d.Id()) - resp.Diagnostics.Append(resp.State.Set(ctx, &data)...) -} + // Set the new name + p.SetName(d.Get("name").(string)) -func (r *resourceCloudstackServiceOffering) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) { - var data resourceCloudStackServiceOfferingModel + // Update the name + _, err := cs.ServiceOffering.UpdateServiceOffering(p) + if err != nil { + return fmt.Errorf( + "Error updating the name for service offering %s: %s", name, err) + } - resp.Diagnostics.Append(req.State.Get(ctx, &data)...) + } - s, count, err := r.client.ServiceOffering.GetServiceOfferingByID(data.Id.ValueString()) + // Check if the display text is changed and if so, update seervice offering + if d.HasChange("display_text") { + log.Printf("[DEBUG] Display text changed for %s, starting update", name) - if count == 0 { - resp.State.RemoveResource(ctx) - return - } + // Create a new parameter struct + p := cs.ServiceOffering.NewUpdateServiceOfferingParams(d.Id()) - if err != nil { - resp.Diagnostics.AddError( - "Error reading service offering", - fmt.Sprintf("Error while trying to read service offering: %s", err), - ) - return - } + // Set the new display text + p.SetName(d.Get("display_text").(string)) - data.Id = types.StringValue(s.Id) - data.Name = types.StringValue(s.Name) - data.DisplayText = types.StringValue(s.Displaytext) - data.CpuNumber = types.Int64Value(int64(s.Cpunumber)) - data.CpuSpeed = types.Int64Value(int64(s.Cpuspeed)) - data.HostTags = types.StringValue(s.Hosttags) - data.LimitCpuUse = types.BoolValue(s.Limitcpuuse) - data.Memory = types.Int64Value(int64(s.Memory)) - data.OfferHa = types.BoolValue(s.Offerha) - data.StorageType = types.StringValue(s.Storagetype) - - resp.Diagnostics.Append(resp.State.Set(ctx, &data)...) -} + // Update the display text + _, err := cs.ServiceOffering.UpdateServiceOffering(p) + if err != nil { + return fmt.Errorf( + "Error updating the display text for service offering %s: %s", name, err) + } -func (r *resourceCloudstackServiceOffering) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) { - var data resourceCloudStackServiceOfferingModel + } - resp.Diagnostics.Append(req.Plan.Get(ctx, &data)...) + if d.HasChange("host_tags") { + log.Printf("[DEBUG] Host tags changed for %s, starting update", name) - p := r.client.ServiceOffering.NewUpdateServiceOfferingParams(data.Id.ValueString()) + // Create a new parameter struct + p := cs.ServiceOffering.NewUpdateServiceOfferingParams(d.Id()) - p.SetName(data.Name.ValueString()) - p.SetDisplaytext(data.DisplayText.ValueString()) - p.SetHosttags(data.HostTags.ValueString()) - p.SetStoragetags(data.HostTags.ValueString()) + // Set the new host tags + p.SetHosttags(d.Get("host_tags").(string)) - _, err := r.client.ServiceOffering.UpdateServiceOffering(p) + // Update the host tags + _, err := cs.ServiceOffering.UpdateServiceOffering(p) + if err != nil { + return fmt.Errorf( + "Error updating the host tags for service offering %s: %s", name, err) + } - if err != nil { - resp.Diagnostics.AddError( - "Error updating service offering", - fmt.Sprintf("Error while trying to update service offering: %s", err), - ) - return } - resp.Diagnostics.Append(resp.State.Set(ctx, &data)...) + return resourceCloudStackServiceOfferingRead(d, meta) } -func (r *resourceCloudstackServiceOffering) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) { - var data resourceCloudStackServiceOfferingModel +func resourceCloudStackServiceOfferingDelete(d *schema.ResourceData, meta interface{}) error { + cs := meta.(*cloudstack.CloudStackClient) - resp.Diagnostics.Append(req.State.Get(ctx, &data)...) - - p := r.client.ServiceOffering.NewDeleteServiceOfferingParams(data.Id.ValueString()) - _, err := r.client.ServiceOffering.DeleteServiceOffering(p) + // Create a new parameter struct + p := cs.ServiceOffering.NewDeleteServiceOfferingParams(d.Id()) + _, err := cs.ServiceOffering.DeleteServiceOffering(p) if err != nil { - resp.Diagnostics.AddError( - "Error deleting service offering", - fmt.Sprintf("Error while trying to delete service offering: %s", err), - ) - return + return fmt.Errorf("Error deleting Service Offering: %s", err) } -} -func (r *resourceCloudstackServiceOffering) ImportState(ctx context.Context, req resource.ImportStateRequest, resp *resource.ImportStateResponse) { - resource.ImportStatePassthroughID(ctx, path.Root("id"), req, resp) + return nil } diff --git a/cloudstack/resource_cloudstack_service_offering_test.go b/cloudstack/resource_cloudstack_service_offering_test.go index b54de766..e5bbcdaa 100644 --- a/cloudstack/resource_cloudstack_service_offering_test.go +++ b/cloudstack/resource_cloudstack_service_offering_test.go @@ -23,71 +23,63 @@ import ( "fmt" "testing" - "github.com/hashicorp/terraform-plugin-testing/helper/acctest" - "github.com/hashicorp/terraform-plugin-testing/helper/resource" + "github.com/apache/cloudstack-go/v2/cloudstack" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource" + "github.com/hashicorp/terraform-plugin-sdk/v2/terraform" ) func TestAccCloudStackServiceOffering_basic(t *testing.T) { - // ctx := context.Background() - rName := acctest.RandomWithPrefix("service_offering") - - // var so cloudstack.ServiceOffering + var so cloudstack.ServiceOffering resource.Test(t, resource.TestCase{ - ProtoV6ProviderFactories: testAccMuxProvider, + PreCheck: func() { testAccPreCheck(t) }, + Providers: testAccProviders, Steps: []resource.TestStep{ { - Config: fmt.Sprintf(` - resource "cloudstack_service_offering" "test" { - name = "%s" - display_text = "Test" - cpu_number = 2 - cpu_speed = 2200 - memory = 8096 - }`, rName), - Check: resource.ComposeTestCheckFunc( - resource.TestCheckResourceAttr("cloudstack_service_offering.test", "cpu_number", "2"), - resource.TestCheckResourceAttr("cloudstack_service_offering.test", "cpu_speed", "2200"), - resource.TestCheckResourceAttr("cloudstack_service_offering.test", "memory", "8096"), - ), - }, - { - ResourceName: "cloudstack_service_offering.test", - ImportState: true, - ImportStateVerify: true, - ImportStateVerifyIgnore: []string{ - "custom_iops", - "customized", - "disk_offering_strictness", - "dynamic_scaling", - "encrypt", - "host_tags", - "provisioning_type", - "qos_type", - "volatile", - "write_cache_type", - }, - }, - { - Config: fmt.Sprintf(` - resource "cloudstack_service_offering" "test2" { - name = "%s" - display_text = "Test" - customized = true - cpu_number_min = 2 - cpu_number_max = 4 - memory_min = 8096 - memory_max = 16384 - cpu_speed = 2200 - }`, rName), + Config: testAccCloudStackServiceOffering_basic, Check: resource.ComposeTestCheckFunc( - // testAccCheckCloudStackServiceOfferingExists("cloudstack_service_offering.test2", &so), - resource.TestCheckResourceAttr("cloudstack_service_offering.test2", "cpu_number_min", "2"), - resource.TestCheckResourceAttr("cloudstack_service_offering.test2", "cpu_number_max", "4"), - resource.TestCheckResourceAttr("cloudstack_service_offering.test2", "memory_min", "8096"), - resource.TestCheckResourceAttr("cloudstack_service_offering.test2", "memory_max", "16384"), - resource.TestCheckResourceAttr("cloudstack_service_offering.test2", "cpu_speed", "2200"), + testAccCheckCloudStackServiceOfferingExists("cloudstack_service_offering.test1", &so), + resource.TestCheckResourceAttr("cloudstack_service_offering.test1", "cpu_number", "2"), + resource.TestCheckResourceAttr("cloudstack_service_offering.test1", "cpu_speed", "2200"), + resource.TestCheckResourceAttr("cloudstack_service_offering.test1", "memory", "8096"), ), }, }, }) } + +const testAccCloudStackServiceOffering_basic = ` +resource "cloudstack_service_offering" "test1" { + name = "service_offering_1" + display_text = "Test" + cpu_number = 2 + cpu_speed = 2200 + memory = 8096 +} +` + +func testAccCheckCloudStackServiceOfferingExists(n string, so *cloudstack.ServiceOffering) resource.TestCheckFunc { + return func(s *terraform.State) error { + rs, ok := s.RootModule().Resources[n] + if !ok { + return fmt.Errorf("Not found: %s", n) + } + + if rs.Primary.ID == "" { + return fmt.Errorf("No service offering ID is set") + } + + cs := testAccProvider.Meta().(*cloudstack.CloudStackClient) + resp, _, err := cs.ServiceOffering.GetServiceOfferingByID(rs.Primary.ID) + if err != nil { + return err + } + + if resp.Id != rs.Primary.ID { + return fmt.Errorf("Service offering not found") + } + + *so = *resp + + return nil + } +} From dd3bfab68e07b12542988426591908085ccb96b8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fa=CC=81bio=20Matavelli?= Date: Thu, 4 Jul 2024 11:44:28 +0100 Subject: [PATCH 7/7] chore: revert tests --- cloudstack/data_source_cloudstack_service_offering_test.go | 7 ++----- cloudstack/provider_test.go | 2 +- cloudstack/resource_cloudstack_service_offering_test.go | 4 ++-- 3 files changed, 5 insertions(+), 8 deletions(-) diff --git a/cloudstack/data_source_cloudstack_service_offering_test.go b/cloudstack/data_source_cloudstack_service_offering_test.go index 81574849..afe94d5e 100644 --- a/cloudstack/data_source_cloudstack_service_offering_test.go +++ b/cloudstack/data_source_cloudstack_service_offering_test.go @@ -30,8 +30,8 @@ func TestAccServiceOfferingDataSource_basic(t *testing.T) { datasourceName := "data.cloudstack_service_offering.service-offering-data-source" resource.Test(t, resource.TestCase{ - PreCheck: func() { testAccPreCheck(t) }, - ProtoV6ProviderFactories: testAccMuxProvider, + PreCheck: func() { testAccPreCheck(t) }, + Providers: testAccProviders, Steps: []resource.TestStep{ { Config: testServiceOfferingDataSourceConfig_basic, @@ -47,9 +47,6 @@ const testServiceOfferingDataSourceConfig_basic = ` resource "cloudstack_service_offering" "service-offering-resource" { name = "TestServiceUpdate" display_text = "DisplayService" - cpu_number = 2 - cpu_speed = 2200 - memory = 8096 } data "cloudstack_service_offering" "service-offering-data-source" { diff --git a/cloudstack/provider_test.go b/cloudstack/provider_test.go index 6792df5a..fb868e4b 100644 --- a/cloudstack/provider_test.go +++ b/cloudstack/provider_test.go @@ -104,7 +104,7 @@ func TestMuxServer(t *testing.T) { const testMuxServerConfig_basic = ` resource "cloudstack_zone" "zone_resource"{ - name = "TestZone1" + name = "TestZone" dns1 = "8.8.8.8" internal_dns1 = "172.20.0.1" network_type = "Advanced" diff --git a/cloudstack/resource_cloudstack_service_offering_test.go b/cloudstack/resource_cloudstack_service_offering_test.go index e5bbcdaa..fc29628c 100644 --- a/cloudstack/resource_cloudstack_service_offering_test.go +++ b/cloudstack/resource_cloudstack_service_offering_test.go @@ -24,8 +24,8 @@ import ( "testing" "github.com/apache/cloudstack-go/v2/cloudstack" - "github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource" - "github.com/hashicorp/terraform-plugin-sdk/v2/terraform" + "github.com/hashicorp/terraform-plugin-testing/helper/resource" + "github.com/hashicorp/terraform-plugin-testing/terraform" ) func TestAccCloudStackServiceOffering_basic(t *testing.T) {