Skip to content

Commit b047600

Browse files
authored
feat(iaas): list NICs for project (#1242)
relates to STACKITCLI-307 and #1214
1 parent 016e33f commit b047600

File tree

3 files changed

+214
-31
lines changed

3 files changed

+214
-31
lines changed

docs/stackit_network-interface_list.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,9 @@ stackit network-interface list [flags]
1313
### Examples
1414

1515
```
16+
Lists all network interfaces
17+
$ stackit network-interface list
18+
1619
Lists all network interfaces with network ID "xxx"
1720
$ stackit network-interface list --network-id xxx
1821

internal/cmd/network-interface/list/list.go

Lines changed: 94 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,12 @@
11
package list
22

33
import (
4+
"cmp"
45
"context"
56
"fmt"
7+
"slices"
68

9+
"github.com/stackitcloud/stackit-cli/internal/pkg/projectname"
710
"github.com/stackitcloud/stackit-cli/internal/pkg/types"
811

912
"github.com/spf13/cobra"
@@ -30,7 +33,7 @@ type inputModel struct {
3033
*globalflags.GlobalFlagModel
3134
Limit *int64
3235
LabelSelector *string
33-
NetworkId string
36+
NetworkId *string
3437
}
3538

3639
func NewCmd(params *types.CmdParams) *cobra.Command {
@@ -40,6 +43,11 @@ func NewCmd(params *types.CmdParams) *cobra.Command {
4043
Long: "Lists all network interfaces of a network.",
4144
Args: args.NoArgs,
4245
Example: examples.Build(
46+
// Note: this subcommand uses two different API enpoints, which makes the implementation somewhat messy
47+
examples.NewExample(
48+
`Lists all network interfaces`,
49+
`$ stackit network-interface list`,
50+
),
4351
examples.NewExample(
4452
`Lists all network interfaces with network ID "xxx"`,
4553
`$ stackit network-interface list --network-id xxx`,
@@ -70,32 +78,52 @@ func NewCmd(params *types.CmdParams) *cobra.Command {
7078
return err
7179
}
7280

73-
// Call API
74-
req := buildRequest(ctx, model, apiClient)
81+
if model.NetworkId == nil {
82+
// Call API to get all NICs in the Project
83+
req := buildProjectRequest(ctx, model, apiClient)
84+
85+
resp, err := req.Execute()
86+
if err != nil {
87+
return fmt.Errorf("list network interfaces: %w", err)
88+
}
89+
90+
projectLabel, err := projectname.GetProjectName(ctx, params.Printer, params.CliVersion, cmd)
91+
if err != nil {
92+
projectLabel = model.ProjectId
93+
}
94+
95+
// Truncate output
96+
items := utils.GetSliceFromPointer(resp.Items)
97+
if model.Limit != nil && len(items) > int(*model.Limit) {
98+
items = items[:*model.Limit]
99+
}
100+
101+
return outputProjectResult(params.Printer, model.OutputFormat, items, projectLabel)
102+
}
103+
104+
// Call API to get NICs for one Network
105+
req := buildNetworkRequest(ctx, model, apiClient)
106+
75107
resp, err := req.Execute()
76108
if err != nil {
77109
return fmt.Errorf("list network interfaces: %w", err)
78110
}
79111

80-
if resp.Items == nil || len(*resp.Items) == 0 {
81-
networkLabel, err := iaasUtils.GetNetworkName(ctx, apiClient, model.ProjectId, model.Region, model.NetworkId)
82-
if err != nil {
83-
params.Printer.Debug(print.ErrorLevel, "get network name: %v", err)
84-
networkLabel = model.NetworkId
85-
} else if networkLabel == "" {
86-
networkLabel = model.NetworkId
87-
}
88-
params.Printer.Info("No network interfaces found for network %q\n", networkLabel)
89-
return nil
112+
networkLabel, err := iaasUtils.GetNetworkName(ctx, apiClient, model.ProjectId, model.Region, *model.NetworkId)
113+
if err != nil {
114+
params.Printer.Debug(print.ErrorLevel, "get network name: %v", err)
115+
networkLabel = *model.NetworkId
116+
} else if networkLabel == "" {
117+
networkLabel = *model.NetworkId
90118
}
91119

92120
// Truncate output
93-
items := *resp.Items
121+
items := utils.GetSliceFromPointer(resp.Items)
94122
if model.Limit != nil && len(items) > int(*model.Limit) {
95123
items = items[:*model.Limit]
96124
}
97125

98-
return outputResult(params.Printer, model.OutputFormat, items)
126+
return outputNetworkResult(params.Printer, model.OutputFormat, items, networkLabel)
99127
},
100128
}
101129
configureFlags(cmd)
@@ -106,9 +134,6 @@ func configureFlags(cmd *cobra.Command) {
106134
cmd.Flags().Var(flags.UUIDFlag(), networkIdFlag, "Network ID")
107135
cmd.Flags().Int64(limitFlag, 0, "Maximum number of entries to list")
108136
cmd.Flags().String(labelSelectorFlag, "", "Filter by label")
109-
110-
err := flags.MarkFlagsRequired(cmd, networkIdFlag)
111-
cobra.CheckErr(err)
112137
}
113138

114139
func parseInput(p *print.Printer, cmd *cobra.Command, _ []string) (*inputModel, error) {
@@ -129,24 +154,71 @@ func parseInput(p *print.Printer, cmd *cobra.Command, _ []string) (*inputModel,
129154
GlobalFlagModel: globalFlags,
130155
Limit: limit,
131156
LabelSelector: flags.FlagToStringPointer(p, cmd, labelSelectorFlag),
132-
NetworkId: flags.FlagToStringValue(p, cmd, networkIdFlag),
157+
NetworkId: flags.FlagToStringPointer(p, cmd, networkIdFlag),
133158
}
134159

135160
p.DebugInputModel(model)
136161
return &model, nil
137162
}
138163

139-
func buildRequest(ctx context.Context, model *inputModel, apiClient *iaas.APIClient) iaas.ApiListNicsRequest {
140-
req := apiClient.ListNics(ctx, model.ProjectId, model.Region, model.NetworkId)
164+
func buildProjectRequest(ctx context.Context, model *inputModel, apiClient *iaas.APIClient) iaas.ApiListProjectNICsRequest {
165+
req := apiClient.ListProjectNICs(ctx, model.ProjectId, model.Region)
141166
if model.LabelSelector != nil {
142167
req = req.LabelSelector(*model.LabelSelector)
143168
}
144169

145170
return req
146171
}
147172

148-
func outputResult(p *print.Printer, outputFormat string, nics []iaas.NIC) error {
173+
func buildNetworkRequest(ctx context.Context, model *inputModel, apiClient *iaas.APIClient) iaas.ApiListNicsRequest {
174+
req := apiClient.ListNics(ctx, model.ProjectId, model.Region, *model.NetworkId)
175+
if model.LabelSelector != nil {
176+
req = req.LabelSelector(*model.LabelSelector)
177+
}
178+
179+
return req
180+
}
181+
182+
func outputProjectResult(p *print.Printer, outputFormat string, nics []iaas.NIC, projectLabel string) error {
149183
return p.OutputResult(outputFormat, nics, func() error {
184+
if len(nics) == 0 {
185+
p.Outputf("No network interfaces found for project %q\n", projectLabel)
186+
return nil
187+
}
188+
189+
slices.SortFunc(nics, func(a, b iaas.NIC) int {
190+
return cmp.Compare(utils.PtrValue(a.NetworkId), utils.PtrValue(b.NetworkId))
191+
})
192+
193+
table := tables.NewTable()
194+
table.SetHeader("ID", "NAME", "NETWORK ID", "NIC SECURITY", "DEVICE ID", "IPv4 ADDRESS", "STATUS", "TYPE")
195+
196+
for _, nic := range nics {
197+
table.AddRow(
198+
utils.PtrString(nic.Id),
199+
utils.PtrString(nic.Name),
200+
utils.PtrString(nic.NetworkId),
201+
utils.PtrString(nic.NicSecurity),
202+
utils.PtrString(nic.Device),
203+
utils.PtrString(nic.Ipv4),
204+
utils.PtrString(nic.Status),
205+
utils.PtrString(nic.Type),
206+
)
207+
table.AddSeparator()
208+
}
209+
210+
p.Outputln(table.Render())
211+
return nil
212+
})
213+
}
214+
215+
func outputNetworkResult(p *print.Printer, outputFormat string, nics []iaas.NIC, networkLabel string) error {
216+
return p.OutputResult(outputFormat, nics, func() error {
217+
if len(nics) == 0 {
218+
p.Outputf("No network interfaces found for network %q\n", networkLabel)
219+
return nil
220+
}
221+
150222
table := tables.NewTable()
151223
table.SetHeader("ID", "NAME", "NIC SECURITY", "DEVICE ID", "IPv4 ADDRESS", "STATUS", "TYPE")
152224

internal/cmd/network-interface/list/list_test.go

Lines changed: 117 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -53,15 +53,24 @@ func fixtureInputModel(mods ...func(model *inputModel)) *inputModel {
5353
},
5454
Limit: utils.Ptr(int64(10)),
5555
LabelSelector: utils.Ptr(testLabelSelector),
56-
NetworkId: testNetworkId,
56+
NetworkId: utils.Ptr(testNetworkId),
5757
}
5858
for _, mod := range mods {
5959
mod(model)
6060
}
6161
return model
6262
}
6363

64-
func fixtureRequest(mods ...func(request *iaas.ApiListNicsRequest)) iaas.ApiListNicsRequest {
64+
func fixtureProjectRequest(mods ...func(request *iaas.ApiListProjectNICsRequest)) iaas.ApiListProjectNICsRequest {
65+
request := testClient.ListProjectNICs(testCtx, testProjectId, testRegion)
66+
request = request.LabelSelector(testLabelSelector)
67+
for _, mod := range mods {
68+
mod(&request)
69+
}
70+
return request
71+
}
72+
73+
func fixtureNetworkRequest(mods ...func(request *iaas.ApiListNicsRequest)) iaas.ApiListNicsRequest {
6574
request := testClient.ListNics(testCtx, testProjectId, testRegion, testNetworkId)
6675
request = request.LabelSelector(testLabelSelector)
6776
for _, mod := range mods {
@@ -148,7 +157,35 @@ func TestParseInput(t *testing.T) {
148157
}
149158
}
150159

151-
func TestBuildRequest(t *testing.T) {
160+
func TestBuildProjectRequest(t *testing.T) {
161+
tests := []struct {
162+
description string
163+
model *inputModel
164+
expectedRequest iaas.ApiListProjectNICsRequest
165+
}{
166+
{
167+
description: "base",
168+
model: fixtureInputModel(),
169+
expectedRequest: fixtureProjectRequest(),
170+
},
171+
}
172+
173+
for _, tt := range tests {
174+
t.Run(tt.description, func(t *testing.T) {
175+
request := buildProjectRequest(testCtx, tt.model, testClient)
176+
177+
diff := cmp.Diff(request, tt.expectedRequest,
178+
cmp.AllowUnexported(tt.expectedRequest),
179+
cmpopts.EquateComparable(testCtx),
180+
)
181+
if diff != "" {
182+
t.Fatalf("Data does not match: %s", diff)
183+
}
184+
})
185+
}
186+
}
187+
188+
func TestBuildNetworkRequest(t *testing.T) {
152189
tests := []struct {
153190
description string
154191
model *inputModel
@@ -157,13 +194,13 @@ func TestBuildRequest(t *testing.T) {
157194
{
158195
description: "base",
159196
model: fixtureInputModel(),
160-
expectedRequest: fixtureRequest(),
197+
expectedRequest: fixtureNetworkRequest(),
161198
},
162199
}
163200

164201
for _, tt := range tests {
165202
t.Run(tt.description, func(t *testing.T) {
166-
request := buildRequest(testCtx, tt.model, testClient)
203+
request := buildNetworkRequest(testCtx, tt.model, testClient)
167204

168205
diff := cmp.Diff(request, tt.expectedRequest,
169206
cmp.AllowUnexported(tt.expectedRequest),
@@ -176,7 +213,7 @@ func TestBuildRequest(t *testing.T) {
176213
}
177214
}
178215

179-
func TestOutputResult(t *testing.T) {
216+
func TestOutputProjectResult(t *testing.T) {
180217
type args struct {
181218
outputFormat string
182219
nics []iaas.NIC
@@ -187,16 +224,87 @@ func TestOutputResult(t *testing.T) {
187224
wantErr bool
188225
}{
189226
{
190-
name: "empty",
191-
args: args{},
227+
name: "nil as NIC-slice",
228+
args: args{
229+
outputFormat: print.PrettyOutputFormat,
230+
},
231+
wantErr: false,
232+
},
233+
{
234+
name: "empty NIC-slice",
235+
args: args{
236+
outputFormat: print.PrettyOutputFormat,
237+
nics: []iaas.NIC{},
238+
},
239+
wantErr: false,
240+
},
241+
{
242+
name: "empty NIC in NIC-slice",
243+
args: args{
244+
outputFormat: print.PrettyOutputFormat,
245+
nics: []iaas.NIC{{}},
246+
},
247+
wantErr: false,
248+
},
249+
{
250+
name: "two empty NICs in NIC-slice to verify sorting by network id does not break on nil pointers",
251+
args: args{
252+
outputFormat: print.PrettyOutputFormat,
253+
nics: []iaas.NIC{{}, {}},
254+
},
255+
wantErr: false,
256+
},
257+
}
258+
p := print.NewPrinter()
259+
p.Cmd = NewCmd(&types.CmdParams{Printer: p})
260+
for _, tt := range tests {
261+
t.Run(tt.name, func(t *testing.T) {
262+
if err := outputProjectResult(p, tt.args.outputFormat, tt.args.nics, ""); (err != nil) != tt.wantErr {
263+
t.Errorf("outputResult() error = %v, wantErr %v", err, tt.wantErr)
264+
}
265+
})
266+
}
267+
}
268+
269+
func TestOutputNetworkResult(t *testing.T) {
270+
type args struct {
271+
outputFormat string
272+
nics []iaas.NIC
273+
}
274+
tests := []struct {
275+
name string
276+
args args
277+
wantErr bool
278+
}{
279+
{
280+
name: "nil as NIC-slice",
281+
args: args{
282+
outputFormat: print.PrettyOutputFormat,
283+
},
284+
wantErr: false,
285+
},
286+
{
287+
name: "empty NIC-slice",
288+
args: args{
289+
outputFormat: print.PrettyOutputFormat,
290+
nics: []iaas.NIC{},
291+
},
292+
wantErr: false,
293+
},
294+
{
295+
name: "empty NIC in NIC-slice",
296+
args: args{
297+
outputFormat: print.PrettyOutputFormat,
298+
nics: []iaas.NIC{{}},
299+
},
192300
wantErr: false,
193301
},
194302
}
195303
p := print.NewPrinter()
196304
p.Cmd = NewCmd(&types.CmdParams{Printer: p})
197305
for _, tt := range tests {
198306
t.Run(tt.name, func(t *testing.T) {
199-
if err := outputResult(p, tt.args.outputFormat, tt.args.nics); (err != nil) != tt.wantErr {
307+
if err := outputNetworkResult(p, tt.args.outputFormat, tt.args.nics, ""); (err != nil) != tt.wantErr {
200308
t.Errorf("outputResult() error = %v, wantErr %v", err, tt.wantErr)
201309
}
202310
})

0 commit comments

Comments
 (0)