Skip to content

Commit fce91f3

Browse files
committed
feat(iaas): add client-side vcpu and ram filter for machine-types
1 parent b7bad48 commit fce91f3

File tree

3 files changed

+180
-69
lines changed

3 files changed

+180
-69
lines changed

docs/stackit_server_machine-type_list.md

Lines changed: 7 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ Get list of all machine types available in a project
44

55
### Synopsis
66

7-
Get list of all machine types available in a project.
7+
Get list of all machine types available in a project. Supports filtering by minimum vCPU and RAM (GB).
88

99
```
1010
stackit server machine-type list [flags]
@@ -16,18 +16,17 @@ stackit server machine-type list [flags]
1616
Get list of all machine types
1717
$ stackit server machine-type list
1818
19-
Get list of all machine types in JSON format
20-
$ stackit server machine-type list --output-format json
21-
22-
List the first 10 machine types
23-
$ stackit server machine-type list --limit=10
19+
Filter for machines with at least 8 vCPUs and 16GB RAM
20+
$ stackit server machine-type list --min-vcpu 8 --min-ram 16
2421
```
2522

2623
### Options
2724

2825
```
29-
-h, --help Help for "stackit server machine-type list"
30-
--limit int Limit the output to the first n elements
26+
-h, --help Help for "stackit server machine-type list"
27+
--limit int Limit the output to the first n elements
28+
--min-ram int Filter by minimum RAM amount in GB
29+
--min-vcpu int Filter by minimum number of vCPUs
3130
```
3231

3332
### Options inherited from parent commands

internal/cmd/server/machine-type/list/list.go

