Skip to content

feat(iaas): add datasource to query machine types #916

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 3 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
67 changes: 67 additions & 0 deletions docs/data-sources/machine_type.md
Original file line number Diff line number Diff line change
@@ -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 generated by tfplugindocs -->
## 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.
21 changes: 21 additions & 0 deletions examples/data-sources/stackit_machine_type/data-source.tf
Original file line number Diff line number Diff line change
@@ -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"
}
48 changes: 48 additions & 0 deletions stackit/internal/services/iaas/iaas_acc_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 (
Expand Down Expand Up @@ -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

Expand Down Expand Up @@ -4022,6 +4029,47 @@ func TestAccImageMax(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,
Expand Down
237 changes: 237 additions & 0 deletions stackit/internal/services/iaas/machinetype/datasource.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,237 @@
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() && 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
}

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
}

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")
}

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
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
}
Loading
Loading