From 0aea155a7f6f7bedd3133e7f0e762cce058a91bf Mon Sep 17 00:00:00 2001 From: Jan Obernberger Date: Mon, 13 Apr 2026 11:17:41 +0200 Subject: [PATCH 01/25] 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/25] 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/25] 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/25] 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/25] 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/25] 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/25] 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/25] 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/25] 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/25] 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/25] 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/25] 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/25] 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/25] 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/25] 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/25] 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/25] 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 { From 6535939007065edc1beb7d007dde6ec69483c779 Mon Sep 17 00:00:00 2001 From: Jan Obernberger Date: Wed, 15 Apr 2026 15:41:14 +0200 Subject: [PATCH 18/25] adapted volumes list command to align to expectations --- internal/cmd/volume/list/list.go | 23 ++++++++++++----------- internal/cmd/volume/list/list_test.go | 3 ++- 2 files changed, 14 insertions(+), 12 deletions(-) diff --git a/internal/cmd/volume/list/list.go b/internal/cmd/volume/list/list.go index fa4b2c1f0..ed0192343 100644 --- a/internal/cmd/volume/list/list.go +++ b/internal/cmd/volume/list/list.go @@ -76,23 +76,20 @@ func NewCmd(params *types.CmdParams) *cobra.Command { return fmt.Errorf("list volumes: %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 volumes 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 } // 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) @@ -137,8 +134,12 @@ func buildRequest(ctx context.Context, model *inputModel, apiClient *iaas.APICli return req } -func outputResult(p *print.Printer, outputFormat string, volumes []iaas.Volume) error { +func outputResult(p *print.Printer, outputFormat, projectLabel string, volumes []iaas.Volume) error { return p.OutputResult(outputFormat, volumes, func() error { + if len(volumes) == 0 { + p.Outputf("No volumes found for project %q\n", projectLabel) + return nil + } table := tables.NewTable() table.SetHeader("ID", "Name", "Status", "Server", "Availability Zone", "Size (GB)") diff --git a/internal/cmd/volume/list/list_test.go b/internal/cmd/volume/list/list_test.go index 924acee29..eb2dff250 100644 --- a/internal/cmd/volume/list/list_test.go +++ b/internal/cmd/volume/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 volumes []iaas.Volume } 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.volumes); (err != nil) != tt.wantErr { + if err := outputResult(p, tt.args.outputFormat, tt.args.projectLabel, tt.args.volumes); (err != nil) != tt.wantErr { t.Errorf("outputResult() error = %v, wantErr %v", err, tt.wantErr) } }) From d5b1c14a333532fd3b70066f87bf79d6f8a80b9b Mon Sep 17 00:00:00 2001 From: Jan Obernberger Date: Wed, 15 Apr 2026 16:04:48 +0200 Subject: [PATCH 19/25] adapted volume snapshots list command to align to expectations --- internal/cmd/volume/snapshot/list/list.go | 31 ++++++++----------- .../cmd/volume/snapshot/list/list_test.go | 5 +-- 2 files changed, 16 insertions(+), 20 deletions(-) diff --git a/internal/cmd/volume/snapshot/list/list.go b/internal/cmd/volume/snapshot/list/list.go index a2d312b26..97b624189 100644 --- a/internal/cmd/volume/snapshot/list/list.go +++ b/internal/cmd/volume/snapshot/list/list.go @@ -72,25 +72,20 @@ func NewCmd(params *types.CmdParams) *cobra.Command { return fmt.Errorf("list snapshots: %w", err) } - // Check if response is empty - 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 snapshots found for project %q\n", projectLabel) - return nil - } + snapshots := resp.GetItems() - snapshots := *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 + } - // Apply limit if specified + // Truncate output if model.Limit != nil && int(*model.Limit) < len(snapshots) { snapshots = snapshots[:*model.Limit] } - return outputResult(params.Printer, model.OutputFormat, snapshots) + return outputResult(params.Printer, model.OutputFormat, projectLabel, snapshots) }, } @@ -137,12 +132,12 @@ func buildRequest(ctx context.Context, model *inputModel, apiClient *iaas.APICli return req } -func outputResult(p *print.Printer, outputFormat string, snapshots []iaas.Snapshot) error { - if snapshots == nil { - return fmt.Errorf("list snapshots response is empty") - } - +func outputResult(p *print.Printer, outputFormat, projectLabel string, snapshots []iaas.Snapshot) error { return p.OutputResult(outputFormat, snapshots, func() error { + if len(snapshots) == 0 { + p.Outputf("No snapshots found for project %q\n", projectLabel) + return nil + } table := tables.NewTable() table.SetHeader("ID", "NAME", "SIZE", "STATUS", "VOLUME ID", "LABELS", "CREATED AT", "UPDATED AT") diff --git a/internal/cmd/volume/snapshot/list/list_test.go b/internal/cmd/volume/snapshot/list/list_test.go index ff2d86383..cd39631a9 100644 --- a/internal/cmd/volume/snapshot/list/list_test.go +++ b/internal/cmd/volume/snapshot/list/list_test.go @@ -183,6 +183,7 @@ func TestBuildRequest(t *testing.T) { func TestOutputResult(t *testing.T) { type args struct { outputFormat string + projectLabel string snapshots []iaas.Snapshot } tests := []struct { @@ -193,7 +194,7 @@ func TestOutputResult(t *testing.T) { { name: "empty", args: args{}, - wantErr: true, + wantErr: false, }, { name: "empty snapshot in slice", @@ -221,7 +222,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.snapshots); (err != nil) != tt.wantErr { + if err := outputResult(p, tt.args.outputFormat, tt.args.projectLabel, tt.args.snapshots); (err != nil) != tt.wantErr { t.Errorf("outputResult() error = %v, wantErr %v", err, tt.wantErr) } }) From 9a2972bb8c9d8c7f2aa8b4647f65337c8e175e81 Mon Sep 17 00:00:00 2001 From: Jan Obernberger Date: Wed, 15 Apr 2026 16:13:49 +0200 Subject: [PATCH 20/25] adapted volume performance class list command to align to expectations --- .../cmd/volume/performance-class/list/list.go | 23 ++++++++++--------- .../performance-class/list/list_test.go | 3 ++- 2 files changed, 14 insertions(+), 12 deletions(-) diff --git a/internal/cmd/volume/performance-class/list/list.go b/internal/cmd/volume/performance-class/list/list.go index ae62dd65d..9e4f9a453 100644 --- a/internal/cmd/volume/performance-class/list/list.go +++ b/internal/cmd/volume/performance-class/list/list.go @@ -77,23 +77,20 @@ func NewCmd(params *types.CmdParams) *cobra.Command { return fmt.Errorf("list volume performance classes: %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 volume performance class 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 } // 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) @@ -138,8 +135,12 @@ func buildRequest(ctx context.Context, model *inputModel, apiClient *iaas.APICli return req } -func outputResult(p *print.Printer, outputFormat string, performanceClasses []iaas.VolumePerformanceClass) error { +func outputResult(p *print.Printer, outputFormat, projectLabel string, performanceClasses []iaas.VolumePerformanceClass) error { return p.OutputResult(outputFormat, performanceClasses, func() error { + if len(performanceClasses) == 0 { + p.Outputf("No volume performance class found for project %q\n", projectLabel) + return nil + } table := tables.NewTable() table.SetHeader("Name", "Description") diff --git a/internal/cmd/volume/performance-class/list/list_test.go b/internal/cmd/volume/performance-class/list/list_test.go index 79a1b29c8..8f3fc7f2b 100644 --- a/internal/cmd/volume/performance-class/list/list_test.go +++ b/internal/cmd/volume/performance-class/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 performanceClasses []iaas.VolumePerformanceClass } 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.performanceClasses); (err != nil) != tt.wantErr { + if err := outputResult(p, tt.args.outputFormat, tt.args.projectLabel, tt.args.performanceClasses); (err != nil) != tt.wantErr { t.Errorf("outputResult() error = %v, wantErr %v", err, tt.wantErr) } }) From f3a2379d3243f14f2659f19d72b0a24d03fffd9f Mon Sep 17 00:00:00 2001 From: Jan Obernberger Date: Wed, 15 Apr 2026 16:24:45 +0200 Subject: [PATCH 21/25] adapted volume backups list command to align to expectations --- internal/cmd/volume/backup/list/list.go | 28 +++++++++----------- internal/cmd/volume/backup/list/list_test.go | 5 ++-- 2 files changed, 16 insertions(+), 17 deletions(-) diff --git a/internal/cmd/volume/backup/list/list.go b/internal/cmd/volume/backup/list/list.go index 9da088d13..a555d8fb9 100644 --- a/internal/cmd/volume/backup/list/list.go +++ b/internal/cmd/volume/backup/list/list.go @@ -72,23 +72,21 @@ func NewCmd(params *types.CmdParams) *cobra.Command { if err != nil { return fmt.Errorf("get backups: %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 backups found for project %s\n", projectLabel) - return nil + + backups := 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 } - backups := *resp.Items // Truncate output if model.Limit != nil && len(backups) > int(*model.Limit) { backups = backups[:*model.Limit] } - return outputResult(params.Printer, model.OutputFormat, backups) + return outputResult(params.Printer, model.OutputFormat, projectLabel, backups) }, } @@ -137,12 +135,12 @@ func buildRequest(ctx context.Context, model *inputModel, apiClient *iaas.APICli return req } -func outputResult(p *print.Printer, outputFormat string, backups []iaas.Backup) error { - if backups == nil { - return fmt.Errorf("backups is empty") - } - +func outputResult(p *print.Printer, outputFormat, projectLabel string, backups []iaas.Backup) error { return p.OutputResult(outputFormat, backups, func() error { + if len(backups) == 0 { + p.Outputf("No backups found for project %s\n", projectLabel) + return nil + } table := tables.NewTable() table.SetHeader("ID", "NAME", "SIZE", "STATUS", "SNAPSHOT ID", "VOLUME ID", "AVAILABILITY ZONE", "LABELS", "CREATED AT", "UPDATED AT") diff --git a/internal/cmd/volume/backup/list/list_test.go b/internal/cmd/volume/backup/list/list_test.go index 0722dd90d..1649e7341 100644 --- a/internal/cmd/volume/backup/list/list_test.go +++ b/internal/cmd/volume/backup/list/list_test.go @@ -162,6 +162,7 @@ func TestBuildRequest(t *testing.T) { func TestOutputResult(t *testing.T) { type args struct { outputFormat string + projectLabel string backups []iaas.Backup } tests := []struct { @@ -172,7 +173,7 @@ func TestOutputResult(t *testing.T) { { name: "empty", args: args{}, - wantErr: true, + wantErr: false, }, { name: "empty backup in slice", @@ -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.backups); (err != nil) != tt.wantErr { + if err := outputResult(p, tt.args.outputFormat, tt.args.projectLabel, tt.args.backups); (err != nil) != tt.wantErr { t.Errorf("outputResult() error = %v, wantErr %v", err, tt.wantErr) } }) From 84fb653083f54c8f23c437b4274d1a0be00ebd3e Mon Sep 17 00:00:00 2001 From: Jan Obernberger Date: Wed, 15 Apr 2026 16:41:48 +0200 Subject: [PATCH 22/25] fixed order of params --- internal/cmd/network-area/list/list.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/internal/cmd/network-area/list/list.go b/internal/cmd/network-area/list/list.go index 15e1a9c11..93f9163d7 100644 --- a/internal/cmd/network-area/list/list.go +++ b/internal/cmd/network-area/list/list.go @@ -103,7 +103,7 @@ func NewCmd(params *types.CmdParams) *cobra.Command { items = items[:*model.Limit] } - return outputResult(params.Printer, orgLabel, model.OutputFormat, items) + return outputResult(params.Printer, model.OutputFormat, orgLabel, items) }, } configureFlags(cmd) @@ -148,7 +148,7 @@ func buildRequest(ctx context.Context, model *inputModel, apiClient *iaas.APICli return req } -func outputResult(p *print.Printer, orgLabel, outputFormat string, networkAreas []iaas.NetworkArea) error { +func outputResult(p *print.Printer, outputFormat, orgLabel 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) From 5c171953ee30039978ce22b6e05a044a4c3335ea Mon Sep 17 00:00:00 2001 From: Jan Obernberger Date: Wed, 15 Apr 2026 16:55:45 +0200 Subject: [PATCH 23/25] adapted network area network ranges list command to align to expectations --- .../network-area/network-range/list/list.go | 25 ++++++++++--------- .../network-range/list/list_test.go | 7 +++--- 2 files changed, 17 insertions(+), 15 deletions(-) diff --git a/internal/cmd/network-area/network-range/list/list.go b/internal/cmd/network-area/network-range/list/list.go index 4ad161d30..593c3de66 100644 --- a/internal/cmd/network-area/network-range/list/list.go +++ b/internal/cmd/network-area/network-range/list/list.go @@ -75,24 +75,21 @@ func NewCmd(params *types.CmdParams) *cobra.Command { return fmt.Errorf("list network ranges: %w", err) } - if resp.Items == nil || len(*resp.Items) == 0 { - var networkAreaLabel string - networkAreaLabel, err = iaasUtils.GetNetworkAreaName(ctx, apiClient, *model.OrganizationId, *model.NetworkAreaId) - if err != nil { - params.Printer.Debug(print.ErrorLevel, "get organization name: %v", err) - networkAreaLabel = *model.NetworkAreaId - } - params.Printer.Info("No network ranges found for SNA %q\n", networkAreaLabel) - return nil + items := resp.GetItems() + + var networkAreaLabel string + networkAreaLabel, err = iaasUtils.GetNetworkAreaName(ctx, apiClient, *model.OrganizationId, *model.NetworkAreaId) + if err != nil { + params.Printer.Debug(print.ErrorLevel, "get organization name: %v", err) + networkAreaLabel = *model.NetworkAreaId } // 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, networkAreaLabel, items) }, } configureFlags(cmd) @@ -133,8 +130,12 @@ func buildRequest(ctx context.Context, model *inputModel, apiClient *iaas.APICli return apiClient.ListNetworkAreaRanges(ctx, *model.OrganizationId, *model.NetworkAreaId, model.Region) } -func outputResult(p *print.Printer, outputFormat string, networkRanges []iaas.NetworkRange) error { +func outputResult(p *print.Printer, outputFormat, networkAreaLabel string, networkRanges []iaas.NetworkRange) error { return p.OutputResult(outputFormat, networkRanges, func() error { + if len(networkRanges) == 0 { + p.Outputf("No network ranges found for SNA %q\n", networkAreaLabel) + return nil + } table := tables.NewTable() table.SetHeader("ID", "Network Range") diff --git a/internal/cmd/network-area/network-range/list/list_test.go b/internal/cmd/network-area/network-range/list/list_test.go index 80ab8a7c4..f26e4086f 100644 --- a/internal/cmd/network-area/network-range/list/list_test.go +++ b/internal/cmd/network-area/network-range/list/list_test.go @@ -185,8 +185,9 @@ func TestBuildRequest(t *testing.T) { func TestOutputResult(t *testing.T) { type args struct { - outputFormat string - networkRanges []iaas.NetworkRange + outputFormat string + networkAreaLabel string + networkRanges []iaas.NetworkRange } tests := []struct { name string @@ -217,7 +218,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.networkRanges); (err != nil) != tt.wantErr { + if err := outputResult(p, tt.args.outputFormat, tt.args.networkAreaLabel, tt.args.networkRanges); (err != nil) != tt.wantErr { t.Errorf("outputResult() error = %v, wantErr %v", err, tt.wantErr) } }) From 434f580ba7722c8a3b138a78a4264cdab52b6d79 Mon Sep 17 00:00:00 2001 From: Jan Obernberger Date: Wed, 15 Apr 2026 17:22:42 +0200 Subject: [PATCH 24/25] adapted network area routes list command to align to expectations --- internal/cmd/network-area/route/list/list.go | 25 ++++++++++--------- .../cmd/network-area/route/list/list_test.go | 7 +++--- 2 files changed, 17 insertions(+), 15 deletions(-) diff --git a/internal/cmd/network-area/route/list/list.go b/internal/cmd/network-area/route/list/list.go index 8cadbdef7..4506c38a7 100644 --- a/internal/cmd/network-area/route/list/list.go +++ b/internal/cmd/network-area/route/list/list.go @@ -74,24 +74,21 @@ func NewCmd(params *types.CmdParams) *cobra.Command { return fmt.Errorf("list static routes: %w", err) } - if resp.Items == nil || len(*resp.Items) == 0 { - var networkAreaLabel string - networkAreaLabel, err = iaasUtils.GetNetworkAreaName(ctx, apiClient, *model.OrganizationId, *model.NetworkAreaId) - if err != nil { - params.Printer.Debug(print.ErrorLevel, "get network area name: %v", err) - networkAreaLabel = *model.NetworkAreaId - } - params.Printer.Info("No static routes found for STACKIT Network Area %q\n", networkAreaLabel) - return nil + items := resp.GetItems() + + var networkAreaLabel string + networkAreaLabel, err = iaasUtils.GetNetworkAreaName(ctx, apiClient, *model.OrganizationId, *model.NetworkAreaId) + if err != nil { + params.Printer.Debug(print.ErrorLevel, "get network area name: %v", err) + networkAreaLabel = *model.NetworkAreaId } // 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, networkAreaLabel, items) }, } configureFlags(cmd) @@ -132,8 +129,12 @@ func buildRequest(ctx context.Context, model *inputModel, apiClient *iaas.APICli return apiClient.ListNetworkAreaRoutes(ctx, *model.OrganizationId, *model.NetworkAreaId, model.Region) } -func outputResult(p *print.Printer, outputFormat string, routes []iaas.Route) error { +func outputResult(p *print.Printer, outputFormat, networkAreaLabel string, routes []iaas.Route) error { return p.OutputResult(outputFormat, routes, func() error { + if len(routes) == 0 { + p.Outputf("No static routes found for STACKIT Network Area %q\n", networkAreaLabel) + return nil + } table := tables.NewTable() table.SetHeader("Static Route ID", "Next Hop", "Next Hop Type", "Destination") diff --git a/internal/cmd/network-area/route/list/list_test.go b/internal/cmd/network-area/route/list/list_test.go index f40f9bafe..4394546a0 100644 --- a/internal/cmd/network-area/route/list/list_test.go +++ b/internal/cmd/network-area/route/list/list_test.go @@ -185,8 +185,9 @@ func TestBuildRequest(t *testing.T) { func TestOutputResult(t *testing.T) { type args struct { - outputFormat string - routes []iaas.Route + outputFormat string + networkAreaLabel string + routes []iaas.Route } tests := []struct { name string @@ -235,7 +236,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.routes); (err != nil) != tt.wantErr { + if err := outputResult(p, tt.args.outputFormat, tt.args.networkAreaLabel, tt.args.routes); (err != nil) != tt.wantErr { t.Errorf("outputResult() error = %v, wantErr %v", err, tt.wantErr) } }) From cc5c7e985783f1879fa3e8c2d987b4d79317f7a9 Mon Sep 17 00:00:00 2001 From: Jan Obernberger Date: Wed, 15 Apr 2026 17:43:25 +0200 Subject: [PATCH 25/25] adapted security group rules list command to align to expectations --- internal/cmd/security-group/rule/list/list.go | 33 ++++++++++--------- .../cmd/security-group/rule/list/list_test.go | 4 ++- 2 files changed, 20 insertions(+), 17 deletions(-) diff --git a/internal/cmd/security-group/rule/list/list.go b/internal/cmd/security-group/rule/list/list.go index 1d39e5ed8..b552a1e73 100644 --- a/internal/cmd/security-group/rule/list/list.go +++ b/internal/cmd/security-group/rule/list/list.go @@ -74,29 +74,26 @@ func NewCmd(params *types.CmdParams) *cobra.Command { return fmt.Errorf("list security group rules: %w", err) } - if resp.Items == nil || len(*resp.Items) == 0 { - securityGroupLabel, err := iaasUtils.GetSecurityGroupName(ctx, apiClient, model.ProjectId, model.Region, model.SecurityGroupId) - if err != nil { - params.Printer.Debug(print.ErrorLevel, "get security group name: %v", err) - securityGroupLabel = model.SecurityGroupId - } + 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 - } - params.Printer.Info("No rules found in security group %q for project %q\n", securityGroupLabel, projectLabel) - return nil + securityGroupLabel, err := iaasUtils.GetSecurityGroupName(ctx, apiClient, model.ProjectId, model.Region, model.SecurityGroupId) + if err != nil { + params.Printer.Debug(print.ErrorLevel, "get security group name: %v", err) + securityGroupLabel = model.SecurityGroupId + } + + 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, securityGroupLabel, items) }, } configureFlags(cmd) @@ -139,8 +136,12 @@ func buildRequest(ctx context.Context, model *inputModel, apiClient *iaas.APICli return apiClient.ListSecurityGroupRules(ctx, model.ProjectId, model.Region, model.SecurityGroupId) } -func outputResult(p *print.Printer, outputFormat string, securityGroupRules []iaas.SecurityGroupRule) error { +func outputResult(p *print.Printer, outputFormat, projectLabel, securityGroupLabel string, securityGroupRules []iaas.SecurityGroupRule) error { return p.OutputResult(outputFormat, securityGroupRules, func() error { + if len(securityGroupRules) == 0 { + p.Outputf("No rules found in security group %q for project %q\n", securityGroupLabel, projectLabel) + return nil + } table := tables.NewTable() table.SetHeader("ID", "ETHER TYPE", "DIRECTION", "PROTOCOL", "REMOTE SECURITY GROUP ID") diff --git a/internal/cmd/security-group/rule/list/list_test.go b/internal/cmd/security-group/rule/list/list_test.go index b617a1b80..01b94fe2a 100644 --- a/internal/cmd/security-group/rule/list/list_test.go +++ b/internal/cmd/security-group/rule/list/list_test.go @@ -186,6 +186,8 @@ func TestBuildRequest(t *testing.T) { func TestOutputResult(t *testing.T) { type args struct { outputFormat string + projectLabel string + securityGroupLabel string securityGroupRules []iaas.SecurityGroupRule } tests := []struct { @@ -203,7 +205,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.securityGroupRules); (err != nil) != tt.wantErr { + if err := outputResult(p, tt.args.outputFormat, tt.args.projectLabel, tt.args.securityGroupLabel, tt.args.securityGroupRules); (err != nil) != tt.wantErr { t.Errorf("outputResult() error = %v, wantErr %v", err, tt.wantErr) } })