From fedaf72b62ee0666e8b678234c45846908313704 Mon Sep 17 00:00:00 2001 From: Mauritz Uphoff <39736813+h3adex@users.noreply.github.com> Date: Thu, 28 Aug 2025 13:23:11 +0200 Subject: [PATCH] Feat/add datasource to query machine types (#968) * feat(iaas): add datasource to query machine types Signed-off-by: Mauritz Uphoff * review Signed-off-by: Mauritz Uphoff * review Signed-off-by: Mauritz Uphoff --------- Signed-off-by: Mauritz Uphoff --- docs/data-sources/machine_type.md | 70 +++++ .../stackit_machine_type/data-source.tf | 21 ++ .../internal/services/iaas/iaas_acc_test.go | 48 ++++ .../services/iaas/machinetype/datasource.go | 245 ++++++++++++++++++ .../iaas/machinetype/datasource_test.go | 241 +++++++++++++++++ .../iaas/testdata/datasource-machinetype.tf | 18 ++ stackit/internal/testutil/testutil.go | 17 ++ stackit/provider.go | 2 + 8 files changed, 662 insertions(+) create mode 100644 docs/data-sources/machine_type.md create mode 100644 examples/data-sources/stackit_machine_type/data-source.tf create mode 100644 stackit/internal/services/iaas/machinetype/datasource.go create mode 100644 stackit/internal/services/iaas/machinetype/datasource_test.go create mode 100644 stackit/internal/services/iaas/testdata/datasource-machinetype.tf diff --git a/docs/data-sources/machine_type.md b/docs/data-sources/machine_type.md new file mode 100644 index 00000000..2faa8da4 --- /dev/null +++ b/docs/data-sources/machine_type.md @@ -0,0 +1,70 @@ +--- +# generated by https://github.com/hashicorp/terraform-plugin-docs +page_title: "stackit_machine_type Data Source - stackit" +subcategory: "" +description: |- + Machine type data source. + ~> This datasource is in beta and may be subject to breaking changes in the future. Use with caution. See our guide https://registry.terraform.io/providers/stackitcloud/stackit/latest/docs/guides/opting_into_beta_resources for how to opt-in to use beta resources. +--- + +# stackit_machine_type (Data Source) + +Machine type data source. + +~> This datasource is in beta and may be subject to breaking changes in the future. Use with caution. See our [guide](https://registry.terraform.io/providers/stackitcloud/stackit/latest/docs/guides/opting_into_beta_resources) for how to opt-in to use beta resources. + +## Example Usage + +```terraform +data "stackit_machine_type" "two_vcpus_filter" { + project_id = "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx" + filter = "vcpus==2" +} + +data "stackit_machine_type" "filter_sorted_ascending_false" { + project_id = "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx" + filter = "vcpus >= 2 && ram >= 2048" + sort_ascending = false +} + +data "stackit_machine_type" "intel_icelake_generic_filter" { + project_id = "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx" + filter = "extraSpecs.cpu==\"intel-icelake-generic\" && vcpus == 2" +} + +# returns warning +data "stackit_machine_type" "no_match" { + project_id = "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx" + filter = "vcpus == 99" +} +``` + + +## Schema + +### Required + +- `filter` (String) Expr-lang filter for filtering machine types. + +Examples: +- vcpus == 2 +- ram >= 2048 +- extraSpecs.cpu == "intel-icelake-generic" +- extraSpecs.cpu == "intel-icelake-generic" && vcpus == 2 + +See https://expr-lang.org/docs/language-definition for syntax. +- `project_id` (String) STACKIT Project ID. + +### Optional + +- `sort_ascending` (Boolean) Sort machine types by name ascending (`true`) or descending (`false`). Defaults to `false` + +### Read-Only + +- `description` (String) Machine type description. +- `disk` (Number) Disk size in GB. +- `extra_specs` (Map of String) Extra specs (e.g., CPU type, overcommit ratio). +- `id` (String) Terraform's internal resource ID. It is structured as "`project_id`,`image_id`". +- `name` (String) Name of the machine type (e.g. 's1.2'). +- `ram` (Number) RAM size in MB. +- `vcpus` (Number) Number of vCPUs. diff --git a/examples/data-sources/stackit_machine_type/data-source.tf b/examples/data-sources/stackit_machine_type/data-source.tf new file mode 100644 index 00000000..6120b15f --- /dev/null +++ b/examples/data-sources/stackit_machine_type/data-source.tf @@ -0,0 +1,21 @@ +data "stackit_machine_type" "two_vcpus_filter" { + project_id = "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx" + filter = "vcpus==2" +} + +data "stackit_machine_type" "filter_sorted_ascending_false" { + project_id = "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx" + filter = "vcpus >= 2 && ram >= 2048" + sort_ascending = false +} + +data "stackit_machine_type" "intel_icelake_generic_filter" { + project_id = "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx" + filter = "extraSpecs.cpu==\"intel-icelake-generic\" && vcpus == 2" +} + +# returns warning +data "stackit_machine_type" "no_match" { + project_id = "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx" + filter = "vcpus == 99" +} \ No newline at end of file diff --git a/stackit/internal/services/iaas/iaas_acc_test.go b/stackit/internal/services/iaas/iaas_acc_test.go index 3c1c4f64..d0f91249 100644 --- a/stackit/internal/services/iaas/iaas_acc_test.go +++ b/stackit/internal/services/iaas/iaas_acc_test.go @@ -88,6 +88,9 @@ var ( //go:embed testdata/resource-server-max-server-attachments.tf resourceServerMaxAttachmentConfig string + + //go:embed testdata/datasource-machinetype.tf + dataSourceMachineTypeConfig string ) const ( @@ -487,6 +490,10 @@ var testConfigKeyPairMaxUpdated = func() config.Variables { return updatedConfig }() +var testConfigMachineTypeVars = config.Variables{ + "project_id": config.StringVariable(testutil.ProjectId), +} + // if no local file is provided the test should create a default file and work with this instead of failing var localFileForIaasImage os.File @@ -4054,6 +4061,47 @@ func TestAccProject(t *testing.T) { }) } +func TestAccMachineType(t *testing.T) { + t.Logf("TestAccMachineType projectid: %s", testutil.ConvertConfigVariable(testConfigMachineTypeVars["project_id"])) + resource.ParallelTest(t, resource.TestCase{ + ProtoV6ProviderFactories: testutil.TestAccProtoV6ProviderFactories, + Steps: []resource.TestStep{ + { + ConfigVariables: testConfigMachineTypeVars, + Config: fmt.Sprintf("%s\n%s", dataSourceMachineTypeConfig, testutil.IaaSProviderConfigWithBetaResourcesEnabled()), + Check: resource.ComposeTestCheckFunc( + resource.TestCheckResourceAttr("data.stackit_machine_type.two_vcpus_filter", "project_id", testutil.ConvertConfigVariable(testConfigMachineTypeVars["project_id"])), + resource.TestCheckResourceAttrSet("data.stackit_machine_type.two_vcpus_filter", "id"), + resource.TestCheckResourceAttrSet("data.stackit_machine_type.two_vcpus_filter", "name"), + resource.TestCheckResourceAttrSet("data.stackit_machine_type.two_vcpus_filter", "vcpus"), + resource.TestCheckResourceAttr("data.stackit_machine_type.two_vcpus_filter", "vcpus", "2"), + resource.TestCheckResourceAttrSet("data.stackit_machine_type.two_vcpus_filter", "ram"), + resource.TestCheckResourceAttrSet("data.stackit_machine_type.two_vcpus_filter", "disk"), + resource.TestCheckResourceAttrSet("data.stackit_machine_type.two_vcpus_filter", "description"), + resource.TestCheckResourceAttrSet("data.stackit_machine_type.two_vcpus_filter", "extra_specs.cpu"), + + resource.TestCheckResourceAttr("data.stackit_machine_type.filter_sorted_ascending_false", "project_id", testutil.ConvertConfigVariable(testConfigMachineTypeVars["project_id"])), + resource.TestCheckResourceAttrSet("data.stackit_machine_type.filter_sorted_ascending_false", "id"), + resource.TestCheckResourceAttrSet("data.stackit_machine_type.filter_sorted_ascending_false", "name"), + resource.TestCheckResourceAttrSet("data.stackit_machine_type.filter_sorted_ascending_false", "vcpus"), + resource.TestCheckResourceAttrSet("data.stackit_machine_type.filter_sorted_ascending_false", "ram"), + resource.TestCheckResourceAttrSet("data.stackit_machine_type.filter_sorted_ascending_false", "disk"), + resource.TestCheckResourceAttrSet("data.stackit_machine_type.filter_sorted_ascending_false", "description"), + resource.TestCheckResourceAttrSet("data.stackit_machine_type.filter_sorted_ascending_false", "extra_specs.cpu"), + + resource.TestCheckResourceAttr("data.stackit_machine_type.no_match", "project_id", testutil.ConvertConfigVariable(testConfigMachineTypeVars["project_id"])), + resource.TestCheckNoResourceAttr("data.stackit_machine_type.no_match", "description"), + resource.TestCheckNoResourceAttr("data.stackit_machine_type.no_match", "disk"), + resource.TestCheckNoResourceAttr("data.stackit_machine_type.no_match", "extra_specs"), + resource.TestCheckNoResourceAttr("data.stackit_machine_type.no_match", "id"), + resource.TestCheckNoResourceAttr("data.stackit_machine_type.no_match", "name"), + resource.TestCheckNoResourceAttr("data.stackit_machine_type.no_match", "ram"), + ), + }, + }, + }) +} + func testAccCheckDestroy(s *terraform.State) error { checkFunctions := []func(s *terraform.State) error{ testAccCheckNetworkV1Destroy, diff --git a/stackit/internal/services/iaas/machinetype/datasource.go b/stackit/internal/services/iaas/machinetype/datasource.go new file mode 100644 index 00000000..7bc9ce55 --- /dev/null +++ b/stackit/internal/services/iaas/machinetype/datasource.go @@ -0,0 +1,245 @@ +package machineType + +import ( + "context" + "fmt" + "net/http" + "sort" + "strings" + + "github.com/hashicorp/terraform-plugin-framework/datasource" + "github.com/hashicorp/terraform-plugin-framework/datasource/schema" + "github.com/hashicorp/terraform-plugin-framework/diag" + "github.com/hashicorp/terraform-plugin-framework/schema/validator" + "github.com/hashicorp/terraform-plugin-framework/types" + "github.com/hashicorp/terraform-plugin-log/tflog" + "github.com/stackitcloud/stackit-sdk-go/services/iaas" + "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/conversion" + "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/core" + "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/features" + iaasUtils "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/services/iaas/utils" + "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/utils" + "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/validate" +) + +// Ensure the implementation satisfies the expected interfaces. +var _ datasource.DataSource = &machineTypeDataSource{} + +type DataSourceModel struct { + Id types.String `tfsdk:"id"` // required by Terraform to identify state + ProjectId types.String `tfsdk:"project_id"` + SortAscending types.Bool `tfsdk:"sort_ascending"` + Filter types.String `tfsdk:"filter"` + Description types.String `tfsdk:"description"` + Disk types.Int64 `tfsdk:"disk"` + ExtraSpecs types.Map `tfsdk:"extra_specs"` + Name types.String `tfsdk:"name"` + Ram types.Int64 `tfsdk:"ram"` + Vcpus types.Int64 `tfsdk:"vcpus"` +} + +// NewMachineTypeDataSource instantiates the data source +func NewMachineTypeDataSource() datasource.DataSource { + return &machineTypeDataSource{} +} + +type machineTypeDataSource struct { + client *iaas.APIClient +} + +func (d *machineTypeDataSource) Metadata(_ context.Context, req datasource.MetadataRequest, resp *datasource.MetadataResponse) { + resp.TypeName = req.ProviderTypeName + "_machine_type" +} + +func (d *machineTypeDataSource) Configure(ctx context.Context, req datasource.ConfigureRequest, resp *datasource.ConfigureResponse) { + providerData, ok := conversion.ParseProviderData(ctx, req.ProviderData, &resp.Diagnostics) + if !ok { + return + } + + features.CheckBetaResourcesEnabled(ctx, &providerData, &resp.Diagnostics, "stackit_machine_type", "datasource") + if resp.Diagnostics.HasError() { + return + } + + client := iaasUtils.ConfigureClient(ctx, &providerData, &resp.Diagnostics) + if resp.Diagnostics.HasError() { + return + } + d.client = client + + tflog.Info(ctx, "IAAS client configured") +} + +func (d *machineTypeDataSource) Schema(_ context.Context, _ datasource.SchemaRequest, resp *datasource.SchemaResponse) { + resp.Schema = schema.Schema{ + MarkdownDescription: features.AddBetaDescription("Machine type data source.", core.Datasource), + Attributes: map[string]schema.Attribute{ + "id": schema.StringAttribute{ + Description: "Terraform's internal resource ID. It is structured as \"`project_id`,`image_id`\".", + Computed: true, + }, + "project_id": schema.StringAttribute{ + Description: "STACKIT Project ID.", + Required: true, + Validators: []validator.String{ + validate.UUID(), + validate.NoSeparator(), + }, + }, + "sort_ascending": schema.BoolAttribute{ + Description: "Sort machine types by name ascending (`true`) or descending (`false`). Defaults to `false`", + Optional: true, + }, + "filter": schema.StringAttribute{ + Description: `Expr-lang filter for filtering machine types. + +Examples: +- vcpus == 2 +- ram >= 2048 +- extraSpecs.cpu == "intel-icelake-generic" +- extraSpecs.cpu == "intel-icelake-generic" && vcpus == 2 + +See https://expr-lang.org/docs/language-definition for syntax.`, + Required: true, + }, + "description": schema.StringAttribute{ + Description: "Machine type description.", + Computed: true, + }, + "disk": schema.Int64Attribute{ + Description: "Disk size in GB.", + Computed: true, + }, + "extra_specs": schema.MapAttribute{ + Description: "Extra specs (e.g., CPU type, overcommit ratio).", + ElementType: types.StringType, + Computed: true, + }, + "name": schema.StringAttribute{ + Description: "Name of the machine type (e.g. 's1.2').", + Computed: true, + }, + "ram": schema.Int64Attribute{ + Description: "RAM size in MB.", + Computed: true, + }, + "vcpus": schema.Int64Attribute{ + Description: "Number of vCPUs.", + Computed: true, + }, + }, + } +} + +func (d *machineTypeDataSource) Read(ctx context.Context, req datasource.ReadRequest, resp *datasource.ReadResponse) { // nolint:gocritic // function signature required by Terraform + var model DataSourceModel + resp.Diagnostics.Append(req.Config.Get(ctx, &model)...) + if resp.Diagnostics.HasError() { + return + } + + projectId := model.ProjectId.ValueString() + sortAscending := model.SortAscending.ValueBool() + + ctx = tflog.SetField(ctx, "project_id", projectId) + ctx = tflog.SetField(ctx, "filter_is_null", model.Filter.IsNull()) + ctx = tflog.SetField(ctx, "filter_is_unknown", model.Filter.IsUnknown()) + + listMachineTypeReq := d.client.ListMachineTypes(ctx, projectId) + + if !model.Filter.IsNull() && !model.Filter.IsUnknown() && strings.TrimSpace(model.Filter.ValueString()) != "" { + listMachineTypeReq = listMachineTypeReq.Filter(strings.TrimSpace(model.Filter.ValueString())) + } + + apiResp, err := listMachineTypeReq.Execute() + if err != nil { + utils.LogError(ctx, &resp.Diagnostics, err, "Failed to read machine types", + fmt.Sprintf("Unable to retrieve machine types for project %q %s.", projectId, err), + map[int]string{ + http.StatusForbidden: fmt.Sprintf("Access denied to project %q.", projectId), + }, + ) + resp.State.RemoveResource(ctx) + return + } + + if apiResp.Items == nil || len(*apiResp.Items) == 0 { + core.LogAndAddWarning(ctx, &resp.Diagnostics, "No machine types found", "No matching machine types.") + return + } + + // Convert items to []*iaas.MachineType + machineTypes := make([]*iaas.MachineType, len(*apiResp.Items)) + for i := range *apiResp.Items { + machineTypes[i] = &(*apiResp.Items)[i] + } + + sorted, err := sortMachineTypeByName(machineTypes, sortAscending) + if err != nil { + core.LogAndAddWarning(ctx, &resp.Diagnostics, "Unable to sort", err.Error()) + return + } + + if err := mapDataSourceFields(ctx, sorted[0], &model); err != nil { + core.LogAndAddError(ctx, &resp.Diagnostics, "Error reading machine type", fmt.Sprintf("Failed to translate API response: %v", err)) + return + } + + resp.Diagnostics.Append(resp.State.Set(ctx, model)...) + if resp.Diagnostics.HasError() { + return + } + tflog.Info(ctx, "Successfully read machine type") +} + +func mapDataSourceFields(ctx context.Context, machineType *iaas.MachineType, model *DataSourceModel) error { + if machineType == nil || model == nil { + return fmt.Errorf("nil input provided") + } + + if machineType.Name == nil || *machineType.Name == "" { + return fmt.Errorf("machine type name is missing") + } + + model.Id = utils.BuildInternalTerraformId(model.ProjectId.ValueString(), *machineType.Name) + model.Name = types.StringPointerValue(machineType.Name) + model.Description = types.StringPointerValue(machineType.Description) + model.Disk = types.Int64PointerValue(machineType.Disk) + model.Ram = types.Int64PointerValue(machineType.Ram) + model.Vcpus = types.Int64PointerValue(machineType.Vcpus) + + extra := types.MapNull(types.StringType) + if machineType.ExtraSpecs != nil && len(*machineType.ExtraSpecs) > 0 { + var diags diag.Diagnostics + extra, diags = types.MapValueFrom(ctx, types.StringType, *machineType.ExtraSpecs) + if diags.HasError() { + return fmt.Errorf("converting extraspecs: %w", core.DiagsToError(diags)) + } + } + model.ExtraSpecs = extra + return nil +} + +func sortMachineTypeByName(input []*iaas.MachineType, ascending bool) ([]*iaas.MachineType, error) { + if input == nil { + return nil, fmt.Errorf("input slice is nil") + } + + // Filter out nil or missing name + var filtered []*iaas.MachineType + for _, m := range input { + if m != nil && m.Name != nil { + filtered = append(filtered, m) + } + } + + sort.SliceStable(filtered, func(i, j int) bool { + if ascending { + return *filtered[i].Name < *filtered[j].Name + } + return *filtered[i].Name > *filtered[j].Name + }) + + return filtered, nil +} diff --git a/stackit/internal/services/iaas/machinetype/datasource_test.go b/stackit/internal/services/iaas/machinetype/datasource_test.go new file mode 100644 index 00000000..3fde4794 --- /dev/null +++ b/stackit/internal/services/iaas/machinetype/datasource_test.go @@ -0,0 +1,241 @@ +package machineType + +import ( + "context" + "strings" + "testing" + + "github.com/google/go-cmp/cmp" + "github.com/hashicorp/terraform-plugin-framework/attr" + "github.com/hashicorp/terraform-plugin-framework/types" + "github.com/stackitcloud/stackit-sdk-go/core/utils" + "github.com/stackitcloud/stackit-sdk-go/services/iaas" +) + +func TestMapDataSourceFields(t *testing.T) { + tests := []struct { + name string + initial DataSourceModel + input *iaas.MachineType + expected DataSourceModel + expectError bool + }{ + { + name: "valid simple values", + initial: DataSourceModel{ + ProjectId: types.StringValue("pid"), + }, + input: &iaas.MachineType{ + Name: utils.Ptr("s1.2"), + Description: utils.Ptr("general-purpose small"), + Disk: utils.Ptr(int64(20)), + Ram: utils.Ptr(int64(2048)), + Vcpus: utils.Ptr(int64(2)), + ExtraSpecs: &map[string]interface{}{ + "cpu": "amd-epycrome-7702", + "overcommit": "1", + "environment": "general", + }, + }, + expected: DataSourceModel{ + Id: types.StringValue("pid,s1.2"), + ProjectId: types.StringValue("pid"), + Name: types.StringValue("s1.2"), + Description: types.StringValue("general-purpose small"), + Disk: types.Int64Value(20), + Ram: types.Int64Value(2048), + Vcpus: types.Int64Value(2), + ExtraSpecs: types.MapValueMust(types.StringType, map[string]attr.Value{ + "cpu": types.StringValue("amd-epycrome-7702"), + "overcommit": types.StringValue("1"), + "environment": types.StringValue("general"), + }), + }, + expectError: false, + }, + { + name: "missing name should fail", + initial: DataSourceModel{ + ProjectId: types.StringValue("pid-456"), + }, + input: &iaas.MachineType{ + Description: utils.Ptr("gp-medium"), + }, + expected: DataSourceModel{}, + expectError: true, + }, + { + name: "nil machineType should fail", + initial: DataSourceModel{}, + input: nil, + expected: DataSourceModel{}, + expectError: true, + }, + { + name: "empty extraSpecs should return null map", + initial: DataSourceModel{ + ProjectId: types.StringValue("pid-789"), + }, + input: &iaas.MachineType{ + Name: utils.Ptr("m1.noextras"), + Description: utils.Ptr("no extras"), + Disk: utils.Ptr(int64(10)), + Ram: utils.Ptr(int64(1024)), + Vcpus: utils.Ptr(int64(1)), + ExtraSpecs: &map[string]interface{}{}, + }, + expected: DataSourceModel{ + Id: types.StringValue("pid-789,m1.noextras"), + ProjectId: types.StringValue("pid-789"), + Name: types.StringValue("m1.noextras"), + Description: types.StringValue("no extras"), + Disk: types.Int64Value(10), + Ram: types.Int64Value(1024), + Vcpus: types.Int64Value(1), + ExtraSpecs: types.MapNull(types.StringType), + }, + expectError: false, + }, + { + name: "nil extrasSpecs should return null map", + initial: DataSourceModel{ + ProjectId: types.StringValue("pid-987"), + }, + input: &iaas.MachineType{ + Name: utils.Ptr("g1.nil"), + Description: utils.Ptr("missing extras"), + Disk: utils.Ptr(int64(40)), + Ram: utils.Ptr(int64(8096)), + Vcpus: utils.Ptr(int64(4)), + ExtraSpecs: nil, + }, + expected: DataSourceModel{ + Id: types.StringValue("pid-987,g1.nil"), + ProjectId: types.StringValue("pid-987"), + Name: types.StringValue("g1.nil"), + Description: types.StringValue("missing extras"), + Disk: types.Int64Value(40), + Ram: types.Int64Value(8096), + Vcpus: types.Int64Value(4), + ExtraSpecs: types.MapNull(types.StringType), + }, + expectError: false, + }, + { + name: "invalid extraSpecs with non-string values", + initial: DataSourceModel{ + ProjectId: types.StringValue("test-err"), + }, + input: &iaas.MachineType{ + Name: utils.Ptr("invalid"), + Description: utils.Ptr("bad map"), + Disk: utils.Ptr(int64(10)), + Ram: utils.Ptr(int64(4096)), + Vcpus: utils.Ptr(int64(2)), + ExtraSpecs: &map[string]interface{}{ + "cpu": "intel", + "burst": true, // not a string + "gen": 8, // not a string + }, + }, + expected: DataSourceModel{}, + expectError: true, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + err := mapDataSourceFields(context.Background(), tt.input, &tt.initial) + if tt.expectError { + if err == nil { + t.Errorf("expected error but got none") + } + return + } + + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + + diff := cmp.Diff(tt.expected, tt.initial) + if diff != "" { + t.Errorf("unexpected diff (-want +got):\n%s", diff) + } + + // Extra sanity check for proper ID format + if id := tt.initial.Id.ValueString(); !strings.HasPrefix(id, tt.initial.ProjectId.ValueString()+",") { + t.Errorf("unexpected ID format: got %q", id) + } + }) + } +} + +func TestSortMachineTypeByName(t *testing.T) { + tests := []struct { + name string + input []*iaas.MachineType + ascending bool + expected []string + expectError bool + }{ + { + name: "ascending order", + input: []*iaas.MachineType{{Name: utils.Ptr("zeta")}, {Name: utils.Ptr("alpha")}, {Name: utils.Ptr("gamma")}}, + ascending: true, + expected: []string{"alpha", "gamma", "zeta"}, + }, + { + name: "descending order", + input: []*iaas.MachineType{{Name: utils.Ptr("zeta")}, {Name: utils.Ptr("alpha")}, {Name: utils.Ptr("gamma")}}, + ascending: false, + expected: []string{"zeta", "gamma", "alpha"}, + }, + { + name: "handles nil names", + input: []*iaas.MachineType{{Name: utils.Ptr("beta")}, nil, {Name: nil}, {Name: utils.Ptr("alpha")}}, + ascending: true, + expected: []string{"alpha", "beta"}, + }, + { + name: "empty input", + input: []*iaas.MachineType{}, + ascending: true, + expected: nil, + expectError: false, + }, + { + name: "nil input", + input: nil, + ascending: true, + expectError: true, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + sorted, err := sortMachineTypeByName(tt.input, tt.ascending) + + if tt.expectError { + if err == nil { + t.Errorf("expected error but got none") + } + return + } + + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + + var result []string + for _, mt := range sorted { + if mt.Name != nil { + result = append(result, *mt.Name) + } + } + + if diff := cmp.Diff(tt.expected, result); diff != "" { + t.Errorf("unexpected sorted order (-want +got):\n%s", diff) + } + }) + } +} diff --git a/stackit/internal/services/iaas/testdata/datasource-machinetype.tf b/stackit/internal/services/iaas/testdata/datasource-machinetype.tf new file mode 100644 index 00000000..3475f34d --- /dev/null +++ b/stackit/internal/services/iaas/testdata/datasource-machinetype.tf @@ -0,0 +1,18 @@ +variable "project_id" {} + +data "stackit_machine_type" "two_vcpus_filter" { + project_id = var.project_id + filter = "vcpus==2" +} + +data "stackit_machine_type" "filter_sorted_ascending_false" { + project_id = var.project_id + filter = "vcpus >= 2 && ram >= 2048" + sort_ascending = false +} + +# returns warning +data "stackit_machine_type" "no_match" { + project_id = var.project_id + filter = "vcpus == 99" +} \ No newline at end of file diff --git a/stackit/internal/testutil/testutil.go b/stackit/internal/testutil/testutil.go index 77dee35f..4c1d46a6 100644 --- a/stackit/internal/testutil/testutil.go +++ b/stackit/internal/testutil/testutil.go @@ -136,6 +136,23 @@ func IaaSProviderConfig() string { ) } +func IaaSProviderConfigWithBetaResourcesEnabled() string { + if IaaSCustomEndpoint == "" { + return ` + provider "stackit" { + enable_beta_resources = true + default_region = "eu01" + }` + } + return fmt.Sprintf(` + provider "stackit" { + enable_beta_resources = true + iaas_custom_endpoint = "%s" + }`, + IaaSCustomEndpoint, + ) +} + func IaaSProviderConfigWithExperiments() string { if IaaSCustomEndpoint == "" { return ` diff --git a/stackit/provider.go b/stackit/provider.go index f1079bec..698af5c8 100644 --- a/stackit/provider.go +++ b/stackit/provider.go @@ -27,6 +27,7 @@ import ( iaasAffinityGroup "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/services/iaas/affinitygroup" iaasImage "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/services/iaas/image" iaasKeyPair "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/services/iaas/keypair" + machineType "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/services/iaas/machinetype" iaasNetwork "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/services/iaas/network" iaasNetworkArea "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/services/iaas/networkarea" iaasNetworkAreaRoute "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/services/iaas/networkarearoute" @@ -476,6 +477,7 @@ func (p *Provider) DataSources(_ context.Context) []func() datasource.DataSource logMeInstance.NewInstanceDataSource, logMeCredential.NewCredentialDataSource, logAlertGroup.NewLogAlertGroupDataSource, + machineType.NewMachineTypeDataSource, mariaDBInstance.NewInstanceDataSource, mariaDBCredential.NewCredentialDataSource, mongoDBFlexInstance.NewInstanceDataSource,