diff --git a/docs/data-sources/sqlserverflex_flavors.md b/docs/data-sources/sqlserverflex_flavors.md new file mode 100644 index 000000000..de13eed00 --- /dev/null +++ b/docs/data-sources/sqlserverflex_flavors.md @@ -0,0 +1,75 @@ +--- +# generated by https://github.com/hashicorp/terraform-plugin-docs +page_title: "stackit_sqlserverflex_flavors Data Source - stackit" +subcategory: "" +description: |- + SqlserverFlex flavors data source schema +--- + +# stackit_sqlserverflex_flavors (Data Source) + +SqlserverFlex flavors data source schema + +## Example Usage + +```terraform +data "stackit_sqlserverflex_flavors" "example" { + project_id = "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx" + // region taken from provider +} + +// example usage with instance +resource "stackit_sqlserverflex_instance" "example" { + project_id = "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx" + name = "example" + flavor_id = one([for f in data.stackit_sqlserverflex_flavors.example.flavors : f.id if f.cpu == 4 && f.memory == 16]) +} +``` + + +## Schema + +### Required + +- `project_id` (String) The project ID. + +### Optional + +- `region` (String) SqlserverFlex flavors data source region. If undefined the providers region is used. +- `timeouts` (Attributes) (see [below for nested schema](#nestedatt--timeouts)) + +### Read-Only + +- `flavors` (Attributes List) A list of available flavors. (see [below for nested schema](#nestedatt--flavors)) +- `id` (String) Terraform's internal data source ID, structured as "`project_id`,`region`" + + +### Nested Schema for `timeouts` + +Optional: + +- `read` (String) A string that can be [parsed as a duration](https://pkg.go.dev/time#ParseDuration) consisting of numbers and unit suffixes, such as "30s" or "2h45m". Valid time units are "s" (seconds), "m" (minutes), "h" (hours). + + + +### Nested Schema for `flavors` + +Read-Only: + +- `cpu` (Number) The CPU count of the instance. +- `description` (String) Description of the flavor. +- `id` (String) ID of the flavor. +- `max_gb` (Number) Maximum storage, which can be ordered for the flavor in Gigabyte. +- `memory` (Number) The memory (Gibibyte) of the instance. +- `min_gb` (Number) Minimum storage, which is required to order in Gigabyte. +- `node_type` (String) Defines the node type (either single or HA). +- `storage_classes` (Attributes List) ⚠️TODO, also nested attr descriptions are freestyled (see [below for nested schema](#nestedatt--flavors--storage_classes)) + + +### Nested Schema for `flavors.storage_classes` + +Read-Only: + +- `class` (String) Class of the instance. +- `max_io_per_sec` (Number) Maximum I/O per second. +- `max_through_in_mb` (Number) Maximum throughput in Megabyte. diff --git a/examples/data-sources/stackit_sqlserverflex_flavors/data-source.tf b/examples/data-sources/stackit_sqlserverflex_flavors/data-source.tf new file mode 100644 index 000000000..0e4d09b19 --- /dev/null +++ b/examples/data-sources/stackit_sqlserverflex_flavors/data-source.tf @@ -0,0 +1,12 @@ +data "stackit_sqlserverflex_flavors" "example" { + project_id = "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx" + // region taken from provider +} + +// example usage with instance +resource "stackit_sqlserverflex_instance" "example" { + project_id = "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx" + name = "example" + flavor_id = one([for f in data.stackit_sqlserverflex_flavors.example.flavors : f.id if f.cpu == 4 && f.memory == 16]) +} + diff --git a/stackit/internal/services/sqlserverflex/flavors/datasource.go b/stackit/internal/services/sqlserverflex/flavors/datasource.go new file mode 100644 index 000000000..20f40bd3a --- /dev/null +++ b/stackit/internal/services/sqlserverflex/flavors/datasource.go @@ -0,0 +1,257 @@ +package flavors + +import ( + "context" + "fmt" + "slices" + "strings" + + "github.com/hashicorp/terraform-plugin-framework-timeouts/datasource/timeouts" + "github.com/hashicorp/terraform-plugin-framework/datasource" + "github.com/hashicorp/terraform-plugin-framework/datasource/schema" + "github.com/hashicorp/terraform-plugin-framework/schema/validator" + "github.com/hashicorp/terraform-plugin-framework/types" + "github.com/hashicorp/terraform-plugin-log/tflog" + sqlserverflex "github.com/stackitcloud/stackit-sdk-go/services/sqlserverflex/v3api" + + "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/conversion" + "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/core" + sqlserverflexUtils "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/services/sqlserverflex/utils" + "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/utils" + "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/validate" +) + +var ( + _ datasource.DataSource = new(flavors) + _ datasource.DataSourceWithConfigure = new(flavors) +) + +type model struct { + ID types.String `tfsdk:"id"` + ProjectId types.String `tfsdk:"project_id"` + Region types.String `tfsdk:"region"` + Flavors []flavor `tfsdk:"flavors"` + Timeouts timeouts.Value `tfsdk:"timeouts"` +} + +type flavor struct { + Id types.String `tfsdk:"id"` + Description types.String `tfsdk:"description"` + CPU types.Int64 `tfsdk:"cpu"` + Memory types.Int64 `tfsdk:"memory"` + MinGB types.Int32 `tfsdk:"min_gb"` + MaxGB types.Int32 `tfsdk:"max_gb"` + NodeType types.String `tfsdk:"node_type"` + StorageClasses []storageClass `tfsdk:"storage_classes"` +} + +type storageClass struct { + Class types.String `tfsdk:"class"` + MaxIOPerSec types.Int32 `tfsdk:"max_io_per_sec"` + MaxThroughInMB types.Int32 `tfsdk:"max_through_in_mb"` +} + +type flavors struct { + client *sqlserverflex.APIClient + providerData core.ProviderData +} + +func NewFlavorsDataSource() datasource.DataSource { + return new(flavors) +} + +func (f *flavors) Metadata(_ context.Context, req datasource.MetadataRequest, resp *datasource.MetadataResponse) { + resp.TypeName = req.ProviderTypeName + "_sqlserverflex_flavors" +} + +func (f *flavors) Configure(ctx context.Context, req datasource.ConfigureRequest, resp *datasource.ConfigureResponse) { + var ok bool + f.providerData, ok = conversion.ParseProviderData(ctx, req.ProviderData, &resp.Diagnostics) + if !ok { + return + } + + apiClient := sqlserverflexUtils.ConfigureClient(ctx, &f.providerData, &resp.Diagnostics) + if resp.Diagnostics.HasError() { + return + } + f.client = apiClient + tflog.Info(ctx, "SqlserverFlex client configured") +} + +func (f *flavors) Schema(ctx context.Context, _ datasource.SchemaRequest, resp *datasource.SchemaResponse) { + resp.Schema = schema.Schema{ + Description: "SqlserverFlex flavors data source schema", + Attributes: map[string]schema.Attribute{ + "id": schema.StringAttribute{ + Description: "Terraform's internal data source ID, structured as \"`project_id`,`region`\"", + Computed: true, + }, + "project_id": schema.StringAttribute{ + Description: "The project ID.", + Required: true, + Validators: []validator.String{ + validate.UUID(), + validate.NoSeparator(), + }, + }, + "region": schema.StringAttribute{ + Description: "SqlserverFlex flavors data source region. If undefined the providers region is used.", + Optional: true, + Computed: true, + }, + "timeouts": timeouts.Attributes(ctx), + "flavors": schema.ListNestedAttribute{ + Description: "A list of available flavors.", + Computed: true, + NestedObject: schema.NestedAttributeObject{ + Attributes: map[string]schema.Attribute{ + "id": schema.StringAttribute{ + Description: "ID of the flavor.", + Computed: true, + }, + "description": schema.StringAttribute{ + Description: "Description of the flavor.", + Computed: true, + }, + "cpu": schema.Int64Attribute{ + Description: "The CPU count of the instance.", + Computed: true, + }, + "memory": schema.Int64Attribute{ + Description: "The memory (Gibibyte) of the instance.", + Computed: true, + }, + "min_gb": schema.Int64Attribute{ + Description: "Minimum storage, which is required to order in Gigabyte.", + Computed: true, + }, + "max_gb": schema.Int64Attribute{ + Description: "Maximum storage, which can be ordered for the flavor in Gigabyte.", + Computed: true, + }, + "node_type": schema.StringAttribute{ + Description: "Defines the node type (either single or HA).", + Computed: true, + }, + "storage_classes": schema.ListNestedAttribute{ + Description: "⚠️TODO, also nested attr descriptions are freestyled", + Computed: true, + NestedObject: schema.NestedAttributeObject{ + Attributes: map[string]schema.Attribute{ + "class": schema.StringAttribute{ + Description: "Class of the instance.", + Computed: true, + }, + "max_io_per_sec": schema.Int32Attribute{ + Description: "Maximum I/O per second.", + Computed: true, + }, + "max_through_in_mb": schema.Int32Attribute{ + Description: "Maximum throughput in Megabyte.", + Computed: true, + }, + }, + }, + }, + }, + }, + }, + }, + } +} + +func (f *flavors) Read(ctx context.Context, req datasource.ReadRequest, resp *datasource.ReadResponse) { // nolint:gocritic // function signature required by Terraform + var model model + diags := req.Config.Get(ctx, &model) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } + + readTimeout, diags := model.Timeouts.Read(ctx, core.DefaultOperationTimeout) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } + ctx, cancel := context.WithTimeout(ctx, readTimeout) + defer cancel() + + projectId := model.ProjectId.ValueString() + region := f.providerData.GetRegionWithOverride(model.Region) + ctx = utils.SetAndLogStateFields(ctx, &resp.Diagnostics, &resp.State, map[string]any{ + "project_id": projectId, + "region": region, + }) + + ctx = core.InitProviderContext(ctx) + + const pageSize = 100 + flavorsResp, err := f.client.DefaultAPI.ListFlavors(ctx, projectId, region).Size(pageSize).Execute() + if err != nil { + core.LogAndAddError(ctx, &resp.Diagnostics, "Reading flavors", fmt.Sprintf("Error calling ListFlavors: %v", err)) + return + } + if flavorsResp.Pagination.TotalRows > pageSize { + core.LogAndAddWarning(ctx, &resp.Diagnostics, + "Truncated results", + fmt.Sprintf("Due to API limitations we currently do not support more than %d flavors, but %d exist. The result is truncated", pageSize, flavorsResp.Pagination.TotalRows), + ) + } + + ctx = core.LogResponse(ctx) + + err = mapFields(flavorsResp, &model) + if err != nil { + core.LogAndAddError(ctx, &resp.Diagnostics, "Reading Flavors", fmt.Sprintf("Processing API payload: %v", err)) + return + } + + resp.Diagnostics.Append(resp.State.Set(ctx, model)...) + if resp.Diagnostics.HasError() { + return + } + tflog.Info(ctx, "SqlserverFlex flavor read") +} + +func mapFields(resp *sqlserverflex.ListFlavorsResponse, m *model) error { + if resp == nil { + return fmt.Errorf("nil response") + } + if m == nil { + return fmt.Errorf("nil model") + } + + m.ID = utils.BuildInternalTerraformId(m.ProjectId.ValueString(), m.Region.ValueString()) + + slices.SortFunc(resp.Flavors, func(a, b sqlserverflex.ListFlavors) int { + return strings.Compare(a.Id, b.Id) + }) + + for _, respFlavor := range resp.Flavors { + modelFlavor := flavor{ + Id: types.StringValue(respFlavor.Id), + Description: types.StringValue(respFlavor.Description), + CPU: types.Int64Value(respFlavor.Cpu), + Memory: types.Int64Value(respFlavor.Memory), + MinGB: types.Int32Value(respFlavor.MinGB), + MaxGB: types.Int32Value(respFlavor.MaxGB), + NodeType: types.StringValue(respFlavor.NodeType), + } + + slices.SortFunc(respFlavor.StorageClasses, func(a, b sqlserverflex.FlavorStorageClassesStorageClass) int { + return strings.Compare(a.Class, b.Class) + }) + + for _, respSC := range respFlavor.StorageClasses { + modelSC := storageClass{ + Class: types.StringValue(respSC.Class), + MaxIOPerSec: types.Int32Value(respSC.MaxIoPerSec), + MaxThroughInMB: types.Int32Value(respSC.MaxThroughInMb), + } + modelFlavor.StorageClasses = append(modelFlavor.StorageClasses, modelSC) + } + m.Flavors = append(m.Flavors, modelFlavor) + } + return nil +} diff --git a/stackit/internal/services/sqlserverflex/flavors/datasource_test.go b/stackit/internal/services/sqlserverflex/flavors/datasource_test.go new file mode 100644 index 000000000..44bb1f245 --- /dev/null +++ b/stackit/internal/services/sqlserverflex/flavors/datasource_test.go @@ -0,0 +1,165 @@ +package flavors + +import ( + "testing" + + "github.com/google/go-cmp/cmp" + "github.com/hashicorp/terraform-plugin-framework/types" + sqlserverflex "github.com/stackitcloud/stackit-sdk-go/services/sqlserverflex/v3api" +) + +func TestMapFields(t *testing.T) { + tests := []struct { + description string + input *sqlserverflex.ListFlavorsResponse + state *model + expected *model + isValid bool + }{ + { + description: "default_values_and_sorting", + input: &sqlserverflex.ListFlavorsResponse{ + Flavors: []sqlserverflex.ListFlavors{ + { + Id: "id2", + Description: "desc2", + Cpu: 4, + Memory: 8, + MinGB: 20, + MaxGB: 200, + NodeType: "ha", + StorageClasses: []sqlserverflex.FlavorStorageClassesStorageClass{ + { + Class: "class2", + MaxIoPerSec: 2000, + MaxThroughInMb: 200, + }, + { + Class: "class1", + MaxIoPerSec: 1000, + MaxThroughInMb: 100, + }, + }, + }, + { + Id: "id1", + Description: "desc1", + Cpu: 2, + Memory: 4, + MinGB: 10, + MaxGB: 100, + NodeType: "single", + StorageClasses: []sqlserverflex.FlavorStorageClassesStorageClass{ + { + Class: "class3", + MaxIoPerSec: 3000, + MaxThroughInMb: 300, + }, + }, + }, + }, + }, + state: &model{ + ProjectId: types.StringValue("project_id"), + Region: types.StringValue("region"), + }, + expected: &model{ + ID: types.StringValue("project_id,region"), + ProjectId: types.StringValue("project_id"), + Region: types.StringValue("region"), + Flavors: []flavor{ + { + Id: types.StringValue("id1"), + Description: types.StringValue("desc1"), + CPU: types.Int64Value(2), + Memory: types.Int64Value(4), + MinGB: types.Int32Value(10), + MaxGB: types.Int32Value(100), + NodeType: types.StringValue("single"), + StorageClasses: []storageClass{ + { + Class: types.StringValue("class3"), + MaxIOPerSec: types.Int32Value(3000), + MaxThroughInMB: types.Int32Value(300), + }, + }, + }, + { + Id: types.StringValue("id2"), + Description: types.StringValue("desc2"), + CPU: types.Int64Value(4), + Memory: types.Int64Value(8), + MinGB: types.Int32Value(20), + MaxGB: types.Int32Value(200), + NodeType: types.StringValue("ha"), + StorageClasses: []storageClass{ + { + Class: types.StringValue("class1"), + MaxIOPerSec: types.Int32Value(1000), + MaxThroughInMB: types.Int32Value(100), + }, + { + Class: types.StringValue("class2"), + MaxIOPerSec: types.Int32Value(2000), + MaxThroughInMB: types.Int32Value(200), + }, + }, + }, + }, + }, + isValid: true, + }, + { + description: "empty_response", + input: &sqlserverflex.ListFlavorsResponse{}, + state: &model{ + ProjectId: types.StringValue("project_id"), + Region: types.StringValue("region"), + }, + expected: &model{ + ID: types.StringValue("project_id,region"), + ProjectId: types.StringValue("project_id"), + Region: types.StringValue("region"), + }, + isValid: true, + }, + { + description: "nil_response", + input: nil, + state: &model{ + ProjectId: types.StringValue("project_id"), + Region: types.StringValue("region"), + }, + expected: &model{ + ProjectId: types.StringValue("project_id"), + Region: types.StringValue("region"), + }, + isValid: false, + }, + { + description: "nil_model", + input: &sqlserverflex.ListFlavorsResponse{}, + state: nil, + expected: nil, + isValid: false, + }, + } + + for _, tt := range tests { + t.Run(tt.description, func(t *testing.T) { + err := mapFields(tt.input, tt.state) + if !tt.isValid && err == nil { + t.Fatalf("Should have failed") + } + if tt.isValid && err != nil { + t.Fatalf("Should not have failed: %v", err) + } + if tt.isValid { + diff := cmp.Diff(tt.expected, tt.state) + if diff != "" { + t.Fatalf("Data does not match: %s", diff) + } + } + }) + } +} diff --git a/stackit/internal/services/sqlserverflex/sqlserverflex_acc_test.go b/stackit/internal/services/sqlserverflex/sqlserverflex_acc_test.go index ad7f44ccd..9dadf38f2 100644 --- a/stackit/internal/services/sqlserverflex/sqlserverflex_acc_test.go +++ b/stackit/internal/services/sqlserverflex/sqlserverflex_acc_test.go @@ -418,6 +418,39 @@ func TestAccSQLServerFlexMaxResource(t *testing.T) { }) } +func TestAccSqlServerFlexFlavorsDatasource(t *testing.T) { + resource.Test(t, resource.TestCase{ + ProtoV6ProviderFactories: testutil.TestAccProtoV6ProviderFactories, + Steps: []resource.TestStep{ + { + ConfigVariables: config.Variables{ + "project_id": config.StringVariable(testutil.ProjectId), + }, + Config: fmt.Sprintf(` + %s + + variable "project_id" {} + + data "stackit_sqlserverflex_flavors" "datasource" { + project_id = var.project_id + }`, testutil.NewConfigBuilder().BuildProviderConfig()), + Check: resource.ComposeAggregateTestCheckFunc( + resource.TestCheckResourceAttrSet("data.stackit_sqlserverflex_flavors.datasource", "flavors.0.id"), + resource.TestCheckResourceAttrSet("data.stackit_sqlserverflex_flavors.datasource", "flavors.0.description"), + resource.TestCheckResourceAttrSet("data.stackit_sqlserverflex_flavors.datasource", "flavors.0.cpu"), + resource.TestCheckResourceAttrSet("data.stackit_sqlserverflex_flavors.datasource", "flavors.0.memory"), + resource.TestCheckResourceAttrSet("data.stackit_sqlserverflex_flavors.datasource", "flavors.0.min_gb"), + resource.TestCheckResourceAttrSet("data.stackit_sqlserverflex_flavors.datasource", "flavors.0.max_gb"), + resource.TestCheckResourceAttrSet("data.stackit_sqlserverflex_flavors.datasource", "flavors.0.node_type"), + resource.TestCheckResourceAttrSet("data.stackit_sqlserverflex_flavors.datasource", "flavors.0.storage_classes.0.class"), + resource.TestCheckResourceAttrSet("data.stackit_sqlserverflex_flavors.datasource", "flavors.0.storage_classes.0.max_io_per_sec"), + resource.TestCheckResourceAttrSet("data.stackit_sqlserverflex_flavors.datasource", "flavors.0.storage_classes.0.max_through_in_mb"), + ), + }, + }, + }) +} + func testAccChecksqlserverflexDestroy(s *terraform.State) error { ctx := context.Background() client, err := sqlserverflex.NewAPIClient(testutil.NewConfigBuilder().BuildClientOptions(testutil.SQLServerFlexCustomEndpoint, false)...) diff --git a/stackit/provider.go b/stackit/provider.go index f99c5eb3c..c985a2ada 100644 --- a/stackit/provider.go +++ b/stackit/provider.go @@ -129,6 +129,7 @@ import ( skeKubeconfig "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/services/ske/kubeconfig" skeKubernetesVersion "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/services/ske/provideroptions/kubernetesversions" skeMachineImages "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/services/ske/provideroptions/machineimages" + sqlServerFlexFlavors "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/services/sqlserverflex/flavors" sqlServerFlexInstance "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/services/sqlserverflex/instance" sqlServerFlexUser "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/services/sqlserverflex/user" telemetryLink "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/services/telemetrylink/link" @@ -756,6 +757,7 @@ func (p *Provider) DataSources(_ context.Context) []func() datasource.DataSource secretsManagerUser.NewUserDataSource, sqlServerFlexInstance.NewInstanceDataSource, sqlServerFlexUser.NewUserDataSource, + sqlServerFlexFlavors.NewFlavorsDataSource, serverBackupSchedule.NewScheduleDataSource, serverBackupSchedule.NewSchedulesDataSource, serverUpdateSchedule.NewScheduleDataSource,