From 2600dc73b203aa9ec42f235788a445f5c617acf1 Mon Sep 17 00:00:00 2001 From: Carlo Goetz Date: Wed, 29 Jul 2026 11:19:11 +0200 Subject: [PATCH 1/3] fix(sqlserver): support pagination when listing flavors - add ListAllFlavors utils method - replace all calls to DefaultAPI.ListFlavors with call to util --- .../sqlserverflex/instance/create/create.go | 6 +- .../sqlserverflex/instance/update/update.go | 8 +- .../cmd/beta/sqlserverflex/options/options.go | 9 +- .../pkg/services/sqlserverflex/utils/utils.go | 23 ++- .../sqlserverflex/utils/utils_test.go | 153 ++++++++++++++++++ 5 files changed, 185 insertions(+), 14 deletions(-) diff --git a/internal/cmd/beta/sqlserverflex/instance/create/create.go b/internal/cmd/beta/sqlserverflex/instance/create/create.go index 029da292d..e42121700 100644 --- a/internal/cmd/beta/sqlserverflex/instance/create/create.go +++ b/internal/cmd/beta/sqlserverflex/instance/create/create.go @@ -186,13 +186,13 @@ func buildRequest(ctx context.Context, model *inputModel, apiClient sqlserverfle var flavorId string var err error - flavors, err := apiClient.ListFlavors(ctx, model.ProjectId, model.Region).Execute() + flavors, err := sqlserverflexUtils.ListAllFlavors(ctx, apiClient, model.ProjectId, model.Region) if err != nil { return req, fmt.Errorf("get SQLServer Flex flavors: %w", err) } if model.FlavorId == nil { - flavorId, err = sqlserverflexUtils.LoadFlavorId(*model.CPU, *model.RAM, flavors.Flavors) + flavorId, err = sqlserverflexUtils.LoadFlavorId(*model.CPU, *model.RAM, flavors) if err != nil { var dsaInvalidPlanError *cliErr.DSAInvalidPlanError if !errors.As(err, &dsaInvalidPlanError) { @@ -201,7 +201,7 @@ func buildRequest(ctx context.Context, model *inputModel, apiClient sqlserverfle return req, err } } else { - err := sqlserverflexUtils.ValidateFlavorId(*model.FlavorId, flavors.Flavors) + err := sqlserverflexUtils.ValidateFlavorId(*model.FlavorId, flavors) if err != nil { return req, err } diff --git a/internal/cmd/beta/sqlserverflex/instance/update/update.go b/internal/cmd/beta/sqlserverflex/instance/update/update.go index ecadebaad..e6330848a 100644 --- a/internal/cmd/beta/sqlserverflex/instance/update/update.go +++ b/internal/cmd/beta/sqlserverflex/instance/update/update.go @@ -184,7 +184,7 @@ func buildRequest(ctx context.Context, model *inputModel, apiClient sqlserverfle var flavorId *string var err error - flavors, err := apiClient.ListFlavors(ctx, model.ProjectId, model.Region).Execute() + flavors, err := sqlserverflexUtils.ListAllFlavors(ctx, apiClient, model.ProjectId, model.Region) if err != nil { return req, fmt.Errorf("get SQLServer Flex flavors: %w", err) } @@ -198,7 +198,7 @@ func buildRequest(ctx context.Context, model *inputModel, apiClient sqlserverfle return req, fmt.Errorf("get SQLServer Flex instance: %w", err) } var currentFlavor *sqlserverflex.ListFlavors - for _, flavor := range flavors.Flavors { + for _, flavor := range flavors { if flavor.Id == currentInstance.FlavorId { currentFlavor = &flavor } @@ -213,7 +213,7 @@ func buildRequest(ctx context.Context, model *inputModel, apiClient sqlserverfle cpu = ¤tFlavor.Cpu } } - loadedId, err := sqlserverflexUtils.LoadFlavorId(*cpu, *ram, flavors.Flavors) + loadedId, err := sqlserverflexUtils.LoadFlavorId(*cpu, *ram, flavors) if err != nil { var dsaInvalidPlanError *cliErr.DSAInvalidPlanError if !errors.As(err, &dsaInvalidPlanError) { @@ -223,7 +223,7 @@ func buildRequest(ctx context.Context, model *inputModel, apiClient sqlserverfle } flavorId = &loadedId } else if model.FlavorId != nil { - err := sqlserverflexUtils.ValidateFlavorId(*model.FlavorId, flavors.Flavors) + err := sqlserverflexUtils.ValidateFlavorId(*model.FlavorId, flavors) if err != nil { return req, err } diff --git a/internal/cmd/beta/sqlserverflex/options/options.go b/internal/cmd/beta/sqlserverflex/options/options.go index abbfdf5fd..7ec5249c3 100644 --- a/internal/cmd/beta/sqlserverflex/options/options.go +++ b/internal/cmd/beta/sqlserverflex/options/options.go @@ -4,6 +4,7 @@ import ( "context" "fmt" + sqlserverflexUtils "github.com/stackitcloud/stackit-cli/internal/pkg/services/sqlserverflex/utils" "github.com/stackitcloud/stackit-cli/internal/pkg/types" "github.com/stackitcloud/stackit-cli/internal/pkg/utils" @@ -181,7 +182,7 @@ func parseInput(p *print.Printer, cmd *cobra.Command, _ []string) (*inputModel, } func buildAndExecuteRequest(ctx context.Context, p *print.Printer, model *inputModel, apiClient sqlserverflex.DefaultAPI) error { - var flavors *sqlserverflex.ListFlavorsResponse + var flavors []sqlserverflex.ListFlavors var versions *sqlserverflex.ListVersionsResponse var storages *sqlserverflex.ListStoragesResponse var userRoles *sqlserverflex.ListRolesResponse @@ -190,7 +191,7 @@ func buildAndExecuteRequest(ctx context.Context, p *print.Printer, model *inputM var err error if model.Flavors { - flavors, err = apiClient.ListFlavors(ctx, model.ProjectId, model.Region).Execute() + flavors, err = sqlserverflexUtils.ListAllFlavors(ctx, apiClient, model.ProjectId, model.Region) if err != nil { return fmt.Errorf("get SQL Server Flex flavors: %w", err) } @@ -229,10 +230,10 @@ func buildAndExecuteRequest(ctx context.Context, p *print.Printer, model *inputM return outputResult(p, model, flavors, versions, storages, userRoles, dbCollations, dbCompatibilities) } -func outputResult(p *print.Printer, model *inputModel, flavors *sqlserverflex.ListFlavorsResponse, versions *sqlserverflex.ListVersionsResponse, storages *sqlserverflex.ListStoragesResponse, userRoles *sqlserverflex.ListRolesResponse, dbCollations *sqlserverflex.ListCollationsResponse, dbCompatibilities *sqlserverflex.ListCompatibilityResponse) error { +func outputResult(p *print.Printer, model *inputModel, flavors []sqlserverflex.ListFlavors, versions *sqlserverflex.ListVersionsResponse, storages *sqlserverflex.ListStoragesResponse, userRoles *sqlserverflex.ListRolesResponse, dbCollations *sqlserverflex.ListCollationsResponse, dbCompatibilities *sqlserverflex.ListCompatibilityResponse) error { options := &options{} if flavors != nil { - options.Flavors = flavors.Flavors + options.Flavors = flavors } if versions != nil { options.Versions = versions.Versions diff --git a/internal/pkg/services/sqlserverflex/utils/utils.go b/internal/pkg/services/sqlserverflex/utils/utils.go index d35f69f21..d1d0c53d6 100644 --- a/internal/pkg/services/sqlserverflex/utils/utils.go +++ b/internal/pkg/services/sqlserverflex/utils/utils.go @@ -3,6 +3,7 @@ package utils import ( "context" "fmt" + "math" "strings" "github.com/stackitcloud/stackit-cli/internal/pkg/errors" @@ -75,6 +76,23 @@ func LoadFlavorId(cpu, ram int64, flavors []sqlserverflex.ListFlavors) (string, } } +func ListAllFlavors(ctx context.Context, api sqlserverflex.DefaultAPI, projectId, region string) ([]sqlserverflex.ListFlavors, error) { + const pageSize = 100 + const sort = sqlserverflex.FLAVORSORT_ID_ASC + totalRows := int64(math.MaxInt64) + var page int64 = 1 + var result []sqlserverflex.ListFlavors + for ; int64(len(result)) < totalRows; page++ { + flavors, err := api.ListFlavors(ctx, projectId, region).Page(page).Sort(sort).Size(pageSize).Execute() + if err != nil { + return result, fmt.Errorf("listing flavors: %w", err) + } + result = append(result, flavors.Flavors...) + totalRows = flavors.Pagination.TotalRows + } + return result, nil +} + func GetInstanceName(ctx context.Context, apiClient sqlserverflex.DefaultAPI, projectId, instanceId, region string) (string, error) { resp, err := apiClient.GetInstance(ctx, projectId, region, instanceId).Execute() if err != nil { @@ -92,12 +110,11 @@ func GetUserName(ctx context.Context, apiClient sqlserverflex.DefaultAPI, projec } func GetFlavor(ctx context.Context, client sqlserverflex.DefaultAPI, projectId, region, flavorId string) (*sqlserverflex.ListFlavors, error) { - req := client.ListFlavors(ctx, projectId, region) - flavorsResp, err := client.ListFlavorsExecute(req) + flavors, err := ListAllFlavors(ctx, client, projectId, region) if err != nil { return nil, fmt.Errorf("failed to list flavors: %w", err) } - for _, flavor := range flavorsResp.Flavors { + for _, flavor := range flavors { if flavor.Id == flavorId { return &flavor, nil } diff --git a/internal/pkg/services/sqlserverflex/utils/utils_test.go b/internal/pkg/services/sqlserverflex/utils/utils_test.go index bf9eb5abc..6b4fa4a6f 100644 --- a/internal/pkg/services/sqlserverflex/utils/utils_test.go +++ b/internal/pkg/services/sqlserverflex/utils/utils_test.go @@ -25,6 +25,10 @@ const ( ) type mockSettings struct { + listFlavorsFails bool + listFlavorsResp *sqlserverflex.ListFlavorsResponse + listFlavorsResps []*sqlserverflex.ListFlavorsResponse + listFlavorsCallCount int listVersionsFails bool listVersionsResp *sqlserverflex.ListVersionsResponse getInstanceFails bool @@ -37,6 +41,20 @@ type mockSettings struct { func newApiMock(s *mockSettings) sqlserverflex.DefaultAPI { return &sqlserverflex.DefaultAPIServiceMock{ + ListFlavorsExecuteMock: utils.Ptr(func(_ sqlserverflex.ApiListFlavorsRequest) (*sqlserverflex.ListFlavorsResponse, error) { + if s.listFlavorsFails { + return nil, fmt.Errorf("could not list flavors") + } + if len(s.listFlavorsResps) > 0 { + if s.listFlavorsCallCount >= len(s.listFlavorsResps) { + return nil, fmt.Errorf("no more mock responses") + } + resp := s.listFlavorsResps[s.listFlavorsCallCount] + s.listFlavorsCallCount++ + return resp, nil + } + return s.listFlavorsResp, nil + }), ListVersionsExecuteMock: utils.Ptr(func(_ sqlserverflex.ApiListVersionsRequest) (*sqlserverflex.ListVersionsResponse, error) { if s.listVersionsFails { return nil, fmt.Errorf("could not list versions") @@ -64,6 +82,141 @@ func newApiMock(s *mockSettings) sqlserverflex.DefaultAPI { } } +func TestListAllFlavors(t *testing.T) { + tests := []struct { + description string + listFlavorsFails bool + listFlavorsResp *sqlserverflex.ListFlavorsResponse + listFlavorsResps []*sqlserverflex.ListFlavorsResponse + isValid bool + expectedOutput []sqlserverflex.ListFlavors + }{ + { + description: "base", + listFlavorsResp: &sqlserverflex.ListFlavorsResponse{ + Flavors: []sqlserverflex.ListFlavors{ + { + Id: "flavor-1", + }, + { + Id: "flavor-2", + }, + }, + Pagination: sqlserverflex.Pagination{ + TotalRows: 2, + }, + }, + isValid: true, + expectedOutput: []sqlserverflex.ListFlavors{ + { + Id: "flavor-1", + }, + { + Id: "flavor-2", + }, + }, + }, + { + description: "multiple pages", + listFlavorsResps: []*sqlserverflex.ListFlavorsResponse{ + { + Flavors: []sqlserverflex.ListFlavors{ + { + Id: "flavor-1", + }, + { + Id: "flavor-2", + }, + }, + Pagination: sqlserverflex.Pagination{ + TotalRows: 3, + }, + }, + { + Flavors: []sqlserverflex.ListFlavors{ + { + Id: "flavor-3", + }, + }, + Pagination: sqlserverflex.Pagination{ + TotalRows: 3, + }, + }, + }, + isValid: true, + expectedOutput: []sqlserverflex.ListFlavors{ + { + Id: "flavor-1", + }, + { + Id: "flavor-2", + }, + { + Id: "flavor-3", + }, + }, + }, + { + description: "empty response", + listFlavorsResp: &sqlserverflex.ListFlavorsResponse{ + Flavors: []sqlserverflex.ListFlavors{}, + Pagination: sqlserverflex.Pagination{ + TotalRows: 0, + }, + }, + isValid: true, + expectedOutput: nil, + }, + { + description: "list flavors fails", + listFlavorsFails: true, + isValid: false, + }, + { + description: "page 2 fails", + listFlavorsResps: []*sqlserverflex.ListFlavorsResponse{ + { + Flavors: []sqlserverflex.ListFlavors{ + { + Id: "flavor-1", + }, + }, + Pagination: sqlserverflex.Pagination{ + TotalRows: 2, + }, + }, + }, + isValid: false, + }, + } + + for _, tt := range tests { + t.Run(tt.description, func(t *testing.T) { + settings := &mockSettings{ + listFlavorsFails: tt.listFlavorsFails, + listFlavorsResp: tt.listFlavorsResp, + listFlavorsResps: tt.listFlavorsResps, + } + + output, err := ListAllFlavors(context.Background(), newApiMock(settings), testProjectId, testRegion) + + if tt.isValid && err != nil { + t.Fatalf("failed on valid input: %v", err) + } + if !tt.isValid && err == nil { + t.Fatalf("did not fail on invalid input") + } + if !tt.isValid { + return + } + diff := cmp.Diff(output, tt.expectedOutput) + if diff != "" { + t.Fatalf("outputs do not match: %s", diff) + } + }) + } +} + func TestValidateStorage(t *testing.T) { tests := []struct { description string From 0f67ac1277670570be7b628a40c004be553505e6 Mon Sep 17 00:00:00 2001 From: Carlo Goetz Date: Wed, 29 Jul 2026 11:25:12 +0200 Subject: [PATCH 2/3] fix test --- internal/cmd/beta/sqlserverflex/options/options_test.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/internal/cmd/beta/sqlserverflex/options/options_test.go b/internal/cmd/beta/sqlserverflex/options/options_test.go index 52b9ce0c4..5d028bcb6 100644 --- a/internal/cmd/beta/sqlserverflex/options/options_test.go +++ b/internal/cmd/beta/sqlserverflex/options/options_test.go @@ -466,7 +466,7 @@ func TestBuildAndExecuteRequest(t *testing.T) { func TestOutputResult(t *testing.T) { type args struct { model *inputModel - flavors *sqlserverflex.ListFlavorsResponse + flavors []sqlserverflex.ListFlavors versions *sqlserverflex.ListVersionsResponse storages *sqlserverflex.ListStoragesResponse userRoles *sqlserverflex.ListRolesResponse @@ -489,7 +489,7 @@ func TestOutputResult(t *testing.T) { name: "all input set", args: args{ model: fixtureInputModelAllTrue(), - flavors: &sqlserverflex.ListFlavorsResponse{Flavors: []sqlserverflex.ListFlavors{}}, + flavors: []sqlserverflex.ListFlavors{}, versions: &sqlserverflex.ListVersionsResponse{Versions: []sqlserverflex.Version{}}, storages: &sqlserverflex.ListStoragesResponse{StorageClasses: []sqlserverflex.FlavorStorageClassesStorageClass{}}, userRoles: &sqlserverflex.ListRolesResponse{Roles: []string{}}, From 5a33359c49c2c3104195d532897e1b30f9639583 Mon Sep 17 00:00:00 2001 From: Carlo Goetz Date: Wed, 29 Jul 2026 11:36:54 +0200 Subject: [PATCH 3/3] fix test --- .../cmd/beta/sqlserverflex/instance/update/update_test.go | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/internal/cmd/beta/sqlserverflex/instance/update/update_test.go b/internal/cmd/beta/sqlserverflex/instance/update/update_test.go index 4b4c4a86a..e34ab6b31 100644 --- a/internal/cmd/beta/sqlserverflex/instance/update/update_test.go +++ b/internal/cmd/beta/sqlserverflex/instance/update/update_test.go @@ -356,6 +356,9 @@ func TestBuildRequest(t *testing.T) { model: fixtureRequiredInputModel(), isValid: true, expectedRequest: fixtureRequest(), + listFlavorsResp: &sqlserverflex.ListFlavorsResponse{ + Flavors: make([]sqlserverflex.ListFlavors, 0), + }, }, { description: "update flavor from id", @@ -443,6 +446,9 @@ func TestBuildRequest(t *testing.T) { ), getInstanceFails: true, isValid: false, + listFlavorsResp: &sqlserverflex.ListFlavorsResponse{ + Flavors: make([]sqlserverflex.ListFlavors, 0), + }, }, { description: "get storages fails",