diff --git a/docs/stackit_ske_options.md b/docs/stackit_ske_options.md index 76afbe93c..2590f989c 100644 --- a/docs/stackit_ske_options.md +++ b/docs/stackit_ske_options.md @@ -4,6 +4,7 @@ Lists SKE provider options ### Synopsis +Command "options" is deprecated, use the subcommands instead. Lists STACKIT Kubernetes Engine (SKE) provider options (availability zones, Kubernetes versions, machine images and types, volume types). Pass one or more flags to filter what categories are shown. @@ -11,28 +12,10 @@ Pass one or more flags to filter what categories are shown. stackit ske options [flags] ``` -### Examples - -``` - List SKE options for all categories - $ stackit ske options - - List SKE options regarding Kubernetes versions only - $ stackit ske options --kubernetes-versions - - List SKE options regarding Kubernetes versions and machine images - $ stackit ske options --kubernetes-versions --machine-images -``` - ### Options ``` - --availability-zones Lists availability zones - -h, --help Help for "stackit ske options" - --kubernetes-versions Lists supported kubernetes versions - --machine-images Lists supported machine images - --machine-types Lists supported machine types - --volume-types Lists supported volume types + -h, --help Help for "stackit ske options" ``` ### Options inherited from parent commands @@ -49,4 +32,9 @@ stackit ske options [flags] ### SEE ALSO * [stackit ske](./stackit_ske.md) - Provides functionality for SKE +* [stackit ske options availability-zones](./stackit_ske_options_availability-zones.md) - Lists SKE provider options for availability-zones +* [stackit ske options kubernetes-versions](./stackit_ske_options_kubernetes-versions.md) - Lists SKE provider options for kubernetes-versions +* [stackit ske options machine-images](./stackit_ske_options_machine-images.md) - Lists SKE provider options for machine-images +* [stackit ske options machine-types](./stackit_ske_options_machine-types.md) - Lists SKE provider options for machine-types +* [stackit ske options volume-types](./stackit_ske_options_volume-types.md) - Lists SKE provider options for volume-types diff --git a/docs/stackit_ske_options_availability-zones.md b/docs/stackit_ske_options_availability-zones.md new file mode 100644 index 000000000..4bf77c67f --- /dev/null +++ b/docs/stackit_ske_options_availability-zones.md @@ -0,0 +1,40 @@ +## stackit ske options availability-zones + +Lists SKE provider options for availability-zones + +### Synopsis + +Lists STACKIT Kubernetes Engine (SKE) provider options for availability-zones. + +``` +stackit ske options availability-zones [flags] +``` + +### Examples + +``` + List SKE options for availability-zones + $ stackit ske options availability-zones +``` + +### Options + +``` + -h, --help Help for "stackit ske options availability-zones" +``` + +### Options inherited from parent commands + +``` + -y, --assume-yes If set, skips all confirmation prompts + --async If set, runs the command asynchronously + -o, --output-format string Output format, one of ["json" "pretty" "none" "yaml"] + -p, --project-id string Project ID + --region string Target region for region-specific requests + --verbosity string Verbosity of the CLI, one of ["debug" "info" "warning" "error"] (default "info") +``` + +### SEE ALSO + +* [stackit ske options](./stackit_ske_options.md) - Lists SKE provider options + diff --git a/docs/stackit_ske_options_kubernetes-versions.md b/docs/stackit_ske_options_kubernetes-versions.md new file mode 100644 index 000000000..a2dd50edd --- /dev/null +++ b/docs/stackit_ske_options_kubernetes-versions.md @@ -0,0 +1,44 @@ +## stackit ske options kubernetes-versions + +Lists SKE provider options for kubernetes-versions + +### Synopsis + +Lists STACKIT Kubernetes Engine (SKE) provider options for kubernetes-versions. + +``` +stackit ske options kubernetes-versions [flags] +``` + +### Examples + +``` + List SKE options for kubernetes-versions + $ stackit ske options kubernetes-versions + + List SKE options for supported kubernetes-versions + $ stackit ske options kubernetes-versions --supported +``` + +### Options + +``` + -h, --help Help for "stackit ske options kubernetes-versions" + --supported List supported versions only +``` + +### Options inherited from parent commands + +``` + -y, --assume-yes If set, skips all confirmation prompts + --async If set, runs the command asynchronously + -o, --output-format string Output format, one of ["json" "pretty" "none" "yaml"] + -p, --project-id string Project ID + --region string Target region for region-specific requests + --verbosity string Verbosity of the CLI, one of ["debug" "info" "warning" "error"] (default "info") +``` + +### SEE ALSO + +* [stackit ske options](./stackit_ske_options.md) - Lists SKE provider options + diff --git a/docs/stackit_ske_options_machine-images.md b/docs/stackit_ske_options_machine-images.md new file mode 100644 index 000000000..f6deb67db --- /dev/null +++ b/docs/stackit_ske_options_machine-images.md @@ -0,0 +1,40 @@ +## stackit ske options machine-images + +Lists SKE provider options for machine-images + +### Synopsis + +Lists STACKIT Kubernetes Engine (SKE) provider options for machine-images. + +``` +stackit ske options machine-images [flags] +``` + +### Examples + +``` + List SKE options for machine-images + $ stackit ske options machine-images +``` + +### Options + +``` + -h, --help Help for "stackit ske options machine-images" +``` + +### Options inherited from parent commands + +``` + -y, --assume-yes If set, skips all confirmation prompts + --async If set, runs the command asynchronously + -o, --output-format string Output format, one of ["json" "pretty" "none" "yaml"] + -p, --project-id string Project ID + --region string Target region for region-specific requests + --verbosity string Verbosity of the CLI, one of ["debug" "info" "warning" "error"] (default "info") +``` + +### SEE ALSO + +* [stackit ske options](./stackit_ske_options.md) - Lists SKE provider options + diff --git a/docs/stackit_ske_options_machine-types.md b/docs/stackit_ske_options_machine-types.md new file mode 100644 index 000000000..333384fc3 --- /dev/null +++ b/docs/stackit_ske_options_machine-types.md @@ -0,0 +1,40 @@ +## stackit ske options machine-types + +Lists SKE provider options for machine-types + +### Synopsis + +Lists STACKIT Kubernetes Engine (SKE) provider options for machine-types. + +``` +stackit ske options machine-types [flags] +``` + +### Examples + +``` + List SKE options for machine-types + $ stackit ske options machine-types +``` + +### Options + +``` + -h, --help Help for "stackit ske options machine-types" +``` + +### Options inherited from parent commands + +``` + -y, --assume-yes If set, skips all confirmation prompts + --async If set, runs the command asynchronously + -o, --output-format string Output format, one of ["json" "pretty" "none" "yaml"] + -p, --project-id string Project ID + --region string Target region for region-specific requests + --verbosity string Verbosity of the CLI, one of ["debug" "info" "warning" "error"] (default "info") +``` + +### SEE ALSO + +* [stackit ske options](./stackit_ske_options.md) - Lists SKE provider options + diff --git a/docs/stackit_ske_options_volume-types.md b/docs/stackit_ske_options_volume-types.md new file mode 100644 index 000000000..aeea921dc --- /dev/null +++ b/docs/stackit_ske_options_volume-types.md @@ -0,0 +1,40 @@ +## stackit ske options volume-types + +Lists SKE provider options for volume-types + +### Synopsis + +Lists STACKIT Kubernetes Engine (SKE) provider options for volume-types. + +``` +stackit ske options volume-types [flags] +``` + +### Examples + +``` + List SKE options for volume-types + $ stackit ske options volume-types +``` + +### Options + +``` + -h, --help Help for "stackit ske options volume-types" +``` + +### Options inherited from parent commands + +``` + -y, --assume-yes If set, skips all confirmation prompts + --async If set, runs the command asynchronously + -o, --output-format string Output format, one of ["json" "pretty" "none" "yaml"] + -p, --project-id string Project ID + --region string Target region for region-specific requests + --verbosity string Verbosity of the CLI, one of ["debug" "info" "warning" "error"] (default "info") +``` + +### SEE ALSO + +* [stackit ske options](./stackit_ske_options.md) - Lists SKE provider options + diff --git a/internal/cmd/ske/options/availability_zones/availability_zones.go b/internal/cmd/ske/options/availability_zones/availability_zones.go new file mode 100644 index 000000000..bef599ad4 --- /dev/null +++ b/internal/cmd/ske/options/availability_zones/availability_zones.go @@ -0,0 +1,103 @@ +package availability_zones + +import ( + "context" + "fmt" + + "github.com/stackitcloud/stackit-cli/internal/pkg/types" + "github.com/stackitcloud/stackit-cli/internal/pkg/utils" + + "github.com/spf13/cobra" + "github.com/stackitcloud/stackit-cli/internal/pkg/args" + "github.com/stackitcloud/stackit-cli/internal/pkg/examples" + "github.com/stackitcloud/stackit-cli/internal/pkg/globalflags" + "github.com/stackitcloud/stackit-cli/internal/pkg/print" + "github.com/stackitcloud/stackit-cli/internal/pkg/services/ske/client" + "github.com/stackitcloud/stackit-cli/internal/pkg/tables" + "github.com/stackitcloud/stackit-sdk-go/services/ske" +) + +type inputModel struct { + globalflags.GlobalFlagModel +} + +func NewCmd(params *types.CmdParams) *cobra.Command { + cmd := &cobra.Command{ + Use: "availability-zones", + Short: "Lists SKE provider options for availability-zones", + Long: "Lists STACKIT Kubernetes Engine (SKE) provider options for availability-zones.", + Args: args.NoArgs, + Example: examples.Build( + examples.NewExample( + `List SKE options for availability-zones`, + "$ stackit ske options availability-zones"), + ), + RunE: func(cmd *cobra.Command, args []string) error { + ctx := context.Background() + model, err := parseInput(params.Printer, cmd, args) + if err != nil { + return err + } + + // Configure API client + apiClient, err := client.ConfigureClient(params.Printer, params.CliVersion) + if err != nil { + return err + } + + // Call API + req := buildRequest(ctx, apiClient, model) + resp, err := req.Execute() + if err != nil { + return fmt.Errorf("get SKE provider options: %w", err) + } + + return outputResult(params.Printer, model, resp) + }, + } + return cmd +} + +func parseInput(p *print.Printer, cmd *cobra.Command, _ []string) (*inputModel, error) { + globalFlags := globalflags.Parse(p, cmd) + + model := inputModel{ + GlobalFlagModel: utils.PtrValue(globalFlags), + } + + p.DebugInputModel(model) + return &model, nil +} + +func buildRequest(ctx context.Context, apiClient *ske.APIClient, model *inputModel) ske.ApiListProviderOptionsRequest { + req := apiClient.ListProviderOptions(ctx, model.Region) + return req +} + +func outputResult(p *print.Printer, model *inputModel, options *ske.ProviderOptions) error { + if options == nil { + return fmt.Errorf("options is nil") + } + + options.KubernetesVersions = nil + options.MachineImages = nil + options.MachineTypes = nil + options.VolumeTypes = nil + + return p.OutputResult(model.OutputFormat, options, func() error { + zones := utils.PtrValue(options.AvailabilityZones) + + table := tables.NewTable() + table.SetHeader("ZONE") + for i := range zones { + z := zones[i] + table.AddRow(utils.PtrValue(z.Name)) + } + + err := table.Display(p) + if err != nil { + return fmt.Errorf("display output: %w", err) + } + return nil + }) +} diff --git a/internal/cmd/ske/options/availability_zones/availability_zones_test.go b/internal/cmd/ske/options/availability_zones/availability_zones_test.go new file mode 100644 index 000000000..e66bca441 --- /dev/null +++ b/internal/cmd/ske/options/availability_zones/availability_zones_test.go @@ -0,0 +1,203 @@ +package availability_zones + +import ( + "context" + "testing" + + "github.com/stackitcloud/stackit-cli/internal/pkg/types" + "github.com/stackitcloud/stackit-cli/internal/pkg/utils" + + "github.com/stackitcloud/stackit-cli/internal/pkg/globalflags" + "github.com/stackitcloud/stackit-cli/internal/pkg/print" + "github.com/stackitcloud/stackit-cli/internal/pkg/testutils" + + "github.com/google/go-cmp/cmp" + "github.com/google/go-cmp/cmp/cmpopts" + "github.com/stackitcloud/stackit-sdk-go/services/ske" +) + +type testCtxKey struct{} + +var testCtx = context.WithValue(context.Background(), testCtxKey{}, "foo") +var testClient = &ske.APIClient{} + +const testRegion = "eu01" + +func fixtureFlagValues(mods ...func(flagValues map[string]string)) map[string]string { + flagValues := map[string]string{ + globalflags.RegionFlag: testRegion, + } + for _, mod := range mods { + mod(flagValues) + } + return flagValues +} + +func fixtureInputModel(mods ...func(model *inputModel)) *inputModel { + model := &inputModel{ + GlobalFlagModel: globalflags.GlobalFlagModel{ + Region: testRegion, + Verbosity: globalflags.VerbosityDefault, + }, + } + for _, mod := range mods { + mod(model) + } + return model +} + +func TestParseInput(t *testing.T) { + tests := []struct { + description string + argValues []string + flagValues map[string]string + isValid bool + expectedModel *inputModel + }{ + { + description: "base", + flagValues: fixtureFlagValues(), + isValid: true, + expectedModel: fixtureInputModel(), + }, + { + description: "no values", + flagValues: map[string]string{}, + isValid: true, + expectedModel: fixtureInputModel(func(model *inputModel) { + model.Region = "" + }), + }, + } + + for _, tt := range tests { + t.Run(tt.description, func(t *testing.T) { + testutils.TestParseInput(t, NewCmd, parseInput, tt.expectedModel, tt.argValues, tt.flagValues, tt.isValid) + }) + } +} + +func TestBuildRequest(t *testing.T) { + tests := []struct { + description string + inputModel *inputModel + expectedRequest ske.ApiListProviderOptionsRequest + }{ + { + description: "base", + inputModel: fixtureInputModel(), + expectedRequest: testClient.ListProviderOptions(testCtx, testRegion), + }, + } + + for _, tt := range tests { + t.Run(tt.description, func(t *testing.T) { + request := buildRequest(testCtx, testClient, tt.inputModel) + + diff := cmp.Diff(request, tt.expectedRequest, + cmp.AllowUnexported(tt.expectedRequest), + cmpopts.EquateComparable(testCtx), + ) + if diff != "" { + t.Fatalf("Data does not match: %s", diff) + } + }) + } +} + +func TestOutputResult(t *testing.T) { + type args struct { + model *inputModel + options *ske.ProviderOptions + } + tests := []struct { + name string + args args + wantErr bool + }{ + { + name: "empty", + args: args{}, + wantErr: true, + }, + { + name: "missing options", + args: args{ + model: &inputModel{ + GlobalFlagModel: globalflags.GlobalFlagModel{}, + }, + }, + wantErr: true, + }, + { + name: "empty input model", + args: args{ + model: &inputModel{}, + options: &ske.ProviderOptions{}, + }, + wantErr: false, + }, + { + name: "set model and options", + args: args{ + model: &inputModel{ + GlobalFlagModel: globalflags.GlobalFlagModel{}, + }, + options: &ske.ProviderOptions{}, + }, + wantErr: false, + }, + { + name: "empty values", + args: args{ + model: &inputModel{ + GlobalFlagModel: globalflags.GlobalFlagModel{}, + }, + options: &ske.ProviderOptions{ + AvailabilityZones: &[]ske.AvailabilityZone{}, + }, + }, + wantErr: false, + }, + { + name: "empty value in values", + args: args{ + model: &inputModel{ + GlobalFlagModel: globalflags.GlobalFlagModel{}, + }, + options: &ske.ProviderOptions{ + AvailabilityZones: &[]ske.AvailabilityZone{{}}, + }, + }, + wantErr: false, + }, + { + name: "valid values", + args: args{ + model: &inputModel{ + GlobalFlagModel: globalflags.GlobalFlagModel{}, + }, + options: &ske.ProviderOptions{ + AvailabilityZones: &[]ske.AvailabilityZone{ + { + Name: utils.Ptr("zone1"), + }, + { + Name: utils.Ptr("zone2"), + }, + }, + }, + }, + wantErr: false, + }, + } + p := print.NewPrinter() + p.Cmd = NewCmd(&types.CmdParams{Printer: p}) + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if err := outputResult(p, tt.args.model, tt.args.options); (err != nil) != tt.wantErr { + t.Errorf("outputResult() error = %v, wantErr %v", err, tt.wantErr) + } + }) + } +} diff --git a/internal/cmd/ske/options/kubernetes_versions/kubernetes_versions.go b/internal/cmd/ske/options/kubernetes_versions/kubernetes_versions.go new file mode 100644 index 000000000..f7e0b8ab2 --- /dev/null +++ b/internal/cmd/ske/options/kubernetes_versions/kubernetes_versions.go @@ -0,0 +1,135 @@ +package kubernetes_versions + +import ( + "context" + "encoding/json" + "fmt" + "time" + + "github.com/stackitcloud/stackit-cli/internal/pkg/flags" + "github.com/stackitcloud/stackit-cli/internal/pkg/types" + "github.com/stackitcloud/stackit-cli/internal/pkg/utils" + + "github.com/spf13/cobra" + "github.com/stackitcloud/stackit-cli/internal/pkg/args" + "github.com/stackitcloud/stackit-cli/internal/pkg/examples" + "github.com/stackitcloud/stackit-cli/internal/pkg/globalflags" + "github.com/stackitcloud/stackit-cli/internal/pkg/print" + "github.com/stackitcloud/stackit-cli/internal/pkg/services/ske/client" + "github.com/stackitcloud/stackit-cli/internal/pkg/tables" + "github.com/stackitcloud/stackit-sdk-go/services/ske" +) + +const ( + supportedFlag = "supported" +) + +type inputModel struct { + globalflags.GlobalFlagModel + Supported bool +} + +func NewCmd(params *types.CmdParams) *cobra.Command { + cmd := &cobra.Command{ + Use: "kubernetes-versions", + Short: "Lists SKE provider options for kubernetes-versions", + Long: "Lists STACKIT Kubernetes Engine (SKE) provider options for kubernetes-versions.", + Args: args.NoArgs, + Example: examples.Build( + examples.NewExample( + `List SKE options for kubernetes-versions`, + "$ stackit ske options kubernetes-versions"), + examples.NewExample( + `List SKE options for supported kubernetes-versions`, + "$ stackit ske options kubernetes-versions --supported"), + ), + RunE: func(cmd *cobra.Command, args []string) error { + ctx := context.Background() + model, err := parseInput(params.Printer, cmd, args) + if err != nil { + return err + } + + // Configure API client + apiClient, err := client.ConfigureClient(params.Printer, params.CliVersion) + if err != nil { + return err + } + + // Call API + req := buildRequest(ctx, apiClient, model) + resp, err := req.Execute() + if err != nil { + return fmt.Errorf("get SKE provider options: %w", err) + } + + return outputResult(params.Printer, model, resp) + }, + } + configureFlags(cmd) + return cmd +} + +func configureFlags(cmd *cobra.Command) { + cmd.Flags().Bool(supportedFlag, false, "List supported versions only") +} + +func parseInput(p *print.Printer, cmd *cobra.Command, _ []string) (*inputModel, error) { + globalFlags := globalflags.Parse(p, cmd) + + model := inputModel{ + GlobalFlagModel: utils.PtrValue(globalFlags), + Supported: flags.FlagToBoolValue(p, cmd, supportedFlag), + } + + p.DebugInputModel(model) + return &model, nil +} + +func buildRequest(ctx context.Context, apiClient *ske.APIClient, model *inputModel) ske.ApiListProviderOptionsRequest { + req := apiClient.ListProviderOptions(ctx, model.Region) + if model.Supported { + req = req.VersionState("SUPPORTED") + } + return req +} + +func outputResult(p *print.Printer, model *inputModel, options *ske.ProviderOptions) error { + if options == nil { + return fmt.Errorf("options is nil") + } + + options.AvailabilityZones = nil + options.MachineImages = nil + options.MachineTypes = nil + options.VolumeTypes = nil + + return p.OutputResult(model.OutputFormat, options, func() error { + versions := utils.PtrValue(options.KubernetesVersions) + + table := tables.NewTable() + table.SetHeader("VERSION", "STATE", "EXPIRATION DATE", "FEATURE GATES") + for i := range versions { + v := versions[i] + featureGate, err := json.Marshal(utils.PtrValue(v.FeatureGates)) + if err != nil { + return fmt.Errorf("marshal featureGates of Kubernetes version %q: %w", utils.PtrValue(v.Version), err) + } + expirationDate := "" + if v.ExpirationDate != nil { + expirationDate = v.ExpirationDate.Format(time.RFC3339) + } + table.AddRow( + utils.PtrString(v.Version), + utils.PtrString(v.State), + expirationDate, + string(featureGate)) + } + + err := table.Display(p) + if err != nil { + return fmt.Errorf("display output: %w", err) + } + return nil + }) +} diff --git a/internal/cmd/ske/options/kubernetes_versions/kubernetes_versions_test.go b/internal/cmd/ske/options/kubernetes_versions/kubernetes_versions_test.go new file mode 100644 index 000000000..f8c7bd3c4 --- /dev/null +++ b/internal/cmd/ske/options/kubernetes_versions/kubernetes_versions_test.go @@ -0,0 +1,231 @@ +package kubernetes_versions + +import ( + "context" + "testing" + "time" + + "github.com/stackitcloud/stackit-cli/internal/pkg/types" + "github.com/stackitcloud/stackit-cli/internal/pkg/utils" + + "github.com/stackitcloud/stackit-cli/internal/pkg/globalflags" + "github.com/stackitcloud/stackit-cli/internal/pkg/print" + "github.com/stackitcloud/stackit-cli/internal/pkg/testutils" + + "github.com/google/go-cmp/cmp" + "github.com/google/go-cmp/cmp/cmpopts" + "github.com/stackitcloud/stackit-sdk-go/services/ske" +) + +type testCtxKey struct{} + +var testCtx = context.WithValue(context.Background(), testCtxKey{}, "foo") +var testClient = &ske.APIClient{} + +const testRegion = "eu01" + +func fixtureFlagValues(mods ...func(flagValues map[string]string)) map[string]string { + flagValues := map[string]string{ + globalflags.RegionFlag: testRegion, + supportedFlag: "false", + } + for _, mod := range mods { + mod(flagValues) + } + return flagValues +} + +func fixtureInputModel(mods ...func(model *inputModel)) *inputModel { + model := &inputModel{ + GlobalFlagModel: globalflags.GlobalFlagModel{ + Region: testRegion, + Verbosity: globalflags.VerbosityDefault, + }, + Supported: false, + } + for _, mod := range mods { + mod(model) + } + return model +} + +func TestParseInput(t *testing.T) { + tests := []struct { + description string + argValues []string + flagValues map[string]string + isValid bool + expectedModel *inputModel + }{ + { + description: "base", + flagValues: fixtureFlagValues(), + isValid: true, + expectedModel: fixtureInputModel(), + }, + { + description: "no values", + flagValues: map[string]string{}, + isValid: true, + expectedModel: fixtureInputModel(func(model *inputModel) { + model.Region = "" + }), + }, + { + description: "supported only", + flagValues: map[string]string{ + supportedFlag: "true", + }, + isValid: true, + expectedModel: fixtureInputModel(func(model *inputModel) { + model.Supported = true + model.Region = "" + }), + }, + } + + for _, tt := range tests { + t.Run(tt.description, func(t *testing.T) { + testutils.TestParseInput(t, NewCmd, parseInput, tt.expectedModel, tt.argValues, tt.flagValues, tt.isValid) + }) + } +} + +func TestBuildRequest(t *testing.T) { + tests := []struct { + description string + inputModel *inputModel + expectedRequest ske.ApiListProviderOptionsRequest + }{ + { + description: "base", + inputModel: fixtureInputModel(), + expectedRequest: testClient.ListProviderOptions(testCtx, testRegion), + }, + { + description: "base", + inputModel: fixtureInputModel(func(model *inputModel) { + model.Supported = true + }), + expectedRequest: testClient.ListProviderOptions(testCtx, testRegion).VersionState("SUPPORTED"), + }, + } + + for _, tt := range tests { + t.Run(tt.description, func(t *testing.T) { + request := buildRequest(testCtx, testClient, tt.inputModel) + + diff := cmp.Diff(request, tt.expectedRequest, + cmp.AllowUnexported(tt.expectedRequest), + cmpopts.EquateComparable(testCtx), + ) + if diff != "" { + t.Fatalf("Data does not match: %s", diff) + } + }) + } +} + +func TestOutputResult(t *testing.T) { + type args struct { + model *inputModel + options *ske.ProviderOptions + } + tests := []struct { + name string + args args + wantErr bool + }{ + { + name: "empty", + args: args{}, + wantErr: true, + }, + { + name: "missing options", + args: args{ + model: &inputModel{ + GlobalFlagModel: globalflags.GlobalFlagModel{}, + }, + }, + wantErr: true, + }, + { + name: "empty input model", + args: args{ + model: &inputModel{}, + options: &ske.ProviderOptions{}, + }, + wantErr: false, + }, + { + name: "set model and options", + args: args{ + model: &inputModel{ + GlobalFlagModel: globalflags.GlobalFlagModel{}, + }, + options: &ske.ProviderOptions{}, + }, + wantErr: false, + }, + { + name: "empty values", + args: args{ + model: &inputModel{ + GlobalFlagModel: globalflags.GlobalFlagModel{}, + }, + options: &ske.ProviderOptions{ + KubernetesVersions: &[]ske.KubernetesVersion{}, + }, + }, + wantErr: false, + }, + { + name: "empty value in values", + args: args{ + model: &inputModel{ + GlobalFlagModel: globalflags.GlobalFlagModel{}, + }, + options: &ske.ProviderOptions{ + KubernetesVersions: &[]ske.KubernetesVersion{{}}, + }, + }, + wantErr: false, + }, + { + name: "valid values", + args: args{ + model: &inputModel{ + GlobalFlagModel: globalflags.GlobalFlagModel{}, + }, + options: &ske.ProviderOptions{ + KubernetesVersions: &[]ske.KubernetesVersion{ + { + FeatureGates: &map[string]string{ + "featureGate1": "foo", + "featureGate2": "bar", + }, + State: utils.Ptr("supported"), + Version: utils.Ptr("0.00.0"), + }, + { + ExpirationDate: utils.Ptr(time.Now()), + State: utils.Ptr("deprecated"), + Version: utils.Ptr("0.00.0"), + }, + }, + }, + }, + wantErr: false, + }, + } + p := print.NewPrinter() + p.Cmd = NewCmd(&types.CmdParams{Printer: p}) + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if err := outputResult(p, tt.args.model, tt.args.options); (err != nil) != tt.wantErr { + t.Errorf("outputResult() error = %v, wantErr %v", err, tt.wantErr) + } + }) + } +} diff --git a/internal/cmd/ske/options/machine_images/machine_images.go b/internal/cmd/ske/options/machine_images/machine_images.go new file mode 100644 index 000000000..19eae47d5 --- /dev/null +++ b/internal/cmd/ske/options/machine_images/machine_images.go @@ -0,0 +1,127 @@ +package machine_images + +import ( + "context" + "fmt" + "strings" + "time" + + "github.com/stackitcloud/stackit-cli/internal/pkg/types" + + "github.com/spf13/cobra" + "github.com/stackitcloud/stackit-cli/internal/pkg/args" + "github.com/stackitcloud/stackit-cli/internal/pkg/examples" + "github.com/stackitcloud/stackit-cli/internal/pkg/globalflags" + "github.com/stackitcloud/stackit-cli/internal/pkg/print" + "github.com/stackitcloud/stackit-cli/internal/pkg/services/ske/client" + "github.com/stackitcloud/stackit-cli/internal/pkg/tables" + "github.com/stackitcloud/stackit-cli/internal/pkg/utils" + "github.com/stackitcloud/stackit-sdk-go/services/ske" +) + +type inputModel struct { + globalflags.GlobalFlagModel +} + +func NewCmd(params *types.CmdParams) *cobra.Command { + cmd := &cobra.Command{ + Use: "machine-images", + Short: "Lists SKE provider options for machine-images", + Long: "Lists STACKIT Kubernetes Engine (SKE) provider options for machine-images.", + Args: args.NoArgs, + Example: examples.Build( + examples.NewExample( + `List SKE options for machine-images`, + "$ stackit ske options machine-images"), + ), + RunE: func(cmd *cobra.Command, args []string) error { + ctx := context.Background() + model, err := parseInput(params.Printer, cmd, args) + if err != nil { + return err + } + + // Configure API client + apiClient, err := client.ConfigureClient(params.Printer, params.CliVersion) + if err != nil { + return err + } + + // Call API + req := buildRequest(ctx, apiClient, model) + resp, err := req.Execute() + if err != nil { + return fmt.Errorf("get SKE provider options: %w", err) + } + + return outputResult(params.Printer, model, resp) + }, + } + return cmd +} + +func parseInput(p *print.Printer, cmd *cobra.Command, _ []string) (*inputModel, error) { + globalFlags := globalflags.Parse(p, cmd) + + model := inputModel{ + GlobalFlagModel: utils.PtrValue(globalFlags), + } + + p.DebugInputModel(model) + return &model, nil +} + +func buildRequest(ctx context.Context, apiClient *ske.APIClient, model *inputModel) ske.ApiListProviderOptionsRequest { + req := apiClient.ListProviderOptions(ctx, model.Region) + return req +} + +func outputResult(p *print.Printer, model *inputModel, options *ske.ProviderOptions) error { + if options == nil { + return fmt.Errorf("options is nil") + } + + options.AvailabilityZones = nil + options.KubernetesVersions = nil + options.MachineTypes = nil + options.VolumeTypes = nil + + return p.OutputResult(model.OutputFormat, options, func() error { + images := utils.PtrValue(options.MachineImages) + + table := tables.NewTable() + table.SetHeader("NAME", "VERSION", "STATE", "EXPIRATION DATE", "SUPPORTED CRI") + for i := range images { + image := images[i] + versions := utils.PtrValue(image.Versions) + for j := range versions { + version := versions[j] + criNames := make([]string, 0) + for i := range utils.PtrValue(version.Cri) { + cri := utils.PtrValue(version.Cri)[i] + criNames = append(criNames, utils.PtrString(cri.Name)) + } + criNamesString := strings.Join(criNames, ", ") + + expirationDate := "-" + if version.ExpirationDate != nil { + expirationDate = version.ExpirationDate.Format(time.RFC3339) + } + table.AddRow( + utils.PtrString(image.Name), + utils.PtrString(version.Version), + utils.PtrString(version.State), + expirationDate, + criNamesString, + ) + } + } + table.EnableAutoMergeOnColumns(1) + + err := table.Display(p) + if err != nil { + return fmt.Errorf("display output: %w", err) + } + return nil + }) +} diff --git a/internal/cmd/ske/options/machine_images/machine_images_test.go b/internal/cmd/ske/options/machine_images/machine_images_test.go new file mode 100644 index 000000000..e1f073b26 --- /dev/null +++ b/internal/cmd/ske/options/machine_images/machine_images_test.go @@ -0,0 +1,216 @@ +package machine_images + +import ( + "context" + "testing" + "time" + + "github.com/stackitcloud/stackit-cli/internal/pkg/types" + "github.com/stackitcloud/stackit-cli/internal/pkg/utils" + + "github.com/stackitcloud/stackit-cli/internal/pkg/globalflags" + "github.com/stackitcloud/stackit-cli/internal/pkg/print" + "github.com/stackitcloud/stackit-cli/internal/pkg/testutils" + + "github.com/google/go-cmp/cmp" + "github.com/google/go-cmp/cmp/cmpopts" + "github.com/stackitcloud/stackit-sdk-go/services/ske" +) + +type testCtxKey struct{} + +var testCtx = context.WithValue(context.Background(), testCtxKey{}, "foo") +var testClient = &ske.APIClient{} + +const testRegion = "eu01" + +func fixtureFlagValues(mods ...func(flagValues map[string]string)) map[string]string { + flagValues := map[string]string{ + globalflags.RegionFlag: testRegion, + } + for _, mod := range mods { + mod(flagValues) + } + return flagValues +} + +func fixtureInputModel(mods ...func(model *inputModel)) *inputModel { + model := &inputModel{ + GlobalFlagModel: globalflags.GlobalFlagModel{ + Region: testRegion, + Verbosity: globalflags.VerbosityDefault, + }, + } + for _, mod := range mods { + mod(model) + } + return model +} + +func TestParseInput(t *testing.T) { + tests := []struct { + description string + argValues []string + flagValues map[string]string + isValid bool + expectedModel *inputModel + }{ + { + description: "base", + flagValues: fixtureFlagValues(), + isValid: true, + expectedModel: fixtureInputModel(), + }, + { + description: "no values", + flagValues: map[string]string{}, + isValid: true, + expectedModel: fixtureInputModel(func(model *inputModel) { + model.Region = "" + }), + }, + } + + for _, tt := range tests { + t.Run(tt.description, func(t *testing.T) { + testutils.TestParseInput(t, NewCmd, parseInput, tt.expectedModel, tt.argValues, tt.flagValues, tt.isValid) + }) + } +} + +func TestBuildRequest(t *testing.T) { + tests := []struct { + description string + inputModel *inputModel + expectedRequest ske.ApiListProviderOptionsRequest + }{ + { + description: "base", + inputModel: fixtureInputModel(), + expectedRequest: testClient.ListProviderOptions(testCtx, testRegion), + }, + } + + for _, tt := range tests { + t.Run(tt.description, func(t *testing.T) { + request := buildRequest(testCtx, testClient, tt.inputModel) + + diff := cmp.Diff(request, tt.expectedRequest, + cmp.AllowUnexported(tt.expectedRequest), + cmpopts.EquateComparable(testCtx), + ) + if diff != "" { + t.Fatalf("Data does not match: %s", diff) + } + }) + } +} + +func TestOutputResult(t *testing.T) { + type args struct { + model *inputModel + options *ske.ProviderOptions + } + tests := []struct { + name string + args args + wantErr bool + }{ + { + name: "empty", + args: args{}, + wantErr: true, + }, + { + name: "missing options", + args: args{ + model: &inputModel{ + GlobalFlagModel: globalflags.GlobalFlagModel{}, + }, + }, + wantErr: true, + }, + { + name: "empty input model", + args: args{ + model: &inputModel{}, + options: &ske.ProviderOptions{}, + }, + wantErr: false, + }, + { + name: "set model and options", + args: args{ + model: &inputModel{ + GlobalFlagModel: globalflags.GlobalFlagModel{}, + }, + options: &ske.ProviderOptions{}, + }, + wantErr: false, + }, + { + name: "empty values", + args: args{ + model: &inputModel{ + GlobalFlagModel: globalflags.GlobalFlagModel{}, + }, + options: &ske.ProviderOptions{ + MachineImages: &[]ske.MachineImage{}, + }, + }, + wantErr: false, + }, + { + name: "empty value in values", + args: args{ + model: &inputModel{ + GlobalFlagModel: globalflags.GlobalFlagModel{}, + }, + options: &ske.ProviderOptions{ + MachineImages: &[]ske.MachineImage{{}}, + }, + }, + wantErr: false, + }, + { + name: "valid values", + args: args{ + model: &inputModel{ + GlobalFlagModel: globalflags.GlobalFlagModel{}, + }, + options: &ske.ProviderOptions{ + MachineImages: &[]ske.MachineImage{ + { + Name: utils.Ptr("image1"), + Versions: &[]ske.MachineImageVersion{ + { + Cri: &[]ske.CRI{ + { + Name: ske.CRINAME_CONTAINERD.Ptr(), + }, + }, + ExpirationDate: utils.Ptr(time.Now()), + State: utils.Ptr("supported"), + Version: utils.Ptr("0.00.0"), + }, + }, + }, + { + Name: utils.Ptr("zone2"), + }, + }, + }, + }, + wantErr: false, + }, + } + p := print.NewPrinter() + p.Cmd = NewCmd(&types.CmdParams{Printer: p}) + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if err := outputResult(p, tt.args.model, tt.args.options); (err != nil) != tt.wantErr { + t.Errorf("outputResult() error = %v, wantErr %v", err, tt.wantErr) + } + }) + } +} diff --git a/internal/cmd/ske/options/machine_types/machine_types.go b/internal/cmd/ske/options/machine_types/machine_types.go new file mode 100644 index 000000000..26014dc59 --- /dev/null +++ b/internal/cmd/ske/options/machine_types/machine_types.go @@ -0,0 +1,107 @@ +package machine_types + +import ( + "context" + "fmt" + + "github.com/stackitcloud/stackit-cli/internal/pkg/types" + + "github.com/spf13/cobra" + "github.com/stackitcloud/stackit-cli/internal/pkg/args" + "github.com/stackitcloud/stackit-cli/internal/pkg/examples" + "github.com/stackitcloud/stackit-cli/internal/pkg/globalflags" + "github.com/stackitcloud/stackit-cli/internal/pkg/print" + "github.com/stackitcloud/stackit-cli/internal/pkg/services/ske/client" + "github.com/stackitcloud/stackit-cli/internal/pkg/tables" + "github.com/stackitcloud/stackit-cli/internal/pkg/utils" + "github.com/stackitcloud/stackit-sdk-go/services/ske" +) + +type inputModel struct { + globalflags.GlobalFlagModel +} + +func NewCmd(params *types.CmdParams) *cobra.Command { + cmd := &cobra.Command{ + Use: "machine-types", + Short: "Lists SKE provider options for machine-types", + Long: "Lists STACKIT Kubernetes Engine (SKE) provider options for machine-types.", + Args: args.NoArgs, + Example: examples.Build( + examples.NewExample( + `List SKE options for machine-types`, + "$ stackit ske options machine-types"), + ), + RunE: func(cmd *cobra.Command, args []string) error { + ctx := context.Background() + model, err := parseInput(params.Printer, cmd, args) + if err != nil { + return err + } + + // Configure API client + apiClient, err := client.ConfigureClient(params.Printer, params.CliVersion) + if err != nil { + return err + } + + // Call API + req := buildRequest(ctx, apiClient, model) + resp, err := req.Execute() + if err != nil { + return fmt.Errorf("get SKE provider options: %w", err) + } + + return outputResult(params.Printer, model, resp) + }, + } + return cmd +} + +func parseInput(p *print.Printer, cmd *cobra.Command, _ []string) (*inputModel, error) { + globalFlags := globalflags.Parse(p, cmd) + + model := inputModel{ + GlobalFlagModel: utils.PtrValue(globalFlags), + } + + p.DebugInputModel(model) + return &model, nil +} + +func buildRequest(ctx context.Context, apiClient *ske.APIClient, model *inputModel) ske.ApiListProviderOptionsRequest { + req := apiClient.ListProviderOptions(ctx, model.Region) + return req +} + +func outputResult(p *print.Printer, model *inputModel, options *ske.ProviderOptions) error { + if options == nil { + return fmt.Errorf("options is nil") + } + + options.AvailabilityZones = nil + options.KubernetesVersions = nil + options.MachineImages = nil + options.VolumeTypes = nil + + return p.OutputResult(model.OutputFormat, options, func() error { + machineTypes := utils.PtrValue(options.MachineTypes) + + table := tables.NewTable() + table.SetHeader("TYPE", "CPU", "MEMORY") + for i := range machineTypes { + t := machineTypes[i] + table.AddRow( + utils.PtrString(t.Name), + utils.PtrString(t.Cpu), + utils.PtrString(t.Memory), + ) + } + + err := table.Display(p) + if err != nil { + return fmt.Errorf("display output: %w", err) + } + return nil + }) +} diff --git a/internal/cmd/ske/options/machine_types/machine_types_test.go b/internal/cmd/ske/options/machine_types/machine_types_test.go new file mode 100644 index 000000000..7db75d847 --- /dev/null +++ b/internal/cmd/ske/options/machine_types/machine_types_test.go @@ -0,0 +1,211 @@ +package machine_types + +import ( + "context" + "testing" + + "github.com/stackitcloud/stackit-cli/internal/pkg/types" + "github.com/stackitcloud/stackit-cli/internal/pkg/utils" + + "github.com/stackitcloud/stackit-cli/internal/pkg/globalflags" + "github.com/stackitcloud/stackit-cli/internal/pkg/print" + "github.com/stackitcloud/stackit-cli/internal/pkg/testutils" + + "github.com/google/go-cmp/cmp" + "github.com/google/go-cmp/cmp/cmpopts" + "github.com/stackitcloud/stackit-sdk-go/services/ske" +) + +type testCtxKey struct{} + +var testCtx = context.WithValue(context.Background(), testCtxKey{}, "foo") +var testClient = &ske.APIClient{} + +const testRegion = "eu01" + +func fixtureFlagValues(mods ...func(flagValues map[string]string)) map[string]string { + flagValues := map[string]string{ + globalflags.RegionFlag: testRegion, + } + for _, mod := range mods { + mod(flagValues) + } + return flagValues +} + +func fixtureInputModel(mods ...func(model *inputModel)) *inputModel { + model := &inputModel{ + GlobalFlagModel: globalflags.GlobalFlagModel{ + Region: testRegion, + Verbosity: globalflags.VerbosityDefault, + }, + } + for _, mod := range mods { + mod(model) + } + return model +} + +func TestParseInput(t *testing.T) { + tests := []struct { + description string + argValues []string + flagValues map[string]string + isValid bool + expectedModel *inputModel + }{ + { + description: "base", + flagValues: fixtureFlagValues(), + isValid: true, + expectedModel: fixtureInputModel(), + }, + { + description: "no values", + flagValues: map[string]string{}, + isValid: true, + expectedModel: fixtureInputModel(func(model *inputModel) { + model.Region = "" + }), + }, + } + + for _, tt := range tests { + t.Run(tt.description, func(t *testing.T) { + testutils.TestParseInput(t, NewCmd, parseInput, tt.expectedModel, tt.argValues, tt.flagValues, tt.isValid) + }) + } +} + +func TestBuildRequest(t *testing.T) { + tests := []struct { + description string + inputModel *inputModel + expectedRequest ske.ApiListProviderOptionsRequest + }{ + { + description: "base", + inputModel: fixtureInputModel(), + expectedRequest: testClient.ListProviderOptions(testCtx, testRegion), + }, + } + + for _, tt := range tests { + t.Run(tt.description, func(t *testing.T) { + request := buildRequest(testCtx, testClient, tt.inputModel) + + diff := cmp.Diff(request, tt.expectedRequest, + cmp.AllowUnexported(tt.expectedRequest), + cmpopts.EquateComparable(testCtx), + ) + if diff != "" { + t.Fatalf("Data does not match: %s", diff) + } + }) + } +} + +func TestOutputResult(t *testing.T) { + type args struct { + model *inputModel + options *ske.ProviderOptions + } + tests := []struct { + name string + args args + wantErr bool + }{ + { + name: "empty", + args: args{}, + wantErr: true, + }, + { + name: "missing options", + args: args{ + model: &inputModel{ + GlobalFlagModel: globalflags.GlobalFlagModel{}, + }, + }, + wantErr: true, + }, + { + name: "empty input model", + args: args{ + model: &inputModel{}, + options: &ske.ProviderOptions{}, + }, + wantErr: false, + }, + { + name: "set model and options", + args: args{ + model: &inputModel{ + GlobalFlagModel: globalflags.GlobalFlagModel{}, + }, + options: &ske.ProviderOptions{}, + }, + wantErr: false, + }, + { + name: "empty values", + args: args{ + model: &inputModel{ + GlobalFlagModel: globalflags.GlobalFlagModel{}, + }, + options: &ske.ProviderOptions{ + MachineTypes: &[]ske.MachineType{}, + }, + }, + wantErr: false, + }, + { + name: "empty value in values", + args: args{ + model: &inputModel{ + GlobalFlagModel: globalflags.GlobalFlagModel{}, + }, + options: &ske.ProviderOptions{ + MachineTypes: &[]ske.MachineType{{}}, + }, + }, + wantErr: false, + }, + { + name: "valid values", + args: args{ + model: &inputModel{ + GlobalFlagModel: globalflags.GlobalFlagModel{}, + }, + options: &ske.ProviderOptions{ + MachineTypes: &[]ske.MachineType{ + { + Architecture: utils.Ptr("amd64"), + Cpu: utils.Ptr(int64(2)), + Gpu: utils.Ptr(int64(0)), + Memory: utils.Ptr(int64(16)), + Name: utils.Ptr("type1"), + }, + { + Architecture: utils.Ptr("amd64"), + Cpu: utils.Ptr(int64(2)), + Gpu: utils.Ptr(int64(0)), + Memory: utils.Ptr(int64(16)), + Name: utils.Ptr("type2"), + }, + }, + }, + }, + wantErr: false, + }, + } + p := print.NewPrinter() + p.Cmd = NewCmd(&types.CmdParams{Printer: p}) + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if err := outputResult(p, tt.args.model, tt.args.options); (err != nil) != tt.wantErr { + t.Errorf("outputResult() error = %v, wantErr %v", err, tt.wantErr) + } + }) + } +} diff --git a/internal/cmd/ske/options/options.go b/internal/cmd/ske/options/options.go index 21f04d028..7038cee7f 100644 --- a/internal/cmd/ske/options/options.go +++ b/internal/cmd/ske/options/options.go @@ -7,11 +7,15 @@ import ( "strings" "time" + "github.com/stackitcloud/stackit-cli/internal/cmd/ske/options/availability_zones" + "github.com/stackitcloud/stackit-cli/internal/cmd/ske/options/kubernetes_versions" + "github.com/stackitcloud/stackit-cli/internal/cmd/ske/options/machine_images" + "github.com/stackitcloud/stackit-cli/internal/cmd/ske/options/machine_types" + "github.com/stackitcloud/stackit-cli/internal/cmd/ske/options/volume_types" "github.com/stackitcloud/stackit-cli/internal/pkg/types" "github.com/spf13/cobra" "github.com/stackitcloud/stackit-cli/internal/pkg/args" - "github.com/stackitcloud/stackit-cli/internal/pkg/examples" "github.com/stackitcloud/stackit-cli/internal/pkg/flags" "github.com/stackitcloud/stackit-cli/internal/pkg/globalflags" "github.com/stackitcloud/stackit-cli/internal/pkg/print" @@ -30,7 +34,7 @@ const ( ) type inputModel struct { - *globalflags.GlobalFlagModel + globalflags.GlobalFlagModel AvailabilityZones bool KubernetesVersions bool MachineImages bool @@ -42,23 +46,15 @@ func NewCmd(params *types.CmdParams) *cobra.Command { cmd := &cobra.Command{ Use: "options", Short: "Lists SKE provider options", - Long: fmt.Sprintf("%s\n%s", + Long: fmt.Sprintf("%s\n%s\n%s", + "Command \"options\" is deprecated, use the subcommands instead.", "Lists STACKIT Kubernetes Engine (SKE) provider options (availability zones, Kubernetes versions, machine images and types, volume types).", "Pass one or more flags to filter what categories are shown.", ), Args: args.NoArgs, - Example: examples.Build( - examples.NewExample( - `List SKE options for all categories`, - "$ stackit ske options"), - examples.NewExample( - `List SKE options regarding Kubernetes versions only`, - "$ stackit ske options --kubernetes-versions"), - examples.NewExample( - `List SKE options regarding Kubernetes versions and machine images`, - "$ stackit ske options --kubernetes-versions --machine-images"), - ), RunE: func(cmd *cobra.Command, args []string) error { + params.Printer.Info("Command \"options\" is deprecated, use the subcommands instead.\n") + ctx := context.Background() model, err := parseInput(params.Printer, cmd, args) if err != nil { @@ -82,15 +78,30 @@ func NewCmd(params *types.CmdParams) *cobra.Command { }, } configureFlags(cmd) + addSubcommands(cmd, params) return cmd } +func addSubcommands(cmd *cobra.Command, params *types.CmdParams) { + cmd.AddCommand(availability_zones.NewCmd(params)) + cmd.AddCommand(kubernetes_versions.NewCmd(params)) + cmd.AddCommand(machine_images.NewCmd(params)) + cmd.AddCommand(machine_types.NewCmd(params)) + cmd.AddCommand(volume_types.NewCmd(params)) +} + func configureFlags(cmd *cobra.Command) { cmd.Flags().Bool(availabilityZonesFlag, false, "Lists availability zones") cmd.Flags().Bool(kubernetesVersionsFlag, false, "Lists supported kubernetes versions") cmd.Flags().Bool(machineImagesFlag, false, "Lists supported machine images") cmd.Flags().Bool(machineTypesFlag, false, "Lists supported machine types") cmd.Flags().Bool(volumeTypesFlag, false, "Lists supported volume types") + + cobra.CheckErr(cmd.Flags().MarkDeprecated(availabilityZonesFlag, "This flag is deprecated and will be removed on 2026-09-26. Use the availability-zone subcommand instead.")) + cobra.CheckErr(cmd.Flags().MarkDeprecated(kubernetesVersionsFlag, "This flag is deprecated and will be removed on 2026-09-26. Use the kubernetes-versions subcommand instead.")) + cobra.CheckErr(cmd.Flags().MarkDeprecated(machineImagesFlag, "This flag is deprecated and will be removed on 2026-09-26. Use the machine-images subcommand instead.")) + cobra.CheckErr(cmd.Flags().MarkDeprecated(machineTypesFlag, "This flag is deprecated and will be removed on 2026-09-26. Use the machine-types subcommand instead.")) + cobra.CheckErr(cmd.Flags().MarkDeprecated(volumeTypesFlag, "This flag is deprecated and will be removed on 2026-09-26. Use the volume-types subcommand instead.")) } func parseInput(p *print.Printer, cmd *cobra.Command, _ []string) (*inputModel, error) { @@ -111,7 +122,7 @@ func parseInput(p *print.Printer, cmd *cobra.Command, _ []string) (*inputModel, } model := inputModel{ - GlobalFlagModel: globalFlags, + GlobalFlagModel: utils.PtrValue(globalFlags), AvailabilityZones: availabilityZones, KubernetesVersions: kubernetesVersions, MachineImages: machineImages, @@ -129,7 +140,7 @@ func buildRequest(ctx context.Context, apiClient *ske.APIClient, model *inputMod } func outputResult(p *print.Printer, model *inputModel, options *ske.ProviderOptions) error { - if model == nil || model.GlobalFlagModel == nil { + if model == nil { return fmt.Errorf("model is nil") } else if options == nil { return fmt.Errorf("options is nil") diff --git a/internal/cmd/ske/options/options_test.go b/internal/cmd/ske/options/options_test.go index 43f58c5b4..2fe568f2a 100644 --- a/internal/cmd/ske/options/options_test.go +++ b/internal/cmd/ske/options/options_test.go @@ -39,7 +39,7 @@ func fixtureFlagValues(mods ...func(flagValues map[string]string)) map[string]st func fixtureInputModelAllFalse(mods ...func(model *inputModel)) *inputModel { model := &inputModel{ - GlobalFlagModel: &globalflags.GlobalFlagModel{Region: testRegion, Verbosity: globalflags.VerbosityDefault}, + GlobalFlagModel: globalflags.GlobalFlagModel{Region: testRegion, Verbosity: globalflags.VerbosityDefault}, AvailabilityZones: false, KubernetesVersions: false, MachineImages: false, @@ -54,7 +54,7 @@ func fixtureInputModelAllFalse(mods ...func(model *inputModel)) *inputModel { func fixtureInputModelAllTrue(mods ...func(model *inputModel)) *inputModel { model := &inputModel{ - GlobalFlagModel: &globalflags.GlobalFlagModel{Region: testRegion, Verbosity: globalflags.VerbosityDefault}, + GlobalFlagModel: globalflags.GlobalFlagModel{Region: testRegion, Verbosity: globalflags.VerbosityDefault}, AvailabilityZones: true, KubernetesVersions: true, MachineImages: true, @@ -187,24 +187,24 @@ func TestOutputResult(t *testing.T) { name: "missing options", args: args{ model: &inputModel{ - GlobalFlagModel: &globalflags.GlobalFlagModel{}, + GlobalFlagModel: globalflags.GlobalFlagModel{}, }, }, wantErr: true, }, { - name: "missing global flags in model", + name: "empty input model", args: args{ model: &inputModel{}, options: &ske.ProviderOptions{}, }, - wantErr: true, + wantErr: false, }, { name: "set model and options", args: args{ model: &inputModel{ - GlobalFlagModel: &globalflags.GlobalFlagModel{}, + GlobalFlagModel: globalflags.GlobalFlagModel{}, }, options: &ske.ProviderOptions{}, }, diff --git a/internal/cmd/ske/options/volume_types/volume_types.go b/internal/cmd/ske/options/volume_types/volume_types.go new file mode 100644 index 000000000..8d3983487 --- /dev/null +++ b/internal/cmd/ske/options/volume_types/volume_types.go @@ -0,0 +1,103 @@ +package volume_types + +import ( + "context" + "fmt" + + "github.com/stackitcloud/stackit-cli/internal/pkg/types" + "github.com/stackitcloud/stackit-cli/internal/pkg/utils" + + "github.com/spf13/cobra" + "github.com/stackitcloud/stackit-cli/internal/pkg/args" + "github.com/stackitcloud/stackit-cli/internal/pkg/examples" + "github.com/stackitcloud/stackit-cli/internal/pkg/globalflags" + "github.com/stackitcloud/stackit-cli/internal/pkg/print" + "github.com/stackitcloud/stackit-cli/internal/pkg/services/ske/client" + "github.com/stackitcloud/stackit-cli/internal/pkg/tables" + "github.com/stackitcloud/stackit-sdk-go/services/ske" +) + +type inputModel struct { + globalflags.GlobalFlagModel +} + +func NewCmd(params *types.CmdParams) *cobra.Command { + cmd := &cobra.Command{ + Use: "volume-types", + Short: "Lists SKE provider options for volume-types", + Long: "Lists STACKIT Kubernetes Engine (SKE) provider options for volume-types.", + Args: args.NoArgs, + Example: examples.Build( + examples.NewExample( + `List SKE options for volume-types`, + "$ stackit ske options volume-types"), + ), + RunE: func(cmd *cobra.Command, args []string) error { + ctx := context.Background() + model, err := parseInput(params.Printer, cmd, args) + if err != nil { + return err + } + + // Configure API client + apiClient, err := client.ConfigureClient(params.Printer, params.CliVersion) + if err != nil { + return err + } + + // Call API + req := buildRequest(ctx, apiClient, model) + resp, err := req.Execute() + if err != nil { + return fmt.Errorf("get SKE provider options: %w", err) + } + + return outputResult(params.Printer, model, resp) + }, + } + return cmd +} + +func parseInput(p *print.Printer, cmd *cobra.Command, _ []string) (*inputModel, error) { + globalFlags := globalflags.Parse(p, cmd) + + model := inputModel{ + GlobalFlagModel: utils.PtrValue(globalFlags), + } + + p.DebugInputModel(model) + return &model, nil +} + +func buildRequest(ctx context.Context, apiClient *ske.APIClient, model *inputModel) ske.ApiListProviderOptionsRequest { + req := apiClient.ListProviderOptions(ctx, model.Region) + return req +} + +func outputResult(p *print.Printer, model *inputModel, options *ske.ProviderOptions) error { + if options == nil { + return fmt.Errorf("options is nil") + } + + options.AvailabilityZones = nil + options.KubernetesVersions = nil + options.MachineImages = nil + options.MachineTypes = nil + + return p.OutputResult(model.OutputFormat, options, func() error { + volumeTypes := utils.PtrValue(options.VolumeTypes) + + table := tables.NewTable() + table.SetHeader("TYPE") + for i := range volumeTypes { + z := volumeTypes[i] + table.AddRow(utils.PtrString(z.Name)) + } + + err := table.Display(p) + if err != nil { + return fmt.Errorf("display output: %w", err) + } + return nil + }) +} diff --git a/internal/cmd/ske/options/volume_types/volume_types_test.go b/internal/cmd/ske/options/volume_types/volume_types_test.go new file mode 100644 index 000000000..ce37ce498 --- /dev/null +++ b/internal/cmd/ske/options/volume_types/volume_types_test.go @@ -0,0 +1,203 @@ +package volume_types + +import ( + "context" + "testing" + + "github.com/stackitcloud/stackit-cli/internal/pkg/types" + "github.com/stackitcloud/stackit-cli/internal/pkg/utils" + + "github.com/stackitcloud/stackit-cli/internal/pkg/globalflags" + "github.com/stackitcloud/stackit-cli/internal/pkg/print" + "github.com/stackitcloud/stackit-cli/internal/pkg/testutils" + + "github.com/google/go-cmp/cmp" + "github.com/google/go-cmp/cmp/cmpopts" + "github.com/stackitcloud/stackit-sdk-go/services/ske" +) + +type testCtxKey struct{} + +var testCtx = context.WithValue(context.Background(), testCtxKey{}, "foo") +var testClient = &ske.APIClient{} + +const testRegion = "eu01" + +func fixtureFlagValues(mods ...func(flagValues map[string]string)) map[string]string { + flagValues := map[string]string{ + globalflags.RegionFlag: testRegion, + } + for _, mod := range mods { + mod(flagValues) + } + return flagValues +} + +func fixtureInputModel(mods ...func(model *inputModel)) *inputModel { + model := &inputModel{ + GlobalFlagModel: globalflags.GlobalFlagModel{ + Region: testRegion, + Verbosity: globalflags.VerbosityDefault, + }, + } + for _, mod := range mods { + mod(model) + } + return model +} + +func TestParseInput(t *testing.T) { + tests := []struct { + description string + argValues []string + flagValues map[string]string + isValid bool + expectedModel *inputModel + }{ + { + description: "base", + flagValues: fixtureFlagValues(), + isValid: true, + expectedModel: fixtureInputModel(), + }, + { + description: "no values", + flagValues: map[string]string{}, + isValid: true, + expectedModel: fixtureInputModel(func(model *inputModel) { + model.Region = "" + }), + }, + } + + for _, tt := range tests { + t.Run(tt.description, func(t *testing.T) { + testutils.TestParseInput(t, NewCmd, parseInput, tt.expectedModel, tt.argValues, tt.flagValues, tt.isValid) + }) + } +} + +func TestBuildRequest(t *testing.T) { + tests := []struct { + description string + inputModel *inputModel + expectedRequest ske.ApiListProviderOptionsRequest + }{ + { + description: "base", + inputModel: fixtureInputModel(), + expectedRequest: testClient.ListProviderOptions(testCtx, testRegion), + }, + } + + for _, tt := range tests { + t.Run(tt.description, func(t *testing.T) { + request := buildRequest(testCtx, testClient, tt.inputModel) + + diff := cmp.Diff(request, tt.expectedRequest, + cmp.AllowUnexported(tt.expectedRequest), + cmpopts.EquateComparable(testCtx), + ) + if diff != "" { + t.Fatalf("Data does not match: %s", diff) + } + }) + } +} + +func TestOutputResult(t *testing.T) { + type args struct { + model *inputModel + options *ske.ProviderOptions + } + tests := []struct { + name string + args args + wantErr bool + }{ + { + name: "empty", + args: args{}, + wantErr: true, + }, + { + name: "missing options", + args: args{ + model: &inputModel{ + GlobalFlagModel: globalflags.GlobalFlagModel{}, + }, + }, + wantErr: true, + }, + { + name: "empty input model", + args: args{ + model: &inputModel{}, + options: &ske.ProviderOptions{}, + }, + wantErr: false, + }, + { + name: "set model and options", + args: args{ + model: &inputModel{ + GlobalFlagModel: globalflags.GlobalFlagModel{}, + }, + options: &ske.ProviderOptions{}, + }, + wantErr: false, + }, + { + name: "empty values", + args: args{ + model: &inputModel{ + GlobalFlagModel: globalflags.GlobalFlagModel{}, + }, + options: &ske.ProviderOptions{ + VolumeTypes: &[]ske.VolumeType{}, + }, + }, + wantErr: false, + }, + { + name: "empty value in values", + args: args{ + model: &inputModel{ + GlobalFlagModel: globalflags.GlobalFlagModel{}, + }, + options: &ske.ProviderOptions{ + VolumeTypes: &[]ske.VolumeType{{}}, + }, + }, + wantErr: false, + }, + { + name: "valid values", + args: args{ + model: &inputModel{ + GlobalFlagModel: globalflags.GlobalFlagModel{}, + }, + options: &ske.ProviderOptions{ + VolumeTypes: &[]ske.VolumeType{ + { + Name: utils.Ptr("type1"), + }, + { + Name: utils.Ptr("type2"), + }, + }, + }, + }, + wantErr: false, + }, + } + p := print.NewPrinter() + p.Cmd = NewCmd(&types.CmdParams{Printer: p}) + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if err := outputResult(p, tt.args.model, tt.args.options); (err != nil) != tt.wantErr { + t.Errorf("outputResult() error = %v, wantErr %v", err, tt.wantErr) + } + }) + } +}