From b9cca168c9f9f40a6ed400d3fb871226428fc456 Mon Sep 17 00:00:00 2001 From: Mauritz Uphoff Date: Fri, 18 Jul 2025 12:41:54 +0200 Subject: [PATCH 1/3] feat(iaas): add datasource to query machine types Signed-off-by: Mauritz Uphoff --- docs/data-sources/machine_type.md | 67 +++++ .../stackit_machine_type/data-source.tf | 21 ++ .../internal/services/iaas/iaas_acc_test.go | 48 ++++ .../services/iaas/machinetype/datasource.go | 246 ++++++++++++++++ .../iaas/machinetype/datasource_test.go | 264 ++++++++++++++++++ .../iaas/testdata/datasource-machinetype.tf | 18 ++ stackit/provider.go | 2 + 7 files changed, 666 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 000000000..c096f1d01 --- /dev/null +++ b/docs/data-sources/machine_type.md @@ -0,0 +1,67 @@ +--- +# generated by https://github.com/hashicorp/terraform-plugin-docs +page_title: "stackit_machine_type Data Source - stackit" +subcategory: "" +description: |- + Machine type data source. +--- + +# stackit_machine_type (Data Source) + +Machine type data source. + +## 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 000000000..6120b15fc --- /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 3c1c4f649..64a4108f8 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 TestAccMachineTyp(t *testing.T) { + t.Logf("TestAccMachineTyp 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.IaaSProviderConfig()), + 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 000000000..70e8a417c --- /dev/null +++ b/stackit/internal/services/iaas/machinetype/datasource.go @@ -0,0 +1,246 @@ +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" + 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 (m *machineTypeDataSource) Metadata(_ context.Context, req datasource.MetadataRequest, resp *datasource.MetadataResponse) { + resp.TypeName = req.ProviderTypeName + "_machine_type" +} + +func (m *machineTypeDataSource) Configure(ctx context.Context, req datasource.ConfigureRequest, resp *datasource.ConfigureResponse) { + providerData, ok := conversion.ParseProviderData(ctx, req.ProviderData, &resp.Diagnostics) + if !ok { + return + } + + client := iaasUtils.ConfigureClient(ctx, &providerData, &resp.Diagnostics) + if resp.Diagnostics.HasError() { + return + } + m.client = client + + tflog.Info(ctx, "IAAS client configured") +} + +func (m *machineTypeDataSource) Schema(_ context.Context, _ datasource.SchemaRequest, resp *datasource.SchemaResponse) { + resp.Schema = schema.Schema{ + MarkdownDescription: "Machine type data source.", + 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 (m *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 := m.client.ListMachineTypes(ctx, projectId) + + if !model.Filter.IsNull() && !model.Filter.IsUnknown() { + if filter := strings.TrimSpace(model.Filter.ValueString()); filter != "" { + listMachineTypeReq = listMachineTypeReq.Filter(filter) + } + } + + 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 + } + + items := apiResp.Items + if items == nil || len(*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(*items)) + for i := range *items { + machineTypes[i] = &(*items)[i] + } + + sorted, err := sortMachineTypeByName(machineTypes, sortAscending) + if err != nil { + core.LogAndAddWarning(ctx, &resp.Diagnostics, "Unable to sort", err.Error()) + return + } + + first := sorted[0] + if err := mapDataSourceFields(ctx, first, &model); err != nil { + core.LogAndAddError(ctx, &resp.Diagnostics, "Mapping error", fmt.Sprintf("Failed to translate API response: %v", err)) + return + } + + if err := mapDataSourceFields(ctx, first, &model); err != nil { + core.LogAndAddError(ctx, &resp.Diagnostics, "Mapping error", fmt.Sprintf("Failed to translate API response: %v", err)) + return + } + + resp.Diagnostics.Append(resp.State.Set(ctx, model)...) + 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") + } + name := *machineType.Name + + model.Id = utils.BuildInternalTerraformId(model.ProjectId.ValueString(), 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 + filtered := make([]*iaas.MachineType, 0) + 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 000000000..4787844b7 --- /dev/null +++ b/stackit/internal/services/iaas/machinetype/datasource_test.go @@ -0,0 +1,264 @@ +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: mustMapString(map[string]string{ + "cpu": "amd-epycrome-7702", + "overcommit": "1", + "environment": "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, + }, + { + name: "equal names are stable", + input: []*iaas.MachineType{{Name: utils.Ptr("same")}, {Name: utils.Ptr("same")}, {Name: utils.Ptr("same")}}, + ascending: true, + expected: []string{"same", "same", "same"}, + expectError: false, + }, + } + + 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) + } + }) + } +} + +func mustMapString(val map[string]string) types.Map { + m, diags := types.MapValue(types.StringType, convertToAttr(val)) + if diags.HasError() { + panic("invalid test: " + diags.Errors()[0].Detail()) + } + return m +} + +func convertToAttr(m map[string]string) map[string]attr.Value { + res := make(map[string]attr.Value, len(m)) + for k, v := range m { + res[k] = types.StringValue(v) + } + return res +} 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 000000000..3475f34db --- /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/provider.go b/stackit/provider.go index f1079bec3..698af5c85 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, From b2546aee40f6fe9a38d41b764941e6218d601fa5 Mon Sep 17 00:00:00 2001 From: Mauritz Uphoff Date: Fri, 18 Jul 2025 14:43:18 +0200 Subject: [PATCH 2/3] review Signed-off-by: Mauritz Uphoff --- .../services/iaas/machinetype/datasource.go | 23 +++++--------- .../iaas/machinetype/datasource_test.go | 31 +++---------------- 2 files changed, 11 insertions(+), 43 deletions(-) diff --git a/stackit/internal/services/iaas/machinetype/datasource.go b/stackit/internal/services/iaas/machinetype/datasource.go index 70e8a417c..3f3301126 100644 --- a/stackit/internal/services/iaas/machinetype/datasource.go +++ b/stackit/internal/services/iaas/machinetype/datasource.go @@ -142,10 +142,8 @@ func (m *machineTypeDataSource) Read(ctx context.Context, req datasource.ReadReq listMachineTypeReq := m.client.ListMachineTypes(ctx, projectId) - if !model.Filter.IsNull() && !model.Filter.IsUnknown() { - if filter := strings.TrimSpace(model.Filter.ValueString()); filter != "" { - listMachineTypeReq = listMachineTypeReq.Filter(filter) - } + if !model.Filter.IsNull() && !model.Filter.IsUnknown() && strings.TrimSpace(model.Filter.ValueString()) != "" { + listMachineTypeReq = listMachineTypeReq.Filter(strings.TrimSpace(model.Filter.ValueString())) } apiResp, err := listMachineTypeReq.Execute() @@ -160,16 +158,15 @@ func (m *machineTypeDataSource) Read(ctx context.Context, req datasource.ReadReq return } - items := apiResp.Items - if items == nil || len(*items) == 0 { + 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(*items)) - for i := range *items { - machineTypes[i] = &(*items)[i] + machineTypes := make([]*iaas.MachineType, len(*apiResp.Items)) + for i := range *apiResp.Items { + machineTypes[i] = &(*apiResp.Items)[i] } sorted, err := sortMachineTypeByName(machineTypes, sortAscending) @@ -184,11 +181,6 @@ func (m *machineTypeDataSource) Read(ctx context.Context, req datasource.ReadReq return } - if err := mapDataSourceFields(ctx, first, &model); err != nil { - core.LogAndAddError(ctx, &resp.Diagnostics, "Mapping error", fmt.Sprintf("Failed to translate API response: %v", err)) - return - } - resp.Diagnostics.Append(resp.State.Set(ctx, model)...) tflog.Info(ctx, "Successfully read machine type") } @@ -201,9 +193,8 @@ func mapDataSourceFields(ctx context.Context, machineType *iaas.MachineType, mod if machineType.Name == nil || *machineType.Name == "" { return fmt.Errorf("machine type name is missing") } - name := *machineType.Name - model.Id = utils.BuildInternalTerraformId(model.ProjectId.ValueString(), name) + 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) diff --git a/stackit/internal/services/iaas/machinetype/datasource_test.go b/stackit/internal/services/iaas/machinetype/datasource_test.go index 4787844b7..3fde4794d 100644 --- a/stackit/internal/services/iaas/machinetype/datasource_test.go +++ b/stackit/internal/services/iaas/machinetype/datasource_test.go @@ -45,10 +45,10 @@ func TestMapDataSourceFields(t *testing.T) { Disk: types.Int64Value(20), Ram: types.Int64Value(2048), Vcpus: types.Int64Value(2), - ExtraSpecs: mustMapString(map[string]string{ - "cpu": "amd-epycrome-7702", - "overcommit": "1", - "environment": "general", + 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, @@ -209,13 +209,6 @@ func TestSortMachineTypeByName(t *testing.T) { ascending: true, expectError: true, }, - { - name: "equal names are stable", - input: []*iaas.MachineType{{Name: utils.Ptr("same")}, {Name: utils.Ptr("same")}, {Name: utils.Ptr("same")}}, - ascending: true, - expected: []string{"same", "same", "same"}, - expectError: false, - }, } for _, tt := range tests { @@ -246,19 +239,3 @@ func TestSortMachineTypeByName(t *testing.T) { }) } } - -func mustMapString(val map[string]string) types.Map { - m, diags := types.MapValue(types.StringType, convertToAttr(val)) - if diags.HasError() { - panic("invalid test: " + diags.Errors()[0].Detail()) - } - return m -} - -func convertToAttr(m map[string]string) map[string]attr.Value { - res := make(map[string]attr.Value, len(m)) - for k, v := range m { - res[k] = types.StringValue(v) - } - return res -} From 19fd98a8719e9d1694844d639d1ba1bcd1f3c464 Mon Sep 17 00:00:00 2001 From: Mauritz Uphoff Date: Thu, 28 Aug 2025 11:49:32 +0200 Subject: [PATCH 3/3] review Signed-off-by: Mauritz Uphoff --- docs/data-sources/machine_type.md | 3 ++ .../internal/services/iaas/iaas_acc_test.go | 6 ++-- .../services/iaas/machinetype/datasource.go | 30 ++++++++++++------- stackit/internal/testutil/testutil.go | 17 +++++++++++ 4 files changed, 42 insertions(+), 14 deletions(-) diff --git a/docs/data-sources/machine_type.md b/docs/data-sources/machine_type.md index c096f1d01..2faa8da41 100644 --- a/docs/data-sources/machine_type.md +++ b/docs/data-sources/machine_type.md @@ -4,12 +4,15 @@ 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 diff --git a/stackit/internal/services/iaas/iaas_acc_test.go b/stackit/internal/services/iaas/iaas_acc_test.go index 64a4108f8..d0f91249b 100644 --- a/stackit/internal/services/iaas/iaas_acc_test.go +++ b/stackit/internal/services/iaas/iaas_acc_test.go @@ -4061,14 +4061,14 @@ func TestAccProject(t *testing.T) { }) } -func TestAccMachineTyp(t *testing.T) { - t.Logf("TestAccMachineTyp projectid: %s", testutil.ConvertConfigVariable(testConfigMachineTypeVars["project_id"])) +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.IaaSProviderConfig()), + 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"), diff --git a/stackit/internal/services/iaas/machinetype/datasource.go b/stackit/internal/services/iaas/machinetype/datasource.go index 3f3301126..7bc9ce557 100644 --- a/stackit/internal/services/iaas/machinetype/datasource.go +++ b/stackit/internal/services/iaas/machinetype/datasource.go @@ -16,6 +16,7 @@ import ( "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" @@ -46,28 +47,33 @@ type machineTypeDataSource struct { client *iaas.APIClient } -func (m *machineTypeDataSource) Metadata(_ context.Context, req datasource.MetadataRequest, resp *datasource.MetadataResponse) { +func (d *machineTypeDataSource) Metadata(_ context.Context, req datasource.MetadataRequest, resp *datasource.MetadataResponse) { resp.TypeName = req.ProviderTypeName + "_machine_type" } -func (m *machineTypeDataSource) Configure(ctx context.Context, req datasource.ConfigureRequest, resp *datasource.ConfigureResponse) { +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 } - m.client = client + d.client = client tflog.Info(ctx, "IAAS client configured") } -func (m *machineTypeDataSource) Schema(_ context.Context, _ datasource.SchemaRequest, resp *datasource.SchemaResponse) { +func (d *machineTypeDataSource) Schema(_ context.Context, _ datasource.SchemaRequest, resp *datasource.SchemaResponse) { resp.Schema = schema.Schema{ - MarkdownDescription: "Machine type data source.", + 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`\".", @@ -126,7 +132,7 @@ See https://expr-lang.org/docs/language-definition for syntax.`, } } -func (m *machineTypeDataSource) Read(ctx context.Context, req datasource.ReadRequest, resp *datasource.ReadResponse) { // nolint:gocritic // function signature required by Terraform +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() { @@ -140,7 +146,7 @@ func (m *machineTypeDataSource) Read(ctx context.Context, req datasource.ReadReq ctx = tflog.SetField(ctx, "filter_is_null", model.Filter.IsNull()) ctx = tflog.SetField(ctx, "filter_is_unknown", model.Filter.IsUnknown()) - listMachineTypeReq := m.client.ListMachineTypes(ctx, projectId) + 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())) @@ -175,13 +181,15 @@ func (m *machineTypeDataSource) Read(ctx context.Context, req datasource.ReadReq return } - first := sorted[0] - if err := mapDataSourceFields(ctx, first, &model); err != nil { - core.LogAndAddError(ctx, &resp.Diagnostics, "Mapping error", fmt.Sprintf("Failed to translate API response: %v", err)) + 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") } @@ -219,7 +227,7 @@ func sortMachineTypeByName(input []*iaas.MachineType, ascending bool) ([]*iaas.M } // Filter out nil or missing name - filtered := make([]*iaas.MachineType, 0) + var filtered []*iaas.MachineType for _, m := range input { if m != nil && m.Name != nil { filtered = append(filtered, m) diff --git a/stackit/internal/testutil/testutil.go b/stackit/internal/testutil/testutil.go index 77dee35fb..4c1d46a6e 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 `