diff --git a/docs/stackit_network-interface_list.md b/docs/stackit_network-interface_list.md index f202a6667..8ed2447b3 100644 --- a/docs/stackit_network-interface_list.md +++ b/docs/stackit_network-interface_list.md @@ -13,6 +13,9 @@ stackit network-interface list [flags] ### Examples ``` + Lists all network interfaces in your current project + $ stackit network-interface list + Lists all network interfaces with network ID "xxx" $ stackit network-interface list --network-id xxx diff --git a/internal/cmd/network-interface/list/list.go b/internal/cmd/network-interface/list/list.go index 21c41a260..6deb0a7ac 100644 --- a/internal/cmd/network-interface/list/list.go +++ b/internal/cmd/network-interface/list/list.go @@ -1,9 +1,12 @@ package list import ( + "bytes" "context" "fmt" + "sort" + "github.com/stackitcloud/stackit-cli/internal/pkg/projectname" "github.com/stackitcloud/stackit-cli/internal/pkg/types" "github.com/spf13/cobra" @@ -40,6 +43,10 @@ func NewCmd(params *types.CmdParams) *cobra.Command { Long: "Lists all network interfaces of a network.", Args: args.NoArgs, Example: examples.Build( + examples.NewExample( + `Lists all network interfaces in your current project`, + `$ stackit network-interface list`, + ), examples.NewExample( `Lists all network interfaces with network ID "xxx"`, `$ stackit network-interface list --network-id xxx`, @@ -71,22 +78,42 @@ func NewCmd(params *types.CmdParams) *cobra.Command { } // Call API - req := buildRequest(ctx, model, apiClient) - resp, err := req.Execute() - if err != nil { - return fmt.Errorf("list network interfaces: %w", err) - } + if model.NetworkId == "" { + // Return all NICs in the Project + req := buildProjectRequest(ctx, model, apiClient) + projectLabel, err := projectname.GetProjectName(ctx, params.Printer, params.CliVersion, cmd) + if err != nil { + projectLabel = model.ProjectId + } - if resp.Items == nil || len(*resp.Items) == 0 { - networkLabel, err := iaasUtils.GetNetworkName(ctx, apiClient, model.ProjectId, model.Region, model.NetworkId) + resp, err := req.Execute() if err != nil { - params.Printer.Debug(print.ErrorLevel, "get network name: %v", err) - networkLabel = model.NetworkId - } else if networkLabel == "" { - networkLabel = model.NetworkId + return fmt.Errorf("list network interfaces: %w", err) + } + + // Truncate output + items := *resp.Items + if model.Limit != nil && len(items) > int(*model.Limit) { + items = items[:*model.Limit] } - params.Printer.Info("No network interfaces found for network %q\n", networkLabel) - return nil + + return outputProjectResult(params.Printer, model.OutputFormat, items, projectLabel) + } + + // Return the NICs for one Network + req := buildNetworkRequest(ctx, model, apiClient) + + 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 + } + + resp, err := req.Execute() + if err != nil { + return fmt.Errorf("list network interfaces: %w", err) } // Truncate output @@ -95,7 +122,7 @@ func NewCmd(params *types.CmdParams) *cobra.Command { items = items[:*model.Limit] } - return outputResult(params.Printer, model.OutputFormat, items) + return outputNetworkResult(params.Printer, model.OutputFormat, items, networkLabel) }, } configureFlags(cmd) @@ -106,9 +133,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) { @@ -136,7 +160,16 @@ func parseInput(p *print.Printer, cmd *cobra.Command, _ []string) (*inputModel, return &model, nil } -func buildRequest(ctx context.Context, model *inputModel, apiClient *iaas.APIClient) iaas.ApiListNicsRequest { +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 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) @@ -145,8 +178,47 @@ func buildRequest(ctx context.Context, model *inputModel, apiClient *iaas.APICli return req } -func outputResult(p *print.Printer, outputFormat string, nics []iaas.NIC) error { +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 + } + + sort.SliceStable(nics, func(i, j int) bool { + result := bytes.Compare([]byte(*nics[i].NetworkId), []byte(*nics[j].NetworkId)) + return result == -1 + }) + + 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") diff --git a/internal/cmd/network-interface/list/list_test.go b/internal/cmd/network-interface/list/list_test.go index d04c1d9b7..312b4a737 100644 --- a/internal/cmd/network-interface/list/list_test.go +++ b/internal/cmd/network-interface/list/list_test.go @@ -61,7 +61,16 @@ func fixtureInputModel(mods ...func(model *inputModel)) *inputModel { 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 { @@ -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 @@ -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), @@ -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 @@ -191,12 +228,55 @@ func TestOutputResult(t *testing.T) { args: args{}, 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 := outputProjectResult(p, tt.args.outputFormat, tt.args.nics, "Label"); (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: "empty", + args: args{}, + 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, "Label"); (err != nil) != tt.wantErr { t.Errorf("outputResult() error = %v, wantErr %v", err, tt.wantErr) } })