From 0aea155a7f6f7bedd3133e7f0e762cce058a91bf Mon Sep 17 00:00:00 2001 From: Jan Obernberger Date: Mon, 13 Apr 2026 11:17:41 +0200 Subject: [PATCH 01/17] fix returning empty server array in case the response does not return any items via outputResult function instead of returning nil bluntly. adapted tests therefore. --- internal/cmd/server/list/list.go | 27 ++++++++++++++++----------- internal/cmd/server/list/list_test.go | 3 ++- 2 files changed, 18 insertions(+), 12 deletions(-) diff --git a/internal/cmd/server/list/list.go b/internal/cmd/server/list/list.go index 54c058dc0..eeb3507b8 100644 --- a/internal/cmd/server/list/list.go +++ b/internal/cmd/server/list/list.go @@ -78,23 +78,25 @@ func NewCmd(params *types.CmdParams) *cobra.Command { return fmt.Errorf("list servers: %w", err) } - if resp.Items == nil || len(*resp.Items) == 0 { - projectLabel, err := projectname.GetProjectName(ctx, params.Printer, params.CliVersion, cmd) - if err != nil { - params.Printer.Debug(print.ErrorLevel, "get project name: %v", err) - projectLabel = model.ProjectId - } - params.Printer.Info("No servers found for project %q\n", projectLabel) - return nil + var items []iaas.Server + if resp.Items == nil { + items = []iaas.Server{} + } else { + items = *resp.Items + } + + projectLabel, err := projectname.GetProjectName(ctx, params.Printer, params.CliVersion, cmd) + if err != nil { + params.Printer.Debug(print.ErrorLevel, "get project name: %v", err) + projectLabel = model.ProjectId } // Truncate output - items := *resp.Items if model.Limit != nil && len(items) > int(*model.Limit) { items = items[:*model.Limit] } - return outputResult(params.Printer, model.OutputFormat, items) + return outputResult(params.Printer, model.OutputFormat, projectLabel, items) }, } configureFlags(cmd) @@ -140,7 +142,10 @@ func buildRequest(ctx context.Context, model *inputModel, apiClient *iaas.APICli return req } -func outputResult(p *print.Printer, outputFormat string, servers []iaas.Server) error { +func outputResult(p *print.Printer, outputFormat string, projectLabel string, servers []iaas.Server) error { + if len(servers) == 0 { + p.Info("No servers found for project %q\n", projectLabel) + } switch outputFormat { case print.JSONOutputFormat: details, err := json.MarshalIndent(servers, "", " ") diff --git a/internal/cmd/server/list/list_test.go b/internal/cmd/server/list/list_test.go index 4eb3a78cf..5166cc118 100644 --- a/internal/cmd/server/list/list_test.go +++ b/internal/cmd/server/list/list_test.go @@ -177,6 +177,7 @@ func TestBuildRequest(t *testing.T) { func TestOutputResult(t *testing.T) { type args struct { outputFormat string + projectLabel string servers []iaas.Server } tests := []struct { @@ -194,7 +195,7 @@ func TestOutputResult(t *testing.T) { 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.servers); (err != nil) != tt.wantErr { + if err := outputResult(p, tt.args.outputFormat, tt.args.projectLabel, tt.args.servers); (err != nil) != tt.wantErr { t.Errorf("outputResult() error = %v, wantErr %v", err, tt.wantErr) } }) From 3746b4096267872d1a0cf94d6619f3ad1ab24f67 Mon Sep 17 00:00:00 2001 From: Jan Obernberger Date: Mon, 13 Apr 2026 11:53:08 +0200 Subject: [PATCH 02/17] adapted outputResult signature to comply to linter --- internal/cmd/server/list/list.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/internal/cmd/server/list/list.go b/internal/cmd/server/list/list.go index eeb3507b8..bdeb78605 100644 --- a/internal/cmd/server/list/list.go +++ b/internal/cmd/server/list/list.go @@ -142,7 +142,7 @@ func buildRequest(ctx context.Context, model *inputModel, apiClient *iaas.APICli return req } -func outputResult(p *print.Printer, outputFormat string, projectLabel string, servers []iaas.Server) error { +func outputResult(p *print.Printer, outputFormat, projectLabel string, servers []iaas.Server) error { if len(servers) == 0 { p.Info("No servers found for project %q\n", projectLabel) } From cc50e9ba5c5546fcecea173ba0c19a970799964b Mon Sep 17 00:00:00 2001 From: Jan Obernberger Date: Mon, 13 Apr 2026 13:27:54 +0200 Subject: [PATCH 03/17] switched to getter for retrieving the items --- internal/cmd/server/list/list.go | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/internal/cmd/server/list/list.go b/internal/cmd/server/list/list.go index bdeb78605..25aeae32f 100644 --- a/internal/cmd/server/list/list.go +++ b/internal/cmd/server/list/list.go @@ -78,11 +78,9 @@ func NewCmd(params *types.CmdParams) *cobra.Command { return fmt.Errorf("list servers: %w", err) } - var items []iaas.Server - if resp.Items == nil { + items := resp.GetItems() + if items == nil { items = []iaas.Server{} - } else { - items = *resp.Items } projectLabel, err := projectname.GetProjectName(ctx, params.Printer, params.CliVersion, cmd) From a4adb805e4eb8dc450b1b15848bd08d5bf02c6bf Mon Sep 17 00:00:00 2001 From: Jan Obernberger Date: Mon, 13 Apr 2026 13:58:07 +0200 Subject: [PATCH 04/17] adapted printing behavior to align with expected behavior --- internal/cmd/server/list/list.go | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/internal/cmd/server/list/list.go b/internal/cmd/server/list/list.go index 25aeae32f..0880cfd08 100644 --- a/internal/cmd/server/list/list.go +++ b/internal/cmd/server/list/list.go @@ -141,9 +141,6 @@ func buildRequest(ctx context.Context, model *inputModel, apiClient *iaas.APICli } func outputResult(p *print.Printer, outputFormat, projectLabel string, servers []iaas.Server) error { - if len(servers) == 0 { - p.Info("No servers found for project %q\n", projectLabel) - } switch outputFormat { case print.JSONOutputFormat: details, err := json.MarshalIndent(servers, "", " ") @@ -167,6 +164,10 @@ func outputResult(p *print.Printer, outputFormat, projectLabel string, servers [ return nil default: + if len(servers) == 0 { + p.Info("No servers found for project %q\n", projectLabel) + return nil + } table := tables.NewTable() table.SetHeader("ID", "Name", "Status", "Machine Type", "Availability Zones", "Nic IPv4", "Public IPs") From 4542beb676147f4491dda1610ad74ed04f569702 Mon Sep 17 00:00:00 2001 From: Jan Obernberger Date: Mon, 13 Apr 2026 14:32:05 +0200 Subject: [PATCH 05/17] change printing to defined output instead of stderr --- internal/cmd/server/list/list.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/internal/cmd/server/list/list.go b/internal/cmd/server/list/list.go index 0880cfd08..1f504460d 100644 --- a/internal/cmd/server/list/list.go +++ b/internal/cmd/server/list/list.go @@ -165,7 +165,7 @@ func outputResult(p *print.Printer, outputFormat, projectLabel string, servers [ return nil default: if len(servers) == 0 { - p.Info("No servers found for project %q\n", projectLabel) + p.Outputf("No servers found for project %q\n", projectLabel) return nil } table := tables.NewTable() From 7ca2dce0c154a6bfd893db8c5ac83b70cd93d54d Mon Sep 17 00:00:00 2001 From: Jan Obernberger Date: Mon, 13 Apr 2026 14:42:24 +0200 Subject: [PATCH 06/17] adapted network list command to align to expectations --- internal/cmd/network/list/list.go | 28 ++++++++++++++------------ internal/cmd/network/list/list_test.go | 3 ++- 2 files changed, 17 insertions(+), 14 deletions(-) diff --git a/internal/cmd/network/list/list.go b/internal/cmd/network/list/list.go index 6bc0a8b67..2c18746c9 100644 --- a/internal/cmd/network/list/list.go +++ b/internal/cmd/network/list/list.go @@ -77,25 +77,22 @@ func NewCmd(params *types.CmdParams) *cobra.Command { return fmt.Errorf("list networks: %w", err) } - if resp.Items == nil || len(*resp.Items) == 0 { - projectLabel, err := projectname.GetProjectName(ctx, params.Printer, params.CliVersion, cmd) - if err != nil { - params.Printer.Debug(print.ErrorLevel, "get project name: %v", err) - projectLabel = model.ProjectId - } else if projectLabel == "" { - projectLabel = model.ProjectId - } - params.Printer.Info("No networks found for project %q\n", projectLabel) - return nil + items := resp.GetItems() + + projectLabel, err := projectname.GetProjectName(ctx, params.Printer, params.CliVersion, cmd) + if err != nil { + params.Printer.Debug(print.ErrorLevel, "get project name: %v", err) + projectLabel = model.ProjectId + } else if projectLabel == "" { + projectLabel = model.ProjectId } // Truncate output - items := *resp.Items if model.Limit != nil && len(items) > int(*model.Limit) { items = items[:*model.Limit] } - return outputResult(params.Printer, model.OutputFormat, items) + return outputResult(params.Printer, model.OutputFormat, projectLabel, items) }, } configureFlags(cmd) @@ -139,8 +136,13 @@ func buildRequest(ctx context.Context, model *inputModel, apiClient *iaas.APICli return req } -func outputResult(p *print.Printer, outputFormat string, networks []iaas.Network) error { +func outputResult(p *print.Printer, outputFormat, projectLabel string, networks []iaas.Network) error { return p.OutputResult(outputFormat, networks, func() error { + if len(networks) == 0 { + p.Outputf("No networks found for project %q\n", projectLabel) + return nil + } + table := tables.NewTable() table.SetHeader("ID", "NAME", "STATUS", "PUBLIC IP", "PREFIXES", "ROUTED", "ROUTING TABLE ID") diff --git a/internal/cmd/network/list/list_test.go b/internal/cmd/network/list/list_test.go index 67e90a2b4..1769f0547 100644 --- a/internal/cmd/network/list/list_test.go +++ b/internal/cmd/network/list/list_test.go @@ -176,6 +176,7 @@ func TestBuildRequest(t *testing.T) { func TestOutputResult(t *testing.T) { type args struct { outputFormat string + projectLabel string networks []iaas.Network } tests := []struct { @@ -202,7 +203,7 @@ func TestOutputResult(t *testing.T) { 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.networks); (err != nil) != tt.wantErr { + if err := outputResult(p, tt.args.outputFormat, tt.args.projectLabel, tt.args.networks); (err != nil) != tt.wantErr { t.Errorf("outputResult() error = %v, wantErr %v", err, tt.wantErr) } }) From 1a67bad9a37b9982882197ff1197c8dc1d41604c Mon Sep 17 00:00:00 2001 From: Jan Obernberger Date: Mon, 13 Apr 2026 14:45:43 +0200 Subject: [PATCH 07/17] remove unnecessary normalization of nil value in response items after feedback --- internal/cmd/server/list/list.go | 3 --- 1 file changed, 3 deletions(-) diff --git a/internal/cmd/server/list/list.go b/internal/cmd/server/list/list.go index 1f504460d..2ecb2a16a 100644 --- a/internal/cmd/server/list/list.go +++ b/internal/cmd/server/list/list.go @@ -79,9 +79,6 @@ func NewCmd(params *types.CmdParams) *cobra.Command { } items := resp.GetItems() - if items == nil { - items = []iaas.Server{} - } projectLabel, err := projectname.GetProjectName(ctx, params.Printer, params.CliVersion, cmd) if err != nil { From 90dee7f3565d7f1f50c8ccd95e6268d023612822 Mon Sep 17 00:00:00 2001 From: Jan Obernberger Date: Mon, 13 Apr 2026 15:21:50 +0200 Subject: [PATCH 08/17] adapted network-area list command to align to expectations --- internal/cmd/network-area/list/list.go | 40 +++++++++++---------- internal/cmd/network-area/list/list_test.go | 3 +- 2 files changed, 24 insertions(+), 19 deletions(-) diff --git a/internal/cmd/network-area/list/list.go b/internal/cmd/network-area/list/list.go index cf8d9975d..15e1a9c11 100644 --- a/internal/cmd/network-area/list/list.go +++ b/internal/cmd/network-area/list/list.go @@ -80,31 +80,30 @@ func NewCmd(params *types.CmdParams) *cobra.Command { return fmt.Errorf("list network areas: %w", err) } - if resp.Items == nil || len(*resp.Items) == 0 { - var orgLabel string - rmApiClient, err := rmClient.ConfigureClient(params.Printer, params.CliVersion) - if err == nil { - orgLabel, err = rmUtils.GetOrganizationName(ctx, rmApiClient, *model.OrganizationId) - if err != nil { - params.Printer.Debug(print.ErrorLevel, "get organization name: %v", err) - orgLabel = *model.OrganizationId - } else if orgLabel == "" { - orgLabel = *model.OrganizationId - } - } else { - params.Printer.Debug(print.ErrorLevel, "configure resource manager client: %v", err) + items := resp.GetItems() + + var orgLabel string + rmApiClient, err := rmClient.ConfigureClient(params.Printer, params.CliVersion) + if err == nil { + orgLabel, err = rmUtils.GetOrganizationName(ctx, rmApiClient, *model.OrganizationId) + if err != nil { + params.Printer.Debug(print.ErrorLevel, "get organization name: %v", err) + orgLabel = *model.OrganizationId } - params.Printer.Info("No STACKIT Network Areas found for organization %q\n", orgLabel) - return nil + } else { + params.Printer.Debug(print.ErrorLevel, "configure resource manager client: %v", err) + } + + if orgLabel == "" { + orgLabel = *model.OrganizationId } // Truncate output - items := *resp.Items if model.Limit != nil && len(items) > int(*model.Limit) { items = items[:*model.Limit] } - return outputResult(params.Printer, model.OutputFormat, items) + return outputResult(params.Printer, orgLabel, model.OutputFormat, items) }, } configureFlags(cmd) @@ -149,8 +148,13 @@ func buildRequest(ctx context.Context, model *inputModel, apiClient *iaas.APICli return req } -func outputResult(p *print.Printer, outputFormat string, networkAreas []iaas.NetworkArea) error { +func outputResult(p *print.Printer, orgLabel, outputFormat string, networkAreas []iaas.NetworkArea) error { return p.OutputResult(outputFormat, networkAreas, func() error { + if len(networkAreas) == 0 { + p.Outputf("No STACKIT Network Areas found for organization %q\n", orgLabel) + return nil + } + table := tables.NewTable() table.SetHeader("ID", "Name", "# Attached Projects") diff --git a/internal/cmd/network-area/list/list_test.go b/internal/cmd/network-area/list/list_test.go index 2524bb8c8..f411a60da 100644 --- a/internal/cmd/network-area/list/list_test.go +++ b/internal/cmd/network-area/list/list_test.go @@ -169,6 +169,7 @@ func TestBuildRequest(t *testing.T) { func TestOutputResult(t *testing.T) { type args struct { outputFormat string + orgLabel string networkAreas []iaas.NetworkArea } tests := []struct { @@ -200,7 +201,7 @@ func TestOutputResult(t *testing.T) { 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.networkAreas); (err != nil) != tt.wantErr { + if err := outputResult(p, tt.args.outputFormat, tt.args.orgLabel, tt.args.networkAreas); (err != nil) != tt.wantErr { t.Errorf("outputResult() error = %v, wantErr %v", err, tt.wantErr) } }) From a31e39f318b263eb855468946e534fb40f31ad3a Mon Sep 17 00:00:00 2001 From: Jan Obernberger Date: Mon, 13 Apr 2026 15:31:17 +0200 Subject: [PATCH 09/17] removed redundant check --- internal/cmd/network/list/list.go | 2 -- 1 file changed, 2 deletions(-) diff --git a/internal/cmd/network/list/list.go b/internal/cmd/network/list/list.go index 2c18746c9..4731e717a 100644 --- a/internal/cmd/network/list/list.go +++ b/internal/cmd/network/list/list.go @@ -83,8 +83,6 @@ func NewCmd(params *types.CmdParams) *cobra.Command { if err != nil { params.Printer.Debug(print.ErrorLevel, "get project name: %v", err) projectLabel = model.ProjectId - } else if projectLabel == "" { - projectLabel = model.ProjectId } // Truncate output From dca821f4d1027ed377d1ed8b8f11e3a3da9695fd Mon Sep 17 00:00:00 2001 From: Jan Obernberger Date: Wed, 15 Apr 2026 11:10:22 +0200 Subject: [PATCH 10/17] adapted affinity list command to align to expectations --- internal/cmd/affinity-groups/list/list.go | 29 ++++++++++--------- .../cmd/affinity-groups/list/list_test.go | 13 +++++---- 2 files changed, 24 insertions(+), 18 deletions(-) diff --git a/internal/cmd/affinity-groups/list/list.go b/internal/cmd/affinity-groups/list/list.go index fe9abad60..fb75bf2f3 100644 --- a/internal/cmd/affinity-groups/list/list.go +++ b/internal/cmd/affinity-groups/list/list.go @@ -18,6 +18,7 @@ import ( "github.com/stackitcloud/stackit-cli/internal/pkg/flags" "github.com/stackitcloud/stackit-cli/internal/pkg/globalflags" "github.com/stackitcloud/stackit-cli/internal/pkg/print" + "github.com/stackitcloud/stackit-cli/internal/pkg/projectname" "github.com/stackitcloud/stackit-cli/internal/pkg/services/iaas/client" ) @@ -63,16 +64,19 @@ func NewCmd(params *types.CmdParams) *cobra.Command { if err != nil { return fmt.Errorf("list affinity groups: %w", err) } + items := result.GetItems() - if items := result.Items; items != nil { - if model.Limit != nil && len(*items) > int(*model.Limit) { - *items = (*items)[:*model.Limit] - } - return outputResult(params.Printer, *model, *items) + projectLabel, err := projectname.GetProjectName(ctx, params.Printer, params.CliVersion, cmd) + if err != nil { + params.Printer.Debug(print.ErrorLevel, "get project name: %v", err) + projectLabel = model.ProjectId } - params.Printer.Outputln("No affinity groups found") - return nil + // Truncate Output + if model.Limit != nil && len(items) > int(*model.Limit) { + items = items[:*model.Limit] + } + return outputResult(params.Printer, model.OutputFormat, projectLabel, items) }, } configureFlags(cmd) @@ -110,13 +114,12 @@ func parseInput(p *print.Printer, cmd *cobra.Command, _ []string) (*inputModel, return &model, nil } -func outputResult(p *print.Printer, model inputModel, items []iaas.AffinityGroup) error { - var outputFormat string - if model.GlobalFlagModel != nil { - outputFormat = model.OutputFormat - } - +func outputResult(p *print.Printer, outputFormat, projectLabel string, items []iaas.AffinityGroup) error { return p.OutputResult(outputFormat, items, func() error { + if len(items) == 0 { + p.Outputf("No affinity groups found for project %q\n", projectLabel) + return nil + } table := tables.NewTable() table.SetHeader("ID", "NAME", "POLICY") for _, item := range items { diff --git a/internal/cmd/affinity-groups/list/list_test.go b/internal/cmd/affinity-groups/list/list_test.go index c872f4b45..f8d7610da 100644 --- a/internal/cmd/affinity-groups/list/list_test.go +++ b/internal/cmd/affinity-groups/list/list_test.go @@ -142,16 +142,19 @@ func TestBuildRequest(t *testing.T) { } func TestOutputResult(t *testing.T) { + type args struct { + outputFormat string + projectLabel string + instances []iaas.AffinityGroup + } tests := []struct { description string - model inputModel - response []iaas.AffinityGroup + args args isValid bool }{ { description: "empty", - model: inputModel{}, - response: []iaas.AffinityGroup{}, + args: args{}, isValid: true, }, } @@ -159,7 +162,7 @@ func TestOutputResult(t *testing.T) { p.Cmd = NewCmd(&types.CmdParams{Printer: p}) for _, tt := range tests { t.Run(tt.description, func(t *testing.T) { - err := outputResult(p, tt.model, tt.response) + err := outputResult(p, tt.args.outputFormat, tt.args.projectLabel, tt.args.instances) if err != nil { if !tt.isValid { return From fb181fba7daed7d3755e619da8e5bf532939519a Mon Sep 17 00:00:00 2001 From: Jan Obernberger Date: Wed, 15 Apr 2026 11:26:05 +0200 Subject: [PATCH 11/17] adapted image list command to align to expectations --- internal/cmd/image/list/list.go | 23 ++++++++++++----------- internal/cmd/image/list/list_test.go | 3 ++- 2 files changed, 14 insertions(+), 12 deletions(-) diff --git a/internal/cmd/image/list/list.go b/internal/cmd/image/list/list.go index ba21fbe84..26fccfa24 100644 --- a/internal/cmd/image/list/list.go +++ b/internal/cmd/image/list/list.go @@ -81,21 +81,18 @@ func NewCmd(params *types.CmdParams) *cobra.Command { // Call API request := buildRequest(ctx, model, apiClient) - response, err := request.Execute() if err != nil { return fmt.Errorf("list images: %w", err) } + items := response.GetItems() - if items := response.GetItems(); len(items) == 0 { - params.Printer.Info("No images found for project %q", projectLabel) - } else { - if model.Limit != nil && len(items) > int(*model.Limit) { - items = (items)[:*model.Limit] - } - if err := outputResult(params.Printer, model.OutputFormat, items); err != nil { - return fmt.Errorf("output images: %w", err) - } + // Truncate output + if model.Limit != nil && len(items) > int(*model.Limit) { + items = (items)[:*model.Limit] + } + if err := outputResult(params.Printer, model.OutputFormat, projectLabel, items); err != nil { + return fmt.Errorf("output images: %w", err) } return nil @@ -149,8 +146,12 @@ func buildRequest(ctx context.Context, model *inputModel, apiClient *iaas.APICli return request } -func outputResult(p *print.Printer, outputFormat string, items []iaas.Image) error { +func outputResult(p *print.Printer, outputFormat, projectLabel string, items []iaas.Image) error { return p.OutputResult(outputFormat, items, func() error { + if len(items) == 0 { + p.Outputf("No images found for project %q\n", projectLabel) + return nil + } table := tables.NewTable() table.SetHeader("ID", "NAME", "OS", "ARCHITECTURE", "DISTRIBUTION", "VERSION", "SCOPE", "OWNER", "LABELS") for i := range items { diff --git a/internal/cmd/image/list/list_test.go b/internal/cmd/image/list/list_test.go index 7521d2023..b8cd70ce3 100644 --- a/internal/cmd/image/list/list_test.go +++ b/internal/cmd/image/list/list_test.go @@ -189,6 +189,7 @@ func TestBuildRequest(t *testing.T) { func Test_outputResult(t *testing.T) { type args struct { outputFormat string + projectLabel string items []iaas.Image } tests := []struct { @@ -217,7 +218,7 @@ func Test_outputResult(t *testing.T) { 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.items); (err != nil) != tt.wantErr { + if err := outputResult(p, tt.args.outputFormat, tt.args.projectLabel, tt.args.items); (err != nil) != tt.wantErr { t.Errorf("outputResult() error = %v, wantErr %v", err, tt.wantErr) } }) From 111062a6519fc6b9291db945ab05096df92bf3a5 Mon Sep 17 00:00:00 2001 From: Jan Obernberger Date: Wed, 15 Apr 2026 12:05:09 +0200 Subject: [PATCH 12/17] adapted key pair list command to align to expectations --- internal/cmd/key-pair/list/list.go | 22 +++++++++++++++------- internal/cmd/key-pair/list/list_test.go | 3 ++- 2 files changed, 17 insertions(+), 8 deletions(-) diff --git a/internal/cmd/key-pair/list/list.go b/internal/cmd/key-pair/list/list.go index 3820eb038..7a28b20f9 100644 --- a/internal/cmd/key-pair/list/list.go +++ b/internal/cmd/key-pair/list/list.go @@ -5,6 +5,7 @@ import ( "fmt" "strings" + "github.com/stackitcloud/stackit-cli/internal/pkg/projectname" "github.com/stackitcloud/stackit-cli/internal/pkg/types" "github.com/stackitcloud/stackit-cli/internal/pkg/utils" @@ -70,6 +71,11 @@ func NewCmd(params *types.CmdParams) *cobra.Command { return err } + projectLabel, err := projectname.GetProjectName(ctx, params.Printer, params.CliVersion, cmd) + if err != nil { + return fmt.Errorf("list key pairs: %w", err) + } + // Call API req := buildRequest(ctx, model, apiClient) resp, err := req.Execute() @@ -77,17 +83,14 @@ func NewCmd(params *types.CmdParams) *cobra.Command { return fmt.Errorf("list key pairs: %w", err) } - if resp.Items == nil || len(*resp.Items) == 0 { - params.Printer.Info("No key pairs found\n") - return nil - } + items := resp.GetItems() - items := *resp.Items + // Truncate output if model.Limit != nil && len(items) > int(*model.Limit) { items = items[:*model.Limit] } - return outputResult(params.Printer, model.OutputFormat, items) + return outputResult(params.Printer, model.OutputFormat, projectLabel, items) }, } configureFlags(cmd) @@ -128,8 +131,13 @@ func buildRequest(ctx context.Context, model *inputModel, apiClient *iaas.APICli return req } -func outputResult(p *print.Printer, outputFormat string, keyPairs []iaas.Keypair) error { +func outputResult(p *print.Printer, outputFormat, projectLabel string, keyPairs []iaas.Keypair) error { return p.OutputResult(outputFormat, keyPairs, func() error { + if len(keyPairs) == 0 { + p.Outputf("No key pairs found for project %q\n", projectLabel) + return nil + } + table := tables.NewTable() table.SetHeader("KEY PAIR NAME", "LABELS", "FINGERPRINT", "CREATED AT", "UPDATED AT") diff --git a/internal/cmd/key-pair/list/list_test.go b/internal/cmd/key-pair/list/list_test.go index 2ceb0d426..5eee4cced 100644 --- a/internal/cmd/key-pair/list/list_test.go +++ b/internal/cmd/key-pair/list/list_test.go @@ -155,6 +155,7 @@ func TestBuildRequest(t *testing.T) { func Test_outputResult(t *testing.T) { type args struct { outputFormat string + projectLabel string keyPairs []iaas.Keypair } tests := []struct { @@ -179,7 +180,7 @@ func Test_outputResult(t *testing.T) { p := print.NewPrinter() p.Cmd = NewCmd(&types.CmdParams{Printer: p}) - if err := outputResult(p, tt.args.outputFormat, tt.args.keyPairs); (err != nil) != tt.wantErr { + if err := outputResult(p, tt.args.outputFormat, tt.args.projectLabel, tt.args.keyPairs); (err != nil) != tt.wantErr { t.Errorf("outputResult() error = %v, wantErr %v", err, tt.wantErr) } }) From 815287a5be871b4b8d702a9f335b0259197353dd Mon Sep 17 00:00:00 2001 From: Jan Obernberger Date: Wed, 15 Apr 2026 12:23:37 +0200 Subject: [PATCH 13/17] fixed debug print in key pair list command --- internal/cmd/key-pair/list/list.go | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/internal/cmd/key-pair/list/list.go b/internal/cmd/key-pair/list/list.go index 7a28b20f9..92f7f0027 100644 --- a/internal/cmd/key-pair/list/list.go +++ b/internal/cmd/key-pair/list/list.go @@ -73,7 +73,8 @@ func NewCmd(params *types.CmdParams) *cobra.Command { projectLabel, err := projectname.GetProjectName(ctx, params.Printer, params.CliVersion, cmd) if err != nil { - return fmt.Errorf("list key pairs: %w", err) + params.Printer.Debug(print.ErrorLevel, "get project name: %v", err) + projectLabel = model.ProjectId } // Call API From d19379d334f405fe8b0fbd99c2367704b239c580 Mon Sep 17 00:00:00 2001 From: Jan Obernberger Date: Wed, 15 Apr 2026 12:27:23 +0200 Subject: [PATCH 14/17] move project label retrieval --- internal/cmd/key-pair/list/list.go | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/internal/cmd/key-pair/list/list.go b/internal/cmd/key-pair/list/list.go index 92f7f0027..6abc21158 100644 --- a/internal/cmd/key-pair/list/list.go +++ b/internal/cmd/key-pair/list/list.go @@ -71,12 +71,6 @@ func NewCmd(params *types.CmdParams) *cobra.Command { return err } - projectLabel, err := projectname.GetProjectName(ctx, params.Printer, params.CliVersion, cmd) - if err != nil { - params.Printer.Debug(print.ErrorLevel, "get project name: %v", err) - projectLabel = model.ProjectId - } - // Call API req := buildRequest(ctx, model, apiClient) resp, err := req.Execute() @@ -86,6 +80,12 @@ func NewCmd(params *types.CmdParams) *cobra.Command { items := resp.GetItems() + projectLabel, err := projectname.GetProjectName(ctx, params.Printer, params.CliVersion, cmd) + if err != nil { + params.Printer.Debug(print.ErrorLevel, "get project name: %v", err) + projectLabel = model.ProjectId + } + // Truncate output if model.Limit != nil && len(items) > int(*model.Limit) { items = items[:*model.Limit] From 0bc8501a0b57b003785e97a6275a8fcf4da0e47a Mon Sep 17 00:00:00 2001 From: Jan Obernberger Date: Wed, 15 Apr 2026 13:01:25 +0200 Subject: [PATCH 15/17] adapted public ip list command to align to expectations --- internal/cmd/public-ip/list/list.go | 27 ++++++++++++------------ internal/cmd/public-ip/list/list_test.go | 3 ++- 2 files changed, 16 insertions(+), 14 deletions(-) diff --git a/internal/cmd/public-ip/list/list.go b/internal/cmd/public-ip/list/list.go index 1888e2d1d..a75d94f40 100644 --- a/internal/cmd/public-ip/list/list.go +++ b/internal/cmd/public-ip/list/list.go @@ -77,25 +77,22 @@ func NewCmd(params *types.CmdParams) *cobra.Command { return fmt.Errorf("list public IPs: %w", err) } - if resp.Items == nil || len(*resp.Items) == 0 { - projectLabel, err := projectname.GetProjectName(ctx, params.Printer, params.CliVersion, cmd) - if err != nil { - params.Printer.Debug(print.ErrorLevel, "get project name: %v", err) - projectLabel = model.ProjectId - } else if projectLabel == "" { - projectLabel = model.ProjectId - } - params.Printer.Info("No public IPs found for project %q\n", projectLabel) - return nil + items := resp.GetItems() + + projectLabel, err := projectname.GetProjectName(ctx, params.Printer, params.CliVersion, cmd) + if err != nil { + params.Printer.Debug(print.ErrorLevel, "get project name: %v", err) + projectLabel = model.ProjectId + } else if projectLabel == "" { + projectLabel = model.ProjectId } // Truncate output - items := *resp.Items if model.Limit != nil && len(items) > int(*model.Limit) { items = items[:*model.Limit] } - return outputResult(params.Printer, model.OutputFormat, items) + return outputResult(params.Printer, model.OutputFormat, projectLabel, items) }, } configureFlags(cmd) @@ -140,8 +137,12 @@ func buildRequest(ctx context.Context, model *inputModel, apiClient *iaas.APICli return req } -func outputResult(p *print.Printer, outputFormat string, publicIps []iaas.PublicIp) error { +func outputResult(p *print.Printer, outputFormat, projectLabel string, publicIps []iaas.PublicIp) error { return p.OutputResult(outputFormat, publicIps, func() error { + if len(publicIps) == 0 { + p.Outputf("No public IPs found for project %q\n", projectLabel) + return nil + } table := tables.NewTable() table.SetHeader("ID", "IP ADDRESS", "USED BY") diff --git a/internal/cmd/public-ip/list/list_test.go b/internal/cmd/public-ip/list/list_test.go index 9a10067d9..2d6b57f8f 100644 --- a/internal/cmd/public-ip/list/list_test.go +++ b/internal/cmd/public-ip/list/list_test.go @@ -176,6 +176,7 @@ func TestBuildRequest(t *testing.T) { func TestOutputResult(t *testing.T) { type args struct { outputFormat string + projectLabel string publicIps []iaas.PublicIp } tests := []struct { @@ -193,7 +194,7 @@ func TestOutputResult(t *testing.T) { 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.publicIps); (err != nil) != tt.wantErr { + if err := outputResult(p, tt.args.outputFormat, tt.args.projectLabel, tt.args.publicIps); (err != nil) != tt.wantErr { t.Errorf("outputResult() error = %v, wantErr %v", err, tt.wantErr) } }) From 1666e15a28601dca075da5a6f18def349c3b249d Mon Sep 17 00:00:00 2001 From: Jan Obernberger Date: Wed, 15 Apr 2026 14:37:58 +0200 Subject: [PATCH 16/17] adapted security group list command to align to expectations --- internal/cmd/security-group/list/list.go | 27 +++++++++---------- internal/cmd/security-group/list/list_test.go | 3 ++- 2 files changed, 15 insertions(+), 15 deletions(-) diff --git a/internal/cmd/security-group/list/list.go b/internal/cmd/security-group/list/list.go index d3788ee9c..71c7489ba 100644 --- a/internal/cmd/security-group/list/list.go +++ b/internal/cmd/security-group/list/list.go @@ -54,12 +54,6 @@ func NewCmd(params *types.CmdParams) *cobra.Command { return err } - projectLabel, err := projectname.GetProjectName(ctx, params.Printer, params.CliVersion, cmd) - if err != nil { - params.Printer.Debug(print.ErrorLevel, "get project name: %v", err) - projectLabel = model.ProjectId - } - // Call API request := buildRequest(ctx, model, apiClient) @@ -68,15 +62,16 @@ func NewCmd(params *types.CmdParams) *cobra.Command { return fmt.Errorf("list security group: %w", err) } - if items := response.GetItems(); len(items) == 0 { - params.Printer.Info("No security groups found for project %q", projectLabel) - } else { - if err := outputResult(params.Printer, model.OutputFormat, items); err != nil { - return fmt.Errorf("output security groups: %w", err) - } + items := response.GetItems() + + projectLabel, err := projectname.GetProjectName(ctx, params.Printer, params.CliVersion, cmd) + if err != nil { + params.Printer.Debug(print.ErrorLevel, "get project name: %v", err) + projectLabel = model.ProjectId } - return nil + return outputResult(params.Printer, model.OutputFormat, projectLabel, items) + }, } @@ -111,8 +106,12 @@ func buildRequest(ctx context.Context, model *inputModel, apiClient *iaas.APICli return request } -func outputResult(p *print.Printer, outputFormat string, items []iaas.SecurityGroup) error { +func outputResult(p *print.Printer, outputFormat, projectLabel string, items []iaas.SecurityGroup) error { return p.OutputResult(outputFormat, items, func() error { + if len(items) == 0 { + p.Outputf("No security groups found for project %q\n", projectLabel) + return nil + } table := tables.NewTable() table.SetHeader("ID", "NAME", "STATEFUL", "DESCRIPTION", "LABELS") for _, item := range items { diff --git a/internal/cmd/security-group/list/list_test.go b/internal/cmd/security-group/list/list_test.go index 18cfa967e..9f4cd23e7 100644 --- a/internal/cmd/security-group/list/list_test.go +++ b/internal/cmd/security-group/list/list_test.go @@ -184,6 +184,7 @@ func TestBuildRequest(t *testing.T) { func TestOutputResult(t *testing.T) { type args struct { outputFormat string + projectLabel string items []iaas.SecurityGroup } tests := []struct { @@ -201,7 +202,7 @@ func TestOutputResult(t *testing.T) { 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.items); (err != nil) != tt.wantErr { + if err := outputResult(p, tt.args.outputFormat, tt.args.projectLabel, tt.args.items); (err != nil) != tt.wantErr { t.Errorf("outputResult() error = %v, wantErr %v", err, tt.wantErr) } }) From c8429b0b16efb07dc0c786666e6aff7db3d5f7d1 Mon Sep 17 00:00:00 2001 From: Jan Obernberger Date: Wed, 15 Apr 2026 14:50:15 +0200 Subject: [PATCH 17/17] added missing --limit flag for list security groups command --- internal/cmd/security-group/list/list.go | 29 +++++++++++++++++-- internal/cmd/security-group/list/list_test.go | 16 ++++++++++ 2 files changed, 43 insertions(+), 2 deletions(-) diff --git a/internal/cmd/security-group/list/list.go b/internal/cmd/security-group/list/list.go index 71c7489ba..6613a3864 100644 --- a/internal/cmd/security-group/list/list.go +++ b/internal/cmd/security-group/list/list.go @@ -25,10 +25,12 @@ import ( type inputModel struct { *globalflags.GlobalFlagModel LabelSelector *string + Limit *int64 } const ( labelSelectorFlag = "label-selector" + limitFlag = "limit" ) func NewCmd(params *types.CmdParams) *cobra.Command { @@ -38,8 +40,16 @@ func NewCmd(params *types.CmdParams) *cobra.Command { Long: "Lists security groups by its internal ID.", Args: args.NoArgs, Example: examples.Build( - examples.NewExample(`List all groups`, `$ stackit security-group list`), - examples.NewExample(`List groups with labels`, `$ stackit security-group list --label-selector label1=value1,label2=value2`), + examples.NewExample(`Lists all security groups`, `$ stackit security-group list`), + examples.NewExample(`Lists security groups with labels`, `$ stackit security-group list --label-selector label1=value1,label2=value2`), + examples.NewExample( + `Lists all security groups in JSON format`, + "$ stackit security-group list --output-format json", + ), + examples.NewExample( + `Lists up to 10 security groups`, + "$ stackit security-group list --limit 10", + ), ), RunE: func(cmd *cobra.Command, args []string) error { ctx := context.Background() @@ -70,6 +80,11 @@ func NewCmd(params *types.CmdParams) *cobra.Command { projectLabel = model.ProjectId } + // Truncate output + if model.Limit != nil && len(items) > int(*model.Limit) { + items = items[:*model.Limit] + } + return outputResult(params.Printer, model.OutputFormat, projectLabel, items) }, @@ -81,6 +96,7 @@ func NewCmd(params *types.CmdParams) *cobra.Command { func configureFlags(cmd *cobra.Command) { cmd.Flags().String(labelSelectorFlag, "", "Filter by label") + cmd.Flags().Int64(limitFlag, 0, "Maximum number of entries to list") } func parseInput(p *print.Printer, cmd *cobra.Command, _ []string) (*inputModel, error) { @@ -89,9 +105,18 @@ func parseInput(p *print.Printer, cmd *cobra.Command, _ []string) (*inputModel, return nil, &errors.ProjectIdError{} } + limit := flags.FlagToInt64Pointer(p, cmd, limitFlag) + if limit != nil && *limit < 1 { + return nil, &errors.FlagValidationError{ + Flag: limitFlag, + Details: "must be greater than 0", + } + } + model := inputModel{ GlobalFlagModel: globalFlags, LabelSelector: flags.FlagToStringPointer(p, cmd, labelSelectorFlag), + Limit: limit, } p.DebugInputModel(model) diff --git a/internal/cmd/security-group/list/list_test.go b/internal/cmd/security-group/list/list_test.go index 9f4cd23e7..e5a0f1b84 100644 --- a/internal/cmd/security-group/list/list_test.go +++ b/internal/cmd/security-group/list/list_test.go @@ -36,6 +36,7 @@ func fixtureFlagValues(mods ...func(flagValues map[string]string)) map[string]st globalflags.RegionFlag: testRegion, labelSelectorFlag: testLabels, + limitFlag: "10", } for _, mod := range mods { mod(flagValues) @@ -51,6 +52,7 @@ func fixtureInputModel(mods ...func(model *inputModel)) *inputModel { Verbosity: globalflags.VerbosityDefault, }, LabelSelector: utils.Ptr(testLabels), + Limit: utils.Ptr(int64(10)), } for _, mod := range mods { mod(model) @@ -127,6 +129,20 @@ func TestParseInput(t *testing.T) { model.LabelSelector = utils.Ptr("foo=bar") }), }, + { + description: "limit invalid", + flagValues: fixtureFlagValues(func(flagValues map[string]string) { + flagValues[limitFlag] = "invalid" + }), + isValid: false, + }, + { + description: "limit invalid 2", + flagValues: fixtureFlagValues(func(flagValues map[string]string) { + flagValues[limitFlag] = "0" + }), + isValid: false, + }, } for _, tt := range tests {