Lines changed: 78 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -23,31 +23,31 @@ import (
2323

2424
type inputModel struct {
2525
*globalflags.GlobalFlagModel
26-
Limit *int64
26+
Limit *int64
27+
MinVCPUs *int64
28+
MinRAM *int64
2729
}
2830

2931
const (
30-
limitFlag = "limit"
32+
limitFlag = "limit"
33+
minVcpuFlag = "min-vcpu"
34+
minRamFlag = "min-ram"
3135
)
3236

3337
func NewCmd(params *types.CmdParams) *cobra.Command {
3438
cmd := &cobra.Command{
3539
Use: "list",
3640
Short: "Get list of all machine types available in a project",
37-
Long: "Get list of all machine types available in a project.",
41+
Long: "Get list of all machine types available in a project. Supports filtering by minimum vCPU and RAM (GB).",
3842
Args: args.NoArgs,
3943
Example: examples.Build(
4044
examples.NewExample(
4145
`Get list of all machine types`,
4246
"$ stackit server machine-type list",
4347
),
4448
examples.NewExample(
45-
`Get list of all machine types in JSON format`,
46-
"$ stackit server machine-type list --output-format json",
47-
),
48-
examples.NewExample(
49-
`List the first 10 machine types`,
50-
`$ stackit server machine-type list --limit=10`,
49+
`Filter for machines with at least 8 vCPUs and 16GB RAM`,
50+
"$ stackit server machine-type list --min-vcpu 8 --min-ram 16",
5151
),
5252
),
5353
RunE: func(cmd *cobra.Command, args []string) error {
@@ -73,18 +73,26 @@ func NewCmd(params *types.CmdParams) *cobra.Command {
7373
if resp.Items == nil || len(*resp.Items) == 0 {
7474
projectLabel, err := projectname.GetProjectName(ctx, params.Printer, params.CliVersion, cmd)
7575
if err != nil {
76-
params.Printer.Debug(print.ErrorLevel, "get project name: %v", err)
7776
projectLabel = model.ProjectId
7877
}
7978
params.Printer.Info("No machine-types found for project %q\n", projectLabel)
8079
return nil
8180
}
8281

83-
// limit output
84-
if model.Limit != nil && len(*resp.Items) > int(*model.Limit) {
85-
*resp.Items = (*resp.Items)[:*model.Limit]
82+
// Filter the items client-side
83+
filteredItems := filterMachineTypes(resp.Items, model)
84+
85+
if len(filteredItems) == 0 {
86+
params.Printer.Info("No machine-types found matching the criteria\n")
87+
return nil
8688
}
8789

90+
// Apply limit to results
91+
if model.Limit != nil && len(filteredItems) > int(*model.Limit) {
92+
filteredItems = filteredItems[:*model.Limit]
93+
}
94+
95+
resp.Items = &filteredItems
8896
return outputResult(params.Printer, model.OutputFormat, *resp)
8997
},
9098
}
@@ -95,6 +103,8 @@ func NewCmd(params *types.CmdParams) *cobra.Command {
95103

96104
func configureFlags(cmd *cobra.Command) {
97105
cmd.Flags().Int64(limitFlag, 0, "Limit the output to the first n elements")
106+
cmd.Flags().Int64(minVcpuFlag, 0, "Filter by minimum number of vCPUs")
107+
cmd.Flags().Int64(minRamFlag, 0, "Filter by minimum RAM amount in GB")
98108
}
99109

100110
func parseInput(p *print.Printer, cmd *cobra.Command, _ []string) (*inputModel, error) {
@@ -105,21 +115,48 @@ func parseInput(p *print.Printer, cmd *cobra.Command, _ []string) (*inputModel,
105115

106116
limit := flags.FlagToInt64Pointer(p, cmd, limitFlag)
107117
if limit != nil && *limit < 1 {
108-
return nil, &errors.FlagValidationError{
109-
Flag: limitFlag,
110-
Details: "must be greater than 0",
111-
}
118+
return nil, &errors.FlagValidationError{Flag: limitFlag, Details: "must be greater than 0"}
112119
}
113120

114121
model := inputModel{
115122
GlobalFlagModel: globalFlags,
116-
Limit: flags.FlagToInt64Pointer(p, cmd, limitFlag),
123+
Limit: limit,
124+
MinVCPUs: flags.FlagToInt64Pointer(p, cmd, minVcpuFlag),
125+
MinRAM: flags.FlagToInt64Pointer(p, cmd, minRamFlag),
117126
}
118127

119128
p.DebugInputModel(model)
120129
return &model, nil
121130
}
122131

132+
// filterMachineTypes applies logic to filter by resource minimums.
133+
// Note: Deprecated items are NOT hidden.
134+
func filterMachineTypes(items *[]iaas.MachineType, model *inputModel) []iaas.MachineType {
135+
if items == nil {
136+
return []iaas.MachineType{}
137+
}
138+
139+
var filtered []iaas.MachineType
140+
for _, item := range *items {
141+
// Minimum vCPU check
142+
if model.MinVCPUs != nil && *model.MinVCPUs > 0 {
143+
if item.Vcpus == nil || *item.Vcpus < *model.MinVCPUs {
144+
continue
145+
}
146+
}
147+
148+
// Minimum RAM check (converting API MB to GB)
149+
if model.MinRAM != nil && *model.MinRAM > 0 {
150+
if item.Ram == nil || (*item.Ram/1024) < *model.MinRAM {
151+
continue
152+
}
153+
}
154+
155+
filtered = append(filtered, item)
156+
}
157+
return filtered
158+
}
159+
123160
func buildRequest(ctx context.Context, model *inputModel, apiClient *iaas.APIClient) iaas.ApiListMachineTypesRequest {
124161
return apiClient.ListMachineTypes(ctx, model.ProjectId, model.Region)
125162
}
@@ -128,19 +165,32 @@ func outputResult(p *print.Printer, outputFormat string, machineTypes iaas.Machi
128165
return p.OutputResult(outputFormat, machineTypes, func() error {
129166
table := tables.NewTable()
130167
table.SetTitle("Machine-Types")
168+
table.SetHeader("NAME", "VCPUS", "RAM (GB)", "DESCRIPTION", "EXTRA SPECS")
131169

132-
table.SetHeader("NAME", "DESCRIPTION")
133170
if items := machineTypes.GetItems(); len(items) > 0 {
134-
for _, machineType := range items {
135-
table.AddRow(*machineType.Name, utils.PtrString(machineType.Description))
136-
}
137-
}
171+
for _, mt := range items {
172+
extraSpecMap := make(map[string]string)
173+
if mt.ExtraSpecs != nil && len(*mt.ExtraSpecs) > 0 {
174+
for key, value := range *mt.ExtraSpecs {
175+
extraSpecMap[key] = fmt.Sprintf("%v", value)
176+
}
177+
}
138178

139-
err := table.Display(p)
140-
if err != nil {
141-
return fmt.Errorf("render table: %w", err)
142-
}
179+
ramGB := int64(0)
180+
if mt.Ram != nil {
181+
ramGB = *mt.Ram / 1024
182+
}
143183

144-
return nil
184+
table.AddRow(
185+
utils.PtrString(mt.Name),
186+
utils.PtrValue(mt.Vcpus),
187+
ramGB,
188+
utils.PtrString(mt.Description),
189+
utils.JoinStringMap(extraSpecMap, ": ", "\n"),
190+
)
191+
table.AddSeparator()
192+
}
193+
}
194+
return table.Display(p)
145195
})
146196
}

internal/cmd/server/machine-type/list/list_test.go

Lines changed: 95 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -31,8 +31,7 @@ func fixtureFlagValues(mods ...func(flagValues map[string]string)) map[string]st
3131
flagValues := map[string]string{
3232
globalflags.ProjectIdFlag: testProjectId,
3333
globalflags.RegionFlag: testRegion,
34-
35-
limitFlag: "10",
34+
limitFlag: "10",
3635
}
3736
for _, mod := range mods {
3837
mod(flagValues)
@@ -47,7 +46,9 @@ func fixtureInputModel(mods ...func(model *inputModel)) *inputModel {
4746
ProjectId: testProjectId,
4847
Region: testRegion,
4948
},
50-
Limit: utils.Ptr(int64(10)),
49+
Limit: utils.Ptr(int64(10)),
50+
MinVCPUs: nil,
51+
MinRAM: nil,
5152
}
5253
for _, mod := range mods {
5354
mod(model)
@@ -83,50 +84,90 @@ func TestParseInput(t *testing.T) {
8384
isValid: false,
8485
},
8586
{
86-
description: "no flag values",
87-
flagValues: map[string]string{},
88-
isValid: false,
89-
},
90-
{
91-
description: "project id missing",
87+
description: "filter by resources valid",
9288
flagValues: fixtureFlagValues(func(flagValues map[string]string) {
93-
delete(flagValues, globalflags.ProjectIdFlag)
89+
flagValues[minVcpuFlag] = "16"
90+
flagValues[minRamFlag] = "32"
91+
}),
92+
isValid: true,
93+
expectedModel: fixtureInputModel(func(model *inputModel) {
94+
model.MinVCPUs = utils.Ptr(int64(16))
95+
model.MinRAM = utils.Ptr(int64(32))
9496
}),
95-
isValid: false,
9697
},
98+
}
99+
100+
for _, tt := range tests {
101+
t.Run(tt.description, func(t *testing.T) {
102+
testutils.TestParseInput(t, NewCmd, parseInput, tt.expectedModel, tt.argValues, tt.flagValues, tt.isValid)
103+
})
104+
}
105+
}
106+
107+
func TestFilterMachineTypes(t *testing.T) {
108+
items := []iaas.MachineType{
97109
{
98-
description: "project id invalid 1",
99-
flagValues: fixtureFlagValues(func(flagValues map[string]string) {
100-
flagValues[globalflags.ProjectIdFlag] = ""
101-
}),
102-
isValid: false,
110+
Name: utils.Ptr("c3i.16"),
111+
Vcpus: utils.Ptr(int64(16)),
112+
Ram: utils.Ptr(int64(32768)),
113+
Description: utils.Ptr("Intel Emerald Rapids 8580 CPU instance"),
114+
ExtraSpecs: &map[string]interface{}{
115+
"hw:cpu_sockets": "1",
116+
"hw:mem_page_size": "large",
117+
"aggregate": "intel-gen3-oc-cpu-optimized",
118+
},
103119
},
104120
{
105-
description: "project id invalid 2",
106-
flagValues: fixtureFlagValues(func(flagValues map[string]string) {
107-
flagValues[globalflags.ProjectIdFlag] = "invalid-uuid"
108-
}),
109-
isValid: false,
121+
Name: utils.Ptr("c3i.28"),
122+
Vcpus: utils.Ptr(int64(28)),
123+
Ram: utils.Ptr(int64(60416)),
124+
Description: utils.Ptr("Intel Emerald Rapids 8580 CPU instance"),
125+
ExtraSpecs: &map[string]interface{}{
126+
"cpu": "intel-emerald-rapids-8580-dual-socket",
127+
"overcommit": "4",
128+
"hw:mem_page_size": "large",
129+
},
110130
},
111131
{
112-
description: "limit invalid",
113-
flagValues: fixtureFlagValues(func(flagValues map[string]string) {
114-
flagValues[limitFlag] = "invalid"
115-
}),
116-
isValid: false,
132+
Name: utils.Ptr("g2i.1"),
133+
Vcpus: utils.Ptr(int64(1)),
134+
Ram: utils.Ptr(int64(4096)),
135+
Description: utils.Ptr("Intel Ice Lake 4316 CPU instance"),
117136
},
137+
}
138+
139+
tests := []struct {
140+
description string
141+
items *[]iaas.MachineType
142+
model *inputModel
143+
expectedLen int
144+
}{
118145
{
119-
description: "limit invalid 2",
120-
flagValues: fixtureFlagValues(func(flagValues map[string]string) {
121-
flagValues[limitFlag] = "0"
122-
}),
123-
isValid: false,
146+
description: "base filters",
147+
items: &items,
148+
model: fixtureInputModel(),
149+
expectedLen: 3,
150+
},
151+
{
152+
description: "nil items slice",
153+
items: nil,
154+
model: fixtureInputModel(),
155+
expectedLen: 0,
156+
},
157+
{
158+
description: "filter min-vcpu 20",
159+
items: &items,
160+
model: fixtureInputModel(func(m *inputModel) { m.MinVCPUs = utils.Ptr(int64(20)) }),
161+
expectedLen: 1, // c3i.28 only
124162
},
125163
}
126164

127165
for _, tt := range tests {
128166
t.Run(tt.description, func(t *testing.T) {
129-
testutils.TestParseInput(t, NewCmd, parseInput, tt.expectedModel, tt.argValues, tt.flagValues, tt.isValid)
167+
result := filterMachineTypes(tt.items, tt.model)
168+
if len(result) != tt.expectedLen {
169+
t.Errorf("expected %d items, got %d", tt.expectedLen, len(result))
170+
}
130171
})
131172
}
132173
}
@@ -170,10 +211,31 @@ func TestOutputResult(t *testing.T) {
170211
wantErr bool
171212
}{
172213
{
173-
name: "empty",
214+
name: "empty response",
174215
args: args{},
175216
wantErr: false,
176217
},
218+
{
219+
name: "response with extra specs",
220+
args: args{
221+
outputFormat: "table",
222+
machineTypes: iaas.MachineTypeListResponse{
223+
Items: &[]iaas.MachineType{
224+
{
225+
Name: utils.Ptr("c3i.16"),
226+
Vcpus: utils.Ptr(int64(16)),
227+
Ram: utils.Ptr(int64(32768)),
228+
ExtraSpecs: &map[string]interface{}{
229+
"aggregate": "intel-gen3",
230+
"overcommit": 4,
231+
},
232+
Description: utils.Ptr("Intel Emerald Rapids 8580 CPU instance"),
233+
},
234+
},
235+
},
236+
},
237+
wantErr: false,
238+
},
177239
}
178240
p := print.NewPrinter()
179241
p.Cmd = NewCmd(&types.CmdParams{Printer: p})

0 commit comments

Comments
 (0)