Skip to content
Merged
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
3 changes: 3 additions & 0 deletions docs/stackit_network-interface_list.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,9 @@ stackit network-interface list [flags]
### Examples

```
Lists all network interfaces
$ stackit network-interface list

Lists all network interfaces with network ID "xxx"
$ stackit network-interface list --network-id xxx

Expand Down
116 changes: 94 additions & 22 deletions internal/cmd/network-interface/list/list.go
Original file line number Diff line number Diff line change
@@ -1,9 +1,12 @@
package list

import (
"cmp"
"context"
"fmt"
"slices"

"github.com/stackitcloud/stackit-cli/internal/pkg/projectname"
"github.com/stackitcloud/stackit-cli/internal/pkg/types"

"github.com/spf13/cobra"
Expand All @@ -30,7 +33,7 @@ type inputModel struct {
*globalflags.GlobalFlagModel
Limit *int64
LabelSelector *string
NetworkId string
NetworkId *string
}

func NewCmd(params *types.CmdParams) *cobra.Command {
Expand All @@ -40,6 +43,11 @@ func NewCmd(params *types.CmdParams) *cobra.Command {
Long: "Lists all network interfaces of a network.",
Args: args.NoArgs,
Example: examples.Build(
// Note: this subcommand uses two different API enpoints, which makes the implementation somewhat messy
examples.NewExample(
`Lists all network interfaces`,
`$ stackit network-interface list`,
),
examples.NewExample(
`Lists all network interfaces with network ID "xxx"`,
`$ stackit network-interface list --network-id xxx`,
Expand Down Expand Up @@ -70,32 +78,52 @@ func NewCmd(params *types.CmdParams) *cobra.Command {
return err
}

// Call API
req := buildRequest(ctx, model, apiClient)
if model.NetworkId == nil {
// Call API to get all NICs in the Project
req := buildProjectRequest(ctx, model, apiClient)

resp, err := req.Execute()
if err != nil {
return fmt.Errorf("list network interfaces: %w", err)
}

projectLabel, err := projectname.GetProjectName(ctx, params.Printer, params.CliVersion, cmd)
if err != nil {
projectLabel = model.ProjectId
}

// Truncate output
items := utils.GetSliceFromPointer(resp.Items)
if model.Limit != nil && len(items) > int(*model.Limit) {
items = items[:*model.Limit]
}

return outputProjectResult(params.Printer, model.OutputFormat, items, projectLabel)
}

// Call API to get NICs for one Network
req := buildNetworkRequest(ctx, model, apiClient)

resp, err := req.Execute()
if err != nil {
return fmt.Errorf("list network interfaces: %w", err)
}

if resp.Items == nil || len(*resp.Items) == 0 {
networkLabel, err := iaasUtils.GetNetworkName(ctx, apiClient, model.ProjectId, model.Region, model.NetworkId)
if err != nil {
params.Printer.Debug(print.ErrorLevel, "get network name: %v", err)
networkLabel = model.NetworkId
} else if networkLabel == "" {
networkLabel = model.NetworkId
}
params.Printer.Info("No network interfaces found for network %q\n", networkLabel)
return nil
networkLabel, err := iaasUtils.GetNetworkName(ctx, apiClient, model.ProjectId, model.Region, *model.NetworkId)
if err != nil {
params.Printer.Debug(print.ErrorLevel, "get network name: %v", err)
networkLabel = *model.NetworkId
} else if networkLabel == "" {
networkLabel = *model.NetworkId
}

// Truncate output
items := *resp.Items
items := utils.GetSliceFromPointer(resp.Items)
if model.Limit != nil && len(items) > int(*model.Limit) {
items = items[:*model.Limit]
}

return outputResult(params.Printer, model.OutputFormat, items)
return outputNetworkResult(params.Printer, model.OutputFormat, items, networkLabel)
},
}
configureFlags(cmd)
Expand All @@ -106,9 +134,6 @@ func configureFlags(cmd *cobra.Command) {
cmd.Flags().Var(flags.UUIDFlag(), networkIdFlag, "Network ID")
cmd.Flags().Int64(limitFlag, 0, "Maximum number of entries to list")
cmd.Flags().String(labelSelectorFlag, "", "Filter by label")

err := flags.MarkFlagsRequired(cmd, networkIdFlag)
cobra.CheckErr(err)
}

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

p.DebugInputModel(model)
return &model, nil
}

func buildRequest(ctx context.Context, model *inputModel, apiClient *iaas.APIClient) iaas.ApiListNicsRequest {
req := apiClient.ListNics(ctx, model.ProjectId, model.Region, model.NetworkId)
func buildProjectRequest(ctx context.Context, model *inputModel, apiClient *iaas.APIClient) iaas.ApiListProjectNICsRequest {
req := apiClient.ListProjectNICs(ctx, model.ProjectId, model.Region)
if model.LabelSelector != nil {
req = req.LabelSelector(*model.LabelSelector)
}

return req
}

func outputResult(p *print.Printer, outputFormat string, nics []iaas.NIC) error {
func buildNetworkRequest(ctx context.Context, model *inputModel, apiClient *iaas.APIClient) iaas.ApiListNicsRequest {
req := apiClient.ListNics(ctx, model.ProjectId, model.Region, *model.NetworkId)
if model.LabelSelector != nil {
req = req.LabelSelector(*model.LabelSelector)
}

return req
}

func outputProjectResult(p *print.Printer, outputFormat string, nics []iaas.NIC, projectLabel string) error {
return p.OutputResult(outputFormat, nics, func() error {
if len(nics) == 0 {
p.Outputf("No network interfaces found for project %q\n", projectLabel)
return nil
}

slices.SortFunc(nics, func(a, b iaas.NIC) int {
return cmp.Compare(utils.PtrValue(a.NetworkId), utils.PtrValue(b.NetworkId))
})

table := tables.NewTable()
table.SetHeader("ID", "NAME", "NETWORK ID", "NIC SECURITY", "DEVICE ID", "IPv4 ADDRESS", "STATUS", "TYPE")

for _, nic := range nics {
table.AddRow(
utils.PtrString(nic.Id),
utils.PtrString(nic.Name),
utils.PtrString(nic.NetworkId),
utils.PtrString(nic.NicSecurity),
utils.PtrString(nic.Device),
utils.PtrString(nic.Ipv4),
utils.PtrString(nic.Status),
utils.PtrString(nic.Type),
)
table.AddSeparator()
}

p.Outputln(table.Render())
return nil
})
}

func outputNetworkResult(p *print.Printer, outputFormat string, nics []iaas.NIC, networkLabel string) error {
return p.OutputResult(outputFormat, nics, func() error {
if len(nics) == 0 {
p.Outputf("No network interfaces found for network %q\n", networkLabel)
return nil
}

table := tables.NewTable()
table.SetHeader("ID", "NAME", "NIC SECURITY", "DEVICE ID", "IPv4 ADDRESS", "STATUS", "TYPE")

Expand Down
126 changes: 117 additions & 9 deletions internal/cmd/network-interface/list/list_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -53,15 +53,24 @@ func fixtureInputModel(mods ...func(model *inputModel)) *inputModel {
},
Limit: utils.Ptr(int64(10)),
LabelSelector: utils.Ptr(testLabelSelector),
NetworkId: testNetworkId,
NetworkId: utils.Ptr(testNetworkId),
}
for _, mod := range mods {
mod(model)
}
return model
}

func fixtureRequest(mods ...func(request *iaas.ApiListNicsRequest)) iaas.ApiListNicsRequest {
func fixtureProjectRequest(mods ...func(request *iaas.ApiListProjectNICsRequest)) iaas.ApiListProjectNICsRequest {
request := testClient.ListProjectNICs(testCtx, testProjectId, testRegion)
request = request.LabelSelector(testLabelSelector)
for _, mod := range mods {
mod(&request)
}
return request
}

func fixtureNetworkRequest(mods ...func(request *iaas.ApiListNicsRequest)) iaas.ApiListNicsRequest {
request := testClient.ListNics(testCtx, testProjectId, testRegion, testNetworkId)
request = request.LabelSelector(testLabelSelector)
for _, mod := range mods {
Expand Down Expand Up @@ -148,7 +157,35 @@ func TestParseInput(t *testing.T) {
}
}

func TestBuildRequest(t *testing.T) {
func TestBuildProjectRequest(t *testing.T) {
tests := []struct {
description string
model *inputModel
expectedRequest iaas.ApiListProjectNICsRequest
}{
{
description: "base",
model: fixtureInputModel(),
expectedRequest: fixtureProjectRequest(),
},
}

for _, tt := range tests {
t.Run(tt.description, func(t *testing.T) {
request := buildProjectRequest(testCtx, tt.model, testClient)

diff := cmp.Diff(request, tt.expectedRequest,
cmp.AllowUnexported(tt.expectedRequest),
cmpopts.EquateComparable(testCtx),
)
if diff != "" {
t.Fatalf("Data does not match: %s", diff)
}
})
}
}

func TestBuildNetworkRequest(t *testing.T) {
tests := []struct {
description string
model *inputModel
Expand All @@ -157,13 +194,13 @@ func TestBuildRequest(t *testing.T) {
{
description: "base",
model: fixtureInputModel(),
expectedRequest: fixtureRequest(),
expectedRequest: fixtureNetworkRequest(),
},
}

for _, tt := range tests {
t.Run(tt.description, func(t *testing.T) {
request := buildRequest(testCtx, tt.model, testClient)
request := buildNetworkRequest(testCtx, tt.model, testClient)

diff := cmp.Diff(request, tt.expectedRequest,
cmp.AllowUnexported(tt.expectedRequest),
Expand All @@ -176,7 +213,7 @@ func TestBuildRequest(t *testing.T) {
}
}

func TestOutputResult(t *testing.T) {
func TestOutputProjectResult(t *testing.T) {
type args struct {
outputFormat string
nics []iaas.NIC
Expand All @@ -187,16 +224,87 @@ func TestOutputResult(t *testing.T) {
wantErr bool
}{
{
name: "empty",
args: args{},
name: "nil as NIC-slice",
args: args{
outputFormat: print.PrettyOutputFormat,
},
wantErr: false,
},
{
name: "empty NIC-slice",
args: args{
outputFormat: print.PrettyOutputFormat,
nics: []iaas.NIC{},
},
wantErr: false,
},
{
name: "empty NIC in NIC-slice",
args: args{
outputFormat: print.PrettyOutputFormat,
nics: []iaas.NIC{{}},
},
wantErr: false,
},
{
name: "two empty NICs in NIC-slice to verify sorting by network id does not break on nil pointers",
args: args{
outputFormat: print.PrettyOutputFormat,
nics: []iaas.NIC{{}, {}},
},
wantErr: false,
},
}
p := print.NewPrinter()
p.Cmd = NewCmd(&types.CmdParams{Printer: p})
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
if err := outputProjectResult(p, tt.args.outputFormat, tt.args.nics, ""); (err != nil) != tt.wantErr {
t.Errorf("outputResult() error = %v, wantErr %v", err, tt.wantErr)
}
})
}
}

func TestOutputNetworkResult(t *testing.T) {
type args struct {
outputFormat string
nics []iaas.NIC
}
tests := []struct {
name string
args args
wantErr bool
}{
{
name: "nil as NIC-slice",
args: args{
outputFormat: print.PrettyOutputFormat,
},
wantErr: false,
},
{
name: "empty NIC-slice",
args: args{
outputFormat: print.PrettyOutputFormat,
nics: []iaas.NIC{},
},
wantErr: false,
},
{
name: "empty NIC in NIC-slice",
args: args{
outputFormat: print.PrettyOutputFormat,
nics: []iaas.NIC{{}},
},
wantErr: false,
},
}
p := print.NewPrinter()
p.Cmd = NewCmd(&types.CmdParams{Printer: p})
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
if err := outputResult(p, tt.args.outputFormat, tt.args.nics); (err != nil) != tt.wantErr {
if err := outputNetworkResult(p, tt.args.outputFormat, tt.args.nics, ""); (err != nil) != tt.wantErr {
t.Errorf("outputResult() error = %v, wantErr %v", err, tt.wantErr)
}
})
Expand Down
Loading