From b13651ee6efb19b2134d027a1e70124d88245a99 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20Palet?= Date: Thu, 9 May 2024 17:01:02 +0100 Subject: [PATCH 01/18] Implement `GetProfile` (#309) * Extract file utils and implement readFileIfExists * Write unit tests for file utils * Implement GetProfile * Fix test * Fix typo * Remove unnecesary condition * Fix lint * Adjustments after review --- internal/pkg/config/config.go | 32 +++---- internal/pkg/config/file_utils.go | 40 +++++++++ internal/pkg/config/file_utils_test.go | 89 +++++++++++++++++++ internal/pkg/config/profiles.go | 51 +++++++++++ internal/pkg/config/profiles_test.go | 59 ++++++++++++ internal/pkg/config/test-data/empty-file.txt | 0 .../config/test-data/file-with-content.txt | 1 + .../config/test-data/folder-exists/dummy.txt | 0 8 files changed, 250 insertions(+), 22 deletions(-) create mode 100644 internal/pkg/config/file_utils.go create mode 100644 internal/pkg/config/file_utils_test.go create mode 100644 internal/pkg/config/profiles.go create mode 100644 internal/pkg/config/profiles_test.go create mode 100644 internal/pkg/config/test-data/empty-file.txt create mode 100644 internal/pkg/config/test-data/file-with-content.txt create mode 100644 internal/pkg/config/test-data/folder-exists/dummy.txt diff --git a/internal/pkg/config/config.go b/internal/pkg/config/config.go index fa367857d..198f49f16 100644 --- a/internal/pkg/config/config.go +++ b/internal/pkg/config/config.go @@ -34,16 +34,20 @@ const ( ServiceAccountCustomEndpointKey = "service_account_custom_endpoint" SKECustomEndpointKey = "ske_custom_endpoint" + ProjectNameKey = "project_name" + AsyncDefault = false SessionTimeLimitDefault = "2h" ) -// Backend config keys const ( - configFolder = "stackit" + configFolder = "stackit" + configFileName = "cli-config" configFileExtension = "json" - ProjectNameKey = "project_name" + + profileFileName = "cli-profile" + profileFileExtension = "txt" ) var ConfigKeys = []string{ @@ -72,17 +76,14 @@ var ConfigKeys = []string{ SKECustomEndpointKey, } -var folderPath string +var configFolderPath string func InitConfig() { configDir, err := os.UserConfigDir() cobra.CheckErr(err) - configFolderPath := filepath.Join(configDir, configFolder) + configFolderPath = filepath.Join(configDir, configFolder) configFilePath := filepath.Join(configFolderPath, fmt.Sprintf("%s.%s", configFileName, configFileExtension)) - // Write config dir path to global variable - folderPath = configFolderPath - // This hack is required to allow creating the config file with `viper.WriteConfig` // see https://github.com/spf13/viper/issues/851#issuecomment-789393451 viper.SetConfigFile(configFilePath) @@ -107,22 +108,9 @@ func InitConfig() { viper.SetEnvPrefix("stackit") } -func createFolderIfNotExists() error { - _, err := os.Stat(folderPath) - if os.IsNotExist(err) { - err := os.MkdirAll(folderPath, os.ModePerm) - if err != nil { - return err - } - } else if err != nil { - return err - } - return nil -} - // Write saves the config file (wrapping `viper.WriteConfig`) and ensures that its directory exists func Write() error { - if err := createFolderIfNotExists(); err != nil { + if err := createFolderIfNotExists(configFolderPath); err != nil { return fmt.Errorf("create config directory: %w", err) } return viper.WriteConfig() diff --git a/internal/pkg/config/file_utils.go b/internal/pkg/config/file_utils.go new file mode 100644 index 000000000..44b0ab38a --- /dev/null +++ b/internal/pkg/config/file_utils.go @@ -0,0 +1,40 @@ +package config + +import ( + "fmt" + "os" +) + +// createFolderIfNotExists creates a folder if it does not exist. +func createFolderIfNotExists(folderPath string) error { + _, err := os.Stat(folderPath) + if os.IsNotExist(err) { + err := os.MkdirAll(folderPath, os.ModePerm) + if err != nil { + return err + } + } else if err != nil { + return err + } + return nil +} + +// readFileIfExists reads the contents of a file and returns it as a string, along with a boolean indicating if the file exists. +// If the file does not exist, it returns an empty string and no error. +// If the file exists but cannot be read, it returns an error. +func readFileIfExists(filePath string) (contents string, exists bool, err error) { + _, err = os.Stat(filePath) + if err != nil { + if os.IsNotExist(err) { + return "", false, nil + } + return "", true, err + } + + content, err := os.ReadFile(filePath) + if err != nil { + return "", true, fmt.Errorf("read file: %w", err) + } + + return string(content), true, nil +} diff --git a/internal/pkg/config/file_utils_test.go b/internal/pkg/config/file_utils_test.go new file mode 100644 index 000000000..b3453c661 --- /dev/null +++ b/internal/pkg/config/file_utils_test.go @@ -0,0 +1,89 @@ +package config + +import ( + "os" + "testing" +) + +func TestCreateFolderIfNotExists(t *testing.T) { + tests := []struct { + description string + folderPath string + needsCleanUp bool + }{ + { + description: "folder exists", + folderPath: "test-data/folder-exists", + }, + { + description: "folder does not exist", + folderPath: "test-data/folder-does-not-exist", + needsCleanUp: true, + }, + } + + for _, tt := range tests { + t.Run(tt.description, func(t *testing.T) { + err := createFolderIfNotExists(tt.folderPath) + if err != nil { + t.Errorf("create folder: %v", err) + } + + // Check if the folder was created + _, err = os.Stat(tt.folderPath) + if os.IsNotExist(err) { + t.Errorf("expected folder to exist but it does not") + } + + // Clean up + if tt.needsCleanUp { + err = os.RemoveAll(tt.folderPath) + if err != nil { + t.Errorf("remove folder: %v", err) + } + } + }) + } +} + +func TestReadFileIfExists(t *testing.T) { + tests := []struct { + description string + filePath string + exists bool + content string + }{ + { + description: "file exists", + filePath: "test-data/file-with-content.txt", + exists: true, + content: "my-content", + }, + { + description: "file does not exist", + filePath: "test-data/file-does-not-exist.txt", + content: "", + }, + { + description: "empty file", + filePath: "test-data/empty-file.txt", + exists: true, + content: "", + }, + } + + for _, tt := range tests { + t.Run(tt.description, func(t *testing.T) { + content, exists, err := readFileIfExists(tt.filePath) + if err != nil { + t.Errorf("read file: %v", err) + } + if exists != tt.exists { + t.Errorf("expected exists to be %t but got %t", tt.exists, exists) + } + if content != tt.content { + t.Errorf("expected content to be %q but got %q", tt.content, content) + } + }) + } +} diff --git a/internal/pkg/config/profiles.go b/internal/pkg/config/profiles.go new file mode 100644 index 000000000..186af0b02 --- /dev/null +++ b/internal/pkg/config/profiles.go @@ -0,0 +1,51 @@ +package config + +import ( + "fmt" + "os" + "path/filepath" + "regexp" +) + +// GetProfile returns the current profile to be used by the CLI. +// +// The profile is determined by the value of the STACKIT_CLI_PROFILE environment variable, or, if not set, +// by the contents of the profile file in the CLI config folder. +// +// If the environment variable is not set and the profile file does not exist, it returns an empty string. +// +// If the profile is not valid, it returns an error. +func GetProfile() (string, error) { + profile, profileSet := os.LookupEnv("STACKIT_CLI_PROFILE") + if !profileSet { + profileFilePath := filepath.Join(configFolderPath, fmt.Sprintf("%s.%s", profileFileName, profileFileExtension)) + contents, exists, err := readFileIfExists(profileFilePath) + if err != nil { + return "", fmt.Errorf("read profile from file: %w", err) + } + if !exists { + return "", nil + } + profile = contents + } + + err := validateProfile(profile) + if err != nil { + return "", fmt.Errorf("validate profile from env var: %w", err) + } + return profile, nil +} + +// validateProfile validates the profile name. +// It can only use letters, numbers, or "-" and cannot be empty. +// If the profile is invalid, it returns an error. +func validateProfile(profile string) error { + match, err := regexp.MatchString("^[a-zA-Z0-9-]+$", profile) + if err != nil { + return fmt.Errorf("match string regex: %w", err) + } + if !match { + return fmt.Errorf("profile name can only contain letters, numbers, and \"-\" and cannot be empty") + } + return nil +} diff --git a/internal/pkg/config/profiles_test.go b/internal/pkg/config/profiles_test.go new file mode 100644 index 000000000..8e96a6e88 --- /dev/null +++ b/internal/pkg/config/profiles_test.go @@ -0,0 +1,59 @@ +package config + +import "testing" + +func TestValidateProfile(t *testing.T) { + tests := []struct { + description string + profile string + isValid bool + }{ + { + description: "valid profile with letters", + profile: "myprofile", + isValid: true, + }, + { + description: "valid profile with uppercase letters", + profile: "myProfile", + isValid: true, + }, + { + description: "valid with letters and hyphen", + profile: "my-profile", + isValid: true, + }, + { + description: "valid with letters, numbers, and hyphen", + profile: "my-profile-123", + isValid: true, + }, + { + description: "invalid empty", + profile: "", + isValid: false, + }, + { + description: "invalid with special characters", + profile: "my_profile", + isValid: false, + }, + { + description: "invalid with spaces", + profile: "my profile", + isValid: false, + }, + } + + for _, tt := range tests { + t.Run(tt.description, func(t *testing.T) { + err := validateProfile(tt.profile) + if tt.isValid && err != nil { + t.Errorf("expected profile to be valid but got error: %v", err) + } + if !tt.isValid && err == nil { + t.Errorf("expected profile to be invalid but got no error") + } + }) + } +} diff --git a/internal/pkg/config/test-data/empty-file.txt b/internal/pkg/config/test-data/empty-file.txt new file mode 100644 index 000000000..e69de29bb diff --git a/internal/pkg/config/test-data/file-with-content.txt b/internal/pkg/config/test-data/file-with-content.txt new file mode 100644 index 000000000..d89fbd214 --- /dev/null +++ b/internal/pkg/config/test-data/file-with-content.txt @@ -0,0 +1 @@ +my-content \ No newline at end of file diff --git a/internal/pkg/config/test-data/folder-exists/dummy.txt b/internal/pkg/config/test-data/folder-exists/dummy.txt new file mode 100644 index 000000000..e69de29bb From 1856fd019a3a4e3fbb6e72babe48c225cafcf549 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20Palet?= Date: Thu, 9 May 2024 17:27:59 +0100 Subject: [PATCH 02/18] Choose config folder based on profile in InitConfig (#310) --- internal/pkg/config/config.go | 8 ++++++++ internal/pkg/config/profiles.go | 2 +- 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/internal/pkg/config/config.go b/internal/pkg/config/config.go index 198f49f16..8070bebc5 100644 --- a/internal/pkg/config/config.go +++ b/internal/pkg/config/config.go @@ -81,7 +81,15 @@ var configFolderPath string func InitConfig() { configDir, err := os.UserConfigDir() cobra.CheckErr(err) + + configProfile, err := GetProfile() + cobra.CheckErr(err) + configFolderPath = filepath.Join(configDir, configFolder) + if configProfile != "" { + configFolderPath = filepath.Join(configFolderPath, configProfile) + } + configFilePath := filepath.Join(configFolderPath, fmt.Sprintf("%s.%s", configFileName, configFileExtension)) // This hack is required to allow creating the config file with `viper.WriteConfig` diff --git a/internal/pkg/config/profiles.go b/internal/pkg/config/profiles.go index 186af0b02..5f21099d9 100644 --- a/internal/pkg/config/profiles.go +++ b/internal/pkg/config/profiles.go @@ -31,7 +31,7 @@ func GetProfile() (string, error) { err := validateProfile(profile) if err != nil { - return "", fmt.Errorf("validate profile from env var: %w", err) + return "", fmt.Errorf("validate profile: %w", err) } return profile, nil } From cebf8dc632e6bfdf9fd2ea30343f381d16310723 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20Palet?= Date: Fri, 10 May 2024 15:43:06 +0100 Subject: [PATCH 03/18] Implement `stackit config profile` commands (#312) * Implement config profile set * Implement config profile unset * Extend config list to show active profile * Adjustments after review --- internal/cmd/config/config.go | 14 ++- internal/cmd/config/list/list.go | 17 ++- internal/cmd/config/profile/profile.go | 35 ++++++ internal/cmd/config/profile/set/set.go | 83 ++++++++++++ internal/cmd/config/profile/set/set_test.go | 132 ++++++++++++++++++++ internal/cmd/config/profile/unset/unset.go | 39 ++++++ internal/cmd/root.go | 18 ++- internal/pkg/config/config.go | 7 +- internal/pkg/config/profiles.go | 38 +++++- internal/pkg/config/profiles_test.go | 2 +- internal/pkg/errors/errors.go | 12 ++ main.go | 2 +- 12 files changed, 379 insertions(+), 20 deletions(-) create mode 100644 internal/cmd/config/profile/profile.go create mode 100644 internal/cmd/config/profile/set/set.go create mode 100644 internal/cmd/config/profile/set/set_test.go create mode 100644 internal/cmd/config/profile/unset/unset.go diff --git a/internal/cmd/config/config.go b/internal/cmd/config/config.go index 7c3fea56f..a96355a54 100644 --- a/internal/cmd/config/config.go +++ b/internal/cmd/config/config.go @@ -4,6 +4,7 @@ import ( "fmt" "github.com/stackitcloud/stackit-cli/internal/cmd/config/list" + "github.com/stackitcloud/stackit-cli/internal/cmd/config/profile" "github.com/stackitcloud/stackit-cli/internal/cmd/config/set" "github.com/stackitcloud/stackit-cli/internal/cmd/config/unset" "github.com/stackitcloud/stackit-cli/internal/pkg/args" @@ -17,12 +18,12 @@ func NewCmd(p *print.Printer) *cobra.Command { cmd := &cobra.Command{ Use: "config", Short: "Provides functionality for CLI configuration options", - Long: fmt.Sprintf("%s\n%s\n%s\n%s\n%s\n%s", "Provides functionality for CLI configuration options", - "The configuration is stored in a file in the user's config directory, which is OS dependent.", - "Windows: %APPDATA%\\stackit", - "Linux: $XDG_CONFIG_HOME/stackit", - "macOS: $HOME/Library/Application Support/stackit", - "The configuration file is named `cli-config.json` and is created automatically in your first CLI run.", + Long: fmt.Sprintf("%s\n%s\n\n%s\n%s\n%s", + "Provides functionality for CLI configuration options.", + `You can set and unset different configuration options via the "stackit config set" and "stackit config unset" commands.`, + "Additionally, you can configure the CLI to use different profiles, each with its own configuration.", + `Additional profiles can be configured via the "STACKIT_CLI_PROFILE" environment variable or using the "stackit config profile set PROFILE" and "stackit config profile unset" commands.`, + "The environment variable takes precedence over what is set via the commands.", ), Args: args.NoArgs, Run: utils.CmdHelp, @@ -35,4 +36,5 @@ func addSubcommands(cmd *cobra.Command, p *print.Printer) { cmd.AddCommand(list.NewCmd(p)) cmd.AddCommand(set.NewCmd(p)) cmd.AddCommand(unset.NewCmd(p)) + cmd.AddCommand(profile.NewCmd(p)) } diff --git a/internal/cmd/config/list/list.go b/internal/cmd/config/list/list.go index 3da8eb0a1..8b890aa4a 100644 --- a/internal/cmd/config/list/list.go +++ b/internal/cmd/config/list/list.go @@ -50,7 +50,13 @@ func NewCmd(p *print.Printer) *cobra.Command { configData := viper.AllSettings() model := parseInput(p, cmd) - return outputResult(p, model.OutputFormat, configData) + + activeProfile, err := config.GetProfile() + if err != nil { + return fmt.Errorf("get profile: %w", err) + } + + return outputResult(p, model.OutputFormat, configData, activeProfile) }, } return cmd @@ -64,9 +70,12 @@ func parseInput(p *print.Printer, cmd *cobra.Command) *inputModel { } } -func outputResult(p *print.Printer, outputFormat string, configData map[string]any) error { +func outputResult(p *print.Printer, outputFormat string, configData map[string]any, activeProfile string) error { switch outputFormat { case print.JSONOutputFormat: + if activeProfile != "" { + configData["active_profile"] = activeProfile + } details, err := json.MarshalIndent(configData, "", " ") if err != nil { return fmt.Errorf("marshal config list: %w", err) @@ -74,6 +83,10 @@ func outputResult(p *print.Printer, outputFormat string, configData map[string]a p.Outputln(string(details)) return nil default: + if activeProfile != "" { + p.Outputf("\n ACTIVE PROFILE: %s\n", activeProfile) + } + // Sort the config options by key configKeys := make([]string, 0, len(configData)) for k := range configData { diff --git a/internal/cmd/config/profile/profile.go b/internal/cmd/config/profile/profile.go new file mode 100644 index 000000000..42bea8e58 --- /dev/null +++ b/internal/cmd/config/profile/profile.go @@ -0,0 +1,35 @@ +package profile + +import ( + "fmt" + + "github.com/stackitcloud/stackit-cli/internal/cmd/config/profile/set" + "github.com/stackitcloud/stackit-cli/internal/cmd/config/profile/unset" + "github.com/stackitcloud/stackit-cli/internal/pkg/args" + "github.com/stackitcloud/stackit-cli/internal/pkg/print" + "github.com/stackitcloud/stackit-cli/internal/pkg/utils" + + "github.com/spf13/cobra" +) + +func NewCmd(p *print.Printer) *cobra.Command { + cmd := &cobra.Command{ + Use: "profile", + Short: "Manage the CLI configuration profiles", + Long: fmt.Sprintf("%s\n%s\n%s\n%s", + "Manage the CLI configuration profiles.", + `The profile to be used can be managed via the "STACKIT_CLI_PROFILE" environment variable or using the "stackit config profile set PROFILE" and "stackit config profile unset" commands.`, + "The environment variable takes precedence over what is set via the commands.", + "When no profile is set, the default profile is used.", + ), + Args: args.NoArgs, + Run: utils.CmdHelp, + } + addSubcommands(cmd, p) + return cmd +} + +func addSubcommands(cmd *cobra.Command, p *print.Printer) { + cmd.AddCommand(set.NewCmd(p)) + cmd.AddCommand(unset.NewCmd(p)) +} diff --git a/internal/cmd/config/profile/set/set.go b/internal/cmd/config/profile/set/set.go new file mode 100644 index 000000000..1ab1cac0e --- /dev/null +++ b/internal/cmd/config/profile/set/set.go @@ -0,0 +1,83 @@ +package set + +import ( + "fmt" + + "github.com/stackitcloud/stackit-cli/internal/pkg/args" + "github.com/stackitcloud/stackit-cli/internal/pkg/config" + "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/spf13/cobra" +) + +const ( + profileArg = "PROFILE" +) + +type inputModel struct { + *globalflags.GlobalFlagModel + Profile string +} + +func NewCmd(p *print.Printer) *cobra.Command { + cmd := &cobra.Command{ + Use: fmt.Sprintf("set %s", profileArg), + Short: "Set a CLI configuration profile", + Long: fmt.Sprintf("%s\n%s\n%s\n%s", + "Set a CLI configuration profile as the active profile.", + `The profile to be used can be managed via the STACKIT_CLI_PROFILE environment variable or using the "stackit config profile set PROFILE" and "stackit config profile unset" commands.`, + "The environment variable takes precedence over what is set via the commands.", + "When no profile is set, the default profile is used.", + ), + Args: args.SingleArg(profileArg, nil), + Example: examples.Build( + examples.NewExample( + `Set the configuration profile "my-profile" as the active profile`, + "$ stackit config profile set my-profile"), + ), + RunE: func(cmd *cobra.Command, args []string) error { + model, err := parseInput(p, cmd, args) + if err != nil { + return err + } + + err = config.SetProfile(model.Profile) + if err != nil { + return fmt.Errorf("set profile: %w", err) + } + + p.Info("Profile %q set successfully as the active profile\n", model.Profile) + return nil + }, + } + return cmd +} + +func parseInput(p *print.Printer, cmd *cobra.Command, inputArgs []string) (*inputModel, error) { + profile := inputArgs[0] + + err := config.ValidateProfile(profile) + if err != nil { + return nil, err + } + + globalFlags := globalflags.Parse(p, cmd) + + model := inputModel{ + GlobalFlagModel: globalFlags, + Profile: profile, + } + + if p.IsVerbosityDebug() { + modelStr, err := print.BuildDebugStrFromInputModel(model) + if err != nil { + p.Debug(print.ErrorLevel, "convert model to string for debugging: %v", err) + } else { + p.Debug(print.DebugLevel, "parsed input values: %s", modelStr) + } + } + + return &model, nil +} diff --git a/internal/cmd/config/profile/set/set_test.go b/internal/cmd/config/profile/set/set_test.go new file mode 100644 index 000000000..47f56ca0b --- /dev/null +++ b/internal/cmd/config/profile/set/set_test.go @@ -0,0 +1,132 @@ +package set + +import ( + "testing" + + "github.com/stackitcloud/stackit-cli/internal/pkg/globalflags" + "github.com/stackitcloud/stackit-cli/internal/pkg/print" + + "github.com/google/go-cmp/cmp" +) + +const testProfile = "test-profile" + +func fixtureArgValues(mods ...func(argValues []string)) []string { + argValues := []string{ + testProfile, + } + for _, mod := range mods { + mod(argValues) + } + return argValues +} + +func fixtureInputModel(mods ...func(model *inputModel)) *inputModel { + model := &inputModel{ + GlobalFlagModel: &globalflags.GlobalFlagModel{ + Verbosity: globalflags.VerbosityDefault, + }, + Profile: testProfile, + } + 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", + argValues: fixtureArgValues(), + isValid: true, + expectedModel: fixtureInputModel(), + }, + { + description: "no values", + argValues: []string{}, + flagValues: map[string]string{}, + isValid: false, + }, + { + description: "no arg values", + argValues: []string{}, + isValid: false, + }, + { + description: "some global flag", + argValues: fixtureArgValues(), + flagValues: map[string]string{ + globalflags.VerbosityFlag: globalflags.DebugVerbosity, + }, + isValid: true, + expectedModel: fixtureInputModel(func(model *inputModel) { + model.GlobalFlagModel.Verbosity = globalflags.DebugVerbosity + }), + }, + { + description: "invalid profile", + argValues: []string{"invalid-profile-&"}, + isValid: false, + }, + } + + for _, tt := range tests { + t.Run(tt.description, func(t *testing.T) { + p := print.NewPrinter() + cmd := NewCmd(p) + err := globalflags.Configure(cmd.Flags()) + if err != nil { + t.Fatalf("configure global flags: %v", err) + } + + for flag, value := range tt.flagValues { + err := cmd.Flags().Set(flag, value) + if err != nil { + if !tt.isValid { + return + } + t.Fatalf("setting flag --%s=%s: %v", flag, value, err) + } + } + + err = cmd.ValidateArgs(tt.argValues) + if err != nil { + if !tt.isValid { + return + } + t.Fatalf("error validating args: %v", err) + } + + err = cmd.ValidateRequiredFlags() + if err != nil { + if !tt.isValid { + return + } + t.Fatalf("error validating flags: %v", err) + } + + model, err := parseInput(p, cmd, tt.argValues) + if err != nil { + if !tt.isValid { + return + } + t.Fatalf("error parsing input: %v", err) + } + + if !tt.isValid { + t.Fatalf("did not fail on invalid input") + } + diff := cmp.Diff(model, tt.expectedModel) + if diff != "" { + t.Fatalf("Data does not match: %s", diff) + } + }) + } +} diff --git a/internal/cmd/config/profile/unset/unset.go b/internal/cmd/config/profile/unset/unset.go new file mode 100644 index 000000000..61ea77607 --- /dev/null +++ b/internal/cmd/config/profile/unset/unset.go @@ -0,0 +1,39 @@ +package unset + +import ( + "fmt" + + "github.com/stackitcloud/stackit-cli/internal/pkg/args" + "github.com/stackitcloud/stackit-cli/internal/pkg/config" + "github.com/stackitcloud/stackit-cli/internal/pkg/examples" + "github.com/stackitcloud/stackit-cli/internal/pkg/print" + + "github.com/spf13/cobra" +) + +func NewCmd(p *print.Printer) *cobra.Command { + cmd := &cobra.Command{ + Use: "unset", + Short: "Unset the current active CLI configuration profile", + Long: fmt.Sprintf("%s\n%s", + "Unset the current active CLI configuration profile.", + "When no profile is set, the default profile will be used.", + ), + Args: args.NoArgs, + Example: examples.Build( + examples.NewExample( + `Unset the currently active configuration profile. The default profile will be used.`, + "$ stackit config profile unset"), + ), + RunE: func(cmd *cobra.Command, args []string) error { + err := config.UnsetProfile() + if err != nil { + return fmt.Errorf("unset profile: %w", err) + } + + p.Info("Profile unset successfully. The default profile will be used.\n") + return nil + }, + } + return cmd +} diff --git a/internal/cmd/root.go b/internal/cmd/root.go index 5845d0c80..93b88ad1e 100644 --- a/internal/cmd/root.go +++ b/internal/cmd/root.go @@ -8,7 +8,7 @@ import ( "github.com/stackitcloud/stackit-cli/internal/cmd/argus" "github.com/stackitcloud/stackit-cli/internal/cmd/auth" - "github.com/stackitcloud/stackit-cli/internal/cmd/config" + configCmd "github.com/stackitcloud/stackit-cli/internal/cmd/config" "github.com/stackitcloud/stackit-cli/internal/cmd/curl" "github.com/stackitcloud/stackit-cli/internal/cmd/dns" loadbalancer "github.com/stackitcloud/stackit-cli/internal/cmd/load-balancer" @@ -26,6 +26,7 @@ import ( serviceaccount "github.com/stackitcloud/stackit-cli/internal/cmd/service-account" "github.com/stackitcloud/stackit-cli/internal/cmd/ske" "github.com/stackitcloud/stackit-cli/internal/pkg/args" + "github.com/stackitcloud/stackit-cli/internal/pkg/config" "github.com/stackitcloud/stackit-cli/internal/pkg/errors" "github.com/stackitcloud/stackit-cli/internal/pkg/flags" "github.com/stackitcloud/stackit-cli/internal/pkg/globalflags" @@ -44,7 +45,7 @@ func NewRootCmd(version, date string, p *print.Printer) *cobra.Command { SilenceErrors: true, // Error is beautified in a custom way before being printed SilenceUsage: true, DisableAutoGenTag: true, - PersistentPreRun: func(cmd *cobra.Command, args []string) { + PersistentPreRunE: func(cmd *cobra.Command, args []string) error { p.Cmd = cmd p.Verbosity = print.Level(globalflags.Parse(p, cmd).Verbosity) @@ -54,9 +55,20 @@ func NewRootCmd(version, date string, p *print.Printer) *cobra.Command { configFilePath := viper.ConfigFileUsed() p.Debug(print.DebugLevel, "using config file: %s", configFilePath) + activeProfile, err := config.GetProfile() + if err != nil { + return fmt.Errorf("get profile: %w", err) + } + if activeProfile == "" { + activeProfile = "(no active profile, the default profile configuration will be used)" + } + p.Debug(print.DebugLevel, "active config profile: %s", activeProfile) + configKeys := viper.AllSettings() configKeysStr := print.BuildDebugStrFromMap(configKeys) p.Debug(print.DebugLevel, "config keys: %s", configKeysStr) + + return nil }, RunE: func(cmd *cobra.Command, args []string) error { if flags.FlagToBoolValue(p, cmd, "version") { @@ -104,7 +116,7 @@ func configureFlags(cmd *cobra.Command) error { func addSubcommands(cmd *cobra.Command, p *print.Printer) { cmd.AddCommand(argus.NewCmd(p)) cmd.AddCommand(auth.NewCmd(p)) - cmd.AddCommand(config.NewCmd(p)) + cmd.AddCommand(configCmd.NewCmd(p)) cmd.AddCommand(curl.NewCmd(p)) cmd.AddCommand(dns.NewCmd(p)) cmd.AddCommand(loadbalancer.NewCmd(p)) diff --git a/internal/pkg/config/config.go b/internal/pkg/config/config.go index 8070bebc5..209710e08 100644 --- a/internal/pkg/config/config.go +++ b/internal/pkg/config/config.go @@ -77,17 +77,20 @@ var ConfigKeys = []string{ } var configFolderPath string +var profileFilePath string func InitConfig() { configDir, err := os.UserConfigDir() cobra.CheckErr(err) + configFolderPath = filepath.Join(configDir, configFolder) // Default config folder + profileFilePath = filepath.Join(configFolderPath, fmt.Sprintf("%s.%s", profileFileName, profileFileExtension)) // Profile file path is in the default config folder + configProfile, err := GetProfile() cobra.CheckErr(err) - configFolderPath = filepath.Join(configDir, configFolder) if configProfile != "" { - configFolderPath = filepath.Join(configFolderPath, configProfile) + configFolderPath = filepath.Join(configFolderPath, configProfile) // If a profile is set, use the profile config folder } configFilePath := filepath.Join(configFolderPath, fmt.Sprintf("%s.%s", configFileName, configFileExtension)) diff --git a/internal/pkg/config/profiles.go b/internal/pkg/config/profiles.go index 5f21099d9..c581b95a1 100644 --- a/internal/pkg/config/profiles.go +++ b/internal/pkg/config/profiles.go @@ -5,6 +5,8 @@ import ( "os" "path/filepath" "regexp" + + "github.com/stackitcloud/stackit-cli/internal/pkg/errors" ) // GetProfile returns the current profile to be used by the CLI. @@ -18,7 +20,6 @@ import ( func GetProfile() (string, error) { profile, profileSet := os.LookupEnv("STACKIT_CLI_PROFILE") if !profileSet { - profileFilePath := filepath.Join(configFolderPath, fmt.Sprintf("%s.%s", profileFileName, profileFileExtension)) contents, exists, err := readFileIfExists(profileFilePath) if err != nil { return "", fmt.Errorf("read profile from file: %w", err) @@ -29,23 +30,50 @@ func GetProfile() (string, error) { profile = contents } - err := validateProfile(profile) + err := ValidateProfile(profile) if err != nil { return "", fmt.Errorf("validate profile: %w", err) } return profile, nil } -// validateProfile validates the profile name. +// SetProfile sets the profile to be used by the CLI. +func SetProfile(profile string) error { + err := ValidateProfile(profile) + if err != nil { + return fmt.Errorf("validate profile: %w", err) + } + + profileFilePath := filepath.Join(configFolderPath, fmt.Sprintf("%s.%s", profileFileName, profileFileExtension)) + err = os.WriteFile(profileFilePath, []byte(profile), os.ModePerm) + if err != nil { + return fmt.Errorf("write profile to file: %w", err) + } + return nil +} + +// UnsetProfile removes the profile file. +// If the profile file does not exist, it does nothing. +func UnsetProfile() error { + err := os.Remove(profileFilePath) + if err != nil && !os.IsNotExist(err) { + return fmt.Errorf("remove profile file: %w", err) + } + return nil +} + +// ValidateProfile validates the profile name. // It can only use letters, numbers, or "-" and cannot be empty. // If the profile is invalid, it returns an error. -func validateProfile(profile string) error { +func ValidateProfile(profile string) error { match, err := regexp.MatchString("^[a-zA-Z0-9-]+$", profile) if err != nil { return fmt.Errorf("match string regex: %w", err) } if !match { - return fmt.Errorf("profile name can only contain letters, numbers, and \"-\" and cannot be empty") + return &errors.InvalidProfileNameError{ + Profile: profile, + } } return nil } diff --git a/internal/pkg/config/profiles_test.go b/internal/pkg/config/profiles_test.go index 8e96a6e88..e97451aeb 100644 --- a/internal/pkg/config/profiles_test.go +++ b/internal/pkg/config/profiles_test.go @@ -47,7 +47,7 @@ func TestValidateProfile(t *testing.T) { for _, tt := range tests { t.Run(tt.description, func(t *testing.T) { - err := validateProfile(tt.profile) + err := ValidateProfile(tt.profile) if tt.isValid && err != nil { t.Errorf("expected profile to be valid but got error: %v", err) } diff --git a/internal/pkg/errors/errors.go b/internal/pkg/errors/errors.go index d421aa1b7..0bb41a4ab 100644 --- a/internal/pkg/errors/errors.go +++ b/internal/pkg/errors/errors.go @@ -113,6 +113,10 @@ For more details on the available storages for the configured flavor (%[3]s), ru SUBCOMMAND_MISSING = `missing subcommand` + INVALID_PROFILE_NAME = `the profile name %q is invalid. + +The profile name can only contain letters, numbers, and "-" and cannot be empty.` + USAGE_TIP = `For usage help, run: $ %s --help` ) @@ -304,3 +308,11 @@ func AppendUsageTip(err error, cmd *cobra.Command) error { tip := fmt.Sprintf(USAGE_TIP, cmd.CommandPath()) return fmt.Errorf("%w.\n\n%s", err, tip) } + +type InvalidProfileNameError struct { + Profile string +} + +func (e *InvalidProfileNameError) Error() string { + return fmt.Sprintf(INVALID_PROFILE_NAME, e.Profile) +} diff --git a/main.go b/main.go index 37689d889..5286783ac 100644 --- a/main.go +++ b/main.go @@ -5,7 +5,7 @@ import ( "github.com/stackitcloud/stackit-cli/internal/pkg/config" ) -// These values are dynamically overridden by GoReleaser +// These values are overwritten by GoReleaser at build time var ( version = "DEV" date = "UNKNOWN" From 81d4448c35f76b25c1bb3e83c8c8de15ae04618d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20Palet?= Date: Mon, 13 May 2024 11:50:36 +0100 Subject: [PATCH 04/18] Improvements to profiles functionality (#315) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Add --used and --unused flags to `load-balancer list` (#308) * initial implementation * add testing, finish functionality * generate docs, minor improvements * more testing * refactor implementation, simplify RunE * remove unused func * address PR comments * generate-docs * change filtercredentials to use enum for operation type * address PR comments * Onboard `load-balancer observability-credentials cleanup` (#311) * command implementation, add testing * rename var, generate docs * address PR comments * fix no credentials listing (#313) * Integrate WinGet distribution in release pipeline (#305) * Integrate WinGet distribution in release pipeline * Add comment regarding skipping prereleases * Fix link * Configure table titles (#314) * Update tables * Configure colors in the less pager * Fix title wrapping, add titles to lb * Re-add -w argument and add titles to mongodb and pgflex options * Add table title to config list * Fixes and improvements to profiles functionality --------- Co-authored-by: Diogo Ferrão Co-authored-by: Vicente Pinto --- .goreleaser.yaml | 25 +- docs/stackit_config.md | 13 +- docs/stackit_config_profile.md | 37 ++ docs/stackit_config_profile_set.md | 43 ++ docs/stackit_config_profile_unset.md | 40 ++ ...load-balancer_observability-credentials.md | 3 +- ...ancer_observability-credentials_cleanup.md | 39 ++ ...balancer_observability-credentials_list.md | 18 +- internal/cmd/config/list/list.go | 8 +- internal/cmd/config/profile/set/set.go | 7 +- internal/cmd/config/profile/unset/unset.go | 2 +- .../cmd/load-balancer/describe/describe.go | 7 +- .../cleanup/cleanup.go | 140 +++++++ .../cleanup/cleanup_test.go | 221 +++++++++++ .../observability-credentials/list/list.go | 72 +++- .../list/list_test.go | 86 ++++ .../observability-credentials.go | 2 + .../target-pool/add-target/add_target_test.go | 4 + .../remove-target/remove_target_test.go | 4 + internal/cmd/mongodbflex/options/options.go | 5 +- internal/cmd/postgresflex/options/options.go | 5 +- internal/cmd/root.go | 6 +- internal/cmd/ske/options/options.go | 15 +- internal/pkg/config/config.go | 6 +- internal/pkg/config/profiles.go | 17 +- internal/pkg/print/print.go | 20 +- internal/pkg/print/print_test.go | 7 +- .../pkg/services/load-balancer/utils/utils.go | 108 +++++ .../load-balancer/utils/utils_test.go | 369 +++++++++++++++++- internal/pkg/tables/tables.go | 19 + 30 files changed, 1287 insertions(+), 61 deletions(-) create mode 100644 docs/stackit_config_profile.md create mode 100644 docs/stackit_config_profile_set.md create mode 100644 docs/stackit_config_profile_unset.md create mode 100644 docs/stackit_load-balancer_observability-credentials_cleanup.md create mode 100644 internal/cmd/load-balancer/observability-credentials/cleanup/cleanup.go create mode 100644 internal/cmd/load-balancer/observability-credentials/cleanup/cleanup_test.go diff --git a/.goreleaser.yaml b/.goreleaser.yaml index 041ddccf5..2a1791612 100644 --- a/.goreleaser.yaml +++ b/.goreleaser.yaml @@ -101,7 +101,7 @@ brews: email: noreply@stackit.de homepage: "https://github.com/stackitcloud/stackit-cli" description: "A command-line interface to manage STACKIT resources.\nThis CLI is in a BETA state. More services and functionality will be supported soon." - folder: Formula + directory: Formula license: "Apache-2.0" # If set to auto, the release will not be uploaded to the homebrew tap repo # if the tag has a prerelease indicator (e.g. v0.0.1-alpha1) @@ -125,3 +125,26 @@ snapcrafts: grade: devel # Whether to publish the Snap to the store publish: true + +winget: + - name: stackit + publisher: stackitcloud + short_description: A command-line interface to manage STACKIT resources. + license: Apache-2.0 + publisher_support_url: "https://github.com/stackitcloud/stackit-cli/issues" + package_identifier: stackitcloud.stackit + homepage: "https://github.com/stackitcloud/stackit-cli" + # If set to auto, the release will not be uploaded to the homebrew tap repo + # if the tag has a prerelease indicator (e.g. v0.0.1-alpha1) + # Temporarily not skipping prereleases to test integration with Winget + # skip_upload: auto + repository: + owner: stackitcloud + name: winget-pkgs + pull_request: + enabled: true + draft: true + base: + owner: microsoft + name: winget-pkgs + branch: master \ No newline at end of file diff --git a/docs/stackit_config.md b/docs/stackit_config.md index ed8212d87..fedd6f2a3 100644 --- a/docs/stackit_config.md +++ b/docs/stackit_config.md @@ -4,12 +4,12 @@ Provides functionality for CLI configuration options ### Synopsis -Provides functionality for CLI configuration options -The configuration is stored in a file in the user's config directory, which is OS dependent. -Windows: %APPDATA%\stackit -Linux: $XDG_CONFIG_HOME/stackit -macOS: $HOME/Library/Application Support/stackit -The configuration file is named `cli-config.json` and is created automatically in your first CLI run. +Provides functionality for CLI configuration options. +You can set and unset different configuration options via the "stackit config set" and "stackit config unset" commands. + +Additionally, you can configure the CLI to use different profiles, each with its own configuration. +Additional profiles can be configured via the "STACKIT_CLI_PROFILE" environment variable or using the "stackit config profile set PROFILE" and "stackit config profile unset" commands. +The environment variable takes precedence over what is set via the commands. ``` stackit config [flags] @@ -35,6 +35,7 @@ stackit config [flags] * [stackit](./stackit.md) - Manage STACKIT resources using the command line * [stackit config list](./stackit_config_list.md) - Lists the current CLI configuration values +* [stackit config profile](./stackit_config_profile.md) - Manage the CLI configuration profiles * [stackit config set](./stackit_config_set.md) - Sets CLI configuration options * [stackit config unset](./stackit_config_unset.md) - Unsets CLI configuration options diff --git a/docs/stackit_config_profile.md b/docs/stackit_config_profile.md new file mode 100644 index 000000000..1c46aeb73 --- /dev/null +++ b/docs/stackit_config_profile.md @@ -0,0 +1,37 @@ +## stackit config profile + +Manage the CLI configuration profiles + +### Synopsis + +Manage the CLI configuration profiles. +The profile to be used can be managed via the "STACKIT_CLI_PROFILE" environment variable or using the "stackit config profile set PROFILE" and "stackit config profile unset" commands. +The environment variable takes precedence over what is set via the commands. +When no profile is set, the default profile is used. + +``` +stackit config profile [flags] +``` + +### Options + +``` + -h, --help Help for "stackit config profile" +``` + +### 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"] + -p, --project-id string Project ID + --verbosity string Verbosity of the CLI, one of ["debug" "info" "warning" "error"] (default "info") +``` + +### SEE ALSO + +* [stackit config](./stackit_config.md) - Provides functionality for CLI configuration options +* [stackit config profile set](./stackit_config_profile_set.md) - Set a CLI configuration profile +* [stackit config profile unset](./stackit_config_profile_unset.md) - Unset the current active CLI configuration profile + diff --git a/docs/stackit_config_profile_set.md b/docs/stackit_config_profile_set.md new file mode 100644 index 000000000..bcc9725a6 --- /dev/null +++ b/docs/stackit_config_profile_set.md @@ -0,0 +1,43 @@ +## stackit config profile set + +Set a CLI configuration profile + +### Synopsis + +Set a CLI configuration profile as the active profile. +The profile to be used can be managed via the STACKIT_CLI_PROFILE environment variable or using the "stackit config profile set PROFILE" and "stackit config profile unset" commands. +The environment variable takes precedence over what is set via the commands. +A new profile is created automatically if it does not exist. +When no profile is set, the default profile is used. + +``` +stackit config profile set PROFILE [flags] +``` + +### Examples + +``` + Set the configuration profile "my-profile" as the active profile + $ stackit config profile set my-profile +``` + +### Options + +``` + -h, --help Help for "stackit config profile set" +``` + +### 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"] + -p, --project-id string Project ID + --verbosity string Verbosity of the CLI, one of ["debug" "info" "warning" "error"] (default "info") +``` + +### SEE ALSO + +* [stackit config profile](./stackit_config_profile.md) - Manage the CLI configuration profiles + diff --git a/docs/stackit_config_profile_unset.md b/docs/stackit_config_profile_unset.md new file mode 100644 index 000000000..0beda0f67 --- /dev/null +++ b/docs/stackit_config_profile_unset.md @@ -0,0 +1,40 @@ +## stackit config profile unset + +Unset the current active CLI configuration profile + +### Synopsis + +Unset the current active CLI configuration profile. +When no profile is set, the default profile will be used. + +``` +stackit config profile unset [flags] +``` + +### Examples + +``` + Unset the currently active configuration profile. The default profile will be used. + $ stackit config profile unset +``` + +### Options + +``` + -h, --help Help for "stackit config profile unset" +``` + +### 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"] + -p, --project-id string Project ID + --verbosity string Verbosity of the CLI, one of ["debug" "info" "warning" "error"] (default "info") +``` + +### SEE ALSO + +* [stackit config profile](./stackit_config_profile.md) - Manage the CLI configuration profiles + diff --git a/docs/stackit_load-balancer_observability-credentials.md b/docs/stackit_load-balancer_observability-credentials.md index 111523c5f..4ccba5c45 100644 --- a/docs/stackit_load-balancer_observability-credentials.md +++ b/docs/stackit_load-balancer_observability-credentials.md @@ -30,8 +30,9 @@ stackit load-balancer observability-credentials [flags] * [stackit load-balancer](./stackit_load-balancer.md) - Provides functionality for Load Balancer * [stackit load-balancer observability-credentials add](./stackit_load-balancer_observability-credentials_add.md) - Adds observability credentials to Load Balancer +* [stackit load-balancer observability-credentials cleanup](./stackit_load-balancer_observability-credentials_cleanup.md) - Deletes observability credentials unused by any Load Balancer * [stackit load-balancer observability-credentials delete](./stackit_load-balancer_observability-credentials_delete.md) - Deletes observability credentials for Load Balancer * [stackit load-balancer observability-credentials describe](./stackit_load-balancer_observability-credentials_describe.md) - Shows details of observability credentials for Load Balancer -* [stackit load-balancer observability-credentials list](./stackit_load-balancer_observability-credentials_list.md) - Lists all observability credentials for Load Balancer +* [stackit load-balancer observability-credentials list](./stackit_load-balancer_observability-credentials_list.md) - Lists observability credentials for Load Balancer * [stackit load-balancer observability-credentials update](./stackit_load-balancer_observability-credentials_update.md) - Updates observability credentials for Load Balancer diff --git a/docs/stackit_load-balancer_observability-credentials_cleanup.md b/docs/stackit_load-balancer_observability-credentials_cleanup.md new file mode 100644 index 000000000..e994d0803 --- /dev/null +++ b/docs/stackit_load-balancer_observability-credentials_cleanup.md @@ -0,0 +1,39 @@ +## stackit load-balancer observability-credentials cleanup + +Deletes observability credentials unused by any Load Balancer + +### Synopsis + +Deletes observability credentials unused by any Load Balancer. + +``` +stackit load-balancer observability-credentials cleanup [flags] +``` + +### Examples + +``` + Delete observability credentials unused by any Load Balancer + $ stackit load-balancer observability-credentials cleanup +``` + +### Options + +``` + -h, --help Help for "stackit load-balancer observability-credentials cleanup" +``` + +### 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"] + -p, --project-id string Project ID + --verbosity string Verbosity of the CLI, one of ["debug" "info" "warning" "error"] (default "info") +``` + +### SEE ALSO + +* [stackit load-balancer observability-credentials](./stackit_load-balancer_observability-credentials.md) - Provides functionality for Load Balancer observability credentials + diff --git a/docs/stackit_load-balancer_observability-credentials_list.md b/docs/stackit_load-balancer_observability-credentials_list.md index ee183e01f..581aa37a8 100644 --- a/docs/stackit_load-balancer_observability-credentials_list.md +++ b/docs/stackit_load-balancer_observability-credentials_list.md @@ -1,10 +1,10 @@ ## stackit load-balancer observability-credentials list -Lists all observability credentials for Load Balancer +Lists observability credentials for Load Balancer ### Synopsis -Lists all observability credentials for Load Balancer. +Lists observability credentials for Load Balancer. ``` stackit load-balancer observability-credentials list [flags] @@ -13,13 +13,19 @@ stackit load-balancer observability-credentials list [flags] ### Examples ``` - List all observability credentials for Load Balancer + List all Load Balancer observability credentials $ stackit load-balancer observability-credentials list - List all observability credentials for Load Balancer in JSON format + List all observability credentials being used by Load Balancer + $ stackit load-balancer observability-credentials list --used + + List all observability credentials not being used by Load Balancer + $ stackit load-balancer observability-credentials list --unused + + List all Load Balancer observability credentials in JSON format $ stackit load-balancer observability-credentials list --output-format json - List up to 10 observability credentials for Load Balancer + List up to 10 Load Balancer observability credentials $ stackit load-balancer observability-credentials list --limit 10 ``` @@ -28,6 +34,8 @@ stackit load-balancer observability-credentials list [flags] ``` -h, --help Help for "stackit load-balancer observability-credentials list" --limit int Maximum number of entries to list + --unused List only credentials not being used by a Load Balancer + --used List only credentials being used by a Load Balancer ``` ### Options inherited from parent commands diff --git a/internal/cmd/config/list/list.go b/internal/cmd/config/list/list.go index 8b890aa4a..d6c3e2055 100644 --- a/internal/cmd/config/list/list.go +++ b/internal/cmd/config/list/list.go @@ -74,7 +74,7 @@ func outputResult(p *print.Printer, outputFormat string, configData map[string]a switch outputFormat { case print.JSONOutputFormat: if activeProfile != "" { - configData["active_profile"] = activeProfile + configData["profile"] = activeProfile } details, err := json.MarshalIndent(configData, "", " ") if err != nil { @@ -83,9 +83,6 @@ func outputResult(p *print.Printer, outputFormat string, configData map[string]a p.Outputln(string(details)) return nil default: - if activeProfile != "" { - p.Outputf("\n ACTIVE PROFILE: %s\n", activeProfile) - } // Sort the config options by key configKeys := make([]string, 0, len(configData)) @@ -95,6 +92,9 @@ func outputResult(p *print.Printer, outputFormat string, configData map[string]a sort.Strings(configKeys) table := tables.NewTable() + if activeProfile != "" { + table.SetTitle(fmt.Sprintf("Profile: %q", activeProfile)) + } table.SetHeader("NAME", "VALUE") for _, key := range configKeys { value := configData[key] diff --git a/internal/cmd/config/profile/set/set.go b/internal/cmd/config/profile/set/set.go index 1ab1cac0e..9dd3172e9 100644 --- a/internal/cmd/config/profile/set/set.go +++ b/internal/cmd/config/profile/set/set.go @@ -25,10 +25,11 @@ func NewCmd(p *print.Printer) *cobra.Command { cmd := &cobra.Command{ Use: fmt.Sprintf("set %s", profileArg), Short: "Set a CLI configuration profile", - Long: fmt.Sprintf("%s\n%s\n%s\n%s", + Long: fmt.Sprintf("%s\n%s\n%s\n%s\n%s", "Set a CLI configuration profile as the active profile.", `The profile to be used can be managed via the STACKIT_CLI_PROFILE environment variable or using the "stackit config profile set PROFILE" and "stackit config profile unset" commands.`, "The environment variable takes precedence over what is set via the commands.", + "A new profile is created automatically if it does not exist.", "When no profile is set, the default profile is used.", ), Args: args.SingleArg(profileArg, nil), @@ -43,12 +44,12 @@ func NewCmd(p *print.Printer) *cobra.Command { return err } - err = config.SetProfile(model.Profile) + err = config.SetProfile(p, model.Profile) if err != nil { return fmt.Errorf("set profile: %w", err) } - p.Info("Profile %q set successfully as the active profile\n", model.Profile) + p.Info("Successfully set active profile to %q\n", model.Profile) return nil }, } diff --git a/internal/cmd/config/profile/unset/unset.go b/internal/cmd/config/profile/unset/unset.go index 61ea77607..339439262 100644 --- a/internal/cmd/config/profile/unset/unset.go +++ b/internal/cmd/config/profile/unset/unset.go @@ -26,7 +26,7 @@ func NewCmd(p *print.Printer) *cobra.Command { "$ stackit config profile unset"), ), RunE: func(cmd *cobra.Command, args []string) error { - err := config.UnsetProfile() + err := config.UnsetProfile(p) if err != nil { return fmt.Errorf("unset profile: %w", err) } diff --git a/internal/cmd/load-balancer/describe/describe.go b/internal/cmd/load-balancer/describe/describe.go index fe28b86d7..ff6defaf0 100644 --- a/internal/cmd/load-balancer/describe/describe.go +++ b/internal/cmd/load-balancer/describe/describe.go @@ -162,6 +162,7 @@ func renderLoadBalancer(loadBalancer *loadbalancer.LoadBalancer) string { } table := tables.NewTable() + table.SetTitle("Load Balancer") table.AddRow("NAME", *loadBalancer.Name) table.AddSeparator() table.AddRow("STATE", *loadBalancer.Status) @@ -182,7 +183,8 @@ func renderLoadBalancer(loadBalancer *loadbalancer.LoadBalancer) string { func renderListeners(listeners []loadbalancer.Listener) string { table := tables.NewTable() - table.SetHeader("LISTENER NAME", "PORT", "PROTOCOL", "TARGET POOL") + table.SetTitle("Listeners") + table.SetHeader("NAME", "PORT", "PROTOCOL", "TARGET POOL") for i := range listeners { listener := listeners[i] table.AddRow(*listener.Name, *listener.Port, *listener.Protocol, *listener.TargetPool) @@ -192,7 +194,8 @@ func renderListeners(listeners []loadbalancer.Listener) string { func renderTargetPools(targetPools []loadbalancer.TargetPool) string { table := tables.NewTable() - table.SetHeader("TARGET POOL NAME", "PORT", "TARGETS") + table.SetTitle("Target Pools") + table.SetHeader("NAME", "PORT", "TARGETS") for _, targetPool := range targetPools { table.AddRow(*targetPool.Name, *targetPool.TargetPort, len(*targetPool.Targets)) } diff --git a/internal/cmd/load-balancer/observability-credentials/cleanup/cleanup.go b/internal/cmd/load-balancer/observability-credentials/cleanup/cleanup.go new file mode 100644 index 000000000..2192b4591 --- /dev/null +++ b/internal/cmd/load-balancer/observability-credentials/cleanup/cleanup.go @@ -0,0 +1,140 @@ +package cleanup + +import ( + "context" + "fmt" + + "github.com/stackitcloud/stackit-cli/internal/pkg/args" + "github.com/stackitcloud/stackit-cli/internal/pkg/errors" + "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/projectname" + "github.com/stackitcloud/stackit-cli/internal/pkg/services/load-balancer/client" + "github.com/stackitcloud/stackit-cli/internal/pkg/services/load-balancer/utils" + + "github.com/spf13/cobra" + "github.com/stackitcloud/stackit-sdk-go/services/loadbalancer" +) + +type inputModel struct { + *globalflags.GlobalFlagModel +} + +func NewCmd(p *print.Printer) *cobra.Command { + cmd := &cobra.Command{ + Use: "cleanup", + Short: "Deletes observability credentials unused by any Load Balancer", + Long: "Deletes observability credentials unused by any Load Balancer.", + Args: args.NoArgs, + Example: examples.Build( + examples.NewExample( + `Delete observability credentials unused by any Load Balancer`, + "$ stackit load-balancer observability-credentials cleanup"), + ), + RunE: func(cmd *cobra.Command, args []string) error { + ctx := context.Background() + model, err := parseInput(p, cmd) + if err != nil { + return err + } + + // Configure API client + apiClient, err := client.ConfigureClient(p) + if err != nil { + return err + } + + projectLabel, err := projectname.GetProjectName(ctx, p, cmd) + if err != nil { + p.Debug(print.ErrorLevel, "get project name: %v", err) + projectLabel = model.ProjectId + } + + listReq := buildListCredentialsRequest(ctx, model, apiClient) + resp, err := listReq.Execute() + if err != nil { + return fmt.Errorf("list Load Balancer observability credentials: %w", err) + } + + var credentials []loadbalancer.CredentialsResponse + if resp.Credentials != nil && len(*resp.Credentials) > 0 { + credentials, err = utils.FilterCredentials(ctx, apiClient, *resp.Credentials, model.ProjectId, utils.OP_FILTER_UNUSED) + if err != nil { + return fmt.Errorf("filter Load Balancer observability credentials: %w", err) + } + } + + if len(credentials) == 0 { + p.Info("No unused observability credentials found on project %q\n", projectLabel) + return nil + } + + if !model.AssumeYes { + prompt := "Will delete the following unused observability credentials: \n" + for _, credential := range credentials { + if credential.DisplayName == nil || credential.Username == nil { + return fmt.Errorf("list unused Load Balancer observability credentials: credentials %q missing display name or username", *credential.CredentialsRef) + } + name := *credential.DisplayName + username := *credential.Username + prompt += fmt.Sprintf(" - %s (username: %q)\n", name, username) + } + prompt += fmt.Sprintf("Are you sure you want to delete unused observability credentials on project %q? (This cannot be undone)", projectLabel) + err = p.PromptForConfirmation(prompt) + if err != nil { + return err + } + } + + for _, credential := range credentials { + if credential.CredentialsRef == nil { + return fmt.Errorf("delete Load Balancer observability credentials: missing credentials reference") + } + credentialsRef := *credential.CredentialsRef + // Call API + req := buildDeleteCredentialRequest(ctx, model, apiClient, credentialsRef) + _, err = req.Execute() + if err != nil { + return fmt.Errorf("delete Load Balancer observability credentials: %w", err) + } + } + + p.Info("Deleted unused Load Balancer observability credentials on project %q\n", projectLabel) + return nil + }, + } + return cmd +} + +func parseInput(p *print.Printer, cmd *cobra.Command) (*inputModel, error) { + globalFlags := globalflags.Parse(p, cmd) + if globalFlags.ProjectId == "" { + return nil, &errors.ProjectIdError{} + } + + model := inputModel{ + GlobalFlagModel: globalFlags, + } + + if p.IsVerbosityDebug() { + modelStr, err := print.BuildDebugStrFromInputModel(model) + if err != nil { + p.Debug(print.ErrorLevel, "convert model to string for debugging: %v", err) + } else { + p.Debug(print.DebugLevel, "parsed input values: %s", modelStr) + } + } + + return &model, nil +} + +func buildDeleteCredentialRequest(ctx context.Context, model *inputModel, apiClient *loadbalancer.APIClient, credentialsRef string) loadbalancer.ApiDeleteCredentialsRequest { + req := apiClient.DeleteCredentials(ctx, model.ProjectId, credentialsRef) + return req +} + +func buildListCredentialsRequest(ctx context.Context, model *inputModel, apiClient *loadbalancer.APIClient) loadbalancer.ApiListCredentialsRequest { + req := apiClient.ListCredentials(ctx, model.ProjectId) + return req +} diff --git a/internal/cmd/load-balancer/observability-credentials/cleanup/cleanup_test.go b/internal/cmd/load-balancer/observability-credentials/cleanup/cleanup_test.go new file mode 100644 index 000000000..ffa7a07e7 --- /dev/null +++ b/internal/cmd/load-balancer/observability-credentials/cleanup/cleanup_test.go @@ -0,0 +1,221 @@ +package cleanup + +import ( + "context" + "testing" + + "github.com/stackitcloud/stackit-cli/internal/pkg/globalflags" + "github.com/stackitcloud/stackit-cli/internal/pkg/print" + "github.com/stackitcloud/stackit-sdk-go/services/loadbalancer" + + "github.com/google/go-cmp/cmp" + "github.com/google/go-cmp/cmp/cmpopts" + "github.com/google/uuid" +) + +const testCredentialsRef = "credentials-1" + +var projectIdFlag = globalflags.ProjectIdFlag + +type testCtxKey struct{} + +var testCtx = context.WithValue(context.Background(), testCtxKey{}, "foo") +var testClient = &loadbalancer.APIClient{} +var testProjectId = uuid.NewString() + +func fixtureFlagValues(mods ...func(flagValues map[string]string)) map[string]string { + flagValues := map[string]string{ + projectIdFlag: testProjectId, + } + for _, mod := range mods { + mod(flagValues) + } + return flagValues +} + +func fixtureInputModel(mods ...func(model *inputModel)) *inputModel { + model := &inputModel{ + GlobalFlagModel: &globalflags.GlobalFlagModel{ + ProjectId: testProjectId, + Verbosity: globalflags.VerbosityDefault, + }, + } + for _, mod := range mods { + mod(model) + } + return model +} + +func fixtureDeleteCredentialRequest(mods ...func(request *loadbalancer.ApiDeleteCredentialsRequest)) loadbalancer.ApiDeleteCredentialsRequest { + request := testClient.DeleteCredentials(testCtx, testProjectId, testCredentialsRef) + for _, mod := range mods { + mod(&request) + } + return request +} + +func fixtureListCredentialsRequest(mods ...func(request *loadbalancer.ApiListCredentialsRequest)) loadbalancer.ApiListCredentialsRequest { + request := testClient.ListCredentials(testCtx, testProjectId) + for _, mod := range mods { + mod(&request) + } + return request +} + +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", + argValues: []string{}, + flagValues: map[string]string{}, + isValid: false, + }, + { + description: "no flag values", + flagValues: map[string]string{}, + isValid: false, + }, + { + description: "project id missing", + flagValues: fixtureFlagValues(func(flagValues map[string]string) { + delete(flagValues, projectIdFlag) + }), + isValid: false, + }, + { + description: "project id invalid 1", + flagValues: fixtureFlagValues(func(flagValues map[string]string) { + flagValues[projectIdFlag] = "" + }), + isValid: false, + }, + { + description: "project id invalid 2", + flagValues: fixtureFlagValues(func(flagValues map[string]string) { + flagValues[projectIdFlag] = "invalid-uuid" + }), + isValid: false, + }, + } + + for _, tt := range tests { + t.Run(tt.description, func(t *testing.T) { + p := print.NewPrinter() + cmd := NewCmd(p) + err := globalflags.Configure(cmd.Flags()) + if err != nil { + t.Fatalf("configure global flags: %v", err) + } + + for flag, value := range tt.flagValues { + err := cmd.Flags().Set(flag, value) + if err != nil { + if !tt.isValid { + return + } + t.Fatalf("setting flag --%s=%s: %v", flag, value, err) + } + } + + err = cmd.ValidateArgs(tt.argValues) + if err != nil { + if !tt.isValid { + return + } + t.Fatalf("error validating args: %v", err) + } + + err = cmd.ValidateRequiredFlags() + if err != nil { + if !tt.isValid { + return + } + t.Fatalf("error validating flags: %v", err) + } + + model, err := parseInput(p, cmd) + if err != nil { + if !tt.isValid { + return + } + t.Fatalf("error parsing input: %v", err) + } + + if !tt.isValid { + t.Fatalf("did not fail on invalid input") + } + diff := cmp.Diff(model, tt.expectedModel) + if diff != "" { + t.Fatalf("Data does not match: %s", diff) + } + }) + } +} + +func TestBuildDeleteCredentialRequest(t *testing.T) { + tests := []struct { + description string + model *inputModel + expectedRequest loadbalancer.ApiDeleteCredentialsRequest + }{ + { + description: "base", + model: fixtureInputModel(), + expectedRequest: fixtureDeleteCredentialRequest(), + }, + } + + for _, tt := range tests { + t.Run(tt.description, func(t *testing.T) { + request := buildDeleteCredentialRequest(testCtx, tt.model, testClient, testCredentialsRef) + + diff := cmp.Diff(request, tt.expectedRequest, + cmp.AllowUnexported(tt.expectedRequest), + cmpopts.EquateComparable(testCtx), + ) + if diff != "" { + t.Fatalf("Data does not match: %s", diff) + } + }) + } +} + +func TestListCredentialsRequest(t *testing.T) { + tests := []struct { + description string + model *inputModel + expectedRequest loadbalancer.ApiListCredentialsRequest + }{ + { + description: "base", + model: fixtureInputModel(), + expectedRequest: fixtureListCredentialsRequest(), + }, + } + + for _, tt := range tests { + t.Run(tt.description, func(t *testing.T) { + request := buildListCredentialsRequest(testCtx, tt.model, testClient) + + diff := cmp.Diff(request, tt.expectedRequest, + cmp.AllowUnexported(tt.expectedRequest), + cmpopts.EquateComparable(testCtx), + ) + if diff != "" { + t.Fatalf("Data does not match: %s", diff) + } + }) + } +} diff --git a/internal/cmd/load-balancer/observability-credentials/list/list.go b/internal/cmd/load-balancer/observability-credentials/list/list.go index 21cb4dcad..f71ab67a9 100644 --- a/internal/cmd/load-balancer/observability-credentials/list/list.go +++ b/internal/cmd/load-balancer/observability-credentials/list/list.go @@ -13,6 +13,7 @@ import ( "github.com/stackitcloud/stackit-cli/internal/pkg/print" "github.com/stackitcloud/stackit-cli/internal/pkg/projectname" "github.com/stackitcloud/stackit-cli/internal/pkg/services/load-balancer/client" + "github.com/stackitcloud/stackit-cli/internal/pkg/services/load-balancer/utils" "github.com/stackitcloud/stackit-cli/internal/pkg/tables" "github.com/spf13/cobra" @@ -22,28 +23,38 @@ import ( const ( instanceIdFlag = "instance-id" limitFlag = "limit" + usedFlag = "used" + unusedFlag = "unused" ) type inputModel struct { *globalflags.GlobalFlagModel - Limit *int64 + Limit *int64 + Used bool + Unused bool } func NewCmd(p *print.Printer) *cobra.Command { cmd := &cobra.Command{ Use: "list", - Short: "Lists all observability credentials for Load Balancer", - Long: "Lists all observability credentials for Load Balancer.", + Short: "Lists observability credentials for Load Balancer", + Long: "Lists observability credentials for Load Balancer.", Args: args.NoArgs, Example: examples.Build( examples.NewExample( - `List all observability credentials for Load Balancer`, + `List all Load Balancer observability credentials`, "$ stackit load-balancer observability-credentials list"), examples.NewExample( - `List all observability credentials for Load Balancer in JSON format`, + `List all observability credentials being used by Load Balancer`, + "$ stackit load-balancer observability-credentials list --used"), + examples.NewExample( + `List all observability credentials not being used by Load Balancer`, + "$ stackit load-balancer observability-credentials list --unused"), + examples.NewExample( + `List all Load Balancer observability credentials in JSON format`, "$ stackit load-balancer observability-credentials list --output-format json"), examples.NewExample( - `List up to 10 observability credentials for Load Balancer`, + `List up to 10 Load Balancer observability credentials`, "$ stackit load-balancer observability-credentials list --limit 10"), ), RunE: func(cmd *cobra.Command, args []string) error { @@ -72,12 +83,30 @@ func NewCmd(p *print.Printer) *cobra.Command { return fmt.Errorf("list Load Balancer observability credentials: %w", err) } credentialsPtr := resp.Credentials - if credentialsPtr == nil || (credentialsPtr != nil && len(*credentialsPtr) == 0) { - p.Info("No observability credentials found for Load Balancer on project %q\n", projectLabel) - return nil + + var credentials []loadbalancer.CredentialsResponse + if credentialsPtr != nil && len(*credentialsPtr) > 0 { + credentials = *credentialsPtr + filterOp, err := getFilterOp(model.Used, model.Unused) + if err != nil { + return err + } + credentials, err = utils.FilterCredentials(ctx, apiClient, credentials, model.ProjectId, filterOp) + if err != nil { + return fmt.Errorf("filter credentials: %w", err) + } } - credentials := *credentialsPtr + if len(credentials) == 0 { + opLabel := "No " + if model.Used { + opLabel += "used" + } else if model.Unused { + opLabel += "unused" + } + p.Info("%s observability credentials found for Load Balancer on project %q\n", opLabel, projectLabel) + return nil + } // Truncate output if model.Limit != nil && len(credentials) > int(*model.Limit) { @@ -92,6 +121,10 @@ func NewCmd(p *print.Printer) *cobra.Command { func configureFlags(cmd *cobra.Command) { cmd.Flags().Int64(limitFlag, 0, "Maximum number of entries to list") + cmd.Flags().Bool(usedFlag, false, "List only credentials being used by a Load Balancer") + cmd.Flags().Bool(unusedFlag, false, "List only credentials not being used by a Load Balancer") + + cmd.MarkFlagsMutuallyExclusive(usedFlag, unusedFlag) } func parseInput(p *print.Printer, cmd *cobra.Command) (*inputModel, error) { @@ -111,6 +144,8 @@ func parseInput(p *print.Printer, cmd *cobra.Command) (*inputModel, error) { model := inputModel{ GlobalFlagModel: globalFlags, Limit: limit, + Used: flags.FlagToBoolValue(p, cmd, usedFlag), + Unused: flags.FlagToBoolValue(p, cmd, unusedFlag), } if p.IsVerbosityDebug() { @@ -155,3 +190,20 @@ func outputResult(p *print.Printer, outputFormat string, credentials []loadbalan return nil } } + +func getFilterOp(used, unused bool) (int, error) { + // should not happen, cobra handles this + if used && unused { + return 0, fmt.Errorf("used and unused flags are mutually exclusive") + } + + if !used && !unused { + return utils.OP_FILTER_NOP, nil + } + + if used { + return utils.OP_FILTER_USED, nil + } + + return utils.OP_FILTER_UNUSED, nil +} diff --git a/internal/cmd/load-balancer/observability-credentials/list/list_test.go b/internal/cmd/load-balancer/observability-credentials/list/list_test.go index 9c564117e..736adf134 100644 --- a/internal/cmd/load-balancer/observability-credentials/list/list_test.go +++ b/internal/cmd/load-balancer/observability-credentials/list/list_test.go @@ -6,6 +6,7 @@ import ( "github.com/stackitcloud/stackit-cli/internal/pkg/globalflags" "github.com/stackitcloud/stackit-cli/internal/pkg/print" + lbUtils "github.com/stackitcloud/stackit-cli/internal/pkg/services/load-balancer/utils" "github.com/stackitcloud/stackit-cli/internal/pkg/utils" "github.com/google/go-cmp/cmp" @@ -108,6 +109,34 @@ func TestParseInput(t *testing.T) { }), isValid: false, }, + { + description: "used", + flagValues: fixtureFlagValues(func(flagValues map[string]string) { + flagValues[usedFlag] = "true" + }), + isValid: true, + expectedModel: fixtureInputModel(func(model *inputModel) { + model.Used = true + }), + }, + { + description: "unused", + flagValues: fixtureFlagValues(func(flagValues map[string]string) { + flagValues[unusedFlag] = "true" + }), + isValid: true, + expectedModel: fixtureInputModel(func(model *inputModel) { + model.Unused = true + }), + }, + { + description: "used and unused", + flagValues: fixtureFlagValues(func(flagValues map[string]string) { + flagValues[usedFlag] = "true" + flagValues[unusedFlag] = "true" + }), + isValid: false, + }, } for _, tt := range tests { @@ -137,6 +166,14 @@ func TestParseInput(t *testing.T) { t.Fatalf("error validating flags: %v", err) } + err = cmd.ValidateFlagGroups() + if err != nil { + if !tt.isValid { + return + } + t.Fatalf("error validating flags: %v", err) + } + model, err := parseInput(p, cmd) if err != nil { if !tt.isValid { @@ -183,3 +220,52 @@ func TestBuildRequest(t *testing.T) { }) } } + +func TestGetFilterOp(t *testing.T) { + tests := []struct { + description string + used bool + unused bool + expectedFilterOp int + isValid bool + }{ + { + description: "used", + used: true, + expectedFilterOp: lbUtils.OP_FILTER_USED, + isValid: true, + }, + { + description: "unused", + unused: true, + expectedFilterOp: lbUtils.OP_FILTER_UNUSED, + isValid: true, + }, + { + description: "used and unused", + used: true, + unused: true, + isValid: false, + }, + { + description: "neither used nor unused", + expectedFilterOp: lbUtils.OP_FILTER_NOP, + isValid: true, + }, + } + + for _, tt := range tests { + t.Run(tt.description, func(t *testing.T) { + filterOp, err := getFilterOp(tt.used, tt.unused) + if err != nil { + if !tt.isValid { + return + } + t.Fatalf("error getting filter op: %v", err) + } + if filterOp != tt.expectedFilterOp { + t.Fatalf("Data does not match: %d", filterOp) + } + }) + } +} diff --git a/internal/cmd/load-balancer/observability-credentials/observability-credentials.go b/internal/cmd/load-balancer/observability-credentials/observability-credentials.go index 1c7dcec9c..9ad8500d1 100644 --- a/internal/cmd/load-balancer/observability-credentials/observability-credentials.go +++ b/internal/cmd/load-balancer/observability-credentials/observability-credentials.go @@ -2,6 +2,7 @@ package credentials import ( "github.com/stackitcloud/stackit-cli/internal/cmd/load-balancer/observability-credentials/add" + "github.com/stackitcloud/stackit-cli/internal/cmd/load-balancer/observability-credentials/cleanup" "github.com/stackitcloud/stackit-cli/internal/cmd/load-balancer/observability-credentials/delete" "github.com/stackitcloud/stackit-cli/internal/cmd/load-balancer/observability-credentials/describe" "github.com/stackitcloud/stackit-cli/internal/cmd/load-balancer/observability-credentials/list" @@ -32,4 +33,5 @@ func addSubcommands(cmd *cobra.Command, p *print.Printer) { cmd.AddCommand(delete.NewCmd(p)) cmd.AddCommand(update.NewCmd(p)) cmd.AddCommand(list.NewCmd(p)) + cmd.AddCommand(cleanup.NewCmd(p)) } diff --git a/internal/cmd/load-balancer/target-pool/add-target/add_target_test.go b/internal/cmd/load-balancer/target-pool/add-target/add_target_test.go index 128e2c60a..8120dc497 100644 --- a/internal/cmd/load-balancer/target-pool/add-target/add_target_test.go +++ b/internal/cmd/load-balancer/target-pool/add-target/add_target_test.go @@ -57,6 +57,10 @@ func (m *loadBalancerClientMocked) UpdateTargetPool(ctx context.Context, project return testClient.UpdateTargetPool(ctx, projectId, loadBalancerName, targetPoolName) } +func (m *loadBalancerClientMocked) ListLoadBalancersExecute(_ context.Context, _ string) (*loadbalancer.ListLoadBalancersResponse, error) { + return nil, nil +} + func fixtureArgValues(mods ...func(argValues []string)) []string { argValues := []string{ testIP, diff --git a/internal/cmd/load-balancer/target-pool/remove-target/remove_target_test.go b/internal/cmd/load-balancer/target-pool/remove-target/remove_target_test.go index bec625abd..ec31f13e2 100644 --- a/internal/cmd/load-balancer/target-pool/remove-target/remove_target_test.go +++ b/internal/cmd/load-balancer/target-pool/remove-target/remove_target_test.go @@ -57,6 +57,10 @@ func (m *loadBalancerClientMocked) UpdateTargetPool(ctx context.Context, project return testClient.UpdateTargetPool(ctx, projectId, loadBalancerName, targetPoolName) } +func (m *loadBalancerClientMocked) ListLoadBalancersExecute(_ context.Context, _ string) (*loadbalancer.ListLoadBalancersResponse, error) { + return nil, nil +} + func fixtureArgValues(mods ...func(argValues []string)) []string { argValues := []string{ testIP, diff --git a/internal/cmd/mongodbflex/options/options.go b/internal/cmd/mongodbflex/options/options.go index 5811b0c09..217756194 100644 --- a/internal/cmd/mongodbflex/options/options.go +++ b/internal/cmd/mongodbflex/options/options.go @@ -222,6 +222,7 @@ func renderFlavors(flavors []mongodbflex.HandlersInfraFlavor) string { } table := tables.NewTable() + table.SetTitle("Flavors") table.SetHeader("ID", "CPU", "MEMORY", "DESCRIPTION", "VALID INSTANCE TYPES") for i := range flavors { f := flavors[i] @@ -236,6 +237,7 @@ func renderVersions(versions []string) string { } table := tables.NewTable() + table.SetTitle("Versions") table.SetHeader("VERSION") for i := range versions { v := versions[i] @@ -251,7 +253,8 @@ func renderStorages(resp *mongodbflex.ListStoragesResponse) string { storageClasses := *resp.StorageClasses table := tables.NewTable() - table.SetHeader("MIN STORAGE", "MAX STORAGE", "STORAGE CLASS") + table.SetTitle("Storages") + table.SetHeader("MINIMUM", "MAXIMUM", "STORAGE CLASS") for i := range storageClasses { sc := storageClasses[i] table.AddRow(*resp.StorageRange.Min, *resp.StorageRange.Max, sc) diff --git a/internal/cmd/postgresflex/options/options.go b/internal/cmd/postgresflex/options/options.go index c245a453c..7341f4ea3 100644 --- a/internal/cmd/postgresflex/options/options.go +++ b/internal/cmd/postgresflex/options/options.go @@ -222,6 +222,7 @@ func renderFlavors(flavors []postgresflex.Flavor) string { } table := tables.NewTable() + table.SetTitle("Flavors") table.SetHeader("ID", "CPU", "MEMORY", "DESCRIPTION") for i := range flavors { f := flavors[i] @@ -236,6 +237,7 @@ func renderVersions(versions []string) string { } table := tables.NewTable() + table.SetTitle("Versions") table.SetHeader("VERSION") for i := range versions { v := versions[i] @@ -251,7 +253,8 @@ func renderStorages(resp *postgresflex.ListStoragesResponse) string { storageClasses := *resp.StorageClasses table := tables.NewTable() - table.SetHeader("MIN STORAGE", "MAX STORAGE", "STORAGE CLASS") + table.SetTitle("Storages") + table.SetHeader("MINIMUM", "MAXIMUM", "STORAGE CLASS") for i := range storageClasses { sc := storageClasses[i] table.AddRow(*resp.StorageRange.Min, *resp.StorageRange.Max, sc) diff --git a/internal/cmd/root.go b/internal/cmd/root.go index 93b88ad1e..8f4193142 100644 --- a/internal/cmd/root.go +++ b/internal/cmd/root.go @@ -53,7 +53,7 @@ func NewRootCmd(version, date string, p *print.Printer) *cobra.Command { p.Debug(print.DebugLevel, "arguments: %s", argsString) configFilePath := viper.ConfigFileUsed() - p.Debug(print.DebugLevel, "using config file: %s", configFilePath) + p.Debug(print.DebugLevel, "configuration is persisted and read from: %s", configFilePath) activeProfile, err := config.GetProfile() if err != nil { @@ -62,11 +62,11 @@ func NewRootCmd(version, date string, p *print.Printer) *cobra.Command { if activeProfile == "" { activeProfile = "(no active profile, the default profile configuration will be used)" } - p.Debug(print.DebugLevel, "active config profile: %s", activeProfile) + p.Debug(print.DebugLevel, "active configuration profile: %s", activeProfile) configKeys := viper.AllSettings() configKeysStr := print.BuildDebugStrFromMap(configKeys) - p.Debug(print.DebugLevel, "config keys: %s", configKeysStr) + p.Debug(print.DebugLevel, "configuration keys: %s", configKeysStr) return nil }, diff --git a/internal/cmd/ske/options/options.go b/internal/cmd/ske/options/options.go index 74b651ab2..b41682e08 100644 --- a/internal/cmd/ske/options/options.go +++ b/internal/cmd/ske/options/options.go @@ -181,7 +181,8 @@ func renderAvailabilityZones(resp *ske.ProviderOptions) string { zones := *resp.AvailabilityZones table := tables.NewTable() - table.SetHeader("AVAILABILITY ZONES") + table.SetTitle("Availability Zones") + table.SetHeader("ZONE") for i := range zones { z := zones[i] table.AddRow(*z.Name) @@ -193,7 +194,8 @@ func renderKubernetesVersions(resp *ske.ProviderOptions) (string, error) { versions := *resp.KubernetesVersions table := tables.NewTable() - table.SetHeader("KUBERNETES VERSION", "STATE", "EXPIRATION DATE", "FEATURE GATES") + table.SetTitle("Kubernetes Versions") + table.SetHeader("VERSION", "STATE", "EXPIRATION DATE", "FEATURE GATES") for i := range versions { v := versions[i] featureGate, err := json.Marshal(*v.FeatureGates) @@ -213,7 +215,8 @@ func renderMachineImages(resp *ske.ProviderOptions) string { images := *resp.MachineImages table := tables.NewTable() - table.SetHeader("MACHINE IMAGE NAME", "VERSION", "STATE", "EXPIRATION DATE", "SUPPORTED CRI") + table.SetTitle("Machine Images") + table.SetHeader("NAME", "VERSION", "STATE", "EXPIRATION DATE", "SUPPORTED CRI") for i := range images { image := images[i] versions := *image.Versions @@ -241,7 +244,8 @@ func renderMachineTypes(resp *ske.ProviderOptions) string { types := *resp.MachineTypes table := tables.NewTable() - table.SetHeader("MACHINE TYPE", "CPU", "MEMORY") + table.SetTitle("Machine Types") + table.SetHeader("TYPE", "CPU", "MEMORY") for i := range types { t := types[i] table.AddRow(*t.Name, *t.Cpu, *t.Memory) @@ -253,7 +257,8 @@ func renderVolumeTypes(resp *ske.ProviderOptions) string { types := *resp.VolumeTypes table := tables.NewTable() - table.SetHeader("VOLUME TYPE") + table.SetTitle("Volume Types") + table.SetHeader("TYPE") for i := range types { z := types[i] table.AddRow(*z.Name) diff --git a/internal/pkg/config/config.go b/internal/pkg/config/config.go index 209710e08..8982352a8 100644 --- a/internal/pkg/config/config.go +++ b/internal/pkg/config/config.go @@ -76,6 +76,7 @@ var ConfigKeys = []string{ SKECustomEndpointKey, } +var defaultConfigFolderPath string var configFolderPath string var profileFilePath string @@ -83,12 +84,13 @@ func InitConfig() { configDir, err := os.UserConfigDir() cobra.CheckErr(err) - configFolderPath = filepath.Join(configDir, configFolder) // Default config folder - profileFilePath = filepath.Join(configFolderPath, fmt.Sprintf("%s.%s", profileFileName, profileFileExtension)) // Profile file path is in the default config folder + defaultConfigFolderPath = filepath.Join(configDir, configFolder) + profileFilePath = filepath.Join(defaultConfigFolderPath, fmt.Sprintf("%s.%s", profileFileName, profileFileExtension)) // Profile file path is in the default config folder configProfile, err := GetProfile() cobra.CheckErr(err) + configFolderPath = defaultConfigFolderPath if configProfile != "" { configFolderPath = filepath.Join(configFolderPath, configProfile) // If a profile is set, use the profile config folder } diff --git a/internal/pkg/config/profiles.go b/internal/pkg/config/profiles.go index c581b95a1..219fa879c 100644 --- a/internal/pkg/config/profiles.go +++ b/internal/pkg/config/profiles.go @@ -7,6 +7,7 @@ import ( "regexp" "github.com/stackitcloud/stackit-cli/internal/pkg/errors" + "github.com/stackitcloud/stackit-cli/internal/pkg/print" ) // GetProfile returns the current profile to be used by the CLI. @@ -38,27 +39,37 @@ func GetProfile() (string, error) { } // SetProfile sets the profile to be used by the CLI. -func SetProfile(profile string) error { +func SetProfile(p *print.Printer, profile string) error { err := ValidateProfile(profile) if err != nil { return fmt.Errorf("validate profile: %w", err) } - profileFilePath := filepath.Join(configFolderPath, fmt.Sprintf("%s.%s", profileFileName, profileFileExtension)) err = os.WriteFile(profileFilePath, []byte(profile), os.ModePerm) if err != nil { return fmt.Errorf("write profile to file: %w", err) } + p.Debug(print.DebugLevel, "persisted new active profile in: %s", profileFilePath) + + configFolderPath = filepath.Join(defaultConfigFolderPath, profile) + err = createFolderIfNotExists(configFolderPath) + if err != nil { + return fmt.Errorf("create config folder: %w", err) + } + p.Debug(print.DebugLevel, "created folder for the new profile: %s", configFolderPath) + p.Debug(print.DebugLevel, "profile %q is now active", profile) + return nil } // UnsetProfile removes the profile file. // If the profile file does not exist, it does nothing. -func UnsetProfile() error { +func UnsetProfile(p *print.Printer) error { err := os.Remove(profileFilePath) if err != nil && !os.IsNotExist(err) { return fmt.Errorf("remove profile file: %w", err) } + p.Debug(print.DebugLevel, "removed active profile file: %s", profileFilePath) return nil } diff --git a/internal/pkg/print/print.go b/internal/pkg/print/print.go index 2bdd483cc..f9f7ce00a 100644 --- a/internal/pkg/print/print.go +++ b/internal/pkg/print/print.go @@ -15,7 +15,7 @@ import ( "github.com/mattn/go-colorable" "github.com/spf13/cobra" "github.com/spf13/viper" - "github.com/stackitcloud/stackit-cli/internal/pkg/config" + "golang.org/x/term" ) @@ -27,6 +27,10 @@ const ( WarningLevel Level = "warning" ErrorLevel Level = "error" + // Needed to avoid import cycle + // Originally defined in "internal/pkg/config/config.go" + outputFormatKey = "output-format" + JSONOutputFormat = "json" PrettyOutputFormat = "pretty" NoneOutputFormat = "none" @@ -53,7 +57,7 @@ func NewPrinter() *Printer { // Print an output using Printf to the defined output (falling back to Stderr if not set). // If output format is set to none, it does nothing func (p *Printer) Outputf(msg string, args ...any) { - outputFormat := viper.GetString(config.OutputFormatKey) + outputFormat := viper.GetString(outputFormatKey) if outputFormat == NoneOutputFormat { return } @@ -63,7 +67,7 @@ func (p *Printer) Outputf(msg string, args ...any) { // Print an output using Println to the defined output (falling back to Stderr if not set). // If output format is set to none, it does nothing func (p *Printer) Outputln(msg string) { - outputFormat := viper.GetString(config.OutputFormatKey) + outputFormat := viper.GetString(outputFormatKey) if outputFormat == NoneOutputFormat { return } @@ -167,11 +171,17 @@ func (p *Printer) PromptForPassword(prompt string) (string, error) { // Shows the content in the command's stdout using the "less" command // If output format is set to none, it does nothing func (p *Printer) PagerDisplay(content string) error { - outputFormat := viper.GetString(config.OutputFormatKey) + outputFormat := viper.GetString(outputFormatKey) if outputFormat == NoneOutputFormat { return nil } - pagerCmd := exec.Command("less", "-F", "-S", "-w") + + // less arguments + // -F: exits if the entire file fits on the first screen + // -S: disables line wrapping + // -w: highlight the first line after moving one full page down + // -R: interprets ANSI color and style sequences + pagerCmd := exec.Command("less", "-F", "-S", "-w", "-R") pagerCmd.Stdin = strings.NewReader(content) pagerCmd.Stdout = p.Cmd.OutOrStdout() diff --git a/internal/pkg/print/print_test.go b/internal/pkg/print/print_test.go index 712288639..f5841de37 100644 --- a/internal/pkg/print/print_test.go +++ b/internal/pkg/print/print_test.go @@ -10,7 +10,6 @@ import ( "github.com/spf13/cobra" "github.com/spf13/viper" - "github.com/stackitcloud/stackit-cli/internal/pkg/config" ) func TestOutputf(t *testing.T) { @@ -66,7 +65,7 @@ func TestOutputf(t *testing.T) { viper.Reset() if tt.outputFormatNone { - viper.Set(config.OutputFormatKey, NoneOutputFormat) + viper.Set(outputFormatKey, NoneOutputFormat) } if len(tt.args) == 0 { @@ -137,7 +136,7 @@ func TestOutputln(t *testing.T) { viper.Reset() if tt.outputFormatNone { - viper.Set(config.OutputFormatKey, NoneOutputFormat) + viper.Set(outputFormatKey, NoneOutputFormat) } p.Outputln(tt.message) @@ -201,7 +200,7 @@ func TestPagerDisplay(t *testing.T) { viper.Reset() if tt.outputFormatNone { - viper.Set(config.OutputFormatKey, NoneOutputFormat) + viper.Set(outputFormatKey, NoneOutputFormat) } err := p.PagerDisplay(tt.content) diff --git a/internal/pkg/services/load-balancer/utils/utils.go b/internal/pkg/services/load-balancer/utils/utils.go index 0f173b251..1ad93ce18 100644 --- a/internal/pkg/services/load-balancer/utils/utils.go +++ b/internal/pkg/services/load-balancer/utils/utils.go @@ -3,14 +3,23 @@ package utils import ( "context" "fmt" + "slices" + "sort" "github.com/stackitcloud/stackit-sdk-go/services/loadbalancer" ) +const ( + OP_FILTER_NOP = iota + OP_FILTER_USED + OP_FILTER_UNUSED +) + type LoadBalancerClient interface { GetCredentialsExecute(ctx context.Context, projectId, credentialsRef string) (*loadbalancer.GetCredentialsResponse, error) GetLoadBalancerExecute(ctx context.Context, projectId, name string) (*loadbalancer.LoadBalancer, error) UpdateTargetPool(ctx context.Context, projectId, loadBalancerName, targetPoolName string) loadbalancer.ApiUpdateTargetPoolRequest + ListLoadBalancersExecute(ctx context.Context, projectId string) (*loadbalancer.ListLoadBalancersResponse, error) } func GetCredentialsDisplayName(ctx context.Context, apiClient LoadBalancerClient, projectId, credentialsRef string) (string, error) { @@ -130,3 +139,102 @@ func GetTargetName(ctx context.Context, apiClient LoadBalancerClient, projectId, } return "", fmt.Errorf("target not found") } + +// GetUsedObsCredentials returns a list of credentials that are used by load balancers for observability metrics or logs. +// It goes through all load balancers and checks what observability credentials are being used, then returns a list of those credentials. +func GetUsedObsCredentials(ctx context.Context, apiClient LoadBalancerClient, allCredentials []loadbalancer.CredentialsResponse, projectId string) ([]loadbalancer.CredentialsResponse, error) { + var usedCredentialsSlice []loadbalancer.CredentialsResponse + + loadBalancers, err := apiClient.ListLoadBalancersExecute(ctx, projectId) + if err != nil { + return nil, fmt.Errorf("list load balancers: %w", err) + } + if loadBalancers == nil || loadBalancers.LoadBalancers == nil { + return usedCredentialsSlice, nil + } + + var usedCredentialsRefs []string + for _, loadBalancer := range *loadBalancers.LoadBalancers { + if loadBalancer.Options == nil || loadBalancer.Options.Observability == nil { + continue + } + + if loadBalancer.Options != nil && loadBalancer.Options.Observability != nil && loadBalancer.Options.Observability.Logs != nil && loadBalancer.Options.Observability.Logs.CredentialsRef != nil { + usedCredentialsRefs = append(usedCredentialsRefs, *loadBalancer.Options.Observability.Logs.CredentialsRef) + } + if loadBalancer.Options != nil && loadBalancer.Options.Observability != nil && loadBalancer.Options.Observability.Metrics != nil && loadBalancer.Options.Observability.Metrics.CredentialsRef != nil { + usedCredentialsRefs = append(usedCredentialsRefs, *loadBalancer.Options.Observability.Metrics.CredentialsRef) + } + } + + usedCredentialsMap := make(map[string]loadbalancer.CredentialsResponse) + for _, credential := range allCredentials { + if credential.CredentialsRef == nil { + continue + } + ref := *credential.CredentialsRef + if slices.Contains(usedCredentialsRefs, ref) { + usedCredentialsMap[ref] = credential + } + } + + for _, credential := range usedCredentialsMap { + usedCredentialsSlice = append(usedCredentialsSlice, credential) + } + + // sort credentials by reference to make output deterministic + sort.Slice(usedCredentialsSlice, func(i, j int) bool { + return *usedCredentialsSlice[i].CredentialsRef < *usedCredentialsSlice[j].CredentialsRef + }) + + return usedCredentialsSlice, nil +} + +// GetUnusedObsCredentials returns a list of credentials that are not used by any load balancer for observability metrics or logs. +// It compares the list of all credentials with the list of used credentials and returns a list of credentials that are not used. +func GetUnusedObsCredentials(usedCredentials, allCredentials []loadbalancer.CredentialsResponse) []loadbalancer.CredentialsResponse { + var unusedCredentials []loadbalancer.CredentialsResponse + usedCredentialsRefs := make(map[string]bool) + for _, credential := range usedCredentials { + if credential.CredentialsRef != nil { + usedCredentialsRefs[*credential.CredentialsRef] = true + } + } + + for _, credential := range allCredentials { + if credential.CredentialsRef == nil { + continue + } + if !usedCredentialsRefs[*credential.CredentialsRef] { + unusedCredentials = append(unusedCredentials, credential) + } + } + + return unusedCredentials +} + +// FilterCredentials filters a list of credentials based on the used and unused flags. +// If used is true, it returns only the credentials that are used by load balancers for observability metrics or logs. +// If unused is true, it returns only the credentials that are not used by any load balancer for observability metrics or logs. +// If both used and unused are true, it returns an error. +// If both used and unused are false, it returns the original list of credentials. +func FilterCredentials(ctx context.Context, client LoadBalancerClient, allCredentials []loadbalancer.CredentialsResponse, projectId string, filterOp int) ([]loadbalancer.CredentialsResponse, error) { + // check that filter OP is valid + if filterOp != OP_FILTER_USED && filterOp != OP_FILTER_UNUSED && filterOp != OP_FILTER_NOP { + return nil, fmt.Errorf("invalid filter operation") + } + + if filterOp == OP_FILTER_NOP { + return allCredentials, nil + } + + usedCredentials, err := GetUsedObsCredentials(ctx, client, allCredentials, projectId) + if err != nil { + return nil, fmt.Errorf("get used observability credentials: %w", err) + } + + if filterOp == OP_FILTER_UNUSED { + return GetUnusedObsCredentials(usedCredentials, allCredentials), nil + } + return usedCredentials, nil +} diff --git a/internal/pkg/services/load-balancer/utils/utils_test.go b/internal/pkg/services/load-balancer/utils/utils_test.go index 2d4d96c0d..93b8f6ff3 100644 --- a/internal/pkg/services/load-balancer/utils/utils_test.go +++ b/internal/pkg/services/load-balancer/utils/utils_test.go @@ -15,6 +15,7 @@ import ( var ( testProjectId = uuid.NewString() + testCtx = context.Background() ) const ( @@ -24,10 +25,12 @@ const ( ) type loadBalancerClientMocked struct { - getCredentialsFails bool - getCredentialsResp *loadbalancer.GetCredentialsResponse - getLoadBalancerFails bool - getLoadBalancerResp *loadbalancer.LoadBalancer + getCredentialsFails bool + getCredentialsResp *loadbalancer.GetCredentialsResponse + getLoadBalancerFails bool + getLoadBalancerResp *loadbalancer.LoadBalancer + listLoadBalancersFails bool + listLoadBalancersResp *loadbalancer.ListLoadBalancersResponse } func (m *loadBalancerClientMocked) GetCredentialsExecute(_ context.Context, _, _ string) (*loadbalancer.GetCredentialsResponse, error) { @@ -44,6 +47,13 @@ func (m *loadBalancerClientMocked) GetLoadBalancerExecute(_ context.Context, _, return m.getLoadBalancerResp, nil } +func (m *loadBalancerClientMocked) ListLoadBalancersExecute(_ context.Context, _ string) (*loadbalancer.ListLoadBalancersResponse, error) { + if m.listLoadBalancersFails { + return nil, fmt.Errorf("could not list load balancers") + } + return m.listLoadBalancersResp, nil +} + func (m *loadBalancerClientMocked) UpdateTargetPool(_ context.Context, _, _, _ string) loadbalancer.ApiUpdateTargetPoolRequest { return loadbalancer.ApiUpdateTargetPoolRequest{} } @@ -79,6 +89,18 @@ func fixtureLoadBalancer(mods ...func(*loadbalancer.LoadBalancer)) *loadbalancer }, }, }, + Options: &loadbalancer.LoadBalancerOptions{ + Observability: &loadbalancer.LoadbalancerOptionObservability{ + Logs: &loadbalancer.LoadbalancerOptionLogs{ + CredentialsRef: utils.Ptr("credentials-ref-1"), + PushUrl: utils.Ptr("https://logs.stackit.cloud"), + }, + Metrics: &loadbalancer.LoadbalancerOptionMetrics{ + CredentialsRef: utils.Ptr("credentials-ref-2"), + PushUrl: utils.Ptr("https://metrics.stackit.cloud"), + }, + }, + }, } for _, mod := range mods { @@ -87,6 +109,32 @@ func fixtureLoadBalancer(mods ...func(*loadbalancer.LoadBalancer)) *loadbalancer return &lb } +func fixtureCredentials(mod ...func([]loadbalancer.CredentialsResponse)) []loadbalancer.CredentialsResponse { + credentials := []loadbalancer.CredentialsResponse{ + { + CredentialsRef: utils.Ptr("credentials-ref-1"), + DisplayName: utils.Ptr("credentials-1"), + Username: utils.Ptr("user-1"), + }, + { + CredentialsRef: utils.Ptr("credentials-ref-2"), + DisplayName: utils.Ptr("credentials-2"), + Username: utils.Ptr("user-2"), + }, + { + CredentialsRef: utils.Ptr("credentials-ref-3"), + DisplayName: utils.Ptr("credentials-3"), + Username: utils.Ptr("user-3"), + }, + } + + for _, m := range mod { + m(credentials) + } + + return credentials +} + func fixtureTargets(mod ...func(*[]loadbalancer.Target)) *[]loadbalancer.Target { targets := &[]loadbalancer.Target{ { @@ -793,3 +841,316 @@ func TestGetTargetName(t *testing.T) { }) } } + +func TestGetUsedObsCredentials(t *testing.T) { + tests := []struct { + description string + allCredentials []loadbalancer.CredentialsResponse + listLoadBalancersFails bool + listLoadBalancersResp *loadbalancer.ListLoadBalancersResponse + isValid bool + expectedOutput []loadbalancer.CredentialsResponse + }{ + { + description: "base", + allCredentials: fixtureCredentials(), + listLoadBalancersResp: &loadbalancer.ListLoadBalancersResponse{ + LoadBalancers: &[]loadbalancer.LoadBalancer{ + *fixtureLoadBalancer(), + }, + }, + isValid: true, + expectedOutput: []loadbalancer.CredentialsResponse{ + { + DisplayName: utils.Ptr("credentials-1"), + CredentialsRef: utils.Ptr("credentials-ref-1"), + Username: utils.Ptr("user-1"), + }, + { + DisplayName: utils.Ptr("credentials-2"), + CredentialsRef: utils.Ptr("credentials-ref-2"), + Username: utils.Ptr("user-2"), + }, + }, + }, + { + description: "repeated credentials in different load balancers", + allCredentials: fixtureCredentials(), + listLoadBalancersResp: &loadbalancer.ListLoadBalancersResponse{ + LoadBalancers: &[]loadbalancer.LoadBalancer{ + *fixtureLoadBalancer(), + *fixtureLoadBalancer(), + }, + }, + isValid: true, + expectedOutput: []loadbalancer.CredentialsResponse{ + { + DisplayName: utils.Ptr("credentials-1"), + CredentialsRef: utils.Ptr("credentials-ref-1"), + Username: utils.Ptr("user-1"), + }, + { + DisplayName: utils.Ptr("credentials-2"), + CredentialsRef: utils.Ptr("credentials-ref-2"), + Username: utils.Ptr("user-2"), + }, + }, + }, + { + description: "no repeated credentials in different load balancers", + allCredentials: fixtureCredentials(), + listLoadBalancersResp: &loadbalancer.ListLoadBalancersResponse{ + LoadBalancers: &[]loadbalancer.LoadBalancer{ + *fixtureLoadBalancer(), + *fixtureLoadBalancer(func(lb *loadbalancer.LoadBalancer) { + lb.Options.Observability.Logs.CredentialsRef = utils.Ptr("credentials-ref-3") + lb.Options.Observability.Metrics.CredentialsRef = utils.Ptr("credentials-ref-3") + }), + }, + }, + isValid: true, + expectedOutput: fixtureCredentials(), + }, + { + description: "no load balancers, no credentials", + listLoadBalancersResp: &loadbalancer.ListLoadBalancersResponse{}, + isValid: true, + expectedOutput: nil, + }, + { + description: "no load balancers", + allCredentials: fixtureCredentials(), + listLoadBalancersResp: &loadbalancer.ListLoadBalancersResponse{}, + isValid: true, + expectedOutput: nil, + }, + { + description: "no credentials", + allCredentials: []loadbalancer.CredentialsResponse{}, + listLoadBalancersResp: &loadbalancer.ListLoadBalancersResponse{ + LoadBalancers: &[]loadbalancer.LoadBalancer{ + *fixtureLoadBalancer(), + }, + }, + isValid: true, + expectedOutput: nil, + }, + { + description: "list load balancers fails", + listLoadBalancersFails: true, + isValid: false, + }, + { + description: "no observability options", + allCredentials: fixtureCredentials(), + listLoadBalancersResp: &loadbalancer.ListLoadBalancersResponse{ + LoadBalancers: &[]loadbalancer.LoadBalancer{ + *fixtureLoadBalancer(func(lb *loadbalancer.LoadBalancer) { + lb.Options = nil + }), + }, + }, + isValid: true, + expectedOutput: nil, + }, + } + + for _, tt := range tests { + t.Run(tt.description, func(t *testing.T) { + client := &loadBalancerClientMocked{ + listLoadBalancersFails: tt.listLoadBalancersFails, + listLoadBalancersResp: tt.listLoadBalancersResp, + } + + output, err := GetUsedObsCredentials(testCtx, client, tt.allCredentials, testProjectId) + + if tt.isValid && err != nil { + t.Errorf("failed on valid input") + } + if !tt.isValid && err == nil { + t.Errorf("did not fail on invalid input") + } + if !tt.isValid { + return + } + diff := cmp.Diff(output, tt.expectedOutput) + if diff != "" { + t.Fatalf("Data does not match: %s", diff) + } + }) + } +} + +func TestGetUnusedObsCredentials(t *testing.T) { + tests := []struct { + description string + allCredentials []loadbalancer.CredentialsResponse + usedCredentials []loadbalancer.CredentialsResponse + isValid bool + expectedOutput []loadbalancer.CredentialsResponse + }{ + { + description: "base", + allCredentials: fixtureCredentials(), + usedCredentials: []loadbalancer.CredentialsResponse{ + { + DisplayName: utils.Ptr("credentials-1"), + CredentialsRef: utils.Ptr("credentials-ref-1"), + Username: utils.Ptr("user-1"), + }, + }, + isValid: true, + expectedOutput: []loadbalancer.CredentialsResponse{ + { + DisplayName: utils.Ptr("credentials-2"), + CredentialsRef: utils.Ptr("credentials-ref-2"), + Username: utils.Ptr("user-2"), + }, + { + DisplayName: utils.Ptr("credentials-3"), + CredentialsRef: utils.Ptr("credentials-ref-3"), + Username: utils.Ptr("user-3"), + }, + }, + }, + { + description: "no used credentials", + allCredentials: fixtureCredentials(), + usedCredentials: nil, + isValid: true, + expectedOutput: fixtureCredentials(), + }, + { + description: "no credentials", + allCredentials: []loadbalancer.CredentialsResponse{}, + usedCredentials: []loadbalancer.CredentialsResponse{ + { + DisplayName: utils.Ptr("credentials-1"), + CredentialsRef: utils.Ptr("credentials-ref-1"), + Username: utils.Ptr("user-1"), + }, + }, + isValid: true, + expectedOutput: nil, + }, + { + description: "no used credentials, no credentials", + allCredentials: []loadbalancer.CredentialsResponse{}, + usedCredentials: nil, + isValid: true, + expectedOutput: nil, + }, + } + + for _, tt := range tests { + t.Run(tt.description, func(t *testing.T) { + output := GetUnusedObsCredentials(tt.usedCredentials, tt.allCredentials) + + diff := cmp.Diff(output, tt.expectedOutput) + if diff != "" { + t.Fatalf("Data does not match: %s", diff) + } + }) + } +} + +func TestFilterCredentials(t *testing.T) { + tests := []struct { + description string + filterOp int + allCredentials []loadbalancer.CredentialsResponse + listLoadBalancersResp *loadbalancer.ListLoadBalancersResponse + listLoadBalancersFails bool + expectedCredentials []loadbalancer.CredentialsResponse + isValid bool + }{ + { + description: "unfiltered credentials", + filterOp: OP_FILTER_NOP, + allCredentials: fixtureCredentials(), + expectedCredentials: fixtureCredentials(), + isValid: true, + }, + { + description: "used credentials", + filterOp: OP_FILTER_USED, + allCredentials: fixtureCredentials(), + listLoadBalancersResp: &loadbalancer.ListLoadBalancersResponse{ + LoadBalancers: &[]loadbalancer.LoadBalancer{ + *fixtureLoadBalancer(), + }, + }, + expectedCredentials: []loadbalancer.CredentialsResponse{ + { + CredentialsRef: utils.Ptr("credentials-ref-1"), + DisplayName: utils.Ptr("credentials-1"), + Username: utils.Ptr("user-1"), + }, + { + CredentialsRef: utils.Ptr("credentials-ref-2"), + DisplayName: utils.Ptr("credentials-2"), + Username: utils.Ptr("user-2"), + }, + }, + isValid: true, + }, + { + description: "unused credentials", + filterOp: OP_FILTER_UNUSED, + allCredentials: fixtureCredentials(), + listLoadBalancersResp: &loadbalancer.ListLoadBalancersResponse{ + LoadBalancers: &[]loadbalancer.LoadBalancer{ + *fixtureLoadBalancer(), + }, + }, + expectedCredentials: []loadbalancer.CredentialsResponse{ + { + CredentialsRef: utils.Ptr("credentials-ref-3"), + DisplayName: utils.Ptr("credentials-3"), + Username: utils.Ptr("user-3"), + }, + }, + isValid: true, + }, + { + description: "no credentials", + filterOp: OP_FILTER_NOP, + allCredentials: []loadbalancer.CredentialsResponse{}, + expectedCredentials: []loadbalancer.CredentialsResponse{}, + isValid: true, + }, + { + description: "list load balancers fails", + filterOp: OP_FILTER_USED, + listLoadBalancersFails: true, + isValid: false, + }, + { + description: "invalid filter operation", + filterOp: 999, + allCredentials: fixtureCredentials(), + isValid: false, + }, + } + + for _, tt := range tests { + t.Run(tt.description, func(t *testing.T) { + client := &loadBalancerClientMocked{ + listLoadBalancersResp: tt.listLoadBalancersResp, + listLoadBalancersFails: tt.listLoadBalancersFails, + } + filteredCredentials, err := FilterCredentials(testCtx, client, tt.allCredentials, testProjectId, tt.filterOp) + if err != nil { + if !tt.isValid { + return + } + t.Fatalf("error filtering credentials: %v", err) + } + + diff := cmp.Diff(filteredCredentials, tt.expectedCredentials) + if diff != "" { + t.Fatalf("Data does not match: %s", diff) + } + }) + } +} diff --git a/internal/pkg/tables/tables.go b/internal/pkg/tables/tables.go index a60de1fc8..de8e0b397 100644 --- a/internal/pkg/tables/tables.go +++ b/internal/pkg/tables/tables.go @@ -6,6 +6,7 @@ import ( "github.com/stackitcloud/stackit-cli/internal/pkg/print" "github.com/jedib0t/go-pretty/v6/table" + "github.com/jedib0t/go-pretty/v6/text" ) type Table struct { @@ -20,6 +21,20 @@ func NewTable() Table { } } +// Sets the title of the table +func (t *Table) SetTitle(title string) { + t.table.SetTitle(title) + + // prevent title wrapping by setting the width of the first column to the length of the title + // this is a workaround for a bug in the tables pkg, see https://github.com/jedib0t/go-pretty/issues/135 + t.table.SetColumnConfigs([]table.ColumnConfig{ + { + Number: 1, + WidthMin: len(title), + }, + }) +} + // Sets the header of the table func (t *Table) SetHeader(header ...interface{}) { t.table.AppendHeader(table.Row(header)) @@ -47,6 +62,10 @@ func (t *Table) EnableAutoMergeOnColumns(columns ...int) { // Returns the table rendered func (t *Table) Render() string { t.table.SetStyle(table.StyleLight) + + t.table.Style().Title = table.TitleOptionsBlackOnCyan + t.table.Style().Title.Align = text.AlignCenter + t.table.Style().Options.DrawBorder = false t.table.Style().Options.SeparateRows = false t.table.Style().Options.SeparateColumns = true From adcbc5ff4196e769fc8514a90e7f65fccf910131 Mon Sep 17 00:00:00 2001 From: GokceGK <161626272+GokceGK@users.noreply.github.com> Date: Mon, 20 May 2024 11:06:47 +0200 Subject: [PATCH 05/18] Implement profiles for auth settings (#326) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * add profiles for auth settings * adapt unit tests * Use filepath.Join for keyring path too --------- Co-authored-by: João Palet --- internal/pkg/auth/storage.go | 59 +++++++++++++++++++++++++++++-- internal/pkg/auth/storage_test.go | 25 ++++++++++++- 2 files changed, 80 insertions(+), 4 deletions(-) diff --git a/internal/pkg/auth/storage.go b/internal/pkg/auth/storage.go index 9d08ca070..de08f25d5 100644 --- a/internal/pkg/auth/storage.go +++ b/internal/pkg/auth/storage.go @@ -7,6 +7,8 @@ import ( "os" "path/filepath" + "github.com/stackitcloud/stackit-cli/internal/pkg/config" + "github.com/zalando/go-keyring" ) @@ -69,6 +71,15 @@ func SetAuthField(key authFieldKey, value string) error { } func setAuthFieldInKeyring(key authFieldKey, value string) error { + activeProfile, err := config.GetProfile() + if err != nil { + return fmt.Errorf("get profile: %w", err) + } + + if activeProfile != "" { + activeProfileKeyring := filepath.Join(keyringService, activeProfile) + return keyring.Set(activeProfileKeyring, string(key), value) + } return keyring.Set(keyringService, string(key), value) } @@ -82,7 +93,18 @@ func setAuthFieldInEncodedTextFile(key authFieldKey, value string) error { if err != nil { return fmt.Errorf("get config dir: %w", err) } - textFileDir := filepath.Join(configDir, textFileFolderName) + + activeProfile, err := config.GetProfile() + if err != nil { + return fmt.Errorf("get profile: %w", err) + } + + profileTextFileFolderName := textFileFolderName + if activeProfile != "" { + profileTextFileFolderName = filepath.Join(activeProfile, textFileFolderName) + } + + textFileDir := filepath.Join(configDir, profileTextFileFolderName) textFilePath := filepath.Join(textFileDir, textFileName) contentEncoded, err := os.ReadFile(textFilePath) @@ -143,6 +165,15 @@ func GetAuthField(key authFieldKey) (string, error) { } func getAuthFieldFromKeyring(key authFieldKey) (string, error) { + activeProfile, err := config.GetProfile() + if err != nil { + return "", fmt.Errorf("get profile: %w", err) + } + + if activeProfile != "" { + activeProfileKeyring := filepath.Join(keyringService, activeProfile) + return keyring.Get(activeProfileKeyring, string(key)) + } return keyring.Get(keyringService, string(key)) } @@ -156,7 +187,18 @@ func getAuthFieldFromEncodedTextFile(key authFieldKey) (string, error) { if err != nil { return "", fmt.Errorf("get config dir: %w", err) } - textFileDir := filepath.Join(configDir, textFileFolderName) + + activeProfile, err := config.GetProfile() + if err != nil { + return "", fmt.Errorf("get profile: %w", err) + } + + profileTextFileFolderName := textFileFolderName + if activeProfile != "" { + profileTextFileFolderName = filepath.Join(activeProfile, textFileFolderName) + } + + textFileDir := filepath.Join(configDir, profileTextFileFolderName) textFilePath := filepath.Join(textFileDir, textFileName) contentEncoded, err := os.ReadFile(textFilePath) @@ -187,7 +229,18 @@ func createEncodedTextFile() error { if err != nil { return fmt.Errorf("get config dir: %w", err) } - textFileDir := filepath.Join(configDir, textFileFolderName) + + activeProfile, err := config.GetProfile() + if err != nil { + return fmt.Errorf("get profile: %w", err) + } + + profileTextFileFolderName := textFileFolderName + if activeProfile != "" { + profileTextFileFolderName = filepath.Join(activeProfile, textFileFolderName) + } + + textFileDir := filepath.Join(configDir, profileTextFileFolderName) textFilePath := filepath.Join(textFileDir, textFileName) err = os.MkdirAll(textFileDir, os.ModePerm) diff --git a/internal/pkg/auth/storage_test.go b/internal/pkg/auth/storage_test.go index 1d3d4dab9..306b4deee 100644 --- a/internal/pkg/auth/storage_test.go +++ b/internal/pkg/auth/storage_test.go @@ -10,6 +10,8 @@ import ( "time" "github.com/zalando/go-keyring" + + "github.com/stackitcloud/stackit-cli/internal/pkg/config" ) func TestSetGetAuthField(t *testing.T) { @@ -336,6 +338,16 @@ func TestSetGetAuthFieldEncodedTextFile(t *testing.T) { } func deleteAuthFieldInKeyring(key authFieldKey) error { + activeProfile, err := config.GetProfile() + if err != nil { + return fmt.Errorf("get profile: %w", err) + } + + if activeProfile != "" { + activeProfileKeyring := filepath.Join(keyringService, activeProfile) + return keyring.Delete(activeProfileKeyring, string(key)) + } + return keyring.Delete(keyringService, string(key)) } @@ -349,7 +361,18 @@ func deleteAuthFieldInEncodedTextFile(key authFieldKey) error { if err != nil { return fmt.Errorf("get config dir: %w", err) } - textFileDir := filepath.Join(configDir, textFileFolderName) + + activeProfile, err := config.GetProfile() + if err != nil { + return fmt.Errorf("get profile: %w", err) + } + + profileTextFileFolderName := textFileFolderName + if activeProfile != "" { + profileTextFileFolderName = filepath.Join(activeProfile, textFileFolderName) + } + + textFileDir := filepath.Join(configDir, profileTextFileFolderName) textFilePath := filepath.Join(textFileDir, textFileName) contentEncoded, err := os.ReadFile(textFilePath) From f5791414ad337a4892d04b3f0da5aff48ccb6970 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Diogo=20Ferr=C3=A3o?= Date: Mon, 20 May 2024 14:29:09 +0100 Subject: [PATCH 06/18] Add warnings when set/unset profile with no auth (#327) * initial implementation for auth warning on new profiles * add debug logs * simplify code, improve messages --- internal/cmd/config/profile/set/set.go | 9 +++++++++ internal/cmd/config/profile/unset/unset.go | 9 +++++++++ 2 files changed, 18 insertions(+) diff --git a/internal/cmd/config/profile/set/set.go b/internal/cmd/config/profile/set/set.go index 9dd3172e9..baa650362 100644 --- a/internal/cmd/config/profile/set/set.go +++ b/internal/cmd/config/profile/set/set.go @@ -4,6 +4,7 @@ import ( "fmt" "github.com/stackitcloud/stackit-cli/internal/pkg/args" + "github.com/stackitcloud/stackit-cli/internal/pkg/auth" "github.com/stackitcloud/stackit-cli/internal/pkg/config" "github.com/stackitcloud/stackit-cli/internal/pkg/examples" "github.com/stackitcloud/stackit-cli/internal/pkg/globalflags" @@ -49,6 +50,14 @@ func NewCmd(p *print.Printer) *cobra.Command { return fmt.Errorf("set profile: %w", err) } + flow, err := auth.GetAuthFlow() + if err != nil { + p.Debug(print.WarningLevel, "both keyring and text file storage failed to find a valid authentication flow for the active profile") + p.Warn("The active profile %q is not authenticated, please login using the 'stackit auth login' command.\n", model.Profile) + } else { + p.Debug(print.DebugLevel, "found valid authentication flow for active profile: %s", flow) + } + p.Info("Successfully set active profile to %q\n", model.Profile) return nil }, diff --git a/internal/cmd/config/profile/unset/unset.go b/internal/cmd/config/profile/unset/unset.go index 339439262..6c30e53e1 100644 --- a/internal/cmd/config/profile/unset/unset.go +++ b/internal/cmd/config/profile/unset/unset.go @@ -4,6 +4,7 @@ import ( "fmt" "github.com/stackitcloud/stackit-cli/internal/pkg/args" + "github.com/stackitcloud/stackit-cli/internal/pkg/auth" "github.com/stackitcloud/stackit-cli/internal/pkg/config" "github.com/stackitcloud/stackit-cli/internal/pkg/examples" "github.com/stackitcloud/stackit-cli/internal/pkg/print" @@ -31,6 +32,14 @@ func NewCmd(p *print.Printer) *cobra.Command { return fmt.Errorf("unset profile: %w", err) } + flow, err := auth.GetAuthFlow() + if err != nil { + p.Debug(print.WarningLevel, "both keyring and text file storage failed to find a valid authentication flow for the active profile") + p.Warn("The default profile is not authenticated, please login using the 'stackit auth login' command.\n") + } else { + p.Debug(print.DebugLevel, "found valid authentication flow for active profile: %s", flow) + } + p.Info("Profile unset successfully. The default profile will be used.\n") return nil }, From bbdb98fb1b57d995234f01d58b0a2a0ff5800498 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Diogo=20Ferr=C3=A3o?= Date: Mon, 20 May 2024 15:48:00 +0100 Subject: [PATCH 07/18] address acceptance comments (#328) --- internal/cmd/config/profile/set/set.go | 7 ++++--- internal/cmd/config/profile/unset/unset.go | 7 ++++--- 2 files changed, 8 insertions(+), 6 deletions(-) diff --git a/internal/cmd/config/profile/set/set.go b/internal/cmd/config/profile/set/set.go index baa650362..3594883fe 100644 --- a/internal/cmd/config/profile/set/set.go +++ b/internal/cmd/config/profile/set/set.go @@ -50,15 +50,16 @@ func NewCmd(p *print.Printer) *cobra.Command { return fmt.Errorf("set profile: %w", err) } + p.Info("Successfully set active profile to %q\n", model.Profile) + flow, err := auth.GetAuthFlow() if err != nil { p.Debug(print.WarningLevel, "both keyring and text file storage failed to find a valid authentication flow for the active profile") p.Warn("The active profile %q is not authenticated, please login using the 'stackit auth login' command.\n", model.Profile) - } else { - p.Debug(print.DebugLevel, "found valid authentication flow for active profile: %s", flow) + return nil } + p.Debug(print.DebugLevel, "found valid authentication flow for active profile: %s", flow) - p.Info("Successfully set active profile to %q\n", model.Profile) return nil }, } diff --git a/internal/cmd/config/profile/unset/unset.go b/internal/cmd/config/profile/unset/unset.go index 6c30e53e1..f767cbfe4 100644 --- a/internal/cmd/config/profile/unset/unset.go +++ b/internal/cmd/config/profile/unset/unset.go @@ -32,15 +32,16 @@ func NewCmd(p *print.Printer) *cobra.Command { return fmt.Errorf("unset profile: %w", err) } + p.Info("Profile unset successfully. The default profile will be used.\n") + flow, err := auth.GetAuthFlow() if err != nil { p.Debug(print.WarningLevel, "both keyring and text file storage failed to find a valid authentication flow for the active profile") p.Warn("The default profile is not authenticated, please login using the 'stackit auth login' command.\n") - } else { - p.Debug(print.DebugLevel, "found valid authentication flow for active profile: %s", flow) + return nil } + p.Debug(print.DebugLevel, "found valid authentication flow for active profile: %s", flow) - p.Info("Profile unset successfully. The default profile will be used.\n") return nil }, } From 74c5fa331968d6856931eacbea606b0d76086100 Mon Sep 17 00:00:00 2001 From: GokceGK <161626272+GokceGK@users.noreply.github.com> Date: Tue, 21 May 2024 15:33:59 +0200 Subject: [PATCH 08/18] Rework active profile handling in auth (#329) * move reading profile to caller functions * adapt unit tests * adapt unit tests * adapt unit tests --- internal/pkg/auth/storage.go | 57 +++++-------- internal/pkg/auth/storage_test.go | 131 ++++++++++++++++++++++++------ 2 files changed, 127 insertions(+), 61 deletions(-) diff --git a/internal/pkg/auth/storage.go b/internal/pkg/auth/storage.go index de08f25d5..87ecd9747 100644 --- a/internal/pkg/auth/storage.go +++ b/internal/pkg/auth/storage.go @@ -60,9 +60,14 @@ func SetAuthFieldMap(keyMap map[authFieldKey]string) error { } func SetAuthField(key authFieldKey, value string) error { - err := setAuthFieldInKeyring(key, value) + activeProfile, err := config.GetProfile() + if err != nil { + return fmt.Errorf("get profile: %w", err) + } + + err = setAuthFieldInKeyring(activeProfile, key, value) if err != nil { - errFallback := setAuthFieldInEncodedTextFile(key, value) + errFallback := setAuthFieldInEncodedTextFile(activeProfile, key, value) if errFallback != nil { return fmt.Errorf("write to keyring failed (%w), try writing to encoded text file: %w", err, errFallback) } @@ -70,12 +75,7 @@ func SetAuthField(key authFieldKey, value string) error { return nil } -func setAuthFieldInKeyring(key authFieldKey, value string) error { - activeProfile, err := config.GetProfile() - if err != nil { - return fmt.Errorf("get profile: %w", err) - } - +func setAuthFieldInKeyring(activeProfile string, key authFieldKey, value string) error { if activeProfile != "" { activeProfileKeyring := filepath.Join(keyringService, activeProfile) return keyring.Set(activeProfileKeyring, string(key), value) @@ -83,8 +83,8 @@ func setAuthFieldInKeyring(key authFieldKey, value string) error { return keyring.Set(keyringService, string(key), value) } -func setAuthFieldInEncodedTextFile(key authFieldKey, value string) error { - err := createEncodedTextFile() +func setAuthFieldInEncodedTextFile(activeProfile string, key authFieldKey, value string) error { + err := createEncodedTextFile(activeProfile) if err != nil { return err } @@ -94,11 +94,6 @@ func setAuthFieldInEncodedTextFile(key authFieldKey, value string) error { return fmt.Errorf("get config dir: %w", err) } - activeProfile, err := config.GetProfile() - if err != nil { - return fmt.Errorf("get profile: %w", err) - } - profileTextFileFolderName := textFileFolderName if activeProfile != "" { profileTextFileFolderName = filepath.Join(activeProfile, textFileFolderName) @@ -153,10 +148,15 @@ func GetAuthFlow() (AuthFlow, error) { } func GetAuthField(key authFieldKey) (string, error) { - value, err := getAuthFieldFromKeyring(key) + activeProfile, err := config.GetProfile() + if err != nil { + return "", fmt.Errorf("get profile: %w", err) + } + + value, err := getAuthFieldFromKeyring(activeProfile, key) if err != nil { var errFallback error - value, errFallback = getAuthFieldFromEncodedTextFile(key) + value, errFallback = getAuthFieldFromEncodedTextFile(activeProfile, key) if errFallback != nil { return "", fmt.Errorf("read from keyring: %w, read from encoded file as fallback: %w", err, errFallback) } @@ -164,12 +164,7 @@ func GetAuthField(key authFieldKey) (string, error) { return value, nil } -func getAuthFieldFromKeyring(key authFieldKey) (string, error) { - activeProfile, err := config.GetProfile() - if err != nil { - return "", fmt.Errorf("get profile: %w", err) - } - +func getAuthFieldFromKeyring(activeProfile string, key authFieldKey) (string, error) { if activeProfile != "" { activeProfileKeyring := filepath.Join(keyringService, activeProfile) return keyring.Get(activeProfileKeyring, string(key)) @@ -177,8 +172,8 @@ func getAuthFieldFromKeyring(key authFieldKey) (string, error) { return keyring.Get(keyringService, string(key)) } -func getAuthFieldFromEncodedTextFile(key authFieldKey) (string, error) { - err := createEncodedTextFile() +func getAuthFieldFromEncodedTextFile(activeProfile string, key authFieldKey) (string, error) { + err := createEncodedTextFile(activeProfile) if err != nil { return "", err } @@ -188,11 +183,6 @@ func getAuthFieldFromEncodedTextFile(key authFieldKey) (string, error) { return "", fmt.Errorf("get config dir: %w", err) } - activeProfile, err := config.GetProfile() - if err != nil { - return "", fmt.Errorf("get profile: %w", err) - } - profileTextFileFolderName := textFileFolderName if activeProfile != "" { profileTextFileFolderName = filepath.Join(activeProfile, textFileFolderName) @@ -224,17 +214,12 @@ func getAuthFieldFromEncodedTextFile(key authFieldKey) (string, error) { // Checks if the encoded text file exist. // If it doesn't, creates it with the content "{}" encoded. // If it does, does nothing (and returns nil). -func createEncodedTextFile() error { +func createEncodedTextFile(activeProfile string) error { configDir, err := os.UserConfigDir() if err != nil { return fmt.Errorf("get config dir: %w", err) } - activeProfile, err := config.GetProfile() - if err != nil { - return fmt.Errorf("get profile: %w", err) - } - profileTextFileFolderName := textFileFolderName if activeProfile != "" { profileTextFileFolderName = filepath.Join(activeProfile, textFileFolderName) diff --git a/internal/pkg/auth/storage_test.go b/internal/pkg/auth/storage_test.go index 306b4deee..5733ab69a 100644 --- a/internal/pkg/auth/storage_test.go +++ b/internal/pkg/auth/storage_test.go @@ -115,6 +115,11 @@ func TestSetGetAuthField(t *testing.T) { for _, tt := range tests { t.Run(tt.description, func(t *testing.T) { + activeProfile, err := config.GetProfile() + if err != nil { + t.Errorf("get profile: %v", err) + } + if !tt.keyringFails { keyring.MockInit() } else { @@ -142,12 +147,12 @@ func TestSetGetAuthField(t *testing.T) { } if !tt.keyringFails { - err = deleteAuthFieldInKeyring(key) + err = deleteAuthFieldInKeyring(activeProfile, key) if err != nil { t.Errorf("Post-test cleanup failed: remove field \"%s\" from keyring: %v. Please remove it manually", key, err) } } else { - err = deleteAuthFieldInEncodedTextFile(key) + err = deleteAuthFieldInEncodedTextFile(activeProfile, key) if err != nil { t.Errorf("Post-test cleanup failed: remove field \"%s\" from text file: %v. Please remove it manually", key, err) } @@ -174,9 +179,11 @@ func TestSetGetAuthFieldKeyring(t *testing.T) { description string valueAssignments []valueAssignment expectedValues map[authFieldKey]string + activeProfile string }{ { - description: "simple assignments", + description: "simple assignments with default profile", + activeProfile: "", valueAssignments: []valueAssignment{ { key: testField1, @@ -193,7 +200,48 @@ func TestSetGetAuthFieldKeyring(t *testing.T) { }, }, { - description: "overlapping assignments", + description: "overlapping assignments with default profile", + activeProfile: "", + valueAssignments: []valueAssignment{ + { + key: testField1, + value: testValue1, + }, + { + key: testField2, + value: testValue2, + }, + { + key: testField1, + value: testValue3, + }, + }, + expectedValues: map[authFieldKey]string{ + testField1: testValue3, + testField2: testValue2, + }, + }, + { + description: "simple assignments with testProfile", + activeProfile: "testProfile", + valueAssignments: []valueAssignment{ + { + key: testField1, + value: testValue1, + }, + { + key: testField2, + value: testValue2, + }, + }, + expectedValues: map[authFieldKey]string{ + testField1: testValue1, + testField2: testValue2, + }, + }, + { + description: "overlapping assignments with testProfile", + activeProfile: "testProfile", valueAssignments: []valueAssignment{ { key: testField1, @@ -220,7 +268,7 @@ func TestSetGetAuthFieldKeyring(t *testing.T) { keyring.MockInit() for _, assignment := range tt.valueAssignments { - err := setAuthFieldInKeyring(assignment.key, assignment.value) + err := setAuthFieldInKeyring(tt.activeProfile, assignment.key, assignment.value) if err != nil { t.Fatalf("Failed to set \"%s\" as \"%s\": %v", assignment.key, assignment.value, err) } @@ -231,7 +279,7 @@ func TestSetGetAuthFieldKeyring(t *testing.T) { } for key, valueExpected := range tt.expectedValues { - value, err := getAuthFieldFromKeyring(key) + value, err := getAuthFieldFromKeyring(tt.activeProfile, key) if err != nil { t.Errorf("Failed to get value of \"%s\": %v", key, err) continue @@ -239,7 +287,7 @@ func TestSetGetAuthFieldKeyring(t *testing.T) { t.Errorf("Value of field \"%s\" is wrong: expected \"%s\", got \"%s\"", key, valueExpected, value) } - err = deleteAuthFieldInKeyring(key) + err = deleteAuthFieldInKeyring(tt.activeProfile, key) if err != nil { t.Errorf("Post-test cleanup failed: remove field \"%s\" from keyring: %v. Please remove it manually", key, err) } @@ -263,11 +311,13 @@ func TestSetGetAuthFieldEncodedTextFile(t *testing.T) { tests := []struct { description string + activeProfile string valueAssignments []valueAssignment expectedValues map[authFieldKey]string }{ { - description: "simple assignments", + description: "simple assignments with default profile", + activeProfile: "", valueAssignments: []valueAssignment{ { key: testField1, @@ -284,7 +334,48 @@ func TestSetGetAuthFieldEncodedTextFile(t *testing.T) { }, }, { - description: "overlapping assignments", + description: "overlapping assignments with default profile", + activeProfile: "", + valueAssignments: []valueAssignment{ + { + key: testField1, + value: testValue1, + }, + { + key: testField2, + value: testValue2, + }, + { + key: testField1, + value: testValue3, + }, + }, + expectedValues: map[authFieldKey]string{ + testField1: testValue3, + testField2: testValue2, + }, + }, + { + description: "simple assignments with testProfile", + activeProfile: "testProfile", + valueAssignments: []valueAssignment{ + { + key: testField1, + value: testValue1, + }, + { + key: testField2, + value: testValue2, + }, + }, + expectedValues: map[authFieldKey]string{ + testField1: testValue1, + testField2: testValue2, + }, + }, + { + description: "overlapping assignments with testProfile", + activeProfile: "testProfile", valueAssignments: []valueAssignment{ { key: testField1, @@ -309,7 +400,7 @@ func TestSetGetAuthFieldEncodedTextFile(t *testing.T) { for _, tt := range tests { t.Run(tt.description, func(t *testing.T) { for _, assignment := range tt.valueAssignments { - err := setAuthFieldInEncodedTextFile(assignment.key, assignment.value) + err := setAuthFieldInEncodedTextFile(tt.activeProfile, assignment.key, assignment.value) if err != nil { t.Fatalf("Failed to set \"%s\" as \"%s\": %v", assignment.key, assignment.value, err) } @@ -320,7 +411,7 @@ func TestSetGetAuthFieldEncodedTextFile(t *testing.T) { } for key, valueExpected := range tt.expectedValues { - value, err := getAuthFieldFromEncodedTextFile(key) + value, err := getAuthFieldFromEncodedTextFile(tt.activeProfile, key) if err != nil { t.Errorf("Failed to get value of \"%s\": %v", key, err) continue @@ -328,7 +419,7 @@ func TestSetGetAuthFieldEncodedTextFile(t *testing.T) { t.Errorf("Value of field \"%s\" is wrong: expected \"%s\", got \"%s\"", key, valueExpected, value) } - err = deleteAuthFieldInEncodedTextFile(key) + err = deleteAuthFieldInEncodedTextFile(tt.activeProfile, key) if err != nil { t.Errorf("Post-test cleanup failed: remove field \"%s\" from text file: %v. Please remove it manually", key, err) } @@ -337,12 +428,7 @@ func TestSetGetAuthFieldEncodedTextFile(t *testing.T) { } } -func deleteAuthFieldInKeyring(key authFieldKey) error { - activeProfile, err := config.GetProfile() - if err != nil { - return fmt.Errorf("get profile: %w", err) - } - +func deleteAuthFieldInKeyring(activeProfile string, key authFieldKey) error { if activeProfile != "" { activeProfileKeyring := filepath.Join(keyringService, activeProfile) return keyring.Delete(activeProfileKeyring, string(key)) @@ -351,8 +437,8 @@ func deleteAuthFieldInKeyring(key authFieldKey) error { return keyring.Delete(keyringService, string(key)) } -func deleteAuthFieldInEncodedTextFile(key authFieldKey) error { - err := createEncodedTextFile() +func deleteAuthFieldInEncodedTextFile(activeProfile string, key authFieldKey) error { + err := createEncodedTextFile(activeProfile) if err != nil { return err } @@ -362,11 +448,6 @@ func deleteAuthFieldInEncodedTextFile(key authFieldKey) error { return fmt.Errorf("get config dir: %w", err) } - activeProfile, err := config.GetProfile() - if err != nil { - return fmt.Errorf("get profile: %w", err) - } - profileTextFileFolderName := textFileFolderName if activeProfile != "" { profileTextFileFolderName = filepath.Join(activeProfile, textFileFolderName) From 919fe7a8b1d2856ed78f11795f4616067b4bb2a2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20Palet?= Date: Tue, 21 May 2024 17:46:05 +0100 Subject: [PATCH 09/18] Merge latest updates from `main` into `feature/multi-config` (#330) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Add --used and --unused flags to `load-balancer list` (#308) * initial implementation * add testing, finish functionality * generate docs, minor improvements * more testing * refactor implementation, simplify RunE * remove unused func * address PR comments * generate-docs * change filtercredentials to use enum for operation type * address PR comments * Onboard `load-balancer observability-credentials cleanup` (#311) * command implementation, add testing * rename var, generate docs * address PR comments * fix no credentials listing (#313) * Integrate WinGet distribution in release pipeline (#305) * Integrate WinGet distribution in release pipeline * Add comment regarding skipping prereleases * Fix link * Configure table titles (#314) * Update tables * Configure colors in the less pager * Fix title wrapping, add titles to lb * Re-add -w argument and add titles to mongodb and pgflex options * fix(deps): update stackit sdk modules to v0.14.0 (#317) Co-authored-by: Renovate Bot * MongoDB backup list, describe, restore-jobs (#307) * Draft implementation mongodb backup list, describe, restore-jobs * Fix descriptions * Add expire date§ g * Add docs * Fix examples * Add restore status * Refactor getRestoreStatus * Sort restore jobs array * Add another date format to unit test§ * Implement YAML output format (#298) * initial implementation yamlOutputFormal * sort imports * change yaml library * add yaml output format * tidy up imports * tidy up imports * update docs * add yaml output for project and organization role lists * extend contribution.md * change yaml library * Mongodb backup schedule, update-schedule, restore (#316) * initial update schedule implementation * implement update and list schedule commands, add testing * restore command and testing * add waiters * generate docs * merge changes * address PR comments * add custom error, fix restore examples * Add yaml output format to mongodbflex backup commands (#319) * add yaml output to mongodbflex commands * update docs * Update internal/cmd/mongodbflex/backup/describe/describe.go Co-authored-by: João Palet * change error log --------- Co-authored-by: João Palet * Add yaml output to lb commands (#321) * update docs * add yaml output * fix(deps): update stackit sdk modules (#318) Co-authored-by: Renovate Bot * Add SKE login command (#157) * Add SKE login command Co-authored-by: Maximilian Geberl <48486938+dergeberl@users.noreply.github.com> * Different improvement from code review * remove unused function * rearrange functions and improve error messages * Add tests for cache pkg * Extend kubeconfig create command with flag to retrieve login kubeconfig * small extension to the kubeconfig login description * improve descriptions and examples * fix yaml output * codereview: use os.UserCacheDir instead of external lib * codereview: drop parseInput and use parseClusterConfig directly; move cacheKey into clusterConfig * codereview: add one nil check * codereview: print user facing error that explains that the login command should not be used directly * fixup! codereview: use os.UserCacheDir instead of external lib * codereview: first try at improving the description * generate-docs * Add tests for login (buildRequest & parseKubeConfigToExecCredential * cache: call Init func directly and return err * use p.Outputf instead of cmd.Print --------- Co-authored-by: Maximilian Geberl <48486938+dergeberl@users.noreply.github.com> * Fix outputF call (#322) * Add custom pager handling (#299) * add custom pager handling * ignore linter temporarily * change condition * add debug line * add debug log * add pager infos to README * Update README.md Co-authored-by: João Palet * edit README.md * edit README.md * Update README.md Co-authored-by: Vicente Pinto * Update README.md Co-authored-by: Vicente Pinto --------- Co-authored-by: João Palet Co-authored-by: Vicente Pinto * change marshal function to add indentation (#323) * upgrade dependency to fix high vulnerability (#324) * Replace createFolderIfNotExists with MkdirAll --------- Co-authored-by: Diogo Ferrão Co-authored-by: Vicente Pinto Co-authored-by: stackit-pipeline <142982727+stackit-pipeline@users.noreply.github.com> Co-authored-by: Renovate Bot Co-authored-by: GokceGK <161626272+GokceGK@users.noreply.github.com> Co-authored-by: Kumm-Kai <70690427+Kumm-Kai@users.noreply.github.com> Co-authored-by: Maximilian Geberl <48486938+dergeberl@users.noreply.github.com> --- .gitignore | 1 + CONTRIBUTION.md | 7 + README.md | 27 ++ docs/stackit.md | 2 +- docs/stackit_argus.md | 2 +- docs/stackit_argus_credentials.md | 2 +- docs/stackit_argus_credentials_create.md | 2 +- docs/stackit_argus_credentials_delete.md | 2 +- docs/stackit_argus_credentials_list.md | 2 +- docs/stackit_argus_grafana.md | 2 +- docs/stackit_argus_grafana_describe.md | 2 +- ...tackit_argus_grafana_public-read-access.md | 2 +- ...rgus_grafana_public-read-access_disable.md | 2 +- ...argus_grafana_public-read-access_enable.md | 2 +- docs/stackit_argus_grafana_single-sign-on.md | 2 +- ...it_argus_grafana_single-sign-on_disable.md | 2 +- ...kit_argus_grafana_single-sign-on_enable.md | 2 +- docs/stackit_argus_instance.md | 2 +- docs/stackit_argus_instance_create.md | 2 +- docs/stackit_argus_instance_delete.md | 2 +- docs/stackit_argus_instance_describe.md | 2 +- docs/stackit_argus_instance_list.md | 2 +- docs/stackit_argus_instance_update.md | 2 +- docs/stackit_argus_plans.md | 2 +- docs/stackit_argus_scrape-config.md | 2 +- docs/stackit_argus_scrape-config_create.md | 2 +- docs/stackit_argus_scrape-config_delete.md | 2 +- docs/stackit_argus_scrape-config_describe.md | 2 +- ...it_argus_scrape-config_generate-payload.md | 2 +- docs/stackit_argus_scrape-config_list.md | 2 +- docs/stackit_argus_scrape-config_update.md | 2 +- docs/stackit_auth.md | 2 +- docs/stackit_auth_activate-service-account.md | 2 +- docs/stackit_auth_login.md | 2 +- docs/stackit_config.md | 2 +- docs/stackit_config_list.md | 2 +- docs/stackit_config_set.md | 2 +- docs/stackit_curl.md | 2 +- docs/stackit_dns.md | 2 +- docs/stackit_dns_record-set.md | 2 +- docs/stackit_dns_record-set_create.md | 2 +- docs/stackit_dns_record-set_delete.md | 2 +- docs/stackit_dns_record-set_describe.md | 2 +- docs/stackit_dns_record-set_list.md | 2 +- docs/stackit_dns_record-set_update.md | 2 +- docs/stackit_dns_zone.md | 2 +- docs/stackit_dns_zone_create.md | 2 +- docs/stackit_dns_zone_delete.md | 2 +- docs/stackit_dns_zone_describe.md | 2 +- docs/stackit_dns_zone_list.md | 2 +- docs/stackit_dns_zone_update.md | 2 +- docs/stackit_load-balancer.md | 2 +- docs/stackit_load-balancer_create.md | 2 +- docs/stackit_load-balancer_delete.md | 2 +- docs/stackit_load-balancer_describe.md | 2 +- .../stackit_load-balancer_generate-payload.md | 2 +- docs/stackit_load-balancer_list.md | 2 +- ...load-balancer_observability-credentials.md | 2 +- ...-balancer_observability-credentials_add.md | 2 +- ...ancer_observability-credentials_cleanup.md | 5 +- ...lancer_observability-credentials_delete.md | 2 +- ...ncer_observability-credentials_describe.md | 2 +- ...balancer_observability-credentials_list.md | 2 +- ...lancer_observability-credentials_update.md | 2 +- docs/stackit_load-balancer_quota.md | 2 +- docs/stackit_load-balancer_target-pool.md | 2 +- ...it_load-balancer_target-pool_add-target.md | 2 +- ...ckit_load-balancer_target-pool_describe.md | 2 +- ...load-balancer_target-pool_remove-target.md | 2 +- docs/stackit_load-balancer_update.md | 2 +- docs/stackit_logme.md | 2 +- docs/stackit_logme_credentials.md | 2 +- docs/stackit_logme_credentials_create.md | 2 +- docs/stackit_logme_credentials_delete.md | 2 +- docs/stackit_logme_credentials_describe.md | 2 +- docs/stackit_logme_credentials_list.md | 2 +- docs/stackit_logme_instance.md | 2 +- docs/stackit_logme_instance_create.md | 2 +- docs/stackit_logme_instance_delete.md | 2 +- docs/stackit_logme_instance_describe.md | 2 +- docs/stackit_logme_instance_list.md | 2 +- docs/stackit_logme_instance_update.md | 2 +- docs/stackit_logme_plans.md | 2 +- docs/stackit_mariadb.md | 2 +- docs/stackit_mariadb_credentials.md | 2 +- docs/stackit_mariadb_credentials_create.md | 2 +- docs/stackit_mariadb_credentials_delete.md | 2 +- docs/stackit_mariadb_credentials_describe.md | 2 +- docs/stackit_mariadb_credentials_list.md | 2 +- docs/stackit_mariadb_instance.md | 2 +- docs/stackit_mariadb_instance_create.md | 2 +- docs/stackit_mariadb_instance_delete.md | 2 +- docs/stackit_mariadb_instance_describe.md | 2 +- docs/stackit_mariadb_instance_list.md | 2 +- docs/stackit_mariadb_instance_update.md | 2 +- docs/stackit_mariadb_plans.md | 2 +- docs/stackit_mongodbflex.md | 3 +- docs/stackit_mongodbflex_backup.md | 38 ++ docs/stackit_mongodbflex_backup_describe.md | 43 +++ docs/stackit_mongodbflex_backup_list.md | 47 +++ ...stackit_mongodbflex_backup_restore-jobs.md | 47 +++ docs/stackit_mongodbflex_backup_restore.md | 51 +++ docs/stackit_mongodbflex_backup_schedule.md | 43 +++ ...ckit_mongodbflex_backup_update-schedule.md | 51 +++ docs/stackit_mongodbflex_instance.md | 2 +- docs/stackit_mongodbflex_instance_create.md | 2 +- docs/stackit_mongodbflex_instance_delete.md | 2 +- docs/stackit_mongodbflex_instance_describe.md | 2 +- docs/stackit_mongodbflex_instance_list.md | 2 +- docs/stackit_mongodbflex_instance_update.md | 2 +- docs/stackit_mongodbflex_options.md | 2 +- docs/stackit_mongodbflex_user.md | 2 +- docs/stackit_mongodbflex_user_create.md | 2 +- docs/stackit_mongodbflex_user_delete.md | 2 +- docs/stackit_mongodbflex_user_describe.md | 2 +- docs/stackit_mongodbflex_user_list.md | 2 +- ...stackit_mongodbflex_user_reset-password.md | 2 +- docs/stackit_mongodbflex_user_update.md | 2 +- docs/stackit_object-storage.md | 2 +- docs/stackit_object-storage_bucket.md | 2 +- docs/stackit_object-storage_bucket_create.md | 2 +- docs/stackit_object-storage_bucket_delete.md | 2 +- .../stackit_object-storage_bucket_describe.md | 2 +- docs/stackit_object-storage_bucket_list.md | 2 +- ...tackit_object-storage_credentials-group.md | 2 +- ...object-storage_credentials-group_create.md | 2 +- ...object-storage_credentials-group_delete.md | 2 +- ...t_object-storage_credentials-group_list.md | 2 +- docs/stackit_object-storage_credentials.md | 2 +- ...ackit_object-storage_credentials_create.md | 2 +- ...ackit_object-storage_credentials_delete.md | 2 +- ...stackit_object-storage_credentials_list.md | 2 +- docs/stackit_object-storage_disable.md | 2 +- docs/stackit_object-storage_enable.md | 2 +- docs/stackit_opensearch.md | 2 +- docs/stackit_opensearch_credentials.md | 2 +- docs/stackit_opensearch_credentials_create.md | 2 +- docs/stackit_opensearch_credentials_delete.md | 2 +- ...stackit_opensearch_credentials_describe.md | 2 +- docs/stackit_opensearch_credentials_list.md | 2 +- docs/stackit_opensearch_instance.md | 2 +- docs/stackit_opensearch_instance_create.md | 2 +- docs/stackit_opensearch_instance_delete.md | 2 +- docs/stackit_opensearch_instance_describe.md | 2 +- docs/stackit_opensearch_instance_list.md | 2 +- docs/stackit_opensearch_instance_update.md | 2 +- docs/stackit_opensearch_plans.md | 2 +- docs/stackit_organization.md | 2 +- docs/stackit_organization_member.md | 2 +- docs/stackit_organization_member_add.md | 2 +- docs/stackit_organization_member_list.md | 2 +- docs/stackit_organization_member_remove.md | 2 +- docs/stackit_organization_role.md | 2 +- docs/stackit_organization_role_list.md | 2 +- docs/stackit_postgresflex.md | 2 +- docs/stackit_postgresflex_backup.md | 2 +- docs/stackit_postgresflex_backup_describe.md | 2 +- docs/stackit_postgresflex_backup_list.md | 2 +- ...kit_postgresflex_backup_update-schedule.md | 2 +- docs/stackit_postgresflex_instance.md | 2 +- docs/stackit_postgresflex_instance_clone.md | 2 +- docs/stackit_postgresflex_instance_create.md | 2 +- docs/stackit_postgresflex_instance_delete.md | 2 +- .../stackit_postgresflex_instance_describe.md | 2 +- docs/stackit_postgresflex_instance_list.md | 2 +- docs/stackit_postgresflex_instance_update.md | 2 +- docs/stackit_postgresflex_options.md | 2 +- docs/stackit_postgresflex_user.md | 2 +- docs/stackit_postgresflex_user_create.md | 2 +- docs/stackit_postgresflex_user_delete.md | 2 +- docs/stackit_postgresflex_user_describe.md | 2 +- docs/stackit_postgresflex_user_list.md | 2 +- ...tackit_postgresflex_user_reset-password.md | 2 +- docs/stackit_postgresflex_user_update.md | 2 +- docs/stackit_project.md | 2 +- docs/stackit_project_create.md | 2 +- docs/stackit_project_delete.md | 2 +- docs/stackit_project_describe.md | 2 +- docs/stackit_project_list.md | 2 +- docs/stackit_project_member.md | 2 +- docs/stackit_project_member_add.md | 2 +- docs/stackit_project_member_list.md | 2 +- docs/stackit_project_member_remove.md | 2 +- docs/stackit_project_role.md | 2 +- docs/stackit_project_role_list.md | 2 +- docs/stackit_project_update.md | 2 +- docs/stackit_rabbitmq.md | 2 +- docs/stackit_rabbitmq_credentials.md | 2 +- docs/stackit_rabbitmq_credentials_create.md | 2 +- docs/stackit_rabbitmq_credentials_delete.md | 2 +- docs/stackit_rabbitmq_credentials_describe.md | 2 +- docs/stackit_rabbitmq_credentials_list.md | 2 +- docs/stackit_rabbitmq_instance.md | 2 +- docs/stackit_rabbitmq_instance_create.md | 2 +- docs/stackit_rabbitmq_instance_delete.md | 2 +- docs/stackit_rabbitmq_instance_describe.md | 2 +- docs/stackit_rabbitmq_instance_list.md | 2 +- docs/stackit_rabbitmq_instance_update.md | 2 +- docs/stackit_rabbitmq_plans.md | 2 +- docs/stackit_redis.md | 2 +- docs/stackit_redis_credentials.md | 2 +- docs/stackit_redis_credentials_create.md | 2 +- docs/stackit_redis_credentials_delete.md | 2 +- docs/stackit_redis_credentials_describe.md | 2 +- docs/stackit_redis_credentials_list.md | 2 +- docs/stackit_redis_instance.md | 2 +- docs/stackit_redis_instance_create.md | 2 +- docs/stackit_redis_instance_delete.md | 2 +- docs/stackit_redis_instance_describe.md | 2 +- docs/stackit_redis_instance_list.md | 2 +- docs/stackit_redis_instance_update.md | 2 +- docs/stackit_redis_plans.md | 2 +- docs/stackit_secrets-manager.md | 2 +- docs/stackit_secrets-manager_instance.md | 2 +- ...stackit_secrets-manager_instance_create.md | 2 +- ...stackit_secrets-manager_instance_delete.md | 2 +- ...ackit_secrets-manager_instance_describe.md | 2 +- docs/stackit_secrets-manager_instance_list.md | 2 +- ...stackit_secrets-manager_instance_update.md | 2 +- docs/stackit_secrets-manager_user.md | 2 +- docs/stackit_secrets-manager_user_create.md | 2 +- docs/stackit_secrets-manager_user_delete.md | 2 +- docs/stackit_secrets-manager_user_describe.md | 2 +- docs/stackit_secrets-manager_user_list.md | 2 +- docs/stackit_secrets-manager_user_update.md | 2 +- docs/stackit_service-account.md | 2 +- docs/stackit_service-account_create.md | 2 +- docs/stackit_service-account_delete.md | 2 +- docs/stackit_service-account_get-jwks.md | 2 +- docs/stackit_service-account_key.md | 2 +- docs/stackit_service-account_key_create.md | 2 +- docs/stackit_service-account_key_delete.md | 2 +- docs/stackit_service-account_key_describe.md | 2 +- docs/stackit_service-account_key_list.md | 2 +- docs/stackit_service-account_key_update.md | 2 +- docs/stackit_service-account_list.md | 2 +- docs/stackit_service-account_token.md | 2 +- docs/stackit_service-account_token_create.md | 2 +- docs/stackit_service-account_token_list.md | 2 +- docs/stackit_service-account_token_revoke.md | 2 +- docs/stackit_ske.md | 2 +- docs/stackit_ske_cluster.md | 2 +- docs/stackit_ske_cluster_create.md | 2 +- docs/stackit_ske_cluster_delete.md | 2 +- docs/stackit_ske_cluster_describe.md | 2 +- docs/stackit_ske_cluster_generate-payload.md | 2 +- docs/stackit_ske_cluster_list.md | 2 +- docs/stackit_ske_cluster_update.md | 2 +- docs/stackit_ske_credentials.md | 2 +- ...ackit_ske_credentials_complete-rotation.md | 2 +- .../stackit_ske_credentials_start-rotation.md | 2 +- docs/stackit_ske_describe.md | 2 +- docs/stackit_ske_disable.md | 2 +- docs/stackit_ske_enable.md | 2 +- docs/stackit_ske_kubeconfig.md | 3 +- docs/stackit_ske_kubeconfig_create.md | 6 +- docs/stackit_ske_kubeconfig_login.md | 45 +++ docs/stackit_ske_options.md | 2 +- go.mod | 47 ++- go.sum | 146 ++++++-- internal/cmd/argus/credentials/list/list.go | 9 + .../cmd/argus/grafana/describe/describe.go | 9 + internal/cmd/argus/instance/create/create.go | 9 + .../cmd/argus/instance/describe/describe.go | 9 + internal/cmd/argus/instance/list/list.go | 9 + internal/cmd/argus/plans/plans.go | 9 + .../argus/scrape-config/describe/describe.go | 9 + internal/cmd/argus/scrape-config/list/list.go | 9 + internal/cmd/config/list/list.go | 9 + internal/cmd/dns/record-set/create/create.go | 9 + .../cmd/dns/record-set/describe/describe.go | 9 + internal/cmd/dns/record-set/list/list.go | 9 + internal/cmd/dns/zone/create/create.go | 9 + internal/cmd/dns/zone/describe/describe.go | 9 + internal/cmd/dns/zone/list/list.go | 9 + .../cmd/load-balancer/describe/describe.go | 9 + internal/cmd/load-balancer/list/list.go | 9 + .../observability-credentials/add/add.go | 9 + .../describe/describe.go | 9 + .../observability-credentials/list/list.go | 9 + internal/cmd/load-balancer/quota/quota.go | 9 + .../target-pool/describe/describe.go | 24 +- .../cmd/logme/credentials/create/create.go | 18 +- .../logme/credentials/describe/describe.go | 9 + internal/cmd/logme/credentials/list/list.go | 9 + internal/cmd/logme/instance/create/create.go | 9 + .../cmd/logme/instance/describe/describe.go | 9 + internal/cmd/logme/instance/list/list.go | 9 + internal/cmd/logme/plans/plans.go | 9 + .../cmd/mariadb/credentials/create/create.go | 15 +- .../mariadb/credentials/describe/describe.go | 9 + internal/cmd/mariadb/credentials/list/list.go | 9 + .../cmd/mariadb/instance/create/create.go | 9 + .../cmd/mariadb/instance/describe/describe.go | 9 + internal/cmd/mariadb/instance/list/list.go | 9 + internal/cmd/mariadb/plans/plans.go | 9 + internal/cmd/mongodbflex/backup/backup.go | 36 ++ .../mongodbflex/backup/describe/describe.go | 165 +++++++++ .../backup/describe/describe_test.go | 239 +++++++++++++ internal/cmd/mongodbflex/backup/list/list.go | 180 ++++++++++ .../cmd/mongodbflex/backup/list/list_test.go | 209 +++++++++++ .../backup/restore-jobs/restore_jobs.go | 174 ++++++++++ .../backup/restore-jobs/restore_jobs_test.go | 209 +++++++++++ .../cmd/mongodbflex/backup/restore/restore.go | 205 +++++++++++ .../backup/restore/restore_test.go | 310 +++++++++++++++++ .../mongodbflex/backup/schedule/schedule.go | 161 +++++++++ .../backup/schedule/schedule_test.go | 195 +++++++++++ .../backup/update-schedule/update_schedule.go | 233 +++++++++++++ .../update-schedule/update_schedule_test.go | 324 ++++++++++++++++++ .../cmd/mongodbflex/instance/create/create.go | 9 + .../mongodbflex/instance/describe/describe.go | 11 + .../cmd/mongodbflex/instance/list/list.go | 9 + .../cmd/mongodbflex/instance/update/update.go | 9 + internal/cmd/mongodbflex/mongodbflex.go | 2 + internal/cmd/mongodbflex/options/options.go | 9 + .../cmd/mongodbflex/user/create/create.go | 9 + .../cmd/mongodbflex/user/describe/describe.go | 9 + internal/cmd/mongodbflex/user/list/list.go | 9 + .../user/reset-password/reset_password.go | 9 + .../object-storage/bucket/create/create.go | 9 + .../bucket/describe/describe.go | 9 + .../cmd/object-storage/bucket/list/list.go | 9 + .../credentials-group/create/create.go | 9 + .../credentials-group/list/list.go | 9 + .../credentials/create/create.go | 9 + .../object-storage/credentials/list/list.go | 9 + .../opensearch/credentials/create/create.go | 15 +- .../credentials/describe/describe.go | 9 + .../cmd/opensearch/credentials/list/list.go | 9 + .../cmd/opensearch/instance/create/create.go | 9 + .../opensearch/instance/describe/describe.go | 9 + internal/cmd/opensearch/instance/list/list.go | 9 + internal/cmd/opensearch/plans/plans.go | 9 + internal/cmd/organization/member/list/list.go | 9 + internal/cmd/organization/role/list/list.go | 9 + .../postgresflex/backup/describe/describe.go | 9 + internal/cmd/postgresflex/backup/list/list.go | 11 +- .../cmd/postgresflex/instance/clone/clone.go | 9 + .../postgresflex/instance/create/create.go | 9 + .../instance/describe/describe.go | 9 + .../cmd/postgresflex/instance/list/list.go | 9 + .../postgresflex/instance/update/update.go | 9 + internal/cmd/postgresflex/options/options.go | 9 + .../cmd/postgresflex/user/create/create.go | 9 + .../postgresflex/user/describe/describe.go | 9 + internal/cmd/postgresflex/user/list/list.go | 9 + .../user/reset-password/reset_password.go | 9 + internal/cmd/project/create/create.go | 9 + internal/cmd/project/describe/describe.go | 9 + internal/cmd/project/list/list.go | 9 + internal/cmd/project/member/list/list.go | 9 + internal/cmd/project/role/list/list.go | 9 + .../cmd/rabbitmq/credentials/create/create.go | 18 +- .../rabbitmq/credentials/describe/describe.go | 9 + .../cmd/rabbitmq/credentials/list/list.go | 9 + .../cmd/rabbitmq/instance/create/create.go | 9 + .../rabbitmq/instance/describe/describe.go | 9 + internal/cmd/rabbitmq/instance/list/list.go | 9 + internal/cmd/rabbitmq/plans/plans.go | 9 + .../cmd/redis/credentials/create/create.go | 18 +- .../redis/credentials/describe/describe.go | 9 + internal/cmd/redis/credentials/list/list.go | 9 + internal/cmd/redis/instance/create/create.go | 9 + .../cmd/redis/instance/describe/describe.go | 9 + internal/cmd/redis/instance/list/list.go | 9 + internal/cmd/redis/plans/plans.go | 9 + .../secrets-manager/instance/create/create.go | 9 + .../instance/describe/describe.go | 19 +- .../cmd/secrets-manager/instance/list/list.go | 9 + .../cmd/secrets-manager/user/create/create.go | 9 + .../secrets-manager/user/describe/describe.go | 9 + .../cmd/secrets-manager/user/list/list.go | 9 + internal/cmd/service-account/create/create.go | 9 + internal/cmd/service-account/key/list/list.go | 9 + internal/cmd/service-account/list/list.go | 7 + .../service-account/token/create/create.go | 9 + .../cmd/service-account/token/list/list.go | 9 + internal/cmd/ske/cluster/create/create.go | 9 + internal/cmd/ske/cluster/describe/describe.go | 9 + internal/cmd/ske/cluster/list/list.go | 9 + internal/cmd/ske/cluster/update/update.go | 9 + .../cmd/ske/credentials/describe/describe.go | 9 + internal/cmd/ske/describe/describe.go | 9 + internal/cmd/ske/kubeconfig/create/create.go | 100 +++++- .../cmd/ske/kubeconfig/create/create_test.go | 16 +- internal/cmd/ske/kubeconfig/kubeconfig.go | 2 + internal/cmd/ske/kubeconfig/login/login.go | 253 ++++++++++++++ .../cmd/ske/kubeconfig/login/login_test.go | 140 ++++++++ internal/cmd/ske/options/options.go | 9 + internal/pkg/cache/cache.go | 73 ++++ internal/pkg/cache/cache_test.go | 207 +++++++++++ internal/pkg/config/config.go | 3 +- internal/pkg/config/file_utils.go | 14 - internal/pkg/config/file_utils_test.go | 42 --- internal/pkg/config/profiles.go | 2 +- internal/pkg/errors/errors.go | 10 + internal/pkg/globalflags/global_flags.go | 2 +- internal/pkg/print/print.go | 7 + .../pkg/services/mongodbflex/utils/utils.go | 25 ++ .../services/mongodbflex/utils/utils_test.go | 140 +++++++- 400 files changed, 5429 insertions(+), 401 deletions(-) create mode 100644 docs/stackit_mongodbflex_backup.md create mode 100644 docs/stackit_mongodbflex_backup_describe.md create mode 100644 docs/stackit_mongodbflex_backup_list.md create mode 100644 docs/stackit_mongodbflex_backup_restore-jobs.md create mode 100644 docs/stackit_mongodbflex_backup_restore.md create mode 100644 docs/stackit_mongodbflex_backup_schedule.md create mode 100644 docs/stackit_mongodbflex_backup_update-schedule.md create mode 100644 docs/stackit_ske_kubeconfig_login.md create mode 100644 internal/cmd/mongodbflex/backup/backup.go create mode 100644 internal/cmd/mongodbflex/backup/describe/describe.go create mode 100644 internal/cmd/mongodbflex/backup/describe/describe_test.go create mode 100644 internal/cmd/mongodbflex/backup/list/list.go create mode 100644 internal/cmd/mongodbflex/backup/list/list_test.go create mode 100644 internal/cmd/mongodbflex/backup/restore-jobs/restore_jobs.go create mode 100644 internal/cmd/mongodbflex/backup/restore-jobs/restore_jobs_test.go create mode 100644 internal/cmd/mongodbflex/backup/restore/restore.go create mode 100644 internal/cmd/mongodbflex/backup/restore/restore_test.go create mode 100644 internal/cmd/mongodbflex/backup/schedule/schedule.go create mode 100644 internal/cmd/mongodbflex/backup/schedule/schedule_test.go create mode 100644 internal/cmd/mongodbflex/backup/update-schedule/update_schedule.go create mode 100644 internal/cmd/mongodbflex/backup/update-schedule/update_schedule_test.go create mode 100644 internal/cmd/ske/kubeconfig/login/login.go create mode 100644 internal/cmd/ske/kubeconfig/login/login_test.go create mode 100644 internal/pkg/cache/cache.go create mode 100644 internal/pkg/cache/cache_test.go diff --git a/.gitignore b/.gitignore index 70c5d7e8f..428e142ad 100644 --- a/.gitignore +++ b/.gitignore @@ -4,6 +4,7 @@ dist/ # IDE .vscode +.idea # OS generated files .DS_Store diff --git a/CONTRIBUTION.md b/CONTRIBUTION.md index 9c8488a9a..be0a54346 100644 --- a/CONTRIBUTION.md +++ b/CONTRIBUTION.md @@ -173,6 +173,13 @@ func outputResult(p *print.Printer, cmd *cobra.Command, outputFormat string, res } p.Outputln(string(details)) return nil + case print.YAMLOutputFormat: + details, err := yaml.Marshal(resources) + if err != nil { + return fmt.Errorf("marshal resource list: %w", err) + } + p.Outputln(string(details)) + return nil default: table := tables.NewTable() table.SetHeader("ID", "NAME", "STATE") diff --git a/README.md b/README.md index 3e33a00de..adcbb5e43 100644 --- a/README.md +++ b/README.md @@ -130,6 +130,33 @@ stackit config list You can also edit the configuration file manually. +## Customization + +### Pager + +To specify a custom pager, use the `PAGER` environment variable. + +If the variable is not set, STACKIT CLI uses the `less` as default pager. + +When using `less` as a pager, STACKIT CLI will automatically pass following options +- -F, --quit-if-one-screen - Less will automatically exit if the entire file can be displayed on the first screen. +- -S, --chop-long-lines - Lines longer than the screen width will be chopped rather than being folded. +- -w, --hilite-unread - Temporarily highlights the first "new" line after a forward movement of a full page. +- -R, --RAW-CONTROL-CHARS - ANSI color and style sequences will be interpreted. + +> These options will not be added automatically if a custom pager is defined. +> +> In that case, users can define the parameters by using the specific environment variable required by the `PAGER` (if supported). + +> +> For example, if user sets the `PAGER` environment variable to `less` and would like to pass some arguments, `LESS` environment variable must be used as following: + +> +> export PAGER="less" +> +> export LESS="-R" + + ## Autocompletion If you wish to set up command autocompletion in your shell for the STACKIT CLI, please refer to our [autocompletion guide](./AUTOCOMPLETION.md). diff --git a/docs/stackit.md b/docs/stackit.md index 5f5c5b0b6..5d8cc339e 100644 --- a/docs/stackit.md +++ b/docs/stackit.md @@ -18,7 +18,7 @@ stackit [flags] -y, --assume-yes If set, skips all confirmation prompts --async If set, runs the command asynchronously -h, --help Help for "stackit" - -o, --output-format string Output format, one of ["json" "pretty" "none"] + -o, --output-format string Output format, one of ["json" "pretty" "none" "yaml"] -p, --project-id string Project ID --verbosity string Verbosity of the CLI, one of ["debug" "info" "warning" "error"] (default "info") -v, --version Show "stackit" version diff --git a/docs/stackit_argus.md b/docs/stackit_argus.md index 4aa39f3de..b09e8d344 100644 --- a/docs/stackit_argus.md +++ b/docs/stackit_argus.md @@ -21,7 +21,7 @@ stackit argus [flags] ``` -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"] + -o, --output-format string Output format, one of ["json" "pretty" "none" "yaml"] -p, --project-id string Project ID --verbosity string Verbosity of the CLI, one of ["debug" "info" "warning" "error"] (default "info") ``` diff --git a/docs/stackit_argus_credentials.md b/docs/stackit_argus_credentials.md index 9205b3781..b8cb24b2d 100644 --- a/docs/stackit_argus_credentials.md +++ b/docs/stackit_argus_credentials.md @@ -21,7 +21,7 @@ stackit argus credentials [flags] ``` -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"] + -o, --output-format string Output format, one of ["json" "pretty" "none" "yaml"] -p, --project-id string Project ID --verbosity string Verbosity of the CLI, one of ["debug" "info" "warning" "error"] (default "info") ``` diff --git a/docs/stackit_argus_credentials_create.md b/docs/stackit_argus_credentials_create.md index d2cd837e7..9a5c0de17 100644 --- a/docs/stackit_argus_credentials_create.md +++ b/docs/stackit_argus_credentials_create.md @@ -30,7 +30,7 @@ stackit argus credentials create [flags] ``` -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"] + -o, --output-format string Output format, one of ["json" "pretty" "none" "yaml"] -p, --project-id string Project ID --verbosity string Verbosity of the CLI, one of ["debug" "info" "warning" "error"] (default "info") ``` diff --git a/docs/stackit_argus_credentials_delete.md b/docs/stackit_argus_credentials_delete.md index 9437208f4..b88f82b44 100644 --- a/docs/stackit_argus_credentials_delete.md +++ b/docs/stackit_argus_credentials_delete.md @@ -29,7 +29,7 @@ stackit argus credentials delete USERNAME [flags] ``` -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"] + -o, --output-format string Output format, one of ["json" "pretty" "none" "yaml"] -p, --project-id string Project ID --verbosity string Verbosity of the CLI, one of ["debug" "info" "warning" "error"] (default "info") ``` diff --git a/docs/stackit_argus_credentials_list.md b/docs/stackit_argus_credentials_list.md index e51666bb9..889f08e04 100644 --- a/docs/stackit_argus_credentials_list.md +++ b/docs/stackit_argus_credentials_list.md @@ -36,7 +36,7 @@ stackit argus credentials list [flags] ``` -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"] + -o, --output-format string Output format, one of ["json" "pretty" "none" "yaml"] -p, --project-id string Project ID --verbosity string Verbosity of the CLI, one of ["debug" "info" "warning" "error"] (default "info") ``` diff --git a/docs/stackit_argus_grafana.md b/docs/stackit_argus_grafana.md index dbfb5d0e4..3307c0b37 100644 --- a/docs/stackit_argus_grafana.md +++ b/docs/stackit_argus_grafana.md @@ -21,7 +21,7 @@ stackit argus grafana [flags] ``` -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"] + -o, --output-format string Output format, one of ["json" "pretty" "none" "yaml"] -p, --project-id string Project ID --verbosity string Verbosity of the CLI, one of ["debug" "info" "warning" "error"] (default "info") ``` diff --git a/docs/stackit_argus_grafana_describe.md b/docs/stackit_argus_grafana_describe.md index 1245939fa..53ea3812a 100644 --- a/docs/stackit_argus_grafana_describe.md +++ b/docs/stackit_argus_grafana_describe.md @@ -37,7 +37,7 @@ stackit argus grafana describe INSTANCE_ID [flags] ``` -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"] + -o, --output-format string Output format, one of ["json" "pretty" "none" "yaml"] -p, --project-id string Project ID --verbosity string Verbosity of the CLI, one of ["debug" "info" "warning" "error"] (default "info") ``` diff --git a/docs/stackit_argus_grafana_public-read-access.md b/docs/stackit_argus_grafana_public-read-access.md index 00a09e974..963dbec1d 100644 --- a/docs/stackit_argus_grafana_public-read-access.md +++ b/docs/stackit_argus_grafana_public-read-access.md @@ -22,7 +22,7 @@ stackit argus grafana public-read-access [flags] ``` -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"] + -o, --output-format string Output format, one of ["json" "pretty" "none" "yaml"] -p, --project-id string Project ID --verbosity string Verbosity of the CLI, one of ["debug" "info" "warning" "error"] (default "info") ``` diff --git a/docs/stackit_argus_grafana_public-read-access_disable.md b/docs/stackit_argus_grafana_public-read-access_disable.md index da7f21453..b2f232934 100644 --- a/docs/stackit_argus_grafana_public-read-access_disable.md +++ b/docs/stackit_argus_grafana_public-read-access_disable.md @@ -29,7 +29,7 @@ stackit argus grafana public-read-access disable INSTANCE_ID [flags] ``` -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"] + -o, --output-format string Output format, one of ["json" "pretty" "none" "yaml"] -p, --project-id string Project ID --verbosity string Verbosity of the CLI, one of ["debug" "info" "warning" "error"] (default "info") ``` diff --git a/docs/stackit_argus_grafana_public-read-access_enable.md b/docs/stackit_argus_grafana_public-read-access_enable.md index 096620a41..1cbe9579a 100644 --- a/docs/stackit_argus_grafana_public-read-access_enable.md +++ b/docs/stackit_argus_grafana_public-read-access_enable.md @@ -29,7 +29,7 @@ stackit argus grafana public-read-access enable INSTANCE_ID [flags] ``` -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"] + -o, --output-format string Output format, one of ["json" "pretty" "none" "yaml"] -p, --project-id string Project ID --verbosity string Verbosity of the CLI, one of ["debug" "info" "warning" "error"] (default "info") ``` diff --git a/docs/stackit_argus_grafana_single-sign-on.md b/docs/stackit_argus_grafana_single-sign-on.md index f78be2f10..37540150e 100644 --- a/docs/stackit_argus_grafana_single-sign-on.md +++ b/docs/stackit_argus_grafana_single-sign-on.md @@ -22,7 +22,7 @@ stackit argus grafana single-sign-on [flags] ``` -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"] + -o, --output-format string Output format, one of ["json" "pretty" "none" "yaml"] -p, --project-id string Project ID --verbosity string Verbosity of the CLI, one of ["debug" "info" "warning" "error"] (default "info") ``` diff --git a/docs/stackit_argus_grafana_single-sign-on_disable.md b/docs/stackit_argus_grafana_single-sign-on_disable.md index d90f5e761..2766cb326 100644 --- a/docs/stackit_argus_grafana_single-sign-on_disable.md +++ b/docs/stackit_argus_grafana_single-sign-on_disable.md @@ -29,7 +29,7 @@ stackit argus grafana single-sign-on disable INSTANCE_ID [flags] ``` -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"] + -o, --output-format string Output format, one of ["json" "pretty" "none" "yaml"] -p, --project-id string Project ID --verbosity string Verbosity of the CLI, one of ["debug" "info" "warning" "error"] (default "info") ``` diff --git a/docs/stackit_argus_grafana_single-sign-on_enable.md b/docs/stackit_argus_grafana_single-sign-on_enable.md index e3bc43627..33b6ae4e1 100644 --- a/docs/stackit_argus_grafana_single-sign-on_enable.md +++ b/docs/stackit_argus_grafana_single-sign-on_enable.md @@ -29,7 +29,7 @@ stackit argus grafana single-sign-on enable INSTANCE_ID [flags] ``` -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"] + -o, --output-format string Output format, one of ["json" "pretty" "none" "yaml"] -p, --project-id string Project ID --verbosity string Verbosity of the CLI, one of ["debug" "info" "warning" "error"] (default "info") ``` diff --git a/docs/stackit_argus_instance.md b/docs/stackit_argus_instance.md index 26ffa10bb..8683c76eb 100644 --- a/docs/stackit_argus_instance.md +++ b/docs/stackit_argus_instance.md @@ -21,7 +21,7 @@ stackit argus instance [flags] ``` -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"] + -o, --output-format string Output format, one of ["json" "pretty" "none" "yaml"] -p, --project-id string Project ID --verbosity string Verbosity of the CLI, one of ["debug" "info" "warning" "error"] (default "info") ``` diff --git a/docs/stackit_argus_instance_create.md b/docs/stackit_argus_instance_create.md index 12728f040..ed63c92e0 100644 --- a/docs/stackit_argus_instance_create.md +++ b/docs/stackit_argus_instance_create.md @@ -34,7 +34,7 @@ stackit argus instance create [flags] ``` -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"] + -o, --output-format string Output format, one of ["json" "pretty" "none" "yaml"] -p, --project-id string Project ID --verbosity string Verbosity of the CLI, one of ["debug" "info" "warning" "error"] (default "info") ``` diff --git a/docs/stackit_argus_instance_delete.md b/docs/stackit_argus_instance_delete.md index 6a9432f57..b0c681607 100644 --- a/docs/stackit_argus_instance_delete.md +++ b/docs/stackit_argus_instance_delete.md @@ -28,7 +28,7 @@ stackit argus instance delete INSTANCE_ID [flags] ``` -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"] + -o, --output-format string Output format, one of ["json" "pretty" "none" "yaml"] -p, --project-id string Project ID --verbosity string Verbosity of the CLI, one of ["debug" "info" "warning" "error"] (default "info") ``` diff --git a/docs/stackit_argus_instance_describe.md b/docs/stackit_argus_instance_describe.md index fec05a0d4..77d3c6546 100644 --- a/docs/stackit_argus_instance_describe.md +++ b/docs/stackit_argus_instance_describe.md @@ -31,7 +31,7 @@ stackit argus instance describe INSTANCE_ID [flags] ``` -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"] + -o, --output-format string Output format, one of ["json" "pretty" "none" "yaml"] -p, --project-id string Project ID --verbosity string Verbosity of the CLI, one of ["debug" "info" "warning" "error"] (default "info") ``` diff --git a/docs/stackit_argus_instance_list.md b/docs/stackit_argus_instance_list.md index f93235105..b91308146 100644 --- a/docs/stackit_argus_instance_list.md +++ b/docs/stackit_argus_instance_list.md @@ -35,7 +35,7 @@ stackit argus instance list [flags] ``` -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"] + -o, --output-format string Output format, one of ["json" "pretty" "none" "yaml"] -p, --project-id string Project ID --verbosity string Verbosity of the CLI, one of ["debug" "info" "warning" "error"] (default "info") ``` diff --git a/docs/stackit_argus_instance_update.md b/docs/stackit_argus_instance_update.md index c2db5047b..25155965e 100644 --- a/docs/stackit_argus_instance_update.md +++ b/docs/stackit_argus_instance_update.md @@ -37,7 +37,7 @@ stackit argus instance update INSTANCE_ID [flags] ``` -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"] + -o, --output-format string Output format, one of ["json" "pretty" "none" "yaml"] -p, --project-id string Project ID --verbosity string Verbosity of the CLI, one of ["debug" "info" "warning" "error"] (default "info") ``` diff --git a/docs/stackit_argus_plans.md b/docs/stackit_argus_plans.md index 33dfa4e79..bc65afb9e 100644 --- a/docs/stackit_argus_plans.md +++ b/docs/stackit_argus_plans.md @@ -35,7 +35,7 @@ stackit argus plans [flags] ``` -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"] + -o, --output-format string Output format, one of ["json" "pretty" "none" "yaml"] -p, --project-id string Project ID --verbosity string Verbosity of the CLI, one of ["debug" "info" "warning" "error"] (default "info") ``` diff --git a/docs/stackit_argus_scrape-config.md b/docs/stackit_argus_scrape-config.md index 15dea4a7f..8a96ec535 100644 --- a/docs/stackit_argus_scrape-config.md +++ b/docs/stackit_argus_scrape-config.md @@ -21,7 +21,7 @@ stackit argus scrape-config [flags] ``` -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"] + -o, --output-format string Output format, one of ["json" "pretty" "none" "yaml"] -p, --project-id string Project ID --verbosity string Verbosity of the CLI, one of ["debug" "info" "warning" "error"] (default "info") ``` diff --git a/docs/stackit_argus_scrape-config_create.md b/docs/stackit_argus_scrape-config_create.md index cf1dfec34..0df2e91bd 100644 --- a/docs/stackit_argus_scrape-config_create.md +++ b/docs/stackit_argus_scrape-config_create.md @@ -44,7 +44,7 @@ stackit argus scrape-config create [flags] ``` -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"] + -o, --output-format string Output format, one of ["json" "pretty" "none" "yaml"] -p, --project-id string Project ID --verbosity string Verbosity of the CLI, one of ["debug" "info" "warning" "error"] (default "info") ``` diff --git a/docs/stackit_argus_scrape-config_delete.md b/docs/stackit_argus_scrape-config_delete.md index 8264d85fc..503433973 100644 --- a/docs/stackit_argus_scrape-config_delete.md +++ b/docs/stackit_argus_scrape-config_delete.md @@ -29,7 +29,7 @@ stackit argus scrape-config delete JOB_NAME [flags] ``` -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"] + -o, --output-format string Output format, one of ["json" "pretty" "none" "yaml"] -p, --project-id string Project ID --verbosity string Verbosity of the CLI, one of ["debug" "info" "warning" "error"] (default "info") ``` diff --git a/docs/stackit_argus_scrape-config_describe.md b/docs/stackit_argus_scrape-config_describe.md index 94a3be074..565c8f65f 100644 --- a/docs/stackit_argus_scrape-config_describe.md +++ b/docs/stackit_argus_scrape-config_describe.md @@ -32,7 +32,7 @@ stackit argus scrape-config describe JOB_NAME [flags] ``` -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"] + -o, --output-format string Output format, one of ["json" "pretty" "none" "yaml"] -p, --project-id string Project ID --verbosity string Verbosity of the CLI, one of ["debug" "info" "warning" "error"] (default "info") ``` diff --git a/docs/stackit_argus_scrape-config_generate-payload.md b/docs/stackit_argus_scrape-config_generate-payload.md index 94da2ec8d..9f246dfbd 100644 --- a/docs/stackit_argus_scrape-config_generate-payload.md +++ b/docs/stackit_argus_scrape-config_generate-payload.md @@ -43,7 +43,7 @@ stackit argus scrape-config generate-payload [flags] ``` -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"] + -o, --output-format string Output format, one of ["json" "pretty" "none" "yaml"] -p, --project-id string Project ID --verbosity string Verbosity of the CLI, one of ["debug" "info" "warning" "error"] (default "info") ``` diff --git a/docs/stackit_argus_scrape-config_list.md b/docs/stackit_argus_scrape-config_list.md index 45dc232d6..a4ae284f4 100644 --- a/docs/stackit_argus_scrape-config_list.md +++ b/docs/stackit_argus_scrape-config_list.md @@ -36,7 +36,7 @@ stackit argus scrape-config list [flags] ``` -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"] + -o, --output-format string Output format, one of ["json" "pretty" "none" "yaml"] -p, --project-id string Project ID --verbosity string Verbosity of the CLI, one of ["debug" "info" "warning" "error"] (default "info") ``` diff --git a/docs/stackit_argus_scrape-config_update.md b/docs/stackit_argus_scrape-config_update.md index a3b523377..f335fc673 100644 --- a/docs/stackit_argus_scrape-config_update.md +++ b/docs/stackit_argus_scrape-config_update.md @@ -40,7 +40,7 @@ stackit argus scrape-config update JOB_NAME [flags] ``` -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"] + -o, --output-format string Output format, one of ["json" "pretty" "none" "yaml"] -p, --project-id string Project ID --verbosity string Verbosity of the CLI, one of ["debug" "info" "warning" "error"] (default "info") ``` diff --git a/docs/stackit_auth.md b/docs/stackit_auth.md index 726a902dd..00211ca7f 100644 --- a/docs/stackit_auth.md +++ b/docs/stackit_auth.md @@ -21,7 +21,7 @@ stackit auth [flags] ``` -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"] + -o, --output-format string Output format, one of ["json" "pretty" "none" "yaml"] -p, --project-id string Project ID --verbosity string Verbosity of the CLI, one of ["debug" "info" "warning" "error"] (default "info") ``` diff --git a/docs/stackit_auth_activate-service-account.md b/docs/stackit_auth_activate-service-account.md index f0a3b8578..c7f83a103 100644 --- a/docs/stackit_auth_activate-service-account.md +++ b/docs/stackit_auth_activate-service-account.md @@ -41,7 +41,7 @@ stackit auth activate-service-account [flags] ``` -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"] + -o, --output-format string Output format, one of ["json" "pretty" "none" "yaml"] -p, --project-id string Project ID --verbosity string Verbosity of the CLI, one of ["debug" "info" "warning" "error"] (default "info") ``` diff --git a/docs/stackit_auth_login.md b/docs/stackit_auth_login.md index fabd45362..e1f41a63e 100644 --- a/docs/stackit_auth_login.md +++ b/docs/stackit_auth_login.md @@ -28,7 +28,7 @@ stackit auth login [flags] ``` -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"] + -o, --output-format string Output format, one of ["json" "pretty" "none" "yaml"] -p, --project-id string Project ID --verbosity string Verbosity of the CLI, one of ["debug" "info" "warning" "error"] (default "info") ``` diff --git a/docs/stackit_config.md b/docs/stackit_config.md index fedd6f2a3..7a5d45296 100644 --- a/docs/stackit_config.md +++ b/docs/stackit_config.md @@ -26,7 +26,7 @@ stackit config [flags] ``` -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"] + -o, --output-format string Output format, one of ["json" "pretty" "none" "yaml"] -p, --project-id string Project ID --verbosity string Verbosity of the CLI, one of ["debug" "info" "warning" "error"] (default "info") ``` diff --git a/docs/stackit_config_list.md b/docs/stackit_config_list.md index 81491012e..f8b68be6e 100644 --- a/docs/stackit_config_list.md +++ b/docs/stackit_config_list.md @@ -37,7 +37,7 @@ stackit config list [flags] ``` -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"] + -o, --output-format string Output format, one of ["json" "pretty" "none" "yaml"] -p, --project-id string Project ID --verbosity string Verbosity of the CLI, one of ["debug" "info" "warning" "error"] (default "info") ``` diff --git a/docs/stackit_config_set.md b/docs/stackit_config_set.md index faa93fa1a..02b608352 100644 --- a/docs/stackit_config_set.md +++ b/docs/stackit_config_set.md @@ -54,7 +54,7 @@ stackit config set [flags] ``` -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"] + -o, --output-format string Output format, one of ["json" "pretty" "none" "yaml"] -p, --project-id string Project ID --verbosity string Verbosity of the CLI, one of ["debug" "info" "warning" "error"] (default "info") ``` diff --git a/docs/stackit_curl.md b/docs/stackit_curl.md index 32c0aa110..27938ca6d 100644 --- a/docs/stackit_curl.md +++ b/docs/stackit_curl.md @@ -43,7 +43,7 @@ stackit curl URL [flags] ``` -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"] + -o, --output-format string Output format, one of ["json" "pretty" "none" "yaml"] -p, --project-id string Project ID --verbosity string Verbosity of the CLI, one of ["debug" "info" "warning" "error"] (default "info") ``` diff --git a/docs/stackit_dns.md b/docs/stackit_dns.md index c4868f149..07d1fa87c 100644 --- a/docs/stackit_dns.md +++ b/docs/stackit_dns.md @@ -21,7 +21,7 @@ stackit dns [flags] ``` -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"] + -o, --output-format string Output format, one of ["json" "pretty" "none" "yaml"] -p, --project-id string Project ID --verbosity string Verbosity of the CLI, one of ["debug" "info" "warning" "error"] (default "info") ``` diff --git a/docs/stackit_dns_record-set.md b/docs/stackit_dns_record-set.md index 45856f397..33816f70f 100644 --- a/docs/stackit_dns_record-set.md +++ b/docs/stackit_dns_record-set.md @@ -21,7 +21,7 @@ stackit dns record-set [flags] ``` -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"] + -o, --output-format string Output format, one of ["json" "pretty" "none" "yaml"] -p, --project-id string Project ID --verbosity string Verbosity of the CLI, one of ["debug" "info" "warning" "error"] (default "info") ``` diff --git a/docs/stackit_dns_record-set_create.md b/docs/stackit_dns_record-set_create.md index 341fe3cf3..0d8cb5a53 100644 --- a/docs/stackit_dns_record-set_create.md +++ b/docs/stackit_dns_record-set_create.md @@ -34,7 +34,7 @@ stackit dns record-set create [flags] ``` -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"] + -o, --output-format string Output format, one of ["json" "pretty" "none" "yaml"] -p, --project-id string Project ID --verbosity string Verbosity of the CLI, one of ["debug" "info" "warning" "error"] (default "info") ``` diff --git a/docs/stackit_dns_record-set_delete.md b/docs/stackit_dns_record-set_delete.md index 3621c8ae2..c831ba208 100644 --- a/docs/stackit_dns_record-set_delete.md +++ b/docs/stackit_dns_record-set_delete.md @@ -29,7 +29,7 @@ stackit dns record-set delete RECORD_SET_ID [flags] ``` -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"] + -o, --output-format string Output format, one of ["json" "pretty" "none" "yaml"] -p, --project-id string Project ID --verbosity string Verbosity of the CLI, one of ["debug" "info" "warning" "error"] (default "info") ``` diff --git a/docs/stackit_dns_record-set_describe.md b/docs/stackit_dns_record-set_describe.md index 09473b59b..a29428708 100644 --- a/docs/stackit_dns_record-set_describe.md +++ b/docs/stackit_dns_record-set_describe.md @@ -32,7 +32,7 @@ stackit dns record-set describe RECORD_SET_ID [flags] ``` -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"] + -o, --output-format string Output format, one of ["json" "pretty" "none" "yaml"] -p, --project-id string Project ID --verbosity string Verbosity of the CLI, one of ["debug" "info" "warning" "error"] (default "info") ``` diff --git a/docs/stackit_dns_record-set_list.md b/docs/stackit_dns_record-set_list.md index f69026510..4b6e7654d 100644 --- a/docs/stackit_dns_record-set_list.md +++ b/docs/stackit_dns_record-set_list.md @@ -48,7 +48,7 @@ stackit dns record-set list [flags] ``` -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"] + -o, --output-format string Output format, one of ["json" "pretty" "none" "yaml"] -p, --project-id string Project ID --verbosity string Verbosity of the CLI, one of ["debug" "info" "warning" "error"] (default "info") ``` diff --git a/docs/stackit_dns_record-set_update.md b/docs/stackit_dns_record-set_update.md index 8b55c3fec..ff150283f 100644 --- a/docs/stackit_dns_record-set_update.md +++ b/docs/stackit_dns_record-set_update.md @@ -33,7 +33,7 @@ stackit dns record-set update RECORD_SET_ID [flags] ``` -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"] + -o, --output-format string Output format, one of ["json" "pretty" "none" "yaml"] -p, --project-id string Project ID --verbosity string Verbosity of the CLI, one of ["debug" "info" "warning" "error"] (default "info") ``` diff --git a/docs/stackit_dns_zone.md b/docs/stackit_dns_zone.md index d46ebe906..2d484e5e6 100644 --- a/docs/stackit_dns_zone.md +++ b/docs/stackit_dns_zone.md @@ -21,7 +21,7 @@ stackit dns zone [flags] ``` -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"] + -o, --output-format string Output format, one of ["json" "pretty" "none" "yaml"] -p, --project-id string Project ID --verbosity string Verbosity of the CLI, one of ["debug" "info" "warning" "error"] (default "info") ``` diff --git a/docs/stackit_dns_zone_create.md b/docs/stackit_dns_zone_create.md index 2ef4da873..2ac88572c 100644 --- a/docs/stackit_dns_zone_create.md +++ b/docs/stackit_dns_zone_create.md @@ -44,7 +44,7 @@ stackit dns zone create [flags] ``` -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"] + -o, --output-format string Output format, one of ["json" "pretty" "none" "yaml"] -p, --project-id string Project ID --verbosity string Verbosity of the CLI, one of ["debug" "info" "warning" "error"] (default "info") ``` diff --git a/docs/stackit_dns_zone_delete.md b/docs/stackit_dns_zone_delete.md index c5605b826..04f96576f 100644 --- a/docs/stackit_dns_zone_delete.md +++ b/docs/stackit_dns_zone_delete.md @@ -28,7 +28,7 @@ stackit dns zone delete ZONE_ID [flags] ``` -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"] + -o, --output-format string Output format, one of ["json" "pretty" "none" "yaml"] -p, --project-id string Project ID --verbosity string Verbosity of the CLI, one of ["debug" "info" "warning" "error"] (default "info") ``` diff --git a/docs/stackit_dns_zone_describe.md b/docs/stackit_dns_zone_describe.md index 312a1ec77..f65163566 100644 --- a/docs/stackit_dns_zone_describe.md +++ b/docs/stackit_dns_zone_describe.md @@ -31,7 +31,7 @@ stackit dns zone describe ZONE_ID [flags] ``` -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"] + -o, --output-format string Output format, one of ["json" "pretty" "none" "yaml"] -p, --project-id string Project ID --verbosity string Verbosity of the CLI, one of ["debug" "info" "warning" "error"] (default "info") ``` diff --git a/docs/stackit_dns_zone_list.md b/docs/stackit_dns_zone_list.md index 324110b6a..92c527c00 100644 --- a/docs/stackit_dns_zone_list.md +++ b/docs/stackit_dns_zone_list.md @@ -44,7 +44,7 @@ stackit dns zone list [flags] ``` -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"] + -o, --output-format string Output format, one of ["json" "pretty" "none" "yaml"] -p, --project-id string Project ID --verbosity string Verbosity of the CLI, one of ["debug" "info" "warning" "error"] (default "info") ``` diff --git a/docs/stackit_dns_zone_update.md b/docs/stackit_dns_zone_update.md index d09e8cec6..334d7fbec 100644 --- a/docs/stackit_dns_zone_update.md +++ b/docs/stackit_dns_zone_update.md @@ -38,7 +38,7 @@ stackit dns zone update ZONE_ID [flags] ``` -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"] + -o, --output-format string Output format, one of ["json" "pretty" "none" "yaml"] -p, --project-id string Project ID --verbosity string Verbosity of the CLI, one of ["debug" "info" "warning" "error"] (default "info") ``` diff --git a/docs/stackit_load-balancer.md b/docs/stackit_load-balancer.md index e4bff615a..8c154e2b8 100644 --- a/docs/stackit_load-balancer.md +++ b/docs/stackit_load-balancer.md @@ -21,7 +21,7 @@ stackit load-balancer [flags] ``` -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"] + -o, --output-format string Output format, one of ["json" "pretty" "none" "yaml"] -p, --project-id string Project ID --verbosity string Verbosity of the CLI, one of ["debug" "info" "warning" "error"] (default "info") ``` diff --git a/docs/stackit_load-balancer_create.md b/docs/stackit_load-balancer_create.md index b685dd6e7..054947308 100644 --- a/docs/stackit_load-balancer_create.md +++ b/docs/stackit_load-balancer_create.md @@ -39,7 +39,7 @@ stackit load-balancer create [flags] ``` -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"] + -o, --output-format string Output format, one of ["json" "pretty" "none" "yaml"] -p, --project-id string Project ID --verbosity string Verbosity of the CLI, one of ["debug" "info" "warning" "error"] (default "info") ``` diff --git a/docs/stackit_load-balancer_delete.md b/docs/stackit_load-balancer_delete.md index 3dd43b2ae..4c8c895a9 100644 --- a/docs/stackit_load-balancer_delete.md +++ b/docs/stackit_load-balancer_delete.md @@ -28,7 +28,7 @@ stackit load-balancer delete LOAD_BALANCER_NAME [flags] ``` -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"] + -o, --output-format string Output format, one of ["json" "pretty" "none" "yaml"] -p, --project-id string Project ID --verbosity string Verbosity of the CLI, one of ["debug" "info" "warning" "error"] (default "info") ``` diff --git a/docs/stackit_load-balancer_describe.md b/docs/stackit_load-balancer_describe.md index f3b41e9ce..de7abebc8 100644 --- a/docs/stackit_load-balancer_describe.md +++ b/docs/stackit_load-balancer_describe.md @@ -31,7 +31,7 @@ stackit load-balancer describe LOAD_BALANCER_NAME [flags] ``` -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"] + -o, --output-format string Output format, one of ["json" "pretty" "none" "yaml"] -p, --project-id string Project ID --verbosity string Verbosity of the CLI, one of ["debug" "info" "warning" "error"] (default "info") ``` diff --git a/docs/stackit_load-balancer_generate-payload.md b/docs/stackit_load-balancer_generate-payload.md index f01579eca..b0533dfb2 100644 --- a/docs/stackit_load-balancer_generate-payload.md +++ b/docs/stackit_load-balancer_generate-payload.md @@ -37,7 +37,7 @@ stackit load-balancer generate-payload [flags] ``` -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"] + -o, --output-format string Output format, one of ["json" "pretty" "none" "yaml"] -p, --project-id string Project ID --verbosity string Verbosity of the CLI, one of ["debug" "info" "warning" "error"] (default "info") ``` diff --git a/docs/stackit_load-balancer_list.md b/docs/stackit_load-balancer_list.md index e4525cff4..b73422bf6 100644 --- a/docs/stackit_load-balancer_list.md +++ b/docs/stackit_load-balancer_list.md @@ -35,7 +35,7 @@ stackit load-balancer list [flags] ``` -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"] + -o, --output-format string Output format, one of ["json" "pretty" "none" "yaml"] -p, --project-id string Project ID --verbosity string Verbosity of the CLI, one of ["debug" "info" "warning" "error"] (default "info") ``` diff --git a/docs/stackit_load-balancer_observability-credentials.md b/docs/stackit_load-balancer_observability-credentials.md index 4ccba5c45..187047d14 100644 --- a/docs/stackit_load-balancer_observability-credentials.md +++ b/docs/stackit_load-balancer_observability-credentials.md @@ -21,7 +21,7 @@ stackit load-balancer observability-credentials [flags] ``` -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"] + -o, --output-format string Output format, one of ["json" "pretty" "none" "yaml"] -p, --project-id string Project ID --verbosity string Verbosity of the CLI, one of ["debug" "info" "warning" "error"] (default "info") ``` diff --git a/docs/stackit_load-balancer_observability-credentials_add.md b/docs/stackit_load-balancer_observability-credentials_add.md index f66ccf0e6..8abbc2fe9 100644 --- a/docs/stackit_load-balancer_observability-credentials_add.md +++ b/docs/stackit_load-balancer_observability-credentials_add.md @@ -34,7 +34,7 @@ stackit load-balancer observability-credentials add [flags] ``` -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"] + -o, --output-format string Output format, one of ["json" "pretty" "none" "yaml"] -p, --project-id string Project ID --verbosity string Verbosity of the CLI, one of ["debug" "info" "warning" "error"] (default "info") ``` diff --git a/docs/stackit_load-balancer_observability-credentials_cleanup.md b/docs/stackit_load-balancer_observability-credentials_cleanup.md index e994d0803..6f72a76c2 100644 --- a/docs/stackit_load-balancer_observability-credentials_cleanup.md +++ b/docs/stackit_load-balancer_observability-credentials_cleanup.md @@ -28,12 +28,11 @@ stackit load-balancer observability-credentials cleanup [flags] ``` -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"] + -o, --output-format string Output format, one of ["json" "pretty" "none" "yaml"] -p, --project-id string Project ID --verbosity string Verbosity of the CLI, one of ["debug" "info" "warning" "error"] (default "info") ``` ### SEE ALSO -* [stackit load-balancer observability-credentials](./stackit_load-balancer_observability-credentials.md) - Provides functionality for Load Balancer observability credentials - +- [stackit load-balancer observability-credentials](./stackit_load-balancer_observability-credentials.md) - Provides functionality for Load Balancer observability credentials diff --git a/docs/stackit_load-balancer_observability-credentials_delete.md b/docs/stackit_load-balancer_observability-credentials_delete.md index c58199974..7c2965552 100644 --- a/docs/stackit_load-balancer_observability-credentials_delete.md +++ b/docs/stackit_load-balancer_observability-credentials_delete.md @@ -28,7 +28,7 @@ stackit load-balancer observability-credentials delete CREDENTIALS_REF [flags] ``` -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"] + -o, --output-format string Output format, one of ["json" "pretty" "none" "yaml"] -p, --project-id string Project ID --verbosity string Verbosity of the CLI, one of ["debug" "info" "warning" "error"] (default "info") ``` diff --git a/docs/stackit_load-balancer_observability-credentials_describe.md b/docs/stackit_load-balancer_observability-credentials_describe.md index cf0d63e59..5feb0a190 100644 --- a/docs/stackit_load-balancer_observability-credentials_describe.md +++ b/docs/stackit_load-balancer_observability-credentials_describe.md @@ -28,7 +28,7 @@ stackit load-balancer observability-credentials describe CREDENTIALS_REF [flags] ``` -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"] + -o, --output-format string Output format, one of ["json" "pretty" "none" "yaml"] -p, --project-id string Project ID --verbosity string Verbosity of the CLI, one of ["debug" "info" "warning" "error"] (default "info") ``` diff --git a/docs/stackit_load-balancer_observability-credentials_list.md b/docs/stackit_load-balancer_observability-credentials_list.md index 581aa37a8..893fecdbf 100644 --- a/docs/stackit_load-balancer_observability-credentials_list.md +++ b/docs/stackit_load-balancer_observability-credentials_list.md @@ -43,7 +43,7 @@ stackit load-balancer observability-credentials list [flags] ``` -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"] + -o, --output-format string Output format, one of ["json" "pretty" "none" "yaml"] -p, --project-id string Project ID --verbosity string Verbosity of the CLI, one of ["debug" "info" "warning" "error"] (default "info") ``` diff --git a/docs/stackit_load-balancer_observability-credentials_update.md b/docs/stackit_load-balancer_observability-credentials_update.md index 86646227e..2ef125bbc 100644 --- a/docs/stackit_load-balancer_observability-credentials_update.md +++ b/docs/stackit_load-balancer_observability-credentials_update.md @@ -34,7 +34,7 @@ stackit load-balancer observability-credentials update [flags] ``` -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"] + -o, --output-format string Output format, one of ["json" "pretty" "none" "yaml"] -p, --project-id string Project ID --verbosity string Verbosity of the CLI, one of ["debug" "info" "warning" "error"] (default "info") ``` diff --git a/docs/stackit_load-balancer_quota.md b/docs/stackit_load-balancer_quota.md index b81c73575..9ffb0b5e3 100644 --- a/docs/stackit_load-balancer_quota.md +++ b/docs/stackit_load-balancer_quota.md @@ -28,7 +28,7 @@ stackit load-balancer quota [flags] ``` -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"] + -o, --output-format string Output format, one of ["json" "pretty" "none" "yaml"] -p, --project-id string Project ID --verbosity string Verbosity of the CLI, one of ["debug" "info" "warning" "error"] (default "info") ``` diff --git a/docs/stackit_load-balancer_target-pool.md b/docs/stackit_load-balancer_target-pool.md index 92f97009f..cce372cee 100644 --- a/docs/stackit_load-balancer_target-pool.md +++ b/docs/stackit_load-balancer_target-pool.md @@ -21,7 +21,7 @@ stackit load-balancer target-pool [flags] ``` -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"] + -o, --output-format string Output format, one of ["json" "pretty" "none" "yaml"] -p, --project-id string Project ID --verbosity string Verbosity of the CLI, one of ["debug" "info" "warning" "error"] (default "info") ``` diff --git a/docs/stackit_load-balancer_target-pool_add-target.md b/docs/stackit_load-balancer_target-pool_add-target.md index ba18837e5..c5de949df 100644 --- a/docs/stackit_load-balancer_target-pool_add-target.md +++ b/docs/stackit_load-balancer_target-pool_add-target.md @@ -32,7 +32,7 @@ stackit load-balancer target-pool add-target TARGET_IP [flags] ``` -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"] + -o, --output-format string Output format, one of ["json" "pretty" "none" "yaml"] -p, --project-id string Project ID --verbosity string Verbosity of the CLI, one of ["debug" "info" "warning" "error"] (default "info") ``` diff --git a/docs/stackit_load-balancer_target-pool_describe.md b/docs/stackit_load-balancer_target-pool_describe.md index 02a409205..25564de96 100644 --- a/docs/stackit_load-balancer_target-pool_describe.md +++ b/docs/stackit_load-balancer_target-pool_describe.md @@ -32,7 +32,7 @@ stackit load-balancer target-pool describe TARGET_POOL_NAME [flags] ``` -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"] + -o, --output-format string Output format, one of ["json" "pretty" "none" "yaml"] -p, --project-id string Project ID --verbosity string Verbosity of the CLI, one of ["debug" "info" "warning" "error"] (default "info") ``` diff --git a/docs/stackit_load-balancer_target-pool_remove-target.md b/docs/stackit_load-balancer_target-pool_remove-target.md index a910fb3a9..af638f939 100644 --- a/docs/stackit_load-balancer_target-pool_remove-target.md +++ b/docs/stackit_load-balancer_target-pool_remove-target.md @@ -30,7 +30,7 @@ stackit load-balancer target-pool remove-target TARGET_IP [flags] ``` -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"] + -o, --output-format string Output format, one of ["json" "pretty" "none" "yaml"] -p, --project-id string Project ID --verbosity string Verbosity of the CLI, one of ["debug" "info" "warning" "error"] (default "info") ``` diff --git a/docs/stackit_load-balancer_update.md b/docs/stackit_load-balancer_update.md index 97eeddb81..a2d9b771f 100644 --- a/docs/stackit_load-balancer_update.md +++ b/docs/stackit_load-balancer_update.md @@ -39,7 +39,7 @@ stackit load-balancer update LOAD_BALANCER_NAME [flags] ``` -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"] + -o, --output-format string Output format, one of ["json" "pretty" "none" "yaml"] -p, --project-id string Project ID --verbosity string Verbosity of the CLI, one of ["debug" "info" "warning" "error"] (default "info") ``` diff --git a/docs/stackit_logme.md b/docs/stackit_logme.md index 4d11081b6..38a1419ab 100644 --- a/docs/stackit_logme.md +++ b/docs/stackit_logme.md @@ -21,7 +21,7 @@ stackit logme [flags] ``` -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"] + -o, --output-format string Output format, one of ["json" "pretty" "none" "yaml"] -p, --project-id string Project ID --verbosity string Verbosity of the CLI, one of ["debug" "info" "warning" "error"] (default "info") ``` diff --git a/docs/stackit_logme_credentials.md b/docs/stackit_logme_credentials.md index 6fdc82ba1..0d2712c1d 100644 --- a/docs/stackit_logme_credentials.md +++ b/docs/stackit_logme_credentials.md @@ -21,7 +21,7 @@ stackit logme credentials [flags] ``` -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"] + -o, --output-format string Output format, one of ["json" "pretty" "none" "yaml"] -p, --project-id string Project ID --verbosity string Verbosity of the CLI, one of ["debug" "info" "warning" "error"] (default "info") ``` diff --git a/docs/stackit_logme_credentials_create.md b/docs/stackit_logme_credentials_create.md index 3a6a2ad6f..2dc853cec 100644 --- a/docs/stackit_logme_credentials_create.md +++ b/docs/stackit_logme_credentials_create.md @@ -33,7 +33,7 @@ stackit logme credentials create [flags] ``` -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"] + -o, --output-format string Output format, one of ["json" "pretty" "none" "yaml"] -p, --project-id string Project ID --verbosity string Verbosity of the CLI, one of ["debug" "info" "warning" "error"] (default "info") ``` diff --git a/docs/stackit_logme_credentials_delete.md b/docs/stackit_logme_credentials_delete.md index 9da8d2e23..020b53c99 100644 --- a/docs/stackit_logme_credentials_delete.md +++ b/docs/stackit_logme_credentials_delete.md @@ -29,7 +29,7 @@ stackit logme credentials delete CREDENTIALS_ID [flags] ``` -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"] + -o, --output-format string Output format, one of ["json" "pretty" "none" "yaml"] -p, --project-id string Project ID --verbosity string Verbosity of the CLI, one of ["debug" "info" "warning" "error"] (default "info") ``` diff --git a/docs/stackit_logme_credentials_describe.md b/docs/stackit_logme_credentials_describe.md index 828d46231..a882a1c70 100644 --- a/docs/stackit_logme_credentials_describe.md +++ b/docs/stackit_logme_credentials_describe.md @@ -32,7 +32,7 @@ stackit logme credentials describe CREDENTIALS_ID [flags] ``` -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"] + -o, --output-format string Output format, one of ["json" "pretty" "none" "yaml"] -p, --project-id string Project ID --verbosity string Verbosity of the CLI, one of ["debug" "info" "warning" "error"] (default "info") ``` diff --git a/docs/stackit_logme_credentials_list.md b/docs/stackit_logme_credentials_list.md index 3bb6cfc44..2ae3d2bfa 100644 --- a/docs/stackit_logme_credentials_list.md +++ b/docs/stackit_logme_credentials_list.md @@ -36,7 +36,7 @@ stackit logme credentials list [flags] ``` -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"] + -o, --output-format string Output format, one of ["json" "pretty" "none" "yaml"] -p, --project-id string Project ID --verbosity string Verbosity of the CLI, one of ["debug" "info" "warning" "error"] (default "info") ``` diff --git a/docs/stackit_logme_instance.md b/docs/stackit_logme_instance.md index edaa09f89..90c389041 100644 --- a/docs/stackit_logme_instance.md +++ b/docs/stackit_logme_instance.md @@ -21,7 +21,7 @@ stackit logme instance [flags] ``` -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"] + -o, --output-format string Output format, one of ["json" "pretty" "none" "yaml"] -p, --project-id string Project ID --verbosity string Verbosity of the CLI, one of ["debug" "info" "warning" "error"] (default "info") ``` diff --git a/docs/stackit_logme_instance_create.md b/docs/stackit_logme_instance_create.md index 5b6d78082..281d13e9b 100644 --- a/docs/stackit_logme_instance_create.md +++ b/docs/stackit_logme_instance_create.md @@ -45,7 +45,7 @@ stackit logme instance create [flags] ``` -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"] + -o, --output-format string Output format, one of ["json" "pretty" "none" "yaml"] -p, --project-id string Project ID --verbosity string Verbosity of the CLI, one of ["debug" "info" "warning" "error"] (default "info") ``` diff --git a/docs/stackit_logme_instance_delete.md b/docs/stackit_logme_instance_delete.md index f1c45b4d6..94bc52c53 100644 --- a/docs/stackit_logme_instance_delete.md +++ b/docs/stackit_logme_instance_delete.md @@ -28,7 +28,7 @@ stackit logme instance delete INSTANCE_ID [flags] ``` -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"] + -o, --output-format string Output format, one of ["json" "pretty" "none" "yaml"] -p, --project-id string Project ID --verbosity string Verbosity of the CLI, one of ["debug" "info" "warning" "error"] (default "info") ``` diff --git a/docs/stackit_logme_instance_describe.md b/docs/stackit_logme_instance_describe.md index ca3105bec..26241ca7a 100644 --- a/docs/stackit_logme_instance_describe.md +++ b/docs/stackit_logme_instance_describe.md @@ -31,7 +31,7 @@ stackit logme instance describe INSTANCE_ID [flags] ``` -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"] + -o, --output-format string Output format, one of ["json" "pretty" "none" "yaml"] -p, --project-id string Project ID --verbosity string Verbosity of the CLI, one of ["debug" "info" "warning" "error"] (default "info") ``` diff --git a/docs/stackit_logme_instance_list.md b/docs/stackit_logme_instance_list.md index ded2abe71..9180286b2 100644 --- a/docs/stackit_logme_instance_list.md +++ b/docs/stackit_logme_instance_list.md @@ -35,7 +35,7 @@ stackit logme instance list [flags] ``` -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"] + -o, --output-format string Output format, one of ["json" "pretty" "none" "yaml"] -p, --project-id string Project ID --verbosity string Verbosity of the CLI, one of ["debug" "info" "warning" "error"] (default "info") ``` diff --git a/docs/stackit_logme_instance_update.md b/docs/stackit_logme_instance_update.md index f29639948..d6ac88f29 100644 --- a/docs/stackit_logme_instance_update.md +++ b/docs/stackit_logme_instance_update.md @@ -41,7 +41,7 @@ stackit logme instance update INSTANCE_ID [flags] ``` -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"] + -o, --output-format string Output format, one of ["json" "pretty" "none" "yaml"] -p, --project-id string Project ID --verbosity string Verbosity of the CLI, one of ["debug" "info" "warning" "error"] (default "info") ``` diff --git a/docs/stackit_logme_plans.md b/docs/stackit_logme_plans.md index aafe53e00..f2450971f 100644 --- a/docs/stackit_logme_plans.md +++ b/docs/stackit_logme_plans.md @@ -35,7 +35,7 @@ stackit logme plans [flags] ``` -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"] + -o, --output-format string Output format, one of ["json" "pretty" "none" "yaml"] -p, --project-id string Project ID --verbosity string Verbosity of the CLI, one of ["debug" "info" "warning" "error"] (default "info") ``` diff --git a/docs/stackit_mariadb.md b/docs/stackit_mariadb.md index 42dff9880..18d6875c2 100644 --- a/docs/stackit_mariadb.md +++ b/docs/stackit_mariadb.md @@ -21,7 +21,7 @@ stackit mariadb [flags] ``` -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"] + -o, --output-format string Output format, one of ["json" "pretty" "none" "yaml"] -p, --project-id string Project ID --verbosity string Verbosity of the CLI, one of ["debug" "info" "warning" "error"] (default "info") ``` diff --git a/docs/stackit_mariadb_credentials.md b/docs/stackit_mariadb_credentials.md index 2a4950db2..34f79706f 100644 --- a/docs/stackit_mariadb_credentials.md +++ b/docs/stackit_mariadb_credentials.md @@ -21,7 +21,7 @@ stackit mariadb credentials [flags] ``` -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"] + -o, --output-format string Output format, one of ["json" "pretty" "none" "yaml"] -p, --project-id string Project ID --verbosity string Verbosity of the CLI, one of ["debug" "info" "warning" "error"] (default "info") ``` diff --git a/docs/stackit_mariadb_credentials_create.md b/docs/stackit_mariadb_credentials_create.md index 1ea118fb3..beb6159c6 100644 --- a/docs/stackit_mariadb_credentials_create.md +++ b/docs/stackit_mariadb_credentials_create.md @@ -33,7 +33,7 @@ stackit mariadb credentials create [flags] ``` -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"] + -o, --output-format string Output format, one of ["json" "pretty" "none" "yaml"] -p, --project-id string Project ID --verbosity string Verbosity of the CLI, one of ["debug" "info" "warning" "error"] (default "info") ``` diff --git a/docs/stackit_mariadb_credentials_delete.md b/docs/stackit_mariadb_credentials_delete.md index 5cf28431d..812c4f7bc 100644 --- a/docs/stackit_mariadb_credentials_delete.md +++ b/docs/stackit_mariadb_credentials_delete.md @@ -29,7 +29,7 @@ stackit mariadb credentials delete CREDENTIALS_ID [flags] ``` -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"] + -o, --output-format string Output format, one of ["json" "pretty" "none" "yaml"] -p, --project-id string Project ID --verbosity string Verbosity of the CLI, one of ["debug" "info" "warning" "error"] (default "info") ``` diff --git a/docs/stackit_mariadb_credentials_describe.md b/docs/stackit_mariadb_credentials_describe.md index b89d54f4c..79b828146 100644 --- a/docs/stackit_mariadb_credentials_describe.md +++ b/docs/stackit_mariadb_credentials_describe.md @@ -32,7 +32,7 @@ stackit mariadb credentials describe CREDENTIALS_ID [flags] ``` -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"] + -o, --output-format string Output format, one of ["json" "pretty" "none" "yaml"] -p, --project-id string Project ID --verbosity string Verbosity of the CLI, one of ["debug" "info" "warning" "error"] (default "info") ``` diff --git a/docs/stackit_mariadb_credentials_list.md b/docs/stackit_mariadb_credentials_list.md index 0ff725526..cc49e23c4 100644 --- a/docs/stackit_mariadb_credentials_list.md +++ b/docs/stackit_mariadb_credentials_list.md @@ -36,7 +36,7 @@ stackit mariadb credentials list [flags] ``` -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"] + -o, --output-format string Output format, one of ["json" "pretty" "none" "yaml"] -p, --project-id string Project ID --verbosity string Verbosity of the CLI, one of ["debug" "info" "warning" "error"] (default "info") ``` diff --git a/docs/stackit_mariadb_instance.md b/docs/stackit_mariadb_instance.md index 5ba8366ad..2da27c38b 100644 --- a/docs/stackit_mariadb_instance.md +++ b/docs/stackit_mariadb_instance.md @@ -21,7 +21,7 @@ stackit mariadb instance [flags] ``` -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"] + -o, --output-format string Output format, one of ["json" "pretty" "none" "yaml"] -p, --project-id string Project ID --verbosity string Verbosity of the CLI, one of ["debug" "info" "warning" "error"] (default "info") ``` diff --git a/docs/stackit_mariadb_instance_create.md b/docs/stackit_mariadb_instance_create.md index db1898f61..3a8a063b9 100644 --- a/docs/stackit_mariadb_instance_create.md +++ b/docs/stackit_mariadb_instance_create.md @@ -45,7 +45,7 @@ stackit mariadb instance create [flags] ``` -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"] + -o, --output-format string Output format, one of ["json" "pretty" "none" "yaml"] -p, --project-id string Project ID --verbosity string Verbosity of the CLI, one of ["debug" "info" "warning" "error"] (default "info") ``` diff --git a/docs/stackit_mariadb_instance_delete.md b/docs/stackit_mariadb_instance_delete.md index 32a5e786f..564057900 100644 --- a/docs/stackit_mariadb_instance_delete.md +++ b/docs/stackit_mariadb_instance_delete.md @@ -28,7 +28,7 @@ stackit mariadb instance delete INSTANCE_ID [flags] ``` -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"] + -o, --output-format string Output format, one of ["json" "pretty" "none" "yaml"] -p, --project-id string Project ID --verbosity string Verbosity of the CLI, one of ["debug" "info" "warning" "error"] (default "info") ``` diff --git a/docs/stackit_mariadb_instance_describe.md b/docs/stackit_mariadb_instance_describe.md index 89e9468d2..13c5014e1 100644 --- a/docs/stackit_mariadb_instance_describe.md +++ b/docs/stackit_mariadb_instance_describe.md @@ -31,7 +31,7 @@ stackit mariadb instance describe INSTANCE_ID [flags] ``` -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"] + -o, --output-format string Output format, one of ["json" "pretty" "none" "yaml"] -p, --project-id string Project ID --verbosity string Verbosity of the CLI, one of ["debug" "info" "warning" "error"] (default "info") ``` diff --git a/docs/stackit_mariadb_instance_list.md b/docs/stackit_mariadb_instance_list.md index b0e0fbab5..301787ef6 100644 --- a/docs/stackit_mariadb_instance_list.md +++ b/docs/stackit_mariadb_instance_list.md @@ -35,7 +35,7 @@ stackit mariadb instance list [flags] ``` -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"] + -o, --output-format string Output format, one of ["json" "pretty" "none" "yaml"] -p, --project-id string Project ID --verbosity string Verbosity of the CLI, one of ["debug" "info" "warning" "error"] (default "info") ``` diff --git a/docs/stackit_mariadb_instance_update.md b/docs/stackit_mariadb_instance_update.md index aa1fc35e2..1c7712c00 100644 --- a/docs/stackit_mariadb_instance_update.md +++ b/docs/stackit_mariadb_instance_update.md @@ -41,7 +41,7 @@ stackit mariadb instance update INSTANCE_ID [flags] ``` -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"] + -o, --output-format string Output format, one of ["json" "pretty" "none" "yaml"] -p, --project-id string Project ID --verbosity string Verbosity of the CLI, one of ["debug" "info" "warning" "error"] (default "info") ``` diff --git a/docs/stackit_mariadb_plans.md b/docs/stackit_mariadb_plans.md index cc179c71d..4ad4db981 100644 --- a/docs/stackit_mariadb_plans.md +++ b/docs/stackit_mariadb_plans.md @@ -35,7 +35,7 @@ stackit mariadb plans [flags] ``` -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"] + -o, --output-format string Output format, one of ["json" "pretty" "none" "yaml"] -p, --project-id string Project ID --verbosity string Verbosity of the CLI, one of ["debug" "info" "warning" "error"] (default "info") ``` diff --git a/docs/stackit_mongodbflex.md b/docs/stackit_mongodbflex.md index d44688246..3d6810d68 100644 --- a/docs/stackit_mongodbflex.md +++ b/docs/stackit_mongodbflex.md @@ -21,7 +21,7 @@ stackit mongodbflex [flags] ``` -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"] + -o, --output-format string Output format, one of ["json" "pretty" "none" "yaml"] -p, --project-id string Project ID --verbosity string Verbosity of the CLI, one of ["debug" "info" "warning" "error"] (default "info") ``` @@ -29,6 +29,7 @@ stackit mongodbflex [flags] ### SEE ALSO * [stackit](./stackit.md) - Manage STACKIT resources using the command line +* [stackit mongodbflex backup](./stackit_mongodbflex_backup.md) - Provides functionality for MongoDB Flex instance backups * [stackit mongodbflex instance](./stackit_mongodbflex_instance.md) - Provides functionality for MongoDB Flex instances * [stackit mongodbflex options](./stackit_mongodbflex_options.md) - Lists MongoDB Flex options * [stackit mongodbflex user](./stackit_mongodbflex_user.md) - Provides functionality for MongoDB Flex users diff --git a/docs/stackit_mongodbflex_backup.md b/docs/stackit_mongodbflex_backup.md new file mode 100644 index 000000000..de46caf3a --- /dev/null +++ b/docs/stackit_mongodbflex_backup.md @@ -0,0 +1,38 @@ +## stackit mongodbflex backup + +Provides functionality for MongoDB Flex instance backups + +### Synopsis + +Provides functionality for MongoDB Flex instance backups. + +``` +stackit mongodbflex backup [flags] +``` + +### Options + +``` + -h, --help Help for "stackit mongodbflex backup" +``` + +### 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 + --verbosity string Verbosity of the CLI, one of ["debug" "info" "warning" "error"] (default "info") +``` + +### SEE ALSO + +* [stackit mongodbflex](./stackit_mongodbflex.md) - Provides functionality for MongoDB Flex +* [stackit mongodbflex backup describe](./stackit_mongodbflex_backup_describe.md) - Shows details of a backup for a MongoDB Flex instance +* [stackit mongodbflex backup list](./stackit_mongodbflex_backup_list.md) - Lists all backups which are available for a MongoDB Flex instance +* [stackit mongodbflex backup restore](./stackit_mongodbflex_backup_restore.md) - Restores a MongoDB Flex instance from a backup +* [stackit mongodbflex backup restore-jobs](./stackit_mongodbflex_backup_restore-jobs.md) - Lists all restore jobs which have been run for a MongoDB Flex instance +* [stackit mongodbflex backup schedule](./stackit_mongodbflex_backup_schedule.md) - Shows details of the backup schedule and retention policy of a MongoDB Flex instance +* [stackit mongodbflex backup update-schedule](./stackit_mongodbflex_backup_update-schedule.md) - Updates the backup schedule and retention policy for a MongoDB Flex instance + diff --git a/docs/stackit_mongodbflex_backup_describe.md b/docs/stackit_mongodbflex_backup_describe.md new file mode 100644 index 000000000..bb422f064 --- /dev/null +++ b/docs/stackit_mongodbflex_backup_describe.md @@ -0,0 +1,43 @@ +## stackit mongodbflex backup describe + +Shows details of a backup for a MongoDB Flex instance + +### Synopsis + +Shows details of a backup for a MongoDB Flex instance. + +``` +stackit mongodbflex backup describe BACKUP_ID [flags] +``` + +### Examples + +``` + Get details of a backup with ID "xxx" for a MongoDB Flex instance with ID "yyy" + $ stackit mongodbflex backup describe xxx --instance-id yyy + + Get details of a backup with ID "xxx" for a MongoDB Flex instance with ID "yyy" in JSON format + $ stackit mongodbflex backup describe xxx --instance-id yyy --output-format json +``` + +### Options + +``` + -h, --help Help for "stackit mongodbflex backup describe" + --instance-id string Instance ID +``` + +### 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 + --verbosity string Verbosity of the CLI, one of ["debug" "info" "warning" "error"] (default "info") +``` + +### SEE ALSO + +* [stackit mongodbflex backup](./stackit_mongodbflex_backup.md) - Provides functionality for MongoDB Flex instance backups + diff --git a/docs/stackit_mongodbflex_backup_list.md b/docs/stackit_mongodbflex_backup_list.md new file mode 100644 index 000000000..611ae3a22 --- /dev/null +++ b/docs/stackit_mongodbflex_backup_list.md @@ -0,0 +1,47 @@ +## stackit mongodbflex backup list + +Lists all backups which are available for a MongoDB Flex instance + +### Synopsis + +Lists all backups which are available for a MongoDB Flex instance. + +``` +stackit mongodbflex backup list [flags] +``` + +### Examples + +``` + List all backups of instance with ID "xxx" + $ stackit mongodbflex backup list --instance-id xxx + + List all backups of instance with ID "xxx" in JSON format + $ stackit mongodbflex backup list --instance-id xxx --output-format json + + List up to 10 backups of instance with ID "xxx" + $ stackit mongodbflex backup list --instance-id xxx --limit 10 +``` + +### Options + +``` + -h, --help Help for "stackit mongodbflex backup list" + --instance-id string Instance ID + --limit int Maximum number of entries to list +``` + +### 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 + --verbosity string Verbosity of the CLI, one of ["debug" "info" "warning" "error"] (default "info") +``` + +### SEE ALSO + +* [stackit mongodbflex backup](./stackit_mongodbflex_backup.md) - Provides functionality for MongoDB Flex instance backups + diff --git a/docs/stackit_mongodbflex_backup_restore-jobs.md b/docs/stackit_mongodbflex_backup_restore-jobs.md new file mode 100644 index 000000000..8b909182b --- /dev/null +++ b/docs/stackit_mongodbflex_backup_restore-jobs.md @@ -0,0 +1,47 @@ +## stackit mongodbflex backup restore-jobs + +Lists all restore jobs which have been run for a MongoDB Flex instance + +### Synopsis + +Lists all restore jobs which have been run for a MongoDB Flex instance. + +``` +stackit mongodbflex backup restore-jobs [flags] +``` + +### Examples + +``` + List all restore jobs of instance with ID "xxx" + $ stackit mongodbflex backup restore-jobs --instance-id xxx + + List all restore jobs of instance with ID "xxx" in JSON format + $ stackit mongodbflex backup restore-jobs --instance-id xxx --output-format json + + List up to 10 restore jobs of instance with ID "xxx" + $ stackit mongodbflex backup restore-jobs --instance-id xxx --limit 10 +``` + +### Options + +``` + -h, --help Help for "stackit mongodbflex backup restore-jobs" + --instance-id string Instance ID + --limit int Maximum number of entries to list +``` + +### 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 + --verbosity string Verbosity of the CLI, one of ["debug" "info" "warning" "error"] (default "info") +``` + +### SEE ALSO + +* [stackit mongodbflex backup](./stackit_mongodbflex_backup.md) - Provides functionality for MongoDB Flex instance backups + diff --git a/docs/stackit_mongodbflex_backup_restore.md b/docs/stackit_mongodbflex_backup_restore.md new file mode 100644 index 000000000..87185a3e2 --- /dev/null +++ b/docs/stackit_mongodbflex_backup_restore.md @@ -0,0 +1,51 @@ +## stackit mongodbflex backup restore + +Restores a MongoDB Flex instance from a backup + +### Synopsis + +Restores a MongoDB Flex instance from a backup of an instance or clones a MongoDB Flex instance from a point-in-time snapshot. +The backup is specified by a backup ID and the point-in-time snapshot is specified by a timestamp. +You can specify the instance to which the backup will be applied. If not specified, the backup will be applied to the same instance from which it was taken. + +``` +stackit mongodbflex backup restore [flags] +``` + +### Examples + +``` + Restore a MongoDB Flex instance with ID "yyy" using backup with ID "zzz" + $ stackit mongodbflex backup restore --instance-id yyy --backup-id zzz + + Clone a MongoDB Flex instance with ID "yyy" via point-in-time restore to timestamp "2024-05-14T14:31:48Z" + $ stackit mongodbflex backup restore --instance-id yyy --timestamp 2024-05-14T14:31:48Z + + Restore a MongoDB Flex instance with ID "yyy", using backup from instance with ID "zzz" with backup ID "xxx" + $ stackit mongodbflex backup restore --instance-id zzz --backup-instance-id yyy --backup-id xxx +``` + +### Options + +``` + --backup-id string Backup ID + --backup-instance-id string Instance ID of the target instance to restore the backup to + -h, --help Help for "stackit mongodbflex backup restore" + --instance-id string Instance ID + --timestamp string Timestamp of the snapshot to use as a source for cloning the instance in a date-time with the RFC3339 layout format, e.g. 2024-01-01T00:00:00Z +``` + +### 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 + --verbosity string Verbosity of the CLI, one of ["debug" "info" "warning" "error"] (default "info") +``` + +### SEE ALSO + +* [stackit mongodbflex backup](./stackit_mongodbflex_backup.md) - Provides functionality for MongoDB Flex instance backups + diff --git a/docs/stackit_mongodbflex_backup_schedule.md b/docs/stackit_mongodbflex_backup_schedule.md new file mode 100644 index 000000000..945d5081b --- /dev/null +++ b/docs/stackit_mongodbflex_backup_schedule.md @@ -0,0 +1,43 @@ +## stackit mongodbflex backup schedule + +Shows details of the backup schedule and retention policy of a MongoDB Flex instance + +### Synopsis + +Shows details of the backup schedule and retention policy of a MongoDB Flex instance. + +``` +stackit mongodbflex backup schedule [flags] +``` + +### Examples + +``` + Get details of the backup schedule of a MongoDB Flex instance with ID "xxx" + $ stackit mongodbflex backup schedule --instance-id xxx + + Get details of the backup schedule of a MongoDB Flex instance with ID "xxx" in JSON format + $ stackit mongodbflex backup schedule --instance-id xxx --output-format json +``` + +### Options + +``` + -h, --help Help for "stackit mongodbflex backup schedule" + --instance-id string Instance ID +``` + +### 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 + --verbosity string Verbosity of the CLI, one of ["debug" "info" "warning" "error"] (default "info") +``` + +### SEE ALSO + +* [stackit mongodbflex backup](./stackit_mongodbflex_backup.md) - Provides functionality for MongoDB Flex instance backups + diff --git a/docs/stackit_mongodbflex_backup_update-schedule.md b/docs/stackit_mongodbflex_backup_update-schedule.md new file mode 100644 index 000000000..ee51091cf --- /dev/null +++ b/docs/stackit_mongodbflex_backup_update-schedule.md @@ -0,0 +1,51 @@ +## stackit mongodbflex backup update-schedule + +Updates the backup schedule and retention policy for a MongoDB Flex instance + +### Synopsis + +Updates the backup schedule and retention policy for a MongoDB Flex instance. +The current backup schedule and retention policy can be seen in the output of the "stackit mongodbflex backup schedule" command. +The backup schedule is defined in the cron scheduling system format e.g. '0 0 * * *'. +See below for more detail on the retention policy options. + +``` +stackit mongodbflex backup update-schedule [flags] +``` + +### Examples + +``` + Update the backup schedule of a MongoDB Flex instance with ID "xxx" + $ stackit mongodbflex backup update-schedule --instance-id xxx --schedule '6 6 * * *' + + Update the retention days for snapshots of a MongoDB Flex instance with ID "xxx" to 5 days + $ stackit mongodbflex backup update-schedule --instance-id xxx --save-snapshot-days 5 +``` + +### Options + +``` + -h, --help Help for "stackit mongodbflex backup update-schedule" + --instance-id string Instance ID + --save-daily-snapshot-days int Number of days to retain daily snapshots. Should be less than or equal to the number of days of the selected weekly or monthly value. + --save-monthly-snapshot-months int Number of months to retain monthly snapshots + --save-snapshot-days int Number of days to retain snapshots. Should be less than or equal to the value of the daily backup. + --save-weekly-snapshot-weeks int Number of weeks to retain weekly snapshots. Should be less than or equal to the number of weeks of the selected monthly value. + --schedule string Backup schedule, in the cron scheduling system format e.g. '0 0 * * *' +``` + +### 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 + --verbosity string Verbosity of the CLI, one of ["debug" "info" "warning" "error"] (default "info") +``` + +### SEE ALSO + +* [stackit mongodbflex backup](./stackit_mongodbflex_backup.md) - Provides functionality for MongoDB Flex instance backups + diff --git a/docs/stackit_mongodbflex_instance.md b/docs/stackit_mongodbflex_instance.md index 5c470ac76..c38c90538 100644 --- a/docs/stackit_mongodbflex_instance.md +++ b/docs/stackit_mongodbflex_instance.md @@ -21,7 +21,7 @@ stackit mongodbflex instance [flags] ``` -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"] + -o, --output-format string Output format, one of ["json" "pretty" "none" "yaml"] -p, --project-id string Project ID --verbosity string Verbosity of the CLI, one of ["debug" "info" "warning" "error"] (default "info") ``` diff --git a/docs/stackit_mongodbflex_instance_create.md b/docs/stackit_mongodbflex_instance_create.md index 141b5c73a..2b424836f 100644 --- a/docs/stackit_mongodbflex_instance_create.md +++ b/docs/stackit_mongodbflex_instance_create.md @@ -44,7 +44,7 @@ stackit mongodbflex instance create [flags] ``` -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"] + -o, --output-format string Output format, one of ["json" "pretty" "none" "yaml"] -p, --project-id string Project ID --verbosity string Verbosity of the CLI, one of ["debug" "info" "warning" "error"] (default "info") ``` diff --git a/docs/stackit_mongodbflex_instance_delete.md b/docs/stackit_mongodbflex_instance_delete.md index 1486979ae..88718bb37 100644 --- a/docs/stackit_mongodbflex_instance_delete.md +++ b/docs/stackit_mongodbflex_instance_delete.md @@ -28,7 +28,7 @@ stackit mongodbflex instance delete INSTANCE_ID [flags] ``` -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"] + -o, --output-format string Output format, one of ["json" "pretty" "none" "yaml"] -p, --project-id string Project ID --verbosity string Verbosity of the CLI, one of ["debug" "info" "warning" "error"] (default "info") ``` diff --git a/docs/stackit_mongodbflex_instance_describe.md b/docs/stackit_mongodbflex_instance_describe.md index 0d07e5d7f..870da5e13 100644 --- a/docs/stackit_mongodbflex_instance_describe.md +++ b/docs/stackit_mongodbflex_instance_describe.md @@ -31,7 +31,7 @@ stackit mongodbflex instance describe INSTANCE_ID [flags] ``` -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"] + -o, --output-format string Output format, one of ["json" "pretty" "none" "yaml"] -p, --project-id string Project ID --verbosity string Verbosity of the CLI, one of ["debug" "info" "warning" "error"] (default "info") ``` diff --git a/docs/stackit_mongodbflex_instance_list.md b/docs/stackit_mongodbflex_instance_list.md index d3027c649..3b3995ed6 100644 --- a/docs/stackit_mongodbflex_instance_list.md +++ b/docs/stackit_mongodbflex_instance_list.md @@ -35,7 +35,7 @@ stackit mongodbflex instance list [flags] ``` -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"] + -o, --output-format string Output format, one of ["json" "pretty" "none" "yaml"] -p, --project-id string Project ID --verbosity string Verbosity of the CLI, one of ["debug" "info" "warning" "error"] (default "info") ``` diff --git a/docs/stackit_mongodbflex_instance_update.md b/docs/stackit_mongodbflex_instance_update.md index e6c1c1b3d..ef64d5088 100644 --- a/docs/stackit_mongodbflex_instance_update.md +++ b/docs/stackit_mongodbflex_instance_update.md @@ -41,7 +41,7 @@ stackit mongodbflex instance update INSTANCE_ID [flags] ``` -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"] + -o, --output-format string Output format, one of ["json" "pretty" "none" "yaml"] -p, --project-id string Project ID --verbosity string Verbosity of the CLI, one of ["debug" "info" "warning" "error"] (default "info") ``` diff --git a/docs/stackit_mongodbflex_options.md b/docs/stackit_mongodbflex_options.md index d1bab2db1..b695104a8 100644 --- a/docs/stackit_mongodbflex_options.md +++ b/docs/stackit_mongodbflex_options.md @@ -39,7 +39,7 @@ stackit mongodbflex options [flags] ``` -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"] + -o, --output-format string Output format, one of ["json" "pretty" "none" "yaml"] -p, --project-id string Project ID --verbosity string Verbosity of the CLI, one of ["debug" "info" "warning" "error"] (default "info") ``` diff --git a/docs/stackit_mongodbflex_user.md b/docs/stackit_mongodbflex_user.md index 2cfcf5ea4..7b45211b7 100644 --- a/docs/stackit_mongodbflex_user.md +++ b/docs/stackit_mongodbflex_user.md @@ -21,7 +21,7 @@ stackit mongodbflex user [flags] ``` -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"] + -o, --output-format string Output format, one of ["json" "pretty" "none" "yaml"] -p, --project-id string Project ID --verbosity string Verbosity of the CLI, one of ["debug" "info" "warning" "error"] (default "info") ``` diff --git a/docs/stackit_mongodbflex_user_create.md b/docs/stackit_mongodbflex_user_create.md index 71fa36df5..6a6645be0 100644 --- a/docs/stackit_mongodbflex_user_create.md +++ b/docs/stackit_mongodbflex_user_create.md @@ -38,7 +38,7 @@ stackit mongodbflex user create [flags] ``` -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"] + -o, --output-format string Output format, one of ["json" "pretty" "none" "yaml"] -p, --project-id string Project ID --verbosity string Verbosity of the CLI, one of ["debug" "info" "warning" "error"] (default "info") ``` diff --git a/docs/stackit_mongodbflex_user_delete.md b/docs/stackit_mongodbflex_user_delete.md index da9383752..6297faa1f 100644 --- a/docs/stackit_mongodbflex_user_delete.md +++ b/docs/stackit_mongodbflex_user_delete.md @@ -30,7 +30,7 @@ stackit mongodbflex user delete USER_ID [flags] ``` -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"] + -o, --output-format string Output format, one of ["json" "pretty" "none" "yaml"] -p, --project-id string Project ID --verbosity string Verbosity of the CLI, one of ["debug" "info" "warning" "error"] (default "info") ``` diff --git a/docs/stackit_mongodbflex_user_describe.md b/docs/stackit_mongodbflex_user_describe.md index 7b19e5da9..2295b1c84 100644 --- a/docs/stackit_mongodbflex_user_describe.md +++ b/docs/stackit_mongodbflex_user_describe.md @@ -34,7 +34,7 @@ stackit mongodbflex user describe USER_ID [flags] ``` -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"] + -o, --output-format string Output format, one of ["json" "pretty" "none" "yaml"] -p, --project-id string Project ID --verbosity string Verbosity of the CLI, one of ["debug" "info" "warning" "error"] (default "info") ``` diff --git a/docs/stackit_mongodbflex_user_list.md b/docs/stackit_mongodbflex_user_list.md index 1cfa7a703..df3305a73 100644 --- a/docs/stackit_mongodbflex_user_list.md +++ b/docs/stackit_mongodbflex_user_list.md @@ -36,7 +36,7 @@ stackit mongodbflex user list [flags] ``` -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"] + -o, --output-format string Output format, one of ["json" "pretty" "none" "yaml"] -p, --project-id string Project ID --verbosity string Verbosity of the CLI, one of ["debug" "info" "warning" "error"] (default "info") ``` diff --git a/docs/stackit_mongodbflex_user_reset-password.md b/docs/stackit_mongodbflex_user_reset-password.md index 339cd6a8d..e269e1ff7 100644 --- a/docs/stackit_mongodbflex_user_reset-password.md +++ b/docs/stackit_mongodbflex_user_reset-password.md @@ -30,7 +30,7 @@ stackit mongodbflex user reset-password USER_ID [flags] ``` -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"] + -o, --output-format string Output format, one of ["json" "pretty" "none" "yaml"] -p, --project-id string Project ID --verbosity string Verbosity of the CLI, one of ["debug" "info" "warning" "error"] (default "info") ``` diff --git a/docs/stackit_mongodbflex_user_update.md b/docs/stackit_mongodbflex_user_update.md index 011c0220b..3702eb36c 100644 --- a/docs/stackit_mongodbflex_user_update.md +++ b/docs/stackit_mongodbflex_user_update.md @@ -31,7 +31,7 @@ stackit mongodbflex user update USER_ID [flags] ``` -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"] + -o, --output-format string Output format, one of ["json" "pretty" "none" "yaml"] -p, --project-id string Project ID --verbosity string Verbosity of the CLI, one of ["debug" "info" "warning" "error"] (default "info") ``` diff --git a/docs/stackit_object-storage.md b/docs/stackit_object-storage.md index 6f13ef1e4..676bc3110 100644 --- a/docs/stackit_object-storage.md +++ b/docs/stackit_object-storage.md @@ -21,7 +21,7 @@ stackit object-storage [flags] ``` -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"] + -o, --output-format string Output format, one of ["json" "pretty" "none" "yaml"] -p, --project-id string Project ID --verbosity string Verbosity of the CLI, one of ["debug" "info" "warning" "error"] (default "info") ``` diff --git a/docs/stackit_object-storage_bucket.md b/docs/stackit_object-storage_bucket.md index 138d6afcf..b8d8df4b6 100644 --- a/docs/stackit_object-storage_bucket.md +++ b/docs/stackit_object-storage_bucket.md @@ -21,7 +21,7 @@ stackit object-storage bucket [flags] ``` -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"] + -o, --output-format string Output format, one of ["json" "pretty" "none" "yaml"] -p, --project-id string Project ID --verbosity string Verbosity of the CLI, one of ["debug" "info" "warning" "error"] (default "info") ``` diff --git a/docs/stackit_object-storage_bucket_create.md b/docs/stackit_object-storage_bucket_create.md index 95851f1f0..f22115b06 100644 --- a/docs/stackit_object-storage_bucket_create.md +++ b/docs/stackit_object-storage_bucket_create.md @@ -28,7 +28,7 @@ stackit object-storage bucket create BUCKET_NAME [flags] ``` -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"] + -o, --output-format string Output format, one of ["json" "pretty" "none" "yaml"] -p, --project-id string Project ID --verbosity string Verbosity of the CLI, one of ["debug" "info" "warning" "error"] (default "info") ``` diff --git a/docs/stackit_object-storage_bucket_delete.md b/docs/stackit_object-storage_bucket_delete.md index d93981f09..56380a641 100644 --- a/docs/stackit_object-storage_bucket_delete.md +++ b/docs/stackit_object-storage_bucket_delete.md @@ -28,7 +28,7 @@ stackit object-storage bucket delete BUCKET_NAME [flags] ``` -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"] + -o, --output-format string Output format, one of ["json" "pretty" "none" "yaml"] -p, --project-id string Project ID --verbosity string Verbosity of the CLI, one of ["debug" "info" "warning" "error"] (default "info") ``` diff --git a/docs/stackit_object-storage_bucket_describe.md b/docs/stackit_object-storage_bucket_describe.md index 132161e79..7520a8217 100644 --- a/docs/stackit_object-storage_bucket_describe.md +++ b/docs/stackit_object-storage_bucket_describe.md @@ -31,7 +31,7 @@ stackit object-storage bucket describe BUCKET_NAME [flags] ``` -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"] + -o, --output-format string Output format, one of ["json" "pretty" "none" "yaml"] -p, --project-id string Project ID --verbosity string Verbosity of the CLI, one of ["debug" "info" "warning" "error"] (default "info") ``` diff --git a/docs/stackit_object-storage_bucket_list.md b/docs/stackit_object-storage_bucket_list.md index b9ec5faee..11cd639fb 100644 --- a/docs/stackit_object-storage_bucket_list.md +++ b/docs/stackit_object-storage_bucket_list.md @@ -35,7 +35,7 @@ stackit object-storage bucket list [flags] ``` -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"] + -o, --output-format string Output format, one of ["json" "pretty" "none" "yaml"] -p, --project-id string Project ID --verbosity string Verbosity of the CLI, one of ["debug" "info" "warning" "error"] (default "info") ``` diff --git a/docs/stackit_object-storage_credentials-group.md b/docs/stackit_object-storage_credentials-group.md index 958b29bd3..3c028ed12 100644 --- a/docs/stackit_object-storage_credentials-group.md +++ b/docs/stackit_object-storage_credentials-group.md @@ -21,7 +21,7 @@ stackit object-storage credentials-group [flags] ``` -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"] + -o, --output-format string Output format, one of ["json" "pretty" "none" "yaml"] -p, --project-id string Project ID --verbosity string Verbosity of the CLI, one of ["debug" "info" "warning" "error"] (default "info") ``` diff --git a/docs/stackit_object-storage_credentials-group_create.md b/docs/stackit_object-storage_credentials-group_create.md index 37ff1c105..291be494a 100644 --- a/docs/stackit_object-storage_credentials-group_create.md +++ b/docs/stackit_object-storage_credentials-group_create.md @@ -29,7 +29,7 @@ stackit object-storage credentials-group create [flags] ``` -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"] + -o, --output-format string Output format, one of ["json" "pretty" "none" "yaml"] -p, --project-id string Project ID --verbosity string Verbosity of the CLI, one of ["debug" "info" "warning" "error"] (default "info") ``` diff --git a/docs/stackit_object-storage_credentials-group_delete.md b/docs/stackit_object-storage_credentials-group_delete.md index 5c36b6299..d9d94802a 100644 --- a/docs/stackit_object-storage_credentials-group_delete.md +++ b/docs/stackit_object-storage_credentials-group_delete.md @@ -28,7 +28,7 @@ stackit object-storage credentials-group delete CREDENTIALS_GROUP_ID [flags] ``` -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"] + -o, --output-format string Output format, one of ["json" "pretty" "none" "yaml"] -p, --project-id string Project ID --verbosity string Verbosity of the CLI, one of ["debug" "info" "warning" "error"] (default "info") ``` diff --git a/docs/stackit_object-storage_credentials-group_list.md b/docs/stackit_object-storage_credentials-group_list.md index f66df9fc5..bd91a8942 100644 --- a/docs/stackit_object-storage_credentials-group_list.md +++ b/docs/stackit_object-storage_credentials-group_list.md @@ -35,7 +35,7 @@ stackit object-storage credentials-group list [flags] ``` -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"] + -o, --output-format string Output format, one of ["json" "pretty" "none" "yaml"] -p, --project-id string Project ID --verbosity string Verbosity of the CLI, one of ["debug" "info" "warning" "error"] (default "info") ``` diff --git a/docs/stackit_object-storage_credentials.md b/docs/stackit_object-storage_credentials.md index d135bc8b5..de61b0195 100644 --- a/docs/stackit_object-storage_credentials.md +++ b/docs/stackit_object-storage_credentials.md @@ -21,7 +21,7 @@ stackit object-storage credentials [flags] ``` -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"] + -o, --output-format string Output format, one of ["json" "pretty" "none" "yaml"] -p, --project-id string Project ID --verbosity string Verbosity of the CLI, one of ["debug" "info" "warning" "error"] (default "info") ``` diff --git a/docs/stackit_object-storage_credentials_create.md b/docs/stackit_object-storage_credentials_create.md index 92a45a3c1..6e91d075a 100644 --- a/docs/stackit_object-storage_credentials_create.md +++ b/docs/stackit_object-storage_credentials_create.md @@ -33,7 +33,7 @@ stackit object-storage credentials create [flags] ``` -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"] + -o, --output-format string Output format, one of ["json" "pretty" "none" "yaml"] -p, --project-id string Project ID --verbosity string Verbosity of the CLI, one of ["debug" "info" "warning" "error"] (default "info") ``` diff --git a/docs/stackit_object-storage_credentials_delete.md b/docs/stackit_object-storage_credentials_delete.md index e44f350b5..b14154ec8 100644 --- a/docs/stackit_object-storage_credentials_delete.md +++ b/docs/stackit_object-storage_credentials_delete.md @@ -29,7 +29,7 @@ stackit object-storage credentials delete CREDENTIALS_ID [flags] ``` -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"] + -o, --output-format string Output format, one of ["json" "pretty" "none" "yaml"] -p, --project-id string Project ID --verbosity string Verbosity of the CLI, one of ["debug" "info" "warning" "error"] (default "info") ``` diff --git a/docs/stackit_object-storage_credentials_list.md b/docs/stackit_object-storage_credentials_list.md index be0b501ec..647c024b5 100644 --- a/docs/stackit_object-storage_credentials_list.md +++ b/docs/stackit_object-storage_credentials_list.md @@ -36,7 +36,7 @@ stackit object-storage credentials list [flags] ``` -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"] + -o, --output-format string Output format, one of ["json" "pretty" "none" "yaml"] -p, --project-id string Project ID --verbosity string Verbosity of the CLI, one of ["debug" "info" "warning" "error"] (default "info") ``` diff --git a/docs/stackit_object-storage_disable.md b/docs/stackit_object-storage_disable.md index 7cf45af0b..0699e5249 100644 --- a/docs/stackit_object-storage_disable.md +++ b/docs/stackit_object-storage_disable.md @@ -28,7 +28,7 @@ stackit object-storage disable [flags] ``` -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"] + -o, --output-format string Output format, one of ["json" "pretty" "none" "yaml"] -p, --project-id string Project ID --verbosity string Verbosity of the CLI, one of ["debug" "info" "warning" "error"] (default "info") ``` diff --git a/docs/stackit_object-storage_enable.md b/docs/stackit_object-storage_enable.md index d9eee4bfb..441997bd6 100644 --- a/docs/stackit_object-storage_enable.md +++ b/docs/stackit_object-storage_enable.md @@ -28,7 +28,7 @@ stackit object-storage enable [flags] ``` -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"] + -o, --output-format string Output format, one of ["json" "pretty" "none" "yaml"] -p, --project-id string Project ID --verbosity string Verbosity of the CLI, one of ["debug" "info" "warning" "error"] (default "info") ``` diff --git a/docs/stackit_opensearch.md b/docs/stackit_opensearch.md index d20efcea2..30af3bd1f 100644 --- a/docs/stackit_opensearch.md +++ b/docs/stackit_opensearch.md @@ -21,7 +21,7 @@ stackit opensearch [flags] ``` -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"] + -o, --output-format string Output format, one of ["json" "pretty" "none" "yaml"] -p, --project-id string Project ID --verbosity string Verbosity of the CLI, one of ["debug" "info" "warning" "error"] (default "info") ``` diff --git a/docs/stackit_opensearch_credentials.md b/docs/stackit_opensearch_credentials.md index deed441c6..13358215e 100644 --- a/docs/stackit_opensearch_credentials.md +++ b/docs/stackit_opensearch_credentials.md @@ -21,7 +21,7 @@ stackit opensearch credentials [flags] ``` -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"] + -o, --output-format string Output format, one of ["json" "pretty" "none" "yaml"] -p, --project-id string Project ID --verbosity string Verbosity of the CLI, one of ["debug" "info" "warning" "error"] (default "info") ``` diff --git a/docs/stackit_opensearch_credentials_create.md b/docs/stackit_opensearch_credentials_create.md index 2f2ab2c0d..c90ae9f11 100644 --- a/docs/stackit_opensearch_credentials_create.md +++ b/docs/stackit_opensearch_credentials_create.md @@ -33,7 +33,7 @@ stackit opensearch credentials create [flags] ``` -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"] + -o, --output-format string Output format, one of ["json" "pretty" "none" "yaml"] -p, --project-id string Project ID --verbosity string Verbosity of the CLI, one of ["debug" "info" "warning" "error"] (default "info") ``` diff --git a/docs/stackit_opensearch_credentials_delete.md b/docs/stackit_opensearch_credentials_delete.md index e144a0ce6..e19e2a091 100644 --- a/docs/stackit_opensearch_credentials_delete.md +++ b/docs/stackit_opensearch_credentials_delete.md @@ -29,7 +29,7 @@ stackit opensearch credentials delete CREDENTIALS_ID [flags] ``` -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"] + -o, --output-format string Output format, one of ["json" "pretty" "none" "yaml"] -p, --project-id string Project ID --verbosity string Verbosity of the CLI, one of ["debug" "info" "warning" "error"] (default "info") ``` diff --git a/docs/stackit_opensearch_credentials_describe.md b/docs/stackit_opensearch_credentials_describe.md index 87159d515..bb0095b67 100644 --- a/docs/stackit_opensearch_credentials_describe.md +++ b/docs/stackit_opensearch_credentials_describe.md @@ -32,7 +32,7 @@ stackit opensearch credentials describe CREDENTIALS_ID [flags] ``` -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"] + -o, --output-format string Output format, one of ["json" "pretty" "none" "yaml"] -p, --project-id string Project ID --verbosity string Verbosity of the CLI, one of ["debug" "info" "warning" "error"] (default "info") ``` diff --git a/docs/stackit_opensearch_credentials_list.md b/docs/stackit_opensearch_credentials_list.md index 41d5b9ff0..6aea2e1c7 100644 --- a/docs/stackit_opensearch_credentials_list.md +++ b/docs/stackit_opensearch_credentials_list.md @@ -36,7 +36,7 @@ stackit opensearch credentials list [flags] ``` -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"] + -o, --output-format string Output format, one of ["json" "pretty" "none" "yaml"] -p, --project-id string Project ID --verbosity string Verbosity of the CLI, one of ["debug" "info" "warning" "error"] (default "info") ``` diff --git a/docs/stackit_opensearch_instance.md b/docs/stackit_opensearch_instance.md index 968d343af..26182779c 100644 --- a/docs/stackit_opensearch_instance.md +++ b/docs/stackit_opensearch_instance.md @@ -21,7 +21,7 @@ stackit opensearch instance [flags] ``` -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"] + -o, --output-format string Output format, one of ["json" "pretty" "none" "yaml"] -p, --project-id string Project ID --verbosity string Verbosity of the CLI, one of ["debug" "info" "warning" "error"] (default "info") ``` diff --git a/docs/stackit_opensearch_instance_create.md b/docs/stackit_opensearch_instance_create.md index 45f32f32c..ef67d29f1 100644 --- a/docs/stackit_opensearch_instance_create.md +++ b/docs/stackit_opensearch_instance_create.md @@ -46,7 +46,7 @@ stackit opensearch instance create [flags] ``` -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"] + -o, --output-format string Output format, one of ["json" "pretty" "none" "yaml"] -p, --project-id string Project ID --verbosity string Verbosity of the CLI, one of ["debug" "info" "warning" "error"] (default "info") ``` diff --git a/docs/stackit_opensearch_instance_delete.md b/docs/stackit_opensearch_instance_delete.md index ba6c0e571..49c4ab8ee 100644 --- a/docs/stackit_opensearch_instance_delete.md +++ b/docs/stackit_opensearch_instance_delete.md @@ -28,7 +28,7 @@ stackit opensearch instance delete INSTANCE_ID [flags] ``` -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"] + -o, --output-format string Output format, one of ["json" "pretty" "none" "yaml"] -p, --project-id string Project ID --verbosity string Verbosity of the CLI, one of ["debug" "info" "warning" "error"] (default "info") ``` diff --git a/docs/stackit_opensearch_instance_describe.md b/docs/stackit_opensearch_instance_describe.md index 7dc48d49f..3aafa406d 100644 --- a/docs/stackit_opensearch_instance_describe.md +++ b/docs/stackit_opensearch_instance_describe.md @@ -31,7 +31,7 @@ stackit opensearch instance describe INSTANCE_ID [flags] ``` -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"] + -o, --output-format string Output format, one of ["json" "pretty" "none" "yaml"] -p, --project-id string Project ID --verbosity string Verbosity of the CLI, one of ["debug" "info" "warning" "error"] (default "info") ``` diff --git a/docs/stackit_opensearch_instance_list.md b/docs/stackit_opensearch_instance_list.md index 27b622e58..a67f9821f 100644 --- a/docs/stackit_opensearch_instance_list.md +++ b/docs/stackit_opensearch_instance_list.md @@ -35,7 +35,7 @@ stackit opensearch instance list [flags] ``` -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"] + -o, --output-format string Output format, one of ["json" "pretty" "none" "yaml"] -p, --project-id string Project ID --verbosity string Verbosity of the CLI, one of ["debug" "info" "warning" "error"] (default "info") ``` diff --git a/docs/stackit_opensearch_instance_update.md b/docs/stackit_opensearch_instance_update.md index e50f7f7a7..5d688fdc3 100644 --- a/docs/stackit_opensearch_instance_update.md +++ b/docs/stackit_opensearch_instance_update.md @@ -42,7 +42,7 @@ stackit opensearch instance update INSTANCE_ID [flags] ``` -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"] + -o, --output-format string Output format, one of ["json" "pretty" "none" "yaml"] -p, --project-id string Project ID --verbosity string Verbosity of the CLI, one of ["debug" "info" "warning" "error"] (default "info") ``` diff --git a/docs/stackit_opensearch_plans.md b/docs/stackit_opensearch_plans.md index 97925f34a..f733314ab 100644 --- a/docs/stackit_opensearch_plans.md +++ b/docs/stackit_opensearch_plans.md @@ -35,7 +35,7 @@ stackit opensearch plans [flags] ``` -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"] + -o, --output-format string Output format, one of ["json" "pretty" "none" "yaml"] -p, --project-id string Project ID --verbosity string Verbosity of the CLI, one of ["debug" "info" "warning" "error"] (default "info") ``` diff --git a/docs/stackit_organization.md b/docs/stackit_organization.md index 643b64f8d..2c3091969 100644 --- a/docs/stackit_organization.md +++ b/docs/stackit_organization.md @@ -22,7 +22,7 @@ stackit organization [flags] ``` -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"] + -o, --output-format string Output format, one of ["json" "pretty" "none" "yaml"] -p, --project-id string Project ID --verbosity string Verbosity of the CLI, one of ["debug" "info" "warning" "error"] (default "info") ``` diff --git a/docs/stackit_organization_member.md b/docs/stackit_organization_member.md index accffe572..e7651ee04 100644 --- a/docs/stackit_organization_member.md +++ b/docs/stackit_organization_member.md @@ -21,7 +21,7 @@ stackit organization member [flags] ``` -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"] + -o, --output-format string Output format, one of ["json" "pretty" "none" "yaml"] -p, --project-id string Project ID --verbosity string Verbosity of the CLI, one of ["debug" "info" "warning" "error"] (default "info") ``` diff --git a/docs/stackit_organization_member_add.md b/docs/stackit_organization_member_add.md index fa75db1a5..46f7dedf0 100644 --- a/docs/stackit_organization_member_add.md +++ b/docs/stackit_organization_member_add.md @@ -34,7 +34,7 @@ stackit organization member add SUBJECT [flags] ``` -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"] + -o, --output-format string Output format, one of ["json" "pretty" "none" "yaml"] -p, --project-id string Project ID --verbosity string Verbosity of the CLI, one of ["debug" "info" "warning" "error"] (default "info") ``` diff --git a/docs/stackit_organization_member_list.md b/docs/stackit_organization_member_list.md index 13e6112f3..2f3772038 100644 --- a/docs/stackit_organization_member_list.md +++ b/docs/stackit_organization_member_list.md @@ -38,7 +38,7 @@ stackit organization member list [flags] ``` -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"] + -o, --output-format string Output format, one of ["json" "pretty" "none" "yaml"] -p, --project-id string Project ID --verbosity string Verbosity of the CLI, one of ["debug" "info" "warning" "error"] (default "info") ``` diff --git a/docs/stackit_organization_member_remove.md b/docs/stackit_organization_member_remove.md index 745cd22c4..f164052b8 100644 --- a/docs/stackit_organization_member_remove.md +++ b/docs/stackit_organization_member_remove.md @@ -36,7 +36,7 @@ stackit organization member remove SUBJECT [flags] ``` -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"] + -o, --output-format string Output format, one of ["json" "pretty" "none" "yaml"] -p, --project-id string Project ID --verbosity string Verbosity of the CLI, one of ["debug" "info" "warning" "error"] (default "info") ``` diff --git a/docs/stackit_organization_role.md b/docs/stackit_organization_role.md index 9f7675ffb..e95f20a39 100644 --- a/docs/stackit_organization_role.md +++ b/docs/stackit_organization_role.md @@ -21,7 +21,7 @@ stackit organization role [flags] ``` -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"] + -o, --output-format string Output format, one of ["json" "pretty" "none" "yaml"] -p, --project-id string Project ID --verbosity string Verbosity of the CLI, one of ["debug" "info" "warning" "error"] (default "info") ``` diff --git a/docs/stackit_organization_role_list.md b/docs/stackit_organization_role_list.md index bb8690e3e..9f2f9b491 100644 --- a/docs/stackit_organization_role_list.md +++ b/docs/stackit_organization_role_list.md @@ -36,7 +36,7 @@ stackit organization role list [flags] ``` -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"] + -o, --output-format string Output format, one of ["json" "pretty" "none" "yaml"] -p, --project-id string Project ID --verbosity string Verbosity of the CLI, one of ["debug" "info" "warning" "error"] (default "info") ``` diff --git a/docs/stackit_postgresflex.md b/docs/stackit_postgresflex.md index 22d00bf6b..005ec9f19 100644 --- a/docs/stackit_postgresflex.md +++ b/docs/stackit_postgresflex.md @@ -21,7 +21,7 @@ stackit postgresflex [flags] ``` -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"] + -o, --output-format string Output format, one of ["json" "pretty" "none" "yaml"] -p, --project-id string Project ID --verbosity string Verbosity of the CLI, one of ["debug" "info" "warning" "error"] (default "info") ``` diff --git a/docs/stackit_postgresflex_backup.md b/docs/stackit_postgresflex_backup.md index 2a3d9a7e3..c1caacfce 100644 --- a/docs/stackit_postgresflex_backup.md +++ b/docs/stackit_postgresflex_backup.md @@ -21,7 +21,7 @@ stackit postgresflex backup [flags] ``` -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"] + -o, --output-format string Output format, one of ["json" "pretty" "none" "yaml"] -p, --project-id string Project ID --verbosity string Verbosity of the CLI, one of ["debug" "info" "warning" "error"] (default "info") ``` diff --git a/docs/stackit_postgresflex_backup_describe.md b/docs/stackit_postgresflex_backup_describe.md index 81a3309f0..cb5171718 100644 --- a/docs/stackit_postgresflex_backup_describe.md +++ b/docs/stackit_postgresflex_backup_describe.md @@ -32,7 +32,7 @@ stackit postgresflex backup describe BACKUP_ID [flags] ``` -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"] + -o, --output-format string Output format, one of ["json" "pretty" "none" "yaml"] -p, --project-id string Project ID --verbosity string Verbosity of the CLI, one of ["debug" "info" "warning" "error"] (default "info") ``` diff --git a/docs/stackit_postgresflex_backup_list.md b/docs/stackit_postgresflex_backup_list.md index e655afdcf..92b8373f7 100644 --- a/docs/stackit_postgresflex_backup_list.md +++ b/docs/stackit_postgresflex_backup_list.md @@ -36,7 +36,7 @@ stackit postgresflex backup list [flags] ``` -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"] + -o, --output-format string Output format, one of ["json" "pretty" "none" "yaml"] -p, --project-id string Project ID --verbosity string Verbosity of the CLI, one of ["debug" "info" "warning" "error"] (default "info") ``` diff --git a/docs/stackit_postgresflex_backup_update-schedule.md b/docs/stackit_postgresflex_backup_update-schedule.md index 02362430c..7971a9b89 100644 --- a/docs/stackit_postgresflex_backup_update-schedule.md +++ b/docs/stackit_postgresflex_backup_update-schedule.md @@ -30,7 +30,7 @@ stackit postgresflex backup update-schedule [flags] ``` -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"] + -o, --output-format string Output format, one of ["json" "pretty" "none" "yaml"] -p, --project-id string Project ID --verbosity string Verbosity of the CLI, one of ["debug" "info" "warning" "error"] (default "info") ``` diff --git a/docs/stackit_postgresflex_instance.md b/docs/stackit_postgresflex_instance.md index 1376ac743..679dfc2a2 100644 --- a/docs/stackit_postgresflex_instance.md +++ b/docs/stackit_postgresflex_instance.md @@ -21,7 +21,7 @@ stackit postgresflex instance [flags] ``` -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"] + -o, --output-format string Output format, one of ["json" "pretty" "none" "yaml"] -p, --project-id string Project ID --verbosity string Verbosity of the CLI, one of ["debug" "info" "warning" "error"] (default "info") ``` diff --git a/docs/stackit_postgresflex_instance_clone.md b/docs/stackit_postgresflex_instance_clone.md index 609dcab34..d158b15b8 100644 --- a/docs/stackit_postgresflex_instance_clone.md +++ b/docs/stackit_postgresflex_instance_clone.md @@ -37,7 +37,7 @@ stackit postgresflex instance clone INSTANCE_ID [flags] ``` -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"] + -o, --output-format string Output format, one of ["json" "pretty" "none" "yaml"] -p, --project-id string Project ID --verbosity string Verbosity of the CLI, one of ["debug" "info" "warning" "error"] (default "info") ``` diff --git a/docs/stackit_postgresflex_instance_create.md b/docs/stackit_postgresflex_instance_create.md index 112307adf..ea5c1f247 100644 --- a/docs/stackit_postgresflex_instance_create.md +++ b/docs/stackit_postgresflex_instance_create.md @@ -44,7 +44,7 @@ stackit postgresflex instance create [flags] ``` -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"] + -o, --output-format string Output format, one of ["json" "pretty" "none" "yaml"] -p, --project-id string Project ID --verbosity string Verbosity of the CLI, one of ["debug" "info" "warning" "error"] (default "info") ``` diff --git a/docs/stackit_postgresflex_instance_delete.md b/docs/stackit_postgresflex_instance_delete.md index 2461854dd..0f864709e 100644 --- a/docs/stackit_postgresflex_instance_delete.md +++ b/docs/stackit_postgresflex_instance_delete.md @@ -34,7 +34,7 @@ stackit postgresflex instance delete INSTANCE_ID [flags] ``` -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"] + -o, --output-format string Output format, one of ["json" "pretty" "none" "yaml"] -p, --project-id string Project ID --verbosity string Verbosity of the CLI, one of ["debug" "info" "warning" "error"] (default "info") ``` diff --git a/docs/stackit_postgresflex_instance_describe.md b/docs/stackit_postgresflex_instance_describe.md index df2e0434c..957dd5450 100644 --- a/docs/stackit_postgresflex_instance_describe.md +++ b/docs/stackit_postgresflex_instance_describe.md @@ -31,7 +31,7 @@ stackit postgresflex instance describe INSTANCE_ID [flags] ``` -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"] + -o, --output-format string Output format, one of ["json" "pretty" "none" "yaml"] -p, --project-id string Project ID --verbosity string Verbosity of the CLI, one of ["debug" "info" "warning" "error"] (default "info") ``` diff --git a/docs/stackit_postgresflex_instance_list.md b/docs/stackit_postgresflex_instance_list.md index 295baca57..a05845ba0 100644 --- a/docs/stackit_postgresflex_instance_list.md +++ b/docs/stackit_postgresflex_instance_list.md @@ -35,7 +35,7 @@ stackit postgresflex instance list [flags] ``` -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"] + -o, --output-format string Output format, one of ["json" "pretty" "none" "yaml"] -p, --project-id string Project ID --verbosity string Verbosity of the CLI, one of ["debug" "info" "warning" "error"] (default "info") ``` diff --git a/docs/stackit_postgresflex_instance_update.md b/docs/stackit_postgresflex_instance_update.md index 3a8980d7d..75a10e519 100644 --- a/docs/stackit_postgresflex_instance_update.md +++ b/docs/stackit_postgresflex_instance_update.md @@ -41,7 +41,7 @@ stackit postgresflex instance update INSTANCE_ID [flags] ``` -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"] + -o, --output-format string Output format, one of ["json" "pretty" "none" "yaml"] -p, --project-id string Project ID --verbosity string Verbosity of the CLI, one of ["debug" "info" "warning" "error"] (default "info") ``` diff --git a/docs/stackit_postgresflex_options.md b/docs/stackit_postgresflex_options.md index 57cc6ed9b..0638b2ec4 100644 --- a/docs/stackit_postgresflex_options.md +++ b/docs/stackit_postgresflex_options.md @@ -39,7 +39,7 @@ stackit postgresflex options [flags] ``` -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"] + -o, --output-format string Output format, one of ["json" "pretty" "none" "yaml"] -p, --project-id string Project ID --verbosity string Verbosity of the CLI, one of ["debug" "info" "warning" "error"] (default "info") ``` diff --git a/docs/stackit_postgresflex_user.md b/docs/stackit_postgresflex_user.md index 720c85cd0..2e0d97ffc 100644 --- a/docs/stackit_postgresflex_user.md +++ b/docs/stackit_postgresflex_user.md @@ -21,7 +21,7 @@ stackit postgresflex user [flags] ``` -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"] + -o, --output-format string Output format, one of ["json" "pretty" "none" "yaml"] -p, --project-id string Project ID --verbosity string Verbosity of the CLI, one of ["debug" "info" "warning" "error"] (default "info") ``` diff --git a/docs/stackit_postgresflex_user_create.md b/docs/stackit_postgresflex_user_create.md index 1a33e4ae4..335038430 100644 --- a/docs/stackit_postgresflex_user_create.md +++ b/docs/stackit_postgresflex_user_create.md @@ -37,7 +37,7 @@ stackit postgresflex user create [flags] ``` -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"] + -o, --output-format string Output format, one of ["json" "pretty" "none" "yaml"] -p, --project-id string Project ID --verbosity string Verbosity of the CLI, one of ["debug" "info" "warning" "error"] (default "info") ``` diff --git a/docs/stackit_postgresflex_user_delete.md b/docs/stackit_postgresflex_user_delete.md index 7f75f6dc3..f85225ede 100644 --- a/docs/stackit_postgresflex_user_delete.md +++ b/docs/stackit_postgresflex_user_delete.md @@ -31,7 +31,7 @@ stackit postgresflex user delete USER_ID [flags] ``` -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"] + -o, --output-format string Output format, one of ["json" "pretty" "none" "yaml"] -p, --project-id string Project ID --verbosity string Verbosity of the CLI, one of ["debug" "info" "warning" "error"] (default "info") ``` diff --git a/docs/stackit_postgresflex_user_describe.md b/docs/stackit_postgresflex_user_describe.md index 5fbcdc899..dd1d74e8e 100644 --- a/docs/stackit_postgresflex_user_describe.md +++ b/docs/stackit_postgresflex_user_describe.md @@ -34,7 +34,7 @@ stackit postgresflex user describe USER_ID [flags] ``` -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"] + -o, --output-format string Output format, one of ["json" "pretty" "none" "yaml"] -p, --project-id string Project ID --verbosity string Verbosity of the CLI, one of ["debug" "info" "warning" "error"] (default "info") ``` diff --git a/docs/stackit_postgresflex_user_list.md b/docs/stackit_postgresflex_user_list.md index 23e84a491..9db37967d 100644 --- a/docs/stackit_postgresflex_user_list.md +++ b/docs/stackit_postgresflex_user_list.md @@ -36,7 +36,7 @@ stackit postgresflex user list [flags] ``` -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"] + -o, --output-format string Output format, one of ["json" "pretty" "none" "yaml"] -p, --project-id string Project ID --verbosity string Verbosity of the CLI, one of ["debug" "info" "warning" "error"] (default "info") ``` diff --git a/docs/stackit_postgresflex_user_reset-password.md b/docs/stackit_postgresflex_user_reset-password.md index 4bf83d21a..857313808 100644 --- a/docs/stackit_postgresflex_user_reset-password.md +++ b/docs/stackit_postgresflex_user_reset-password.md @@ -30,7 +30,7 @@ stackit postgresflex user reset-password USER_ID [flags] ``` -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"] + -o, --output-format string Output format, one of ["json" "pretty" "none" "yaml"] -p, --project-id string Project ID --verbosity string Verbosity of the CLI, one of ["debug" "info" "warning" "error"] (default "info") ``` diff --git a/docs/stackit_postgresflex_user_update.md b/docs/stackit_postgresflex_user_update.md index dd027426e..f0a0e5250 100644 --- a/docs/stackit_postgresflex_user_update.md +++ b/docs/stackit_postgresflex_user_update.md @@ -30,7 +30,7 @@ stackit postgresflex user update USER_ID [flags] ``` -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"] + -o, --output-format string Output format, one of ["json" "pretty" "none" "yaml"] -p, --project-id string Project ID --verbosity string Verbosity of the CLI, one of ["debug" "info" "warning" "error"] (default "info") ``` diff --git a/docs/stackit_project.md b/docs/stackit_project.md index 698df8078..5c30842c4 100644 --- a/docs/stackit_project.md +++ b/docs/stackit_project.md @@ -22,7 +22,7 @@ stackit project [flags] ``` -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"] + -o, --output-format string Output format, one of ["json" "pretty" "none" "yaml"] -p, --project-id string Project ID --verbosity string Verbosity of the CLI, one of ["debug" "info" "warning" "error"] (default "info") ``` diff --git a/docs/stackit_project_create.md b/docs/stackit_project_create.md index 582d1f29a..b3168ab91 100644 --- a/docs/stackit_project_create.md +++ b/docs/stackit_project_create.md @@ -34,7 +34,7 @@ stackit project create [flags] ``` -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"] + -o, --output-format string Output format, one of ["json" "pretty" "none" "yaml"] -p, --project-id string Project ID --verbosity string Verbosity of the CLI, one of ["debug" "info" "warning" "error"] (default "info") ``` diff --git a/docs/stackit_project_delete.md b/docs/stackit_project_delete.md index ca90cedb0..03566109f 100644 --- a/docs/stackit_project_delete.md +++ b/docs/stackit_project_delete.md @@ -31,7 +31,7 @@ stackit project delete [flags] ``` -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"] + -o, --output-format string Output format, one of ["json" "pretty" "none" "yaml"] -p, --project-id string Project ID --verbosity string Verbosity of the CLI, one of ["debug" "info" "warning" "error"] (default "info") ``` diff --git a/docs/stackit_project_describe.md b/docs/stackit_project_describe.md index c9d746c94..24f145174 100644 --- a/docs/stackit_project_describe.md +++ b/docs/stackit_project_describe.md @@ -35,7 +35,7 @@ stackit project describe [flags] ``` -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"] + -o, --output-format string Output format, one of ["json" "pretty" "none" "yaml"] -p, --project-id string Project ID --verbosity string Verbosity of the CLI, one of ["debug" "info" "warning" "error"] (default "info") ``` diff --git a/docs/stackit_project_list.md b/docs/stackit_project_list.md index a182923c0..c968bef0c 100644 --- a/docs/stackit_project_list.md +++ b/docs/stackit_project_list.md @@ -43,7 +43,7 @@ stackit project list [flags] ``` -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"] + -o, --output-format string Output format, one of ["json" "pretty" "none" "yaml"] -p, --project-id string Project ID --verbosity string Verbosity of the CLI, one of ["debug" "info" "warning" "error"] (default "info") ``` diff --git a/docs/stackit_project_member.md b/docs/stackit_project_member.md index 6835a6d59..6cf65e449 100644 --- a/docs/stackit_project_member.md +++ b/docs/stackit_project_member.md @@ -21,7 +21,7 @@ stackit project member [flags] ``` -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"] + -o, --output-format string Output format, one of ["json" "pretty" "none" "yaml"] -p, --project-id string Project ID --verbosity string Verbosity of the CLI, one of ["debug" "info" "warning" "error"] (default "info") ``` diff --git a/docs/stackit_project_member_add.md b/docs/stackit_project_member_add.md index 9ef13b064..b92c1d1e0 100644 --- a/docs/stackit_project_member_add.md +++ b/docs/stackit_project_member_add.md @@ -33,7 +33,7 @@ stackit project member add SUBJECT [flags] ``` -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"] + -o, --output-format string Output format, one of ["json" "pretty" "none" "yaml"] -p, --project-id string Project ID --verbosity string Verbosity of the CLI, one of ["debug" "info" "warning" "error"] (default "info") ``` diff --git a/docs/stackit_project_member_list.md b/docs/stackit_project_member_list.md index 1d91e8886..dbc1b5f06 100644 --- a/docs/stackit_project_member_list.md +++ b/docs/stackit_project_member_list.md @@ -37,7 +37,7 @@ stackit project member list [flags] ``` -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"] + -o, --output-format string Output format, one of ["json" "pretty" "none" "yaml"] -p, --project-id string Project ID --verbosity string Verbosity of the CLI, one of ["debug" "info" "warning" "error"] (default "info") ``` diff --git a/docs/stackit_project_member_remove.md b/docs/stackit_project_member_remove.md index 0a8dd80d4..f94e96a04 100644 --- a/docs/stackit_project_member_remove.md +++ b/docs/stackit_project_member_remove.md @@ -35,7 +35,7 @@ stackit project member remove SUBJECT [flags] ``` -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"] + -o, --output-format string Output format, one of ["json" "pretty" "none" "yaml"] -p, --project-id string Project ID --verbosity string Verbosity of the CLI, one of ["debug" "info" "warning" "error"] (default "info") ``` diff --git a/docs/stackit_project_role.md b/docs/stackit_project_role.md index 3294413f9..17551b60d 100644 --- a/docs/stackit_project_role.md +++ b/docs/stackit_project_role.md @@ -21,7 +21,7 @@ stackit project role [flags] ``` -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"] + -o, --output-format string Output format, one of ["json" "pretty" "none" "yaml"] -p, --project-id string Project ID --verbosity string Verbosity of the CLI, one of ["debug" "info" "warning" "error"] (default "info") ``` diff --git a/docs/stackit_project_role_list.md b/docs/stackit_project_role_list.md index e9770999e..2c34a8727 100644 --- a/docs/stackit_project_role_list.md +++ b/docs/stackit_project_role_list.md @@ -35,7 +35,7 @@ stackit project role list [flags] ``` -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"] + -o, --output-format string Output format, one of ["json" "pretty" "none" "yaml"] -p, --project-id string Project ID --verbosity string Verbosity of the CLI, one of ["debug" "info" "warning" "error"] (default "info") ``` diff --git a/docs/stackit_project_update.md b/docs/stackit_project_update.md index 784041f14..f9e569c8a 100644 --- a/docs/stackit_project_update.md +++ b/docs/stackit_project_update.md @@ -37,7 +37,7 @@ stackit project update [flags] ``` -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"] + -o, --output-format string Output format, one of ["json" "pretty" "none" "yaml"] -p, --project-id string Project ID --verbosity string Verbosity of the CLI, one of ["debug" "info" "warning" "error"] (default "info") ``` diff --git a/docs/stackit_rabbitmq.md b/docs/stackit_rabbitmq.md index 8855b6463..e3060985c 100644 --- a/docs/stackit_rabbitmq.md +++ b/docs/stackit_rabbitmq.md @@ -21,7 +21,7 @@ stackit rabbitmq [flags] ``` -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"] + -o, --output-format string Output format, one of ["json" "pretty" "none" "yaml"] -p, --project-id string Project ID --verbosity string Verbosity of the CLI, one of ["debug" "info" "warning" "error"] (default "info") ``` diff --git a/docs/stackit_rabbitmq_credentials.md b/docs/stackit_rabbitmq_credentials.md index 48e9342af..96968f4b3 100644 --- a/docs/stackit_rabbitmq_credentials.md +++ b/docs/stackit_rabbitmq_credentials.md @@ -21,7 +21,7 @@ stackit rabbitmq credentials [flags] ``` -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"] + -o, --output-format string Output format, one of ["json" "pretty" "none" "yaml"] -p, --project-id string Project ID --verbosity string Verbosity of the CLI, one of ["debug" "info" "warning" "error"] (default "info") ``` diff --git a/docs/stackit_rabbitmq_credentials_create.md b/docs/stackit_rabbitmq_credentials_create.md index 2f836291e..5221b186e 100644 --- a/docs/stackit_rabbitmq_credentials_create.md +++ b/docs/stackit_rabbitmq_credentials_create.md @@ -33,7 +33,7 @@ stackit rabbitmq credentials create [flags] ``` -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"] + -o, --output-format string Output format, one of ["json" "pretty" "none" "yaml"] -p, --project-id string Project ID --verbosity string Verbosity of the CLI, one of ["debug" "info" "warning" "error"] (default "info") ``` diff --git a/docs/stackit_rabbitmq_credentials_delete.md b/docs/stackit_rabbitmq_credentials_delete.md index 3046a6da2..ff8c1e207 100644 --- a/docs/stackit_rabbitmq_credentials_delete.md +++ b/docs/stackit_rabbitmq_credentials_delete.md @@ -29,7 +29,7 @@ stackit rabbitmq credentials delete CREDENTIALS_ID [flags] ``` -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"] + -o, --output-format string Output format, one of ["json" "pretty" "none" "yaml"] -p, --project-id string Project ID --verbosity string Verbosity of the CLI, one of ["debug" "info" "warning" "error"] (default "info") ``` diff --git a/docs/stackit_rabbitmq_credentials_describe.md b/docs/stackit_rabbitmq_credentials_describe.md index bca32624d..48fb1b2ab 100644 --- a/docs/stackit_rabbitmq_credentials_describe.md +++ b/docs/stackit_rabbitmq_credentials_describe.md @@ -32,7 +32,7 @@ stackit rabbitmq credentials describe CREDENTIALS_ID [flags] ``` -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"] + -o, --output-format string Output format, one of ["json" "pretty" "none" "yaml"] -p, --project-id string Project ID --verbosity string Verbosity of the CLI, one of ["debug" "info" "warning" "error"] (default "info") ``` diff --git a/docs/stackit_rabbitmq_credentials_list.md b/docs/stackit_rabbitmq_credentials_list.md index 44208eb88..105de4d9f 100644 --- a/docs/stackit_rabbitmq_credentials_list.md +++ b/docs/stackit_rabbitmq_credentials_list.md @@ -36,7 +36,7 @@ stackit rabbitmq credentials list [flags] ``` -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"] + -o, --output-format string Output format, one of ["json" "pretty" "none" "yaml"] -p, --project-id string Project ID --verbosity string Verbosity of the CLI, one of ["debug" "info" "warning" "error"] (default "info") ``` diff --git a/docs/stackit_rabbitmq_instance.md b/docs/stackit_rabbitmq_instance.md index d56d4b9fb..e417d7664 100644 --- a/docs/stackit_rabbitmq_instance.md +++ b/docs/stackit_rabbitmq_instance.md @@ -21,7 +21,7 @@ stackit rabbitmq instance [flags] ``` -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"] + -o, --output-format string Output format, one of ["json" "pretty" "none" "yaml"] -p, --project-id string Project ID --verbosity string Verbosity of the CLI, one of ["debug" "info" "warning" "error"] (default "info") ``` diff --git a/docs/stackit_rabbitmq_instance_create.md b/docs/stackit_rabbitmq_instance_create.md index ee83c5f28..bc14709c9 100644 --- a/docs/stackit_rabbitmq_instance_create.md +++ b/docs/stackit_rabbitmq_instance_create.md @@ -46,7 +46,7 @@ stackit rabbitmq instance create [flags] ``` -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"] + -o, --output-format string Output format, one of ["json" "pretty" "none" "yaml"] -p, --project-id string Project ID --verbosity string Verbosity of the CLI, one of ["debug" "info" "warning" "error"] (default "info") ``` diff --git a/docs/stackit_rabbitmq_instance_delete.md b/docs/stackit_rabbitmq_instance_delete.md index 66b30a589..f216d6cd3 100644 --- a/docs/stackit_rabbitmq_instance_delete.md +++ b/docs/stackit_rabbitmq_instance_delete.md @@ -28,7 +28,7 @@ stackit rabbitmq instance delete INSTANCE_ID [flags] ``` -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"] + -o, --output-format string Output format, one of ["json" "pretty" "none" "yaml"] -p, --project-id string Project ID --verbosity string Verbosity of the CLI, one of ["debug" "info" "warning" "error"] (default "info") ``` diff --git a/docs/stackit_rabbitmq_instance_describe.md b/docs/stackit_rabbitmq_instance_describe.md index e491b2417..1aa75f6ef 100644 --- a/docs/stackit_rabbitmq_instance_describe.md +++ b/docs/stackit_rabbitmq_instance_describe.md @@ -31,7 +31,7 @@ stackit rabbitmq instance describe INSTANCE_ID [flags] ``` -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"] + -o, --output-format string Output format, one of ["json" "pretty" "none" "yaml"] -p, --project-id string Project ID --verbosity string Verbosity of the CLI, one of ["debug" "info" "warning" "error"] (default "info") ``` diff --git a/docs/stackit_rabbitmq_instance_list.md b/docs/stackit_rabbitmq_instance_list.md index 335db3700..5fc3d72f1 100644 --- a/docs/stackit_rabbitmq_instance_list.md +++ b/docs/stackit_rabbitmq_instance_list.md @@ -35,7 +35,7 @@ stackit rabbitmq instance list [flags] ``` -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"] + -o, --output-format string Output format, one of ["json" "pretty" "none" "yaml"] -p, --project-id string Project ID --verbosity string Verbosity of the CLI, one of ["debug" "info" "warning" "error"] (default "info") ``` diff --git a/docs/stackit_rabbitmq_instance_update.md b/docs/stackit_rabbitmq_instance_update.md index 1208bc817..6976587dc 100644 --- a/docs/stackit_rabbitmq_instance_update.md +++ b/docs/stackit_rabbitmq_instance_update.md @@ -42,7 +42,7 @@ stackit rabbitmq instance update INSTANCE_ID [flags] ``` -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"] + -o, --output-format string Output format, one of ["json" "pretty" "none" "yaml"] -p, --project-id string Project ID --verbosity string Verbosity of the CLI, one of ["debug" "info" "warning" "error"] (default "info") ``` diff --git a/docs/stackit_rabbitmq_plans.md b/docs/stackit_rabbitmq_plans.md index b801e281f..ebd2d6652 100644 --- a/docs/stackit_rabbitmq_plans.md +++ b/docs/stackit_rabbitmq_plans.md @@ -35,7 +35,7 @@ stackit rabbitmq plans [flags] ``` -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"] + -o, --output-format string Output format, one of ["json" "pretty" "none" "yaml"] -p, --project-id string Project ID --verbosity string Verbosity of the CLI, one of ["debug" "info" "warning" "error"] (default "info") ``` diff --git a/docs/stackit_redis.md b/docs/stackit_redis.md index b8fff49e6..16beda8f8 100644 --- a/docs/stackit_redis.md +++ b/docs/stackit_redis.md @@ -21,7 +21,7 @@ stackit redis [flags] ``` -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"] + -o, --output-format string Output format, one of ["json" "pretty" "none" "yaml"] -p, --project-id string Project ID --verbosity string Verbosity of the CLI, one of ["debug" "info" "warning" "error"] (default "info") ``` diff --git a/docs/stackit_redis_credentials.md b/docs/stackit_redis_credentials.md index 12508a4bc..37c2a269d 100644 --- a/docs/stackit_redis_credentials.md +++ b/docs/stackit_redis_credentials.md @@ -21,7 +21,7 @@ stackit redis credentials [flags] ``` -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"] + -o, --output-format string Output format, one of ["json" "pretty" "none" "yaml"] -p, --project-id string Project ID --verbosity string Verbosity of the CLI, one of ["debug" "info" "warning" "error"] (default "info") ``` diff --git a/docs/stackit_redis_credentials_create.md b/docs/stackit_redis_credentials_create.md index 0097e7eed..18fa56dad 100644 --- a/docs/stackit_redis_credentials_create.md +++ b/docs/stackit_redis_credentials_create.md @@ -33,7 +33,7 @@ stackit redis credentials create [flags] ``` -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"] + -o, --output-format string Output format, one of ["json" "pretty" "none" "yaml"] -p, --project-id string Project ID --verbosity string Verbosity of the CLI, one of ["debug" "info" "warning" "error"] (default "info") ``` diff --git a/docs/stackit_redis_credentials_delete.md b/docs/stackit_redis_credentials_delete.md index f8f304bee..a975a0a71 100644 --- a/docs/stackit_redis_credentials_delete.md +++ b/docs/stackit_redis_credentials_delete.md @@ -29,7 +29,7 @@ stackit redis credentials delete CREDENTIALS_ID [flags] ``` -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"] + -o, --output-format string Output format, one of ["json" "pretty" "none" "yaml"] -p, --project-id string Project ID --verbosity string Verbosity of the CLI, one of ["debug" "info" "warning" "error"] (default "info") ``` diff --git a/docs/stackit_redis_credentials_describe.md b/docs/stackit_redis_credentials_describe.md index 23968aeeb..274c06563 100644 --- a/docs/stackit_redis_credentials_describe.md +++ b/docs/stackit_redis_credentials_describe.md @@ -32,7 +32,7 @@ stackit redis credentials describe CREDENTIALS_ID [flags] ``` -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"] + -o, --output-format string Output format, one of ["json" "pretty" "none" "yaml"] -p, --project-id string Project ID --verbosity string Verbosity of the CLI, one of ["debug" "info" "warning" "error"] (default "info") ``` diff --git a/docs/stackit_redis_credentials_list.md b/docs/stackit_redis_credentials_list.md index 7ed152e24..421d09924 100644 --- a/docs/stackit_redis_credentials_list.md +++ b/docs/stackit_redis_credentials_list.md @@ -36,7 +36,7 @@ stackit redis credentials list [flags] ``` -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"] + -o, --output-format string Output format, one of ["json" "pretty" "none" "yaml"] -p, --project-id string Project ID --verbosity string Verbosity of the CLI, one of ["debug" "info" "warning" "error"] (default "info") ``` diff --git a/docs/stackit_redis_instance.md b/docs/stackit_redis_instance.md index a9fd3ba5b..d5c956ee8 100644 --- a/docs/stackit_redis_instance.md +++ b/docs/stackit_redis_instance.md @@ -21,7 +21,7 @@ stackit redis instance [flags] ``` -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"] + -o, --output-format string Output format, one of ["json" "pretty" "none" "yaml"] -p, --project-id string Project ID --verbosity string Verbosity of the CLI, one of ["debug" "info" "warning" "error"] (default "info") ``` diff --git a/docs/stackit_redis_instance_create.md b/docs/stackit_redis_instance_create.md index 3935f3638..92f3892cb 100644 --- a/docs/stackit_redis_instance_create.md +++ b/docs/stackit_redis_instance_create.md @@ -45,7 +45,7 @@ stackit redis instance create [flags] ``` -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"] + -o, --output-format string Output format, one of ["json" "pretty" "none" "yaml"] -p, --project-id string Project ID --verbosity string Verbosity of the CLI, one of ["debug" "info" "warning" "error"] (default "info") ``` diff --git a/docs/stackit_redis_instance_delete.md b/docs/stackit_redis_instance_delete.md index 46b0cdf08..4ad2da13b 100644 --- a/docs/stackit_redis_instance_delete.md +++ b/docs/stackit_redis_instance_delete.md @@ -28,7 +28,7 @@ stackit redis instance delete INSTANCE_ID [flags] ``` -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"] + -o, --output-format string Output format, one of ["json" "pretty" "none" "yaml"] -p, --project-id string Project ID --verbosity string Verbosity of the CLI, one of ["debug" "info" "warning" "error"] (default "info") ``` diff --git a/docs/stackit_redis_instance_describe.md b/docs/stackit_redis_instance_describe.md index 5a2a2f4d9..35c839779 100644 --- a/docs/stackit_redis_instance_describe.md +++ b/docs/stackit_redis_instance_describe.md @@ -31,7 +31,7 @@ stackit redis instance describe INSTANCE_ID [flags] ``` -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"] + -o, --output-format string Output format, one of ["json" "pretty" "none" "yaml"] -p, --project-id string Project ID --verbosity string Verbosity of the CLI, one of ["debug" "info" "warning" "error"] (default "info") ``` diff --git a/docs/stackit_redis_instance_list.md b/docs/stackit_redis_instance_list.md index 05ee3442b..e32b8763e 100644 --- a/docs/stackit_redis_instance_list.md +++ b/docs/stackit_redis_instance_list.md @@ -35,7 +35,7 @@ stackit redis instance list [flags] ``` -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"] + -o, --output-format string Output format, one of ["json" "pretty" "none" "yaml"] -p, --project-id string Project ID --verbosity string Verbosity of the CLI, one of ["debug" "info" "warning" "error"] (default "info") ``` diff --git a/docs/stackit_redis_instance_update.md b/docs/stackit_redis_instance_update.md index bebe8f631..2971e74b6 100644 --- a/docs/stackit_redis_instance_update.md +++ b/docs/stackit_redis_instance_update.md @@ -41,7 +41,7 @@ stackit redis instance update INSTANCE_ID [flags] ``` -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"] + -o, --output-format string Output format, one of ["json" "pretty" "none" "yaml"] -p, --project-id string Project ID --verbosity string Verbosity of the CLI, one of ["debug" "info" "warning" "error"] (default "info") ``` diff --git a/docs/stackit_redis_plans.md b/docs/stackit_redis_plans.md index 1926b02a8..5cce98b9a 100644 --- a/docs/stackit_redis_plans.md +++ b/docs/stackit_redis_plans.md @@ -35,7 +35,7 @@ stackit redis plans [flags] ``` -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"] + -o, --output-format string Output format, one of ["json" "pretty" "none" "yaml"] -p, --project-id string Project ID --verbosity string Verbosity of the CLI, one of ["debug" "info" "warning" "error"] (default "info") ``` diff --git a/docs/stackit_secrets-manager.md b/docs/stackit_secrets-manager.md index e2e61f6f5..59e7a99d6 100644 --- a/docs/stackit_secrets-manager.md +++ b/docs/stackit_secrets-manager.md @@ -21,7 +21,7 @@ stackit secrets-manager [flags] ``` -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"] + -o, --output-format string Output format, one of ["json" "pretty" "none" "yaml"] -p, --project-id string Project ID --verbosity string Verbosity of the CLI, one of ["debug" "info" "warning" "error"] (default "info") ``` diff --git a/docs/stackit_secrets-manager_instance.md b/docs/stackit_secrets-manager_instance.md index 66dde69b3..d7857e067 100644 --- a/docs/stackit_secrets-manager_instance.md +++ b/docs/stackit_secrets-manager_instance.md @@ -21,7 +21,7 @@ stackit secrets-manager instance [flags] ``` -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"] + -o, --output-format string Output format, one of ["json" "pretty" "none" "yaml"] -p, --project-id string Project ID --verbosity string Verbosity of the CLI, one of ["debug" "info" "warning" "error"] (default "info") ``` diff --git a/docs/stackit_secrets-manager_instance_create.md b/docs/stackit_secrets-manager_instance_create.md index e55be6683..d41249ca5 100644 --- a/docs/stackit_secrets-manager_instance_create.md +++ b/docs/stackit_secrets-manager_instance_create.md @@ -33,7 +33,7 @@ stackit secrets-manager instance create [flags] ``` -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"] + -o, --output-format string Output format, one of ["json" "pretty" "none" "yaml"] -p, --project-id string Project ID --verbosity string Verbosity of the CLI, one of ["debug" "info" "warning" "error"] (default "info") ``` diff --git a/docs/stackit_secrets-manager_instance_delete.md b/docs/stackit_secrets-manager_instance_delete.md index fd615bfaa..057e9b1ba 100644 --- a/docs/stackit_secrets-manager_instance_delete.md +++ b/docs/stackit_secrets-manager_instance_delete.md @@ -28,7 +28,7 @@ stackit secrets-manager instance delete INSTANCE_ID [flags] ``` -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"] + -o, --output-format string Output format, one of ["json" "pretty" "none" "yaml"] -p, --project-id string Project ID --verbosity string Verbosity of the CLI, one of ["debug" "info" "warning" "error"] (default "info") ``` diff --git a/docs/stackit_secrets-manager_instance_describe.md b/docs/stackit_secrets-manager_instance_describe.md index b19e49e06..d0f6756f5 100644 --- a/docs/stackit_secrets-manager_instance_describe.md +++ b/docs/stackit_secrets-manager_instance_describe.md @@ -31,7 +31,7 @@ stackit secrets-manager instance describe INSTANCE_ID [flags] ``` -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"] + -o, --output-format string Output format, one of ["json" "pretty" "none" "yaml"] -p, --project-id string Project ID --verbosity string Verbosity of the CLI, one of ["debug" "info" "warning" "error"] (default "info") ``` diff --git a/docs/stackit_secrets-manager_instance_list.md b/docs/stackit_secrets-manager_instance_list.md index 76dc7031e..46cbcd4cd 100644 --- a/docs/stackit_secrets-manager_instance_list.md +++ b/docs/stackit_secrets-manager_instance_list.md @@ -35,7 +35,7 @@ stackit secrets-manager instance list [flags] ``` -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"] + -o, --output-format string Output format, one of ["json" "pretty" "none" "yaml"] -p, --project-id string Project ID --verbosity string Verbosity of the CLI, one of ["debug" "info" "warning" "error"] (default "info") ``` diff --git a/docs/stackit_secrets-manager_instance_update.md b/docs/stackit_secrets-manager_instance_update.md index 8393e1c9e..54f4b877c 100644 --- a/docs/stackit_secrets-manager_instance_update.md +++ b/docs/stackit_secrets-manager_instance_update.md @@ -29,7 +29,7 @@ stackit secrets-manager instance update INSTANCE_ID [flags] ``` -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"] + -o, --output-format string Output format, one of ["json" "pretty" "none" "yaml"] -p, --project-id string Project ID --verbosity string Verbosity of the CLI, one of ["debug" "info" "warning" "error"] (default "info") ``` diff --git a/docs/stackit_secrets-manager_user.md b/docs/stackit_secrets-manager_user.md index a284739db..65899e83f 100644 --- a/docs/stackit_secrets-manager_user.md +++ b/docs/stackit_secrets-manager_user.md @@ -21,7 +21,7 @@ stackit secrets-manager user [flags] ``` -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"] + -o, --output-format string Output format, one of ["json" "pretty" "none" "yaml"] -p, --project-id string Project ID --verbosity string Verbosity of the CLI, one of ["debug" "info" "warning" "error"] (default "info") ``` diff --git a/docs/stackit_secrets-manager_user_create.md b/docs/stackit_secrets-manager_user_create.md index 6fc635628..043914508 100644 --- a/docs/stackit_secrets-manager_user_create.md +++ b/docs/stackit_secrets-manager_user_create.md @@ -36,7 +36,7 @@ stackit secrets-manager user create [flags] ``` -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"] + -o, --output-format string Output format, one of ["json" "pretty" "none" "yaml"] -p, --project-id string Project ID --verbosity string Verbosity of the CLI, one of ["debug" "info" "warning" "error"] (default "info") ``` diff --git a/docs/stackit_secrets-manager_user_delete.md b/docs/stackit_secrets-manager_user_delete.md index 15e1f9df3..4d0c30091 100644 --- a/docs/stackit_secrets-manager_user_delete.md +++ b/docs/stackit_secrets-manager_user_delete.md @@ -30,7 +30,7 @@ stackit secrets-manager user delete USER_ID [flags] ``` -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"] + -o, --output-format string Output format, one of ["json" "pretty" "none" "yaml"] -p, --project-id string Project ID --verbosity string Verbosity of the CLI, one of ["debug" "info" "warning" "error"] (default "info") ``` diff --git a/docs/stackit_secrets-manager_user_describe.md b/docs/stackit_secrets-manager_user_describe.md index 75de3d2d0..0d4d77739 100644 --- a/docs/stackit_secrets-manager_user_describe.md +++ b/docs/stackit_secrets-manager_user_describe.md @@ -32,7 +32,7 @@ stackit secrets-manager user describe USER_ID [flags] ``` -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"] + -o, --output-format string Output format, one of ["json" "pretty" "none" "yaml"] -p, --project-id string Project ID --verbosity string Verbosity of the CLI, one of ["debug" "info" "warning" "error"] (default "info") ``` diff --git a/docs/stackit_secrets-manager_user_list.md b/docs/stackit_secrets-manager_user_list.md index ac502af4c..cb500f260 100644 --- a/docs/stackit_secrets-manager_user_list.md +++ b/docs/stackit_secrets-manager_user_list.md @@ -36,7 +36,7 @@ stackit secrets-manager user list [flags] ``` -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"] + -o, --output-format string Output format, one of ["json" "pretty" "none" "yaml"] -p, --project-id string Project ID --verbosity string Verbosity of the CLI, one of ["debug" "info" "warning" "error"] (default "info") ``` diff --git a/docs/stackit_secrets-manager_user_update.md b/docs/stackit_secrets-manager_user_update.md index dedbd24a9..a6388f6d1 100644 --- a/docs/stackit_secrets-manager_user_update.md +++ b/docs/stackit_secrets-manager_user_update.md @@ -34,7 +34,7 @@ stackit secrets-manager user update USER_ID [flags] ``` -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"] + -o, --output-format string Output format, one of ["json" "pretty" "none" "yaml"] -p, --project-id string Project ID --verbosity string Verbosity of the CLI, one of ["debug" "info" "warning" "error"] (default "info") ``` diff --git a/docs/stackit_service-account.md b/docs/stackit_service-account.md index 17726dcec..c2fed18c5 100644 --- a/docs/stackit_service-account.md +++ b/docs/stackit_service-account.md @@ -21,7 +21,7 @@ stackit service-account [flags] ``` -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"] + -o, --output-format string Output format, one of ["json" "pretty" "none" "yaml"] -p, --project-id string Project ID --verbosity string Verbosity of the CLI, one of ["debug" "info" "warning" "error"] (default "info") ``` diff --git a/docs/stackit_service-account_create.md b/docs/stackit_service-account_create.md index c3a5c3fd9..8f9d62ba3 100644 --- a/docs/stackit_service-account_create.md +++ b/docs/stackit_service-account_create.md @@ -29,7 +29,7 @@ stackit service-account create [flags] ``` -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"] + -o, --output-format string Output format, one of ["json" "pretty" "none" "yaml"] -p, --project-id string Project ID --verbosity string Verbosity of the CLI, one of ["debug" "info" "warning" "error"] (default "info") ``` diff --git a/docs/stackit_service-account_delete.md b/docs/stackit_service-account_delete.md index 5b0723b71..0e61bef0f 100644 --- a/docs/stackit_service-account_delete.md +++ b/docs/stackit_service-account_delete.md @@ -28,7 +28,7 @@ stackit service-account delete EMAIL [flags] ``` -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"] + -o, --output-format string Output format, one of ["json" "pretty" "none" "yaml"] -p, --project-id string Project ID --verbosity string Verbosity of the CLI, one of ["debug" "info" "warning" "error"] (default "info") ``` diff --git a/docs/stackit_service-account_get-jwks.md b/docs/stackit_service-account_get-jwks.md index 34837edea..b09aa4771 100644 --- a/docs/stackit_service-account_get-jwks.md +++ b/docs/stackit_service-account_get-jwks.md @@ -28,7 +28,7 @@ stackit service-account get-jwks EMAIL [flags] ``` -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"] + -o, --output-format string Output format, one of ["json" "pretty" "none" "yaml"] -p, --project-id string Project ID --verbosity string Verbosity of the CLI, one of ["debug" "info" "warning" "error"] (default "info") ``` diff --git a/docs/stackit_service-account_key.md b/docs/stackit_service-account_key.md index d07f25588..40b9c8c26 100644 --- a/docs/stackit_service-account_key.md +++ b/docs/stackit_service-account_key.md @@ -21,7 +21,7 @@ stackit service-account key [flags] ``` -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"] + -o, --output-format string Output format, one of ["json" "pretty" "none" "yaml"] -p, --project-id string Project ID --verbosity string Verbosity of the CLI, one of ["debug" "info" "warning" "error"] (default "info") ``` diff --git a/docs/stackit_service-account_key_create.md b/docs/stackit_service-account_key_create.md index 1c9cb9f6b..0e33bd90a 100644 --- a/docs/stackit_service-account_key_create.md +++ b/docs/stackit_service-account_key_create.md @@ -39,7 +39,7 @@ stackit service-account key create [flags] ``` -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"] + -o, --output-format string Output format, one of ["json" "pretty" "none" "yaml"] -p, --project-id string Project ID --verbosity string Verbosity of the CLI, one of ["debug" "info" "warning" "error"] (default "info") ``` diff --git a/docs/stackit_service-account_key_delete.md b/docs/stackit_service-account_key_delete.md index 44cac0374..c882dc490 100644 --- a/docs/stackit_service-account_key_delete.md +++ b/docs/stackit_service-account_key_delete.md @@ -29,7 +29,7 @@ stackit service-account key delete KEY_ID [flags] ``` -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"] + -o, --output-format string Output format, one of ["json" "pretty" "none" "yaml"] -p, --project-id string Project ID --verbosity string Verbosity of the CLI, one of ["debug" "info" "warning" "error"] (default "info") ``` diff --git a/docs/stackit_service-account_key_describe.md b/docs/stackit_service-account_key_describe.md index 469deee4e..21f949d79 100644 --- a/docs/stackit_service-account_key_describe.md +++ b/docs/stackit_service-account_key_describe.md @@ -29,7 +29,7 @@ stackit service-account key describe KEY_ID [flags] ``` -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"] + -o, --output-format string Output format, one of ["json" "pretty" "none" "yaml"] -p, --project-id string Project ID --verbosity string Verbosity of the CLI, one of ["debug" "info" "warning" "error"] (default "info") ``` diff --git a/docs/stackit_service-account_key_list.md b/docs/stackit_service-account_key_list.md index 66d71702d..c141676ac 100644 --- a/docs/stackit_service-account_key_list.md +++ b/docs/stackit_service-account_key_list.md @@ -36,7 +36,7 @@ stackit service-account key list [flags] ``` -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"] + -o, --output-format string Output format, one of ["json" "pretty" "none" "yaml"] -p, --project-id string Project ID --verbosity string Verbosity of the CLI, one of ["debug" "info" "warning" "error"] (default "info") ``` diff --git a/docs/stackit_service-account_key_update.md b/docs/stackit_service-account_key_update.md index af95f84fa..2c8e2f751 100644 --- a/docs/stackit_service-account_key_update.md +++ b/docs/stackit_service-account_key_update.md @@ -39,7 +39,7 @@ stackit service-account key update KEY_ID [flags] ``` -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"] + -o, --output-format string Output format, one of ["json" "pretty" "none" "yaml"] -p, --project-id string Project ID --verbosity string Verbosity of the CLI, one of ["debug" "info" "warning" "error"] (default "info") ``` diff --git a/docs/stackit_service-account_list.md b/docs/stackit_service-account_list.md index 649aa3500..1ffec01ad 100644 --- a/docs/stackit_service-account_list.md +++ b/docs/stackit_service-account_list.md @@ -29,7 +29,7 @@ stackit service-account list [flags] ``` -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"] + -o, --output-format string Output format, one of ["json" "pretty" "none" "yaml"] -p, --project-id string Project ID --verbosity string Verbosity of the CLI, one of ["debug" "info" "warning" "error"] (default "info") ``` diff --git a/docs/stackit_service-account_token.md b/docs/stackit_service-account_token.md index f59414260..03888a32c 100644 --- a/docs/stackit_service-account_token.md +++ b/docs/stackit_service-account_token.md @@ -21,7 +21,7 @@ stackit service-account token [flags] ``` -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"] + -o, --output-format string Output format, one of ["json" "pretty" "none" "yaml"] -p, --project-id string Project ID --verbosity string Verbosity of the CLI, one of ["debug" "info" "warning" "error"] (default "info") ``` diff --git a/docs/stackit_service-account_token_create.md b/docs/stackit_service-account_token_create.md index 3ecf567f9..ebca082ca 100644 --- a/docs/stackit_service-account_token_create.md +++ b/docs/stackit_service-account_token_create.md @@ -35,7 +35,7 @@ stackit service-account token create [flags] ``` -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"] + -o, --output-format string Output format, one of ["json" "pretty" "none" "yaml"] -p, --project-id string Project ID --verbosity string Verbosity of the CLI, one of ["debug" "info" "warning" "error"] (default "info") ``` diff --git a/docs/stackit_service-account_token_list.md b/docs/stackit_service-account_token_list.md index 21478fbb2..daaa3d06d 100644 --- a/docs/stackit_service-account_token_list.md +++ b/docs/stackit_service-account_token_list.md @@ -38,7 +38,7 @@ stackit service-account token list [flags] ``` -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"] + -o, --output-format string Output format, one of ["json" "pretty" "none" "yaml"] -p, --project-id string Project ID --verbosity string Verbosity of the CLI, one of ["debug" "info" "warning" "error"] (default "info") ``` diff --git a/docs/stackit_service-account_token_revoke.md b/docs/stackit_service-account_token_revoke.md index 794c0c1ff..57c73899f 100644 --- a/docs/stackit_service-account_token_revoke.md +++ b/docs/stackit_service-account_token_revoke.md @@ -31,7 +31,7 @@ stackit service-account token revoke TOKEN_ID [flags] ``` -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"] + -o, --output-format string Output format, one of ["json" "pretty" "none" "yaml"] -p, --project-id string Project ID --verbosity string Verbosity of the CLI, one of ["debug" "info" "warning" "error"] (default "info") ``` diff --git a/docs/stackit_ske.md b/docs/stackit_ske.md index 117baca74..5e8404f43 100644 --- a/docs/stackit_ske.md +++ b/docs/stackit_ske.md @@ -21,7 +21,7 @@ stackit ske [flags] ``` -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"] + -o, --output-format string Output format, one of ["json" "pretty" "none" "yaml"] -p, --project-id string Project ID --verbosity string Verbosity of the CLI, one of ["debug" "info" "warning" "error"] (default "info") ``` diff --git a/docs/stackit_ske_cluster.md b/docs/stackit_ske_cluster.md index 79e768ef3..d98cd3bf9 100644 --- a/docs/stackit_ske_cluster.md +++ b/docs/stackit_ske_cluster.md @@ -21,7 +21,7 @@ stackit ske cluster [flags] ``` -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"] + -o, --output-format string Output format, one of ["json" "pretty" "none" "yaml"] -p, --project-id string Project ID --verbosity string Verbosity of the CLI, one of ["debug" "info" "warning" "error"] (default "info") ``` diff --git a/docs/stackit_ske_cluster_create.md b/docs/stackit_ske_cluster_create.md index e27566c41..5b15cb217 100644 --- a/docs/stackit_ske_cluster_create.md +++ b/docs/stackit_ske_cluster_create.md @@ -42,7 +42,7 @@ stackit ske cluster create CLUSTER_NAME [flags] ``` -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"] + -o, --output-format string Output format, one of ["json" "pretty" "none" "yaml"] -p, --project-id string Project ID --verbosity string Verbosity of the CLI, one of ["debug" "info" "warning" "error"] (default "info") ``` diff --git a/docs/stackit_ske_cluster_delete.md b/docs/stackit_ske_cluster_delete.md index e496aa242..c2bb2ccd8 100644 --- a/docs/stackit_ske_cluster_delete.md +++ b/docs/stackit_ske_cluster_delete.md @@ -28,7 +28,7 @@ stackit ske cluster delete CLUSTER_NAME [flags] ``` -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"] + -o, --output-format string Output format, one of ["json" "pretty" "none" "yaml"] -p, --project-id string Project ID --verbosity string Verbosity of the CLI, one of ["debug" "info" "warning" "error"] (default "info") ``` diff --git a/docs/stackit_ske_cluster_describe.md b/docs/stackit_ske_cluster_describe.md index 8382dd33a..733a9fa83 100644 --- a/docs/stackit_ske_cluster_describe.md +++ b/docs/stackit_ske_cluster_describe.md @@ -31,7 +31,7 @@ stackit ske cluster describe CLUSTER_NAME [flags] ``` -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"] + -o, --output-format string Output format, one of ["json" "pretty" "none" "yaml"] -p, --project-id string Project ID --verbosity string Verbosity of the CLI, one of ["debug" "info" "warning" "error"] (default "info") ``` diff --git a/docs/stackit_ske_cluster_generate-payload.md b/docs/stackit_ske_cluster_generate-payload.md index 127be6ef2..dc07bfa22 100644 --- a/docs/stackit_ske_cluster_generate-payload.md +++ b/docs/stackit_ske_cluster_generate-payload.md @@ -37,7 +37,7 @@ stackit ske cluster generate-payload [flags] ``` -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"] + -o, --output-format string Output format, one of ["json" "pretty" "none" "yaml"] -p, --project-id string Project ID --verbosity string Verbosity of the CLI, one of ["debug" "info" "warning" "error"] (default "info") ``` diff --git a/docs/stackit_ske_cluster_list.md b/docs/stackit_ske_cluster_list.md index 1a9c25ef6..d0438ef6c 100644 --- a/docs/stackit_ske_cluster_list.md +++ b/docs/stackit_ske_cluster_list.md @@ -35,7 +35,7 @@ stackit ske cluster list [flags] ``` -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"] + -o, --output-format string Output format, one of ["json" "pretty" "none" "yaml"] -p, --project-id string Project ID --verbosity string Verbosity of the CLI, one of ["debug" "info" "warning" "error"] (default "info") ``` diff --git a/docs/stackit_ske_cluster_update.md b/docs/stackit_ske_cluster_update.md index 2cb4f65bb..0e0482693 100644 --- a/docs/stackit_ske_cluster_update.md +++ b/docs/stackit_ske_cluster_update.md @@ -39,7 +39,7 @@ stackit ske cluster update CLUSTER_NAME [flags] ``` -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"] + -o, --output-format string Output format, one of ["json" "pretty" "none" "yaml"] -p, --project-id string Project ID --verbosity string Verbosity of the CLI, one of ["debug" "info" "warning" "error"] (default "info") ``` diff --git a/docs/stackit_ske_credentials.md b/docs/stackit_ske_credentials.md index 70b584bc8..51d65fe36 100644 --- a/docs/stackit_ske_credentials.md +++ b/docs/stackit_ske_credentials.md @@ -21,7 +21,7 @@ stackit ske credentials [flags] ``` -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"] + -o, --output-format string Output format, one of ["json" "pretty" "none" "yaml"] -p, --project-id string Project ID --verbosity string Verbosity of the CLI, one of ["debug" "info" "warning" "error"] (default "info") ``` diff --git a/docs/stackit_ske_credentials_complete-rotation.md b/docs/stackit_ske_credentials_complete-rotation.md index 255187290..04fc73052 100644 --- a/docs/stackit_ske_credentials_complete-rotation.md +++ b/docs/stackit_ske_credentials_complete-rotation.md @@ -43,7 +43,7 @@ stackit ske credentials complete-rotation CLUSTER_NAME [flags] ``` -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"] + -o, --output-format string Output format, one of ["json" "pretty" "none" "yaml"] -p, --project-id string Project ID --verbosity string Verbosity of the CLI, one of ["debug" "info" "warning" "error"] (default "info") ``` diff --git a/docs/stackit_ske_credentials_start-rotation.md b/docs/stackit_ske_credentials_start-rotation.md index 12d47d5f1..35e3a7f5d 100644 --- a/docs/stackit_ske_credentials_start-rotation.md +++ b/docs/stackit_ske_credentials_start-rotation.md @@ -47,7 +47,7 @@ stackit ske credentials start-rotation CLUSTER_NAME [flags] ``` -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"] + -o, --output-format string Output format, one of ["json" "pretty" "none" "yaml"] -p, --project-id string Project ID --verbosity string Verbosity of the CLI, one of ["debug" "info" "warning" "error"] (default "info") ``` diff --git a/docs/stackit_ske_describe.md b/docs/stackit_ske_describe.md index 3037f6362..d45433c2d 100644 --- a/docs/stackit_ske_describe.md +++ b/docs/stackit_ske_describe.md @@ -28,7 +28,7 @@ stackit ske describe [flags] ``` -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"] + -o, --output-format string Output format, one of ["json" "pretty" "none" "yaml"] -p, --project-id string Project ID --verbosity string Verbosity of the CLI, one of ["debug" "info" "warning" "error"] (default "info") ``` diff --git a/docs/stackit_ske_disable.md b/docs/stackit_ske_disable.md index f6cc385c5..c9e973c6b 100644 --- a/docs/stackit_ske_disable.md +++ b/docs/stackit_ske_disable.md @@ -28,7 +28,7 @@ stackit ske disable [flags] ``` -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"] + -o, --output-format string Output format, one of ["json" "pretty" "none" "yaml"] -p, --project-id string Project ID --verbosity string Verbosity of the CLI, one of ["debug" "info" "warning" "error"] (default "info") ``` diff --git a/docs/stackit_ske_enable.md b/docs/stackit_ske_enable.md index 44f4256d2..19c88ac2d 100644 --- a/docs/stackit_ske_enable.md +++ b/docs/stackit_ske_enable.md @@ -28,7 +28,7 @@ stackit ske enable [flags] ``` -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"] + -o, --output-format string Output format, one of ["json" "pretty" "none" "yaml"] -p, --project-id string Project ID --verbosity string Verbosity of the CLI, one of ["debug" "info" "warning" "error"] (default "info") ``` diff --git a/docs/stackit_ske_kubeconfig.md b/docs/stackit_ske_kubeconfig.md index 306184110..d21edc87f 100644 --- a/docs/stackit_ske_kubeconfig.md +++ b/docs/stackit_ske_kubeconfig.md @@ -21,7 +21,7 @@ stackit ske kubeconfig [flags] ``` -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"] + -o, --output-format string Output format, one of ["json" "pretty" "none" "yaml"] -p, --project-id string Project ID --verbosity string Verbosity of the CLI, one of ["debug" "info" "warning" "error"] (default "info") ``` @@ -30,4 +30,5 @@ stackit ske kubeconfig [flags] * [stackit ske](./stackit_ske.md) - Provides functionality for SKE * [stackit ske kubeconfig create](./stackit_ske_kubeconfig_create.md) - Creates a kubeconfig for an SKE cluster +* [stackit ske kubeconfig login](./stackit_ske_kubeconfig_login.md) - Login plugin for kubernetes clients diff --git a/docs/stackit_ske_kubeconfig_create.md b/docs/stackit_ske_kubeconfig_create.md index d1ef0883d..a779cacc0 100644 --- a/docs/stackit_ske_kubeconfig_create.md +++ b/docs/stackit_ske_kubeconfig_create.md @@ -21,6 +21,9 @@ stackit ske kubeconfig create CLUSTER_NAME [flags] Create a kubeconfig for the SKE cluster with name "my-cluster" $ stackit ske kubeconfig create my-cluster + Get a login kubeconfig for the SKE cluster with name "my-cluster". This kubeconfig does not contain any credentials and instead obtains valid credentials via the `stackit ske kubeconfig login` command. + $ stackit ske kubeconfig create my-cluster --login + Create a kubeconfig for the SKE cluster with name "my-cluster" and set the expiration time to 30 days $ stackit ske kubeconfig create my-cluster --expiration 30d @@ -37,6 +40,7 @@ stackit ske kubeconfig create CLUSTER_NAME [flags] -e, --expiration string Expiration time for the kubeconfig in seconds(s), minutes(m), hours(h), days(d) or months(M). Example: 30d. By default, expiration time is 1h --filepath string Path to create the kubeconfig file. By default, the kubeconfig is created as 'config' in the .kube folder, in the user's home directory. -h, --help Help for "stackit ske kubeconfig create" + -l, --login Create a login kubeconfig that obtains valid credentials via the STACKIT CLI. This flag is mutually exclusive with the expiration flag. ``` ### Options inherited from parent commands @@ -44,7 +48,7 @@ stackit ske kubeconfig create CLUSTER_NAME [flags] ``` -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"] + -o, --output-format string Output format, one of ["json" "pretty" "none" "yaml"] -p, --project-id string Project ID --verbosity string Verbosity of the CLI, one of ["debug" "info" "warning" "error"] (default "info") ``` diff --git a/docs/stackit_ske_kubeconfig_login.md b/docs/stackit_ske_kubeconfig_login.md new file mode 100644 index 000000000..d7e2d7691 --- /dev/null +++ b/docs/stackit_ske_kubeconfig_login.md @@ -0,0 +1,45 @@ +## stackit ske kubeconfig login + +Login plugin for kubernetes clients + +### Synopsis + +Login plugin for kubernetes clients, that creates short-lived credentials to authenticate against a STACKIT Kubernetes Engine (SKE) cluster. +First you need to obtain a kubeconfig for use with the login command (first example). +Secondly you use the kubeconfig with your chosen Kubernetes client (second example), the client will automatically retrieve the credentials via the STACKIT CLI. + +``` +stackit ske kubeconfig login [flags] +``` + +### Examples + +``` + Get a login kubeconfig for the SKE cluster with name "my-cluster". This kubeconfig does not contain any credentials and instead obtains valid credentials via the `stackit ske kubeconfig login` command. + $ stackit ske kubeconfig create my-cluster --login + + Use the previously saved kubeconfig to authenticate to the SKE cluster, in this case with kubectl. + $ kubectl cluster-info + $ kubectl get pods +``` + +### Options + +``` + -h, --help Help for "stackit ske kubeconfig login" +``` + +### 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 + --verbosity string Verbosity of the CLI, one of ["debug" "info" "warning" "error"] (default "info") +``` + +### SEE ALSO + +* [stackit ske kubeconfig](./stackit_ske_kubeconfig.md) - Provides functionality for SKE kubeconfig + diff --git a/docs/stackit_ske_options.md b/docs/stackit_ske_options.md index e243ca30f..303bbe6e4 100644 --- a/docs/stackit_ske_options.md +++ b/docs/stackit_ske_options.md @@ -40,7 +40,7 @@ stackit ske options [flags] ``` -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"] + -o, --output-format string Output format, one of ["json" "pretty" "none" "yaml"] -p, --project-id string Project ID --verbosity string Verbosity of the CLI, one of ["debug" "info" "warning" "error"] (default "info") ``` diff --git a/go.mod b/go.mod index 609df8fd8..067795c9c 100644 --- a/go.mod +++ b/go.mod @@ -3,6 +3,7 @@ module github.com/stackitcloud/stackit-cli go 1.22 require ( + github.com/goccy/go-yaml v1.11.3 github.com/golang-jwt/jwt/v5 v5.2.1 github.com/google/go-cmp v0.6.0 github.com/google/uuid v1.6.0 @@ -16,37 +17,55 @@ require ( github.com/stackitcloud/stackit-sdk-go/core v0.12.0 github.com/stackitcloud/stackit-sdk-go/services/authorization v0.2.0 github.com/stackitcloud/stackit-sdk-go/services/dns v0.9.1 - github.com/stackitcloud/stackit-sdk-go/services/mongodbflex v0.12.0 - github.com/stackitcloud/stackit-sdk-go/services/opensearch v0.13.0 + github.com/stackitcloud/stackit-sdk-go/services/mongodbflex v0.13.0 + github.com/stackitcloud/stackit-sdk-go/services/opensearch v0.14.0 github.com/stackitcloud/stackit-sdk-go/services/postgresflex v0.13.0 github.com/stackitcloud/stackit-sdk-go/services/resourcemanager v0.8.0 github.com/stackitcloud/stackit-sdk-go/services/secretsmanager v0.7.0 github.com/stackitcloud/stackit-sdk-go/services/serviceaccount v0.4.0 - github.com/stackitcloud/stackit-sdk-go/services/ske v0.14.0 + github.com/stackitcloud/stackit-sdk-go/services/ske v0.15.0 github.com/zalando/go-keyring v0.2.4 golang.org/x/mod v0.17.0 golang.org/x/oauth2 v0.20.0 golang.org/x/term v0.20.0 golang.org/x/text v0.15.0 + k8s.io/apimachinery v0.29.2 + k8s.io/client-go v0.29.2 ) -require github.com/mattn/go-isatty v0.0.17 // indirect +require ( + golang.org/x/net v0.23.0 // indirect + golang.org/x/time v0.5.0 // indirect + gopkg.in/inf.v0 v0.9.1 // indirect +) + +require ( + github.com/fatih/color v1.14.1 // indirect + github.com/mattn/go-isatty v0.0.17 // indirect + golang.org/x/xerrors v0.0.0-20220907171357-04be3eba64a2 // indirect +) require ( github.com/alessio/shellescape v1.4.2 // indirect github.com/cpuguy83/go-md2man/v2 v2.0.3 // indirect github.com/danieljoos/wincred v1.2.1 // indirect + github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect github.com/fsnotify/fsnotify v1.7.0 // indirect + github.com/go-logr/logr v1.3.0 // indirect github.com/godbus/dbus/v5 v5.1.0 // indirect + github.com/gogo/protobuf v1.3.2 // indirect + github.com/google/gofuzz v1.2.0 // indirect github.com/hashicorp/hcl v1.0.0 // indirect + github.com/imdario/mergo v0.3.6 // indirect github.com/inconshreveable/mousetrap v1.1.0 // indirect + github.com/json-iterator/go v1.1.12 // indirect github.com/magiconair/properties v1.8.7 // indirect github.com/mattn/go-runewidth v0.0.15 // indirect github.com/mitchellh/mapstructure v1.5.0 // indirect - github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e // indirect + github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect + github.com/modern-go/reflect2 v1.0.2 // indirect github.com/pelletier/go-toml/v2 v2.1.1 // indirect github.com/rivo/uniseg v0.4.4 // indirect - github.com/rogpeppe/go-internal v1.10.0 // indirect github.com/russross/blackfriday/v2 v2.1.0 // indirect github.com/sagikazarmark/locafero v0.4.0 // indirect github.com/sagikazarmark/slog-shim v0.1.0 // indirect @@ -55,16 +74,22 @@ require ( github.com/spf13/cast v1.6.0 // indirect github.com/stackitcloud/stackit-sdk-go/services/argus v0.10.0 github.com/stackitcloud/stackit-sdk-go/services/loadbalancer v0.12.0 - github.com/stackitcloud/stackit-sdk-go/services/logme v0.13.0 - github.com/stackitcloud/stackit-sdk-go/services/mariadb v0.13.0 + github.com/stackitcloud/stackit-sdk-go/services/logme v0.14.0 + github.com/stackitcloud/stackit-sdk-go/services/mariadb v0.14.0 github.com/stackitcloud/stackit-sdk-go/services/objectstorage v0.9.0 - github.com/stackitcloud/stackit-sdk-go/services/rabbitmq v0.13.0 - github.com/stackitcloud/stackit-sdk-go/services/redis v0.13.0 + github.com/stackitcloud/stackit-sdk-go/services/rabbitmq v0.14.0 + github.com/stackitcloud/stackit-sdk-go/services/redis v0.14.0 github.com/subosito/gotenv v1.6.0 // indirect go.uber.org/multierr v1.11.0 // indirect golang.org/x/exp v0.0.0-20240119083558-1b970713d09a // indirect golang.org/x/sys v0.20.0 // indirect - gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f // indirect gopkg.in/ini.v1 v1.67.0 // indirect + gopkg.in/yaml.v2 v2.4.0 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect + k8s.io/api v0.29.2 // indirect + k8s.io/klog/v2 v2.110.1 // indirect + k8s.io/utils v0.0.0-20230726121419-3b25d923346b // indirect + sigs.k8s.io/json v0.0.0-20221116044647-bc3834ca7abd // indirect + sigs.k8s.io/structured-merge-diff/v4 v4.4.1 // indirect + sigs.k8s.io/yaml v1.3.0 // indirect ) diff --git a/go.sum b/go.sum index eeec3313b..d99700f5a 100644 --- a/go.sum +++ b/go.sum @@ -8,36 +8,76 @@ github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSs github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM= github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/emicklei/go-restful/v3 v3.11.0 h1:rAQeMHw1c7zTmncogyy8VvRZwtkmkZ4FxERmMY4rD+g= +github.com/emicklei/go-restful/v3 v3.11.0/go.mod h1:6n3XBCmQQb25CM2LCACGz8ukIrRry+4bhvbpWn3mrbc= +github.com/fatih/color v1.14.1 h1:qfhVLaG5s+nCROl1zJsZRxFeYrHLqWroPOQ8BWiNb4w= +github.com/fatih/color v1.14.1/go.mod h1:2oHN61fhTpgcxD3TSWCgKDiH1+x4OiDVVGH8WlgGZGg= github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8= github.com/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0= github.com/fsnotify/fsnotify v1.7.0 h1:8JEhPFa5W2WU7YfeZzPNqzMP6Lwt7L2715Ggo0nosvA= github.com/fsnotify/fsnotify v1.7.0/go.mod h1:40Bi/Hjc2AVfZrqy+aj+yEI+/bRxZnMJyTJwOpGvigM= +github.com/go-logr/logr v1.3.0 h1:2y3SDp0ZXuc6/cjLSZ+Q3ir+QB9T/iG5yYRXqsagWSY= +github.com/go-logr/logr v1.3.0/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= +github.com/go-openapi/jsonpointer v0.19.6 h1:eCs3fxoIi3Wh6vtgmLTOjdhSpiqphQ+DaPn38N2ZdrE= +github.com/go-openapi/jsonpointer v0.19.6/go.mod h1:osyAmYz/mB/C3I+WsTTSgw1ONzaLJoLCyoi6/zppojs= +github.com/go-openapi/jsonreference v0.20.2 h1:3sVjiK66+uXK/6oQ8xgcRKcFgQ5KXa2KvnJRumpMGbE= +github.com/go-openapi/jsonreference v0.20.2/go.mod h1:Bl1zwGIM8/wsvqjsOQLJ/SH+En5Ap4rVB5KVcIDZG2k= +github.com/go-openapi/swag v0.22.3 h1:yMBqmnQ0gyZvEb/+KzuWZOXgllrXT4SADYbvDaXHv/g= +github.com/go-openapi/swag v0.22.3/go.mod h1:UzaqsxGiab7freDnrUUra0MwWfN/q7tE4j+VcZ0yl14= +github.com/go-playground/locales v0.13.0 h1:HyWk6mgj5qFqCT5fjGBuRArbVDfE4hi8+e8ceBS/t7Q= +github.com/go-playground/locales v0.13.0/go.mod h1:taPMhCMXrRLJO55olJkUXHZBHCxTMfnGwq/HNwmWNS8= +github.com/go-playground/universal-translator v0.17.0 h1:icxd5fm+REJzpZx7ZfpaD876Lmtgy7VtROAbHHXk8no= +github.com/go-playground/universal-translator v0.17.0/go.mod h1:UkSxE5sNxxRwHyU+Scu5vgOQjsIJAF8j9muTVoKLVtA= +github.com/go-playground/validator/v10 v10.4.1 h1:pH2c5ADXtd66mxoE0Zm9SUhxE20r7aM3F26W0hOn+GE= +github.com/go-playground/validator/v10 v10.4.1/go.mod h1:nlOn6nFhuKACm19sB/8EGNn9GlaMV7XkbRSipzJ0Ii4= +github.com/goccy/go-yaml v1.11.3 h1:B3W9IdWbvrUu2OYQGwvU1nZtvMQJPBKgBUuweJjLj6I= +github.com/goccy/go-yaml v1.11.3/go.mod h1:wKnAMd44+9JAAnGQpWVEgBzGt3YuTaQ4uXoHvE4m7WU= github.com/godbus/dbus/v5 v5.1.0 h1:4KLkAxT3aOY8Li4FRJe/KvhoNFFxo0m6fNuFUO8QJUk= github.com/godbus/dbus/v5 v5.1.0/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA= +github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q= +github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q= github.com/golang-jwt/jwt/v5 v5.2.1 h1:OuVbFODueb089Lh128TAcimifWaLhJwVflnrgM17wHk= github.com/golang-jwt/jwt/v5 v5.2.1/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk= +github.com/golang/protobuf v1.5.3 h1:KhyjKVUg7Usr/dYsdSqoFveMYd5ko72D+zANwlG1mmg= +github.com/golang/protobuf v1.5.3/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY= +github.com/google/gnostic-models v0.6.8 h1:yo/ABAfM5IMRsS1VnXjTBvUb61tFIHozhlYvRgGre9I= +github.com/google/gnostic-models v0.6.8/go.mod h1:5n7qKqH0f5wFt+aWF8CW6pZLLNOfYuF5OpfBSENuI8U= +github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= +github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= +github.com/google/gofuzz v1.2.0 h1:xRy4A+RhZaiKjJ1bPfwQ8sedCA+YS2YcCHW6ec7JMi0= +github.com/google/gofuzz v1.2.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/hashicorp/hcl v1.0.0 h1:0Anlzjpi4vEasTeNFn2mLJgTSwt0+6sfsiTG8qcWGx4= github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ= +github.com/imdario/mergo v0.3.6 h1:xTNEAn+kxVO7dTZGu0CegyqKZmoWFI0rF8UxjlB2d28= +github.com/imdario/mergo v0.3.6/go.mod h1:2EnlNZ0deacrJVfApfmtdGgDfMuh/nq6Ok1EcJh5FfA= github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8= github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= github.com/inhies/go-bytesize v0.0.0-20220417184213-4913239db9cf h1:FtEj8sfIcaaBfAKrE1Cwb61YDtYq9JxChK1c7AKce7s= github.com/inhies/go-bytesize v0.0.0-20220417184213-4913239db9cf/go.mod h1:yrqSXGoD/4EKfF26AOGzscPOgTTJcyAwM2rpixWT+t4= github.com/jedib0t/go-pretty/v6 v6.5.9 h1:ACteMBRrrmm1gMsXe9PSTOClQ63IXDUt03H5U+UV8OU= github.com/jedib0t/go-pretty/v6 v6.5.9/go.mod h1:zbn98qrYlh95FIhwwsbIip0LYpwSG8SUOScs+v9/t0E= +github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY= +github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y= +github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM= +github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= +github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8= +github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= -github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= -github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= +github.com/leodido/go-urn v1.2.0 h1:hpXL4XnriNwQ/ABnpepYM/1vCLWNDfUNts8dX3xTG6Y= +github.com/leodido/go-urn v1.2.0/go.mod h1:+8+nEpDfqqsY+g338gtMEUOtuK+4dEMhiQEgxpxOKII= github.com/lmittmann/tint v1.0.4 h1:LeYihpJ9hyGvE0w+K2okPTGUdVLfng1+nDNVR4vWISc= github.com/lmittmann/tint v1.0.4/go.mod h1:HIS3gSy7qNwGCj+5oRjAutErFBl4BzdQP6cJZ0NfMwE= github.com/magiconair/properties v1.8.7 h1:IeQXZAiQcpL9mgcAe1Nu6cX9LLw6ExEHKjN0VQdvPDY= github.com/magiconair/properties v1.8.7/go.mod h1:Dhd985XPs7jluiymwWYZ0G4Z61jb3vdS329zhj2hYo0= +github.com/mailru/easyjson v0.7.7 h1:UGYAvKxe3sBsEDzO8ZeWOSlIQfWFlxbzLZe7hwFURr0= +github.com/mailru/easyjson v0.7.7/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc= github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA= github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg= github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= @@ -47,8 +87,13 @@ github.com/mattn/go-runewidth v0.0.15 h1:UNAjwbU9l54TA3KzvqLGxwWjHmMgBUVhBiTjelZ github.com/mattn/go-runewidth v0.0.15/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w= github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyuac5Z2hdY= github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo= -github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e h1:fD57ERR4JtEqsWbfPhv4DMiApHyliiK5xCTNVSPiaAs= -github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno= +github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= +github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg= +github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= +github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M= +github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= +github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA= +github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ= github.com/pelletier/go-toml/v2 v2.1.1 h1:LWAJwfNvjQZCFIDKWYQaM62NcYeYViCmWIwmOStowAI= github.com/pelletier/go-toml/v2 v2.1.1/go.mod h1:tJU2Z3ZkXwnxa4DPO899bsyIoywizdUvyaeZurnPPDc= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= @@ -87,62 +132,123 @@ github.com/stackitcloud/stackit-sdk-go/services/dns v0.9.1 h1:pj2nAJvgzFSckA56rC github.com/stackitcloud/stackit-sdk-go/services/dns v0.9.1/go.mod h1:MdZcRbs19s2NLeJmSLSoqTzm9IPIQhE1ZEMpo9gePq0= github.com/stackitcloud/stackit-sdk-go/services/loadbalancer v0.12.0 h1:LAteZO46XmqTsmPw0QV8n8WiGM205pxrcqHqWznNmyY= github.com/stackitcloud/stackit-sdk-go/services/loadbalancer v0.12.0/go.mod h1:wsO3+vXe1XiKLeCIctWAptaHQZ07Un7kmLTQ+drbj7w= -github.com/stackitcloud/stackit-sdk-go/services/logme v0.13.0 h1:/wqs+pfHSjFWTakJVQGD/KwArxmFN8qdYrJDUgA1Gxw= -github.com/stackitcloud/stackit-sdk-go/services/logme v0.13.0/go.mod h1:bj9cn1treNSxKTRCEmESwqfENN8vCYn60HUnEA0P83c= -github.com/stackitcloud/stackit-sdk-go/services/mariadb v0.13.0 h1:qioXHNeQ2xdwT5iGOk0c3hQIpct7zlAYzLRqXvsJ7CA= -github.com/stackitcloud/stackit-sdk-go/services/mariadb v0.13.0/go.mod h1:kPetkX9hNm9HkRyiKQL/tlgdi8frZdMP8afg0mEvQ9s= -github.com/stackitcloud/stackit-sdk-go/services/mongodbflex v0.12.0 h1:/m6N/CdsFxomexsowU7PwT1S4UTmI39PnEvvWGsDh1s= -github.com/stackitcloud/stackit-sdk-go/services/mongodbflex v0.12.0/go.mod h1:iFerEzGmkg6R13ldFUyHUWHm0ac9cS4ftTDLhP0k/dU= +github.com/stackitcloud/stackit-sdk-go/services/logme v0.14.0 h1:vvQFCN5sKZA9tdzrbDnAVMsaTijX8lvTYnPaKQHmkoI= +github.com/stackitcloud/stackit-sdk-go/services/logme v0.14.0/go.mod h1:bj9cn1treNSxKTRCEmESwqfENN8vCYn60HUnEA0P83c= +github.com/stackitcloud/stackit-sdk-go/services/mariadb v0.14.0 h1:tK6imWrbZ5TgQJbukWCUz7yDgcvvFMX8wamxkPTLuDo= +github.com/stackitcloud/stackit-sdk-go/services/mariadb v0.14.0/go.mod h1:kPetkX9hNm9HkRyiKQL/tlgdi8frZdMP8afg0mEvQ9s= +github.com/stackitcloud/stackit-sdk-go/services/mongodbflex v0.13.0 h1:Dhanx9aV5VRfpHg22Li07661FbRT5FR9/M6FowN08a8= +github.com/stackitcloud/stackit-sdk-go/services/mongodbflex v0.13.0/go.mod h1:iFerEzGmkg6R13ldFUyHUWHm0ac9cS4ftTDLhP0k/dU= github.com/stackitcloud/stackit-sdk-go/services/objectstorage v0.9.0 h1:rWgy4/eCIgyA2dUuc4a30pldmS6taQDwiLqoeZmyeP8= github.com/stackitcloud/stackit-sdk-go/services/objectstorage v0.9.0/go.mod h1:dkVMJI88eJ3Xs0ZV15r4tUpgitUGJXcvrX3RL4Zq2bQ= -github.com/stackitcloud/stackit-sdk-go/services/opensearch v0.13.0 h1:dzt2wd1QkFVctKPQDBlyMCsHEJV4Bf2bRkZTUFFCZhs= -github.com/stackitcloud/stackit-sdk-go/services/opensearch v0.13.0/go.mod h1:ZecMIf9oYj2DGZqWh93l97WdVaRdLl+tW5Fq3YKGwBM= +github.com/stackitcloud/stackit-sdk-go/services/opensearch v0.14.0 h1:zkhm0r0OZ5NbHJFrm+7B+h11QL0bNLC53nzXhqCaLWo= +github.com/stackitcloud/stackit-sdk-go/services/opensearch v0.14.0/go.mod h1:ZecMIf9oYj2DGZqWh93l97WdVaRdLl+tW5Fq3YKGwBM= github.com/stackitcloud/stackit-sdk-go/services/postgresflex v0.13.0 h1:PGLjBZxWM7NIrH1+W1+f+/4kZEgwv9DGnXcUzOqM0M8= github.com/stackitcloud/stackit-sdk-go/services/postgresflex v0.13.0/go.mod h1:SdrqGLCkilL6wl1+jcxmLtks2IocgIg+bsyeyYUIzR4= -github.com/stackitcloud/stackit-sdk-go/services/rabbitmq v0.13.0 h1:bdmZhVAuyPiaAeJnBRLMVtVix6DYzNYbpdPiq/Z3XCI= -github.com/stackitcloud/stackit-sdk-go/services/rabbitmq v0.13.0/go.mod h1:eSgnPBknTJh7t+jVKN+xzeAh+Cg1USOlH3QCyfvG20g= -github.com/stackitcloud/stackit-sdk-go/services/redis v0.13.0 h1:MlREN/9zDntk90kahS0aJa/gPGAoUV4NLNnrTLs9CLc= -github.com/stackitcloud/stackit-sdk-go/services/redis v0.13.0/go.mod h1:3LhiTR/DMbKR2HuleTzlFHltR1MT1KD0DeW46X6K2GE= +github.com/stackitcloud/stackit-sdk-go/services/rabbitmq v0.14.0 h1:wJ+LSMrRol4wlm/ML4wvVPGwIw51VHMFwMCOtwluvKQ= +github.com/stackitcloud/stackit-sdk-go/services/rabbitmq v0.14.0/go.mod h1:eSgnPBknTJh7t+jVKN+xzeAh+Cg1USOlH3QCyfvG20g= +github.com/stackitcloud/stackit-sdk-go/services/redis v0.14.0 h1:wcfA/3mTI7UmTFmKX09EKIVsEqflfkiuEoWL/j5cMvg= +github.com/stackitcloud/stackit-sdk-go/services/redis v0.14.0/go.mod h1:3LhiTR/DMbKR2HuleTzlFHltR1MT1KD0DeW46X6K2GE= github.com/stackitcloud/stackit-sdk-go/services/resourcemanager v0.8.0 h1:7AIvLkB7JZ5lYKtYLwI0rgJ0185hwQC1PFiUrjcinDM= github.com/stackitcloud/stackit-sdk-go/services/resourcemanager v0.8.0/go.mod h1:p16qz/pAW8b1gEhqMpIgJfutRPeDPqQLlbVGyCo3f8o= github.com/stackitcloud/stackit-sdk-go/services/secretsmanager v0.7.0 h1:1Ho+M4DyZHrwbDe1peW//x+/hegIuaUdZqbQEbPlr4k= github.com/stackitcloud/stackit-sdk-go/services/secretsmanager v0.7.0/go.mod h1:LX0Mcyr7/QP77zf7e05fHCJO38RMuTxr7nEDUDZ3oPQ= github.com/stackitcloud/stackit-sdk-go/services/serviceaccount v0.4.0 h1:JB1O0E9+L50ZaO36uz7azurvUuB5JdX5s2ZXuIdb9t8= github.com/stackitcloud/stackit-sdk-go/services/serviceaccount v0.4.0/go.mod h1:Ni9RBJvcaXRIrDIuQBpJcuQvCQSj27crQSyc+WM4p0c= -github.com/stackitcloud/stackit-sdk-go/services/ske v0.14.0 h1:GH67aTvjXiXC2XmYhgmqNXfG13JHKB3wsk5JlTErsjg= -github.com/stackitcloud/stackit-sdk-go/services/ske v0.14.0/go.mod h1:0fFs4R7kg+gU7FNAIzzFvlCZJz6gyZ8CFhbK3eSrAwQ= +github.com/stackitcloud/stackit-sdk-go/services/ske v0.15.0 h1:7iTzdiglvJmKMaHlr4JUPvNOmA730rAniry74cnZ8zI= +github.com/stackitcloud/stackit-sdk-go/services/ske v0.15.0/go.mod h1:0fFs4R7kg+gU7FNAIzzFvlCZJz6gyZ8CFhbK3eSrAwQ= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= github.com/stretchr/objx v0.5.0 h1:1zr/of2m5FGMsad5YfcqgdqdWrIhu+EBEJRhR1U7z/c= github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= +github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk= github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= github.com/subosito/gotenv v1.6.0 h1:9NlTDc1FTs4qu0DDq7AEtTPNw6SVm7uBMsUCUjABIf8= github.com/subosito/gotenv v1.6.0/go.mod h1:Dk4QP5c2W3ibzajGcXpNraDfq2IrhjMIvMSWPKKo0FU= +github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= +github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/zalando/go-keyring v0.2.4 h1:wi2xxTqdiwMKbM6TWwi+uJCG/Tum2UV0jqaQhCa9/68= github.com/zalando/go-keyring v0.2.4/go.mod h1:HL4k+OXQfJUWaMnqyuSOc0drfGPX2b51Du6K+MRgZMk= go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0= go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y= +golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= +golang.org/x/crypto v0.21.0 h1:X31++rzVUdKhX5sWmSOFZxx8UW/ldWx55cbf08iNAMA= +golang.org/x/crypto v0.21.0/go.mod h1:0BP7YvVV9gBbVKyeTG0Gyn+gZm94bibOW5BjDEYAOMs= golang.org/x/exp v0.0.0-20240119083558-1b970713d09a h1:Q8/wZp0KX97QFTc2ywcOE0YRjZPVIx+MXInMzdvQqcA= golang.org/x/exp v0.0.0-20240119083558-1b970713d09a/go.mod h1:idGWGoKP1toJGkd5/ig9ZLuPcZBC3ewk7SzmH0uou08= +golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.17.0 h1:zY54UmvipHiNd+pm+m0x9KhZ9hl1/7QNMyxXbc6ICqA= golang.org/x/mod v0.17.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= +golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= +golang.org/x/net v0.23.0 h1:7EYJ93RZ9vYSZAIb2x3lnuvqO5zneoD6IvWjuhfxjTs= +golang.org/x/net v0.23.0/go.mod h1:JKghWKKOSdJwpW2GEx0Ja7fmaKnMsbu+MWVZTokSYmg= golang.org/x/oauth2 v0.20.0 h1:4mQdhULixXKP1rwYBW0vAijoXnkTG0BLCDRzfe1idMo= golang.org/x/oauth2 v0.20.0/go.mod h1:XYTD2NtWslqkgxebSiOHnXEap4TF09sJSc7H1sXbhtI= +golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.20.0 h1:Od9JTbYCk261bKm4M/mw7AklTlFYIa0bIp9BgSm1S8Y= golang.org/x/sys v0.20.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/term v0.20.0 h1:VnkxpohqXaOBYJtBmEppKUG6mXpi+4O6purfc2+sMhw= golang.org/x/term v0.20.0/go.mod h1:8UkIAJTvZgivsXaD6/pH6U9ecQzZ45awqEOzuCvwpFY= +golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.15.0 h1:h1V/4gjBv8v9cjcR6+AR5+/cIYK5N/WAgiv4xlsEtAk= golang.org/x/text v0.15.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= +golang.org/x/time v0.5.0 h1:o7cqy6amK/52YcAKIPlM3a+Fpj35zvRj2TP+e1xFSfk= +golang.org/x/time v0.5.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM= +golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= +golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= +golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20220907171357-04be3eba64a2 h1:H2TDz8ibqkAF6YGhCdN3jS9O0/s90v0rJh3X/OLHEUk= +golang.org/x/xerrors v0.0.0-20220907171357-04be3eba64a2/go.mod h1:K8+ghG5WaK9qNqU5K3HdILfMLy1f3aNYFI/wnl100a8= +google.golang.org/protobuf v1.31.0 h1:g0LDEJHgrBl9N9r17Ru3sqWhkIx2NB67okBHPwC7hs8= +google.golang.org/protobuf v1.31.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= -gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f h1:BLraFXnmrev5lT+xlilqcH8XK9/i0At2xKjWk4p6zsU= -gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= +gopkg.in/inf.v0 v0.9.1 h1:73M5CoZyi3ZLMOyDlQh031Cx6N9NDJ2Vvfl76EDAgDc= +gopkg.in/inf.v0 v0.9.1/go.mod h1:cWUDdTG/fYaXco+Dcufb5Vnc6Gp2YChqWtbxRZE0mXw= gopkg.in/ini.v1 v1.67.0 h1:Dgnx+6+nfE+IfzjUEISNeydPJh9AXNNsWbGP9KzCsOA= gopkg.in/ini.v1 v1.67.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k= +gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= +gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +k8s.io/api v0.29.2 h1:hBC7B9+MU+ptchxEqTNW2DkUosJpp1P+Wn6YncZ474A= +k8s.io/api v0.29.2/go.mod h1:sdIaaKuU7P44aoyyLlikSLayT6Vb7bvJNCX105xZXY0= +k8s.io/apimachinery v0.29.2 h1:EWGpfJ856oj11C52NRCHuU7rFDwxev48z+6DSlGNsV8= +k8s.io/apimachinery v0.29.2/go.mod h1:6HVkd1FwxIagpYrHSwJlQqZI3G9LfYWRPAkUvLnXTKU= +k8s.io/client-go v0.29.2 h1:FEg85el1TeZp+/vYJM7hkDlSTFZ+c5nnK44DJ4FyoRg= +k8s.io/client-go v0.29.2/go.mod h1:knlvFZE58VpqbQpJNbCbctTVXcd35mMyAAwBdpt4jrA= +k8s.io/klog/v2 v2.110.1 h1:U/Af64HJf7FcwMcXyKm2RPM22WZzyR7OSpYj5tg3cL0= +k8s.io/klog/v2 v2.110.1/go.mod h1:YGtd1984u+GgbuZ7e08/yBuAfKLSO0+uR1Fhi6ExXjo= +k8s.io/kube-openapi v0.0.0-20231010175941-2dd684a91f00 h1:aVUu9fTY98ivBPKR9Y5w/AuzbMm96cd3YHRTU83I780= +k8s.io/kube-openapi v0.0.0-20231010175941-2dd684a91f00/go.mod h1:AsvuZPBlUDVuCdzJ87iajxtXuR9oktsTctW/R9wwouA= +k8s.io/utils v0.0.0-20230726121419-3b25d923346b h1:sgn3ZU783SCgtaSJjpcVVlRqd6GSnlTLKgpAAttJvpI= +k8s.io/utils v0.0.0-20230726121419-3b25d923346b/go.mod h1:OLgZIPagt7ERELqWJFomSt595RzquPNLL48iOWgYOg0= +sigs.k8s.io/json v0.0.0-20221116044647-bc3834ca7abd h1:EDPBXCAspyGV4jQlpZSudPeMmr1bNJefnuqLsRAsHZo= +sigs.k8s.io/json v0.0.0-20221116044647-bc3834ca7abd/go.mod h1:B8JuhiUyNFVKdsE8h686QcCxMaH6HrOAZj4vswFpcB0= +sigs.k8s.io/structured-merge-diff/v4 v4.4.1 h1:150L+0vs/8DA78h1u02ooW1/fFq/Lwr+sGiqlzvrtq4= +sigs.k8s.io/structured-merge-diff/v4 v4.4.1/go.mod h1:N8hJocpFajUSSeSJ9bOZ77VzejKZaXsTtZo4/u7Io08= +sigs.k8s.io/yaml v1.3.0 h1:a2VclLzOGrwOHDiV8EfBGhvjHvP46CtW5j6POvhYGGo= +sigs.k8s.io/yaml v1.3.0/go.mod h1:GeOyir5tyXNByN85N/dRIT9es5UQNerPYEKK56eTBm8= diff --git a/internal/cmd/argus/credentials/list/list.go b/internal/cmd/argus/credentials/list/list.go index c4739d5c9..795b4278e 100644 --- a/internal/cmd/argus/credentials/list/list.go +++ b/internal/cmd/argus/credentials/list/list.go @@ -5,6 +5,7 @@ import ( "encoding/json" "fmt" + "github.com/goccy/go-yaml" "github.com/stackitcloud/stackit-cli/internal/pkg/args" "github.com/stackitcloud/stackit-cli/internal/pkg/errors" "github.com/stackitcloud/stackit-cli/internal/pkg/examples" @@ -131,6 +132,14 @@ func outputResult(p *print.Printer, outputFormat string, credentials []argus.Ser } p.Outputln(string(details)) + return nil + case print.YAMLOutputFormat: + details, err := yaml.MarshalWithOptions(credentials, yaml.IndentSequence(true)) + if err != nil { + return fmt.Errorf("marshal Argus credentials list: %w", err) + } + p.Outputln(string(details)) + return nil default: table := tables.NewTable() diff --git a/internal/cmd/argus/grafana/describe/describe.go b/internal/cmd/argus/grafana/describe/describe.go index ef17e74b2..4c8f930b7 100644 --- a/internal/cmd/argus/grafana/describe/describe.go +++ b/internal/cmd/argus/grafana/describe/describe.go @@ -5,6 +5,7 @@ import ( "encoding/json" "fmt" + "github.com/goccy/go-yaml" "github.com/stackitcloud/stackit-cli/internal/pkg/args" "github.com/stackitcloud/stackit-cli/internal/pkg/errors" "github.com/stackitcloud/stackit-cli/internal/pkg/examples" @@ -132,6 +133,14 @@ func outputResult(p *print.Printer, inputModel *inputModel, grafanaConfigs *argu } p.Outputln(string(details)) + return nil + case print.YAMLOutputFormat: + details, err := yaml.MarshalWithOptions(grafanaConfigs, yaml.IndentSequence(true)) + if err != nil { + return fmt.Errorf("marshal Grafana configs: %w", err) + } + p.Outputln(string(details)) + return nil default: initialAdminPassword := *instance.Instance.GrafanaAdminPassword diff --git a/internal/cmd/argus/instance/create/create.go b/internal/cmd/argus/instance/create/create.go index 333ad7ac3..b37396bb7 100644 --- a/internal/cmd/argus/instance/create/create.go +++ b/internal/cmd/argus/instance/create/create.go @@ -6,6 +6,7 @@ import ( "errors" "fmt" + "github.com/goccy/go-yaml" "github.com/stackitcloud/stackit-cli/internal/pkg/args" cliErr "github.com/stackitcloud/stackit-cli/internal/pkg/errors" "github.com/stackitcloud/stackit-cli/internal/pkg/examples" @@ -207,6 +208,14 @@ func outputResult(p *print.Printer, model *inputModel, projectLabel string, resp } p.Outputln(string(details)) + return nil + case print.YAMLOutputFormat: + details, err := yaml.MarshalWithOptions(resp, yaml.IndentSequence(true)) + if err != nil { + return fmt.Errorf("marshal Argus instance: %w", err) + } + p.Outputln(string(details)) + return nil default: operationState := "Created" diff --git a/internal/cmd/argus/instance/describe/describe.go b/internal/cmd/argus/instance/describe/describe.go index c2ff58b7d..c6e8bc197 100644 --- a/internal/cmd/argus/instance/describe/describe.go +++ b/internal/cmd/argus/instance/describe/describe.go @@ -5,6 +5,7 @@ import ( "encoding/json" "fmt" + "github.com/goccy/go-yaml" "github.com/stackitcloud/stackit-cli/internal/pkg/args" "github.com/stackitcloud/stackit-cli/internal/pkg/errors" "github.com/stackitcloud/stackit-cli/internal/pkg/examples" @@ -105,6 +106,14 @@ func outputResult(p *print.Printer, outputFormat string, instance *argus.GetInst } p.Outputln(string(details)) + return nil + case print.YAMLOutputFormat: + details, err := yaml.MarshalWithOptions(instance, yaml.IndentSequence(true)) + if err != nil { + return fmt.Errorf("marshal Argus instance: %w", err) + } + p.Outputln(string(details)) + return nil default: table := tables.NewTable() diff --git a/internal/cmd/argus/instance/list/list.go b/internal/cmd/argus/instance/list/list.go index 13b089afa..98a8e74d5 100644 --- a/internal/cmd/argus/instance/list/list.go +++ b/internal/cmd/argus/instance/list/list.go @@ -5,6 +5,7 @@ import ( "encoding/json" "fmt" + "github.com/goccy/go-yaml" "github.com/stackitcloud/stackit-cli/internal/pkg/args" "github.com/stackitcloud/stackit-cli/internal/pkg/errors" "github.com/stackitcloud/stackit-cli/internal/pkg/examples" @@ -137,6 +138,14 @@ func outputResult(p *print.Printer, outputFormat string, instances []argus.Proje } p.Outputln(string(details)) + return nil + case print.YAMLOutputFormat: + details, err := yaml.MarshalWithOptions(instances, yaml.IndentSequence(true)) + if err != nil { + return fmt.Errorf("marshal Argus instance list: %w", err) + } + p.Outputln(string(details)) + return nil default: table := tables.NewTable() diff --git a/internal/cmd/argus/plans/plans.go b/internal/cmd/argus/plans/plans.go index 084dfcd04..a80800177 100644 --- a/internal/cmd/argus/plans/plans.go +++ b/internal/cmd/argus/plans/plans.go @@ -5,6 +5,7 @@ import ( "encoding/json" "fmt" + "github.com/goccy/go-yaml" "github.com/stackitcloud/stackit-cli/internal/pkg/args" "github.com/stackitcloud/stackit-cli/internal/pkg/errors" "github.com/stackitcloud/stackit-cli/internal/pkg/examples" @@ -137,6 +138,14 @@ func outputResult(p *print.Printer, outputFormat string, plans []argus.Plan) err } p.Outputln(string(details)) + return nil + case print.YAMLOutputFormat: + details, err := yaml.MarshalWithOptions(plans, yaml.IndentSequence(true)) + if err != nil { + return fmt.Errorf("marshal Argus plans: %w", err) + } + p.Outputln(string(details)) + return nil default: table := tables.NewTable() diff --git a/internal/cmd/argus/scrape-config/describe/describe.go b/internal/cmd/argus/scrape-config/describe/describe.go index 940c431c6..2b9d1fb99 100644 --- a/internal/cmd/argus/scrape-config/describe/describe.go +++ b/internal/cmd/argus/scrape-config/describe/describe.go @@ -6,6 +6,7 @@ import ( "fmt" "strings" + "github.com/goccy/go-yaml" "github.com/stackitcloud/stackit-cli/internal/pkg/args" "github.com/stackitcloud/stackit-cli/internal/pkg/errors" "github.com/stackitcloud/stackit-cli/internal/pkg/examples" @@ -107,6 +108,14 @@ func outputResult(p *print.Printer, outputFormat string, config *argus.Job) erro } p.Outputln(string(details)) + return nil + case print.YAMLOutputFormat: + details, err := yaml.MarshalWithOptions(config, yaml.IndentSequence(true)) + if err != nil { + return fmt.Errorf("marshal scrape configuration: %w", err) + } + p.Outputln(string(details)) + return nil default: saml2Enabled := "Enabled" diff --git a/internal/cmd/argus/scrape-config/list/list.go b/internal/cmd/argus/scrape-config/list/list.go index c726b53aa..60f6598a0 100644 --- a/internal/cmd/argus/scrape-config/list/list.go +++ b/internal/cmd/argus/scrape-config/list/list.go @@ -5,6 +5,7 @@ import ( "encoding/json" "fmt" + "github.com/goccy/go-yaml" "github.com/stackitcloud/stackit-cli/internal/pkg/args" "github.com/stackitcloud/stackit-cli/internal/pkg/errors" "github.com/stackitcloud/stackit-cli/internal/pkg/examples" @@ -134,6 +135,14 @@ func outputResult(p *print.Printer, outputFormat string, configs []argus.Job) er } p.Outputln(string(details)) + return nil + case print.YAMLOutputFormat: + details, err := yaml.MarshalWithOptions(configs, yaml.IndentSequence(true)) + if err != nil { + return fmt.Errorf("marshal scrape configurations list: %w", err) + } + p.Outputln(string(details)) + return nil default: table := tables.NewTable() diff --git a/internal/cmd/config/list/list.go b/internal/cmd/config/list/list.go index d6c3e2055..8ba6160f4 100644 --- a/internal/cmd/config/list/list.go +++ b/internal/cmd/config/list/list.go @@ -9,6 +9,7 @@ import ( "strconv" "strings" + "github.com/goccy/go-yaml" "github.com/stackitcloud/stackit-cli/internal/pkg/args" "github.com/stackitcloud/stackit-cli/internal/pkg/config" "github.com/stackitcloud/stackit-cli/internal/pkg/examples" @@ -81,6 +82,14 @@ func outputResult(p *print.Printer, outputFormat string, configData map[string]a return fmt.Errorf("marshal config list: %w", err) } p.Outputln(string(details)) + return nil + case print.YAMLOutputFormat: + details, err := yaml.MarshalWithOptions(configData, yaml.IndentSequence(true)) + if err != nil { + return fmt.Errorf("marshal config list: %w", err) + } + p.Outputln(string(details)) + return nil default: diff --git a/internal/cmd/dns/record-set/create/create.go b/internal/cmd/dns/record-set/create/create.go index f85e041fb..a6beefdc3 100644 --- a/internal/cmd/dns/record-set/create/create.go +++ b/internal/cmd/dns/record-set/create/create.go @@ -5,6 +5,7 @@ import ( "encoding/json" "fmt" + "github.com/goccy/go-yaml" "github.com/stackitcloud/stackit-cli/internal/pkg/args" "github.com/stackitcloud/stackit-cli/internal/pkg/errors" "github.com/stackitcloud/stackit-cli/internal/pkg/examples" @@ -174,6 +175,14 @@ func outputResult(p *print.Printer, model *inputModel, zoneLabel string, resp *d } p.Outputln(string(details)) + return nil + case print.YAMLOutputFormat: + details, err := yaml.MarshalWithOptions(resp, yaml.IndentSequence(true)) + if err != nil { + return fmt.Errorf("marshal DNS record-set: %w", err) + } + p.Outputln(string(details)) + return nil default: operationState := "Created" diff --git a/internal/cmd/dns/record-set/describe/describe.go b/internal/cmd/dns/record-set/describe/describe.go index c10d003eb..221729d91 100644 --- a/internal/cmd/dns/record-set/describe/describe.go +++ b/internal/cmd/dns/record-set/describe/describe.go @@ -6,6 +6,7 @@ import ( "fmt" "strings" + "github.com/goccy/go-yaml" "github.com/stackitcloud/stackit-cli/internal/pkg/args" "github.com/stackitcloud/stackit-cli/internal/pkg/errors" "github.com/stackitcloud/stackit-cli/internal/pkg/examples" @@ -121,6 +122,14 @@ func outputResult(p *print.Printer, outputFormat string, recordSet *dns.RecordSe } p.Outputln(string(details)) + return nil + case print.YAMLOutputFormat: + details, err := yaml.MarshalWithOptions(recordSet, yaml.IndentSequence(true)) + if err != nil { + return fmt.Errorf("marshal DNS record set: %w", err) + } + p.Outputln(string(details)) + return nil default: recordsData := make([]string, 0, len(*recordSet.Records)) diff --git a/internal/cmd/dns/record-set/list/list.go b/internal/cmd/dns/record-set/list/list.go index 3b8aa0c6d..a3a842832 100644 --- a/internal/cmd/dns/record-set/list/list.go +++ b/internal/cmd/dns/record-set/list/list.go @@ -6,6 +6,7 @@ import ( "fmt" "strings" + "github.com/goccy/go-yaml" "github.com/stackitcloud/stackit-cli/internal/pkg/args" "github.com/stackitcloud/stackit-cli/internal/pkg/errors" "github.com/stackitcloud/stackit-cli/internal/pkg/examples" @@ -242,6 +243,14 @@ func outputResult(p *print.Printer, outputFormat string, recordSets []dns.Record } p.Outputln(string(details)) + return nil + case print.YAMLOutputFormat: + details, err := yaml.MarshalWithOptions(recordSets, yaml.IndentSequence(true)) + if err != nil { + return fmt.Errorf("marshal DNS record set list: %w", err) + } + p.Outputln(string(details)) + return nil default: table := tables.NewTable() diff --git a/internal/cmd/dns/zone/create/create.go b/internal/cmd/dns/zone/create/create.go index 442776e75..95507e40d 100644 --- a/internal/cmd/dns/zone/create/create.go +++ b/internal/cmd/dns/zone/create/create.go @@ -5,6 +5,7 @@ import ( "encoding/json" "fmt" + "github.com/goccy/go-yaml" "github.com/stackitcloud/stackit-cli/internal/pkg/args" "github.com/stackitcloud/stackit-cli/internal/pkg/errors" "github.com/stackitcloud/stackit-cli/internal/pkg/examples" @@ -203,6 +204,14 @@ func outputResult(p *print.Printer, model *inputModel, projectLabel string, resp } p.Outputln(string(details)) + return nil + case print.YAMLOutputFormat: + details, err := yaml.MarshalWithOptions(resp, yaml.IndentSequence(true)) + if err != nil { + return fmt.Errorf("marshal DNS zone: %w", err) + } + p.Outputln(string(details)) + return nil default: operationState := "Created" diff --git a/internal/cmd/dns/zone/describe/describe.go b/internal/cmd/dns/zone/describe/describe.go index 1830471b4..578dee03c 100644 --- a/internal/cmd/dns/zone/describe/describe.go +++ b/internal/cmd/dns/zone/describe/describe.go @@ -5,6 +5,7 @@ import ( "encoding/json" "fmt" + "github.com/goccy/go-yaml" "github.com/stackitcloud/stackit-cli/internal/pkg/args" "github.com/stackitcloud/stackit-cli/internal/pkg/errors" "github.com/stackitcloud/stackit-cli/internal/pkg/examples" @@ -106,6 +107,14 @@ func outputResult(p *print.Printer, outputFormat string, zone *dns.Zone) error { } p.Outputln(string(details)) + return nil + case print.YAMLOutputFormat: + details, err := yaml.MarshalWithOptions(zone, yaml.IndentSequence(true)) + if err != nil { + return fmt.Errorf("marshal DNS zone: %w", err) + } + p.Outputln(string(details)) + return nil default: table := tables.NewTable() diff --git a/internal/cmd/dns/zone/list/list.go b/internal/cmd/dns/zone/list/list.go index bc378b10f..49e23a0b0 100644 --- a/internal/cmd/dns/zone/list/list.go +++ b/internal/cmd/dns/zone/list/list.go @@ -6,6 +6,7 @@ import ( "fmt" "strings" + "github.com/goccy/go-yaml" "github.com/stackitcloud/stackit-cli/internal/pkg/args" "github.com/stackitcloud/stackit-cli/internal/pkg/errors" "github.com/stackitcloud/stackit-cli/internal/pkg/examples" @@ -231,6 +232,14 @@ func outputResult(p *print.Printer, outputFormat string, zones []dns.Zone) error } p.Outputln(string(details)) + return nil + case print.YAMLOutputFormat: + details, err := yaml.MarshalWithOptions(zones, yaml.IndentSequence(true)) + if err != nil { + return fmt.Errorf("marshal DNS zone list: %w", err) + } + p.Outputln(string(details)) + return nil default: table := tables.NewTable() diff --git a/internal/cmd/load-balancer/describe/describe.go b/internal/cmd/load-balancer/describe/describe.go index ff6defaf0..d5cf4dec9 100644 --- a/internal/cmd/load-balancer/describe/describe.go +++ b/internal/cmd/load-balancer/describe/describe.go @@ -6,6 +6,7 @@ import ( "fmt" "strings" + "github.com/goccy/go-yaml" "github.com/stackitcloud/stackit-cli/internal/pkg/args" "github.com/stackitcloud/stackit-cli/internal/pkg/errors" "github.com/stackitcloud/stackit-cli/internal/pkg/examples" @@ -105,6 +106,14 @@ func outputResult(p *print.Printer, outputFormat string, loadBalancer *loadbalan } p.Outputln(string(details)) + return nil + case print.YAMLOutputFormat: + details, err := yaml.MarshalWithOptions(loadBalancer, yaml.IndentSequence(true)) + if err != nil { + return fmt.Errorf("marshal load balancer: %w", err) + } + p.Outputln(string(details)) + return nil default: return outputResultAsTable(p, loadBalancer) diff --git a/internal/cmd/load-balancer/list/list.go b/internal/cmd/load-balancer/list/list.go index d5b568216..a82a0384d 100644 --- a/internal/cmd/load-balancer/list/list.go +++ b/internal/cmd/load-balancer/list/list.go @@ -5,6 +5,7 @@ import ( "encoding/json" "fmt" + "github.com/goccy/go-yaml" "github.com/stackitcloud/stackit-cli/internal/pkg/args" "github.com/stackitcloud/stackit-cli/internal/pkg/errors" "github.com/stackitcloud/stackit-cli/internal/pkg/examples" @@ -138,6 +139,14 @@ func outputResult(p *print.Printer, outputFormat string, loadBalancers []loadbal } p.Outputln(string(details)) + return nil + case print.YAMLOutputFormat: + details, err := yaml.MarshalWithOptions(loadBalancers, yaml.IndentSequence(true)) + if err != nil { + return fmt.Errorf("marshal load balancer list: %w", err) + } + p.Outputln(string(details)) + return nil default: table := tables.NewTable() diff --git a/internal/cmd/load-balancer/observability-credentials/add/add.go b/internal/cmd/load-balancer/observability-credentials/add/add.go index e97963d9f..b1213eefd 100644 --- a/internal/cmd/load-balancer/observability-credentials/add/add.go +++ b/internal/cmd/load-balancer/observability-credentials/add/add.go @@ -5,6 +5,7 @@ import ( "encoding/json" "fmt" + "github.com/goccy/go-yaml" "github.com/google/uuid" "github.com/stackitcloud/stackit-cli/internal/pkg/args" "github.com/stackitcloud/stackit-cli/internal/pkg/errors" @@ -156,6 +157,14 @@ func outputResult(p *print.Printer, model *inputModel, projectLabel string, resp } p.Outputln(string(details)) + return nil + case print.YAMLOutputFormat: + details, err := yaml.MarshalWithOptions(resp, yaml.IndentSequence(true)) + if err != nil { + return fmt.Errorf("marshal Load Balancer observability credentials: %w", err) + } + p.Outputln(string(details)) + return nil default: p.Outputf("Added Load Balancer observability credentials on project %q. Credentials reference: %q\n", projectLabel, *resp.Credential.CredentialsRef) diff --git a/internal/cmd/load-balancer/observability-credentials/describe/describe.go b/internal/cmd/load-balancer/observability-credentials/describe/describe.go index 31d109501..27d27caf8 100644 --- a/internal/cmd/load-balancer/observability-credentials/describe/describe.go +++ b/internal/cmd/load-balancer/observability-credentials/describe/describe.go @@ -5,6 +5,7 @@ import ( "encoding/json" "fmt" + "github.com/goccy/go-yaml" "github.com/stackitcloud/stackit-cli/internal/pkg/args" "github.com/stackitcloud/stackit-cli/internal/pkg/errors" "github.com/stackitcloud/stackit-cli/internal/pkg/examples" @@ -102,6 +103,14 @@ func outputResult(p *print.Printer, outputFormat string, credentials *loadbalanc } p.Outputln(string(details)) + return nil + case print.YAMLOutputFormat: + details, err := yaml.MarshalWithOptions(credentials, yaml.IndentSequence(true)) + if err != nil { + return fmt.Errorf("marshal Load Balancer observability credentials: %w", err) + } + p.Outputln(string(details)) + return nil default: table := tables.NewTable() diff --git a/internal/cmd/load-balancer/observability-credentials/list/list.go b/internal/cmd/load-balancer/observability-credentials/list/list.go index f71ab67a9..226d9f06a 100644 --- a/internal/cmd/load-balancer/observability-credentials/list/list.go +++ b/internal/cmd/load-balancer/observability-credentials/list/list.go @@ -5,6 +5,7 @@ import ( "encoding/json" "fmt" + "github.com/goccy/go-yaml" "github.com/stackitcloud/stackit-cli/internal/pkg/args" "github.com/stackitcloud/stackit-cli/internal/pkg/errors" "github.com/stackitcloud/stackit-cli/internal/pkg/examples" @@ -174,6 +175,14 @@ func outputResult(p *print.Printer, outputFormat string, credentials []loadbalan } p.Outputln(string(details)) + return nil + case print.YAMLOutputFormat: + details, err := yaml.MarshalWithOptions(credentials, yaml.IndentSequence(true)) + if err != nil { + return fmt.Errorf("marshal Load Balancer observability credentials list: %w", err) + } + p.Outputln(string(details)) + return nil default: table := tables.NewTable() diff --git a/internal/cmd/load-balancer/quota/quota.go b/internal/cmd/load-balancer/quota/quota.go index 7762e78bc..593e117b7 100644 --- a/internal/cmd/load-balancer/quota/quota.go +++ b/internal/cmd/load-balancer/quota/quota.go @@ -6,6 +6,7 @@ import ( "fmt" "strconv" + "github.com/goccy/go-yaml" "github.com/stackitcloud/stackit-cli/internal/pkg/args" "github.com/stackitcloud/stackit-cli/internal/pkg/errors" "github.com/stackitcloud/stackit-cli/internal/pkg/examples" @@ -95,6 +96,14 @@ func outputResult(p *print.Printer, outputFormat string, quota *loadbalancer.Get p.Outputf("Maximum number of load balancers allowed: %s\n", maxLoadBalancers) + return nil + case print.YAMLOutputFormat: + details, err := yaml.MarshalWithOptions(quota, yaml.IndentSequence(true)) + if err != nil { + return fmt.Errorf("marshal quota: %w", err) + } + p.Outputln(string(details)) + return nil default: details, err := json.MarshalIndent(quota, "", " ") diff --git a/internal/cmd/load-balancer/target-pool/describe/describe.go b/internal/cmd/load-balancer/target-pool/describe/describe.go index c1f06d196..e3e5921fc 100644 --- a/internal/cmd/load-balancer/target-pool/describe/describe.go +++ b/internal/cmd/load-balancer/target-pool/describe/describe.go @@ -7,6 +7,7 @@ import ( "strconv" "strings" + "github.com/goccy/go-yaml" "github.com/stackitcloud/stackit-cli/internal/pkg/args" "github.com/stackitcloud/stackit-cli/internal/pkg/errors" "github.com/stackitcloud/stackit-cli/internal/pkg/examples" @@ -119,21 +120,30 @@ func buildRequest(ctx context.Context, model *inputModel, apiClient *loadbalance } func outputResult(p *print.Printer, outputFormat string, targetPool loadbalancer.TargetPool, listener *loadbalancer.Listener) error { + output := struct { + *loadbalancer.TargetPool + Listener *loadbalancer.Listener `json:"attached_listener"` + }{ + &targetPool, + listener, + } + switch outputFormat { case print.JSONOutputFormat: - output := struct { - *loadbalancer.TargetPool - Listener *loadbalancer.Listener `json:"attached_listener"` - }{ - &targetPool, - listener, - } details, err := json.MarshalIndent(output, "", " ") if err != nil { return fmt.Errorf("marshal load balancer: %w", err) } p.Outputln(string(details)) + return nil + case print.YAMLOutputFormat: + details, err := yaml.MarshalWithOptions(output, yaml.IndentSequence(true)) + if err != nil { + return fmt.Errorf("marshal load balancer: %w", err) + } + p.Outputln(string(details)) + return nil default: return outputResultAsTable(p, targetPool, listener) diff --git a/internal/cmd/logme/credentials/create/create.go b/internal/cmd/logme/credentials/create/create.go index e50efa5c7..76b3b14f1 100644 --- a/internal/cmd/logme/credentials/create/create.go +++ b/internal/cmd/logme/credentials/create/create.go @@ -5,6 +5,8 @@ import ( "encoding/json" "fmt" + "github.com/goccy/go-yaml" + "github.com/spf13/cobra" "github.com/stackitcloud/stackit-cli/internal/pkg/args" "github.com/stackitcloud/stackit-cli/internal/pkg/errors" "github.com/stackitcloud/stackit-cli/internal/pkg/examples" @@ -14,8 +16,6 @@ import ( "github.com/stackitcloud/stackit-cli/internal/pkg/services/logme/client" logmeUtils "github.com/stackitcloud/stackit-cli/internal/pkg/services/logme/utils" "github.com/stackitcloud/stackit-cli/internal/pkg/utils" - - "github.com/spf13/cobra" "github.com/stackitcloud/stackit-sdk-go/services/logme" ) @@ -123,17 +123,25 @@ func buildRequest(ctx context.Context, model *inputModel, apiClient *logme.APICl } func outputResult(p *print.Printer, model *inputModel, instanceLabel string, resp *logme.CredentialsResponse) error { + if !model.ShowPassword { + resp.Raw.Credentials.Password = utils.Ptr("hidden") + } switch model.OutputFormat { case print.JSONOutputFormat: - if !model.ShowPassword { - resp.Raw.Credentials.Password = utils.Ptr("hidden") - } details, err := json.MarshalIndent(resp, "", " ") if err != nil { return fmt.Errorf("marshal LogMe credentials: %w", err) } p.Outputln(string(details)) + return nil + case print.YAMLOutputFormat: + details, err := yaml.MarshalWithOptions(resp, yaml.IndentSequence(true)) + if err != nil { + return fmt.Errorf("marshal LogMe credentials: %w", err) + } + p.Outputln(string(details)) + return nil default: p.Outputf("Created credentials for instance %q. Credentials ID: %s\n\n", instanceLabel, *resp.Id) diff --git a/internal/cmd/logme/credentials/describe/describe.go b/internal/cmd/logme/credentials/describe/describe.go index e9b5f7987..e2b7551a9 100644 --- a/internal/cmd/logme/credentials/describe/describe.go +++ b/internal/cmd/logme/credentials/describe/describe.go @@ -5,6 +5,7 @@ import ( "encoding/json" "fmt" + "github.com/goccy/go-yaml" "github.com/stackitcloud/stackit-cli/internal/pkg/args" "github.com/stackitcloud/stackit-cli/internal/pkg/errors" "github.com/stackitcloud/stackit-cli/internal/pkg/examples" @@ -119,6 +120,14 @@ func outputResult(p *print.Printer, outputFormat string, credentials *logme.Cred } p.Outputln(string(details)) + return nil + case print.YAMLOutputFormat: + details, err := yaml.MarshalWithOptions(credentials, yaml.IndentSequence(true)) + if err != nil { + return fmt.Errorf("marshal LogMe credentials: %w", err) + } + p.Outputln(string(details)) + return nil default: table := tables.NewTable() diff --git a/internal/cmd/logme/credentials/list/list.go b/internal/cmd/logme/credentials/list/list.go index db24234af..653902d56 100644 --- a/internal/cmd/logme/credentials/list/list.go +++ b/internal/cmd/logme/credentials/list/list.go @@ -5,6 +5,7 @@ import ( "encoding/json" "fmt" + "github.com/goccy/go-yaml" "github.com/stackitcloud/stackit-cli/internal/pkg/args" "github.com/stackitcloud/stackit-cli/internal/pkg/errors" "github.com/stackitcloud/stackit-cli/internal/pkg/examples" @@ -142,6 +143,14 @@ func outputResult(p *print.Printer, outputFormat string, credentials []logme.Cre } p.Outputln(string(details)) + return nil + case print.YAMLOutputFormat: + details, err := yaml.MarshalWithOptions(credentials, yaml.IndentSequence(true)) + if err != nil { + return fmt.Errorf("marshal LogMe credentials list: %w", err) + } + p.Outputln(string(details)) + return nil default: table := tables.NewTable() diff --git a/internal/cmd/logme/instance/create/create.go b/internal/cmd/logme/instance/create/create.go index 8ad379da4..7e2b15e2f 100644 --- a/internal/cmd/logme/instance/create/create.go +++ b/internal/cmd/logme/instance/create/create.go @@ -7,6 +7,7 @@ import ( "fmt" "strings" + "github.com/goccy/go-yaml" "github.com/stackitcloud/stackit-cli/internal/pkg/args" cliErr "github.com/stackitcloud/stackit-cli/internal/pkg/errors" "github.com/stackitcloud/stackit-cli/internal/pkg/examples" @@ -259,6 +260,14 @@ func outputResult(p *print.Printer, model *inputModel, projectLabel string, resp } p.Outputln(string(details)) + return nil + case print.YAMLOutputFormat: + details, err := yaml.MarshalWithOptions(resp, yaml.IndentSequence(true)) + if err != nil { + return fmt.Errorf("marshal LogMe instance: %w", err) + } + p.Outputln(string(details)) + return nil default: operationState := "Created" diff --git a/internal/cmd/logme/instance/describe/describe.go b/internal/cmd/logme/instance/describe/describe.go index 613091bc0..9e477f16a 100644 --- a/internal/cmd/logme/instance/describe/describe.go +++ b/internal/cmd/logme/instance/describe/describe.go @@ -5,6 +5,7 @@ import ( "encoding/json" "fmt" + "github.com/goccy/go-yaml" "github.com/stackitcloud/stackit-cli/internal/pkg/args" "github.com/stackitcloud/stackit-cli/internal/pkg/errors" "github.com/stackitcloud/stackit-cli/internal/pkg/examples" @@ -107,6 +108,14 @@ func outputResult(p *print.Printer, outputFormat string, instance *logme.Instanc } p.Outputln(string(details)) + return nil + case print.YAMLOutputFormat: + details, err := yaml.MarshalWithOptions(instance, yaml.IndentSequence(true)) + if err != nil { + return fmt.Errorf("marshal LogMe instance: %w", err) + } + p.Outputln(string(details)) + return nil default: table := tables.NewTable() diff --git a/internal/cmd/logme/instance/list/list.go b/internal/cmd/logme/instance/list/list.go index e3b7dd712..24e3cf999 100644 --- a/internal/cmd/logme/instance/list/list.go +++ b/internal/cmd/logme/instance/list/list.go @@ -5,6 +5,7 @@ import ( "encoding/json" "fmt" + "github.com/goccy/go-yaml" "github.com/stackitcloud/stackit-cli/internal/pkg/args" "github.com/stackitcloud/stackit-cli/internal/pkg/errors" "github.com/stackitcloud/stackit-cli/internal/pkg/examples" @@ -137,6 +138,14 @@ func outputResult(p *print.Printer, outputFormat string, instances []logme.Insta } p.Outputln(string(details)) + return nil + case print.YAMLOutputFormat: + details, err := yaml.MarshalWithOptions(instances, yaml.IndentSequence(true)) + if err != nil { + return fmt.Errorf("marshal LogMe instance list: %w", err) + } + p.Outputln(string(details)) + return nil default: table := tables.NewTable() diff --git a/internal/cmd/logme/plans/plans.go b/internal/cmd/logme/plans/plans.go index 31094a24d..dfa14ed0e 100644 --- a/internal/cmd/logme/plans/plans.go +++ b/internal/cmd/logme/plans/plans.go @@ -5,6 +5,7 @@ import ( "encoding/json" "fmt" + "github.com/goccy/go-yaml" "github.com/stackitcloud/stackit-cli/internal/pkg/args" "github.com/stackitcloud/stackit-cli/internal/pkg/errors" "github.com/stackitcloud/stackit-cli/internal/pkg/examples" @@ -137,6 +138,14 @@ func outputResult(p *print.Printer, outputFormat string, plans []logme.Offering) } p.Outputln(string(details)) + return nil + case print.YAMLOutputFormat: + details, err := yaml.MarshalWithOptions(plans, yaml.IndentSequence(true)) + if err != nil { + return fmt.Errorf("marshal LogMe plans: %w", err) + } + p.Outputln(string(details)) + return nil default: table := tables.NewTable() diff --git a/internal/cmd/mariadb/credentials/create/create.go b/internal/cmd/mariadb/credentials/create/create.go index bddee6b38..4373782ea 100644 --- a/internal/cmd/mariadb/credentials/create/create.go +++ b/internal/cmd/mariadb/credentials/create/create.go @@ -5,6 +5,7 @@ import ( "encoding/json" "fmt" + "github.com/goccy/go-yaml" "github.com/stackitcloud/stackit-cli/internal/pkg/args" "github.com/stackitcloud/stackit-cli/internal/pkg/errors" "github.com/stackitcloud/stackit-cli/internal/pkg/examples" @@ -123,17 +124,25 @@ func buildRequest(ctx context.Context, model *inputModel, apiClient *mariadb.API } func outputResult(p *print.Printer, model *inputModel, instanceLabel string, resp *mariadb.CredentialsResponse) error { + if !model.ShowPassword { + resp.Raw.Credentials.Password = utils.Ptr("hidden") + } switch model.OutputFormat { case print.JSONOutputFormat: - if !model.ShowPassword { - resp.Raw.Credentials.Password = utils.Ptr("hidden") - } details, err := json.MarshalIndent(resp, "", " ") if err != nil { return fmt.Errorf("marshal MariaDB credentials: %w", err) } p.Outputln(string(details)) + return nil + case print.YAMLOutputFormat: + details, err := yaml.MarshalWithOptions(resp, yaml.IndentSequence(true)) + if err != nil { + return fmt.Errorf("marshal Argus credentials list: %w", err) + } + p.Outputln(string(details)) + return nil default: p.Outputf("Created credentials for instance %q. Credentials ID: %s\n\n", instanceLabel, *resp.Id) diff --git a/internal/cmd/mariadb/credentials/describe/describe.go b/internal/cmd/mariadb/credentials/describe/describe.go index ef37e64b4..ae5eeece0 100644 --- a/internal/cmd/mariadb/credentials/describe/describe.go +++ b/internal/cmd/mariadb/credentials/describe/describe.go @@ -5,6 +5,7 @@ import ( "encoding/json" "fmt" + "github.com/goccy/go-yaml" "github.com/stackitcloud/stackit-cli/internal/pkg/args" "github.com/stackitcloud/stackit-cli/internal/pkg/errors" "github.com/stackitcloud/stackit-cli/internal/pkg/examples" @@ -119,6 +120,14 @@ func outputResult(p *print.Printer, outputFormat string, credentials *mariadb.Cr } p.Outputln(string(details)) + return nil + case print.YAMLOutputFormat: + details, err := yaml.MarshalWithOptions(credentials, yaml.IndentSequence(true)) + if err != nil { + return fmt.Errorf("marshal MariaDB credentials: %w", err) + } + p.Outputln(string(details)) + return nil default: table := tables.NewTable() diff --git a/internal/cmd/mariadb/credentials/list/list.go b/internal/cmd/mariadb/credentials/list/list.go index 7ca6e2f0a..75a438804 100644 --- a/internal/cmd/mariadb/credentials/list/list.go +++ b/internal/cmd/mariadb/credentials/list/list.go @@ -5,6 +5,7 @@ import ( "encoding/json" "fmt" + "github.com/goccy/go-yaml" "github.com/stackitcloud/stackit-cli/internal/pkg/args" "github.com/stackitcloud/stackit-cli/internal/pkg/errors" "github.com/stackitcloud/stackit-cli/internal/pkg/examples" @@ -142,6 +143,14 @@ func outputResult(p *print.Printer, outputFormat string, credentials []mariadb.C } p.Outputln(string(details)) + return nil + case print.YAMLOutputFormat: + details, err := yaml.MarshalWithOptions(credentials, yaml.IndentSequence(true)) + if err != nil { + return fmt.Errorf("marshal MariaDB credentials list: %w", err) + } + p.Outputln(string(details)) + return nil default: table := tables.NewTable() diff --git a/internal/cmd/mariadb/instance/create/create.go b/internal/cmd/mariadb/instance/create/create.go index f92ba4351..754b50fcd 100644 --- a/internal/cmd/mariadb/instance/create/create.go +++ b/internal/cmd/mariadb/instance/create/create.go @@ -7,6 +7,7 @@ import ( "fmt" "strings" + "github.com/goccy/go-yaml" "github.com/stackitcloud/stackit-cli/internal/pkg/args" cliErr "github.com/stackitcloud/stackit-cli/internal/pkg/errors" "github.com/stackitcloud/stackit-cli/internal/pkg/examples" @@ -259,6 +260,14 @@ func outputResult(p *print.Printer, model *inputModel, projectLabel string, resp } p.Outputln(string(details)) + return nil + case print.YAMLOutputFormat: + details, err := yaml.MarshalWithOptions(resp, yaml.IndentSequence(true)) + if err != nil { + return fmt.Errorf("marshal MariaDB instance: %w", err) + } + p.Outputln(string(details)) + return nil default: operationState := "Created" diff --git a/internal/cmd/mariadb/instance/describe/describe.go b/internal/cmd/mariadb/instance/describe/describe.go index 2affb1a98..94323bc5e 100644 --- a/internal/cmd/mariadb/instance/describe/describe.go +++ b/internal/cmd/mariadb/instance/describe/describe.go @@ -5,6 +5,7 @@ import ( "encoding/json" "fmt" + "github.com/goccy/go-yaml" "github.com/stackitcloud/stackit-cli/internal/pkg/args" "github.com/stackitcloud/stackit-cli/internal/pkg/errors" "github.com/stackitcloud/stackit-cli/internal/pkg/examples" @@ -107,6 +108,14 @@ func outputResult(p *print.Printer, outputFormat string, instance *mariadb.Insta } p.Outputln(string(details)) + return nil + case print.YAMLOutputFormat: + details, err := yaml.MarshalWithOptions(instance, yaml.IndentSequence(true)) + if err != nil { + return fmt.Errorf("marshal MariaDB instance: %w", err) + } + p.Outputln(string(details)) + return nil default: table := tables.NewTable() diff --git a/internal/cmd/mariadb/instance/list/list.go b/internal/cmd/mariadb/instance/list/list.go index b5ba59500..4a3b31672 100644 --- a/internal/cmd/mariadb/instance/list/list.go +++ b/internal/cmd/mariadb/instance/list/list.go @@ -5,6 +5,7 @@ import ( "encoding/json" "fmt" + "github.com/goccy/go-yaml" "github.com/stackitcloud/stackit-cli/internal/pkg/args" "github.com/stackitcloud/stackit-cli/internal/pkg/errors" "github.com/stackitcloud/stackit-cli/internal/pkg/examples" @@ -137,6 +138,14 @@ func outputResult(p *print.Printer, outputFormat string, instances []mariadb.Ins } p.Outputln(string(details)) + return nil + case print.YAMLOutputFormat: + details, err := yaml.MarshalWithOptions(instances, yaml.IndentSequence(true)) + if err != nil { + return fmt.Errorf("marshal MariaDB instance list: %w", err) + } + p.Outputln(string(details)) + return nil default: table := tables.NewTable() diff --git a/internal/cmd/mariadb/plans/plans.go b/internal/cmd/mariadb/plans/plans.go index 57a575ca4..4fb14c3c2 100644 --- a/internal/cmd/mariadb/plans/plans.go +++ b/internal/cmd/mariadb/plans/plans.go @@ -5,6 +5,7 @@ import ( "encoding/json" "fmt" + "github.com/goccy/go-yaml" "github.com/stackitcloud/stackit-cli/internal/pkg/args" "github.com/stackitcloud/stackit-cli/internal/pkg/errors" "github.com/stackitcloud/stackit-cli/internal/pkg/examples" @@ -137,6 +138,14 @@ func outputResult(p *print.Printer, outputFormat string, plans []mariadb.Offerin } p.Outputln(string(details)) + return nil + case print.YAMLOutputFormat: + details, err := yaml.MarshalWithOptions(plans, yaml.IndentSequence(true)) + if err != nil { + return fmt.Errorf("marshal MariaDB plans: %w", err) + } + p.Outputln(string(details)) + return nil default: table := tables.NewTable() diff --git a/internal/cmd/mongodbflex/backup/backup.go b/internal/cmd/mongodbflex/backup/backup.go new file mode 100644 index 000000000..738363d78 --- /dev/null +++ b/internal/cmd/mongodbflex/backup/backup.go @@ -0,0 +1,36 @@ +package backup + +import ( + "github.com/stackitcloud/stackit-cli/internal/cmd/mongodbflex/backup/describe" + "github.com/stackitcloud/stackit-cli/internal/cmd/mongodbflex/backup/list" + "github.com/stackitcloud/stackit-cli/internal/cmd/mongodbflex/backup/restore" + restorejobs "github.com/stackitcloud/stackit-cli/internal/cmd/mongodbflex/backup/restore-jobs" + "github.com/stackitcloud/stackit-cli/internal/cmd/mongodbflex/backup/schedule" + updateschedule "github.com/stackitcloud/stackit-cli/internal/cmd/mongodbflex/backup/update-schedule" + "github.com/stackitcloud/stackit-cli/internal/pkg/args" + "github.com/stackitcloud/stackit-cli/internal/pkg/print" + "github.com/stackitcloud/stackit-cli/internal/pkg/utils" + + "github.com/spf13/cobra" +) + +func NewCmd(p *print.Printer) *cobra.Command { + cmd := &cobra.Command{ + Use: "backup", + Short: "Provides functionality for MongoDB Flex instance backups", + Long: "Provides functionality for MongoDB Flex instance backups.", + Args: args.NoArgs, + Run: utils.CmdHelp, + } + addSubcommands(cmd, p) + return cmd +} + +func addSubcommands(cmd *cobra.Command, p *print.Printer) { + cmd.AddCommand(updateschedule.NewCmd(p)) + cmd.AddCommand(schedule.NewCmd(p)) + cmd.AddCommand(restore.NewCmd(p)) + cmd.AddCommand(list.NewCmd(p)) + cmd.AddCommand(describe.NewCmd(p)) + cmd.AddCommand(restorejobs.NewCmd(p)) +} diff --git a/internal/cmd/mongodbflex/backup/describe/describe.go b/internal/cmd/mongodbflex/backup/describe/describe.go new file mode 100644 index 000000000..6488c6ee7 --- /dev/null +++ b/internal/cmd/mongodbflex/backup/describe/describe.go @@ -0,0 +1,165 @@ +package describe + +import ( + "context" + "encoding/json" + "fmt" + + "github.com/goccy/go-yaml" + "github.com/inhies/go-bytesize" + "github.com/spf13/cobra" + "github.com/stackitcloud/stackit-cli/internal/pkg/args" + "github.com/stackitcloud/stackit-cli/internal/pkg/errors" + "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" + "github.com/stackitcloud/stackit-cli/internal/pkg/services/mongodbflex/client" + "github.com/stackitcloud/stackit-cli/internal/pkg/services/mongodbflex/utils" + "github.com/stackitcloud/stackit-cli/internal/pkg/tables" + "github.com/stackitcloud/stackit-sdk-go/services/mongodbflex" +) + +const ( + backupIdArg = "BACKUP_ID" + + instanceIdFlag = "instance-id" +) + +type inputModel struct { + *globalflags.GlobalFlagModel + + InstanceId string + BackupId string +} + +func NewCmd(p *print.Printer) *cobra.Command { + cmd := &cobra.Command{ + Use: fmt.Sprintf("describe %s", backupIdArg), + Short: "Shows details of a backup for a MongoDB Flex instance", + Long: "Shows details of a backup for a MongoDB Flex instance.", + Example: examples.Build( + examples.NewExample( + `Get details of a backup with ID "xxx" for a MongoDB Flex instance with ID "yyy"`, + "$ stackit mongodbflex backup describe xxx --instance-id yyy"), + examples.NewExample( + `Get details of a backup with ID "xxx" for a MongoDB Flex instance with ID "yyy" in JSON format`, + "$ stackit mongodbflex backup describe xxx --instance-id yyy --output-format json"), + ), + Args: args.SingleArg(backupIdArg, nil), + RunE: func(cmd *cobra.Command, args []string) error { + ctx := context.Background() + model, err := parseInput(p, cmd, args) + if err != nil { + return err + } + + // Configure API client + apiClient, err := client.ConfigureClient(p) + if err != nil { + return err + } + + instanceLabel, err := utils.GetInstanceName(ctx, apiClient, model.ProjectId, model.InstanceId) + if err != nil { + p.Debug(print.ErrorLevel, "get instance name: %v", err) + instanceLabel = model.InstanceId + } + + // Call API + req := buildRequest(ctx, model, apiClient) + resp, err := req.Execute() + + if err != nil { + return fmt.Errorf("describe backup for MongoDB Flex instance: %w", err) + } + + restoreJobs, err := apiClient.ListRestoreJobs(ctx, model.ProjectId, model.InstanceId).Execute() + if err != nil { + return fmt.Errorf("get restore jobs for MongoDB Flex instance %q: %w", instanceLabel, err) + } + + restoreJobState := utils.GetRestoreStatus(model.BackupId, restoreJobs) + return outputResult(p, cmd, model.OutputFormat, restoreJobState, *resp.Item) + }, + } + configureFlags(cmd) + return cmd +} + +func configureFlags(cmd *cobra.Command) { + cmd.Flags().Var(flags.UUIDFlag(), instanceIdFlag, "Instance ID") + + err := flags.MarkFlagsRequired(cmd, instanceIdFlag) + cobra.CheckErr(err) +} + +func parseInput(p *print.Printer, cmd *cobra.Command, inputArgs []string) (*inputModel, error) { + backupId := inputArgs[0] + + globalFlags := globalflags.Parse(p, cmd) + if globalFlags.ProjectId == "" { + return nil, &errors.ProjectIdError{} + } + + model := inputModel{ + GlobalFlagModel: globalFlags, + InstanceId: flags.FlagToStringValue(p, cmd, instanceIdFlag), + BackupId: backupId, + } + + if p.IsVerbosityDebug() { + modelStr, err := print.BuildDebugStrFromInputModel(model) + if err != nil { + p.Debug(print.ErrorLevel, "convert model to string for debugging: %v", err) + } else { + p.Debug(print.DebugLevel, "parsed input values: %s", modelStr) + } + } + + return &model, nil +} + +func buildRequest(ctx context.Context, model *inputModel, apiClient *mongodbflex.APIClient) mongodbflex.ApiGetBackupRequest { + req := apiClient.GetBackup(ctx, model.ProjectId, model.InstanceId, model.BackupId) + return req +} + +func outputResult(p *print.Printer, cmd *cobra.Command, outputFormat, restoreStatus string, backup mongodbflex.Backup) error { + switch outputFormat { + case print.JSONOutputFormat: + details, err := json.MarshalIndent(backup, "", " ") + if err != nil { + return fmt.Errorf("marshal MongoDB Flex backup: %w", err) + } + cmd.Println(string(details)) + + return nil + case print.YAMLOutputFormat: + details, err := yaml.MarshalWithOptions(backup, yaml.IndentSequence(true)) + if err != nil { + return fmt.Errorf("marshal MongoDB Flex backup: %w", err) + } + p.Outputln(string(details)) + + return nil + default: + table := tables.NewTable() + table.AddRow("ID", *backup.Id) + table.AddSeparator() + table.AddRow("CREATED AT", *backup.StartTime) + table.AddSeparator() + table.AddRow("EXPIRES AT", *backup.EndTime) + table.AddSeparator() + table.AddRow("BACKUP SIZE", bytesize.New(float64(*backup.Size))) + table.AddSeparator() + table.AddRow("RESTORE STATUS", restoreStatus) + + err := table.Display(p) + if err != nil { + return fmt.Errorf("render table: %w", err) + } + + return nil + } +} diff --git a/internal/cmd/mongodbflex/backup/describe/describe_test.go b/internal/cmd/mongodbflex/backup/describe/describe_test.go new file mode 100644 index 000000000..1d6dc44e4 --- /dev/null +++ b/internal/cmd/mongodbflex/backup/describe/describe_test.go @@ -0,0 +1,239 @@ +package describe + +import ( + "context" + "testing" + + "github.com/stackitcloud/stackit-cli/internal/pkg/globalflags" + "github.com/stackitcloud/stackit-cli/internal/pkg/print" + + "github.com/google/go-cmp/cmp" + "github.com/google/go-cmp/cmp/cmpopts" + "github.com/google/uuid" + "github.com/stackitcloud/stackit-sdk-go/services/mongodbflex" +) + +var projectIdFlag = globalflags.ProjectIdFlag + +type testCtxKey struct{} + +var testCtx = context.WithValue(context.Background(), testCtxKey{}, "foo") +var testClient = &mongodbflex.APIClient{} +var testProjectId = uuid.NewString() +var testInstanceId = uuid.NewString() +var testBackupId = "backupID" + +func fixtureArgValues(mods ...func(argValues []string)) []string { + argValues := []string{ + testBackupId, + } + for _, mod := range mods { + mod(argValues) + } + return argValues +} + +func fixtureFlagValues(mods ...func(flagValues map[string]string)) map[string]string { + flagValues := map[string]string{ + projectIdFlag: testProjectId, + instanceIdFlag: testInstanceId, + } + for _, mod := range mods { + mod(flagValues) + } + return flagValues +} + +func fixtureInputModel(mods ...func(model *inputModel)) *inputModel { + model := &inputModel{ + GlobalFlagModel: &globalflags.GlobalFlagModel{ + ProjectId: testProjectId, + Verbosity: globalflags.VerbosityDefault, + }, + InstanceId: testInstanceId, + BackupId: testBackupId, + } + for _, mod := range mods { + mod(model) + } + return model +} + +func fixtureRequest(mods ...func(request *mongodbflex.ApiGetBackupRequest)) mongodbflex.ApiGetBackupRequest { + request := testClient.GetBackup(testCtx, testProjectId, testInstanceId, testBackupId) + for _, mod := range mods { + mod(&request) + } + return request +} + +func TestParseInput(t *testing.T) { + tests := []struct { + description string + argValues []string + flagValues map[string]string + isValid bool + expectedModel *inputModel + }{ + { + description: "base", + argValues: fixtureArgValues(), + flagValues: fixtureFlagValues(), + isValid: true, + expectedModel: fixtureInputModel(), + }, + { + description: "no values", + argValues: []string{}, + flagValues: map[string]string{}, + isValid: false, + }, + { + description: "no arg values", + argValues: []string{}, + flagValues: fixtureFlagValues(), + isValid: false, + }, + { + description: "no flag values", + argValues: fixtureArgValues(), + flagValues: map[string]string{}, + isValid: false, + }, + { + description: "project id missing", + argValues: fixtureArgValues(), + flagValues: fixtureFlagValues(func(flagValues map[string]string) { + delete(flagValues, projectIdFlag) + }), + isValid: false, + }, + { + description: "project id invalid 1", + argValues: fixtureArgValues(), + flagValues: fixtureFlagValues(func(flagValues map[string]string) { + flagValues[projectIdFlag] = "" + }), + isValid: false, + }, + { + description: "project id invalid 2", + argValues: fixtureArgValues(), + flagValues: fixtureFlagValues(func(flagValues map[string]string) { + flagValues[projectIdFlag] = "invalid-uuid" + }), + isValid: false, + }, + { + description: "instance id missing", + argValues: fixtureArgValues(), + flagValues: fixtureFlagValues(func(flagValues map[string]string) { + delete(flagValues, instanceIdFlag) + }), + isValid: false, + }, + { + description: "instance id invalid 1", + argValues: fixtureArgValues(), + flagValues: fixtureFlagValues(func(flagValues map[string]string) { + flagValues[instanceIdFlag] = "" + }), + isValid: false, + }, + { + description: "instance id invalid 2", + argValues: fixtureArgValues(), + flagValues: fixtureFlagValues(func(flagValues map[string]string) { + flagValues[instanceIdFlag] = "invalid-uuid" + }), + isValid: false, + }, + { + description: "backup id invalid 1", + argValues: []string{""}, + flagValues: fixtureFlagValues(), + isValid: false, + }, + } + + for _, tt := range tests { + t.Run(tt.description, func(t *testing.T) { + p := print.NewPrinter() + cmd := NewCmd(p) + err := globalflags.Configure(cmd.Flags()) + if err != nil { + t.Fatalf("configure global flags: %v", err) + } + + for flag, value := range tt.flagValues { + err := cmd.Flags().Set(flag, value) + if err != nil { + if !tt.isValid { + return + } + t.Fatalf("setting flag --%s=%s: %v", flag, value, err) + } + } + + err = cmd.ValidateArgs(tt.argValues) + if err != nil { + if !tt.isValid { + return + } + t.Fatalf("error validating args: %v", err) + } + + err = cmd.ValidateRequiredFlags() + if err != nil { + if !tt.isValid { + return + } + t.Fatalf("error validating flags: %v", err) + } + + model, err := parseInput(p, cmd, tt.argValues) + if err != nil { + if !tt.isValid { + return + } + t.Fatalf("error parsing input: %v", err) + } + + if !tt.isValid { + t.Fatalf("did not fail on invalid input") + } + diff := cmp.Diff(model, tt.expectedModel) + if diff != "" { + t.Fatalf("Data does not match: %s", diff) + } + }) + } +} + +func TestBuildRequest(t *testing.T) { + tests := []struct { + description string + model *inputModel + expectedRequest mongodbflex.ApiGetBackupRequest + }{ + { + description: "base", + model: fixtureInputModel(), + expectedRequest: fixtureRequest(), + }, + } + + for _, tt := range tests { + t.Run(tt.description, func(t *testing.T) { + request := buildRequest(testCtx, tt.model, testClient) + + diff := cmp.Diff(request, tt.expectedRequest, + cmp.AllowUnexported(tt.expectedRequest), + cmpopts.EquateComparable(testCtx), + ) + if diff != "" { + t.Fatalf("Data does not match: %s", diff) + } + }) + } +} diff --git a/internal/cmd/mongodbflex/backup/list/list.go b/internal/cmd/mongodbflex/backup/list/list.go new file mode 100644 index 000000000..f1a26ce75 --- /dev/null +++ b/internal/cmd/mongodbflex/backup/list/list.go @@ -0,0 +1,180 @@ +package list + +import ( + "context" + "encoding/json" + "fmt" + + "github.com/goccy/go-yaml" + "github.com/inhies/go-bytesize" + "github.com/stackitcloud/stackit-cli/internal/pkg/args" + "github.com/stackitcloud/stackit-cli/internal/pkg/errors" + "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" + "github.com/stackitcloud/stackit-cli/internal/pkg/services/mongodbflex/client" + mongodbflexUtils "github.com/stackitcloud/stackit-cli/internal/pkg/services/mongodbflex/utils" + "github.com/stackitcloud/stackit-cli/internal/pkg/tables" + + "github.com/spf13/cobra" + "github.com/stackitcloud/stackit-sdk-go/services/mongodbflex" +) + +const ( + instanceIdFlag = "instance-id" + limitFlag = "limit" +) + +type inputModel struct { + *globalflags.GlobalFlagModel + + InstanceId *string + Limit *int64 +} + +func NewCmd(p *print.Printer) *cobra.Command { + cmd := &cobra.Command{ + Use: "list", + Short: "Lists all backups which are available for a MongoDB Flex instance", + Long: "Lists all backups which are available for a MongoDB Flex instance.", + Example: examples.Build( + examples.NewExample( + `List all backups of instance with ID "xxx"`, + "$ stackit mongodbflex backup list --instance-id xxx"), + examples.NewExample( + `List all backups of instance with ID "xxx" in JSON format`, + "$ stackit mongodbflex backup list --instance-id xxx --output-format json"), + examples.NewExample( + `List up to 10 backups of instance with ID "xxx"`, + "$ stackit mongodbflex backup list --instance-id xxx --limit 10"), + ), + Args: args.NoArgs, + RunE: func(cmd *cobra.Command, args []string) error { + ctx := context.Background() + model, err := parseInput(p, cmd) + if err != nil { + return err + } + + // Configure API client + apiClient, err := client.ConfigureClient(p) + if err != nil { + return err + } + + instanceLabel, err := mongodbflexUtils.GetInstanceName(ctx, apiClient, model.ProjectId, *model.InstanceId) + if err != nil { + p.Debug(print.ErrorLevel, "get instance name: %v", err) + instanceLabel = *model.InstanceId + } + + // Call API + req := buildRequest(ctx, model, apiClient) + resp, err := req.Execute() + if err != nil { + return fmt.Errorf("get backups for MongoDB Flex instance %q: %w", instanceLabel, err) + } + if resp.Items == nil || len(*resp.Items) == 0 { + cmd.Printf("No backups found for instance %q\n", instanceLabel) + return nil + } + backups := *resp.Items + + restoreJobs, err := apiClient.ListRestoreJobs(ctx, model.ProjectId, *model.InstanceId).Execute() + if err != nil { + return fmt.Errorf("get restore jobs for MongoDB Flex instance %q: %w", instanceLabel, err) + } + + // Truncate output + if model.Limit != nil && len(backups) > int(*model.Limit) { + backups = backups[:*model.Limit] + } + + return outputResult(p, model.OutputFormat, backups, restoreJobs) + }, + } + + configureFlags(cmd) + return cmd +} + +func configureFlags(cmd *cobra.Command) { + cmd.Flags().Var(flags.UUIDFlag(), instanceIdFlag, "Instance ID") + cmd.Flags().Int64(limitFlag, 0, "Maximum number of entries to list") + + err := flags.MarkFlagsRequired(cmd, instanceIdFlag) + cobra.CheckErr(err) +} + +func parseInput(p *print.Printer, cmd *cobra.Command) (*inputModel, error) { + globalFlags := globalflags.Parse(p, cmd) + if globalFlags.ProjectId == "" { + 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, + InstanceId: flags.FlagToStringPointer(p, cmd, instanceIdFlag), + Limit: flags.FlagToInt64Pointer(p, cmd, limitFlag), + } + + if p.IsVerbosityDebug() { + modelStr, err := print.BuildDebugStrFromInputModel(model) + if err != nil { + p.Debug(print.ErrorLevel, "convert model to string for debugging: %v", err) + } else { + p.Debug(print.DebugLevel, "parsed input values: %s", modelStr) + } + } + + return &model, nil +} + +func buildRequest(ctx context.Context, model *inputModel, apiClient *mongodbflex.APIClient) mongodbflex.ApiListBackupsRequest { + req := apiClient.ListBackups(ctx, model.ProjectId, *model.InstanceId) + return req +} + +func outputResult(p *print.Printer, outputFormat string, backups []mongodbflex.Backup, restoreJobs *mongodbflex.ListRestoreJobsResponse) error { + switch outputFormat { + case print.JSONOutputFormat: + details, err := json.MarshalIndent(backups, "", " ") + if err != nil { + return fmt.Errorf("marshal MongoDB Flex backups list: %w", err) + } + p.Outputln(string(details)) + + return nil + case print.YAMLOutputFormat: + details, err := yaml.MarshalWithOptions(backups, yaml.IndentSequence(true)) + if err != nil { + return fmt.Errorf("marshal MongoDB Flex backups list: %w", err) + } + p.Outputln(string(details)) + + return nil + default: + table := tables.NewTable() + table.SetHeader("ID", "CREATED AT", "EXPIRES AT", "BACKUP SIZE", "RESTORE STATUS") + for i := range backups { + backup := backups[i] + restoreStatus := mongodbflexUtils.GetRestoreStatus(*backup.Id, restoreJobs) + table.AddRow(*backup.Id, *backup.StartTime, *backup.EndTime, bytesize.New(float64(*backup.Size)), restoreStatus) + } + err := table.Display(p) + if err != nil { + return fmt.Errorf("render table: %w", err) + } + + return nil + } +} diff --git a/internal/cmd/mongodbflex/backup/list/list_test.go b/internal/cmd/mongodbflex/backup/list/list_test.go new file mode 100644 index 000000000..82de1efca --- /dev/null +++ b/internal/cmd/mongodbflex/backup/list/list_test.go @@ -0,0 +1,209 @@ +package list + +import ( + "context" + "testing" + + "github.com/stackitcloud/stackit-cli/internal/pkg/globalflags" + "github.com/stackitcloud/stackit-cli/internal/pkg/print" + "github.com/stackitcloud/stackit-cli/internal/pkg/utils" + + "github.com/google/go-cmp/cmp" + "github.com/google/go-cmp/cmp/cmpopts" + "github.com/google/uuid" + "github.com/stackitcloud/stackit-sdk-go/services/mongodbflex" +) + +var projectIdFlag = globalflags.ProjectIdFlag + +type testCtxKey struct{} + +var testCtx = context.WithValue(context.Background(), testCtxKey{}, "foo") +var testClient = &mongodbflex.APIClient{} +var testProjectId = uuid.NewString() +var testInstanceId = uuid.NewString() + +func fixtureFlagValues(mods ...func(flagValues map[string]string)) map[string]string { + flagValues := map[string]string{ + projectIdFlag: testProjectId, + instanceIdFlag: testInstanceId, + limitFlag: "10", + } + for _, mod := range mods { + mod(flagValues) + } + return flagValues +} + +func fixtureInputModel(mods ...func(model *inputModel)) *inputModel { + model := &inputModel{ + GlobalFlagModel: &globalflags.GlobalFlagModel{ + ProjectId: testProjectId, + Verbosity: globalflags.VerbosityDefault, + }, + InstanceId: utils.Ptr(testInstanceId), + Limit: utils.Ptr(int64(10)), + } + for _, mod := range mods { + mod(model) + } + return model +} + +func fixtureRequest(mods ...func(request *mongodbflex.ApiListBackupsRequest)) mongodbflex.ApiListBackupsRequest { + request := testClient.ListBackups(testCtx, testProjectId, testInstanceId) + for _, mod := range mods { + mod(&request) + } + return request +} + +func TestParseInput(t *testing.T) { + tests := []struct { + description 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: false, + }, + { + description: "project id missing", + flagValues: fixtureFlagValues(func(flagValues map[string]string) { + delete(flagValues, projectIdFlag) + }), + isValid: false, + }, + { + description: "project id invalid 1", + flagValues: fixtureFlagValues(func(flagValues map[string]string) { + flagValues[projectIdFlag] = "" + }), + isValid: false, + }, + { + description: "project id invalid 2", + flagValues: fixtureFlagValues(func(flagValues map[string]string) { + flagValues[projectIdFlag] = "invalid-uuid" + }), + isValid: false, + }, + { + description: "instance id missing", + flagValues: fixtureFlagValues(func(flagValues map[string]string) { + delete(flagValues, instanceIdFlag) + }), + isValid: false, + }, + { + description: "instance id invalid 1", + flagValues: fixtureFlagValues(func(flagValues map[string]string) { + flagValues[instanceIdFlag] = "" + }), + isValid: false, + }, + { + description: "instance id invalid 2", + flagValues: fixtureFlagValues(func(flagValues map[string]string) { + flagValues[instanceIdFlag] = "invalid-uuid" + }), + isValid: false, + }, + { + 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 { + t.Run(tt.description, func(t *testing.T) { + p := print.NewPrinter() + cmd := NewCmd(p) + err := globalflags.Configure(cmd.Flags()) + if err != nil { + t.Fatalf("configure global flags: %v", err) + } + + for flag, value := range tt.flagValues { + err := cmd.Flags().Set(flag, value) + if err != nil { + if !tt.isValid { + return + } + t.Fatalf("setting flag --%s=%s: %v", flag, value, err) + } + } + + err = cmd.ValidateRequiredFlags() + if err != nil { + if !tt.isValid { + return + } + t.Fatalf("error validating flags: %v", err) + } + + model, err := parseInput(p, cmd) + if err != nil { + if !tt.isValid { + return + } + t.Fatalf("error parsing input: %v", err) + } + + if !tt.isValid { + t.Fatalf("did not fail on invalid input") + } + diff := cmp.Diff(model, tt.expectedModel) + if diff != "" { + t.Fatalf("Data does not match: %s", diff) + } + }) + } +} + +func TestBuildRequest(t *testing.T) { + tests := []struct { + description string + model *inputModel + expectedRequest mongodbflex.ApiListBackupsRequest + }{ + { + description: "base", + model: fixtureInputModel(), + expectedRequest: fixtureRequest(), + }, + } + + for _, tt := range tests { + t.Run(tt.description, func(t *testing.T) { + request := buildRequest(testCtx, tt.model, testClient) + + diff := cmp.Diff(request, tt.expectedRequest, + cmp.AllowUnexported(tt.expectedRequest), + cmpopts.EquateComparable(testCtx), + ) + if diff != "" { + t.Fatalf("Data does not match: %s", diff) + } + }) + } +} diff --git a/internal/cmd/mongodbflex/backup/restore-jobs/restore_jobs.go b/internal/cmd/mongodbflex/backup/restore-jobs/restore_jobs.go new file mode 100644 index 000000000..5d5d0cad8 --- /dev/null +++ b/internal/cmd/mongodbflex/backup/restore-jobs/restore_jobs.go @@ -0,0 +1,174 @@ +package restorejobs + +import ( + "context" + "encoding/json" + "fmt" + + "github.com/goccy/go-yaml" + "github.com/stackitcloud/stackit-cli/internal/pkg/args" + "github.com/stackitcloud/stackit-cli/internal/pkg/errors" + "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" + "github.com/stackitcloud/stackit-cli/internal/pkg/services/mongodbflex/client" + mongodbflexUtils "github.com/stackitcloud/stackit-cli/internal/pkg/services/mongodbflex/utils" + "github.com/stackitcloud/stackit-cli/internal/pkg/tables" + + "github.com/spf13/cobra" + "github.com/stackitcloud/stackit-sdk-go/services/mongodbflex" +) + +const ( + instanceIdFlag = "instance-id" + limitFlag = "limit" +) + +type inputModel struct { + *globalflags.GlobalFlagModel + + InstanceId *string + Limit *int64 +} + +func NewCmd(p *print.Printer) *cobra.Command { + cmd := &cobra.Command{ + Use: "restore-jobs", + Short: "Lists all restore jobs which have been run for a MongoDB Flex instance", + Long: "Lists all restore jobs which have been run for a MongoDB Flex instance.", + Example: examples.Build( + examples.NewExample( + `List all restore jobs of instance with ID "xxx"`, + "$ stackit mongodbflex backup restore-jobs --instance-id xxx"), + examples.NewExample( + `List all restore jobs of instance with ID "xxx" in JSON format`, + "$ stackit mongodbflex backup restore-jobs --instance-id xxx --output-format json"), + examples.NewExample( + `List up to 10 restore jobs of instance with ID "xxx"`, + "$ stackit mongodbflex backup restore-jobs --instance-id xxx --limit 10"), + ), + Args: args.NoArgs, + RunE: func(cmd *cobra.Command, args []string) error { + ctx := context.Background() + model, err := parseInput(p, cmd) + if err != nil { + return err + } + + // Configure API client + apiClient, err := client.ConfigureClient(p) + if err != nil { + return err + } + + instanceLabel, err := mongodbflexUtils.GetInstanceName(ctx, apiClient, model.ProjectId, *model.InstanceId) + if err != nil { + p.Debug(print.ErrorLevel, "get instance name: %v", err) + instanceLabel = *model.InstanceId + } + + // Call API + req := buildRequest(ctx, model, apiClient) + resp, err := req.Execute() + if err != nil { + return fmt.Errorf("get restore jobs for MongoDB Flex instance %q: %w", instanceLabel, err) + } + if resp.Items == nil || len(*resp.Items) == 0 { + cmd.Printf("No restore jobs found for instance %q\n", instanceLabel) + return nil + } + restoreJobs := *resp.Items + + // Truncate output + if model.Limit != nil && len(restoreJobs) > int(*model.Limit) { + restoreJobs = restoreJobs[:*model.Limit] + } + + return outputResult(p, model.OutputFormat, restoreJobs) + }, + } + + configureFlags(cmd) + return cmd +} + +func configureFlags(cmd *cobra.Command) { + cmd.Flags().Var(flags.UUIDFlag(), instanceIdFlag, "Instance ID") + cmd.Flags().Int64(limitFlag, 0, "Maximum number of entries to list") + + err := flags.MarkFlagsRequired(cmd, instanceIdFlag) + cobra.CheckErr(err) +} + +func parseInput(p *print.Printer, cmd *cobra.Command) (*inputModel, error) { + globalFlags := globalflags.Parse(p, cmd) + if globalFlags.ProjectId == "" { + 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, + InstanceId: flags.FlagToStringPointer(p, cmd, instanceIdFlag), + Limit: flags.FlagToInt64Pointer(p, cmd, limitFlag), + } + + if p.IsVerbosityDebug() { + modelStr, err := print.BuildDebugStrFromInputModel(model) + if err != nil { + p.Debug(print.ErrorLevel, "convert model to string for debugging: %v", err) + } else { + p.Debug(print.DebugLevel, "parsed input values: %s", modelStr) + } + } + + return &model, nil +} + +func buildRequest(ctx context.Context, model *inputModel, apiClient *mongodbflex.APIClient) mongodbflex.ApiListRestoreJobsRequest { + req := apiClient.ListRestoreJobs(ctx, model.ProjectId, *model.InstanceId) + return req +} + +func outputResult(p *print.Printer, outputFormat string, restoreJobs []mongodbflex.RestoreInstanceStatus) error { + switch outputFormat { + case print.JSONOutputFormat: + details, err := json.MarshalIndent(restoreJobs, "", " ") + if err != nil { + return fmt.Errorf("marshal MongoDB Flex restore jobs list: %w", err) + } + p.Outputln(string(details)) + + return nil + case print.YAMLOutputFormat: + details, err := yaml.MarshalWithOptions(restoreJobs, yaml.IndentSequence(true)) + if err != nil { + return fmt.Errorf("marshal MongoDB Flex restore jobs list: %w", err) + } + p.Outputln(string(details)) + + return nil + default: + table := tables.NewTable() + table.SetHeader("ID", "BACKUP ID", "BACKUP INSTANCE ID", "DATE", "STATUS") + for i := range restoreJobs { + restoreJob := restoreJobs[i] + + table.AddRow(*restoreJob.Id, *restoreJob.BackupID, *restoreJob.InstanceId, *restoreJob.Date, *restoreJob.Status) + } + err := table.Display(p) + if err != nil { + return fmt.Errorf("render table: %w", err) + } + + return nil + } +} diff --git a/internal/cmd/mongodbflex/backup/restore-jobs/restore_jobs_test.go b/internal/cmd/mongodbflex/backup/restore-jobs/restore_jobs_test.go new file mode 100644 index 000000000..c7c5ec84a --- /dev/null +++ b/internal/cmd/mongodbflex/backup/restore-jobs/restore_jobs_test.go @@ -0,0 +1,209 @@ +package restorejobs + +import ( + "context" + "testing" + + "github.com/stackitcloud/stackit-cli/internal/pkg/globalflags" + "github.com/stackitcloud/stackit-cli/internal/pkg/print" + "github.com/stackitcloud/stackit-cli/internal/pkg/utils" + + "github.com/google/go-cmp/cmp" + "github.com/google/go-cmp/cmp/cmpopts" + "github.com/google/uuid" + "github.com/stackitcloud/stackit-sdk-go/services/mongodbflex" +) + +var projectIdFlag = globalflags.ProjectIdFlag + +type testCtxKey struct{} + +var testCtx = context.WithValue(context.Background(), testCtxKey{}, "foo") +var testClient = &mongodbflex.APIClient{} +var testProjectId = uuid.NewString() +var testInstanceId = uuid.NewString() + +func fixtureFlagValues(mods ...func(flagValues map[string]string)) map[string]string { + flagValues := map[string]string{ + projectIdFlag: testProjectId, + instanceIdFlag: testInstanceId, + limitFlag: "10", + } + for _, mod := range mods { + mod(flagValues) + } + return flagValues +} + +func fixtureInputModel(mods ...func(model *inputModel)) *inputModel { + model := &inputModel{ + GlobalFlagModel: &globalflags.GlobalFlagModel{ + ProjectId: testProjectId, + Verbosity: globalflags.VerbosityDefault, + }, + InstanceId: utils.Ptr(testInstanceId), + Limit: utils.Ptr(int64(10)), + } + for _, mod := range mods { + mod(model) + } + return model +} + +func fixtureRequest(mods ...func(request *mongodbflex.ApiListRestoreJobsRequest)) mongodbflex.ApiListRestoreJobsRequest { + request := testClient.ListRestoreJobs(testCtx, testProjectId, testInstanceId) + for _, mod := range mods { + mod(&request) + } + return request +} + +func TestParseInput(t *testing.T) { + tests := []struct { + description 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: false, + }, + { + description: "project id missing", + flagValues: fixtureFlagValues(func(flagValues map[string]string) { + delete(flagValues, projectIdFlag) + }), + isValid: false, + }, + { + description: "project id invalid 1", + flagValues: fixtureFlagValues(func(flagValues map[string]string) { + flagValues[projectIdFlag] = "" + }), + isValid: false, + }, + { + description: "project id invalid 2", + flagValues: fixtureFlagValues(func(flagValues map[string]string) { + flagValues[projectIdFlag] = "invalid-uuid" + }), + isValid: false, + }, + { + description: "instance id missing", + flagValues: fixtureFlagValues(func(flagValues map[string]string) { + delete(flagValues, instanceIdFlag) + }), + isValid: false, + }, + { + description: "instance id invalid 1", + flagValues: fixtureFlagValues(func(flagValues map[string]string) { + flagValues[instanceIdFlag] = "" + }), + isValid: false, + }, + { + description: "instance id invalid 2", + flagValues: fixtureFlagValues(func(flagValues map[string]string) { + flagValues[instanceIdFlag] = "invalid-uuid" + }), + isValid: false, + }, + { + 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 { + t.Run(tt.description, func(t *testing.T) { + p := print.NewPrinter() + cmd := NewCmd(p) + err := globalflags.Configure(cmd.Flags()) + if err != nil { + t.Fatalf("configure global flags: %v", err) + } + + for flag, value := range tt.flagValues { + err := cmd.Flags().Set(flag, value) + if err != nil { + if !tt.isValid { + return + } + t.Fatalf("setting flag --%s=%s: %v", flag, value, err) + } + } + + err = cmd.ValidateRequiredFlags() + if err != nil { + if !tt.isValid { + return + } + t.Fatalf("error validating flags: %v", err) + } + + model, err := parseInput(p, cmd) + if err != nil { + if !tt.isValid { + return + } + t.Fatalf("error parsing input: %v", err) + } + + if !tt.isValid { + t.Fatalf("did not fail on invalid input") + } + diff := cmp.Diff(model, tt.expectedModel) + if diff != "" { + t.Fatalf("Data does not match: %s", diff) + } + }) + } +} + +func TestBuildRequest(t *testing.T) { + tests := []struct { + description string + model *inputModel + expectedRequest mongodbflex.ApiListRestoreJobsRequest + }{ + { + description: "base", + model: fixtureInputModel(), + expectedRequest: fixtureRequest(), + }, + } + + for _, tt := range tests { + t.Run(tt.description, func(t *testing.T) { + request := buildRequest(testCtx, tt.model, testClient) + + diff := cmp.Diff(request, tt.expectedRequest, + cmp.AllowUnexported(tt.expectedRequest), + cmpopts.EquateComparable(testCtx), + ) + if diff != "" { + t.Fatalf("Data does not match: %s", diff) + } + }) + } +} diff --git a/internal/cmd/mongodbflex/backup/restore/restore.go b/internal/cmd/mongodbflex/backup/restore/restore.go new file mode 100644 index 000000000..c043e4292 --- /dev/null +++ b/internal/cmd/mongodbflex/backup/restore/restore.go @@ -0,0 +1,205 @@ +package restore + +import ( + "context" + "fmt" + + "github.com/stackitcloud/stackit-cli/internal/pkg/args" + cliErr "github.com/stackitcloud/stackit-cli/internal/pkg/errors" + "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" + "github.com/stackitcloud/stackit-cli/internal/pkg/services/mongodbflex/client" + mongodbUtils "github.com/stackitcloud/stackit-cli/internal/pkg/services/mongodbflex/utils" + "github.com/stackitcloud/stackit-cli/internal/pkg/spinner" + + "github.com/spf13/cobra" + "github.com/stackitcloud/stackit-sdk-go/services/mongodbflex" + "github.com/stackitcloud/stackit-sdk-go/services/mongodbflex/wait" +) + +const ( + instanceIdFlag = "instance-id" + backupInstanceIdFlag = "backup-instance-id" + backupIdFlag = "backup-id" + timestampFlag = "timestamp" +) + +type inputModel struct { + *globalflags.GlobalFlagModel + InstanceId string + BackupInstanceId string + BackupId string + Timestamp string +} + +func NewCmd(p *print.Printer) *cobra.Command { + cmd := &cobra.Command{ + Use: "restore", + Short: "Restores a MongoDB Flex instance from a backup", + Long: fmt.Sprintf("%s\n%s\n%s", + "Restores a MongoDB Flex instance from a backup of an instance or clones a MongoDB Flex instance from a point-in-time snapshot.", + "The backup is specified by a backup ID and the point-in-time snapshot is specified by a timestamp.", + "You can specify the instance to which the backup will be applied. If not specified, the backup will be applied to the same instance from which it was taken.", + ), + Args: args.NoArgs, + Example: examples.Build( + examples.NewExample( + `Restore a MongoDB Flex instance with ID "yyy" using backup with ID "zzz"`, + `$ stackit mongodbflex backup restore --instance-id yyy --backup-id zzz`), + examples.NewExample( + `Clone a MongoDB Flex instance with ID "yyy" via point-in-time restore to timestamp "2024-05-14T14:31:48Z"`, + `$ stackit mongodbflex backup restore --instance-id yyy --timestamp 2024-05-14T14:31:48Z`), + examples.NewExample( + `Restore a MongoDB Flex instance with ID "yyy", using backup from instance with ID "zzz" with backup ID "xxx"`, + `$ stackit mongodbflex backup restore --instance-id zzz --backup-instance-id yyy --backup-id xxx`), + ), + RunE: func(cmd *cobra.Command, args []string) error { + ctx := context.Background() + + model, err := parseInput(p, cmd) + if err != nil { + return err + } + + // Configure API client + apiClient, err := client.ConfigureClient(p) + if err != nil { + return err + } + + instanceLabel, err := mongodbUtils.GetInstanceName(ctx, apiClient, model.ProjectId, model.InstanceId) + if err != nil { + p.Debug(print.ErrorLevel, "get instance name: %v", err) + instanceLabel = model.ProjectId + } + + if !model.AssumeYes { + prompt := fmt.Sprintf("Are you sure you want to restore MongoDB Flex instance %q?", instanceLabel) + err = p.PromptForConfirmation(prompt) + if err != nil { + return err + } + } + + // If backupInstanceId is not provided, the target is the same instance as the backup + if model.BackupInstanceId == "" { + model.BackupInstanceId = model.InstanceId + } + + isRestoreOperation := getIsRestoreOperation(model.BackupId, model.Timestamp) + + // If backupId is provided, restore the instance from the backup with the backupId + if isRestoreOperation { + req := buildRestoreRequest(ctx, model, apiClient) + _, err = req.Execute() + if err != nil { + return fmt.Errorf("restore MongoDB Flex instance: %w", err) + } + + if !model.Async { + s := spinner.New(p) + s.Start("Restoring instance") + _, err = wait.RestoreInstanceWaitHandler(ctx, apiClient, model.ProjectId, model.InstanceId, model.BackupId).WaitWithContext(ctx) + if err != nil { + return fmt.Errorf("wait for MongoDB Flex instance restoration: %w", err) + } + s.Stop() + } + + p.Outputf("Restored instance %q with backup %q\n", model.InstanceId, model.BackupId) + return nil + } + + // Else, if timestamp is provided, clone the instance from a point-in-time snapshot + req := buildCloneRequest(ctx, model, apiClient) + _, err = req.Execute() + if err != nil { + return fmt.Errorf("clone MongoDB Flex instance: %w", err) + } + + if !model.Async { + s := spinner.New(p) + s.Start("Cloning instance") + _, err = wait.CloneInstanceWaitHandler(ctx, apiClient, model.ProjectId, model.InstanceId).WaitWithContext(ctx) + if err != nil { + return fmt.Errorf("wait for MongoDB Flex instance cloning: %w", err) + } + s.Stop() + } + + p.Outputf("Cloned instance %q from backup with timestamp %q\n", model.InstanceId, model.Timestamp) + return nil + }, + } + configureFlags(cmd) + return cmd +} + +func configureFlags(cmd *cobra.Command) { + cmd.Flags().Var(flags.UUIDFlag(), instanceIdFlag, "Instance ID") + cmd.Flags().Var(flags.UUIDFlag(), backupInstanceIdFlag, "Instance ID of the target instance to restore the backup to") + cmd.Flags().String(backupIdFlag, "", "Backup ID") + cmd.Flags().String(timestampFlag, "", "Timestamp of the snapshot to use as a source for cloning the instance in a date-time with the RFC3339 layout format, e.g. 2024-01-01T00:00:00Z") + + err := flags.MarkFlagsRequired(cmd, instanceIdFlag) + cobra.CheckErr(err) +} + +func parseInput(p *print.Printer, cmd *cobra.Command) (*inputModel, error) { + globalFlags := globalflags.Parse(p, cmd) + if globalFlags.ProjectId == "" { + return nil, &cliErr.ProjectIdError{} + } + + backupId := flags.FlagToStringValue(p, cmd, backupIdFlag) + timestamp := flags.FlagToStringValue(p, cmd, timestampFlag) + + if backupId != "" && timestamp != "" || backupId == "" && timestamp == "" { + return nil, &cliErr.RequiredMutuallyExclusiveFlagsError{ + Flags: []string{backupIdFlag, timestampFlag}, + } + } + + model := inputModel{ + GlobalFlagModel: globalFlags, + InstanceId: flags.FlagToStringValue(p, cmd, instanceIdFlag), + BackupInstanceId: flags.FlagToStringValue(p, cmd, backupInstanceIdFlag), + BackupId: flags.FlagToStringValue(p, cmd, backupIdFlag), + Timestamp: flags.FlagToStringValue(p, cmd, timestampFlag), + } + + if p.IsVerbosityDebug() { + modelStr, err := print.BuildDebugStrFromInputModel(model) + if err != nil { + p.Debug(print.ErrorLevel, "convert model to string for debugging: %v", err) + } else { + p.Debug(print.DebugLevel, "parsed input values: %s", modelStr) + } + } + + return &model, nil +} + +func buildRestoreRequest(ctx context.Context, model *inputModel, apiClient *mongodbflex.APIClient) mongodbflex.ApiRestoreInstanceRequest { + req := apiClient.RestoreInstance(ctx, model.ProjectId, model.InstanceId) + req = req.RestoreInstancePayload(mongodbflex.RestoreInstancePayload{ + BackupId: &model.BackupId, + InstanceId: &model.BackupInstanceId, + }) + return req +} + +func buildCloneRequest(ctx context.Context, model *inputModel, apiClient *mongodbflex.APIClient) mongodbflex.ApiCloneInstanceRequest { + req := apiClient.CloneInstance(ctx, model.ProjectId, model.InstanceId) + req = req.CloneInstancePayload(mongodbflex.CloneInstancePayload{ + Timestamp: &model.Timestamp, + InstanceId: &model.BackupInstanceId, + }) + return req +} + +func getIsRestoreOperation(backupId, timestamp string) bool { + return backupId != "" && timestamp == "" +} diff --git a/internal/cmd/mongodbflex/backup/restore/restore_test.go b/internal/cmd/mongodbflex/backup/restore/restore_test.go new file mode 100644 index 000000000..63e06d9af --- /dev/null +++ b/internal/cmd/mongodbflex/backup/restore/restore_test.go @@ -0,0 +1,310 @@ +package restore + +import ( + "context" + "testing" + + "github.com/stackitcloud/stackit-cli/internal/pkg/globalflags" + "github.com/stackitcloud/stackit-cli/internal/pkg/print" + "github.com/stackitcloud/stackit-cli/internal/pkg/utils" + + "github.com/google/go-cmp/cmp" + "github.com/google/go-cmp/cmp/cmpopts" + "github.com/google/uuid" + "github.com/stackitcloud/stackit-sdk-go/services/mongodbflex" +) + +var projectIdFlag = globalflags.ProjectIdFlag + +type testCtxKey struct{} + +const ( + testBackupId = "backupID" + testTimestamp = "2021-01-01T00:00:00Z" +) + +var testCtx = context.WithValue(context.Background(), testCtxKey{}, "foo") +var testClient = &mongodbflex.APIClient{} + +var testProjectId = uuid.NewString() +var testInstanceId = uuid.NewString() +var testBackupInstanceId = uuid.NewString() + +func fixtureFlagValues(mods ...func(flagValues map[string]string)) map[string]string { + flagValues := map[string]string{ + projectIdFlag: testProjectId, + backupIdFlag: testBackupId, + backupInstanceIdFlag: testBackupInstanceId, + instanceIdFlag: testInstanceId, + } + for _, mod := range mods { + mod(flagValues) + } + return flagValues +} + +func fixtureInputModel(mods ...func(model *inputModel)) *inputModel { + model := &inputModel{ + GlobalFlagModel: &globalflags.GlobalFlagModel{ + ProjectId: testProjectId, + Verbosity: globalflags.VerbosityDefault, + }, + InstanceId: testInstanceId, + BackupId: testBackupId, + BackupInstanceId: testBackupInstanceId, + } + for _, mod := range mods { + mod(model) + } + return model +} + +func fixtureRestoreRequest(mods ...func(request mongodbflex.ApiRestoreInstanceRequest)) mongodbflex.ApiRestoreInstanceRequest { + request := testClient.RestoreInstance(testCtx, testProjectId, testInstanceId) + request = request.RestoreInstancePayload(mongodbflex.RestoreInstancePayload{ + BackupId: utils.Ptr(testBackupId), + InstanceId: utils.Ptr(testBackupInstanceId), + }) + for _, mod := range mods { + mod(request) + } + return request +} + +func fixtureCloneRequest(mods ...func(request mongodbflex.ApiCloneInstanceRequest)) mongodbflex.ApiCloneInstanceRequest { + request := testClient.CloneInstance(testCtx, testProjectId, testInstanceId) + request = request.CloneInstancePayload(mongodbflex.CloneInstancePayload{ + Timestamp: utils.Ptr(testTimestamp), + InstanceId: utils.Ptr(testBackupInstanceId), + }) + for _, mod := range mods { + mod(request) + } + return request +} + +func TestParseInput(t *testing.T) { + tests := []struct { + description string + flagValues map[string]string + aclValues []string + isValid bool + expectedModel *inputModel + }{ + { + description: "no values", + flagValues: map[string]string{}, + isValid: false, + }, + { + description: "base", + flagValues: fixtureFlagValues(), + isValid: true, + expectedModel: fixtureInputModel(), + }, + { + description: "project id missing", + flagValues: fixtureFlagValues(func(flagValues map[string]string) { + delete(flagValues, projectIdFlag) + }), + isValid: false, + }, + { + description: "project id invalid 1", + flagValues: fixtureFlagValues(func(flagValues map[string]string) { + flagValues[projectIdFlag] = "" + }), + isValid: false, + }, + { + description: "project id invalid 2", + flagValues: fixtureFlagValues(func(flagValues map[string]string) { + flagValues[projectIdFlag] = "invalid-uuid" + }), + isValid: false, + }, + { + description: "instance id invalid 1", + flagValues: fixtureFlagValues(func(flagValues map[string]string) { + flagValues[instanceIdFlag] = "" + }), + isValid: false, + }, + { + description: "instance id invalid 2", + flagValues: fixtureFlagValues(func(flagValues map[string]string) { + flagValues[instanceIdFlag] = "invalid-uuid" + }), + isValid: false, + }, + { + description: "backup instance id invalid 1", + flagValues: fixtureFlagValues(func(flagValues map[string]string) { + flagValues[backupInstanceIdFlag] = "" + }), + isValid: false, + }, + { + description: "backup instance id invalid 2", + flagValues: fixtureFlagValues(func(flagValues map[string]string) { + flagValues[backupInstanceIdFlag] = "invalid-uuid" + }), + isValid: false, + }, + { + description: "timestamp and backup id both provided", + flagValues: fixtureFlagValues(func(flagValues map[string]string) { + flagValues[timestampFlag] = testTimestamp + }), + isValid: false, + }, + { + description: "timestamp and backup id missing", + flagValues: fixtureFlagValues(func(flagValues map[string]string) { + delete(flagValues, backupIdFlag) + }), + isValid: false, + }, + } + + for _, tt := range tests { + t.Run(tt.description, func(t *testing.T) { + p := print.NewPrinter() + cmd := NewCmd(p) + err := globalflags.Configure(cmd.Flags()) + if err != nil { + t.Fatalf("configure global flags: %v", err) + } + + for flag, value := range tt.flagValues { + err := cmd.Flags().Set(flag, value) + if err != nil { + if !tt.isValid { + return + } + t.Fatalf("setting flag --%s=%s: %v", flag, value, err) + } + } + + err = cmd.ValidateRequiredFlags() + if err != nil { + if !tt.isValid { + return + } + t.Fatalf("error validating flags: %v", err) + } + + err = cmd.ValidateFlagGroups() + if err != nil { + if !tt.isValid { + return + } + t.Fatalf("error validating flag groups: %v", err) + } + + model, err := parseInput(p, cmd) + if err != nil { + if !tt.isValid { + return + } + t.Fatalf("error parsing flags: %v", err) + } + + if !tt.isValid { + t.Fatalf("did not fail on invalid input") + } + diff := cmp.Diff(model, tt.expectedModel) + if diff != "" { + t.Fatalf("Data does not match: %s", diff) + } + }) + } +} + +func TestBuildRestoreRequest(t *testing.T) { + tests := []struct { + description string + model *inputModel + expectedRequest mongodbflex.ApiRestoreInstanceRequest + }{ + { + description: "base", + model: fixtureInputModel(), + expectedRequest: fixtureRestoreRequest(), + }, + } + + for _, tt := range tests { + t.Run(tt.description, func(t *testing.T) { + request := buildRestoreRequest(testCtx, tt.model, testClient) + + diff := cmp.Diff(request, tt.expectedRequest, + cmp.AllowUnexported(tt.expectedRequest), + cmpopts.EquateComparable(testCtx)) + if diff != "" { + t.Fatalf("Data does not match: %s", diff) + } + }) + } +} + +func TestBuildCloneRequest(t *testing.T) { + tests := []struct { + description string + model *inputModel + expectedRequest mongodbflex.ApiCloneInstanceRequest + }{ + { + description: "base", + model: fixtureInputModel(func(model *inputModel) { + model.BackupId = "" + model.Timestamp = testTimestamp + }), + expectedRequest: fixtureCloneRequest(), + }, + } + + for _, tt := range tests { + t.Run(tt.description, func(t *testing.T) { + request := buildCloneRequest(testCtx, tt.model, testClient) + + diff := cmp.Diff(request, tt.expectedRequest, + cmp.AllowUnexported(tt.expectedRequest), + cmpopts.EquateComparable(testCtx)) + if diff != "" { + t.Fatalf("Data does not match: %s", diff) + } + }) + } +} + +func TestGetIsRestoreOperation(t *testing.T) { + tests := []struct { + description string + model *inputModel + expected bool + }{ + { + description: "true", + model: fixtureInputModel(), + expected: true, + }, + { + description: "false", + model: fixtureInputModel(func(model *inputModel) { + model.BackupId = "" + model.Timestamp = testTimestamp + }), + expected: false, + }, + } + + for _, tt := range tests { + t.Run(tt.description, func(t *testing.T) { + result := getIsRestoreOperation(tt.model.BackupId, tt.model.Timestamp) + if result != tt.expected { + t.Fatalf("Data does not match: %t", result) + } + }) + } +} diff --git a/internal/cmd/mongodbflex/backup/schedule/schedule.go b/internal/cmd/mongodbflex/backup/schedule/schedule.go new file mode 100644 index 000000000..ffd84ca3e --- /dev/null +++ b/internal/cmd/mongodbflex/backup/schedule/schedule.go @@ -0,0 +1,161 @@ +package schedule + +import ( + "context" + "encoding/json" + "fmt" + + "github.com/goccy/go-yaml" + "github.com/stackitcloud/stackit-cli/internal/pkg/args" + "github.com/stackitcloud/stackit-cli/internal/pkg/errors" + "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" + "github.com/stackitcloud/stackit-cli/internal/pkg/services/mongodbflex/client" + "github.com/stackitcloud/stackit-cli/internal/pkg/tables" + + "github.com/spf13/cobra" + "github.com/stackitcloud/stackit-sdk-go/services/mongodbflex" +) + +const ( + instanceIdFlag = "instance-id" +) + +type inputModel struct { + *globalflags.GlobalFlagModel + InstanceId string +} + +func NewCmd(p *print.Printer) *cobra.Command { + cmd := &cobra.Command{ + Use: "schedule", + Short: "Shows details of the backup schedule and retention policy of a MongoDB Flex instance", + Long: "Shows details of the backup schedule and retention policy of a MongoDB Flex instance.", + Args: args.NoArgs, + Example: examples.Build( + examples.NewExample( + `Get details of the backup schedule of a MongoDB Flex instance with ID "xxx"`, + "$ stackit mongodbflex backup schedule --instance-id xxx"), + examples.NewExample( + `Get details of the backup schedule of a MongoDB Flex instance with ID "xxx" in JSON format`, + "$ stackit mongodbflex backup schedule --instance-id xxx --output-format json"), + ), + RunE: func(cmd *cobra.Command, args []string) error { + ctx := context.Background() + model, err := parseInput(p, cmd) + if err != nil { + return err + } + // Configure API client + apiClient, err := client.ConfigureClient(p) + if err != nil { + return err + } + + // Call API + req := buildRequest(ctx, model, apiClient) + resp, err := req.Execute() + if err != nil { + return fmt.Errorf("read MongoDB Flex instance: %w", err) + } + + return outputResult(p, model.OutputFormat, resp.Item) + }, + } + configureFlags(cmd) + return cmd +} + +func configureFlags(cmd *cobra.Command) { + cmd.Flags().Var(flags.UUIDFlag(), instanceIdFlag, "Instance ID") + + err := flags.MarkFlagsRequired(cmd, instanceIdFlag) + cobra.CheckErr(err) +} + +func parseInput(p *print.Printer, cmd *cobra.Command) (*inputModel, error) { + globalFlags := globalflags.Parse(p, cmd) + if globalFlags.ProjectId == "" { + return nil, &errors.ProjectIdError{} + } + + model := inputModel{ + GlobalFlagModel: globalFlags, + InstanceId: *flags.FlagToStringPointer(p, cmd, instanceIdFlag), + } + + if p.IsVerbosityDebug() { + modelStr, err := print.BuildDebugStrFromInputModel(model) + if err != nil { + p.Debug(print.ErrorLevel, "convert model to string for debugging: %v", err) + } else { + p.Debug(print.DebugLevel, "parsed input values: %s", modelStr) + } + } + + return &model, nil +} + +func buildRequest(ctx context.Context, model *inputModel, apiClient *mongodbflex.APIClient) mongodbflex.ApiGetInstanceRequest { + req := apiClient.GetInstance(ctx, model.ProjectId, model.InstanceId) + return req +} + +func outputResult(p *print.Printer, outputFormat string, instance *mongodbflex.Instance) error { + output := struct { + BackupSchedule string `json:"backup_schedule"` + DailySnaphotRetentionDays string `json:"daily_snapshot_retention_days"` + MonthlySnapshotRetentionMonths string `json:"monthly_snapshot_retention_months"` + PointInTimeWindowHours string `json:"point_in_time_window_hours"` + SnapshotRetentionDays string `json:"snapshot_retention_days"` + WeeklySnapshotRetentionWeeks string `json:"weekly_snapshot_retention_weeks"` + }{ + BackupSchedule: *instance.BackupSchedule, + DailySnaphotRetentionDays: (*instance.Options)["dailySnapshotRetentionDays"], + MonthlySnapshotRetentionMonths: (*instance.Options)["monthlySnapshotRetentionDays"], + PointInTimeWindowHours: (*instance.Options)["pointInTimeWindowHours"], + SnapshotRetentionDays: (*instance.Options)["snapshotRetentionDays"], + WeeklySnapshotRetentionWeeks: (*instance.Options)["weeklySnapshotRetentionWeeks"], + } + + switch outputFormat { + case print.JSONOutputFormat: + details, err := json.MarshalIndent(output, "", " ") + if err != nil { + return fmt.Errorf("marshal MongoDB Flex backup schedule: %w", err) + } + p.Outputln(string(details)) + + return nil + case print.YAMLOutputFormat: + details, err := yaml.MarshalWithOptions(output, yaml.IndentSequence(true)) + if err != nil { + return fmt.Errorf("marshal MongoDB Flex backup schedule: %w", err) + } + p.Outputln(string(details)) + + return nil + default: + table := tables.NewTable() + table.AddRow("BACKUP SCHEDULE", *instance.BackupSchedule) + table.AddSeparator() + table.AddRow("DAILY SNAPSHOT RETENTION (DAYS)", (*instance.Options)["dailySnapshotRetentionDays"]) + table.AddSeparator() + table.AddRow("MONTHLY SNAPSHOT RETENTION (MONTHS)", (*instance.Options)["monthlySnapshotRetentionMonths"]) + table.AddSeparator() + table.AddRow("POINT IN TIME WINDOW (HOURS)", (*instance.Options)["pointInTimeWindowHours"]) + table.AddSeparator() + table.AddRow("SNAPSHOT RETENTION (DAYS)", (*instance.Options)["snapshotRetentionDays"]) + table.AddSeparator() + table.AddRow("WEEKLY SNAPSHOT RETENTION (WEEKS)", (*instance.Options)["weeklySnapshotRetentionWeeks"]) + table.AddSeparator() + err := table.Display(p) + if err != nil { + return fmt.Errorf("render table: %w", err) + } + + return nil + } +} diff --git a/internal/cmd/mongodbflex/backup/schedule/schedule_test.go b/internal/cmd/mongodbflex/backup/schedule/schedule_test.go new file mode 100644 index 000000000..e25957058 --- /dev/null +++ b/internal/cmd/mongodbflex/backup/schedule/schedule_test.go @@ -0,0 +1,195 @@ +package schedule + +import ( + "context" + "testing" + + "github.com/stackitcloud/stackit-cli/internal/pkg/globalflags" + "github.com/stackitcloud/stackit-cli/internal/pkg/print" + + "github.com/google/go-cmp/cmp" + "github.com/google/go-cmp/cmp/cmpopts" + "github.com/google/uuid" + "github.com/spf13/cobra" + "github.com/stackitcloud/stackit-sdk-go/services/mongodbflex" +) + +var projectIdFlag = globalflags.ProjectIdFlag + +type testCtxKey struct{} + +var testCtx = context.WithValue(context.Background(), testCtxKey{}, "foo") +var testClient = &mongodbflex.APIClient{} +var testProjectId = uuid.NewString() +var testInstanceId = uuid.NewString() + +func fixtureFlagValues(mods ...func(flagValues map[string]string)) map[string]string { + flagValues := map[string]string{ + projectIdFlag: testProjectId, + instanceIdFlag: testInstanceId, + } + for _, mod := range mods { + mod(flagValues) + } + return flagValues +} + +func fixtureInputModel(mods ...func(model *inputModel)) *inputModel { + model := &inputModel{ + GlobalFlagModel: &globalflags.GlobalFlagModel{ + ProjectId: testProjectId, + Verbosity: globalflags.VerbosityDefault, + }, + InstanceId: testInstanceId, + } + for _, mod := range mods { + mod(model) + } + return model +} + +func fixtureRequest(mods ...func(request *mongodbflex.ApiGetInstanceRequest)) mongodbflex.ApiGetInstanceRequest { + request := testClient.GetInstance(testCtx, testProjectId, testInstanceId) + for _, mod := range mods { + mod(&request) + } + return request +} + +func TestParseInput(t *testing.T) { + tests := []struct { + description 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: false, + }, + { + description: "project id missing", + flagValues: fixtureFlagValues(func(flagValues map[string]string) { + delete(flagValues, projectIdFlag) + }), + isValid: false, + }, + { + description: "project id invalid 1", + flagValues: fixtureFlagValues(func(flagValues map[string]string) { + flagValues[projectIdFlag] = "" + }), + isValid: false, + }, + { + description: "project id invalid 2", + flagValues: fixtureFlagValues(func(flagValues map[string]string) { + flagValues[projectIdFlag] = "invalid-uuid" + }), + isValid: false, + }, + { + description: "instance id missing", + flagValues: fixtureFlagValues(func(flagValues map[string]string) { + delete(flagValues, instanceIdFlag) + }), + isValid: false, + }, + { + description: "instance id invalid 1", + flagValues: fixtureFlagValues(func(flagValues map[string]string) { + flagValues[instanceIdFlag] = "" + }), + isValid: false, + }, + { + description: "instance id invalid 2", + flagValues: fixtureFlagValues(func(flagValues map[string]string) { + flagValues[instanceIdFlag] = "invalid-uuid" + }), + isValid: false, + }, + } + + for _, tt := range tests { + t.Run(tt.description, func(t *testing.T) { + cmd := &cobra.Command{} + err := globalflags.Configure(cmd.Flags()) + if err != nil { + t.Fatalf("configure global flags: %v", err) + } + + configureFlags(cmd) + + for flag, value := range tt.flagValues { + err := cmd.Flags().Set(flag, value) + if err != nil { + if !tt.isValid { + return + } + t.Fatalf("setting flag --%s=%s: %v", flag, value, err) + } + } + + err = cmd.ValidateRequiredFlags() + if err != nil { + if !tt.isValid { + return + } + t.Fatalf("error validating flags: %v", err) + } + + p := print.NewPrinter() + model, err := parseInput(p, cmd) + if err != nil { + if !tt.isValid { + return + } + t.Fatalf("error parsing flags: %v", err) + } + + if !tt.isValid { + t.Fatalf("did not fail on invalid input") + } + diff := cmp.Diff(model, tt.expectedModel) + if diff != "" { + t.Fatalf("Data does not match: %s", diff) + } + }) + } +} + +func TestBuildRequest(t *testing.T) { + tests := []struct { + description string + model *inputModel + expectedRequest mongodbflex.ApiGetInstanceRequest + }{ + { + description: "base", + model: fixtureInputModel(), + expectedRequest: fixtureRequest(), + }, + } + + for _, tt := range tests { + t.Run(tt.description, func(t *testing.T) { + request := buildRequest(testCtx, tt.model, testClient) + + diff := cmp.Diff(request, tt.expectedRequest, + cmp.AllowUnexported(tt.expectedRequest), + cmpopts.EquateComparable(testCtx), + ) + if diff != "" { + t.Fatalf("Data does not match: %s", diff) + } + }) + } +} diff --git a/internal/cmd/mongodbflex/backup/update-schedule/update_schedule.go b/internal/cmd/mongodbflex/backup/update-schedule/update_schedule.go new file mode 100644 index 000000000..cf296ea92 --- /dev/null +++ b/internal/cmd/mongodbflex/backup/update-schedule/update_schedule.go @@ -0,0 +1,233 @@ +package updateschedule + +import ( + "context" + "fmt" + "strconv" + + "github.com/spf13/cobra" + "github.com/stackitcloud/stackit-cli/internal/pkg/args" + cliErr "github.com/stackitcloud/stackit-cli/internal/pkg/errors" + "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" + "github.com/stackitcloud/stackit-cli/internal/pkg/services/mongodbflex/client" + mongoDBflexUtils "github.com/stackitcloud/stackit-cli/internal/pkg/services/mongodbflex/utils" + "github.com/stackitcloud/stackit-cli/internal/pkg/utils" + "github.com/stackitcloud/stackit-sdk-go/services/mongodbflex" +) + +const ( + instanceIdFlag = "instance-id" + scheduleFlag = "schedule" + snapshotRetentionDaysFlag = "save-snapshot-days" + dailySnapshotRetentionDaysFlag = "save-daily-snapshot-days" + weeklySnapshotRetentionWeeksFlag = "save-weekly-snapshot-weeks" + monthlySnapshotRetentionMonthsFlag = "save-monthly-snapshot-months" + + // Default values for the backup schedule options + defaultBackupSchedule = "0 0/6 * * *" + defaultSnapshotRetentionDays int64 = 3 + defaultDailySnapshotRetentionDays int64 = 0 + defaultWeeklySnapshotRetentionWeeks int64 = 3 + defaultMonthlySnapshotRetentionMonths int64 = 1 + defaultPointInTimeWindowHours int64 = 30 +) + +type inputModel struct { + *globalflags.GlobalFlagModel + + InstanceId *string + BackupSchedule *string + SnapshotRetentionDays *int64 + DailySnaphotRetentionDays *int64 + WeeklySnapshotRetentionWeeks *int64 + MonthlySnapshotRetentionMonths *int64 +} + +func NewCmd(p *print.Printer) *cobra.Command { + cmd := &cobra.Command{ + Use: "update-schedule", + Short: "Updates the backup schedule and retention policy for a MongoDB Flex instance", + Long: fmt.Sprintf("%s\n%s\n%s\n%s", + "Updates the backup schedule and retention policy for a MongoDB Flex instance.", + `The current backup schedule and retention policy can be seen in the output of the "stackit mongodbflex backup schedule" command.`, + "The backup schedule is defined in the cron scheduling system format e.g. '0 0 * * *'.", + "See below for more detail on the retention policy options.", + ), + Args: args.NoArgs, + Example: examples.Build( + examples.NewExample( + `Update the backup schedule of a MongoDB Flex instance with ID "xxx"`, + "$ stackit mongodbflex backup update-schedule --instance-id xxx --schedule '6 6 * * *'"), + examples.NewExample( + `Update the retention days for snapshots of a MongoDB Flex instance with ID "xxx" to 5 days`, + "$ stackit mongodbflex backup update-schedule --instance-id xxx --save-snapshot-days 5"), + ), + + RunE: func(cmd *cobra.Command, args []string) error { + ctx := context.Background() + + model, err := parseInput(p, cmd) + if err != nil { + return err + } + + // Configure API client + apiClient, err := client.ConfigureClient(p) + if err != nil { + return err + } + + instanceLabel, err := mongoDBflexUtils.GetInstanceName(ctx, apiClient, model.ProjectId, *model.InstanceId) + if err != nil { + p.Debug(print.ErrorLevel, "get instance name: %v", err) + instanceLabel = *model.InstanceId + } + + if !model.AssumeYes { + prompt := fmt.Sprintf("Are you sure you want to update backup schedule of instance %q?", instanceLabel) + err = p.PromptForConfirmation(prompt) + if err != nil { + return err + } + } + + // Get current instance + getReq := buildGetInstanceRequest(ctx, model, apiClient) + getResp, err := getReq.Execute() + if err != nil { + return fmt.Errorf("get MongoDB Flex instance %q: %w", instanceLabel, err) + } + + instance := getResp.Item + + // Call API + req := buildUpdateBackupScheduleRequest(ctx, model, instance, apiClient) + _, err = req.Execute() + if err != nil { + return fmt.Errorf("update backup schedule of MongoDB Flex instance: %w", err) + } + + cmd.Printf("Updated backup schedule of instance %q\n", instanceLabel) + return nil + }, + } + configureFlags(cmd) + return cmd +} + +func configureFlags(cmd *cobra.Command) { + cmd.Flags().Var(flags.UUIDFlag(), instanceIdFlag, "Instance ID") + cmd.Flags().String(scheduleFlag, "", "Backup schedule, in the cron scheduling system format e.g. '0 0 * * *'") + cmd.Flags().Int64(snapshotRetentionDaysFlag, 0, "Number of days to retain snapshots. Should be less than or equal to the value of the daily backup.") + cmd.Flags().Int64(dailySnapshotRetentionDaysFlag, 0, "Number of days to retain daily snapshots. Should be less than or equal to the number of days of the selected weekly or monthly value.") + cmd.Flags().Int64(weeklySnapshotRetentionWeeksFlag, 0, "Number of weeks to retain weekly snapshots. Should be less than or equal to the number of weeks of the selected monthly value.") + cmd.Flags().Int64(monthlySnapshotRetentionMonthsFlag, 0, "Number of months to retain monthly snapshots") + + err := flags.MarkFlagsRequired(cmd, instanceIdFlag) + cobra.CheckErr(err) +} + +func parseInput(p *print.Printer, cmd *cobra.Command) (*inputModel, error) { + globalFlags := globalflags.Parse(p, cmd) + if globalFlags.ProjectId == "" { + return nil, &cliErr.ProjectIdError{} + } + + schedule := flags.FlagToStringPointer(p, cmd, scheduleFlag) + snapshotRetentionDays := flags.FlagToInt64Pointer(p, cmd, snapshotRetentionDaysFlag) + dailySnapshotRetentionDays := flags.FlagToInt64Pointer(p, cmd, dailySnapshotRetentionDaysFlag) + weeklySnapshotRetentionWeeks := flags.FlagToInt64Pointer(p, cmd, weeklySnapshotRetentionWeeksFlag) + monthlySnapshotRetentionMonths := flags.FlagToInt64Pointer(p, cmd, monthlySnapshotRetentionMonthsFlag) + + if schedule == nil && snapshotRetentionDays == nil && dailySnapshotRetentionDays == nil && weeklySnapshotRetentionWeeks == nil && monthlySnapshotRetentionMonths == nil { + return nil, &cliErr.EmptyUpdateError{} + } + + return &inputModel{ + GlobalFlagModel: globalFlags, + InstanceId: flags.FlagToStringPointer(p, cmd, instanceIdFlag), + BackupSchedule: schedule, + DailySnaphotRetentionDays: dailySnapshotRetentionDays, + MonthlySnapshotRetentionMonths: monthlySnapshotRetentionMonths, + SnapshotRetentionDays: snapshotRetentionDays, + WeeklySnapshotRetentionWeeks: weeklySnapshotRetentionWeeks, + }, nil +} + +func buildUpdateBackupScheduleRequest(ctx context.Context, model *inputModel, instance *mongodbflex.Instance, apiClient *mongodbflex.APIClient) mongodbflex.ApiUpdateBackupScheduleRequest { + req := apiClient.UpdateBackupSchedule(ctx, model.ProjectId, *model.InstanceId) + + payload := getUpdateBackupSchedulePayload(instance) + + if model.BackupSchedule != nil { + payload.BackupSchedule = model.BackupSchedule + } + if model.DailySnaphotRetentionDays != nil { + payload.DailySnapshotRetentionDays = model.DailySnaphotRetentionDays + } + if model.MonthlySnapshotRetentionMonths != nil { + payload.MonthlySnapshotRetentionMonths = model.MonthlySnapshotRetentionMonths + } + if model.SnapshotRetentionDays != nil { + payload.SnapshotRetentionDays = model.SnapshotRetentionDays + } + if model.WeeklySnapshotRetentionWeeks != nil { + payload.WeeklySnapshotRetentionWeeks = model.WeeklySnapshotRetentionWeeks + } + + req = req.UpdateBackupSchedulePayload(payload) + return req +} + +// getUpdateBackupSchedulePayload creates a payload for the UpdateBackupSchedule API call +// it will use the values already set in the instance object +// falls back to default values if the values are not set +func getUpdateBackupSchedulePayload(instance *mongodbflex.Instance) mongodbflex.UpdateBackupSchedulePayload { + options := make(map[string]string) + if instance == nil || instance.Options != nil { + options = *instance.Options + } + + backupSchedule := instance.BackupSchedule + if backupSchedule == nil { + backupSchedule = utils.Ptr(defaultBackupSchedule) + } + dailySnapshotRetentionDays, err := strconv.ParseInt(options["dailySnapshotRetentionDays"], 10, 64) + if err != nil { + dailySnapshotRetentionDays = defaultDailySnapshotRetentionDays + } + weeklySnapshotRetentionWeeks, err := strconv.ParseInt(options["weeklySnapshotRetentionWeeks"], 10, 64) + if err != nil { + weeklySnapshotRetentionWeeks = defaultWeeklySnapshotRetentionWeeks + } + monthlySnapshotRetentionMonths, err := strconv.ParseInt(options["monthlySnapshotRetentionMonths"], 10, 64) + if err != nil { + monthlySnapshotRetentionMonths = defaultMonthlySnapshotRetentionMonths + } + pointInTimeWindowHours, err := strconv.ParseInt(options["pointInTimeWindowHours"], 10, 64) + if err != nil { + pointInTimeWindowHours = defaultPointInTimeWindowHours + } + snapshotRetentionDays, err := strconv.ParseInt(options["snapshotRetentionDays"], 10, 64) + if err != nil { + snapshotRetentionDays = defaultSnapshotRetentionDays + } + + defaultPayload := mongodbflex.UpdateBackupSchedulePayload{ + BackupSchedule: backupSchedule, + DailySnapshotRetentionDays: &dailySnapshotRetentionDays, + MonthlySnapshotRetentionMonths: &monthlySnapshotRetentionMonths, + PointInTimeWindowHours: &pointInTimeWindowHours, + SnapshotRetentionDays: &snapshotRetentionDays, + WeeklySnapshotRetentionWeeks: &weeklySnapshotRetentionWeeks, + } + return defaultPayload +} + +func buildGetInstanceRequest(ctx context.Context, model *inputModel, apiClient *mongodbflex.APIClient) mongodbflex.ApiGetInstanceRequest { + req := apiClient.GetInstance(ctx, model.ProjectId, *model.InstanceId) + return req +} diff --git a/internal/cmd/mongodbflex/backup/update-schedule/update_schedule_test.go b/internal/cmd/mongodbflex/backup/update-schedule/update_schedule_test.go new file mode 100644 index 000000000..1f6cc5aa0 --- /dev/null +++ b/internal/cmd/mongodbflex/backup/update-schedule/update_schedule_test.go @@ -0,0 +1,324 @@ +package updateschedule + +import ( + "context" + "testing" + + "github.com/stackitcloud/stackit-cli/internal/pkg/globalflags" + "github.com/stackitcloud/stackit-cli/internal/pkg/utils" + + "github.com/google/go-cmp/cmp" + "github.com/google/go-cmp/cmp/cmpopts" + "github.com/google/uuid" + "github.com/stackitcloud/stackit-sdk-go/services/mongodbflex" +) + +var projectIdFlag = globalflags.ProjectIdFlag + +type testCtxKey struct{} + +var testCtx = context.WithValue(context.Background(), testCtxKey{}, "foo") +var testClient = &mongodbflex.APIClient{} +var testProjectId = uuid.NewString() +var testInstanceId = uuid.NewString() +var testSchedule = "0 0/6 * * *" + +func fixtureFlagValues(mods ...func(flagValues map[string]string)) map[string]string { + flagValues := map[string]string{ + projectIdFlag: testProjectId, + scheduleFlag: testSchedule, + instanceIdFlag: testInstanceId, + } + for _, mod := range mods { + mod(flagValues) + } + return flagValues +} + +func fixtureInputModel(mods ...func(model *inputModel)) *inputModel { + model := &inputModel{ + GlobalFlagModel: &globalflags.GlobalFlagModel{ + ProjectId: testProjectId, + Verbosity: globalflags.VerbosityDefault, + }, + InstanceId: utils.Ptr(testInstanceId), + BackupSchedule: &testSchedule, + } + for _, mod := range mods { + mod(model) + } + return model +} + +func fixturePayload(mods ...func(payload *mongodbflex.UpdateBackupSchedulePayload)) mongodbflex.UpdateBackupSchedulePayload { + payload := mongodbflex.UpdateBackupSchedulePayload{ + BackupSchedule: utils.Ptr(testSchedule), + SnapshotRetentionDays: utils.Ptr(int64(3)), + DailySnapshotRetentionDays: utils.Ptr(int64(0)), + WeeklySnapshotRetentionWeeks: utils.Ptr(int64(3)), + MonthlySnapshotRetentionMonths: utils.Ptr(int64(1)), + PointInTimeWindowHours: utils.Ptr(int64(30)), + } + for _, mod := range mods { + mod(&payload) + } + return payload +} + +func fixtureUpdateBackupScheduleRequest(mods ...func(request *mongodbflex.ApiUpdateBackupScheduleRequest)) mongodbflex.ApiUpdateBackupScheduleRequest { + request := testClient.UpdateBackupSchedule(testCtx, testProjectId, testInstanceId) + request = request.UpdateBackupSchedulePayload(fixturePayload()) + for _, mod := range mods { + mod(&request) + } + return request +} + +func fixtureGetInstanceRequest(mods ...func(request *mongodbflex.ApiGetInstanceRequest)) mongodbflex.ApiGetInstanceRequest { + request := testClient.GetInstance(testCtx, testProjectId, testInstanceId) + for _, mod := range mods { + mod(&request) + } + return request +} + +func fixtureInstance(mods ...func(instance *mongodbflex.Instance)) *mongodbflex.Instance { + instance := mongodbflex.Instance{ + BackupSchedule: &testSchedule, + Options: &map[string]string{ + "dailySnapshotRetentionDays": "0", + "weeklySnapshotRetentionWeeks": "3", + "monthlySnapshotRetentionMonths": "1", + "pointInTimeWindowHours": "30", + "snapshotRetentionDays": "3", + }, + } + for _, mod := range mods { + mod(&instance) + } + return &instance +} + +func TestParseInput(t *testing.T) { + tests := []struct { + description string + flagValues map[string]string + aclValues []string + isValid bool + expectedModel *inputModel + }{ + { + description: "base", + flagValues: fixtureFlagValues(), + isValid: true, + expectedModel: fixtureInputModel(), + }, + { + description: "no values", + flagValues: map[string]string{}, + isValid: false, + }, + { + description: "project id missing", + flagValues: fixtureFlagValues(func(flagValues map[string]string) { + delete(flagValues, projectIdFlag) + }), + isValid: false, + }, + { + description: "project id invalid 1", + flagValues: fixtureFlagValues(func(flagValues map[string]string) { + flagValues[projectIdFlag] = "" + }), + isValid: false, + }, + { + description: "project id invalid 2", + flagValues: fixtureFlagValues(func(flagValues map[string]string) { + flagValues[projectIdFlag] = "invalid-uuid" + }), + isValid: false, + }, + { + description: "instance id missing", + flagValues: fixtureFlagValues(func(flagValues map[string]string) { + delete(flagValues, instanceIdFlag) + }), + isValid: false, + }, + { + description: "instance id invalid 1", + flagValues: fixtureFlagValues(func(flagValues map[string]string) { + flagValues[instanceIdFlag] = "" + }), + isValid: false, + }, + { + description: "instance id invalid 2", + flagValues: fixtureFlagValues(func(flagValues map[string]string) { + flagValues[instanceIdFlag] = "invalid-uuid" + }), + isValid: false, + }, + { + description: "backup schedule missing", + flagValues: fixtureFlagValues(func(flagValues map[string]string) { + delete(flagValues, scheduleFlag) + }), + isValid: false, + }, + } + + for _, tt := range tests { + t.Run(tt.description, func(t *testing.T) { + cmd := NewCmd(nil) + err := globalflags.Configure(cmd.Flags()) + if err != nil { + t.Fatalf("configure global flags: %v", err) + } + + for flag, value := range tt.flagValues { + err := cmd.Flags().Set(flag, value) + if err != nil { + if !tt.isValid { + return + } + t.Fatalf("setting flag --%s=%s: %v", flag, value, err) + } + } + + err = cmd.ValidateRequiredFlags() + if err != nil { + if !tt.isValid { + return + } + t.Fatalf("error validating flags: %v", err) + } + + model, err := parseInput(nil, cmd) + if err != nil { + if !tt.isValid { + return + } + t.Fatalf("error parsing flags: %v", err) + } + + if !tt.isValid { + t.Fatalf("did not fail on invalid input") + } + diff := cmp.Diff(model, tt.expectedModel) + if diff != "" { + t.Fatalf("Data does not match: %s", diff) + } + }) + } +} + +func TestBuildGetInstanceRequest(t *testing.T) { + tests := []struct { + description string + model *inputModel + expectedRequest mongodbflex.ApiGetInstanceRequest + }{ + { + description: "base", + model: fixtureInputModel(), + expectedRequest: fixtureGetInstanceRequest(), + }, + } + + for _, tt := range tests { + t.Run(tt.description, func(t *testing.T) { + request := buildGetInstanceRequest(testCtx, tt.model, testClient) + + diff := cmp.Diff(request, tt.expectedRequest, + cmp.AllowUnexported(tt.expectedRequest), + cmpopts.EquateComparable(testCtx), + ) + if diff != "" { + t.Fatalf("Data does not match: %s", diff) + } + }) + } +} + +func TestBuildUpdateBackupScheduleRequest(t *testing.T) { + tests := []struct { + description string + model *inputModel + instance *mongodbflex.Instance + expectedRequest mongodbflex.ApiUpdateBackupScheduleRequest + }{ + { + description: "update backup schedule, read retention policy from instance", + model: fixtureInputModel(), + instance: fixtureInstance(), + expectedRequest: fixtureUpdateBackupScheduleRequest(), + }, + { + description: "update retention policy, read backup schedule from instance", + model: &inputModel{ + GlobalFlagModel: &globalflags.GlobalFlagModel{ + ProjectId: testProjectId, + }, + InstanceId: utils.Ptr(testInstanceId), + DailySnaphotRetentionDays: utils.Ptr(int64(2)), + }, + instance: fixtureInstance(), + expectedRequest: fixtureUpdateBackupScheduleRequest().UpdateBackupSchedulePayload( + fixturePayload(func(payload *mongodbflex.UpdateBackupSchedulePayload) { + payload.DailySnapshotRetentionDays = utils.Ptr(int64(2)) + }), + ), + }, + { + description: "update backup schedule and retention policy", + model: &inputModel{ + GlobalFlagModel: &globalflags.GlobalFlagModel{ + ProjectId: testProjectId, + }, + InstanceId: utils.Ptr(testInstanceId), + BackupSchedule: utils.Ptr("0 0/6 5 2 1"), + DailySnaphotRetentionDays: utils.Ptr(int64(2)), + WeeklySnapshotRetentionWeeks: utils.Ptr(int64(2)), + MonthlySnapshotRetentionMonths: utils.Ptr(int64(2)), + SnapshotRetentionDays: utils.Ptr(int64(2)), + }, + instance: fixtureInstance(), + expectedRequest: fixtureUpdateBackupScheduleRequest().UpdateBackupSchedulePayload( + fixturePayload(func(payload *mongodbflex.UpdateBackupSchedulePayload) { + payload.BackupSchedule = utils.Ptr("0 0/6 5 2 1") + payload.DailySnapshotRetentionDays = utils.Ptr(int64(2)) + payload.WeeklySnapshotRetentionWeeks = utils.Ptr(int64(2)) + payload.MonthlySnapshotRetentionMonths = utils.Ptr(int64(2)) + payload.SnapshotRetentionDays = utils.Ptr(int64(2)) + }), + ), + }, + { + description: "no fields set, empty instance (use defaults)", + model: &inputModel{ + GlobalFlagModel: &globalflags.GlobalFlagModel{ + ProjectId: testProjectId, + }, + InstanceId: utils.Ptr(testInstanceId), + }, + instance: &mongodbflex.Instance{}, + expectedRequest: fixtureUpdateBackupScheduleRequest(), + }, + } + + for _, tt := range tests { + t.Run(tt.description, func(t *testing.T) { + request := buildUpdateBackupScheduleRequest(testCtx, tt.model, tt.instance, testClient) + + diff := cmp.Diff(request, tt.expectedRequest, + cmp.AllowUnexported(tt.expectedRequest), + cmpopts.EquateComparable(testCtx), + ) + if diff != "" { + t.Fatalf("Data does not match: %s", diff) + } + }) + } +} diff --git a/internal/cmd/mongodbflex/instance/create/create.go b/internal/cmd/mongodbflex/instance/create/create.go index 7924c00d5..607774a0c 100644 --- a/internal/cmd/mongodbflex/instance/create/create.go +++ b/internal/cmd/mongodbflex/instance/create/create.go @@ -6,6 +6,7 @@ import ( "errors" "fmt" + "github.com/goccy/go-yaml" "github.com/stackitcloud/stackit-cli/internal/pkg/args" cliErr "github.com/stackitcloud/stackit-cli/internal/pkg/errors" "github.com/stackitcloud/stackit-cli/internal/pkg/examples" @@ -281,6 +282,14 @@ func outputResult(p *print.Printer, model *inputModel, projectLabel string, resp } p.Outputln(string(details)) + return nil + case print.YAMLOutputFormat: + details, err := yaml.MarshalWithOptions(resp, yaml.IndentSequence(true)) + if err != nil { + return fmt.Errorf("marshal MongoDBFlex instance: %w", err) + } + p.Outputln(string(details)) + return nil default: operationState := "Created" diff --git a/internal/cmd/mongodbflex/instance/describe/describe.go b/internal/cmd/mongodbflex/instance/describe/describe.go index 6d7fda1d5..1d1bacb99 100644 --- a/internal/cmd/mongodbflex/instance/describe/describe.go +++ b/internal/cmd/mongodbflex/instance/describe/describe.go @@ -6,6 +6,7 @@ import ( "fmt" "strings" + "github.com/goccy/go-yaml" "github.com/stackitcloud/stackit-cli/internal/pkg/args" "github.com/stackitcloud/stackit-cli/internal/pkg/errors" "github.com/stackitcloud/stackit-cli/internal/pkg/examples" @@ -107,6 +108,14 @@ func outputResult(p *print.Printer, outputFormat string, instance *mongodbflex.I } p.Outputln(string(details)) + return nil + case print.YAMLOutputFormat: + details, err := yaml.MarshalWithOptions(instance, yaml.IndentSequence(true)) + if err != nil { + return fmt.Errorf("marshal MongoDB Flex instance: %w", err) + } + p.Outputln(string(details)) + return nil default: aclsArray := *instance.Acl.Items @@ -141,6 +150,8 @@ func outputResult(p *print.Printer, outputFormat string, instance *mongodbflex.I table.AddSeparator() table.AddRow("RAM", *instance.Flavor.Memory) table.AddSeparator() + table.AddRow("BACKUP SCHEDULE", *instance.BackupSchedule) + table.AddSeparator() err = table.Display(p) if err != nil { return fmt.Errorf("render table: %w", err) diff --git a/internal/cmd/mongodbflex/instance/list/list.go b/internal/cmd/mongodbflex/instance/list/list.go index c49bcc781..443c7ef08 100644 --- a/internal/cmd/mongodbflex/instance/list/list.go +++ b/internal/cmd/mongodbflex/instance/list/list.go @@ -5,6 +5,7 @@ import ( "encoding/json" "fmt" + "github.com/goccy/go-yaml" "github.com/stackitcloud/stackit-cli/internal/pkg/args" "github.com/stackitcloud/stackit-cli/internal/pkg/errors" "github.com/stackitcloud/stackit-cli/internal/pkg/examples" @@ -137,6 +138,14 @@ func outputResult(p *print.Printer, outputFormat string, instances []mongodbflex } p.Outputln(string(details)) + return nil + case print.YAMLOutputFormat: + details, err := yaml.MarshalWithOptions(instances, yaml.IndentSequence(true)) + if err != nil { + return fmt.Errorf("marshal MongoDB Flex instance list: %w", err) + } + p.Outputln(string(details)) + return nil default: table := tables.NewTable() diff --git a/internal/cmd/mongodbflex/instance/update/update.go b/internal/cmd/mongodbflex/instance/update/update.go index 63bf07507..bc3dba632 100644 --- a/internal/cmd/mongodbflex/instance/update/update.go +++ b/internal/cmd/mongodbflex/instance/update/update.go @@ -6,6 +6,7 @@ import ( "errors" "fmt" + "github.com/goccy/go-yaml" "github.com/stackitcloud/stackit-cli/internal/pkg/args" cliErr "github.com/stackitcloud/stackit-cli/internal/pkg/errors" "github.com/stackitcloud/stackit-cli/internal/pkg/examples" @@ -315,6 +316,14 @@ func outputResult(p *print.Printer, model *inputModel, instanceLabel string, res } p.Outputln(string(details)) + return nil + case print.YAMLOutputFormat: + details, err := yaml.MarshalWithOptions(resp, yaml.IndentSequence(true)) + if err != nil { + return fmt.Errorf("marshal update MongoDBFlex instance: %w", err) + } + p.Outputln(string(details)) + return nil default: operationState := "Updated" diff --git a/internal/cmd/mongodbflex/mongodbflex.go b/internal/cmd/mongodbflex/mongodbflex.go index 3aa6bf890..e7373b9b9 100644 --- a/internal/cmd/mongodbflex/mongodbflex.go +++ b/internal/cmd/mongodbflex/mongodbflex.go @@ -1,6 +1,7 @@ package mongodbflex import ( + "github.com/stackitcloud/stackit-cli/internal/cmd/mongodbflex/backup" "github.com/stackitcloud/stackit-cli/internal/cmd/mongodbflex/instance" "github.com/stackitcloud/stackit-cli/internal/cmd/mongodbflex/options" "github.com/stackitcloud/stackit-cli/internal/cmd/mongodbflex/user" @@ -27,4 +28,5 @@ func addSubcommands(cmd *cobra.Command, p *print.Printer) { cmd.AddCommand(instance.NewCmd(p)) cmd.AddCommand(user.NewCmd(p)) cmd.AddCommand(options.NewCmd(p)) + cmd.AddCommand(backup.NewCmd(p)) } diff --git a/internal/cmd/mongodbflex/options/options.go b/internal/cmd/mongodbflex/options/options.go index 217756194..2480e5b10 100644 --- a/internal/cmd/mongodbflex/options/options.go +++ b/internal/cmd/mongodbflex/options/options.go @@ -5,6 +5,7 @@ import ( "encoding/json" "fmt" + "github.com/goccy/go-yaml" "github.com/stackitcloud/stackit-cli/internal/pkg/args" "github.com/stackitcloud/stackit-cli/internal/pkg/examples" "github.com/stackitcloud/stackit-cli/internal/pkg/flags" @@ -190,6 +191,14 @@ func outputResult(p *print.Printer, model *inputModel, flavors *mongodbflex.List return fmt.Errorf("marshal MongoDB Flex options: %w", err) } p.Outputln(string(details)) + return nil + case print.YAMLOutputFormat: + details, err := yaml.MarshalWithOptions(options, yaml.IndentSequence(true)) + if err != nil { + return fmt.Errorf("marshal MongoDB Flex options: %w", err) + } + p.Outputln(string(details)) + return nil default: return outputResultAsTable(p, model, options) diff --git a/internal/cmd/mongodbflex/user/create/create.go b/internal/cmd/mongodbflex/user/create/create.go index cb6d1c9c5..8e8b743c5 100644 --- a/internal/cmd/mongodbflex/user/create/create.go +++ b/internal/cmd/mongodbflex/user/create/create.go @@ -5,6 +5,7 @@ import ( "encoding/json" "fmt" + "github.com/goccy/go-yaml" "github.com/spf13/cobra" "github.com/stackitcloud/stackit-cli/internal/pkg/args" "github.com/stackitcloud/stackit-cli/internal/pkg/errors" @@ -156,6 +157,14 @@ func outputResult(p *print.Printer, model *inputModel, instanceLabel string, use } p.Outputln(string(details)) + return nil + case print.YAMLOutputFormat: + details, err := yaml.MarshalWithOptions(user, yaml.IndentSequence(true)) + if err != nil { + return fmt.Errorf("marshal MongoDB Flex user: %w", err) + } + p.Outputln(string(details)) + return nil default: p.Outputf("Created user for instance %q. User ID: %s\n\n", instanceLabel, *user.Id) diff --git a/internal/cmd/mongodbflex/user/describe/describe.go b/internal/cmd/mongodbflex/user/describe/describe.go index c32eaace7..55d0e4f02 100644 --- a/internal/cmd/mongodbflex/user/describe/describe.go +++ b/internal/cmd/mongodbflex/user/describe/describe.go @@ -5,6 +5,7 @@ import ( "encoding/json" "fmt" + "github.com/goccy/go-yaml" "github.com/stackitcloud/stackit-cli/internal/pkg/args" "github.com/stackitcloud/stackit-cli/internal/pkg/errors" "github.com/stackitcloud/stackit-cli/internal/pkg/examples" @@ -125,6 +126,14 @@ func outputResult(p *print.Printer, outputFormat string, user mongodbflex.Instan } p.Outputln(string(details)) + return nil + case print.YAMLOutputFormat: + details, err := yaml.MarshalWithOptions(user, yaml.IndentSequence(true)) + if err != nil { + return fmt.Errorf("marshal MongoDB Flex user: %w", err) + } + p.Outputln(string(details)) + return nil default: table := tables.NewTable() diff --git a/internal/cmd/mongodbflex/user/list/list.go b/internal/cmd/mongodbflex/user/list/list.go index 6e6be06dd..ada4d0f6f 100644 --- a/internal/cmd/mongodbflex/user/list/list.go +++ b/internal/cmd/mongodbflex/user/list/list.go @@ -5,6 +5,7 @@ import ( "encoding/json" "fmt" + "github.com/goccy/go-yaml" "github.com/stackitcloud/stackit-cli/internal/pkg/args" "github.com/stackitcloud/stackit-cli/internal/pkg/errors" "github.com/stackitcloud/stackit-cli/internal/pkg/examples" @@ -145,6 +146,14 @@ func outputResult(p *print.Printer, outputFormat string, users []mongodbflex.Lis } p.Outputln(string(details)) + return nil + case print.YAMLOutputFormat: + details, err := yaml.MarshalWithOptions(users, yaml.IndentSequence(true)) + if err != nil { + return fmt.Errorf("marshal MongoDB Flex user list: %w", err) + } + p.Outputln(string(details)) + return nil default: table := tables.NewTable() diff --git a/internal/cmd/mongodbflex/user/reset-password/reset_password.go b/internal/cmd/mongodbflex/user/reset-password/reset_password.go index d51daeddd..665ad9b8b 100644 --- a/internal/cmd/mongodbflex/user/reset-password/reset_password.go +++ b/internal/cmd/mongodbflex/user/reset-password/reset_password.go @@ -5,6 +5,7 @@ import ( "encoding/json" "fmt" + "github.com/goccy/go-yaml" "github.com/stackitcloud/stackit-cli/internal/pkg/args" "github.com/stackitcloud/stackit-cli/internal/pkg/errors" "github.com/stackitcloud/stackit-cli/internal/pkg/examples" @@ -141,6 +142,14 @@ func outputResult(p *print.Printer, model *inputModel, userLabel, instanceLabel } p.Outputln(string(details)) + return nil + case print.YAMLOutputFormat: + details, err := yaml.MarshalWithOptions(user, yaml.IndentSequence(true)) + if err != nil { + return fmt.Errorf("marshal MongoDB Flex reset password: %w", err) + } + p.Outputln(string(details)) + return nil default: p.Outputf("Reset password for user %q of instance %q\n\n", userLabel, instanceLabel) diff --git a/internal/cmd/object-storage/bucket/create/create.go b/internal/cmd/object-storage/bucket/create/create.go index a052d9b99..5d145819b 100644 --- a/internal/cmd/object-storage/bucket/create/create.go +++ b/internal/cmd/object-storage/bucket/create/create.go @@ -5,6 +5,7 @@ import ( "encoding/json" "fmt" + "github.com/goccy/go-yaml" "github.com/stackitcloud/stackit-cli/internal/pkg/args" "github.com/stackitcloud/stackit-cli/internal/pkg/errors" "github.com/stackitcloud/stackit-cli/internal/pkg/examples" @@ -122,6 +123,14 @@ func outputResult(p *print.Printer, model *inputModel, resp *objectstorage.Creat } p.Outputln(string(details)) + return nil + case print.YAMLOutputFormat: + details, err := yaml.MarshalWithOptions(resp, yaml.IndentSequence(true)) + if err != nil { + return fmt.Errorf("marshal Object Storage bucket: %w", err) + } + p.Outputln(string(details)) + return nil default: operationState := "Created" diff --git a/internal/cmd/object-storage/bucket/describe/describe.go b/internal/cmd/object-storage/bucket/describe/describe.go index 94220639c..4196de7bc 100644 --- a/internal/cmd/object-storage/bucket/describe/describe.go +++ b/internal/cmd/object-storage/bucket/describe/describe.go @@ -5,6 +5,7 @@ import ( "encoding/json" "fmt" + "github.com/goccy/go-yaml" "github.com/stackitcloud/stackit-cli/internal/pkg/args" "github.com/stackitcloud/stackit-cli/internal/pkg/errors" "github.com/stackitcloud/stackit-cli/internal/pkg/examples" @@ -104,6 +105,14 @@ func outputResult(p *print.Printer, outputFormat string, bucket *objectstorage.B } p.Outputln(string(details)) + return nil + case print.YAMLOutputFormat: + details, err := yaml.MarshalWithOptions(bucket, yaml.IndentSequence(true)) + if err != nil { + return fmt.Errorf("marshal Object Storage bucket: %w", err) + } + p.Outputln(string(details)) + return nil default: table := tables.NewTable() diff --git a/internal/cmd/object-storage/bucket/list/list.go b/internal/cmd/object-storage/bucket/list/list.go index d1f5f0e35..13367f035 100644 --- a/internal/cmd/object-storage/bucket/list/list.go +++ b/internal/cmd/object-storage/bucket/list/list.go @@ -5,6 +5,7 @@ import ( "encoding/json" "fmt" + "github.com/goccy/go-yaml" "github.com/stackitcloud/stackit-cli/internal/pkg/args" "github.com/stackitcloud/stackit-cli/internal/pkg/errors" "github.com/stackitcloud/stackit-cli/internal/pkg/examples" @@ -137,6 +138,14 @@ func outputResult(p *print.Printer, outputFormat string, buckets []objectstorage } p.Outputln(string(details)) + return nil + case print.YAMLOutputFormat: + details, err := yaml.MarshalWithOptions(buckets, yaml.IndentSequence(true)) + if err != nil { + return fmt.Errorf("marshal Object Storage bucket list: %w", err) + } + p.Outputln(string(details)) + return nil default: table := tables.NewTable() diff --git a/internal/cmd/object-storage/credentials-group/create/create.go b/internal/cmd/object-storage/credentials-group/create/create.go index 36a1a71e6..d7399e501 100644 --- a/internal/cmd/object-storage/credentials-group/create/create.go +++ b/internal/cmd/object-storage/credentials-group/create/create.go @@ -5,6 +5,7 @@ import ( "encoding/json" "fmt" + "github.com/goccy/go-yaml" "github.com/stackitcloud/stackit-cli/internal/pkg/args" "github.com/stackitcloud/stackit-cli/internal/pkg/errors" "github.com/stackitcloud/stackit-cli/internal/pkg/examples" @@ -120,6 +121,14 @@ func outputResult(p *print.Printer, model *inputModel, resp *objectstorage.Creat } p.Outputln(string(details)) + return nil + case print.YAMLOutputFormat: + details, err := yaml.MarshalWithOptions(resp, yaml.IndentSequence(true)) + if err != nil { + return fmt.Errorf("marshal Object Storage credentials group: %w", err) + } + p.Outputln(string(details)) + return nil default: p.Outputf("Created credentials group %q. Credentials group ID: %s\n\n", *resp.CredentialsGroup.DisplayName, *resp.CredentialsGroup.CredentialsGroupId) diff --git a/internal/cmd/object-storage/credentials-group/list/list.go b/internal/cmd/object-storage/credentials-group/list/list.go index 821389d34..073a88a9a 100644 --- a/internal/cmd/object-storage/credentials-group/list/list.go +++ b/internal/cmd/object-storage/credentials-group/list/list.go @@ -5,6 +5,7 @@ import ( "encoding/json" "fmt" + "github.com/goccy/go-yaml" "github.com/stackitcloud/stackit-cli/internal/pkg/args" "github.com/stackitcloud/stackit-cli/internal/pkg/errors" "github.com/stackitcloud/stackit-cli/internal/pkg/examples" @@ -129,6 +130,14 @@ func outputResult(p *print.Printer, outputFormat string, credentialsGroups []obj } p.Outputln(string(details)) + return nil + case print.YAMLOutputFormat: + details, err := yaml.MarshalWithOptions(credentialsGroups, yaml.IndentSequence(true)) + if err != nil { + return fmt.Errorf("marshal Object Storage credentials group list: %w", err) + } + p.Outputln(string(details)) + return nil default: table := tables.NewTable() diff --git a/internal/cmd/object-storage/credentials/create/create.go b/internal/cmd/object-storage/credentials/create/create.go index 076448bb2..2d9387cc3 100644 --- a/internal/cmd/object-storage/credentials/create/create.go +++ b/internal/cmd/object-storage/credentials/create/create.go @@ -6,6 +6,7 @@ import ( "fmt" "time" + "github.com/goccy/go-yaml" objectStorageUtils "github.com/stackitcloud/stackit-cli/internal/pkg/services/object-storage/utils" "github.com/spf13/cobra" @@ -145,6 +146,14 @@ func outputResult(p *print.Printer, model *inputModel, credentialsGroupLabel str } p.Outputln(string(details)) + return nil + case print.YAMLOutputFormat: + details, err := yaml.MarshalWithOptions(resp, yaml.IndentSequence(true)) + if err != nil { + return fmt.Errorf("marshal Object Storage credentials: %w", err) + } + p.Outputln(string(details)) + return nil default: expireDate := "Never" diff --git a/internal/cmd/object-storage/credentials/list/list.go b/internal/cmd/object-storage/credentials/list/list.go index ba4a0ec1b..3814a0464 100644 --- a/internal/cmd/object-storage/credentials/list/list.go +++ b/internal/cmd/object-storage/credentials/list/list.go @@ -5,6 +5,7 @@ import ( "encoding/json" "fmt" + "github.com/goccy/go-yaml" objectStorageUtils "github.com/stackitcloud/stackit-cli/internal/pkg/services/object-storage/utils" "github.com/stackitcloud/stackit-cli/internal/pkg/args" @@ -145,6 +146,14 @@ func outputResult(p *print.Printer, outputFormat string, credentials []objectsto } p.Outputln(string(details)) + return nil + case print.YAMLOutputFormat: + details, err := yaml.MarshalWithOptions(credentials, yaml.IndentSequence(true)) + if err != nil { + return fmt.Errorf("marshal Object Storage credentials list: %w", err) + } + p.Outputln(string(details)) + return nil default: table := tables.NewTable() diff --git a/internal/cmd/opensearch/credentials/create/create.go b/internal/cmd/opensearch/credentials/create/create.go index 70a5b8561..eebf71bb8 100644 --- a/internal/cmd/opensearch/credentials/create/create.go +++ b/internal/cmd/opensearch/credentials/create/create.go @@ -5,6 +5,7 @@ import ( "encoding/json" "fmt" + "github.com/goccy/go-yaml" "github.com/stackitcloud/stackit-cli/internal/pkg/args" "github.com/stackitcloud/stackit-cli/internal/pkg/errors" "github.com/stackitcloud/stackit-cli/internal/pkg/examples" @@ -123,17 +124,25 @@ func buildRequest(ctx context.Context, model *inputModel, apiClient *opensearch. } func outputResult(p *print.Printer, model *inputModel, instanceLabel string, resp *opensearch.CredentialsResponse) error { + if !model.ShowPassword { + resp.Raw.Credentials.Password = utils.Ptr("hidden") + } switch model.OutputFormat { case print.JSONOutputFormat: - if !model.ShowPassword { - resp.Raw.Credentials.Password = utils.Ptr("hidden") - } details, err := json.MarshalIndent(resp, "", " ") if err != nil { return fmt.Errorf("marshal OpenSearch credentials: %w", err) } p.Outputln(string(details)) + return nil + case print.YAMLOutputFormat: + details, err := yaml.MarshalWithOptions(resp, yaml.IndentSequence(true)) + if err != nil { + return fmt.Errorf("marshal OpenSearch credentials: %w", err) + } + p.Outputln(string(details)) + return nil default: p.Outputf("Created credentials for instance %q. Credentials ID: %s\n\n", instanceLabel, *resp.Id) // The username field cannot be set by the user so we only display it if it's not returned empty diff --git a/internal/cmd/opensearch/credentials/describe/describe.go b/internal/cmd/opensearch/credentials/describe/describe.go index 73f72e1c5..596ffc3eb 100644 --- a/internal/cmd/opensearch/credentials/describe/describe.go +++ b/internal/cmd/opensearch/credentials/describe/describe.go @@ -5,6 +5,7 @@ import ( "encoding/json" "fmt" + "github.com/goccy/go-yaml" "github.com/stackitcloud/stackit-cli/internal/pkg/args" "github.com/stackitcloud/stackit-cli/internal/pkg/errors" "github.com/stackitcloud/stackit-cli/internal/pkg/examples" @@ -119,6 +120,14 @@ func outputResult(p *print.Printer, outputFormat string, credentials *opensearch } p.Outputln(string(details)) + return nil + case print.YAMLOutputFormat: + details, err := yaml.MarshalWithOptions(credentials, yaml.IndentSequence(true)) + if err != nil { + return fmt.Errorf("marshal OpenSearch credentials: %w", err) + } + p.Outputln(string(details)) + return nil default: table := tables.NewTable() diff --git a/internal/cmd/opensearch/credentials/list/list.go b/internal/cmd/opensearch/credentials/list/list.go index b08c5bea5..e4baea08d 100644 --- a/internal/cmd/opensearch/credentials/list/list.go +++ b/internal/cmd/opensearch/credentials/list/list.go @@ -5,6 +5,7 @@ import ( "encoding/json" "fmt" + "github.com/goccy/go-yaml" "github.com/stackitcloud/stackit-cli/internal/pkg/args" "github.com/stackitcloud/stackit-cli/internal/pkg/errors" "github.com/stackitcloud/stackit-cli/internal/pkg/examples" @@ -142,6 +143,14 @@ func outputResult(p *print.Printer, outputFormat string, credentials []opensearc } p.Outputln(string(details)) + return nil + case print.YAMLOutputFormat: + details, err := yaml.MarshalWithOptions(credentials, yaml.IndentSequence(true)) + if err != nil { + return fmt.Errorf("marshal OpenSearch credentials list: %w", err) + } + p.Outputln(string(details)) + return nil default: table := tables.NewTable() diff --git a/internal/cmd/opensearch/instance/create/create.go b/internal/cmd/opensearch/instance/create/create.go index 09ccfb6b8..0433ce412 100644 --- a/internal/cmd/opensearch/instance/create/create.go +++ b/internal/cmd/opensearch/instance/create/create.go @@ -7,6 +7,7 @@ import ( "fmt" "strings" + "github.com/goccy/go-yaml" "github.com/stackitcloud/stackit-cli/internal/pkg/args" cliErr "github.com/stackitcloud/stackit-cli/internal/pkg/errors" "github.com/stackitcloud/stackit-cli/internal/pkg/examples" @@ -264,6 +265,14 @@ func outputResult(p *print.Printer, model *inputModel, projectLabel, instanceId } p.Outputln(string(details)) + return nil + case print.YAMLOutputFormat: + details, err := yaml.MarshalWithOptions(resp, yaml.IndentSequence(true)) + if err != nil { + return fmt.Errorf("marshal OpenSearch instance: %w", err) + } + p.Outputln(string(details)) + return nil default: operationState := "Created" diff --git a/internal/cmd/opensearch/instance/describe/describe.go b/internal/cmd/opensearch/instance/describe/describe.go index 593b85486..b1f06faff 100644 --- a/internal/cmd/opensearch/instance/describe/describe.go +++ b/internal/cmd/opensearch/instance/describe/describe.go @@ -5,6 +5,7 @@ import ( "encoding/json" "fmt" + "github.com/goccy/go-yaml" "github.com/stackitcloud/stackit-cli/internal/pkg/args" "github.com/stackitcloud/stackit-cli/internal/pkg/errors" "github.com/stackitcloud/stackit-cli/internal/pkg/examples" @@ -107,6 +108,14 @@ func outputResult(p *print.Printer, outputFormat string, instance *opensearch.In } p.Outputln(string(details)) + return nil + case print.YAMLOutputFormat: + details, err := yaml.MarshalWithOptions(instance, yaml.IndentSequence(true)) + if err != nil { + return fmt.Errorf("marshal OpenSearch instance: %w", err) + } + p.Outputln(string(details)) + return nil default: table := tables.NewTable() diff --git a/internal/cmd/opensearch/instance/list/list.go b/internal/cmd/opensearch/instance/list/list.go index dcb7e62a8..a08b7ed1c 100644 --- a/internal/cmd/opensearch/instance/list/list.go +++ b/internal/cmd/opensearch/instance/list/list.go @@ -5,6 +5,7 @@ import ( "encoding/json" "fmt" + "github.com/goccy/go-yaml" "github.com/stackitcloud/stackit-cli/internal/pkg/args" "github.com/stackitcloud/stackit-cli/internal/pkg/errors" "github.com/stackitcloud/stackit-cli/internal/pkg/examples" @@ -137,6 +138,14 @@ func outputResult(p *print.Printer, outputFormat string, instances []opensearch. } p.Outputln(string(details)) + return nil + case print.YAMLOutputFormat: + details, err := yaml.MarshalWithOptions(instances, yaml.IndentSequence(true)) + if err != nil { + return fmt.Errorf("marshal OpenSearch instance list: %w", err) + } + p.Outputln(string(details)) + return nil default: table := tables.NewTable() diff --git a/internal/cmd/opensearch/plans/plans.go b/internal/cmd/opensearch/plans/plans.go index 7e960a2f4..6f8a4ab74 100644 --- a/internal/cmd/opensearch/plans/plans.go +++ b/internal/cmd/opensearch/plans/plans.go @@ -5,6 +5,7 @@ import ( "encoding/json" "fmt" + "github.com/goccy/go-yaml" "github.com/stackitcloud/stackit-cli/internal/pkg/args" "github.com/stackitcloud/stackit-cli/internal/pkg/errors" "github.com/stackitcloud/stackit-cli/internal/pkg/examples" @@ -137,6 +138,14 @@ func outputResult(p *print.Printer, outputFormat string, plans []opensearch.Offe } p.Outputln(string(details)) + return nil + case print.YAMLOutputFormat: + details, err := yaml.MarshalWithOptions(plans, yaml.IndentSequence(true)) + if err != nil { + return fmt.Errorf("marshal OpenSearch plans: %w", err) + } + p.Outputln(string(details)) + return nil default: table := tables.NewTable() diff --git a/internal/cmd/organization/member/list/list.go b/internal/cmd/organization/member/list/list.go index 0091253e9..8b68411cd 100644 --- a/internal/cmd/organization/member/list/list.go +++ b/internal/cmd/organization/member/list/list.go @@ -6,6 +6,7 @@ import ( "fmt" "sort" + "github.com/goccy/go-yaml" "github.com/stackitcloud/stackit-cli/internal/pkg/args" "github.com/stackitcloud/stackit-cli/internal/pkg/errors" "github.com/stackitcloud/stackit-cli/internal/pkg/examples" @@ -164,6 +165,14 @@ func outputResult(p *print.Printer, model *inputModel, members []authorization.M } p.Outputln(string(details)) + return nil + case print.YAMLOutputFormat: + details, err := yaml.MarshalWithOptions(members, yaml.IndentSequence(true)) + if err != nil { + return fmt.Errorf("marshal members: %w", err) + } + p.Outputln(string(details)) + return nil default: table := tables.NewTable() diff --git a/internal/cmd/organization/role/list/list.go b/internal/cmd/organization/role/list/list.go index 6d3a55a9f..68352bff1 100644 --- a/internal/cmd/organization/role/list/list.go +++ b/internal/cmd/organization/role/list/list.go @@ -5,6 +5,7 @@ import ( "encoding/json" "fmt" + "github.com/goccy/go-yaml" "github.com/stackitcloud/stackit-cli/internal/pkg/args" "github.com/stackitcloud/stackit-cli/internal/pkg/errors" "github.com/stackitcloud/stackit-cli/internal/pkg/examples" @@ -137,6 +138,14 @@ func outputRolesResult(p *print.Printer, outputFormat string, roles []authorizat } p.Outputln(string(details)) + return nil + case print.YAMLOutputFormat: + details, err := yaml.MarshalWithOptions(roles, yaml.IndentSequence(true)) + if err != nil { + return fmt.Errorf("marshal roles: %w", err) + } + p.Outputln(string(details)) + return nil default: table := tables.NewTable() diff --git a/internal/cmd/postgresflex/backup/describe/describe.go b/internal/cmd/postgresflex/backup/describe/describe.go index 4e5d8bd49..9e306e2b4 100644 --- a/internal/cmd/postgresflex/backup/describe/describe.go +++ b/internal/cmd/postgresflex/backup/describe/describe.go @@ -7,6 +7,7 @@ import ( "time" + "github.com/goccy/go-yaml" "github.com/inhies/go-bytesize" "github.com/spf13/cobra" "github.com/stackitcloud/stackit-cli/internal/pkg/args" @@ -121,6 +122,14 @@ func outputResult(p *print.Printer, cmd *cobra.Command, outputFormat string, bac } cmd.Println(string(details)) + return nil + case print.YAMLOutputFormat: + details, err := yaml.MarshalWithOptions(backup, yaml.IndentSequence(true)) + if err != nil { + return fmt.Errorf("marshal backup for PostgreSQL Flex backup: %w", err) + } + p.Outputln(string(details)) + return nil default: table := tables.NewTable() diff --git a/internal/cmd/postgresflex/backup/list/list.go b/internal/cmd/postgresflex/backup/list/list.go index b6ff4641b..0ed9a5155 100644 --- a/internal/cmd/postgresflex/backup/list/list.go +++ b/internal/cmd/postgresflex/backup/list/list.go @@ -5,6 +5,7 @@ import ( "encoding/json" "fmt" + "github.com/goccy/go-yaml" "github.com/inhies/go-bytesize" "github.com/stackitcloud/stackit-cli/internal/pkg/args" "github.com/stackitcloud/stackit-cli/internal/pkg/errors" @@ -137,7 +138,15 @@ func outputResult(p *print.Printer, outputFormat string, backups []postgresflex. case print.JSONOutputFormat: details, err := json.MarshalIndent(backups, "", " ") if err != nil { - return fmt.Errorf("marshal PostgreSQL Flex instance list: %w", err) + return fmt.Errorf("marshal PostgreSQL Flex backup list: %w", err) + } + p.Outputln(string(details)) + + return nil + case print.YAMLOutputFormat: + details, err := yaml.MarshalWithOptions(backups, yaml.IndentSequence(true)) + if err != nil { + return fmt.Errorf("marshal PostgreSQL Flex backup list: %w", err) } p.Outputln(string(details)) diff --git a/internal/cmd/postgresflex/instance/clone/clone.go b/internal/cmd/postgresflex/instance/clone/clone.go index 2c9442592..b9d10e78b 100644 --- a/internal/cmd/postgresflex/instance/clone/clone.go +++ b/internal/cmd/postgresflex/instance/clone/clone.go @@ -5,6 +5,7 @@ import ( "encoding/json" "fmt" + "github.com/goccy/go-yaml" "github.com/stackitcloud/stackit-cli/internal/pkg/args" cliErr "github.com/stackitcloud/stackit-cli/internal/pkg/errors" "github.com/stackitcloud/stackit-cli/internal/pkg/examples" @@ -213,6 +214,14 @@ func outputResult(p *print.Printer, model *inputModel, instanceLabel, instanceId } p.Outputln(string(details)) + return nil + case print.YAMLOutputFormat: + details, err := yaml.MarshalWithOptions(resp, yaml.IndentSequence(true)) + if err != nil { + return fmt.Errorf("marshal PostgresFlex instance clone: %w", err) + } + p.Outputln(string(details)) + return nil default: operationState := "Cloned" diff --git a/internal/cmd/postgresflex/instance/create/create.go b/internal/cmd/postgresflex/instance/create/create.go index de826543e..ed7cec878 100644 --- a/internal/cmd/postgresflex/instance/create/create.go +++ b/internal/cmd/postgresflex/instance/create/create.go @@ -6,6 +6,7 @@ import ( "errors" "fmt" + "github.com/goccy/go-yaml" "github.com/stackitcloud/stackit-cli/internal/pkg/args" cliErr "github.com/stackitcloud/stackit-cli/internal/pkg/errors" "github.com/stackitcloud/stackit-cli/internal/pkg/examples" @@ -281,6 +282,14 @@ func outputResult(p *print.Printer, model *inputModel, projectLabel, instanceId } p.Outputln(string(details)) + return nil + case print.YAMLOutputFormat: + details, err := yaml.MarshalWithOptions(resp, yaml.IndentSequence(true)) + if err != nil { + return fmt.Errorf("marshal PostgresFlex instance: %w", err) + } + p.Outputln(string(details)) + return nil default: operationState := "Created" diff --git a/internal/cmd/postgresflex/instance/describe/describe.go b/internal/cmd/postgresflex/instance/describe/describe.go index 0e5687db1..b47dbc025 100644 --- a/internal/cmd/postgresflex/instance/describe/describe.go +++ b/internal/cmd/postgresflex/instance/describe/describe.go @@ -6,6 +6,7 @@ import ( "fmt" "strings" + "github.com/goccy/go-yaml" "github.com/stackitcloud/stackit-cli/internal/pkg/args" "github.com/stackitcloud/stackit-cli/internal/pkg/errors" "github.com/stackitcloud/stackit-cli/internal/pkg/examples" @@ -109,6 +110,14 @@ func outputResult(p *print.Printer, outputFormat string, instance *postgresflex. } p.Outputln(string(details)) + return nil + case print.YAMLOutputFormat: + details, err := yaml.MarshalWithOptions(instance, yaml.IndentSequence(true)) + if err != nil { + return fmt.Errorf("marshal PostgreSQL Flex instance: %w", err) + } + p.Outputln(string(details)) + return nil default: aclsArray := *instance.Acl.Items diff --git a/internal/cmd/postgresflex/instance/list/list.go b/internal/cmd/postgresflex/instance/list/list.go index e0563d02d..3759061ea 100644 --- a/internal/cmd/postgresflex/instance/list/list.go +++ b/internal/cmd/postgresflex/instance/list/list.go @@ -5,6 +5,7 @@ import ( "encoding/json" "fmt" + "github.com/goccy/go-yaml" "github.com/stackitcloud/stackit-cli/internal/pkg/args" "github.com/stackitcloud/stackit-cli/internal/pkg/errors" "github.com/stackitcloud/stackit-cli/internal/pkg/examples" @@ -139,6 +140,14 @@ func outputResult(p *print.Printer, outputFormat string, instances []postgresfle } p.Outputln(string(details)) + return nil + case print.YAMLOutputFormat: + details, err := yaml.MarshalWithOptions(instances, yaml.IndentSequence(true)) + if err != nil { + return fmt.Errorf("marshal PostgreSQL Flex instance list: %w", err) + } + p.Outputln(string(details)) + return nil default: caser := cases.Title(language.English) diff --git a/internal/cmd/postgresflex/instance/update/update.go b/internal/cmd/postgresflex/instance/update/update.go index 8649b4703..975c7c860 100644 --- a/internal/cmd/postgresflex/instance/update/update.go +++ b/internal/cmd/postgresflex/instance/update/update.go @@ -6,6 +6,7 @@ import ( "errors" "fmt" + "github.com/goccy/go-yaml" "github.com/stackitcloud/stackit-cli/internal/pkg/args" cliErr "github.com/stackitcloud/stackit-cli/internal/pkg/errors" "github.com/stackitcloud/stackit-cli/internal/pkg/examples" @@ -315,6 +316,14 @@ func outputResult(p *print.Printer, model *inputModel, instanceLabel string, res } p.Outputln(string(details)) + return nil + case print.YAMLOutputFormat: + details, err := yaml.MarshalWithOptions(resp, yaml.IndentSequence(true)) + if err != nil { + return fmt.Errorf("marshal PostgresFlex instance: %w", err) + } + p.Outputln(string(details)) + return nil default: operationState := "Updated" diff --git a/internal/cmd/postgresflex/options/options.go b/internal/cmd/postgresflex/options/options.go index 7341f4ea3..ba94aea08 100644 --- a/internal/cmd/postgresflex/options/options.go +++ b/internal/cmd/postgresflex/options/options.go @@ -5,6 +5,7 @@ import ( "encoding/json" "fmt" + "github.com/goccy/go-yaml" "github.com/stackitcloud/stackit-cli/internal/pkg/args" "github.com/stackitcloud/stackit-cli/internal/pkg/examples" "github.com/stackitcloud/stackit-cli/internal/pkg/flags" @@ -190,6 +191,14 @@ func outputResult(p *print.Printer, model *inputModel, flavors *postgresflex.Lis return fmt.Errorf("marshal PostgreSQL Flex options: %w", err) } p.Outputln(string(details)) + return nil + case print.YAMLOutputFormat: + details, err := yaml.MarshalWithOptions(options, yaml.IndentSequence(true)) + if err != nil { + return fmt.Errorf("marshal PostgreSQL Flex options: %w", err) + } + p.Outputln(string(details)) + return nil default: return outputResultAsTable(p, model, options) diff --git a/internal/cmd/postgresflex/user/create/create.go b/internal/cmd/postgresflex/user/create/create.go index f72259f84..6a0d1f103 100644 --- a/internal/cmd/postgresflex/user/create/create.go +++ b/internal/cmd/postgresflex/user/create/create.go @@ -5,6 +5,7 @@ import ( "encoding/json" "fmt" + "github.com/goccy/go-yaml" "github.com/stackitcloud/stackit-cli/internal/pkg/args" "github.com/stackitcloud/stackit-cli/internal/pkg/errors" "github.com/stackitcloud/stackit-cli/internal/pkg/examples" @@ -151,6 +152,14 @@ func outputResult(p *print.Printer, model *inputModel, instanceLabel string, res } p.Outputln(string(details)) + return nil + case print.YAMLOutputFormat: + details, err := yaml.MarshalWithOptions(resp, yaml.IndentSequence(true)) + if err != nil { + return fmt.Errorf("marshal PostgresFlex user: %w", err) + } + p.Outputln(string(details)) + return nil default: user := resp.Item diff --git a/internal/cmd/postgresflex/user/describe/describe.go b/internal/cmd/postgresflex/user/describe/describe.go index 5b7b07af6..8f177aa18 100644 --- a/internal/cmd/postgresflex/user/describe/describe.go +++ b/internal/cmd/postgresflex/user/describe/describe.go @@ -5,6 +5,7 @@ import ( "encoding/json" "fmt" + "github.com/goccy/go-yaml" "github.com/stackitcloud/stackit-cli/internal/pkg/args" "github.com/stackitcloud/stackit-cli/internal/pkg/errors" "github.com/stackitcloud/stackit-cli/internal/pkg/examples" @@ -124,6 +125,14 @@ func outputResult(p *print.Printer, outputFormat string, user postgresflex.UserR } p.Outputln(string(details)) + return nil + case print.YAMLOutputFormat: + details, err := yaml.MarshalWithOptions(user, yaml.IndentSequence(true)) + if err != nil { + return fmt.Errorf("marshal PostgreSQL Flex user: %w", err) + } + p.Outputln(string(details)) + return nil default: table := tables.NewTable() diff --git a/internal/cmd/postgresflex/user/list/list.go b/internal/cmd/postgresflex/user/list/list.go index a72f0c9eb..62e354334 100644 --- a/internal/cmd/postgresflex/user/list/list.go +++ b/internal/cmd/postgresflex/user/list/list.go @@ -5,6 +5,7 @@ import ( "encoding/json" "fmt" + "github.com/goccy/go-yaml" "github.com/stackitcloud/stackit-cli/internal/pkg/args" "github.com/stackitcloud/stackit-cli/internal/pkg/errors" "github.com/stackitcloud/stackit-cli/internal/pkg/examples" @@ -145,6 +146,14 @@ func outputResult(p *print.Printer, outputFormat string, users []postgresflex.Li } p.Outputln(string(details)) + return nil + case print.YAMLOutputFormat: + details, err := yaml.MarshalWithOptions(users, yaml.IndentSequence(true)) + if err != nil { + return fmt.Errorf("marshal PostgreSQL Flex user list: %w", err) + } + p.Outputln(string(details)) + return nil default: table := tables.NewTable() diff --git a/internal/cmd/postgresflex/user/reset-password/reset_password.go b/internal/cmd/postgresflex/user/reset-password/reset_password.go index c76c8c400..77c881189 100644 --- a/internal/cmd/postgresflex/user/reset-password/reset_password.go +++ b/internal/cmd/postgresflex/user/reset-password/reset_password.go @@ -5,6 +5,7 @@ import ( "encoding/json" "fmt" + "github.com/goccy/go-yaml" "github.com/stackitcloud/stackit-cli/internal/pkg/args" "github.com/stackitcloud/stackit-cli/internal/pkg/errors" "github.com/stackitcloud/stackit-cli/internal/pkg/examples" @@ -140,6 +141,14 @@ func outputResult(p *print.Printer, model *inputModel, userLabel, instanceLabel } p.Outputln(string(details)) + return nil + case print.YAMLOutputFormat: + details, err := yaml.MarshalWithOptions(user, yaml.IndentSequence(true)) + if err != nil { + return fmt.Errorf("marshal PostgresFlex user: %w", err) + } + p.Outputln(string(details)) + return nil default: p.Outputf("Reset password for user %q of instance %q\n\n", userLabel, instanceLabel) diff --git a/internal/cmd/project/create/create.go b/internal/cmd/project/create/create.go index 09b16313e..59b478584 100644 --- a/internal/cmd/project/create/create.go +++ b/internal/cmd/project/create/create.go @@ -6,6 +6,7 @@ import ( "fmt" "regexp" + "github.com/goccy/go-yaml" "github.com/stackitcloud/stackit-cli/internal/pkg/args" "github.com/stackitcloud/stackit-cli/internal/pkg/auth" "github.com/stackitcloud/stackit-cli/internal/pkg/errors" @@ -197,6 +198,14 @@ func outputResult(p *print.Printer, model *inputModel, resp *resourcemanager.Pro } p.Outputln(string(details)) + return nil + case print.YAMLOutputFormat: + details, err := yaml.MarshalWithOptions(resp, yaml.IndentSequence(true)) + if err != nil { + return fmt.Errorf("marshal project: %w", err) + } + p.Outputln(string(details)) + return nil default: p.Outputf("Created project under the parent with ID %q. Project ID: %s\n", *model.ParentId, *resp.ProjectId) diff --git a/internal/cmd/project/describe/describe.go b/internal/cmd/project/describe/describe.go index 2e1e5e94b..9047fec7a 100644 --- a/internal/cmd/project/describe/describe.go +++ b/internal/cmd/project/describe/describe.go @@ -5,6 +5,7 @@ import ( "encoding/json" "fmt" + "github.com/goccy/go-yaml" "github.com/stackitcloud/stackit-cli/internal/pkg/args" "github.com/stackitcloud/stackit-cli/internal/pkg/examples" "github.com/stackitcloud/stackit-cli/internal/pkg/flags" @@ -126,6 +127,14 @@ func outputResult(p *print.Printer, outputFormat string, project *resourcemanage } p.Outputln(string(details)) + return nil + case print.YAMLOutputFormat: + details, err := yaml.MarshalWithOptions(project, yaml.IndentSequence(true)) + if err != nil { + return fmt.Errorf("marshal project details: %w", err) + } + p.Outputln(string(details)) + return nil default: table := tables.NewTable() diff --git a/internal/cmd/project/list/list.go b/internal/cmd/project/list/list.go index bddf41bdd..73b5bdd9a 100644 --- a/internal/cmd/project/list/list.go +++ b/internal/cmd/project/list/list.go @@ -6,6 +6,7 @@ import ( "fmt" "time" + "github.com/goccy/go-yaml" "github.com/stackitcloud/stackit-cli/internal/pkg/args" "github.com/stackitcloud/stackit-cli/internal/pkg/auth" "github.com/stackitcloud/stackit-cli/internal/pkg/errors" @@ -227,6 +228,14 @@ func outputResult(p *print.Printer, outputFormat string, projects []resourcemana } p.Outputln(string(details)) + return nil + case print.YAMLOutputFormat: + details, err := yaml.MarshalWithOptions(projects, yaml.IndentSequence(true)) + if err != nil { + return fmt.Errorf("marshal projects list: %w", err) + } + p.Outputln(string(details)) + return nil default: table := tables.NewTable() diff --git a/internal/cmd/project/member/list/list.go b/internal/cmd/project/member/list/list.go index ddc2ff08b..6171a0b4e 100644 --- a/internal/cmd/project/member/list/list.go +++ b/internal/cmd/project/member/list/list.go @@ -6,6 +6,7 @@ import ( "fmt" "sort" + "github.com/goccy/go-yaml" "github.com/stackitcloud/stackit-cli/internal/pkg/args" "github.com/stackitcloud/stackit-cli/internal/pkg/errors" "github.com/stackitcloud/stackit-cli/internal/pkg/examples" @@ -166,6 +167,14 @@ func outputResult(p *print.Printer, model *inputModel, members []authorization.M } p.Outputln(string(details)) + return nil + case print.YAMLOutputFormat: + details, err := yaml.MarshalWithOptions(members, yaml.IndentSequence(true)) + if err != nil { + return fmt.Errorf("marshal members: %w", err) + } + p.Outputln(string(details)) + return nil default: table := tables.NewTable() diff --git a/internal/cmd/project/role/list/list.go b/internal/cmd/project/role/list/list.go index d6d3545fd..b00c10b4a 100644 --- a/internal/cmd/project/role/list/list.go +++ b/internal/cmd/project/role/list/list.go @@ -5,6 +5,7 @@ import ( "encoding/json" "fmt" + "github.com/goccy/go-yaml" "github.com/stackitcloud/stackit-cli/internal/pkg/args" "github.com/stackitcloud/stackit-cli/internal/pkg/errors" "github.com/stackitcloud/stackit-cli/internal/pkg/examples" @@ -139,6 +140,14 @@ func outputRolesResult(p *print.Printer, outputFormat string, roles []authorizat } p.Outputln(string(details)) + return nil + case print.YAMLOutputFormat: + details, err := yaml.MarshalWithOptions(roles, yaml.IndentSequence(true)) + if err != nil { + return fmt.Errorf("marshal roles: %w", err) + } + p.Outputln(string(details)) + return nil default: table := tables.NewTable() diff --git a/internal/cmd/rabbitmq/credentials/create/create.go b/internal/cmd/rabbitmq/credentials/create/create.go index 63199b8af..bed0dafd2 100644 --- a/internal/cmd/rabbitmq/credentials/create/create.go +++ b/internal/cmd/rabbitmq/credentials/create/create.go @@ -5,6 +5,8 @@ import ( "encoding/json" "fmt" + "github.com/goccy/go-yaml" + "github.com/spf13/cobra" "github.com/stackitcloud/stackit-cli/internal/pkg/args" "github.com/stackitcloud/stackit-cli/internal/pkg/errors" "github.com/stackitcloud/stackit-cli/internal/pkg/examples" @@ -14,8 +16,6 @@ import ( "github.com/stackitcloud/stackit-cli/internal/pkg/services/rabbitmq/client" rabbitmqUtils "github.com/stackitcloud/stackit-cli/internal/pkg/services/rabbitmq/utils" "github.com/stackitcloud/stackit-cli/internal/pkg/utils" - - "github.com/spf13/cobra" "github.com/stackitcloud/stackit-sdk-go/services/rabbitmq" ) @@ -123,17 +123,25 @@ func buildRequest(ctx context.Context, model *inputModel, apiClient *rabbitmq.AP } func outputResult(p *print.Printer, model *inputModel, instanceLabel string, resp *rabbitmq.CredentialsResponse) error { + if !model.ShowPassword { + resp.Raw.Credentials.Password = utils.Ptr("hidden") + } switch model.OutputFormat { case print.JSONOutputFormat: - if !model.ShowPassword { - resp.Raw.Credentials.Password = utils.Ptr("hidden") - } details, err := json.MarshalIndent(resp, "", " ") if err != nil { return fmt.Errorf("marshal RabbitMQ credentials: %w", err) } p.Outputln(string(details)) + return nil + case print.YAMLOutputFormat: + details, err := yaml.MarshalWithOptions(resp, yaml.IndentSequence(true)) + if err != nil { + return fmt.Errorf("marshal RabbitMQ credentials: %w", err) + } + p.Outputln(string(details)) + return nil default: p.Outputf("Created credentials for instance %q. Credentials ID: %s\n\n", instanceLabel, *resp.Id) diff --git a/internal/cmd/rabbitmq/credentials/describe/describe.go b/internal/cmd/rabbitmq/credentials/describe/describe.go index 9b08752e6..175488d16 100644 --- a/internal/cmd/rabbitmq/credentials/describe/describe.go +++ b/internal/cmd/rabbitmq/credentials/describe/describe.go @@ -5,6 +5,7 @@ import ( "encoding/json" "fmt" + "github.com/goccy/go-yaml" "github.com/stackitcloud/stackit-cli/internal/pkg/args" "github.com/stackitcloud/stackit-cli/internal/pkg/errors" "github.com/stackitcloud/stackit-cli/internal/pkg/examples" @@ -119,6 +120,14 @@ func outputResult(p *print.Printer, outputFormat string, credentials *rabbitmq.C } p.Outputln(string(details)) + return nil + case print.YAMLOutputFormat: + details, err := yaml.MarshalWithOptions(credentials, yaml.IndentSequence(true)) + if err != nil { + return fmt.Errorf("marshal RabbitMQ credentials: %w", err) + } + p.Outputln(string(details)) + return nil default: table := tables.NewTable() diff --git a/internal/cmd/rabbitmq/credentials/list/list.go b/internal/cmd/rabbitmq/credentials/list/list.go index d6ba8fe42..54140ec86 100644 --- a/internal/cmd/rabbitmq/credentials/list/list.go +++ b/internal/cmd/rabbitmq/credentials/list/list.go @@ -5,6 +5,7 @@ import ( "encoding/json" "fmt" + "github.com/goccy/go-yaml" "github.com/stackitcloud/stackit-cli/internal/pkg/args" "github.com/stackitcloud/stackit-cli/internal/pkg/errors" "github.com/stackitcloud/stackit-cli/internal/pkg/examples" @@ -142,6 +143,14 @@ func outputResult(p *print.Printer, outputFormat string, credentials []rabbitmq. } p.Outputln(string(details)) + return nil + case print.YAMLOutputFormat: + details, err := yaml.MarshalWithOptions(credentials, yaml.IndentSequence(true)) + if err != nil { + return fmt.Errorf("marshal RabbitMQ credentials list: %w", err) + } + p.Outputln(string(details)) + return nil default: table := tables.NewTable() diff --git a/internal/cmd/rabbitmq/instance/create/create.go b/internal/cmd/rabbitmq/instance/create/create.go index 9bbd8eaa7..464f587b8 100644 --- a/internal/cmd/rabbitmq/instance/create/create.go +++ b/internal/cmd/rabbitmq/instance/create/create.go @@ -7,6 +7,7 @@ import ( "fmt" "strings" + "github.com/goccy/go-yaml" "github.com/stackitcloud/stackit-cli/internal/pkg/args" cliErr "github.com/stackitcloud/stackit-cli/internal/pkg/errors" "github.com/stackitcloud/stackit-cli/internal/pkg/examples" @@ -264,6 +265,14 @@ func outputResult(p *print.Printer, model *inputModel, projectLabel, instanceId } p.Outputln(string(details)) + return nil + case print.YAMLOutputFormat: + details, err := yaml.MarshalWithOptions(resp, yaml.IndentSequence(true)) + if err != nil { + return fmt.Errorf("marshal RabbitMQ instance: %w", err) + } + p.Outputln(string(details)) + return nil default: operationState := "Created" diff --git a/internal/cmd/rabbitmq/instance/describe/describe.go b/internal/cmd/rabbitmq/instance/describe/describe.go index 65b04e49d..4aeb8fdf8 100644 --- a/internal/cmd/rabbitmq/instance/describe/describe.go +++ b/internal/cmd/rabbitmq/instance/describe/describe.go @@ -5,6 +5,7 @@ import ( "encoding/json" "fmt" + "github.com/goccy/go-yaml" "github.com/stackitcloud/stackit-cli/internal/pkg/args" "github.com/stackitcloud/stackit-cli/internal/pkg/errors" "github.com/stackitcloud/stackit-cli/internal/pkg/examples" @@ -107,6 +108,14 @@ func outputResult(p *print.Printer, outputFormat string, instance *rabbitmq.Inst } p.Outputln(string(details)) + return nil + case print.YAMLOutputFormat: + details, err := yaml.MarshalWithOptions(instance, yaml.IndentSequence(true)) + if err != nil { + return fmt.Errorf("marshal RabbitMQ instance: %w", err) + } + p.Outputln(string(details)) + return nil default: table := tables.NewTable() diff --git a/internal/cmd/rabbitmq/instance/list/list.go b/internal/cmd/rabbitmq/instance/list/list.go index 0e1daafe1..b09ebd083 100644 --- a/internal/cmd/rabbitmq/instance/list/list.go +++ b/internal/cmd/rabbitmq/instance/list/list.go @@ -5,6 +5,7 @@ import ( "encoding/json" "fmt" + "github.com/goccy/go-yaml" "github.com/stackitcloud/stackit-cli/internal/pkg/args" "github.com/stackitcloud/stackit-cli/internal/pkg/errors" "github.com/stackitcloud/stackit-cli/internal/pkg/examples" @@ -137,6 +138,14 @@ func outputResult(p *print.Printer, outputFormat string, instances []rabbitmq.In } p.Outputln(string(details)) + return nil + case print.YAMLOutputFormat: + details, err := yaml.MarshalWithOptions(instances, yaml.IndentSequence(true)) + if err != nil { + return fmt.Errorf("marshal RabbitMQ instance list: %w", err) + } + p.Outputln(string(details)) + return nil default: table := tables.NewTable() diff --git a/internal/cmd/rabbitmq/plans/plans.go b/internal/cmd/rabbitmq/plans/plans.go index 6f7f8be47..ad19ecf18 100644 --- a/internal/cmd/rabbitmq/plans/plans.go +++ b/internal/cmd/rabbitmq/plans/plans.go @@ -5,6 +5,7 @@ import ( "encoding/json" "fmt" + "github.com/goccy/go-yaml" "github.com/stackitcloud/stackit-cli/internal/pkg/args" "github.com/stackitcloud/stackit-cli/internal/pkg/errors" "github.com/stackitcloud/stackit-cli/internal/pkg/examples" @@ -137,6 +138,14 @@ func outputResult(p *print.Printer, outputFormat string, plans []rabbitmq.Offeri } p.Outputln(string(details)) + return nil + case print.YAMLOutputFormat: + details, err := yaml.MarshalWithOptions(plans, yaml.IndentSequence(true)) + if err != nil { + return fmt.Errorf("marshal RabbitMQ plans: %w", err) + } + p.Outputln(string(details)) + return nil default: table := tables.NewTable() diff --git a/internal/cmd/redis/credentials/create/create.go b/internal/cmd/redis/credentials/create/create.go index 508d24130..7906c2885 100644 --- a/internal/cmd/redis/credentials/create/create.go +++ b/internal/cmd/redis/credentials/create/create.go @@ -5,6 +5,7 @@ import ( "encoding/json" "fmt" + "github.com/goccy/go-yaml" "github.com/stackitcloud/stackit-cli/internal/pkg/args" "github.com/stackitcloud/stackit-cli/internal/pkg/errors" "github.com/stackitcloud/stackit-cli/internal/pkg/examples" @@ -123,21 +124,30 @@ func buildRequest(ctx context.Context, model *inputModel, apiClient *redis.APICl } func outputResult(p *print.Printer, model *inputModel, instanceLabel string, resp *redis.CredentialsResponse) error { + if !model.ShowPassword { + resp.Raw.Credentials.Password = utils.Ptr("hidden") + } + switch model.OutputFormat { case print.JSONOutputFormat: - if !model.ShowPassword { - resp.Raw.Credentials.Password = utils.Ptr("hidden") - } details, err := json.MarshalIndent(resp, "", " ") if err != nil { return fmt.Errorf("marshal Redis credentials: %w", err) } p.Outputln(string(details)) + return nil + case print.YAMLOutputFormat: + details, err := yaml.MarshalWithOptions(resp, yaml.IndentSequence(true)) + if err != nil { + return fmt.Errorf("marshal Redis credentials: %w", err) + } + p.Outputln(string(details)) + return nil default: p.Outputf("Created credentials for instance %q. Credentials ID: %s\n\n", instanceLabel, *resp.Id) - // The username field cannot be set by the user so we only display it if it's not returned empty + // The username field cannot be set by the user, so we only display it if it's not returned empty username := *resp.Raw.Credentials.Username if username != "" { p.Outputf("Username: %s\n", *resp.Raw.Credentials.Username) diff --git a/internal/cmd/redis/credentials/describe/describe.go b/internal/cmd/redis/credentials/describe/describe.go index a6831e741..48ec11a7c 100644 --- a/internal/cmd/redis/credentials/describe/describe.go +++ b/internal/cmd/redis/credentials/describe/describe.go @@ -5,6 +5,7 @@ import ( "encoding/json" "fmt" + "github.com/goccy/go-yaml" "github.com/stackitcloud/stackit-cli/internal/pkg/args" "github.com/stackitcloud/stackit-cli/internal/pkg/errors" "github.com/stackitcloud/stackit-cli/internal/pkg/examples" @@ -119,6 +120,14 @@ func outputResult(p *print.Printer, outputFormat string, credentials *redis.Cred } p.Outputln(string(details)) + return nil + case print.YAMLOutputFormat: + details, err := yaml.MarshalWithOptions(credentials, yaml.IndentSequence(true)) + if err != nil { + return fmt.Errorf("marshal Redis credentials: %w", err) + } + p.Outputln(string(details)) + return nil default: table := tables.NewTable() diff --git a/internal/cmd/redis/credentials/list/list.go b/internal/cmd/redis/credentials/list/list.go index eeb98d7b4..b701a03d8 100644 --- a/internal/cmd/redis/credentials/list/list.go +++ b/internal/cmd/redis/credentials/list/list.go @@ -5,6 +5,7 @@ import ( "encoding/json" "fmt" + "github.com/goccy/go-yaml" "github.com/stackitcloud/stackit-cli/internal/pkg/args" "github.com/stackitcloud/stackit-cli/internal/pkg/errors" "github.com/stackitcloud/stackit-cli/internal/pkg/examples" @@ -142,6 +143,14 @@ func outputResult(p *print.Printer, outputFormat string, credentials []redis.Cre } p.Outputln(string(details)) + return nil + case print.YAMLOutputFormat: + details, err := yaml.MarshalWithOptions(credentials, yaml.IndentSequence(true)) + if err != nil { + return fmt.Errorf("marshal Redis credentials list: %w", err) + } + p.Outputln(string(details)) + return nil default: table := tables.NewTable() diff --git a/internal/cmd/redis/instance/create/create.go b/internal/cmd/redis/instance/create/create.go index d71d03314..cd71a84a4 100644 --- a/internal/cmd/redis/instance/create/create.go +++ b/internal/cmd/redis/instance/create/create.go @@ -7,6 +7,7 @@ import ( "fmt" "strings" + "github.com/goccy/go-yaml" "github.com/stackitcloud/stackit-cli/internal/pkg/args" cliErr "github.com/stackitcloud/stackit-cli/internal/pkg/errors" "github.com/stackitcloud/stackit-cli/internal/pkg/examples" @@ -259,6 +260,14 @@ func outputResult(p *print.Printer, model *inputModel, projectLabel, instanceId } p.Outputln(string(details)) + return nil + case print.YAMLOutputFormat: + details, err := yaml.MarshalWithOptions(resp, yaml.IndentSequence(true)) + if err != nil { + return fmt.Errorf("marshal Redis instance: %w", err) + } + p.Outputln(string(details)) + return nil default: operationState := "Created" diff --git a/internal/cmd/redis/instance/describe/describe.go b/internal/cmd/redis/instance/describe/describe.go index eb832bf38..b89ac51f1 100644 --- a/internal/cmd/redis/instance/describe/describe.go +++ b/internal/cmd/redis/instance/describe/describe.go @@ -5,6 +5,7 @@ import ( "encoding/json" "fmt" + "github.com/goccy/go-yaml" "github.com/stackitcloud/stackit-cli/internal/pkg/args" "github.com/stackitcloud/stackit-cli/internal/pkg/errors" "github.com/stackitcloud/stackit-cli/internal/pkg/examples" @@ -107,6 +108,14 @@ func outputResult(p *print.Printer, outputFormat string, instance *redis.Instanc } p.Outputln(string(details)) + return nil + case print.YAMLOutputFormat: + details, err := yaml.MarshalWithOptions(instance, yaml.IndentSequence(true)) + if err != nil { + return fmt.Errorf("marshal Redis instance: %w", err) + } + p.Outputln(string(details)) + return nil default: table := tables.NewTable() diff --git a/internal/cmd/redis/instance/list/list.go b/internal/cmd/redis/instance/list/list.go index 8d3af2136..969b20058 100644 --- a/internal/cmd/redis/instance/list/list.go +++ b/internal/cmd/redis/instance/list/list.go @@ -5,6 +5,7 @@ import ( "encoding/json" "fmt" + "github.com/goccy/go-yaml" "github.com/stackitcloud/stackit-cli/internal/pkg/args" "github.com/stackitcloud/stackit-cli/internal/pkg/errors" "github.com/stackitcloud/stackit-cli/internal/pkg/examples" @@ -137,6 +138,14 @@ func outputResult(p *print.Printer, outputFormat string, instances []redis.Insta } p.Outputln(string(details)) + return nil + case print.YAMLOutputFormat: + details, err := yaml.MarshalWithOptions(instances, yaml.IndentSequence(true)) + if err != nil { + return fmt.Errorf("marshal Redis instance list: %w", err) + } + p.Outputln(string(details)) + return nil default: table := tables.NewTable() diff --git a/internal/cmd/redis/plans/plans.go b/internal/cmd/redis/plans/plans.go index 9f8968a3d..292857121 100644 --- a/internal/cmd/redis/plans/plans.go +++ b/internal/cmd/redis/plans/plans.go @@ -5,6 +5,7 @@ import ( "encoding/json" "fmt" + "github.com/goccy/go-yaml" "github.com/stackitcloud/stackit-cli/internal/pkg/args" "github.com/stackitcloud/stackit-cli/internal/pkg/errors" "github.com/stackitcloud/stackit-cli/internal/pkg/examples" @@ -137,6 +138,14 @@ func outputResult(p *print.Printer, outputFormat string, plans []redis.Offering) } p.Outputln(string(details)) + return nil + case print.YAMLOutputFormat: + details, err := yaml.MarshalWithOptions(plans, yaml.IndentSequence(true)) + if err != nil { + return fmt.Errorf("marshal Redis plans: %w", err) + } + p.Outputln(string(details)) + return nil default: table := tables.NewTable() diff --git a/internal/cmd/secrets-manager/instance/create/create.go b/internal/cmd/secrets-manager/instance/create/create.go index 11254c194..c95cb365f 100644 --- a/internal/cmd/secrets-manager/instance/create/create.go +++ b/internal/cmd/secrets-manager/instance/create/create.go @@ -5,6 +5,7 @@ import ( "encoding/json" "fmt" + "github.com/goccy/go-yaml" "github.com/stackitcloud/stackit-cli/internal/pkg/args" cliErr "github.com/stackitcloud/stackit-cli/internal/pkg/errors" "github.com/stackitcloud/stackit-cli/internal/pkg/examples" @@ -165,6 +166,14 @@ func outputResult(p *print.Printer, model *inputModel, projectLabel, instanceId } p.Outputln(string(details)) + return nil + case print.YAMLOutputFormat: + details, err := yaml.MarshalWithOptions(resp, yaml.IndentSequence(true)) + if err != nil { + return fmt.Errorf("marshal Secrets Manager instance: %w", err) + } + p.Outputln(string(details)) + return nil default: p.Outputf("Created instance for project %q. Instance ID: %s\n", projectLabel, instanceId) diff --git a/internal/cmd/secrets-manager/instance/describe/describe.go b/internal/cmd/secrets-manager/instance/describe/describe.go index b484bdea6..d3f6e56b9 100644 --- a/internal/cmd/secrets-manager/instance/describe/describe.go +++ b/internal/cmd/secrets-manager/instance/describe/describe.go @@ -6,6 +6,7 @@ import ( "fmt" "strings" + "github.com/goccy/go-yaml" "github.com/stackitcloud/stackit-cli/internal/pkg/args" "github.com/stackitcloud/stackit-cli/internal/pkg/errors" "github.com/stackitcloud/stackit-cli/internal/pkg/examples" @@ -110,19 +111,27 @@ func buildListACLsRequest(ctx context.Context, model *inputModel, apiClient *sec } func outputResult(p *print.Printer, outputFormat string, instance *secretsmanager.Instance, aclList *secretsmanager.AclList) error { + output := struct { + *secretsmanager.Instance + *secretsmanager.AclList + }{instance, aclList} + switch outputFormat { case print.JSONOutputFormat: - output := struct { - *secretsmanager.Instance - *secretsmanager.AclList - }{instance, aclList} - details, err := json.MarshalIndent(output, "", " ") if err != nil { return fmt.Errorf("marshal Secrets Manager instance: %w", err) } p.Outputln(string(details)) + return nil + case print.YAMLOutputFormat: + details, err := yaml.MarshalWithOptions(output, yaml.IndentSequence(true)) + if err != nil { + return fmt.Errorf("marshal Secrets Manager instance: %w", err) + } + p.Outputln(string(details)) + return nil default: table := tables.NewTable() diff --git a/internal/cmd/secrets-manager/instance/list/list.go b/internal/cmd/secrets-manager/instance/list/list.go index a24611412..9aa3a6a33 100644 --- a/internal/cmd/secrets-manager/instance/list/list.go +++ b/internal/cmd/secrets-manager/instance/list/list.go @@ -5,6 +5,7 @@ import ( "encoding/json" "fmt" + "github.com/goccy/go-yaml" "github.com/stackitcloud/stackit-cli/internal/pkg/args" "github.com/stackitcloud/stackit-cli/internal/pkg/errors" "github.com/stackitcloud/stackit-cli/internal/pkg/examples" @@ -138,6 +139,14 @@ func outputResult(p *print.Printer, outputFormat string, instances []secretsmana } p.Outputln(string(details)) + return nil + case print.YAMLOutputFormat: + details, err := yaml.MarshalWithOptions(instances, yaml.IndentSequence(true)) + if err != nil { + return fmt.Errorf("marshal Secrets Manager instance list: %w", err) + } + p.Outputln(string(details)) + return nil default: table := tables.NewTable() diff --git a/internal/cmd/secrets-manager/user/create/create.go b/internal/cmd/secrets-manager/user/create/create.go index 8bb5120c9..ac919e456 100644 --- a/internal/cmd/secrets-manager/user/create/create.go +++ b/internal/cmd/secrets-manager/user/create/create.go @@ -5,6 +5,7 @@ import ( "encoding/json" "fmt" + "github.com/goccy/go-yaml" "github.com/stackitcloud/stackit-cli/internal/pkg/args" "github.com/stackitcloud/stackit-cli/internal/pkg/errors" "github.com/stackitcloud/stackit-cli/internal/pkg/examples" @@ -145,6 +146,14 @@ func outputResult(p *print.Printer, model *inputModel, instanceLabel string, res } p.Outputln(string(details)) + return nil + case print.YAMLOutputFormat: + details, err := yaml.MarshalWithOptions(resp, yaml.IndentSequence(true)) + if err != nil { + return fmt.Errorf("marshal Secrets Manager user: %w", err) + } + p.Outputln(string(details)) + return nil default: p.Outputf("Created user for instance %q. User ID: %s\n\n", instanceLabel, *resp.Id) diff --git a/internal/cmd/secrets-manager/user/describe/describe.go b/internal/cmd/secrets-manager/user/describe/describe.go index c3b0c0b96..35b8f927f 100644 --- a/internal/cmd/secrets-manager/user/describe/describe.go +++ b/internal/cmd/secrets-manager/user/describe/describe.go @@ -5,6 +5,7 @@ import ( "encoding/json" "fmt" + "github.com/goccy/go-yaml" "github.com/stackitcloud/stackit-cli/internal/pkg/args" "github.com/stackitcloud/stackit-cli/internal/pkg/errors" "github.com/stackitcloud/stackit-cli/internal/pkg/examples" @@ -121,6 +122,14 @@ func outputResult(p *print.Printer, outputFormat string, user secretsmanager.Use } p.Outputln(string(details)) + return nil + case print.YAMLOutputFormat: + details, err := yaml.MarshalWithOptions(user, yaml.IndentSequence(true)) + if err != nil { + return fmt.Errorf("marshal Secrets Manager user: %w", err) + } + p.Outputln(string(details)) + return nil default: table := tables.NewTable() diff --git a/internal/cmd/secrets-manager/user/list/list.go b/internal/cmd/secrets-manager/user/list/list.go index 4d9d37fba..cfc9b1a87 100644 --- a/internal/cmd/secrets-manager/user/list/list.go +++ b/internal/cmd/secrets-manager/user/list/list.go @@ -5,6 +5,7 @@ import ( "encoding/json" "fmt" + "github.com/goccy/go-yaml" "github.com/stackitcloud/stackit-cli/internal/pkg/args" "github.com/stackitcloud/stackit-cli/internal/pkg/errors" "github.com/stackitcloud/stackit-cli/internal/pkg/examples" @@ -145,6 +146,14 @@ func outputResult(p *print.Printer, outputFormat string, users []secretsmanager. } p.Outputln(string(details)) + return nil + case print.YAMLOutputFormat: + details, err := yaml.MarshalWithOptions(users, yaml.IndentSequence(true)) + if err != nil { + return fmt.Errorf("marshal Secrets Manager user list: %w", err) + } + p.Outputln(string(details)) + return nil default: table := tables.NewTable() diff --git a/internal/cmd/service-account/create/create.go b/internal/cmd/service-account/create/create.go index f776fb483..5314543b8 100644 --- a/internal/cmd/service-account/create/create.go +++ b/internal/cmd/service-account/create/create.go @@ -5,6 +5,7 @@ import ( "encoding/json" "fmt" + "github.com/goccy/go-yaml" "github.com/stackitcloud/stackit-cli/internal/pkg/args" "github.com/stackitcloud/stackit-cli/internal/pkg/errors" "github.com/stackitcloud/stackit-cli/internal/pkg/examples" @@ -126,6 +127,14 @@ func outputResult(p *print.Printer, model *inputModel, projectLabel string, serv } p.Outputln(string(details)) + return nil + case print.YAMLOutputFormat: + details, err := yaml.MarshalWithOptions(serviceAccount, yaml.IndentSequence(true)) + if err != nil { + return fmt.Errorf("marshal service account: %w", err) + } + p.Outputln(string(details)) + return nil default: p.Outputf("Created service account for project %q. Email: %s\n", projectLabel, *serviceAccount.Email) diff --git a/internal/cmd/service-account/key/list/list.go b/internal/cmd/service-account/key/list/list.go index c9db67f72..6c3c29da5 100644 --- a/internal/cmd/service-account/key/list/list.go +++ b/internal/cmd/service-account/key/list/list.go @@ -5,6 +5,7 @@ import ( "encoding/json" "fmt" + "github.com/goccy/go-yaml" "github.com/stackitcloud/stackit-cli/internal/pkg/args" "github.com/stackitcloud/stackit-cli/internal/pkg/errors" "github.com/stackitcloud/stackit-cli/internal/pkg/examples" @@ -147,6 +148,14 @@ func outputResult(p *print.Printer, outputFormat string, keys []serviceaccount.S } p.Outputln(string(details)) + return nil + case print.YAMLOutputFormat: + details, err := yaml.MarshalWithOptions(keys, yaml.IndentSequence(true)) + if err != nil { + return fmt.Errorf("marshal keys metadata: %w", err) + } + p.Outputln(string(details)) + return nil default: table := tables.NewTable() diff --git a/internal/cmd/service-account/list/list.go b/internal/cmd/service-account/list/list.go index 9fd2fa0eb..7fc9ffbff 100644 --- a/internal/cmd/service-account/list/list.go +++ b/internal/cmd/service-account/list/list.go @@ -5,6 +5,7 @@ import ( "encoding/json" "fmt" + "github.com/goccy/go-yaml" "github.com/stackitcloud/stackit-cli/internal/pkg/args" "github.com/stackitcloud/stackit-cli/internal/pkg/errors" "github.com/stackitcloud/stackit-cli/internal/pkg/examples" @@ -130,6 +131,12 @@ func outputResult(p *print.Printer, outputFormat string, serviceAccounts []servi return fmt.Errorf("marshal service accounts list: %w", err) } p.Outputln(string(details)) + case print.YAMLOutputFormat: + details, err := yaml.MarshalWithOptions(serviceAccounts, yaml.IndentSequence(true)) + if err != nil { + return fmt.Errorf("marshal service accounts list: %w", err) + } + p.Outputln(string(details)) default: table := tables.NewTable() table.SetHeader("ID", "EMAIL") diff --git a/internal/cmd/service-account/token/create/create.go b/internal/cmd/service-account/token/create/create.go index e14891b11..fdaf09b01 100644 --- a/internal/cmd/service-account/token/create/create.go +++ b/internal/cmd/service-account/token/create/create.go @@ -5,6 +5,7 @@ import ( "encoding/json" "fmt" + "github.com/goccy/go-yaml" "github.com/stackitcloud/stackit-cli/internal/pkg/args" "github.com/stackitcloud/stackit-cli/internal/pkg/errors" "github.com/stackitcloud/stackit-cli/internal/pkg/examples" @@ -150,6 +151,14 @@ func outputResult(p *print.Printer, model *inputModel, token *serviceaccount.Acc } p.Outputln(string(details)) + return nil + case print.YAMLOutputFormat: + details, err := yaml.MarshalWithOptions(token, yaml.IndentSequence(true)) + if err != nil { + return fmt.Errorf("marshal service account access token: %w", err) + } + p.Outputln(string(details)) + return nil default: p.Outputf("Created access token for service account %s. Token ID: %s\n\n", model.ServiceAccountEmail, *token.Id) diff --git a/internal/cmd/service-account/token/list/list.go b/internal/cmd/service-account/token/list/list.go index 257c61f22..fc345d8cc 100644 --- a/internal/cmd/service-account/token/list/list.go +++ b/internal/cmd/service-account/token/list/list.go @@ -5,6 +5,7 @@ import ( "encoding/json" "fmt" + "github.com/goccy/go-yaml" "github.com/stackitcloud/stackit-cli/internal/pkg/args" "github.com/stackitcloud/stackit-cli/internal/pkg/errors" "github.com/stackitcloud/stackit-cli/internal/pkg/examples" @@ -151,6 +152,14 @@ func outputResult(p *print.Printer, outputFormat string, tokensMetadata []servic } p.Outputln(string(details)) + return nil + case print.YAMLOutputFormat: + details, err := yaml.MarshalWithOptions(tokensMetadata, yaml.IndentSequence(true)) + if err != nil { + return fmt.Errorf("marshal tokens metadata: %w", err) + } + p.Outputln(string(details)) + return nil default: table := tables.NewTable() diff --git a/internal/cmd/ske/cluster/create/create.go b/internal/cmd/ske/cluster/create/create.go index c253e85c8..4a9324f3d 100644 --- a/internal/cmd/ske/cluster/create/create.go +++ b/internal/cmd/ske/cluster/create/create.go @@ -5,6 +5,7 @@ import ( "encoding/json" "fmt" + "github.com/goccy/go-yaml" "github.com/stackitcloud/stackit-cli/internal/pkg/args" "github.com/stackitcloud/stackit-cli/internal/pkg/errors" "github.com/stackitcloud/stackit-cli/internal/pkg/examples" @@ -195,6 +196,14 @@ func outputResult(p *print.Printer, model *inputModel, projectLabel string, resp } p.Outputln(string(details)) + return nil + case print.YAMLOutputFormat: + details, err := yaml.MarshalWithOptions(resp, yaml.IndentSequence(true)) + if err != nil { + return fmt.Errorf("marshal SKE cluster: %w", err) + } + p.Outputln(string(details)) + return nil default: operationState := "Created" diff --git a/internal/cmd/ske/cluster/describe/describe.go b/internal/cmd/ske/cluster/describe/describe.go index 1d24f4563..75a432bdb 100644 --- a/internal/cmd/ske/cluster/describe/describe.go +++ b/internal/cmd/ske/cluster/describe/describe.go @@ -5,6 +5,7 @@ import ( "encoding/json" "fmt" + "github.com/goccy/go-yaml" "github.com/stackitcloud/stackit-cli/internal/pkg/args" "github.com/stackitcloud/stackit-cli/internal/pkg/errors" "github.com/stackitcloud/stackit-cli/internal/pkg/examples" @@ -104,6 +105,14 @@ func outputResult(p *print.Printer, outputFormat string, cluster *ske.Cluster) e } p.Outputln(string(details)) + return nil + case print.YAMLOutputFormat: + details, err := yaml.MarshalWithOptions(cluster, yaml.IndentSequence(true)) + if err != nil { + return fmt.Errorf("marshal SKE cluster: %w", err) + } + p.Outputln(string(details)) + return nil default: acl := []string{} diff --git a/internal/cmd/ske/cluster/list/list.go b/internal/cmd/ske/cluster/list/list.go index 113997c80..d0ee6da45 100644 --- a/internal/cmd/ske/cluster/list/list.go +++ b/internal/cmd/ske/cluster/list/list.go @@ -5,6 +5,7 @@ import ( "encoding/json" "fmt" + "github.com/goccy/go-yaml" "github.com/stackitcloud/stackit-cli/internal/pkg/args" "github.com/stackitcloud/stackit-cli/internal/pkg/errors" "github.com/stackitcloud/stackit-cli/internal/pkg/examples" @@ -147,6 +148,14 @@ func outputResult(p *print.Printer, outputFormat string, clusters []ske.Cluster) } p.Outputln(string(details)) + return nil + case print.YAMLOutputFormat: + details, err := yaml.MarshalWithOptions(clusters, yaml.IndentSequence(true)) + if err != nil { + return fmt.Errorf("marshal SKE cluster list: %w", err) + } + p.Outputln(string(details)) + return nil default: table := tables.NewTable() diff --git a/internal/cmd/ske/cluster/update/update.go b/internal/cmd/ske/cluster/update/update.go index 264bd263b..491daf86e 100644 --- a/internal/cmd/ske/cluster/update/update.go +++ b/internal/cmd/ske/cluster/update/update.go @@ -5,6 +5,7 @@ import ( "encoding/json" "fmt" + "github.com/goccy/go-yaml" "github.com/stackitcloud/stackit-cli/internal/pkg/args" "github.com/stackitcloud/stackit-cli/internal/pkg/errors" "github.com/stackitcloud/stackit-cli/internal/pkg/examples" @@ -167,6 +168,14 @@ func outputResult(p *print.Printer, model *inputModel, resp *ske.Cluster) error } p.Outputln(string(details)) + return nil + case print.YAMLOutputFormat: + details, err := yaml.MarshalWithOptions(resp, yaml.IndentSequence(true)) + if err != nil { + return fmt.Errorf("marshal SKE cluster: %w", err) + } + p.Outputln(string(details)) + return nil default: operationState := "Updated" diff --git a/internal/cmd/ske/credentials/describe/describe.go b/internal/cmd/ske/credentials/describe/describe.go index cb7233bcd..692bb9ace 100644 --- a/internal/cmd/ske/credentials/describe/describe.go +++ b/internal/cmd/ske/credentials/describe/describe.go @@ -5,6 +5,7 @@ import ( "encoding/json" "fmt" + "github.com/goccy/go-yaml" "github.com/stackitcloud/stackit-cli/internal/pkg/args" "github.com/stackitcloud/stackit-cli/internal/pkg/errors" "github.com/stackitcloud/stackit-cli/internal/pkg/examples" @@ -121,6 +122,14 @@ func outputResult(p *print.Printer, outputFormat string, credentials *ske.Creden } p.Outputln(string(details)) + return nil + case print.YAMLOutputFormat: + details, err := yaml.MarshalWithOptions(credentials, yaml.IndentSequence(true)) + if err != nil { + return fmt.Errorf("marshal SKE credentials: %w", err) + } + p.Outputln(string(details)) + return nil default: table := tables.NewTable() diff --git a/internal/cmd/ske/describe/describe.go b/internal/cmd/ske/describe/describe.go index 4b83c1306..433a60cd3 100644 --- a/internal/cmd/ske/describe/describe.go +++ b/internal/cmd/ske/describe/describe.go @@ -5,6 +5,7 @@ import ( "encoding/json" "fmt" + "github.com/goccy/go-yaml" "github.com/stackitcloud/stackit-cli/internal/pkg/args" "github.com/stackitcloud/stackit-cli/internal/pkg/errors" "github.com/stackitcloud/stackit-cli/internal/pkg/examples" @@ -93,6 +94,14 @@ func outputResult(p *print.Printer, outputFormat string, project *ske.ProjectRes } p.Outputln(string(details)) + return nil + case print.YAMLOutputFormat: + details, err := yaml.MarshalWithOptions(project, yaml.IndentSequence(true)) + if err != nil { + return fmt.Errorf("marshal SKE project details: %w", err) + } + p.Outputln(string(details)) + return nil default: table := tables.NewTable() diff --git a/internal/cmd/ske/kubeconfig/create/create.go b/internal/cmd/ske/kubeconfig/create/create.go index c0b2d7a65..d72c54b9c 100644 --- a/internal/cmd/ske/kubeconfig/create/create.go +++ b/internal/cmd/ske/kubeconfig/create/create.go @@ -5,6 +5,7 @@ import ( "encoding/json" "fmt" + "github.com/goccy/go-yaml" "github.com/stackitcloud/stackit-cli/internal/pkg/args" "github.com/stackitcloud/stackit-cli/internal/pkg/errors" "github.com/stackitcloud/stackit-cli/internal/pkg/examples" @@ -21,6 +22,7 @@ import ( const ( clusterNameArg = "CLUSTER_NAME" + loginFlag = "login" expirationFlag = "expiration" filepathFlag = "filepath" ) @@ -30,6 +32,7 @@ type inputModel struct { ClusterName string Filepath *string ExpirationTime *string + Login bool } func NewCmd(p *print.Printer) *cobra.Command { @@ -47,6 +50,10 @@ func NewCmd(p *print.Printer) *cobra.Command { examples.NewExample( `Create a kubeconfig for the SKE cluster with name "my-cluster"`, "$ stackit ske kubeconfig create my-cluster"), + examples.NewExample( + `Get a login kubeconfig for the SKE cluster with name "my-cluster". `+ + "This kubeconfig does not contain any credentials and instead obtains valid credentials via the `stackit ske kubeconfig login` command.", + "$ stackit ske kubeconfig create my-cluster --login"), examples.NewExample( `Create a kubeconfig for the SKE cluster with name "my-cluster" and set the expiration time to 30 days`, "$ stackit ske kubeconfig create my-cluster --expiration 30d"), @@ -79,20 +86,41 @@ func NewCmd(p *print.Printer) *cobra.Command { } // Call API - req, err := buildRequest(ctx, model, apiClient) - if err != nil { - return fmt.Errorf("build kubeconfig create request: %w", err) - } - resp, err := req.Execute() - if err != nil { - return fmt.Errorf("create kubeconfig for SKE cluster: %w", err) + var ( + kubeconfig string + respKubeconfig *ske.Kubeconfig + respLogin *ske.V1LoginKubeconfig + ) + + if !model.Login { + req, err := buildRequestCreate(ctx, model, apiClient) + if err != nil { + return fmt.Errorf("build kubeconfig create request: %w", err) + } + respKubeconfig, err = req.Execute() + if err != nil { + return fmt.Errorf("create kubeconfig for SKE cluster: %w", err) + } + if respKubeconfig.Kubeconfig == nil { + return fmt.Errorf("no kubeconfig returned from the API") + } + kubeconfig = *respKubeconfig.Kubeconfig + } else { + req, err := buildRequestLogin(ctx, model, apiClient) + if err != nil { + return fmt.Errorf("build login kubeconfig create request: %w", err) + } + respLogin, err = req.Execute() + if err != nil { + return fmt.Errorf("create login kubeconfig for SKE cluster: %w", err) + } + if respLogin.Kubeconfig == nil { + return fmt.Errorf("no login kubeconfig returned from the API") + } + kubeconfig = *respLogin.Kubeconfig } // Create the config file - if resp.Kubeconfig == nil { - return fmt.Errorf("no kubeconfig returned from the API") - } - var kubeconfigPath string if model.Filepath == nil { kubeconfigPath, err = skeUtils.GetDefaultKubeconfigPath() @@ -103,12 +131,14 @@ func NewCmd(p *print.Printer) *cobra.Command { kubeconfigPath = *model.Filepath } - err = skeUtils.WriteConfigFile(kubeconfigPath, *resp.Kubeconfig) - if err != nil { - return fmt.Errorf("write kubeconfig file: %w", err) + if model.OutputFormat != print.JSONOutputFormat { + err = skeUtils.WriteConfigFile(kubeconfigPath, kubeconfig) + if err != nil { + return fmt.Errorf("write kubeconfig file: %w", err) + } } - return outputResult(p, model, kubeconfigPath, resp) + return outputResult(p, model, kubeconfigPath, respKubeconfig, respLogin) }, } configureFlags(cmd) @@ -116,8 +146,11 @@ func NewCmd(p *print.Printer) *cobra.Command { } func configureFlags(cmd *cobra.Command) { + cmd.Flags().BoolP(loginFlag, "l", false, "Create a login kubeconfig that obtains valid credentials via the STACKIT CLI. This flag is mutually exclusive with the expiration flag.") cmd.Flags().StringP(expirationFlag, "e", "", "Expiration time for the kubeconfig in seconds(s), minutes(m), hours(h), days(d) or months(M). Example: 30d. By default, expiration time is 1h") cmd.Flags().String(filepathFlag, "", "Path to create the kubeconfig file. By default, the kubeconfig is created as 'config' in the .kube folder, in the user's home directory.") + + cmd.MarkFlagsMutuallyExclusive(loginFlag, expirationFlag) } func parseInput(p *print.Printer, cmd *cobra.Command, inputArgs []string) (*inputModel, error) { @@ -146,6 +179,7 @@ func parseInput(p *print.Printer, cmd *cobra.Command, inputArgs []string) (*inpu ClusterName: clusterName, Filepath: flags.FlagToStringPointer(p, cmd, filepathFlag), ExpirationTime: expTime, + Login: flags.FlagToBoolValue(p, cmd, loginFlag), } if p.IsVerbosityDebug() { @@ -160,7 +194,7 @@ func parseInput(p *print.Printer, cmd *cobra.Command, inputArgs []string) (*inpu return &model, nil } -func buildRequest(ctx context.Context, model *inputModel, apiClient *ske.APIClient) (ske.ApiCreateKubeconfigRequest, error) { +func buildRequestCreate(ctx context.Context, model *inputModel, apiClient *ske.APIClient) (ske.ApiCreateKubeconfigRequest, error) { req := apiClient.CreateKubeconfig(ctx, model.ProjectId, model.ClusterName) payload := ske.CreateKubeconfigPayload{} @@ -172,10 +206,34 @@ func buildRequest(ctx context.Context, model *inputModel, apiClient *ske.APIClie return req.CreateKubeconfigPayload(payload), nil } -func outputResult(p *print.Printer, model *inputModel, kubeconfigPath string, resp *ske.Kubeconfig) error { +func buildRequestLogin(ctx context.Context, model *inputModel, apiClient *ske.APIClient) (ske.ApiGetLoginKubeconfigRequest, error) { + return apiClient.GetLoginKubeconfig(ctx, model.ProjectId, model.ClusterName), nil +} + +func outputResult(p *print.Printer, model *inputModel, kubeconfigPath string, respKubeconfig *ske.Kubeconfig, respLogin *ske.V1LoginKubeconfig) error { switch model.OutputFormat { case print.JSONOutputFormat: - details, err := json.MarshalIndent(resp, "", " ") + var err error + var details []byte + if respKubeconfig != nil { + details, err = json.MarshalIndent(respKubeconfig, "", " ") + } else if respLogin != nil { + details, err = json.MarshalIndent(respLogin, "", " ") + } + if err != nil { + return fmt.Errorf("marshal SKE Kubeconfig: %w", err) + } + p.Outputln(string(details)) + + return nil + case print.YAMLOutputFormat: + var err error + var details []byte + if respKubeconfig != nil { + details, err = yaml.MarshalWithOptions(respKubeconfig, yaml.IndentSequence(true)) + } else if respLogin != nil { + details, err = yaml.MarshalWithOptions(respLogin, yaml.IndentSequence(true)) + } if err != nil { return fmt.Errorf("marshal SKE Kubeconfig: %w", err) } @@ -183,7 +241,11 @@ func outputResult(p *print.Printer, model *inputModel, kubeconfigPath string, re return nil default: - p.Outputf("Created kubeconfig file for cluster %s in %q, with expiration date %v (UTC)\n", model.ClusterName, kubeconfigPath, *resp.ExpirationTimestamp) + var expiration string + if respKubeconfig != nil { + expiration = fmt.Sprintf(", with expiration date %v (UTC)", *respKubeconfig.ExpirationTimestamp) + } + p.Outputf("Created kubeconfig file for cluster %s in %q%s\n", model.ClusterName, kubeconfigPath, expiration) return nil } diff --git a/internal/cmd/ske/kubeconfig/create/create_test.go b/internal/cmd/ske/kubeconfig/create/create_test.go index 17b30a4f6..86fb29ac4 100644 --- a/internal/cmd/ske/kubeconfig/create/create_test.go +++ b/internal/cmd/ske/kubeconfig/create/create_test.go @@ -92,7 +92,17 @@ func TestParseInput(t *testing.T) { model.ExpirationTime = utils.Ptr("2592000") }), }, - + { + description: "login", + argValues: fixtureArgValues(), + flagValues: fixtureFlagValues(func(flagValues map[string]string) { + flagValues["login"] = "true" + }), + isValid: true, + expectedModel: fixtureInputModel(func(model *inputModel) { + model.Login = true + }), + }, { description: "custom filepath", argValues: fixtureArgValues(), @@ -202,7 +212,7 @@ func TestParseInput(t *testing.T) { } } -func TestBuildRequest(t *testing.T) { +func TestBuildRequestCreate(t *testing.T) { tests := []struct { description string model *inputModel @@ -225,7 +235,7 @@ func TestBuildRequest(t *testing.T) { for _, tt := range tests { t.Run(tt.description, func(t *testing.T) { - request, _ := buildRequest(testCtx, tt.model, testClient) + request, _ := buildRequestCreate(testCtx, tt.model, testClient) diff := cmp.Diff(request, tt.expectedRequest, cmp.AllowUnexported(tt.expectedRequest), diff --git a/internal/cmd/ske/kubeconfig/kubeconfig.go b/internal/cmd/ske/kubeconfig/kubeconfig.go index 3370a8529..44803f14b 100644 --- a/internal/cmd/ske/kubeconfig/kubeconfig.go +++ b/internal/cmd/ske/kubeconfig/kubeconfig.go @@ -2,6 +2,7 @@ package kubeconfig import ( "github.com/stackitcloud/stackit-cli/internal/cmd/ske/kubeconfig/create" + "github.com/stackitcloud/stackit-cli/internal/cmd/ske/kubeconfig/login" "github.com/stackitcloud/stackit-cli/internal/pkg/args" "github.com/stackitcloud/stackit-cli/internal/pkg/print" "github.com/stackitcloud/stackit-cli/internal/pkg/utils" @@ -23,4 +24,5 @@ func NewCmd(p *print.Printer) *cobra.Command { func addSubcommands(cmd *cobra.Command, p *print.Printer) { cmd.AddCommand(create.NewCmd(p)) + cmd.AddCommand(login.NewCmd(p)) } diff --git a/internal/cmd/ske/kubeconfig/login/login.go b/internal/cmd/ske/kubeconfig/login/login.go new file mode 100644 index 000000000..568b34c98 --- /dev/null +++ b/internal/cmd/ske/kubeconfig/login/login.go @@ -0,0 +1,253 @@ +package login + +import ( + "context" + "crypto/sha256" + "crypto/x509" + "encoding/json" + "encoding/pem" + "errors" + "fmt" + "os" + "strconv" + "time" + + "github.com/stackitcloud/stackit-cli/internal/pkg/cache" + "k8s.io/client-go/rest" + + "github.com/stackitcloud/stackit-cli/internal/pkg/args" + "github.com/stackitcloud/stackit-cli/internal/pkg/examples" + "github.com/stackitcloud/stackit-cli/internal/pkg/print" + "github.com/stackitcloud/stackit-cli/internal/pkg/services/ske/client" + + "github.com/spf13/cobra" + "github.com/stackitcloud/stackit-sdk-go/services/ske" + v1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/client-go/kubernetes/scheme" + clientauthenticationv1 "k8s.io/client-go/pkg/apis/clientauthentication/v1" + "k8s.io/client-go/tools/auth/exec" + "k8s.io/client-go/tools/clientcmd" +) + +const ( + expirationSeconds = 30 * 60 // 30 min + refreshBeforeDuration = 15 * time.Minute // 15 min +) + +func NewCmd(p *print.Printer) *cobra.Command { + cmd := &cobra.Command{ + Use: "login", + Short: "Login plugin for kubernetes clients", + Long: fmt.Sprintf("%s\n%s\n%s", + "Login plugin for kubernetes clients, that creates short-lived credentials to authenticate against a STACKIT Kubernetes Engine (SKE) cluster.", + "First you need to obtain a kubeconfig for use with the login command (first example).", + "Secondly you use the kubeconfig with your chosen Kubernetes client (second example), the client will automatically retrieve the credentials via the STACKIT CLI.", + ), + Args: args.NoArgs, + Example: examples.Build( + examples.NewExample( + `Get a login kubeconfig for the SKE cluster with name "my-cluster". `+ + "This kubeconfig does not contain any credentials and instead obtains valid credentials via the `stackit ske kubeconfig login` command.", + "$ stackit ske kubeconfig create my-cluster --login"), + examples.NewExample( + "Use the previously saved kubeconfig to authenticate to the SKE cluster, in this case with kubectl.", + "$ kubectl cluster-info", + "$ kubectl get pods"), + ), + RunE: func(cmd *cobra.Command, args []string) error { + ctx := context.Background() + + if err := cache.Init(); err != nil { + return fmt.Errorf("cache init failed: %w", err) + } + + env := os.Getenv("KUBERNETES_EXEC_INFO") + if env == "" { + return fmt.Errorf("%s\n%s\n%s", "KUBERNETES_EXEC_INFO env var is unset or empty.", + "The command probably was not called from a Kubernetes client application!", + "See `stackit ske kubeconfig login --help` for detailed usage instructions.") + } + + clusterConfig, err := parseClusterConfig() + if err != nil { + return fmt.Errorf("parseClusterConfig: %w", err) + } + + // Configure API client + apiClient, err := client.ConfigureClient(p) + if err != nil { + return err + } + + cachedKubeconfig := getCachedKubeConfig(clusterConfig.cacheKey) + + if cachedKubeconfig == nil { + return GetAndOutputKubeconfig(ctx, p, apiClient, clusterConfig, false, nil) + } + + certPem, _ := pem.Decode(cachedKubeconfig.CertData) + if certPem == nil { + _ = cache.DeleteObject(clusterConfig.cacheKey) + return GetAndOutputKubeconfig(ctx, p, apiClient, clusterConfig, false, nil) + } + + certificate, err := x509.ParseCertificate(certPem.Bytes) + if err != nil { + _ = cache.DeleteObject(clusterConfig.cacheKey) + return GetAndOutputKubeconfig(ctx, p, apiClient, clusterConfig, false, nil) + } + + // cert is expired, request new + if time.Now().After(certificate.NotAfter.UTC()) { + _ = cache.DeleteObject(clusterConfig.cacheKey) + return GetAndOutputKubeconfig(ctx, p, apiClient, clusterConfig, false, nil) + } + // cert expires within the next 15min, refresh (try to get a new, use cache on failure) + if time.Now().Add(refreshBeforeDuration).After(certificate.NotAfter.UTC()) { + return GetAndOutputKubeconfig(ctx, p, apiClient, clusterConfig, true, cachedKubeconfig) + } + + // cert not expired, nor will it expire in the next 15min; therefore, use the cached kubeconfig + if err := output(p, clusterConfig.cacheKey, cachedKubeconfig); err != nil { + return err + } + return nil + }, + } + return cmd +} + +type clusterConfig struct { + STACKITProjectID string `json:"stackitProjectId"` + ClusterName string `json:"clusterName"` + + cacheKey string +} + +func parseClusterConfig() (*clusterConfig, error) { + obj, _, err := exec.LoadExecCredentialFromEnv() + if err != nil { + return nil, fmt.Errorf("LoadExecCredentialFromEnv: %w", err) + } + + if err := clientauthenticationv1.AddToScheme(scheme.Scheme); err != nil { + return nil, err + } + + obj, err = scheme.Scheme.ConvertToVersion(obj, clientauthenticationv1.SchemeGroupVersion) + if err != nil { + return nil, fmt.Errorf("ConvertToVersion: %w", err) + } + + execCredential, ok := obj.(*clientauthenticationv1.ExecCredential) + if !ok { + return nil, fmt.Errorf("conversion to ExecCredential failed") + } + if execCredential == nil || execCredential.Spec.Cluster == nil { + return nil, fmt.Errorf("ExecCredential contains not all needed fields") + } + config := &clusterConfig{} + err = json.Unmarshal(execCredential.Spec.Cluster.Config.Raw, config) + if err != nil { + return nil, fmt.Errorf("unmarshal: %w", err) + } + + config.cacheKey = fmt.Sprintf("ske-login-%x", sha256.Sum256([]byte(execCredential.Spec.Cluster.Server))) + + return config, nil +} + +func getCachedKubeConfig(key string) *rest.Config { + cachedKubeconfig, err := cache.GetObject(key) + if err != nil { + return nil + } + + restConfig, err := clientcmd.RESTConfigFromKubeConfig(cachedKubeconfig) + if err != nil { + return nil + } + + return restConfig +} + +func GetAndOutputKubeconfig(ctx context.Context, p *print.Printer, apiClient *ske.APIClient, clusterConfig *clusterConfig, fallbackToCache bool, cachedKubeconfig *rest.Config) error { + req := buildRequest(ctx, apiClient, clusterConfig) + kubeconfigResponse, err := req.Execute() + if err != nil { + if fallbackToCache { + return output(p, clusterConfig.cacheKey, cachedKubeconfig) + } + return fmt.Errorf("request kubeconfig: %w", err) + } + + kubeconfig, err := clientcmd.RESTConfigFromKubeConfig([]byte(*kubeconfigResponse.Kubeconfig)) + if err != nil { + if fallbackToCache { + return output(p, clusterConfig.cacheKey, cachedKubeconfig) + } + return fmt.Errorf("parse kubeconfig: %w", err) + } + if err = cache.PutObject(clusterConfig.cacheKey, []byte(*kubeconfigResponse.Kubeconfig)); err != nil { + if fallbackToCache { + return output(p, clusterConfig.cacheKey, cachedKubeconfig) + } + return fmt.Errorf("cache kubeconfig: %w", err) + } + + return output(p, clusterConfig.cacheKey, kubeconfig) +} + +func buildRequest(ctx context.Context, apiClient *ske.APIClient, clusterConfig *clusterConfig) ske.ApiCreateKubeconfigRequest { + req := apiClient.CreateKubeconfig(ctx, clusterConfig.STACKITProjectID, clusterConfig.ClusterName) + expirationSeconds := strconv.Itoa(expirationSeconds) + + return req.CreateKubeconfigPayload(ske.CreateKubeconfigPayload{ExpirationSeconds: &expirationSeconds}) +} + +func output(p *print.Printer, cacheKey string, kubeconfig *rest.Config) error { + if kubeconfig == nil { + _ = cache.DeleteObject(cacheKey) + return errors.New("kubeconfig is nil") + } + + outputExecCredential, err := parseKubeConfigToExecCredential(kubeconfig) + if err != nil { + _ = cache.DeleteObject(cacheKey) + return fmt.Errorf("convert to ExecCredential: %w", err) + } + + output, err := json.Marshal(outputExecCredential) + if err != nil { + _ = cache.DeleteObject(cacheKey) + return fmt.Errorf("marshal ExecCredential: %w", err) + } + + p.Outputf(string(output)) + return nil +} + +func parseKubeConfigToExecCredential(kubeconfig *rest.Config) (*clientauthenticationv1.ExecCredential, error) { + certPem, _ := pem.Decode(kubeconfig.CertData) + if certPem == nil { + return nil, fmt.Errorf("decoded pem is nil") + } + + certificate, err := x509.ParseCertificate(certPem.Bytes) + if err != nil { + return nil, fmt.Errorf("parse certificate: %w", err) + } + + outputExecCredential := clientauthenticationv1.ExecCredential{ + TypeMeta: v1.TypeMeta{ + APIVersion: clientauthenticationv1.SchemeGroupVersion.String(), + Kind: "ExecCredential", + }, + Status: &clientauthenticationv1.ExecCredentialStatus{ + ExpirationTimestamp: &v1.Time{Time: certificate.NotAfter.Add(-time.Minute * 15)}, + ClientCertificateData: string(kubeconfig.CertData), + ClientKeyData: string(kubeconfig.KeyData), + }, + } + return &outputExecCredential, nil +} diff --git a/internal/cmd/ske/kubeconfig/login/login_test.go b/internal/cmd/ske/kubeconfig/login/login_test.go new file mode 100644 index 000000000..c6b94c9a9 --- /dev/null +++ b/internal/cmd/ske/kubeconfig/login/login_test.go @@ -0,0 +1,140 @@ +package login + +import ( + "context" + "testing" + "time" + + "github.com/google/go-cmp/cmp" + "github.com/google/go-cmp/cmp/cmpopts" + "github.com/google/uuid" + "github.com/stackitcloud/stackit-cli/internal/pkg/utils" + "github.com/stackitcloud/stackit-sdk-go/services/ske" + v1 "k8s.io/apimachinery/pkg/apis/meta/v1" + clientauthenticationv1 "k8s.io/client-go/pkg/apis/clientauthentication/v1" + "k8s.io/client-go/rest" +) + +type testCtxKey struct{} + +var testCtx = context.WithValue(context.Background(), testCtxKey{}, "foo") +var testClient = &ske.APIClient{} +var testProjectId = uuid.NewString() +var testClusterName = "cluster" + +func fixtureClusterConfig(mods ...func(clusterConfig *clusterConfig)) *clusterConfig { + clusterConfig := &clusterConfig{ + STACKITProjectID: testProjectId, + ClusterName: testClusterName, + cacheKey: "", + } + for _, mod := range mods { + mod(clusterConfig) + } + return clusterConfig +} + +func fixtureRequest(mods ...func(request *ske.ApiCreateKubeconfigRequest)) ske.ApiCreateKubeconfigRequest { + request := testClient.CreateKubeconfig(testCtx, testProjectId, testClusterName) + request = request.CreateKubeconfigPayload(ske.CreateKubeconfigPayload{}) + for _, mod := range mods { + mod(&request) + } + return request +} + +func TestBuildRequest(t *testing.T) { + tests := []struct { + description string + clusterConfig *clusterConfig + expectedRequest ske.ApiCreateKubeconfigRequest + }{ + { + description: "expiration time", + clusterConfig: fixtureClusterConfig(), + expectedRequest: fixtureRequest().CreateKubeconfigPayload(ske.CreateKubeconfigPayload{ + ExpirationSeconds: utils.Ptr("1800")}), + }, + } + + for _, tt := range tests { + t.Run(tt.description, func(t *testing.T) { + request := buildRequest(testCtx, testClient, tt.clusterConfig) + + diff := cmp.Diff(request, tt.expectedRequest, + cmp.AllowUnexported(tt.expectedRequest), + cmpopts.EquateComparable(testCtx), + ) + if diff != "" { + t.Fatalf("Data does not match: %s", diff) + } + }) + } +} + +func TestParseKubeConfigToExecCredential(t *testing.T) { + expectedTime, _ := time.Parse(time.RFC3339, "2024-01-01T00:45:00Z") + + tests := []struct { + description string + kubeconfig *rest.Config + expectedExecCredentialRequest *clientauthenticationv1.ExecCredential + }{ + { + description: "expiration time", + kubeconfig: &rest.Config{ + TLSClientConfig: rest.TLSClientConfig{ + CertData: []byte(`-----BEGIN CERTIFICATE----- +MIIBhTCCASugAwIBAgIIF8+zRM8UalAwCgYIKoZIzj0EAwIwGDEWMBQGA1UEAxMN +Y2EtY2xpZW50LXh5ejAeFw0yNDAxMDEwMDAwMDBaFw0yNDAxMDEwMTAwMDBaMC8x +FzAVBgNVBAoTDnN5c3RlbTptYXN0ZXJzMRQwEgYDVQQDEwtza2U6Y2x1c3RlcjBZ +MBMGByqGSM49AgEGCCqGSM49AwEHA0IABJaxZ8G4wEZ1xf44hMV1pQWsti5SL6PH +QF0bRniQEJHSOcZMwc0OrVIfuSV1qSMyvYIaFtBj1j9f2v8oPux7V02jSDBGMA4G +A1UdDwEB/wQEAwIFoDATBgNVHSUEDDAKBggrBgEFBQcDAjAfBgNVHSMEGDAWgBQt +Pn1pNgfb8xcdRVxVnHDIvb8abzAKBggqhkjOPQQDAgNIADBFAiEA8gG2l0schbMu +zbRjZmli7cnenEnfnNoFIGbgkbjGXRUCIC5zFtWXFK7kA+B2vDxD0DlLcQodNwi4 +2JKP8gT9ol16 +-----END CERTIFICATE-----`), + KeyData: []byte("keykeykey"), + }, + }, + expectedExecCredentialRequest: &clientauthenticationv1.ExecCredential{ + TypeMeta: v1.TypeMeta{ + APIVersion: clientauthenticationv1.SchemeGroupVersion.String(), + Kind: "ExecCredential", + }, + Status: &clientauthenticationv1.ExecCredentialStatus{ + ExpirationTimestamp: &v1.Time{Time: expectedTime}, + ClientCertificateData: `-----BEGIN CERTIFICATE----- +MIIBhTCCASugAwIBAgIIF8+zRM8UalAwCgYIKoZIzj0EAwIwGDEWMBQGA1UEAxMN +Y2EtY2xpZW50LXh5ejAeFw0yNDAxMDEwMDAwMDBaFw0yNDAxMDEwMTAwMDBaMC8x +FzAVBgNVBAoTDnN5c3RlbTptYXN0ZXJzMRQwEgYDVQQDEwtza2U6Y2x1c3RlcjBZ +MBMGByqGSM49AgEGCCqGSM49AwEHA0IABJaxZ8G4wEZ1xf44hMV1pQWsti5SL6PH +QF0bRniQEJHSOcZMwc0OrVIfuSV1qSMyvYIaFtBj1j9f2v8oPux7V02jSDBGMA4G +A1UdDwEB/wQEAwIFoDATBgNVHSUEDDAKBggrBgEFBQcDAjAfBgNVHSMEGDAWgBQt +Pn1pNgfb8xcdRVxVnHDIvb8abzAKBggqhkjOPQQDAgNIADBFAiEA8gG2l0schbMu +zbRjZmli7cnenEnfnNoFIGbgkbjGXRUCIC5zFtWXFK7kA+B2vDxD0DlLcQodNwi4 +2JKP8gT9ol16 +-----END CERTIFICATE-----`, + ClientKeyData: "keykeykey", + }, + }, + }, + } + + for _, tt := range tests { + t.Run(tt.description, func(t *testing.T) { + execCredential, err := parseKubeConfigToExecCredential(tt.kubeconfig) + if err != nil { + t.Fatalf("func returned error: %s", err) + } + if execCredential == nil { + t.Fatal("execCredential is nil") + } + diff := cmp.Diff(execCredential, tt.expectedExecCredentialRequest) + if diff != "" { + t.Fatalf("Data does not match: %s", diff) + } + }) + } +} diff --git a/internal/cmd/ske/options/options.go b/internal/cmd/ske/options/options.go index b41682e08..7bc3a5607 100644 --- a/internal/cmd/ske/options/options.go +++ b/internal/cmd/ske/options/options.go @@ -6,6 +6,7 @@ import ( "fmt" "strings" + "github.com/goccy/go-yaml" "github.com/stackitcloud/stackit-cli/internal/pkg/args" "github.com/stackitcloud/stackit-cli/internal/pkg/examples" "github.com/stackitcloud/stackit-cli/internal/pkg/flags" @@ -141,6 +142,14 @@ func outputResult(p *print.Printer, model *inputModel, options *ske.ProviderOpti return fmt.Errorf("marshal SKE options: %w", err) } p.Outputln(string(details)) + return nil + case print.YAMLOutputFormat: + details, err := yaml.MarshalWithOptions(options, yaml.IndentSequence(true)) + if err != nil { + return fmt.Errorf("marshal SKE options: %w", err) + } + p.Outputln(string(details)) + return nil default: return outputResultAsTable(p, model, options) diff --git a/internal/pkg/cache/cache.go b/internal/pkg/cache/cache.go new file mode 100644 index 000000000..d52670fd4 --- /dev/null +++ b/internal/pkg/cache/cache.go @@ -0,0 +1,73 @@ +package cache + +import ( + "errors" + "fmt" + "os" + "path/filepath" + "regexp" +) + +var ( + cacheFolderPath string + + identifierRegex = regexp.MustCompile("^[a-zA-Z0-9-]+$") + ErrorInvalidCacheIdentifier = fmt.Errorf("invalid cache identifier") +) + +func Init() error { + cacheDir, err := os.UserCacheDir() + if err != nil { + return fmt.Errorf("get user cache dir: %w", err) + } + cacheFolderPath = filepath.Join(cacheDir, "stackit") + return nil +} + +func GetObject(identifier string) ([]byte, error) { + if err := validateCacheFolderPath(); err != nil { + return nil, err + } + if !identifierRegex.MatchString(identifier) { + return nil, ErrorInvalidCacheIdentifier + } + + return os.ReadFile(filepath.Join(cacheFolderPath, identifier)) +} + +func PutObject(identifier string, data []byte) error { + if err := validateCacheFolderPath(); err != nil { + return err + } + if !identifierRegex.MatchString(identifier) { + return ErrorInvalidCacheIdentifier + } + + err := os.MkdirAll(cacheFolderPath, os.ModePerm) + if err != nil { + return err + } + + return os.WriteFile(filepath.Join(cacheFolderPath, identifier), data, 0o600) +} + +func DeleteObject(identifier string) error { + if err := validateCacheFolderPath(); err != nil { + return err + } + if !identifierRegex.MatchString(identifier) { + return ErrorInvalidCacheIdentifier + } + + if err := os.Remove(filepath.Join(cacheFolderPath, identifier)); !errors.Is(err, os.ErrNotExist) { + return err + } + return nil +} + +func validateCacheFolderPath() error { + if cacheFolderPath == "" { + return errors.New("cacheFolderPath not set. Forgot to call Init()?") + } + return nil +} diff --git a/internal/pkg/cache/cache_test.go b/internal/pkg/cache/cache_test.go new file mode 100644 index 000000000..4ea003116 --- /dev/null +++ b/internal/pkg/cache/cache_test.go @@ -0,0 +1,207 @@ +package cache + +import ( + "errors" + "os" + "path/filepath" + "testing" + + "github.com/google/uuid" +) + +func TestGetObject(t *testing.T) { + if err := Init(); err != nil { + t.Fatalf("cache init failed: %s", err) + } + + tests := []struct { + description string + identifier string + expectFile bool + expectedErr error + }{ + { + description: "identifier exists", + identifier: "test-cache-get-exists", + expectFile: true, + expectedErr: nil, + }, + { + description: "identifier does not exist", + identifier: "test-cache-get-not-exists", + expectFile: false, + expectedErr: os.ErrNotExist, + }, + { + description: "identifier is invalid", + identifier: "in../../valid", + expectFile: false, + expectedErr: ErrorInvalidCacheIdentifier, + }, + } + + for _, tt := range tests { + t.Run(tt.description, func(t *testing.T) { + id := tt.identifier + "-" + uuid.NewString() + + // setup + if tt.expectFile { + err := os.MkdirAll(cacheFolderPath, os.ModePerm) + if err != nil { + t.Fatalf("create cache folder: %s", err.Error()) + } + path := filepath.Join(cacheFolderPath, id) + if err := os.WriteFile(path, []byte("dummy"), 0o600); err != nil { + t.Fatalf("setup: WriteFile (%s) failed", path) + } + } + // test + file, err := GetObject(id) + + if !errors.Is(err, tt.expectedErr) { + t.Fatalf("returned error (%q) does not match %q", err.Error(), tt.expectedErr.Error()) + } + + if tt.expectFile { + if len(file) < 1 { + t.Fatalf("expected a file but byte array is empty (len %d)", len(file)) + } + } else { + if len(file) > 0 { + t.Fatalf("didn't expect a file, but byte array is not empty (len %d)", len(file)) + } + } + }) + } +} +func TestPutObject(t *testing.T) { + if err := Init(); err != nil { + t.Fatalf("cache init failed: %s", err) + } + + tests := []struct { + description string + identifier string + existingFile bool + expectFile bool + expectedErr error + customPath string + }{ + { + description: "identifier already exists", + identifier: "test-cache-put-exists", + existingFile: true, + expectFile: true, + expectedErr: nil, + }, + { + description: "identifier does not exist", + identifier: "test-cache-put-not-exists", + expectFile: true, + expectedErr: nil, + }, + { + description: "identifier is invalid", + identifier: "in../../valid", + expectFile: false, + expectedErr: ErrorInvalidCacheIdentifier, + }, + { + description: "directory does not yet exist", + identifier: "test-cache-put-folder-not-exists", + expectFile: true, + expectedErr: nil, + customPath: "/tmp/stackit-cli-test", + }, + } + + for _, tt := range tests { + t.Run(tt.description, func(t *testing.T) { + id := tt.identifier + "-" + uuid.NewString() + if tt.customPath != "" { + cacheFolderPath = tt.customPath + } else { + cacheDir, _ := os.UserCacheDir() + cacheFolderPath = filepath.Join(cacheDir, "stackit") + } + path := filepath.Join(cacheFolderPath, id) + + // setup + if tt.existingFile { + if err := os.WriteFile(path, []byte("dummy"), 0o600); err != nil { + t.Fatalf("setup: WriteFile (%s) failed", path) + } + } + // test + err := PutObject(id, []byte("dummy")) + + if !errors.Is(err, tt.expectedErr) { + t.Fatalf("returned error (%q) does not match %q", err.Error(), tt.expectedErr.Error()) + } + + if tt.expectFile { + if _, err := os.Stat(path); errors.Is(err, os.ErrNotExist) { + t.Fatalf("expected file (%q) to exist", path) + } + } + }) + } +} + +func TestDeleteObject(t *testing.T) { + if err := Init(); err != nil { + t.Fatalf("cache init failed: %s", err) + } + + tests := []struct { + description string + identifier string + existingFile bool + expectedErr error + }{ + { + description: "identifier exists", + identifier: "test-cache-delete-exists", + existingFile: true, + expectedErr: nil, + }, + { + description: "identifier does not exist", + identifier: "test-cache-delete-not-exists", + existingFile: false, + expectedErr: nil, + }, + { + description: "identifier is invalid", + identifier: "in../../valid", + existingFile: false, + expectedErr: ErrorInvalidCacheIdentifier, + }, + } + + for _, tt := range tests { + t.Run(tt.description, func(t *testing.T) { + id := tt.identifier + "-" + uuid.NewString() + path := filepath.Join(cacheFolderPath, id) + + // setup + if tt.existingFile { + if err := os.WriteFile(path, []byte("dummy"), 0o600); err != nil { + t.Fatalf("setup: WriteFile (%s) failed", path) + } + } + // test + err := DeleteObject(id) + + if !errors.Is(err, tt.expectedErr) { + t.Fatalf("returned error (%q) does not match %q", err.Error(), tt.expectedErr.Error()) + } + + if tt.existingFile { + if _, err := os.Stat(path); !errors.Is(err, os.ErrNotExist) { + t.Fatalf("expected file (%q) to not exist", path) + } + } + }) + } +} diff --git a/internal/pkg/config/config.go b/internal/pkg/config/config.go index 8982352a8..b9abf9b31 100644 --- a/internal/pkg/config/config.go +++ b/internal/pkg/config/config.go @@ -123,7 +123,8 @@ func InitConfig() { // Write saves the config file (wrapping `viper.WriteConfig`) and ensures that its directory exists func Write() error { - if err := createFolderIfNotExists(configFolderPath); err != nil { + err := os.MkdirAll(configFolderPath, os.ModePerm) + if err != nil { return fmt.Errorf("create config directory: %w", err) } return viper.WriteConfig() diff --git a/internal/pkg/config/file_utils.go b/internal/pkg/config/file_utils.go index 44b0ab38a..0895a9dc5 100644 --- a/internal/pkg/config/file_utils.go +++ b/internal/pkg/config/file_utils.go @@ -5,20 +5,6 @@ import ( "os" ) -// createFolderIfNotExists creates a folder if it does not exist. -func createFolderIfNotExists(folderPath string) error { - _, err := os.Stat(folderPath) - if os.IsNotExist(err) { - err := os.MkdirAll(folderPath, os.ModePerm) - if err != nil { - return err - } - } else if err != nil { - return err - } - return nil -} - // readFileIfExists reads the contents of a file and returns it as a string, along with a boolean indicating if the file exists. // If the file does not exist, it returns an empty string and no error. // If the file exists but cannot be read, it returns an error. diff --git a/internal/pkg/config/file_utils_test.go b/internal/pkg/config/file_utils_test.go index b3453c661..d7f299cb6 100644 --- a/internal/pkg/config/file_utils_test.go +++ b/internal/pkg/config/file_utils_test.go @@ -1,51 +1,9 @@ package config import ( - "os" "testing" ) -func TestCreateFolderIfNotExists(t *testing.T) { - tests := []struct { - description string - folderPath string - needsCleanUp bool - }{ - { - description: "folder exists", - folderPath: "test-data/folder-exists", - }, - { - description: "folder does not exist", - folderPath: "test-data/folder-does-not-exist", - needsCleanUp: true, - }, - } - - for _, tt := range tests { - t.Run(tt.description, func(t *testing.T) { - err := createFolderIfNotExists(tt.folderPath) - if err != nil { - t.Errorf("create folder: %v", err) - } - - // Check if the folder was created - _, err = os.Stat(tt.folderPath) - if os.IsNotExist(err) { - t.Errorf("expected folder to exist but it does not") - } - - // Clean up - if tt.needsCleanUp { - err = os.RemoveAll(tt.folderPath) - if err != nil { - t.Errorf("remove folder: %v", err) - } - } - }) - } -} - func TestReadFileIfExists(t *testing.T) { tests := []struct { description string diff --git a/internal/pkg/config/profiles.go b/internal/pkg/config/profiles.go index 219fa879c..ff07fbd7b 100644 --- a/internal/pkg/config/profiles.go +++ b/internal/pkg/config/profiles.go @@ -52,7 +52,7 @@ func SetProfile(p *print.Printer, profile string) error { p.Debug(print.DebugLevel, "persisted new active profile in: %s", profileFilePath) configFolderPath = filepath.Join(defaultConfigFolderPath, profile) - err = createFolderIfNotExists(configFolderPath) + err = os.MkdirAll(configFolderPath, os.ModePerm) if err != nil { return fmt.Errorf("create config folder: %w", err) } diff --git a/internal/pkg/errors/errors.go b/internal/pkg/errors/errors.go index 0bb41a4ab..1cfe6bce8 100644 --- a/internal/pkg/errors/errors.go +++ b/internal/pkg/errors/errors.go @@ -22,6 +22,8 @@ or you can also set it through the environment variable [STACKIT_PROJECT_ID]` Get details on the available flags by re-running your command with the --help flag.` + REQUIRED_MUTUALLY_EXCLUSIVE_FLAGS = `the following flags are mutually exclusive and at least one of them is required: %s` + FAILED_AUTH = `you are not authenticated. You can authenticate as a user by running: @@ -241,6 +243,14 @@ func (e *FlagValidationError) Error() string { return fmt.Sprintf(FLAG_VALIDATION, e.Flag, e.Details) } +type RequiredMutuallyExclusiveFlagsError struct { + Flags []string +} + +func (e *RequiredMutuallyExclusiveFlagsError) Error() string { + return fmt.Sprintf(REQUIRED_MUTUALLY_EXCLUSIVE_FLAGS, strings.Join(e.Flags, ", ")) +} + type ArgValidationError struct { Arg string Details string diff --git a/internal/pkg/globalflags/global_flags.go b/internal/pkg/globalflags/global_flags.go index aca895991..47c48ea13 100644 --- a/internal/pkg/globalflags/global_flags.go +++ b/internal/pkg/globalflags/global_flags.go @@ -27,7 +27,7 @@ const ( VerbosityDefault = InfoVerbosity ) -var outputFormatFlagOptions = []string{print.JSONOutputFormat, print.PrettyOutputFormat, print.NoneOutputFormat} +var outputFormatFlagOptions = []string{print.JSONOutputFormat, print.PrettyOutputFormat, print.NoneOutputFormat, print.YAMLOutputFormat} var verbosityFlagOptions = []string{DebugVerbosity, InfoVerbosity, WarningVerbosity, ErrorVerbosity} type GlobalFlagModel struct { diff --git a/internal/pkg/print/print.go b/internal/pkg/print/print.go index f9f7ce00a..649590c7c 100644 --- a/internal/pkg/print/print.go +++ b/internal/pkg/print/print.go @@ -34,6 +34,7 @@ const ( JSONOutputFormat = "json" PrettyOutputFormat = "pretty" NoneOutputFormat = "none" + YAMLOutputFormat = "yaml" ) var errAborted = errors.New("operation aborted") @@ -183,9 +184,15 @@ func (p *Printer) PagerDisplay(content string) error { // -R: interprets ANSI color and style sequences pagerCmd := exec.Command("less", "-F", "-S", "-w", "-R") + pager, pagerExists := os.LookupEnv("PAGER") + if pagerExists && pager != "nil" && pager != "" { + pagerCmd = exec.Command(pager) // #nosec G204 + } + pagerCmd.Stdin = strings.NewReader(content) pagerCmd.Stdout = p.Cmd.OutOrStdout() + p.Debug(DebugLevel, "using pager: %s", pagerCmd.Args[0]) err := pagerCmd.Run() if err != nil { p.Debug(ErrorLevel, "run pager command: %v", err) diff --git a/internal/pkg/services/mongodbflex/utils/utils.go b/internal/pkg/services/mongodbflex/utils/utils.go index 30b94369e..c582c8de8 100644 --- a/internal/pkg/services/mongodbflex/utils/utils.go +++ b/internal/pkg/services/mongodbflex/utils/utils.go @@ -1,6 +1,7 @@ package utils import ( + "cmp" "context" "fmt" "slices" @@ -118,6 +119,7 @@ type MongoDBFlexClient interface { ListVersionsExecute(ctx context.Context, projectId string) (*mongodbflex.ListVersionsResponse, error) GetInstanceExecute(ctx context.Context, projectId, instanceId string) (*mongodbflex.GetInstanceResponse, error) GetUserExecute(ctx context.Context, projectId, instanceId, userId string) (*mongodbflex.GetUserResponse, error) + ListRestoreJobsExecute(ctx context.Context, projectId string, instanceId string) (*mongodbflex.ListRestoreJobsResponse, error) } func GetLatestMongoDBVersion(ctx context.Context, apiClient MongoDBFlexClient, projectId string) (string, error) { @@ -157,3 +159,26 @@ func GetUserName(ctx context.Context, apiClient MongoDBFlexClient, projectId, in } return *resp.Item.Username, nil } + +func GetRestoreStatus(backupId string, restoreJobs *mongodbflex.ListRestoreJobsResponse) string { + state := "-" + if restoreJobs.Items == nil { + return state + } + + restoreJobsSlice := *restoreJobs.Items + + // sort array by descending date + slices.SortFunc(restoreJobsSlice, func(i, j mongodbflex.RestoreInstanceStatus) int { + // swap elements to sort by descending order + return cmp.Compare(*j.Date, *i.Date) + }) + + for _, restoreJob := range *restoreJobs.Items { + if *restoreJob.BackupID == backupId { + state = *restoreJob.Status + break + } + } + return state +} diff --git a/internal/pkg/services/mongodbflex/utils/utils_test.go b/internal/pkg/services/mongodbflex/utils/utils_test.go index c8339e822..bdea1a34b 100644 --- a/internal/pkg/services/mongodbflex/utils/utils_test.go +++ b/internal/pkg/services/mongodbflex/utils/utils_test.go @@ -16,6 +16,7 @@ var ( testProjectId = uuid.NewString() testInstanceId = uuid.NewString() testUserId = uuid.NewString() + testBackupId = uuid.NewString() ) const ( @@ -24,12 +25,14 @@ const ( ) type mongoDBFlexClientMocked struct { - listVersionsFails bool - listVersionsResp *mongodbflex.ListVersionsResponse - getInstanceFails bool - getInstanceResp *mongodbflex.GetInstanceResponse - getUserFails bool - getUserResp *mongodbflex.GetUserResponse + listVersionsFails bool + listVersionsResp *mongodbflex.ListVersionsResponse + getInstanceFails bool + getInstanceResp *mongodbflex.GetInstanceResponse + getUserFails bool + getUserResp *mongodbflex.GetUserResponse + listRestoreJobsFails bool + listRestoreJobsResp *mongodbflex.ListRestoreJobsResponse } func (m *mongoDBFlexClientMocked) ListVersionsExecute(_ context.Context, _ string) (*mongodbflex.ListVersionsResponse, error) { @@ -39,6 +42,13 @@ func (m *mongoDBFlexClientMocked) ListVersionsExecute(_ context.Context, _ strin return m.listVersionsResp, nil } +func (m *mongoDBFlexClientMocked) ListRestoreJobsExecute(_ context.Context, _, _ string) (*mongodbflex.ListRestoreJobsResponse, error) { + if m.listRestoreJobsFails { + return nil, fmt.Errorf("could not list versions") + } + return m.listRestoreJobsResp, nil +} + func (m *mongoDBFlexClientMocked) GetInstanceExecute(_ context.Context, _, _ string) (*mongodbflex.GetInstanceResponse, error) { if m.getInstanceFails { return nil, fmt.Errorf("could not get instance") @@ -364,7 +374,7 @@ func TestLoadFlavorId(t *testing.T) { } } -func TestGetLatestPostgreSQLVersion(t *testing.T) { +func TestGetLatestMongoDBFlexVersion(t *testing.T) { tests := []struct { description string listVersionsFails bool @@ -518,3 +528,119 @@ func TestGetUserName(t *testing.T) { }) } } + +func TestGetRestoreStatus(t *testing.T) { + tests := []struct { + description string + listRestoreJobsResp *mongodbflex.ListRestoreJobsResponse + expectedOutput string + }{ + { + description: "base", + listRestoreJobsResp: &mongodbflex.ListRestoreJobsResponse{ + Items: &[]mongodbflex.RestoreInstanceStatus{ + { + BackupID: utils.Ptr(testBackupId), + Date: utils.Ptr("2024-05-14T12:01:11Z"), + Status: utils.Ptr("state"), + }, + { + BackupID: utils.Ptr("bar"), + Date: utils.Ptr("2024-05-14T12:01:11Z"), + Status: utils.Ptr("state 2"), + }, + }, + }, + expectedOutput: "state", + }, + { + description: "get latest restore, ordered array", + listRestoreJobsResp: &mongodbflex.ListRestoreJobsResponse{ + Items: &[]mongodbflex.RestoreInstanceStatus{ + { + BackupID: utils.Ptr(testBackupId), + Date: utils.Ptr("2024-05-14T12:01:11Z"), + Status: utils.Ptr("in progress"), + }, + { + BackupID: utils.Ptr(testBackupId), + Date: utils.Ptr("2024-05-13T12:01:11Z"), + Status: utils.Ptr("finished"), + }, + }, + }, + expectedOutput: "in progress", + }, + { + description: "get latest restore, unordered array", + listRestoreJobsResp: &mongodbflex.ListRestoreJobsResponse{ + Items: &[]mongodbflex.RestoreInstanceStatus{ + { + BackupID: utils.Ptr(testBackupId), + Date: utils.Ptr("2024-05-13T12:01:11Z"), + Status: utils.Ptr("finished"), + }, + { + BackupID: utils.Ptr(testBackupId), + Date: utils.Ptr("2024-05-14T12:01:11Z"), + Status: utils.Ptr("in progress"), + }, + }, + }, + expectedOutput: "in progress", + }, + { + description: "get latest restore, another date format", + listRestoreJobsResp: &mongodbflex.ListRestoreJobsResponse{ + Items: &[]mongodbflex.RestoreInstanceStatus{ + { + BackupID: utils.Ptr(testBackupId), + Date: utils.Ptr("2009-11-10 23:00:00 +0000 UTC m=+0.000000001"), + Status: utils.Ptr("finished"), + }, + { + BackupID: utils.Ptr(testBackupId), + Date: utils.Ptr("2009-11-11 23:00:00 +0000 UTC m=+0.000000001"), + Status: utils.Ptr("in progress"), + }, + }, + }, + expectedOutput: "in progress", + }, + { + description: "no restore job for that backup", + listRestoreJobsResp: &mongodbflex.ListRestoreJobsResponse{ + Items: &[]mongodbflex.RestoreInstanceStatus{ + { + BackupID: utils.Ptr("bar"), + Date: utils.Ptr("2024-05-13T12:01:11Z"), + Status: utils.Ptr("in progress"), + }, + { + BackupID: utils.Ptr("bar"), + Date: utils.Ptr("2024-05-13T12:01:11Z"), + Status: utils.Ptr("finished"), + }, + }, + }, + expectedOutput: "-", + }, + { + description: "no restore jobs", + listRestoreJobsResp: &mongodbflex.ListRestoreJobsResponse{ + Items: nil, + }, + expectedOutput: "-", + }, + } + + for _, tt := range tests { + t.Run(tt.description, func(t *testing.T) { + output := GetRestoreStatus(testBackupId, tt.listRestoreJobsResp) + + if output != tt.expectedOutput { + t.Errorf("expected output to be %s, got %s", tt.expectedOutput, output) + } + }) + } +} From 7949c82d5c7b972146654a68effb4f319ca88f75 Mon Sep 17 00:00:00 2001 From: GokceGK <161626272+GokceGK@users.noreply.github.com> Date: Wed, 22 May 2024 11:25:01 +0200 Subject: [PATCH 10/18] Fix encoded file path for profiles (#333) * Fix file path for the encoded text file * fix unit test --- internal/pkg/auth/storage.go | 6 +++--- internal/pkg/auth/storage_test.go | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/internal/pkg/auth/storage.go b/internal/pkg/auth/storage.go index 87ecd9747..73bef1189 100644 --- a/internal/pkg/auth/storage.go +++ b/internal/pkg/auth/storage.go @@ -96,7 +96,7 @@ func setAuthFieldInEncodedTextFile(activeProfile string, key authFieldKey, value profileTextFileFolderName := textFileFolderName if activeProfile != "" { - profileTextFileFolderName = filepath.Join(activeProfile, textFileFolderName) + profileTextFileFolderName = filepath.Join(textFileFolderName, activeProfile) } textFileDir := filepath.Join(configDir, profileTextFileFolderName) @@ -185,7 +185,7 @@ func getAuthFieldFromEncodedTextFile(activeProfile string, key authFieldKey) (st profileTextFileFolderName := textFileFolderName if activeProfile != "" { - profileTextFileFolderName = filepath.Join(activeProfile, textFileFolderName) + profileTextFileFolderName = filepath.Join(textFileFolderName, activeProfile) } textFileDir := filepath.Join(configDir, profileTextFileFolderName) @@ -222,7 +222,7 @@ func createEncodedTextFile(activeProfile string) error { profileTextFileFolderName := textFileFolderName if activeProfile != "" { - profileTextFileFolderName = filepath.Join(activeProfile, textFileFolderName) + profileTextFileFolderName = filepath.Join(textFileFolderName, activeProfile) } textFileDir := filepath.Join(configDir, profileTextFileFolderName) diff --git a/internal/pkg/auth/storage_test.go b/internal/pkg/auth/storage_test.go index 5733ab69a..4d1593020 100644 --- a/internal/pkg/auth/storage_test.go +++ b/internal/pkg/auth/storage_test.go @@ -450,7 +450,7 @@ func deleteAuthFieldInEncodedTextFile(activeProfile string, key authFieldKey) er profileTextFileFolderName := textFileFolderName if activeProfile != "" { - profileTextFileFolderName = filepath.Join(activeProfile, textFileFolderName) + profileTextFileFolderName = filepath.Join(textFileFolderName, activeProfile) } textFileDir := filepath.Join(configDir, profileTextFileFolderName) From 1b224009ba637e3cdef5b1bda09905bf0b64fe70 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Diogo=20Ferr=C3=A3o?= Date: Fri, 24 May 2024 15:05:08 +0100 Subject: [PATCH 11/18] move config/fileutils to new package --- internal/pkg/config/config_test.go | 6 +-- internal/pkg/config/file_utils.go | 26 ---------- internal/pkg/config/file_utils_test.go | 47 ------------------- internal/pkg/config/profiles.go | 3 +- internal/pkg/fileutils/file_utils.go | 20 ++++++++ internal/pkg/fileutils/file_utils_test.go | 42 +++++++++++++++++ .../test-data/empty-file.txt | 0 .../test-data/file-with-content.txt | 0 .../test-data/folder-exists/dummy.txt | 0 9 files changed, 67 insertions(+), 77 deletions(-) delete mode 100644 internal/pkg/config/file_utils.go delete mode 100644 internal/pkg/config/file_utils_test.go rename internal/pkg/{config => fileutils}/test-data/empty-file.txt (100%) rename internal/pkg/{config => fileutils}/test-data/file-with-content.txt (100%) rename internal/pkg/{config => fileutils}/test-data/folder-exists/dummy.txt (100%) diff --git a/internal/pkg/config/config_test.go b/internal/pkg/config/config_test.go index 194ba8047..b3483a635 100644 --- a/internal/pkg/config/config_test.go +++ b/internal/pkg/config/config_test.go @@ -33,10 +33,10 @@ func TestWrite(t *testing.T) { t.Run(tt.description, func(t *testing.T) { configPath := filepath.Join(os.TempDir(), tt.folderName, "config.json") viper.SetConfigFile(configPath) - folderPath = filepath.Dir(configPath) + configFolderPath = filepath.Dir(configPath) if tt.folderExists { - err := os.MkdirAll(folderPath, os.ModePerm) + err := os.MkdirAll(configFolderPath, os.ModePerm) if err != nil { t.Fatalf("expected error to be nil, got %v", err) } @@ -61,7 +61,7 @@ func TestWrite(t *testing.T) { // Delete the folder if tt.folderName != "" { - err = os.Remove(folderPath) + err = os.Remove(configFolderPath) if err != nil { t.Fatalf("expected error to be nil, got %v", err) } diff --git a/internal/pkg/config/file_utils.go b/internal/pkg/config/file_utils.go deleted file mode 100644 index 0895a9dc5..000000000 --- a/internal/pkg/config/file_utils.go +++ /dev/null @@ -1,26 +0,0 @@ -package config - -import ( - "fmt" - "os" -) - -// readFileIfExists reads the contents of a file and returns it as a string, along with a boolean indicating if the file exists. -// If the file does not exist, it returns an empty string and no error. -// If the file exists but cannot be read, it returns an error. -func readFileIfExists(filePath string) (contents string, exists bool, err error) { - _, err = os.Stat(filePath) - if err != nil { - if os.IsNotExist(err) { - return "", false, nil - } - return "", true, err - } - - content, err := os.ReadFile(filePath) - if err != nil { - return "", true, fmt.Errorf("read file: %w", err) - } - - return string(content), true, nil -} diff --git a/internal/pkg/config/file_utils_test.go b/internal/pkg/config/file_utils_test.go deleted file mode 100644 index d7f299cb6..000000000 --- a/internal/pkg/config/file_utils_test.go +++ /dev/null @@ -1,47 +0,0 @@ -package config - -import ( - "testing" -) - -func TestReadFileIfExists(t *testing.T) { - tests := []struct { - description string - filePath string - exists bool - content string - }{ - { - description: "file exists", - filePath: "test-data/file-with-content.txt", - exists: true, - content: "my-content", - }, - { - description: "file does not exist", - filePath: "test-data/file-does-not-exist.txt", - content: "", - }, - { - description: "empty file", - filePath: "test-data/empty-file.txt", - exists: true, - content: "", - }, - } - - for _, tt := range tests { - t.Run(tt.description, func(t *testing.T) { - content, exists, err := readFileIfExists(tt.filePath) - if err != nil { - t.Errorf("read file: %v", err) - } - if exists != tt.exists { - t.Errorf("expected exists to be %t but got %t", tt.exists, exists) - } - if content != tt.content { - t.Errorf("expected content to be %q but got %q", tt.content, content) - } - }) - } -} diff --git a/internal/pkg/config/profiles.go b/internal/pkg/config/profiles.go index ff07fbd7b..d2bae8013 100644 --- a/internal/pkg/config/profiles.go +++ b/internal/pkg/config/profiles.go @@ -7,6 +7,7 @@ import ( "regexp" "github.com/stackitcloud/stackit-cli/internal/pkg/errors" + "github.com/stackitcloud/stackit-cli/internal/pkg/fileutils" "github.com/stackitcloud/stackit-cli/internal/pkg/print" ) @@ -21,7 +22,7 @@ import ( func GetProfile() (string, error) { profile, profileSet := os.LookupEnv("STACKIT_CLI_PROFILE") if !profileSet { - contents, exists, err := readFileIfExists(profileFilePath) + contents, exists, err := fileutils.ReadFileIfExists(profileFilePath) if err != nil { return "", fmt.Errorf("read profile from file: %w", err) } diff --git a/internal/pkg/fileutils/file_utils.go b/internal/pkg/fileutils/file_utils.go index 69a0234c6..1d1cf767e 100644 --- a/internal/pkg/fileutils/file_utils.go +++ b/internal/pkg/fileutils/file_utils.go @@ -26,3 +26,23 @@ func WriteToFile(outputFileName, content string) (err error) { } return err } + +// ReadFileIfExists reads the contents of a file and returns it as a string, along with a boolean indicating if the file exists. +// If the file does not exist, it returns an empty string and no error. +// If the file exists but cannot be read, it returns an error. +func ReadFileIfExists(filePath string) (contents string, exists bool, err error) { + _, err = os.Stat(filePath) + if err != nil { + if os.IsNotExist(err) { + return "", false, nil + } + return "", true, err + } + + content, err := os.ReadFile(filePath) + if err != nil { + return "", true, fmt.Errorf("read file: %w", err) + } + + return string(content), true, nil +} diff --git a/internal/pkg/fileutils/file_utils_test.go b/internal/pkg/fileutils/file_utils_test.go index 746249353..96bf20b12 100644 --- a/internal/pkg/fileutils/file_utils_test.go +++ b/internal/pkg/fileutils/file_utils_test.go @@ -41,3 +41,45 @@ func TestWriteToFile(t *testing.T) { t.Errorf("failed cleaning test data") } } + +func TestReadFileIfExists(t *testing.T) { + tests := []struct { + description string + filePath string + exists bool + content string + }{ + { + description: "file exists", + filePath: "test-data/file-with-content.txt", + exists: true, + content: "my-content", + }, + { + description: "file does not exist", + filePath: "test-data/file-does-not-exist.txt", + content: "", + }, + { + description: "empty file", + filePath: "test-data/empty-file.txt", + exists: true, + content: "", + }, + } + + for _, tt := range tests { + t.Run(tt.description, func(t *testing.T) { + content, exists, err := ReadFileIfExists(tt.filePath) + if err != nil { + t.Errorf("read file: %v", err) + } + if exists != tt.exists { + t.Errorf("expected exists to be %t but got %t", tt.exists, exists) + } + if content != tt.content { + t.Errorf("expected content to be %q but got %q", tt.content, content) + } + }) + } +} diff --git a/internal/pkg/config/test-data/empty-file.txt b/internal/pkg/fileutils/test-data/empty-file.txt similarity index 100% rename from internal/pkg/config/test-data/empty-file.txt rename to internal/pkg/fileutils/test-data/empty-file.txt diff --git a/internal/pkg/config/test-data/file-with-content.txt b/internal/pkg/fileutils/test-data/file-with-content.txt similarity index 100% rename from internal/pkg/config/test-data/file-with-content.txt rename to internal/pkg/fileutils/test-data/file-with-content.txt diff --git a/internal/pkg/config/test-data/folder-exists/dummy.txt b/internal/pkg/fileutils/test-data/folder-exists/dummy.txt similarity index 100% rename from internal/pkg/config/test-data/folder-exists/dummy.txt rename to internal/pkg/fileutils/test-data/folder-exists/dummy.txt From 286c9c800bd427207fd1d8b692040e71d1f5f596 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Diogo=20Ferr=C3=A3o?= Date: Tue, 28 May 2024 08:51:30 +0100 Subject: [PATCH 12/18] Add `stackit profile create` command and refactor `set` command (#351) * new create command, refactor set command * change flag name, improve create behaviour * better error handling, make argument option in set * address PR comments, improve testing for file utils copy file * check for command existance in set and get profile * always check if profile exists in get profile * remove debug print * fix formatting in error --- docs/stackit_config_profile.md | 3 +- docs/stackit_config_profile_create.md | 48 ++++++ docs/stackit_config_profile_set.md | 3 +- docs/stackit_config_profile_unset.md | 2 +- ...ancer_observability-credentials_cleanup.md | 3 +- internal/cmd/config/profile/create/create.go | 116 +++++++++++++ .../cmd/config/profile/create/create_test.go | 156 ++++++++++++++++++ internal/cmd/config/profile/profile.go | 2 + internal/cmd/config/profile/set/set.go | 12 +- internal/pkg/config/config.go | 3 +- internal/pkg/config/profiles.go | 133 ++++++++++++++- internal/pkg/errors/errors.go | 13 ++ internal/pkg/errors/errors_test.go | 26 +++ internal/pkg/fileutils/file_utils.go | 18 ++ internal/pkg/fileutils/file_utils_test.go | 103 +++++++++++- 15 files changed, 620 insertions(+), 21 deletions(-) create mode 100644 docs/stackit_config_profile_create.md create mode 100644 internal/cmd/config/profile/create/create.go create mode 100644 internal/cmd/config/profile/create/create_test.go diff --git a/docs/stackit_config_profile.md b/docs/stackit_config_profile.md index 1c46aeb73..6dd5807a1 100644 --- a/docs/stackit_config_profile.md +++ b/docs/stackit_config_profile.md @@ -24,7 +24,7 @@ stackit config profile [flags] ``` -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"] + -o, --output-format string Output format, one of ["json" "pretty" "none" "yaml"] -p, --project-id string Project ID --verbosity string Verbosity of the CLI, one of ["debug" "info" "warning" "error"] (default "info") ``` @@ -32,6 +32,7 @@ stackit config profile [flags] ### SEE ALSO * [stackit config](./stackit_config.md) - Provides functionality for CLI configuration options +* [stackit config profile create](./stackit_config_profile_create.md) - Creates a CLI configuration profile * [stackit config profile set](./stackit_config_profile_set.md) - Set a CLI configuration profile * [stackit config profile unset](./stackit_config_profile_unset.md) - Unset the current active CLI configuration profile diff --git a/docs/stackit_config_profile_create.md b/docs/stackit_config_profile_create.md new file mode 100644 index 000000000..5f9a45af3 --- /dev/null +++ b/docs/stackit_config_profile_create.md @@ -0,0 +1,48 @@ +## stackit config profile create + +Creates a CLI configuration profile + +### Synopsis + +Creates a CLI configuration profile based on the currently active profile and sets it as active. +The profile name can be provided via the STACKIT_CLI_PROFILE environment variable or as an argument in this command. +The environment variable takes precedence over the argument. +If you do not want to set the profile as active, use the --no-set flag. +If you want to create the new profile with the initial default configurations, use the --empty flag. + +``` +stackit config profile create PROFILE [flags] +``` + +### Examples + +``` + Create a new configuration profile "my-profile" with the current configuration, setting it as the active profile + $ stackit config profile create my-profile + + Create a new configuration profile "my-profile" with a default initial configuration and don't set it as the active profile + $ stackit config profile create my-profile --empty --no-set +``` + +### Options + +``` + --empty Create the profile with the initial default configurations + -h, --help Help for "stackit config profile create" + --no-set Do not set the profile as the active profile +``` + +### 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 + --verbosity string Verbosity of the CLI, one of ["debug" "info" "warning" "error"] (default "info") +``` + +### SEE ALSO + +* [stackit config profile](./stackit_config_profile.md) - Manage the CLI configuration profiles + diff --git a/docs/stackit_config_profile_set.md b/docs/stackit_config_profile_set.md index bcc9725a6..a39604ff1 100644 --- a/docs/stackit_config_profile_set.md +++ b/docs/stackit_config_profile_set.md @@ -7,7 +7,6 @@ Set a CLI configuration profile Set a CLI configuration profile as the active profile. The profile to be used can be managed via the STACKIT_CLI_PROFILE environment variable or using the "stackit config profile set PROFILE" and "stackit config profile unset" commands. The environment variable takes precedence over what is set via the commands. -A new profile is created automatically if it does not exist. When no profile is set, the default profile is used. ``` @@ -32,7 +31,7 @@ stackit config profile set PROFILE [flags] ``` -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"] + -o, --output-format string Output format, one of ["json" "pretty" "none" "yaml"] -p, --project-id string Project ID --verbosity string Verbosity of the CLI, one of ["debug" "info" "warning" "error"] (default "info") ``` diff --git a/docs/stackit_config_profile_unset.md b/docs/stackit_config_profile_unset.md index 0beda0f67..410469005 100644 --- a/docs/stackit_config_profile_unset.md +++ b/docs/stackit_config_profile_unset.md @@ -29,7 +29,7 @@ stackit config profile unset [flags] ``` -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"] + -o, --output-format string Output format, one of ["json" "pretty" "none" "yaml"] -p, --project-id string Project ID --verbosity string Verbosity of the CLI, one of ["debug" "info" "warning" "error"] (default "info") ``` diff --git a/docs/stackit_load-balancer_observability-credentials_cleanup.md b/docs/stackit_load-balancer_observability-credentials_cleanup.md index 6f72a76c2..f22f88994 100644 --- a/docs/stackit_load-balancer_observability-credentials_cleanup.md +++ b/docs/stackit_load-balancer_observability-credentials_cleanup.md @@ -35,4 +35,5 @@ stackit load-balancer observability-credentials cleanup [flags] ### SEE ALSO -- [stackit load-balancer observability-credentials](./stackit_load-balancer_observability-credentials.md) - Provides functionality for Load Balancer observability credentials +* [stackit load-balancer observability-credentials](./stackit_load-balancer_observability-credentials.md) - Provides functionality for Load Balancer observability credentials + diff --git a/internal/cmd/config/profile/create/create.go b/internal/cmd/config/profile/create/create.go new file mode 100644 index 000000000..ceec4ee04 --- /dev/null +++ b/internal/cmd/config/profile/create/create.go @@ -0,0 +1,116 @@ +package create + +import ( + "fmt" + + "github.com/stackitcloud/stackit-cli/internal/pkg/args" + "github.com/stackitcloud/stackit-cli/internal/pkg/auth" + "github.com/stackitcloud/stackit-cli/internal/pkg/config" + "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" + + "github.com/spf13/cobra" +) + +const ( + profileArg = "PROFILE" + + noSetFlag = "no-set" + fromEmptyProfile = "empty" +) + +type inputModel struct { + *globalflags.GlobalFlagModel + NoSet bool + FromEmptyProfile bool + Profile string +} + +func NewCmd(p *print.Printer) *cobra.Command { + cmd := &cobra.Command{ + Use: fmt.Sprintf("create %s", profileArg), + Short: "Creates a CLI configuration profile", + Long: fmt.Sprintf("%s\n%s\n%s\n%s\n%s", + "Creates a CLI configuration profile based on the currently active profile and sets it as active.", + `The profile name can be provided via the STACKIT_CLI_PROFILE environment variable or as an argument in this command.`, + "The environment variable takes precedence over the argument.", + "If you do not want to set the profile as active, use the --no-set flag.", + "If you want to create the new profile with the initial default configurations, use the --empty flag.", + ), + Args: args.SingleArg(profileArg, nil), + Example: examples.Build( + examples.NewExample( + `Create a new configuration profile "my-profile" with the current configuration, setting it as the active profile`, + "$ stackit config profile create my-profile"), + examples.NewExample( + `Create a new configuration profile "my-profile" with a default initial configuration and don't set it as the active profile`, + "$ stackit config profile create my-profile --empty --no-set"), + ), + RunE: func(cmd *cobra.Command, args []string) error { + model, err := parseInput(p, cmd, args) + if err != nil { + return err + } + + err = config.CreateProfile(p, model.Profile, !model.NoSet, model.FromEmptyProfile) + if err != nil { + return fmt.Errorf("create profile: %w", err) + } + + if model.NoSet { + p.Info("Successfully created profile %q\n", model.Profile) + return nil + } + + p.Info("Successfully created and set active profile to %q\n", model.Profile) + + flow, err := auth.GetAuthFlow() + if err != nil { + p.Debug(print.WarningLevel, "both keyring and text file storage failed to find a valid authentication flow for the active profile") + p.Warn("The active profile %q is not authenticated, please login using the 'stackit auth login' command.\n", model.Profile) + return nil + } + p.Debug(print.DebugLevel, "found valid authentication flow for active profile: %s", flow) + + return nil + }, + } + configureFlags(cmd) + return cmd +} + +func configureFlags(cmd *cobra.Command) { + cmd.Flags().Bool(noSetFlag, false, "Do not set the profile as the active profile") + cmd.Flags().Bool(fromEmptyProfile, false, "Create the profile with the initial default configurations") +} + +func parseInput(p *print.Printer, cmd *cobra.Command, inputArgs []string) (*inputModel, error) { + profile := inputArgs[0] + + err := config.ValidateProfile(profile) + if err != nil { + return nil, err + } + + globalFlags := globalflags.Parse(p, cmd) + + model := inputModel{ + GlobalFlagModel: globalFlags, + Profile: profile, + FromEmptyProfile: flags.FlagToBoolValue(p, cmd, fromEmptyProfile), + NoSet: flags.FlagToBoolValue(p, cmd, noSetFlag), + } + + if p.IsVerbosityDebug() { + modelStr, err := print.BuildDebugStrFromInputModel(model) + if err != nil { + p.Debug(print.ErrorLevel, "convert model to string for debugging: %v", err) + } else { + p.Debug(print.DebugLevel, "parsed input values: %s", modelStr) + } + } + + return &model, nil +} diff --git a/internal/cmd/config/profile/create/create_test.go b/internal/cmd/config/profile/create/create_test.go new file mode 100644 index 000000000..0cc32cc9d --- /dev/null +++ b/internal/cmd/config/profile/create/create_test.go @@ -0,0 +1,156 @@ +package create + +import ( + "testing" + + "github.com/stackitcloud/stackit-cli/internal/pkg/globalflags" + "github.com/stackitcloud/stackit-cli/internal/pkg/print" + + "github.com/google/go-cmp/cmp" +) + +const testProfile = "test-profile" + +func fixtureArgValues(mods ...func(argValues []string)) []string { + argValues := []string{ + testProfile, + } + for _, mod := range mods { + mod(argValues) + } + return argValues +} + +func fixtureInputModel(mods ...func(model *inputModel)) *inputModel { + model := &inputModel{ + GlobalFlagModel: &globalflags.GlobalFlagModel{ + Verbosity: globalflags.VerbosityDefault, + }, + Profile: testProfile, + FromEmptyProfile: false, + NoSet: 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", + argValues: fixtureArgValues(), + isValid: true, + expectedModel: fixtureInputModel(), + }, + { + description: "no values", + argValues: []string{}, + flagValues: map[string]string{}, + isValid: false, + }, + { + description: "no arg values", + argValues: []string{}, + isValid: false, + }, + { + description: "some global flag", + argValues: fixtureArgValues(), + flagValues: map[string]string{ + globalflags.VerbosityFlag: globalflags.DebugVerbosity, + }, + isValid: true, + expectedModel: fixtureInputModel(func(model *inputModel) { + model.GlobalFlagModel.Verbosity = globalflags.DebugVerbosity + }), + }, + { + description: "invalid profile", + argValues: []string{"invalid-profile-&"}, + isValid: false, + }, + { + description: "use default given", + argValues: fixtureArgValues(), + flagValues: map[string]string{ + fromEmptyProfile: "true", + }, + isValid: true, + expectedModel: fixtureInputModel(func(model *inputModel) { + model.FromEmptyProfile = true + }), + }, + { + description: "no set given", + argValues: fixtureArgValues(), + flagValues: map[string]string{ + noSetFlag: "true", + }, + isValid: true, + expectedModel: fixtureInputModel(func(model *inputModel) { + model.NoSet = true + }), + }, + } + + for _, tt := range tests { + t.Run(tt.description, func(t *testing.T) { + p := print.NewPrinter() + cmd := NewCmd(p) + err := globalflags.Configure(cmd.Flags()) + if err != nil { + t.Fatalf("configure global flags: %v", err) + } + + for flag, value := range tt.flagValues { + err := cmd.Flags().Set(flag, value) + if err != nil { + if !tt.isValid { + return + } + t.Fatalf("setting flag --%s=%s: %v", flag, value, err) + } + } + + err = cmd.ValidateArgs(tt.argValues) + if err != nil { + if !tt.isValid { + return + } + t.Fatalf("error validating args: %v", err) + } + + err = cmd.ValidateRequiredFlags() + if err != nil { + if !tt.isValid { + return + } + t.Fatalf("error validating flags: %v", err) + } + + model, err := parseInput(p, cmd, tt.argValues) + if err != nil { + if !tt.isValid { + return + } + t.Fatalf("error parsing input: %v", err) + } + + if !tt.isValid { + t.Fatalf("did not fail on invalid input") + } + diff := cmp.Diff(model, tt.expectedModel) + if diff != "" { + t.Fatalf("Data does not match: %s", diff) + } + }) + } +} diff --git a/internal/cmd/config/profile/profile.go b/internal/cmd/config/profile/profile.go index 42bea8e58..638359c68 100644 --- a/internal/cmd/config/profile/profile.go +++ b/internal/cmd/config/profile/profile.go @@ -3,6 +3,7 @@ package profile import ( "fmt" + "github.com/stackitcloud/stackit-cli/internal/cmd/config/profile/create" "github.com/stackitcloud/stackit-cli/internal/cmd/config/profile/set" "github.com/stackitcloud/stackit-cli/internal/cmd/config/profile/unset" "github.com/stackitcloud/stackit-cli/internal/pkg/args" @@ -32,4 +33,5 @@ func NewCmd(p *print.Printer) *cobra.Command { func addSubcommands(cmd *cobra.Command, p *print.Printer) { cmd.AddCommand(set.NewCmd(p)) cmd.AddCommand(unset.NewCmd(p)) + cmd.AddCommand(create.NewCmd(p)) } diff --git a/internal/cmd/config/profile/set/set.go b/internal/cmd/config/profile/set/set.go index 3594883fe..ac43977b3 100644 --- a/internal/cmd/config/profile/set/set.go +++ b/internal/cmd/config/profile/set/set.go @@ -6,6 +6,7 @@ import ( "github.com/stackitcloud/stackit-cli/internal/pkg/args" "github.com/stackitcloud/stackit-cli/internal/pkg/auth" "github.com/stackitcloud/stackit-cli/internal/pkg/config" + "github.com/stackitcloud/stackit-cli/internal/pkg/errors" "github.com/stackitcloud/stackit-cli/internal/pkg/examples" "github.com/stackitcloud/stackit-cli/internal/pkg/globalflags" "github.com/stackitcloud/stackit-cli/internal/pkg/print" @@ -26,11 +27,10 @@ func NewCmd(p *print.Printer) *cobra.Command { cmd := &cobra.Command{ Use: fmt.Sprintf("set %s", profileArg), Short: "Set a CLI configuration profile", - Long: fmt.Sprintf("%s\n%s\n%s\n%s\n%s", + Long: fmt.Sprintf("%s\n%s\n%s\n%s", "Set a CLI configuration profile as the active profile.", `The profile to be used can be managed via the STACKIT_CLI_PROFILE environment variable or using the "stackit config profile set PROFILE" and "stackit config profile unset" commands.`, "The environment variable takes precedence over what is set via the commands.", - "A new profile is created automatically if it does not exist.", "When no profile is set, the default profile is used.", ), Args: args.SingleArg(profileArg, nil), @@ -45,6 +45,14 @@ func NewCmd(p *print.Printer) *cobra.Command { return err } + profileExists, err := config.ProfileExists(model.Profile) + if err != nil { + return fmt.Errorf("check if profile exists: %w", err) + } + if !profileExists { + return &errors.SetInexistentProfile{Profile: model.Profile} + } + err = config.SetProfile(p, model.Profile) if err != nil { return fmt.Errorf("set profile: %w", err) diff --git a/internal/pkg/config/config.go b/internal/pkg/config/config.go index b9abf9b31..9ca50bccb 100644 --- a/internal/pkg/config/config.go +++ b/internal/pkg/config/config.go @@ -46,6 +46,7 @@ const ( configFileName = "cli-config" configFileExtension = "json" + profileRootFolder = "profiles" profileFileName = "cli-profile" profileFileExtension = "txt" ) @@ -92,7 +93,7 @@ func InitConfig() { configFolderPath = defaultConfigFolderPath if configProfile != "" { - configFolderPath = filepath.Join(configFolderPath, configProfile) // If a profile is set, use the profile config folder + configFolderPath = filepath.Join(configFolderPath, profileRootFolder, configProfile) // If a profile is set, use the profile config folder } configFilePath := filepath.Join(configFolderPath, fmt.Sprintf("%s.%s", configFileName, configFileExtension)) diff --git a/internal/pkg/config/profiles.go b/internal/pkg/config/profiles.go index d2bae8013..833176ae1 100644 --- a/internal/pkg/config/profiles.go +++ b/internal/pkg/config/profiles.go @@ -11,6 +11,8 @@ import ( "github.com/stackitcloud/stackit-cli/internal/pkg/print" ) +const ProfileEnvVar = "STACKIT_CLI_PROFILE" + // GetProfile returns the current profile to be used by the CLI. // // The profile is determined by the value of the STACKIT_CLI_PROFILE environment variable, or, if not set, @@ -20,8 +22,8 @@ import ( // // If the profile is not valid, it returns an error. func GetProfile() (string, error) { - profile, profileSet := os.LookupEnv("STACKIT_CLI_PROFILE") - if !profileSet { + profile, profileSetInEnv := GetProfileFromEnv() + if !profileSetInEnv { contents, exists, err := fileutils.ReadFileIfExists(profileFilePath) if err != nil { return "", fmt.Errorf("read profile from file: %w", err) @@ -32,13 +34,113 @@ func GetProfile() (string, error) { profile = contents } - err := ValidateProfile(profile) + // Make sure the profile exists + profileExists, err := ProfileExists(profile) + if err != nil { + return "", fmt.Errorf("check if profile exists: %w", err) + } + if !profileExists { + return "", &errors.SetInexistentProfile{Profile: profile} + } + + err = ValidateProfile(profile) if err != nil { return "", fmt.Errorf("validate profile: %w", err) } return profile, nil } +// GetProfileFromEnv returns the profile from the environment variable. +// If the environment variable is not set, it returns an empty string. +// If the profile is not valid, it returns an error. +func GetProfileFromEnv() (string, bool) { + return os.LookupEnv(ProfileEnvVar) +} + +// CreateProfile creates a new profile. +// If emptyProfile is true, it creates an empty profile. Otherwise, copies the config from the current profile to the new profile. +// If setProfile is true, it sets the new profile as the active profile. +// If the profile already exists, it returns an error. +func CreateProfile(p *print.Printer, profile string, setProfile, emptyProfile bool) error { + err := ValidateProfile(profile) + if err != nil { + return fmt.Errorf("validate profile: %w", err) + } + + configFolderPath = filepath.Join(defaultConfigFolderPath, profileRootFolder, profile) + + // Error if the profile already exists + _, err = os.Stat(configFolderPath) + if err == nil { + return fmt.Errorf("profile %q already exists", profile) + } + + err = os.MkdirAll(configFolderPath, os.ModePerm) + if err != nil { + return fmt.Errorf("create config folder: %w", err) + } + p.Debug(print.DebugLevel, "created folder for the new profile: %s", configFolderPath) + + currentProfile, err := GetProfile() + if err != nil { + // Cleanup created directory + cleanupErr := os.RemoveAll(configFolderPath) + if cleanupErr != nil { + return fmt.Errorf("get active profile: %w, cleanup directories: %w", err, cleanupErr) + } + return fmt.Errorf("get active profile: %w", err) + } + + p.Debug(print.DebugLevel, "current active profile: %q", currentProfile) + + if !emptyProfile { + p.Debug(print.DebugLevel, "duplicating profile configuration from %q to new profile %q", currentProfile, profile) + err = DuplicateProfileConfiguration(p, currentProfile, profile) + if err != nil { + // Cleanup created directory + cleanupErr := os.RemoveAll(configFolderPath) + if cleanupErr != nil { + return fmt.Errorf("get active profile: %w, cleanup directories: %w", err, cleanupErr) + } + return fmt.Errorf("duplicate profile configuration: %w", err) + } + } + + if setProfile { + err = SetProfile(p, profile) + if err != nil { + return fmt.Errorf("set profile: %w", err) + } + } + + return nil +} + +// DuplicateProfileConfiguration duplicates the current profile configuration to a new profile. +// It copies the config file from the current profile to the new profile. +// If the current profile does not exist, it returns an error. +// If the new profile already exists, it will be overwritten. +func DuplicateProfileConfiguration(p *print.Printer, currentProfile, newProfile string) error { + var currentConfigFilePath string + // If the current profile is empty, its the default profile + if currentProfile == "" { + currentConfigFilePath = filepath.Join(defaultConfigFolderPath, fmt.Sprintf("%s.%s", configFileName, configFileExtension)) + } else { + currentConfigFilePath = filepath.Join(defaultConfigFolderPath, profileRootFolder, currentProfile, fmt.Sprintf("%s.%s", configFileName, configFileExtension)) + } + + newConfigFilePath := filepath.Join(configFolderPath, fmt.Sprintf("%s.%s", configFileName, configFileExtension)) + + err := fileutils.CopyFile(currentConfigFilePath, newConfigFilePath) + if err != nil { + return fmt.Errorf("copy config file: %w", err) + } + + p.Debug(print.DebugLevel, "created new configuration for profile %q based on %q in: %s", newProfile, currentProfile, newConfigFilePath) + + return nil +} + // SetProfile sets the profile to be used by the CLI. func SetProfile(p *print.Printer, profile string) error { err := ValidateProfile(profile) @@ -46,6 +148,15 @@ func SetProfile(p *print.Printer, profile string) error { return fmt.Errorf("validate profile: %w", err) } + profileExists, err := ProfileExists(profile) + if err != nil { + return fmt.Errorf("check if profile exists: %w", err) + } + + if !profileExists { + return fmt.Errorf("profile %q does not exist", profile) + } + err = os.WriteFile(profileFilePath, []byte(profile), os.ModePerm) if err != nil { return fmt.Errorf("write profile to file: %w", err) @@ -53,11 +164,6 @@ func SetProfile(p *print.Printer, profile string) error { p.Debug(print.DebugLevel, "persisted new active profile in: %s", profileFilePath) configFolderPath = filepath.Join(defaultConfigFolderPath, profile) - err = os.MkdirAll(configFolderPath, os.ModePerm) - if err != nil { - return fmt.Errorf("create config folder: %w", err) - } - p.Debug(print.DebugLevel, "created folder for the new profile: %s", configFolderPath) p.Debug(print.DebugLevel, "profile %q is now active", profile) return nil @@ -89,3 +195,14 @@ func ValidateProfile(profile string) error { } return nil } + +func ProfileExists(profile string) (bool, error) { + _, err := os.Stat(filepath.Join(defaultConfigFolderPath, profileRootFolder, profile)) + if err != nil { + if os.IsNotExist(err) { + return false, nil + } + return false, fmt.Errorf("get profile folder: %w", err) + } + return true, nil +} diff --git a/internal/pkg/errors/errors.go b/internal/pkg/errors/errors.go index 1cfe6bce8..e32cc5bae 100644 --- a/internal/pkg/errors/errors.go +++ b/internal/pkg/errors/errors.go @@ -38,6 +38,11 @@ Please double check if they are correctly configured. For more details run: $ stackit auth activate-service-account -h` + SET_INEXISTENT_PROFILE = `the configuration profile %[1]q does not exist. + +To create it, run: + $ stackit config profile create %[1]q` + ARGUS_INVALID_INPUT_PLAN = `the instance plan was not correctly provided. Either provide the plan ID: @@ -147,6 +152,14 @@ func (e *ActivateServiceAccountError) Error() string { return FAILED_SERVICE_ACCOUNT_ACTIVATION } +type SetInexistentProfile struct { + Profile string +} + +func (e *SetInexistentProfile) Error() string { + return fmt.Sprintf(SET_INEXISTENT_PROFILE, e.Profile) +} + type ArgusInputPlanError struct { Cmd *cobra.Command Args []string diff --git a/internal/pkg/errors/errors_test.go b/internal/pkg/errors/errors_test.go index 8e7999d23..4756cd812 100644 --- a/internal/pkg/errors/errors_test.go +++ b/internal/pkg/errors/errors_test.go @@ -101,6 +101,32 @@ func TestArgusInputPlanError(t *testing.T) { } } +func TestSetInexistentProfile(t *testing.T) { + tests := []struct { + description string + profile string + expectedMsg string + }{ + { + description: "base", + profile: "profile", + expectedMsg: fmt.Sprintf(SET_INEXISTENT_PROFILE, "profile", "profile"), + }, + } + + for _, tt := range tests { + t.Run(tt.description, func(t *testing.T) { + err := &SetInexistentProfile{ + Profile: tt.profile, + } + + if err.Error() != tt.expectedMsg { + t.Fatalf("expected error to be %s, got %s", tt.expectedMsg, err.Error()) + } + }) + } +} + func TestArgusInvalidPlanError(t *testing.T) { tests := []struct { description string diff --git a/internal/pkg/fileutils/file_utils.go b/internal/pkg/fileutils/file_utils.go index 1d1cf767e..c9f6770be 100644 --- a/internal/pkg/fileutils/file_utils.go +++ b/internal/pkg/fileutils/file_utils.go @@ -5,6 +5,8 @@ import ( "os" ) +// WriteToFile writes the given content to a file. +// If the file already exists, it will be overwritten. func WriteToFile(outputFileName, content string) (err error) { fo, err := os.Create(outputFileName) if err != nil { @@ -46,3 +48,19 @@ func ReadFileIfExists(filePath string) (contents string, exists bool, err error) return string(content), true, nil } + +// CopyFile copies the contents of a file to another file. +// If the destination file already exists, it will be overwritten. +func CopyFile(src, dst string) (err error) { + contents, err := os.ReadFile(src) + if err != nil { + return fmt.Errorf("read source file: %w", err) + } + + err = WriteToFile(dst, string(contents)) + if err != nil { + return fmt.Errorf("write destination file: %w", err) + } + + return nil +} diff --git a/internal/pkg/fileutils/file_utils_test.go b/internal/pkg/fileutils/file_utils_test.go index 96bf20b12..e979bef94 100644 --- a/internal/pkg/fileutils/file_utils_test.go +++ b/internal/pkg/fileutils/file_utils_test.go @@ -2,6 +2,7 @@ package fileutils import ( "os" + "path/filepath" "testing" ) @@ -31,14 +32,14 @@ func TestWriteToFile(t *testing.T) { t.Fatalf("unexpected error: %s", err.Error()) } if string(output) != tt.content { - t.Errorf("unexpected output: got %q, want %q", output, tt.content) + t.Fatalf("unexpected output: got %q, want %q", output, tt.content) } }) } // Cleanup err := os.RemoveAll(outputFilePath) if err != nil { - t.Errorf("failed cleaning test data") + t.Fatalf("failed cleaning test data") } } @@ -72,13 +73,105 @@ func TestReadFileIfExists(t *testing.T) { t.Run(tt.description, func(t *testing.T) { content, exists, err := ReadFileIfExists(tt.filePath) if err != nil { - t.Errorf("read file: %v", err) + t.Fatalf("read file: %v", err) } if exists != tt.exists { - t.Errorf("expected exists to be %t but got %t", tt.exists, exists) + t.Fatalf("expected exists to be %t but got %t", tt.exists, exists) } if content != tt.content { - t.Errorf("expected content to be %q but got %q", tt.content, content) + t.Fatalf("expected content to be %q but got %q", tt.content, content) + } + }) + } +} + +func TestCopyFile(t *testing.T) { + tests := []struct { + description string + srcExists bool + destExists bool + content string + isValid bool + }{ + { + description: "copy file", + srcExists: true, + content: "my-content", + isValid: true, + }, + { + description: "copy empty file", + srcExists: true, + content: "", + isValid: true, + }, + { + description: "copy non-existent file", + srcExists: false, + content: "", + isValid: false, + }, + { + description: "copy file to existing file", + srcExists: true, + destExists: true, + content: "my-content", + isValid: true, + }, + } + for _, tt := range tests { + t.Run(tt.description, func(t *testing.T) { + basePath := filepath.Join(os.TempDir(), "test-data") + src := filepath.Join(basePath, "file-with-content.txt") + dst := filepath.Join(basePath, "file-with-content-copy.txt") + + err := os.MkdirAll(basePath, os.ModePerm) + if err != nil { + t.Fatalf("unexpected error: %s", err.Error()) + } + + if tt.srcExists { + err := WriteToFile(src, tt.content) + if err != nil { + t.Fatalf("unexpected error: %s", err.Error()) + } + } + + if tt.destExists { + err := WriteToFile(dst, "existing-content") + if err != nil { + t.Fatalf("unexpected error: %s", err.Error()) + } + } + + err = CopyFile(src, dst) + if err != nil { + if tt.isValid { + t.Fatalf("unexpected error: %s", err.Error()) + } + return + } + if !tt.isValid { + t.Fatalf("expected error but got none") + } + + content, exists, err := ReadFileIfExists(dst) + if err != nil { + t.Fatalf("read file: %v", err) + } + + if !exists { + t.Fatalf("expected file to exist but it does not") + } + + if content != tt.content { + t.Fatalf("expected content to be %q but got %q", tt.content, content) + } + + // Cleanup + err = os.RemoveAll(basePath) + if err != nil { + t.Fatalf("failed cleaning test data") } }) } From 48f881b0c3d986f93b5feec6bfdb6fbddbb8c404 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Diogo=20Ferr=C3=A3o?= Date: Tue, 28 May 2024 10:55:32 +0100 Subject: [PATCH 13/18] fix profile existence check (#356) --- internal/cmd/root.go | 9 +++++-- internal/pkg/config/config.go | 5 ++-- internal/pkg/config/profiles.go | 40 ++++++++++++++++------------ internal/pkg/errors/errors.go | 7 +++-- internal/pkg/fileutils/file_utils.go | 2 +- 5 files changed, 39 insertions(+), 24 deletions(-) diff --git a/internal/cmd/root.go b/internal/cmd/root.go index c09feab57..62a5b0b6e 100644 --- a/internal/cmd/root.go +++ b/internal/cmd/root.go @@ -60,8 +60,13 @@ func NewRootCmd(version, date string, p *print.Printer) *cobra.Command { if err != nil { return fmt.Errorf("get profile: %w", err) } - if activeProfile == "" { - activeProfile = "(no active profile, the default profile configuration will be used)" + + profileExists, err := config.ProfileExists(activeProfile) + if err != nil { + return fmt.Errorf("check if profile exists: %w", err) + } + if !profileExists { + p.Warn("active profile does not exist, the default profile configuration will be used\n") } p.Debug(print.DebugLevel, "active configuration profile: %s", activeProfile) diff --git a/internal/pkg/config/config.go b/internal/pkg/config/config.go index 9ca50bccb..b6ab86ea3 100644 --- a/internal/pkg/config/config.go +++ b/internal/pkg/config/config.go @@ -41,7 +41,8 @@ const ( ) const ( - configFolder = "stackit" + configFolder = "stackit" + defaultProfileName = "default" configFileName = "cli-config" configFileExtension = "json" @@ -92,7 +93,7 @@ func InitConfig() { cobra.CheckErr(err) configFolderPath = defaultConfigFolderPath - if configProfile != "" { + if configProfile != defaultProfileName { configFolderPath = filepath.Join(configFolderPath, profileRootFolder, configProfile) // If a profile is set, use the profile config folder } diff --git a/internal/pkg/config/profiles.go b/internal/pkg/config/profiles.go index 833176ae1..9723c3862 100644 --- a/internal/pkg/config/profiles.go +++ b/internal/pkg/config/profiles.go @@ -18,7 +18,7 @@ const ProfileEnvVar = "STACKIT_CLI_PROFILE" // The profile is determined by the value of the STACKIT_CLI_PROFILE environment variable, or, if not set, // by the contents of the profile file in the CLI config folder. // -// If the environment variable is not set and the profile file does not exist, it returns an empty string. +// If the profile is not set (env var or profile file) or is set but does not exist, it falls back to the default profile. // // If the profile is not valid, it returns an error. func GetProfile() (string, error) { @@ -29,7 +29,7 @@ func GetProfile() (string, error) { return "", fmt.Errorf("read profile from file: %w", err) } if !exists { - return "", nil + return defaultProfileName, nil } profile = contents } @@ -40,7 +40,7 @@ func GetProfile() (string, error) { return "", fmt.Errorf("check if profile exists: %w", err) } if !profileExists { - return "", &errors.SetInexistentProfile{Profile: profile} + return defaultProfileName, nil } err = ValidateProfile(profile) @@ -67,6 +67,13 @@ func CreateProfile(p *print.Printer, profile string, setProfile, emptyProfile bo return fmt.Errorf("validate profile: %w", err) } + // Cannot create a profile with the default name + if profile == defaultProfileName { + return &errors.InvalidProfileNameError{ + Profile: profile, + } + } + configFolderPath = filepath.Join(defaultConfigFolderPath, profileRootFolder, profile) // Error if the profile already exists @@ -81,19 +88,18 @@ func CreateProfile(p *print.Printer, profile string, setProfile, emptyProfile bo } p.Debug(print.DebugLevel, "created folder for the new profile: %s", configFolderPath) - currentProfile, err := GetProfile() - if err != nil { - // Cleanup created directory - cleanupErr := os.RemoveAll(configFolderPath) - if cleanupErr != nil { - return fmt.Errorf("get active profile: %w, cleanup directories: %w", err, cleanupErr) + if !emptyProfile { + currentProfile, err := GetProfile() + if err != nil { + // Cleanup created directory + cleanupErr := os.RemoveAll(configFolderPath) + if cleanupErr != nil { + return fmt.Errorf("get active profile: %w, cleanup directories: %w", err, cleanupErr) + } + return fmt.Errorf("get active profile: %w", err) } - return fmt.Errorf("get active profile: %w", err) - } - - p.Debug(print.DebugLevel, "current active profile: %q", currentProfile) - if !emptyProfile { + p.Debug(print.DebugLevel, "current active profile: %q", currentProfile) p.Debug(print.DebugLevel, "duplicating profile configuration from %q to new profile %q", currentProfile, profile) err = DuplicateProfileConfiguration(p, currentProfile, profile) if err != nil { @@ -122,8 +128,8 @@ func CreateProfile(p *print.Printer, profile string, setProfile, emptyProfile bo // If the new profile already exists, it will be overwritten. func DuplicateProfileConfiguration(p *print.Printer, currentProfile, newProfile string) error { var currentConfigFilePath string - // If the current profile is empty, its the default profile - if currentProfile == "" { + + if currentProfile == defaultProfileName { currentConfigFilePath = filepath.Join(defaultConfigFolderPath, fmt.Sprintf("%s.%s", configFileName, configFileExtension)) } else { currentConfigFilePath = filepath.Join(defaultConfigFolderPath, profileRootFolder, currentProfile, fmt.Sprintf("%s.%s", configFileName, configFileExtension)) @@ -154,7 +160,7 @@ func SetProfile(p *print.Printer, profile string) error { } if !profileExists { - return fmt.Errorf("profile %q does not exist", profile) + return &errors.SetInexistentProfile{Profile: profile} } err = os.WriteFile(profileFilePath, []byte(profile), os.ModePerm) diff --git a/internal/pkg/errors/errors.go b/internal/pkg/errors/errors.go index e32cc5bae..4839698ae 100644 --- a/internal/pkg/errors/errors.go +++ b/internal/pkg/errors/errors.go @@ -38,7 +38,10 @@ Please double check if they are correctly configured. For more details run: $ stackit auth activate-service-account -h` - SET_INEXISTENT_PROFILE = `the configuration profile %[1]q does not exist. + SET_INEXISTENT_PROFILE = `the active configuration profile %[1]q does not exist. + +To unset it, run: + $ stackit config profile unset To create it, run: $ stackit config profile create %[1]q` @@ -122,7 +125,7 @@ For more details on the available storages for the configured flavor (%[3]s), ru INVALID_PROFILE_NAME = `the profile name %q is invalid. -The profile name can only contain letters, numbers, and "-" and cannot be empty.` +The profile name can only contain letters, numbers, and "-" and cannot be empty or "default".` USAGE_TIP = `For usage help, run: $ %s --help` diff --git a/internal/pkg/fileutils/file_utils.go b/internal/pkg/fileutils/file_utils.go index c9f6770be..b20bb7779 100644 --- a/internal/pkg/fileutils/file_utils.go +++ b/internal/pkg/fileutils/file_utils.go @@ -30,7 +30,7 @@ func WriteToFile(outputFileName, content string) (err error) { } // ReadFileIfExists reads the contents of a file and returns it as a string, along with a boolean indicating if the file exists. -// If the file does not exist, it returns an empty string and no error. +// If the file does not exist, it returns an empty string, false and no error. // If the file exists but cannot be read, it returns an error. func ReadFileIfExists(filePath string) (contents string, exists bool, err error) { _, err = os.Stat(filePath) From 5601f192e104b52494579a17b7477767bd4e93f8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Diogo=20Ferr=C3=A3o?= Date: Wed, 29 May 2024 10:56:46 +0100 Subject: [PATCH 14/18] fix issues in acceptance (#357) * fix issues in acceptance * fix storage auth file * add debug logs * support duplication of empty profile * fix auth storage tests * add debug logs, improve documentation * fix GetConfiguredProfile * improve debug logs --- internal/cmd/root.go | 22 +++++--- internal/pkg/auth/storage.go | 39 ++----------- internal/pkg/auth/storage_test.go | 64 ++++++++++++++++----- internal/pkg/config/config.go | 39 ++++++++----- internal/pkg/config/config_test.go | 56 +++++++++++++++++++ internal/pkg/config/profiles.go | 84 ++++++++++++++++++++-------- internal/pkg/config/profiles_test.go | 50 ++++++++++++++++- 7 files changed, 260 insertions(+), 94 deletions(-) diff --git a/internal/cmd/root.go b/internal/cmd/root.go index 62a5b0b6e..b35dd546e 100644 --- a/internal/cmd/root.go +++ b/internal/cmd/root.go @@ -56,18 +56,24 @@ func NewRootCmd(version, date string, p *print.Printer) *cobra.Command { configFilePath := viper.ConfigFileUsed() p.Debug(print.DebugLevel, "configuration is persisted and read from: %s", configFilePath) - activeProfile, err := config.GetProfile() + profileSet, activeProfile, configMethod, err := config.GetConfiguredProfile() if err != nil { - return fmt.Errorf("get profile: %w", err) + return fmt.Errorf("get configured profile: %w", err) } - profileExists, err := config.ProfileExists(activeProfile) - if err != nil { - return fmt.Errorf("check if profile exists: %w", err) - } - if !profileExists { - p.Warn("active profile does not exist, the default profile configuration will be used\n") + p.Debug(print.DebugLevel, "read configuration profile %q via %s", profileSet, configMethod) + + if activeProfile != profileSet { + if configMethod == "" { + p.Debug(print.DebugLevel, "no profile is configured in env var or profile file") + } else { + p.Debug(print.DebugLevel, "the configured profile %q does not exist: folder %q is missing", profileSet, config.GetProfileFolderPath(profileSet)) + } + p.Debug(print.DebugLevel, "the %q profile will be used", activeProfile) + + p.Warn("configured profile %q does not exist, the %q profile configuration will be used\n", profileSet, activeProfile) } + p.Debug(print.DebugLevel, "active configuration profile: %s", activeProfile) configKeys := viper.AllSettings() diff --git a/internal/pkg/auth/storage.go b/internal/pkg/auth/storage.go index 73bef1189..fb511dbdb 100644 --- a/internal/pkg/auth/storage.go +++ b/internal/pkg/auth/storage.go @@ -88,18 +88,7 @@ func setAuthFieldInEncodedTextFile(activeProfile string, key authFieldKey, value if err != nil { return err } - - configDir, err := os.UserConfigDir() - if err != nil { - return fmt.Errorf("get config dir: %w", err) - } - - profileTextFileFolderName := textFileFolderName - if activeProfile != "" { - profileTextFileFolderName = filepath.Join(textFileFolderName, activeProfile) - } - - textFileDir := filepath.Join(configDir, profileTextFileFolderName) + textFileDir := config.GetProfileFolderPath(activeProfile) textFilePath := filepath.Join(textFileDir, textFileName) contentEncoded, err := os.ReadFile(textFilePath) @@ -178,17 +167,7 @@ func getAuthFieldFromEncodedTextFile(activeProfile string, key authFieldKey) (st return "", err } - configDir, err := os.UserConfigDir() - if err != nil { - return "", fmt.Errorf("get config dir: %w", err) - } - - profileTextFileFolderName := textFileFolderName - if activeProfile != "" { - profileTextFileFolderName = filepath.Join(textFileFolderName, activeProfile) - } - - textFileDir := filepath.Join(configDir, profileTextFileFolderName) + textFileDir := config.GetProfileFolderPath(activeProfile) textFilePath := filepath.Join(textFileDir, textFileName) contentEncoded, err := os.ReadFile(textFilePath) @@ -215,20 +194,10 @@ func getAuthFieldFromEncodedTextFile(activeProfile string, key authFieldKey) (st // If it doesn't, creates it with the content "{}" encoded. // If it does, does nothing (and returns nil). func createEncodedTextFile(activeProfile string) error { - configDir, err := os.UserConfigDir() - if err != nil { - return fmt.Errorf("get config dir: %w", err) - } - - profileTextFileFolderName := textFileFolderName - if activeProfile != "" { - profileTextFileFolderName = filepath.Join(textFileFolderName, activeProfile) - } - - textFileDir := filepath.Join(configDir, profileTextFileFolderName) + textFileDir := config.GetProfileFolderPath(activeProfile) textFilePath := filepath.Join(textFileDir, textFileName) - err = os.MkdirAll(textFileDir, os.ModePerm) + err := os.MkdirAll(textFileDir, os.ModePerm) if err != nil { return fmt.Errorf("create file dir: %w", err) } diff --git a/internal/pkg/auth/storage_test.go b/internal/pkg/auth/storage_test.go index 4d1593020..f8e6b4dd6 100644 --- a/internal/pkg/auth/storage_test.go +++ b/internal/pkg/auth/storage_test.go @@ -12,6 +12,7 @@ import ( "github.com/zalando/go-keyring" "github.com/stackitcloud/stackit-cli/internal/pkg/config" + "github.com/stackitcloud/stackit-cli/internal/pkg/print" ) func TestSetGetAuthField(t *testing.T) { @@ -183,7 +184,7 @@ func TestSetGetAuthFieldKeyring(t *testing.T) { }{ { description: "simple assignments with default profile", - activeProfile: "", + activeProfile: config.DefaultProfileName, valueAssignments: []valueAssignment{ { key: testField1, @@ -201,7 +202,7 @@ func TestSetGetAuthFieldKeyring(t *testing.T) { }, { description: "overlapping assignments with default profile", - activeProfile: "", + activeProfile: config.DefaultProfileName, valueAssignments: []valueAssignment{ { key: testField1, @@ -267,6 +268,12 @@ func TestSetGetAuthFieldKeyring(t *testing.T) { t.Run(tt.description, func(t *testing.T) { keyring.MockInit() + // Make sure profile name is valid + err := config.ValidateProfile(tt.activeProfile) + if err != nil { + t.Fatalf("Profile name \"%s\" is invalid: %v", tt.activeProfile, err) + } + for _, assignment := range tt.valueAssignments { err := setAuthFieldInKeyring(tt.activeProfile, assignment.key, assignment.value) if err != nil { @@ -317,7 +324,7 @@ func TestSetGetAuthFieldEncodedTextFile(t *testing.T) { }{ { description: "simple assignments with default profile", - activeProfile: "", + activeProfile: config.DefaultProfileName, valueAssignments: []valueAssignment{ { key: testField1, @@ -335,7 +342,7 @@ func TestSetGetAuthFieldEncodedTextFile(t *testing.T) { }, { description: "overlapping assignments with default profile", - activeProfile: "", + activeProfile: config.DefaultProfileName, valueAssignments: []valueAssignment{ { key: testField1, @@ -399,6 +406,26 @@ func TestSetGetAuthFieldEncodedTextFile(t *testing.T) { for _, tt := range tests { t.Run(tt.description, func(t *testing.T) { + // Make sure profile name is valid + err := config.ValidateProfile(tt.activeProfile) + if err != nil { + t.Fatalf("Profile name \"%s\" is invalid: %v", tt.activeProfile, err) + } + + // Create profile if it does not exist + // Will be deleted at the end of the test + profileExists, err := config.ProfileExists(tt.activeProfile) + if err != nil { + t.Fatalf("Failed to check if profile exists: %v", err) + } + if !profileExists { + p := print.NewPrinter() + err := config.CreateProfile(p, tt.activeProfile, true, true) + if err != nil { + t.Fatalf("Failed to create profile: %v", err) + } + } + for _, assignment := range tt.valueAssignments { err := setAuthFieldInEncodedTextFile(tt.activeProfile, assignment.key, assignment.value) if err != nil { @@ -424,6 +451,11 @@ func TestSetGetAuthFieldEncodedTextFile(t *testing.T) { t.Errorf("Post-test cleanup failed: remove field \"%s\" from text file: %v. Please remove it manually", key, err) } } + + err = deleteAuthFieldProfile(tt.activeProfile, profileExists) + if err != nil { + t.Errorf("Post-test cleanup failed: remove profile \"%s\": %v. Please remove it manually", tt.activeProfile, err) + } }) } } @@ -443,17 +475,7 @@ func deleteAuthFieldInEncodedTextFile(activeProfile string, key authFieldKey) er return err } - configDir, err := os.UserConfigDir() - if err != nil { - return fmt.Errorf("get config dir: %w", err) - } - - profileTextFileFolderName := textFileFolderName - if activeProfile != "" { - profileTextFileFolderName = filepath.Join(textFileFolderName, activeProfile) - } - - textFileDir := filepath.Join(configDir, profileTextFileFolderName) + textFileDir := config.GetProfileFolderPath(activeProfile) textFilePath := filepath.Join(textFileDir, textFileName) contentEncoded, err := os.ReadFile(textFilePath) @@ -483,3 +505,15 @@ func deleteAuthFieldInEncodedTextFile(activeProfile string, key authFieldKey) er } return nil } + +func deleteAuthFieldProfile(activeProfile string, profileExisted bool) error { + textFileDir := config.GetProfileFolderPath(activeProfile) + if !profileExisted { + // Remove the entire directory if the profile does not exist + err := os.RemoveAll(textFileDir) + if err != nil { + return fmt.Errorf("remove directory: %w", err) + } + } + return nil +} diff --git a/internal/pkg/config/config.go b/internal/pkg/config/config.go index b6ab86ea3..eb1ed73a2 100644 --- a/internal/pkg/config/config.go +++ b/internal/pkg/config/config.go @@ -34,15 +34,15 @@ const ( ServiceAccountCustomEndpointKey = "service_account_custom_endpoint" SKECustomEndpointKey = "ske_custom_endpoint" - ProjectNameKey = "project_name" + ProjectNameKey = "project_name" + DefaultProfileName = "default" AsyncDefault = false SessionTimeLimitDefault = "2h" ) const ( - configFolder = "stackit" - defaultProfileName = "default" + configFolder = "stackit" configFileName = "cli-config" configFileExtension = "json" @@ -83,21 +83,15 @@ var configFolderPath string var profileFilePath string func InitConfig() { - configDir, err := os.UserConfigDir() - cobra.CheckErr(err) - - defaultConfigFolderPath = filepath.Join(configDir, configFolder) - profileFilePath = filepath.Join(defaultConfigFolderPath, fmt.Sprintf("%s.%s", profileFileName, profileFileExtension)) // Profile file path is in the default config folder + defaultConfigFolderPath = getInitialConfigDir() + profileFilePath = getInitialProfileFilePath() // Profile file path is in the default config folder configProfile, err := GetProfile() cobra.CheckErr(err) - configFolderPath = defaultConfigFolderPath - if configProfile != defaultProfileName { - configFolderPath = filepath.Join(configFolderPath, profileRootFolder, configProfile) // If a profile is set, use the profile config folder - } + configFolderPath = GetProfileFolderPath(configProfile) - configFilePath := filepath.Join(configFolderPath, fmt.Sprintf("%s.%s", configFileName, configFileExtension)) + configFilePath := getConfigFilePath(configFolderPath) // This hack is required to allow creating the config file with `viper.WriteConfig` // see https://github.com/spf13/viper/issues/851#issuecomment-789393451 @@ -151,3 +145,22 @@ func setConfigDefaults() { viper.SetDefault(ServiceAccountCustomEndpointKey, "") viper.SetDefault(SKECustomEndpointKey, "") } + +func getConfigFilePath(configFolder string) string { + return filepath.Join(configFolder, fmt.Sprintf("%s.%s", configFileName, configFileExtension)) +} + +func getInitialConfigDir() string { + configDir, err := os.UserConfigDir() + cobra.CheckErr(err) + + return filepath.Join(configDir, configFolder) +} + +func getInitialProfileFilePath() string { + configFolderPath := defaultConfigFolderPath + if configFolderPath == "" { + configFolderPath = getInitialConfigDir() + } + return filepath.Join(configFolderPath, fmt.Sprintf("%s.%s", profileFileName, profileFileExtension)) +} diff --git a/internal/pkg/config/config_test.go b/internal/pkg/config/config_test.go index b3483a635..7c9954117 100644 --- a/internal/pkg/config/config_test.go +++ b/internal/pkg/config/config_test.go @@ -1,6 +1,7 @@ package config import ( + "fmt" "os" "path/filepath" "testing" @@ -69,3 +70,58 @@ func TestWrite(t *testing.T) { }) } } + +func TestGetInitialConfigDir(t *testing.T) { + tests := []struct { + description string + }{ + { + description: "base", + }, + } + + for _, tt := range tests { + t.Run(tt.description, func(t *testing.T) { + actual := getInitialConfigDir() + + userConfig, err := os.UserConfigDir() + if err != nil { + t.Fatalf("expected error to be nil, got %v", err) + } + + expected := filepath.Join(userConfig, "stackit") + if actual != expected { + t.Fatalf("expected %s, got %s", expected, actual) + } + }) + } +} + +func TestGetInitialProfileFilePath(t *testing.T) { + tests := []struct { + description string + configFolderPath string + }{ + { + description: "base", + configFolderPath: getInitialConfigDir(), + }, + { + description: "empty config folder path", + configFolderPath: "", + }, + } + + for _, tt := range tests { + t.Run(tt.description, func(t *testing.T) { + configFolderPath = getInitialConfigDir() + + actual := getInitialProfileFilePath() + + expected := filepath.Join(configFolderPath, fmt.Sprintf("%s.%s", profileFileName, profileFileExtension)) + if actual != expected { + t.Fatalf("expected %s, got %s", expected, actual) + } + }) + } +} diff --git a/internal/pkg/config/profiles.go b/internal/pkg/config/profiles.go index 9723c3862..c96a42dc8 100644 --- a/internal/pkg/config/profiles.go +++ b/internal/pkg/config/profiles.go @@ -14,40 +14,58 @@ import ( const ProfileEnvVar = "STACKIT_CLI_PROFILE" // GetProfile returns the current profile to be used by the CLI. -// // The profile is determined by the value of the STACKIT_CLI_PROFILE environment variable, or, if not set, // by the contents of the profile file in the CLI config folder. -// // If the profile is not set (env var or profile file) or is set but does not exist, it falls back to the default profile. -// // If the profile is not valid, it returns an error. func GetProfile() (string, error) { + _, profile, _, err := GetConfiguredProfile() + if err != nil { + return "", err + } + + return profile, nil +} + +// GetConfiguredProfile returns the profile configured by the user, the profile to be used by the CLI and the method used to configure the profile. +// The profile is determined by the value of the STACKIT_CLI_PROFILE environment variable, or, if not set, +// by the contents of the profile file in the CLI config folder. +// If the configured profile is not set (env var or profile file) or is set but does not exist, it falls back to the default profile. +// The configuration method can be environment variable, profile file or empty if profile is not configured. +// If the profile is not valid, it returns an error. +func GetConfiguredProfile() (configuredProfile, activeProfile, configurationMethod string, err error) { + var configMethod string profile, profileSetInEnv := GetProfileFromEnv() if !profileSetInEnv { contents, exists, err := fileutils.ReadFileIfExists(profileFilePath) if err != nil { - return "", fmt.Errorf("read profile from file: %w", err) + return "", "", "", fmt.Errorf("read profile from file: %w", err) } if !exists { - return defaultProfileName, nil + // No profile set in env or file + return DefaultProfileName, DefaultProfileName, "", nil } profile = contents + configMethod = "profile file" + } else { + configMethod = "environment variable" } // Make sure the profile exists profileExists, err := ProfileExists(profile) if err != nil { - return "", fmt.Errorf("check if profile exists: %w", err) + return "", "", "", fmt.Errorf("check if profile exists: %w", err) } if !profileExists { - return defaultProfileName, nil + // Profile is configured but does not exist + return profile, DefaultProfileName, configMethod, nil } err = ValidateProfile(profile) if err != nil { - return "", fmt.Errorf("validate profile: %w", err) + return "", "", "", fmt.Errorf("validate profile: %w", err) } - return profile, nil + return profile, profile, configMethod, nil } // GetProfileFromEnv returns the profile from the environment variable. @@ -68,13 +86,13 @@ func CreateProfile(p *print.Printer, profile string, setProfile, emptyProfile bo } // Cannot create a profile with the default name - if profile == defaultProfileName { + if profile == DefaultProfileName { return &errors.InvalidProfileNameError{ Profile: profile, } } - configFolderPath = filepath.Join(defaultConfigFolderPath, profileRootFolder, profile) + configFolderPath = GetProfileFolderPath(profile) // Error if the profile already exists _, err = os.Stat(configFolderPath) @@ -124,20 +142,25 @@ func CreateProfile(p *print.Printer, profile string, setProfile, emptyProfile bo // DuplicateProfileConfiguration duplicates the current profile configuration to a new profile. // It copies the config file from the current profile to the new profile. -// If the current profile does not exist, it returns an error. +// If the current profile does not exist, it does nothing. // If the new profile already exists, it will be overwritten. func DuplicateProfileConfiguration(p *print.Printer, currentProfile, newProfile string) error { - var currentConfigFilePath string + currentProfileFolder := GetProfileFolderPath(currentProfile) + currentConfigFilePath := getConfigFilePath(currentProfileFolder) - if currentProfile == defaultProfileName { - currentConfigFilePath = filepath.Join(defaultConfigFolderPath, fmt.Sprintf("%s.%s", configFileName, configFileExtension)) - } else { - currentConfigFilePath = filepath.Join(defaultConfigFolderPath, profileRootFolder, currentProfile, fmt.Sprintf("%s.%s", configFileName, configFileExtension)) - } + newConfigFilePath := getConfigFilePath(configFolderPath) - newConfigFilePath := filepath.Join(configFolderPath, fmt.Sprintf("%s.%s", configFileName, configFileExtension)) + // If the source profile configuration does not exist, do nothing + _, err := os.Stat(currentConfigFilePath) + if err != nil { + if os.IsNotExist(err) { + p.Debug(print.DebugLevel, "current profile %q has no configuration, nothing to duplicate", currentProfile) + return nil + } + return fmt.Errorf("get current profile configuration: %w", err) + } - err := fileutils.CopyFile(currentConfigFilePath, newConfigFilePath) + err = fileutils.CopyFile(currentConfigFilePath, newConfigFilePath) if err != nil { return fmt.Errorf("copy config file: %w", err) } @@ -163,13 +186,17 @@ func SetProfile(p *print.Printer, profile string) error { return &errors.SetInexistentProfile{Profile: profile} } + if profileFilePath == "" { + profileFilePath = getInitialProfileFilePath() + } + err = os.WriteFile(profileFilePath, []byte(profile), os.ModePerm) if err != nil { return fmt.Errorf("write profile to file: %w", err) } p.Debug(print.DebugLevel, "persisted new active profile in: %s", profileFilePath) - configFolderPath = filepath.Join(defaultConfigFolderPath, profile) + configFolderPath = GetProfileFolderPath(profile) p.Debug(print.DebugLevel, "profile %q is now active", profile) return nil @@ -203,7 +230,7 @@ func ValidateProfile(profile string) error { } func ProfileExists(profile string) (bool, error) { - _, err := os.Stat(filepath.Join(defaultConfigFolderPath, profileRootFolder, profile)) + _, err := os.Stat(GetProfileFolderPath(profile)) if err != nil { if os.IsNotExist(err) { return false, nil @@ -212,3 +239,16 @@ func ProfileExists(profile string) (bool, error) { } return true, nil } + +// GetProfileFolderPath returns the path to the folder where the profile configuration is stored. +// If the profile is the default profile, it returns the default config folder path. +func GetProfileFolderPath(profile string) string { + if defaultConfigFolderPath == "" { + defaultConfigFolderPath = getInitialConfigDir() + } + + if profile == DefaultProfileName { + return defaultConfigFolderPath + } + return filepath.Join(defaultConfigFolderPath, profileRootFolder, profile) +} diff --git a/internal/pkg/config/profiles_test.go b/internal/pkg/config/profiles_test.go index e97451aeb..1250a16aa 100644 --- a/internal/pkg/config/profiles_test.go +++ b/internal/pkg/config/profiles_test.go @@ -1,6 +1,9 @@ package config -import "testing" +import ( + "path/filepath" + "testing" +) func TestValidateProfile(t *testing.T) { tests := []struct { @@ -57,3 +60,48 @@ func TestValidateProfile(t *testing.T) { }) } } + +func TestGetProfileFolderPath(t *testing.T) { + tests := []struct { + description string + defaultConfigFolderNotSet bool + profile string + expected string + }{ + { + description: "default profile", + profile: DefaultProfileName, + expected: getInitialConfigDir(), + }, + { + description: "default profile, default config folder not set", + defaultConfigFolderNotSet: true, + profile: DefaultProfileName, + expected: getInitialConfigDir(), + }, + { + description: "custom profile", + profile: "my-profile", + expected: filepath.Join(getInitialConfigDir(), profileRootFolder, "my-profile"), + }, + { + description: "custom profile, default config folder not set", + defaultConfigFolderNotSet: true, + profile: "my-profile", + expected: filepath.Join(getInitialConfigDir(), profileRootFolder, "my-profile"), + }, + } + + for _, tt := range tests { + t.Run(tt.description, func(t *testing.T) { + defaultConfigFolderPath = getInitialConfigDir() + if tt.defaultConfigFolderNotSet { + defaultConfigFolderPath = "" + } + actual := GetProfileFolderPath(tt.profile) + if actual != tt.expected { + t.Errorf("expected profile folder path to be %q but got %q", tt.expected, actual) + } + }) + } +} From bd2f2c56e9f1835111295c1640ef4aca482de69e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Diogo=20Ferr=C3=A3o?= Date: Fri, 31 May 2024 10:12:11 +0100 Subject: [PATCH 15/18] improve set profile error message (#362) --- internal/pkg/errors/errors.go | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/internal/pkg/errors/errors.go b/internal/pkg/errors/errors.go index 4839698ae..cfc9709c5 100644 --- a/internal/pkg/errors/errors.go +++ b/internal/pkg/errors/errors.go @@ -38,10 +38,7 @@ Please double check if they are correctly configured. For more details run: $ stackit auth activate-service-account -h` - SET_INEXISTENT_PROFILE = `the active configuration profile %[1]q does not exist. - -To unset it, run: - $ stackit config profile unset + SET_INEXISTENT_PROFILE = `the configuration profile %[1]q you are trying to set doesn't exist. To create it, run: $ stackit config profile create %[1]q` From bc7ae7ed033afca2c6ebed443e27db6894809fa8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Diogo=20Ferr=C3=A3o?= Date: Fri, 31 May 2024 16:03:35 +0100 Subject: [PATCH 16/18] Add `stackit config profile list/delete` commands (#359) * better error handling, make argument option in set * address PR comments, improve testing for file utils copy file * check for command existance in set and get profile * initial implementation * get email * list command done * finish list, add delete * force profile names to be lowercase * adapt testing for lowercase profile names * generate docs * add testing to delete * update regex to not allow starting with hyphen * generate docs * add testing for new storage methods * address PR comments * address PR comments * remove unused test logic * delete profile removes keyring entries * address PR comments * add test for keyring failure --- docs/stackit_config_profile.md | 2 + docs/stackit_config_profile_delete.md | 40 ++ docs/stackit_config_profile_list.md | 42 ++ internal/cmd/config/profile/delete/delete.go | 105 ++++ .../cmd/config/profile/delete/delete_test.go | 132 +++++ internal/cmd/config/profile/list/list.go | 131 +++++ internal/cmd/config/profile/profile.go | 4 + internal/pkg/auth/storage.go | 87 ++- internal/pkg/auth/storage_test.go | 554 ++++++++++++++++-- internal/pkg/config/profiles.go | 69 ++- internal/pkg/config/profiles_test.go | 20 +- internal/pkg/errors/errors.go | 15 +- 12 files changed, 1150 insertions(+), 51 deletions(-) create mode 100644 docs/stackit_config_profile_delete.md create mode 100644 docs/stackit_config_profile_list.md create mode 100644 internal/cmd/config/profile/delete/delete.go create mode 100644 internal/cmd/config/profile/delete/delete_test.go create mode 100644 internal/cmd/config/profile/list/list.go diff --git a/docs/stackit_config_profile.md b/docs/stackit_config_profile.md index 6dd5807a1..947f3cc9c 100644 --- a/docs/stackit_config_profile.md +++ b/docs/stackit_config_profile.md @@ -33,6 +33,8 @@ stackit config profile [flags] * [stackit config](./stackit_config.md) - Provides functionality for CLI configuration options * [stackit config profile create](./stackit_config_profile_create.md) - Creates a CLI configuration profile +* [stackit config profile delete](./stackit_config_profile_delete.md) - Delete a CLI configuration profile +* [stackit config profile list](./stackit_config_profile_list.md) - Lists all CLI configuration profiles * [stackit config profile set](./stackit_config_profile_set.md) - Set a CLI configuration profile * [stackit config profile unset](./stackit_config_profile_unset.md) - Unset the current active CLI configuration profile diff --git a/docs/stackit_config_profile_delete.md b/docs/stackit_config_profile_delete.md new file mode 100644 index 000000000..4de5f7545 --- /dev/null +++ b/docs/stackit_config_profile_delete.md @@ -0,0 +1,40 @@ +## stackit config profile delete + +Delete a CLI configuration profile + +### Synopsis + +Delete a CLI configuration profile. +If the deleted profile is the active profile, the default profile will be set to active. + +``` +stackit config profile delete PROFILE [flags] +``` + +### Examples + +``` + Delete the configuration profile "my-profile" + $ stackit config profile delete my-profile +``` + +### Options + +``` + -h, --help Help for "stackit config profile delete" +``` + +### 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 + --verbosity string Verbosity of the CLI, one of ["debug" "info" "warning" "error"] (default "info") +``` + +### SEE ALSO + +* [stackit config profile](./stackit_config_profile.md) - Manage the CLI configuration profiles + diff --git a/docs/stackit_config_profile_list.md b/docs/stackit_config_profile_list.md new file mode 100644 index 000000000..9a8985341 --- /dev/null +++ b/docs/stackit_config_profile_list.md @@ -0,0 +1,42 @@ +## stackit config profile list + +Lists all CLI configuration profiles + +### Synopsis + +Lists all CLI configuration profiles. + +``` +stackit config profile list [flags] +``` + +### Examples + +``` + List the configuration profiles + $ stackit config profile list + + List the configuration profiles in a json format + $ stackit config profile list --output-format json +``` + +### Options + +``` + -h, --help Help for "stackit config profile list" +``` + +### 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 + --verbosity string Verbosity of the CLI, one of ["debug" "info" "warning" "error"] (default "info") +``` + +### SEE ALSO + +* [stackit config profile](./stackit_config_profile.md) - Manage the CLI configuration profiles + diff --git a/internal/cmd/config/profile/delete/delete.go b/internal/cmd/config/profile/delete/delete.go new file mode 100644 index 000000000..7916d27b6 --- /dev/null +++ b/internal/cmd/config/profile/delete/delete.go @@ -0,0 +1,105 @@ +package delete + +import ( + "fmt" + + "github.com/stackitcloud/stackit-cli/internal/pkg/args" + "github.com/stackitcloud/stackit-cli/internal/pkg/auth" + "github.com/stackitcloud/stackit-cli/internal/pkg/config" + "github.com/stackitcloud/stackit-cli/internal/pkg/errors" + "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/spf13/cobra" +) + +const ( + profileArg = "PROFILE" +) + +type inputModel struct { + *globalflags.GlobalFlagModel + Profile string +} + +func NewCmd(p *print.Printer) *cobra.Command { + cmd := &cobra.Command{ + Use: fmt.Sprintf("delete %s", profileArg), + Short: "Delete a CLI configuration profile", + Long: fmt.Sprintf("%s\n%s", + "Delete a CLI configuration profile.", + "If the deleted profile is the active profile, the default profile will be set to active.", + ), + Args: args.SingleArg(profileArg, nil), + Example: examples.Build( + examples.NewExample( + `Delete the configuration profile "my-profile"`, + "$ stackit config profile delete my-profile"), + ), + RunE: func(cmd *cobra.Command, args []string) error { + model, err := parseInput(p, cmd, args) + if err != nil { + return err + } + + profileExists, err := config.ProfileExists(model.Profile) + if err != nil { + return fmt.Errorf("check if profile exists: %w", err) + } + if !profileExists { + return &errors.DeleteInexistentProfile{Profile: model.Profile} + } + + if !model.AssumeYes { + prompt := fmt.Sprintf("Are you sure you want to delete profile %q? (This cannot be undone)", model.Profile) + err = p.PromptForConfirmation(prompt) + if err != nil { + return err + } + } + + err = config.DeleteProfile(p, model.Profile) + if err != nil { + return fmt.Errorf("delete profile: %w", err) + } + + err = auth.DeleteProfileFromKeyring(model.Profile) + if err != nil { + return fmt.Errorf("delete profile from keyring: %w", err) + } + + p.Info("Successfully deleted profile %q\n", model.Profile) + + return nil + }, + } + return cmd +} + +func parseInput(p *print.Printer, cmd *cobra.Command, inputArgs []string) (*inputModel, error) { + profile := inputArgs[0] + + err := config.ValidateProfile(profile) + if err != nil { + return nil, err + } + + globalFlags := globalflags.Parse(p, cmd) + + model := inputModel{ + GlobalFlagModel: globalFlags, + Profile: profile, + } + + if p.IsVerbosityDebug() { + modelStr, err := print.BuildDebugStrFromInputModel(model) + if err != nil { + p.Debug(print.ErrorLevel, "convert model to string for debugging: %v", err) + } else { + p.Debug(print.DebugLevel, "parsed input values: %s", modelStr) + } + } + + return &model, nil +} diff --git a/internal/cmd/config/profile/delete/delete_test.go b/internal/cmd/config/profile/delete/delete_test.go new file mode 100644 index 000000000..3919460b7 --- /dev/null +++ b/internal/cmd/config/profile/delete/delete_test.go @@ -0,0 +1,132 @@ +package delete + +import ( + "testing" + + "github.com/stackitcloud/stackit-cli/internal/pkg/globalflags" + "github.com/stackitcloud/stackit-cli/internal/pkg/print" + + "github.com/google/go-cmp/cmp" +) + +const testProfile = "test-profile" + +func fixtureArgValues(mods ...func(argValues []string)) []string { + argValues := []string{ + testProfile, + } + for _, mod := range mods { + mod(argValues) + } + return argValues +} + +func fixtureInputModel(mods ...func(model *inputModel)) *inputModel { + model := &inputModel{ + GlobalFlagModel: &globalflags.GlobalFlagModel{ + Verbosity: globalflags.VerbosityDefault, + }, + Profile: testProfile, + } + 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", + argValues: fixtureArgValues(), + isValid: true, + expectedModel: fixtureInputModel(), + }, + { + description: "no values", + argValues: []string{}, + flagValues: map[string]string{}, + isValid: false, + }, + { + description: "no arg values", + argValues: []string{}, + isValid: false, + }, + { + description: "some global flag", + argValues: fixtureArgValues(), + flagValues: map[string]string{ + globalflags.VerbosityFlag: globalflags.DebugVerbosity, + }, + isValid: true, + expectedModel: fixtureInputModel(func(model *inputModel) { + model.GlobalFlagModel.Verbosity = globalflags.DebugVerbosity + }), + }, + { + description: "invalid profile", + argValues: []string{"invalid-profile-&"}, + isValid: false, + }, + } + + for _, tt := range tests { + t.Run(tt.description, func(t *testing.T) { + p := print.NewPrinter() + cmd := NewCmd(p) + err := globalflags.Configure(cmd.Flags()) + if err != nil { + t.Fatalf("configure global flags: %v", err) + } + + for flag, value := range tt.flagValues { + err := cmd.Flags().Set(flag, value) + if err != nil { + if !tt.isValid { + return + } + t.Fatalf("setting flag --%s=%s: %v", flag, value, err) + } + } + + err = cmd.ValidateArgs(tt.argValues) + if err != nil { + if !tt.isValid { + return + } + t.Fatalf("error validating args: %v", err) + } + + err = cmd.ValidateRequiredFlags() + if err != nil { + if !tt.isValid { + return + } + t.Fatalf("error validating flags: %v", err) + } + + model, err := parseInput(p, cmd, tt.argValues) + if err != nil { + if !tt.isValid { + return + } + t.Fatalf("error parsing input: %v", err) + } + + if !tt.isValid { + t.Fatalf("did not fail on invalid input") + } + diff := cmp.Diff(model, tt.expectedModel) + if diff != "" { + t.Fatalf("Data does not match: %s", diff) + } + }) + } +} diff --git a/internal/cmd/config/profile/list/list.go b/internal/cmd/config/profile/list/list.go new file mode 100644 index 000000000..7f594d1e1 --- /dev/null +++ b/internal/cmd/config/profile/list/list.go @@ -0,0 +1,131 @@ +package list + +import ( + "encoding/json" + "fmt" + + "github.com/goccy/go-yaml" + "github.com/stackitcloud/stackit-cli/internal/pkg/args" + "github.com/stackitcloud/stackit-cli/internal/pkg/auth" + "github.com/stackitcloud/stackit-cli/internal/pkg/config" + "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/tables" + + "github.com/spf13/cobra" +) + +type inputModel struct { + *globalflags.GlobalFlagModel +} + +func NewCmd(p *print.Printer) *cobra.Command { + cmd := &cobra.Command{ + Use: "list", + Short: "Lists all CLI configuration profiles", + Long: "Lists all CLI configuration profiles.", + Args: args.NoArgs, + Example: examples.Build( + examples.NewExample( + `List the configuration profiles`, + "$ stackit config profile list"), + examples.NewExample( + `List the configuration profiles in a json format`, + "$ stackit config profile list --output-format json"), + ), + RunE: func(cmd *cobra.Command, args []string) error { + model := parseInput(p, cmd) + + profiles, err := config.ListProfiles() + if err != nil { + return fmt.Errorf("get profile: %w", err) + } + + activeProfile, err := config.GetProfile() + if err != nil { + return fmt.Errorf("get profile: %w", err) + } + + outputProfiles := buildOutput(profiles, activeProfile) + + return outputResult(p, model.OutputFormat, outputProfiles) + }, + } + return cmd +} + +func parseInput(p *print.Printer, cmd *cobra.Command) *inputModel { + globalFlags := globalflags.Parse(p, cmd) + + return &inputModel{ + GlobalFlagModel: globalFlags, + } +} + +type profileInfo struct { + Name string + Active bool + Email string +} + +func buildOutput(profiles []string, activeProfile string) []profileInfo { + var configData []profileInfo + + // Add default profile first + configData = append(configData, profileInfo{ + Name: config.DefaultProfileName, + Active: activeProfile == config.DefaultProfileName, + Email: auth.GetProfileEmail(config.DefaultProfileName), + }) + + for _, profile := range profiles { + configData = append(configData, profileInfo{ + Name: profile, + Active: profile == activeProfile, + Email: auth.GetProfileEmail(profile), + }) + } + + return configData +} + +func outputResult(p *print.Printer, outputFormat string, profiles []profileInfo) error { + switch outputFormat { + case print.JSONOutputFormat: + details, err := json.MarshalIndent(profiles, "", " ") + if err != nil { + return fmt.Errorf("marshal config list: %w", err) + } + p.Outputln(string(details)) + return nil + case print.YAMLOutputFormat: + details, err := yaml.MarshalWithOptions(profiles, yaml.IndentSequence(true)) + if err != nil { + return fmt.Errorf("marshal config list: %w", err) + } + p.Outputln(string(details)) + return nil + default: + table := tables.NewTable() + table.SetHeader("NAME", "ACTIVE", "EMAIL") + for _, profile := range profiles { + // Prettify the output + email := profile.Email + active := "" + if profile.Email == "" { + email = "Not authenticated" + } + if profile.Active { + active = "*" + } + table.AddRow(profile.Name, active, email) + table.AddSeparator() + } + err := table.Display(p) + if err != nil { + return fmt.Errorf("render table: %w", err) + } + return nil + } +} diff --git a/internal/cmd/config/profile/profile.go b/internal/cmd/config/profile/profile.go index 638359c68..3a4233ec4 100644 --- a/internal/cmd/config/profile/profile.go +++ b/internal/cmd/config/profile/profile.go @@ -4,6 +4,8 @@ import ( "fmt" "github.com/stackitcloud/stackit-cli/internal/cmd/config/profile/create" + "github.com/stackitcloud/stackit-cli/internal/cmd/config/profile/delete" + "github.com/stackitcloud/stackit-cli/internal/cmd/config/profile/list" "github.com/stackitcloud/stackit-cli/internal/cmd/config/profile/set" "github.com/stackitcloud/stackit-cli/internal/cmd/config/profile/unset" "github.com/stackitcloud/stackit-cli/internal/pkg/args" @@ -34,4 +36,6 @@ func addSubcommands(cmd *cobra.Command, p *print.Printer) { cmd.AddCommand(set.NewCmd(p)) cmd.AddCommand(unset.NewCmd(p)) cmd.AddCommand(create.NewCmd(p)) + cmd.AddCommand(list.NewCmd(p)) + cmd.AddCommand(delete.NewCmd(p)) } diff --git a/internal/pkg/auth/storage.go b/internal/pkg/auth/storage.go index fb511dbdb..20018a70e 100644 --- a/internal/pkg/auth/storage.go +++ b/internal/pkg/auth/storage.go @@ -3,6 +3,7 @@ package auth import ( "encoding/base64" "encoding/json" + "errors" "fmt" "os" "path/filepath" @@ -44,6 +45,21 @@ const ( AUTH_FLOW_SERVICE_ACCOUNT_KEY AuthFlow = "sa_key" ) +// Returns all auth field keys managed by the auth storage +var authFieldKeys = []authFieldKey{ + SESSION_EXPIRES_AT_UNIX, + ACCESS_TOKEN, + REFRESH_TOKEN, + SERVICE_ACCOUNT_TOKEN, + SERVICE_ACCOUNT_EMAIL, + USER_EMAIL, + SERVICE_ACCOUNT_KEY, + PRIVATE_KEY, + TOKEN_CUSTOM_ENDPOINT, + JWKS_CUSTOM_ENDPOINT, + authFlowType, +} + func SetAuthFlow(value AuthFlow) error { return SetAuthField(authFlowType, string(value)) } @@ -65,9 +81,13 @@ func SetAuthField(key authFieldKey, value string) error { return fmt.Errorf("get profile: %w", err) } - err = setAuthFieldInKeyring(activeProfile, key, value) + return setAuthFieldWithProfile(activeProfile, key, value) +} + +func setAuthFieldWithProfile(profile string, key authFieldKey, value string) error { + err := setAuthFieldInKeyring(profile, key, value) if err != nil { - errFallback := setAuthFieldInEncodedTextFile(activeProfile, key, value) + errFallback := setAuthFieldInEncodedTextFile(profile, key, value) if errFallback != nil { return fmt.Errorf("write to keyring failed (%w), try writing to encoded text file: %w", err, errFallback) } @@ -76,13 +96,22 @@ func SetAuthField(key authFieldKey, value string) error { } func setAuthFieldInKeyring(activeProfile string, key authFieldKey, value string) error { - if activeProfile != "" { + if activeProfile != config.DefaultProfileName { activeProfileKeyring := filepath.Join(keyringService, activeProfile) return keyring.Set(activeProfileKeyring, string(key), value) } return keyring.Set(keyringService, string(key), value) } +func deleteAuthFieldInKeyring(activeProfile string, key authFieldKey) error { + keyringServiceLocal := keyringService + if activeProfile != config.DefaultProfileName { + keyringServiceLocal = filepath.Join(keyringService, activeProfile) + } + + return keyring.Delete(keyringServiceLocal, string(key)) +} + func setAuthFieldInEncodedTextFile(activeProfile string, key authFieldKey, value string) error { err := createEncodedTextFile(activeProfile) if err != nil { @@ -141,11 +170,14 @@ func GetAuthField(key authFieldKey) (string, error) { if err != nil { return "", fmt.Errorf("get profile: %w", err) } + return getAuthFieldWithProfile(activeProfile, key) +} - value, err := getAuthFieldFromKeyring(activeProfile, key) +func getAuthFieldWithProfile(profile string, key authFieldKey) (string, error) { + value, err := getAuthFieldFromKeyring(profile, key) if err != nil { var errFallback error - value, errFallback = getAuthFieldFromEncodedTextFile(activeProfile, key) + value, errFallback = getAuthFieldFromEncodedTextFile(profile, key) if errFallback != nil { return "", fmt.Errorf("read from keyring: %w, read from encoded file as fallback: %w", err, errFallback) } @@ -154,7 +186,7 @@ func GetAuthField(key authFieldKey) (string, error) { } func getAuthFieldFromKeyring(activeProfile string, key authFieldKey) (string, error) { - if activeProfile != "" { + if activeProfile != config.DefaultProfileName { activeProfileKeyring := filepath.Join(keyringService, activeProfile) return keyring.Get(activeProfileKeyring, string(key)) } @@ -214,3 +246,46 @@ func createEncodedTextFile(activeProfile string) error { return nil } + +// GetProfileEmail returns the email of the user or service account associated with the given profile. +// If the profile is not authenticated or the email can't be obtained, it returns an empty string. +func GetProfileEmail(profile string) string { + value, err := getAuthFieldWithProfile(profile, authFlowType) + if err != nil { + return "" + } + + var email string + switch AuthFlow(value) { + case AUTH_FLOW_USER_TOKEN: + email, err = getAuthFieldWithProfile(profile, USER_EMAIL) + if err != nil { + email = "" + } + case AUTH_FLOW_SERVICE_ACCOUNT_TOKEN, AUTH_FLOW_SERVICE_ACCOUNT_KEY: + email, err = getAuthFieldWithProfile(profile, SERVICE_ACCOUNT_EMAIL) + if err != nil { + email = "" + } + } + return email +} + +func DeleteProfileFromKeyring(profile string) error { + err := config.ValidateProfile(profile) + if err != nil { + return fmt.Errorf("validate profile: %w", err) + } + + for _, key := range authFieldKeys { + err := deleteAuthFieldInKeyring(profile, key) + if err != nil { + // if the key is not found, we can ignore the error + if !errors.Is(err, keyring.ErrNotFound) { + return fmt.Errorf("delete auth field \"%s\" from keyring: %w", key, err) + } + } + } + + return nil +} diff --git a/internal/pkg/auth/storage_test.go b/internal/pkg/auth/storage_test.go index f8e6b4dd6..9f603e145 100644 --- a/internal/pkg/auth/storage_test.go +++ b/internal/pkg/auth/storage_test.go @@ -12,7 +12,6 @@ import ( "github.com/zalando/go-keyring" "github.com/stackitcloud/stackit-cli/internal/pkg/config" - "github.com/stackitcloud/stackit-cli/internal/pkg/print" ) func TestSetGetAuthField(t *testing.T) { @@ -163,6 +162,169 @@ func TestSetGetAuthField(t *testing.T) { } } +func TestSetGetAuthFieldWithProfile(t *testing.T) { + var testField1 authFieldKey = "test-field-1" + var testField2 authFieldKey = "test-field-2" + + testValue1 := fmt.Sprintf("value-1-%s", time.Now().Format(time.RFC3339)) + testValue2 := fmt.Sprintf("value-2-%s", time.Now().Format(time.RFC3339)) + testValue3 := fmt.Sprintf("value-3-%s", time.Now().Format(time.RFC3339)) + + type valueAssignment struct { + key authFieldKey + value string + } + + tests := []struct { + description string + keyringFails bool + valueAssignments []valueAssignment + activeProfile string + expectedValues map[authFieldKey]string + }{ + { + description: "simple assignments", + valueAssignments: []valueAssignment{ + { + key: testField1, + value: testValue1, + }, + { + key: testField2, + value: testValue2, + }, + }, + activeProfile: "test-profile", + expectedValues: map[authFieldKey]string{ + testField1: testValue1, + testField2: testValue2, + }, + }, + { + description: "simple assignments w/ keyring failing", + keyringFails: true, + valueAssignments: []valueAssignment{ + { + key: testField1, + value: testValue1, + }, + { + key: testField2, + value: testValue2, + }, + }, + activeProfile: "test-profile", + expectedValues: map[authFieldKey]string{ + testField1: testValue1, + testField2: testValue2, + }, + }, + { + description: "overlapping assignments", + keyringFails: true, + valueAssignments: []valueAssignment{ + { + key: testField1, + value: testValue1, + }, + { + key: testField2, + value: testValue2, + }, + { + key: testField1, + value: testValue3, + }, + }, + activeProfile: "test-profile", + expectedValues: map[authFieldKey]string{ + testField1: testValue3, + testField2: testValue2, + }, + }, + { + description: "overlapping assignments w/ keyring failing", + keyringFails: true, + valueAssignments: []valueAssignment{ + { + key: testField1, + value: testValue1, + }, + { + key: testField2, + value: testValue2, + }, + { + key: testField1, + value: testValue3, + }, + }, + activeProfile: "test-profile", + expectedValues: map[authFieldKey]string{ + testField1: testValue3, + testField2: testValue2, + }, + }, + } + + for _, tt := range tests { + t.Run(tt.description, func(t *testing.T) { + // Apppend random string to profile name to avoid conflicts + tt.activeProfile = makeProfileNameUnique(tt.activeProfile) + + // Make sure profile name is valid + err := config.ValidateProfile(tt.activeProfile) + if err != nil { + t.Fatalf("Profile name \"%s\" is invalid: %v", tt.activeProfile, err) + } + + if !tt.keyringFails { + keyring.MockInit() + } else { + keyring.MockInitWithError(fmt.Errorf("keyring unavailable for testing")) + } + + for _, assignment := range tt.valueAssignments { + err := setAuthFieldWithProfile(tt.activeProfile, assignment.key, assignment.value) + if err != nil { + t.Fatalf("Failed to set \"%s\" as \"%s\": %v", assignment.key, assignment.value, err) + } + // Check that this value will be checked + if _, ok := tt.expectedValues[assignment.key]; !ok { + t.Fatalf("Value \"%s\" set but not checked. Please add it to 'expectedValues'", assignment.key) + } + } + + for key, valueExpected := range tt.expectedValues { + value, err := getAuthFieldWithProfile(tt.activeProfile, key) + if err != nil { + t.Errorf("Failed to get value of \"%s\": %v", key, err) + continue + } else if value != valueExpected { + t.Errorf("Value of field \"%s\" is wrong: expected \"%s\", got \"%s\"", key, valueExpected, value) + } + + if !tt.keyringFails { + err = deleteAuthFieldInKeyring(tt.activeProfile, key) + if err != nil { + t.Errorf("Post-test cleanup failed: remove field \"%s\" from keyring: %v. Please remove it manually", key, err) + } + } else { + err = deleteAuthFieldInEncodedTextFile(tt.activeProfile, key) + if err != nil { + t.Errorf("Post-test cleanup failed: remove field \"%s\" from text file: %v. Please remove it manually", key, err) + } + } + } + + err = deleteAuthFieldProfile(tt.activeProfile) + if err != nil { + t.Errorf("Post-test cleanup failed: remove profile \"%s\": %v. Please remove it manually", tt.activeProfile, err) + } + }) + } +} + func TestSetGetAuthFieldKeyring(t *testing.T) { var testField1 authFieldKey = "test-field-1" var testField2 authFieldKey = "test-field-2" @@ -223,8 +385,8 @@ func TestSetGetAuthFieldKeyring(t *testing.T) { }, }, { - description: "simple assignments with testProfile", - activeProfile: "testProfile", + description: "simple assignments with test-profile", + activeProfile: "test-profile", valueAssignments: []valueAssignment{ { key: testField1, @@ -241,8 +403,8 @@ func TestSetGetAuthFieldKeyring(t *testing.T) { }, }, { - description: "overlapping assignments with testProfile", - activeProfile: "testProfile", + description: "overlapping assignments with test-profile", + activeProfile: "test-profile", valueAssignments: []valueAssignment{ { key: testField1, @@ -268,6 +430,9 @@ func TestSetGetAuthFieldKeyring(t *testing.T) { t.Run(tt.description, func(t *testing.T) { keyring.MockInit() + // Apppend random string to profile name to avoid conflicts + tt.activeProfile = makeProfileNameUnique(tt.activeProfile) + // Make sure profile name is valid err := config.ValidateProfile(tt.activeProfile) if err != nil { @@ -303,6 +468,176 @@ func TestSetGetAuthFieldKeyring(t *testing.T) { } } +func TestDeleteAuthFieldKeyring(t *testing.T) { + tests := []struct { + description string + activeProfile string + noKey bool + isValid bool + }{ + { + description: "base, default profile", + activeProfile: config.DefaultProfileName, + isValid: true, + }, + { + description: "key doesnt exist, default profile", + activeProfile: config.DefaultProfileName, + noKey: true, + isValid: false, + }, + { + description: "base, custom profile", + activeProfile: "test-profile", + isValid: true, + }, + { + description: "key doesnt exist, custom profile", + activeProfile: "test-profile", + noKey: true, + isValid: false, + }, + } + + for _, tt := range tests { + t.Run(tt.description, func(t *testing.T) { + keyring.MockInit() + + // Append random string to auth field key and value to avoid conflicts + testField1 := authFieldKey(fmt.Sprintf("test-field-1-%s", time.Now().Format(time.RFC3339))) + testValue1 := fmt.Sprintf("value-1-keyring-%s", time.Now().Format(time.RFC3339)) + + // Append random string to profile name to avoid conflicts + tt.activeProfile = makeProfileNameUnique(tt.activeProfile) + + // Make sure profile name is valid + err := config.ValidateProfile(tt.activeProfile) + if err != nil { + t.Fatalf("Profile name \"%s\" is invalid: %v", tt.activeProfile, err) + } + + if !tt.noKey { + err := setAuthFieldInKeyring(tt.activeProfile, testField1, testValue1) + if err != nil { + t.Fatalf("Failed to set \"%s\" as \"%s\": %v", testField1, testValue1, err) + } + } + + err = deleteAuthFieldInKeyring(tt.activeProfile, testField1) + if err != nil { + if tt.isValid { + t.Fatalf("Failed to delete field \"%s\" from keyring: %v", testField1, err) + } + return + } + + if !tt.isValid { + t.Fatalf("Expected error when deleting field \"%s\" from keyring, got none", testField1) + } + + // Check if key still exists + _, err = getAuthFieldFromKeyring(tt.activeProfile, testField1) + if err == nil { + t.Fatalf("Key \"%s\" still exists in keyring after deletion", testField1) + } + }) + } +} + +func TestDeleteProfileFromKeyring(t *testing.T) { + tests := []struct { + description string + keyringFails bool + keys []authFieldKey + activeProfile string + isValid bool + }{ + { + description: "base, default profile", + keys: authFieldKeys, + activeProfile: config.DefaultProfileName, + isValid: true, + }, + { + description: "missing keys, default profile", + keys: []authFieldKey{ + ACCESS_TOKEN, + SERVICE_ACCOUNT_EMAIL, + }, + activeProfile: config.DefaultProfileName, + isValid: true, + }, + { + description: "base, custom profile", + keys: authFieldKeys, + activeProfile: "test-profile", + isValid: true, + }, + { + description: "missing keys, custom profile", + keys: []authFieldKey{ + ACCESS_TOKEN, + SERVICE_ACCOUNT_EMAIL, + }, + activeProfile: config.DefaultProfileName, + isValid: true, + }, + { + description: "invalid profile", + activeProfile: "INVALID", + isValid: false, + }, + { + description: "keyring fails", + keyringFails: true, + isValid: false, + }, + } + + for _, tt := range tests { + t.Run(tt.description, func(t *testing.T) { + if !tt.keyringFails { + keyring.MockInit() + } else { + keyring.MockInitWithError(fmt.Errorf("keyring unavailable for testing")) + } + + // Append random string to auth field key and value to avoid conflicts + testValue1 := fmt.Sprintf("value-1-keyring-%s", time.Now().Format(time.RFC3339)) + + // Append random string to profile name to avoid conflicts + tt.activeProfile = makeProfileNameUnique(tt.activeProfile) + + for _, key := range tt.keys { + err := setAuthFieldInKeyring(tt.activeProfile, key, testValue1) + if err != nil { + t.Fatalf("Failed to set \"%s\" as \"%s\": %v", key, testValue1, err) + } + } + + err := DeleteProfileFromKeyring(tt.activeProfile) + if err != nil { + if tt.isValid { + t.Fatalf("Failed to delete profile \"%s\" from keyring: %v", tt.activeProfile, err) + } + return + } + + if !tt.isValid { + t.Fatalf("Expected error when deleting profile \"%s\" from keyring, got none", tt.activeProfile) + } + + for _, key := range tt.keys { + // Check if key still exists + _, err = getAuthFieldFromKeyring(tt.activeProfile, key) + if err == nil { + t.Fatalf("Key \"%s\" still exists in keyring after profile deletion", key) + } + } + }) + } +} + func TestSetGetAuthFieldEncodedTextFile(t *testing.T) { var testField1 authFieldKey = "test-field-1" var testField2 authFieldKey = "test-field-2" @@ -363,8 +698,8 @@ func TestSetGetAuthFieldEncodedTextFile(t *testing.T) { }, }, { - description: "simple assignments with testProfile", - activeProfile: "testProfile", + description: "simple assignments with test-profile", + activeProfile: "test-profile", valueAssignments: []valueAssignment{ { key: testField1, @@ -381,8 +716,8 @@ func TestSetGetAuthFieldEncodedTextFile(t *testing.T) { }, }, { - description: "overlapping assignments with testProfile", - activeProfile: "testProfile", + description: "overlapping assignments with test-profile", + activeProfile: "test-profile", valueAssignments: []valueAssignment{ { key: testField1, @@ -406,26 +741,15 @@ func TestSetGetAuthFieldEncodedTextFile(t *testing.T) { for _, tt := range tests { t.Run(tt.description, func(t *testing.T) { + // Append random string to profile name to avoid conflicts + tt.activeProfile = makeProfileNameUnique(tt.activeProfile) + // Make sure profile name is valid err := config.ValidateProfile(tt.activeProfile) if err != nil { t.Fatalf("Profile name \"%s\" is invalid: %v", tt.activeProfile, err) } - // Create profile if it does not exist - // Will be deleted at the end of the test - profileExists, err := config.ProfileExists(tt.activeProfile) - if err != nil { - t.Fatalf("Failed to check if profile exists: %v", err) - } - if !profileExists { - p := print.NewPrinter() - err := config.CreateProfile(p, tt.activeProfile, true, true) - if err != nil { - t.Fatalf("Failed to create profile: %v", err) - } - } - for _, assignment := range tt.valueAssignments { err := setAuthFieldInEncodedTextFile(tt.activeProfile, assignment.key, assignment.value) if err != nil { @@ -452,7 +776,7 @@ func TestSetGetAuthFieldEncodedTextFile(t *testing.T) { } } - err = deleteAuthFieldProfile(tt.activeProfile, profileExists) + err = deleteAuthFieldProfile(tt.activeProfile) if err != nil { t.Errorf("Post-test cleanup failed: remove profile \"%s\": %v. Please remove it manually", tt.activeProfile, err) } @@ -460,13 +784,162 @@ func TestSetGetAuthFieldEncodedTextFile(t *testing.T) { } } -func deleteAuthFieldInKeyring(activeProfile string, key authFieldKey) error { - if activeProfile != "" { - activeProfileKeyring := filepath.Join(keyringService, activeProfile) - return keyring.Delete(activeProfileKeyring, string(key)) +func TestGetProfileEmail(t *testing.T) { + tests := []struct { + description string + activeProfile string + userEmail string + authFlow AuthFlow + serviceAccEmail string + expectedEmail string + }{ + { + description: "default profile, user token", + activeProfile: config.DefaultProfileName, + userEmail: "test@test.com", + authFlow: AUTH_FLOW_USER_TOKEN, + expectedEmail: "test@test.com", + }, + { + description: "default profile, service acc token", + activeProfile: config.DefaultProfileName, + serviceAccEmail: "test@test.com", + authFlow: AUTH_FLOW_SERVICE_ACCOUNT_TOKEN, + expectedEmail: "test@test.com", + }, + { + description: "default profile, service acc key", + activeProfile: config.DefaultProfileName, + serviceAccEmail: "test@test.com", + authFlow: AUTH_FLOW_SERVICE_ACCOUNT_KEY, + expectedEmail: "test@test.com", + }, + { + description: "custom profile, user token", + activeProfile: "test-profile", + userEmail: "test@test.com", + authFlow: AUTH_FLOW_USER_TOKEN, + expectedEmail: "test@test.com", + }, + { + description: "custom profile, service acc token", + activeProfile: "test-profile", + serviceAccEmail: "test@test.com", + authFlow: AUTH_FLOW_SERVICE_ACCOUNT_TOKEN, + expectedEmail: "test@test.com", + }, + { + description: "custom profile, service acc key", + activeProfile: "test-profile", + serviceAccEmail: "test@test.com", + authFlow: AUTH_FLOW_SERVICE_ACCOUNT_KEY, + expectedEmail: "test@test.com", + }, + { + description: "no email, user token", + activeProfile: "test-profile", + authFlow: AUTH_FLOW_USER_TOKEN, + expectedEmail: "", + }, + { + description: "no email, service acc token", + activeProfile: "test-profile", + authFlow: AUTH_FLOW_SERVICE_ACCOUNT_TOKEN, + expectedEmail: "", + }, + { + description: "no email, service acc key", + activeProfile: "test-profile", + authFlow: AUTH_FLOW_SERVICE_ACCOUNT_KEY, + expectedEmail: "", + }, + { + description: "user not authenticated", + activeProfile: "test-profile", + expectedEmail: "", + }, + { + description: "both emails, user not authenticated", + activeProfile: "test-profile", + userEmail: "test@test.com", + serviceAccEmail: "test2@test.com", + expectedEmail: "", + }, + { + description: "both emails, user token", + activeProfile: "test-profile", + userEmail: "test@test.com", + serviceAccEmail: "test2@test.com", + authFlow: AUTH_FLOW_USER_TOKEN, + expectedEmail: "test@test.com", + }, + { + description: "both emails, service account token", + activeProfile: "test-profile", + userEmail: "test@test.com", + serviceAccEmail: "test2@test.com", + authFlow: AUTH_FLOW_SERVICE_ACCOUNT_TOKEN, + expectedEmail: "test2@test.com", + }, + { + description: "both emails, service account key", + activeProfile: "test-profile", + userEmail: "test@test.com", + serviceAccEmail: "test2@test.com", + authFlow: AUTH_FLOW_SERVICE_ACCOUNT_KEY, + expectedEmail: "test2@test.com", + }, } - return keyring.Delete(keyringService, string(key)) + for _, tt := range tests { + t.Run(tt.description, func(t *testing.T) { + keyring.MockInit() + + // Append random string to profile name to avoid conflicts + tt.activeProfile = makeProfileNameUnique(tt.activeProfile) + + // Make sure profile name is valid + err := config.ValidateProfile(tt.activeProfile) + if err != nil { + t.Fatalf("Profile name \"%s\" is invalid: %v", tt.activeProfile, err) + } + + err = setAuthFieldInKeyring(tt.activeProfile, USER_EMAIL, tt.userEmail) + if err != nil { + t.Errorf("Failed to set user email: %v", err) + } + + err = setAuthFieldInKeyring(tt.activeProfile, SERVICE_ACCOUNT_EMAIL, tt.serviceAccEmail) + if err != nil { + t.Errorf("Failed to set service account email: %v", err) + } + + err = setAuthFieldWithProfile(tt.activeProfile, authFlowType, string(tt.authFlow)) + if err != nil { + t.Errorf("Failed to set auth flow: %v", err) + } + + email := GetProfileEmail(tt.activeProfile) + if email != tt.expectedEmail { + t.Errorf("Expected email \"%s\", got \"%s\"", tt.expectedEmail, email) + } + + err = deleteAuthFieldInKeyring(tt.activeProfile, USER_EMAIL) + if err != nil { + t.Fatalf("Failed to remove user email: %v", err) + } + + err = deleteAuthFieldInKeyring(tt.activeProfile, SERVICE_ACCOUNT_EMAIL) + if err != nil { + t.Fatalf("Failed to remove service account email: %v", err) + } + + err = deleteAuthFieldProfile(tt.activeProfile) + if err != nil { + t.Fatalf("Failed to remove profile: %v", err) + } + }) + } } func deleteAuthFieldInEncodedTextFile(activeProfile string, key authFieldKey) error { @@ -506,14 +979,23 @@ func deleteAuthFieldInEncodedTextFile(activeProfile string, key authFieldKey) er return nil } -func deleteAuthFieldProfile(activeProfile string, profileExisted bool) error { +func deleteAuthFieldProfile(activeProfile string) error { + if activeProfile == config.DefaultProfileName { + // Do not delete the default profile + return nil + } textFileDir := config.GetProfileFolderPath(activeProfile) - if !profileExisted { - // Remove the entire directory if the profile does not exist - err := os.RemoveAll(textFileDir) - if err != nil { - return fmt.Errorf("remove directory: %w", err) - } + // Remove the entire directory if the profile does not exist + err := os.RemoveAll(textFileDir) + if err != nil { + return fmt.Errorf("remove directory: %w", err) } return nil } + +func makeProfileNameUnique(profile string) string { + if profile == config.DefaultProfileName { + return profile + } + return fmt.Sprintf("%s-%s", profile, time.Now().Format("20060102150405")) +} diff --git a/internal/pkg/config/profiles.go b/internal/pkg/config/profiles.go index c96a42dc8..bc05dc970 100644 --- a/internal/pkg/config/profiles.go +++ b/internal/pkg/config/profiles.go @@ -209,15 +209,18 @@ func UnsetProfile(p *print.Printer) error { if err != nil && !os.IsNotExist(err) { return fmt.Errorf("remove profile file: %w", err) } - p.Debug(print.DebugLevel, "removed active profile file: %s", profileFilePath) + if p != nil { + p.Debug(print.DebugLevel, "removed active profile file: %s", profileFilePath) + } return nil } // ValidateProfile validates the profile name. -// It can only use letters, numbers, or "-" and cannot be empty. +// It can only use lowercase letters, numbers, or "-" and cannot be empty. +// It can't start with a "-". // If the profile is invalid, it returns an error. func ValidateProfile(profile string) error { - match, err := regexp.MatchString("^[a-zA-Z0-9-]+$", profile) + match, err := regexp.MatchString("^[a-z0-9][a-z0-9-]+$", profile) if err != nil { return fmt.Errorf("match string regex: %w", err) } @@ -252,3 +255,63 @@ func GetProfileFolderPath(profile string) string { } return filepath.Join(defaultConfigFolderPath, profileRootFolder, profile) } + +// ListProfiles returns a list of all profiles. +func ListProfiles() ([]string, error) { + profiles := []string{} + + profileFolders, err := os.ReadDir(filepath.Join(defaultConfigFolderPath, profileRootFolder)) + if err != nil { + return nil, fmt.Errorf("read profile folders: %w", err) + } + + for _, profileFolder := range profileFolders { + if profileFolder.IsDir() { + profiles = append(profiles, profileFolder.Name()) + } + } + + return profiles, nil +} + +// DeleteProfile deletes a profile. +// If the profile does not exist, it returns an error. +// If the profile is the active profile, it sets the active profile to the default profile. +func DeleteProfile(p *print.Printer, profile string) error { + err := ValidateProfile(profile) + if err != nil { + return fmt.Errorf("validate profile: %w", err) + } + + activeProfile, err := GetProfile() + if err != nil { + return fmt.Errorf("get active profile: %w", err) + } + + profileExists, err := ProfileExists(profile) + if err != nil { + return fmt.Errorf("check if profile exists: %w", err) + } + + if !profileExists { + return &errors.DeleteInexistentProfile{Profile: profile} + } + + err = os.RemoveAll(filepath.Join(defaultConfigFolderPath, profileRootFolder, profile)) + if err != nil { + return fmt.Errorf("remove profile folder: %w", err) + } + + if activeProfile == profile { + err = UnsetProfile(p) + if err != nil { + return fmt.Errorf("unset profile: %w", err) + } + } + + if p != nil { + p.Debug(print.DebugLevel, "deleted profile %q", profile) + } + + return nil +} diff --git a/internal/pkg/config/profiles_test.go b/internal/pkg/config/profiles_test.go index 1250a16aa..bb96918c3 100644 --- a/internal/pkg/config/profiles_test.go +++ b/internal/pkg/config/profiles_test.go @@ -16,11 +16,6 @@ func TestValidateProfile(t *testing.T) { profile: "myprofile", isValid: true, }, - { - description: "valid profile with uppercase letters", - profile: "myProfile", - isValid: true, - }, { description: "valid with letters and hyphen", profile: "my-profile", @@ -31,6 +26,11 @@ func TestValidateProfile(t *testing.T) { profile: "my-profile-123", isValid: true, }, + { + description: "valid with letters, numbers, and ending with hyphen", + profile: "my-profile123-", + isValid: true, + }, { description: "invalid empty", profile: "", @@ -46,6 +46,16 @@ func TestValidateProfile(t *testing.T) { profile: "my profile", isValid: false, }, + { + description: "invalid starting with -", + profile: "-my-profile", + isValid: false, + }, + { + description: "invalid profile with uppercase letters", + profile: "myProfile", + isValid: false, + }, } for _, tt := range tests { diff --git a/internal/pkg/errors/errors.go b/internal/pkg/errors/errors.go index cfc9709c5..17fc29ccf 100644 --- a/internal/pkg/errors/errors.go +++ b/internal/pkg/errors/errors.go @@ -43,6 +43,11 @@ For more details run: To create it, run: $ stackit config profile create %[1]q` + DELETE_INEXISTENT_PROFILE = `the configuration profile %q does not exist. + +To list all profiles, run: + $ stackit config profile list` + ARGUS_INVALID_INPUT_PLAN = `the instance plan was not correctly provided. Either provide the plan ID: @@ -122,7 +127,7 @@ For more details on the available storages for the configured flavor (%[3]s), ru INVALID_PROFILE_NAME = `the profile name %q is invalid. -The profile name can only contain letters, numbers, and "-" and cannot be empty or "default".` +The profile name can only contain lowercase letters, numbers, and "-" and cannot be empty or "default". It can't start with a "-".` USAGE_TIP = `For usage help, run: $ %s --help` @@ -160,6 +165,14 @@ func (e *SetInexistentProfile) Error() string { return fmt.Sprintf(SET_INEXISTENT_PROFILE, e.Profile) } +type DeleteInexistentProfile struct { + Profile string +} + +func (e *DeleteInexistentProfile) Error() string { + return fmt.Sprintf(DELETE_INEXISTENT_PROFILE, e.Profile) +} + type ArgusInputPlanError struct { Cmd *cobra.Command Args []string From 44c1e0c3dd9e7bc14a87f54d80aa9eb022707ab7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Diogo=20Ferr=C3=A3o?= Date: Fri, 31 May 2024 17:18:23 +0100 Subject: [PATCH 17/18] Don't allow deleting the default configuration profile (#363) * dont allow deleting default profile * adapt testing --- internal/cmd/config/profile/delete/delete.go | 4 +++ internal/pkg/auth/storage.go | 6 +++++ internal/pkg/auth/storage_test.go | 27 +++++++------------- internal/pkg/config/profiles.go | 7 ++++- internal/pkg/errors/errors.go | 10 ++++++++ 5 files changed, 35 insertions(+), 19 deletions(-) diff --git a/internal/cmd/config/profile/delete/delete.go b/internal/cmd/config/profile/delete/delete.go index 7916d27b6..d468d93ed 100644 --- a/internal/cmd/config/profile/delete/delete.go +++ b/internal/cmd/config/profile/delete/delete.go @@ -51,6 +51,10 @@ func NewCmd(p *print.Printer) *cobra.Command { return &errors.DeleteInexistentProfile{Profile: model.Profile} } + if model.Profile == config.DefaultProfileName { + return &errors.DeleteDefaultProfile{DefaultProfile: config.DefaultProfileName} + } + if !model.AssumeYes { prompt := fmt.Sprintf("Are you sure you want to delete profile %q? (This cannot be undone)", model.Profile) err = p.PromptForConfirmation(prompt) diff --git a/internal/pkg/auth/storage.go b/internal/pkg/auth/storage.go index 20018a70e..d44cc1133 100644 --- a/internal/pkg/auth/storage.go +++ b/internal/pkg/auth/storage.go @@ -5,10 +5,12 @@ import ( "encoding/json" "errors" "fmt" + "os" "path/filepath" "github.com/stackitcloud/stackit-cli/internal/pkg/config" + pkgErrors "github.com/stackitcloud/stackit-cli/internal/pkg/errors" "github.com/zalando/go-keyring" ) @@ -277,6 +279,10 @@ func DeleteProfileFromKeyring(profile string) error { return fmt.Errorf("validate profile: %w", err) } + if profile == config.DefaultProfileName { + return &pkgErrors.DeleteDefaultProfile{DefaultProfile: config.DefaultProfileName} + } + for _, key := range authFieldKeys { err := deleteAuthFieldInKeyring(profile, key) if err != nil { diff --git a/internal/pkg/auth/storage_test.go b/internal/pkg/auth/storage_test.go index 9f603e145..1591a024f 100644 --- a/internal/pkg/auth/storage_test.go +++ b/internal/pkg/auth/storage_test.go @@ -552,34 +552,20 @@ func TestDeleteProfileFromKeyring(t *testing.T) { activeProfile string isValid bool }{ + { - description: "base, default profile", - keys: authFieldKeys, - activeProfile: config.DefaultProfileName, - isValid: true, - }, - { - description: "missing keys, default profile", - keys: []authFieldKey{ - ACCESS_TOKEN, - SERVICE_ACCOUNT_EMAIL, - }, - activeProfile: config.DefaultProfileName, - isValid: true, - }, - { - description: "base, custom profile", + description: "base", keys: authFieldKeys, activeProfile: "test-profile", isValid: true, }, { - description: "missing keys, custom profile", + description: "missing keys", keys: []authFieldKey{ ACCESS_TOKEN, SERVICE_ACCOUNT_EMAIL, }, - activeProfile: config.DefaultProfileName, + activeProfile: "test-profile", isValid: true, }, { @@ -592,6 +578,11 @@ func TestDeleteProfileFromKeyring(t *testing.T) { keyringFails: true, isValid: false, }, + { + description: "default profile", + activeProfile: config.DefaultProfileName, + isValid: false, + }, } for _, tt := range tests { diff --git a/internal/pkg/config/profiles.go b/internal/pkg/config/profiles.go index bc05dc970..703031d27 100644 --- a/internal/pkg/config/profiles.go +++ b/internal/pkg/config/profiles.go @@ -275,7 +275,7 @@ func ListProfiles() ([]string, error) { } // DeleteProfile deletes a profile. -// If the profile does not exist, it returns an error. +// If the profile does not exist or is the default profile, it returns an error. // If the profile is the active profile, it sets the active profile to the default profile. func DeleteProfile(p *print.Printer, profile string) error { err := ValidateProfile(profile) @@ -283,6 +283,11 @@ func DeleteProfile(p *print.Printer, profile string) error { return fmt.Errorf("validate profile: %w", err) } + // Default profile cannot be deleted + if profile == DefaultProfileName { + return &errors.DeleteDefaultProfile{DefaultProfile: DefaultProfileName} + } + activeProfile, err := GetProfile() if err != nil { return fmt.Errorf("get active profile: %w", err) diff --git a/internal/pkg/errors/errors.go b/internal/pkg/errors/errors.go index 17fc29ccf..68d281cdd 100644 --- a/internal/pkg/errors/errors.go +++ b/internal/pkg/errors/errors.go @@ -48,6 +48,8 @@ To create it, run: To list all profiles, run: $ stackit config profile list` + DELETE_DEFAULT_PROFILE = `the default configuration profile %q cannot be deleted.` + ARGUS_INVALID_INPUT_PLAN = `the instance plan was not correctly provided. Either provide the plan ID: @@ -173,6 +175,14 @@ func (e *DeleteInexistentProfile) Error() string { return fmt.Sprintf(DELETE_INEXISTENT_PROFILE, e.Profile) } +type DeleteDefaultProfile struct { + DefaultProfile string +} + +func (e *DeleteDefaultProfile) Error() string { + return fmt.Sprintf(DELETE_DEFAULT_PROFILE, e.DefaultProfile) +} + type ArgusInputPlanError struct { Cmd *cobra.Command Args []string From b2428211f98b1e8f06d6860bf5ba4b473610404e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Diogo=20Ferr=C3=A3o?= Date: Mon, 3 Jun 2024 12:38:16 +0100 Subject: [PATCH 18/18] address acceptance issues (#365) --- internal/cmd/config/profile/delete/delete.go | 8 ++++++++ internal/pkg/config/profiles.go | 12 +++++++++++- 2 files changed, 19 insertions(+), 1 deletion(-) diff --git a/internal/cmd/config/profile/delete/delete.go b/internal/cmd/config/profile/delete/delete.go index d468d93ed..f93a6163f 100644 --- a/internal/cmd/config/profile/delete/delete.go +++ b/internal/cmd/config/profile/delete/delete.go @@ -55,6 +55,14 @@ func NewCmd(p *print.Printer) *cobra.Command { return &errors.DeleteDefaultProfile{DefaultProfile: config.DefaultProfileName} } + activeProfile, err := config.GetProfile() + if err != nil { + return fmt.Errorf("get profile: %w", err) + } + if activeProfile == model.Profile { + p.Warn("The profile you are trying to delete is the active profile. The default profile will be set to active.\n") + } + if !model.AssumeYes { prompt := fmt.Sprintf("Are you sure you want to delete profile %q? (This cannot be undone)", model.Profile) err = p.PromptForConfirmation(prompt) diff --git a/internal/pkg/config/profiles.go b/internal/pkg/config/profiles.go index 703031d27..f05ae9a7b 100644 --- a/internal/pkg/config/profiles.go +++ b/internal/pkg/config/profiles.go @@ -256,10 +256,20 @@ func GetProfileFolderPath(profile string) string { return filepath.Join(defaultConfigFolderPath, profileRootFolder, profile) } -// ListProfiles returns a list of all profiles. +// ListProfiles returns a list of all non-default profiles. +// If there are no profiles, it returns an empty list. func ListProfiles() ([]string, error) { profiles := []string{} + // Check if the profile root folder exists + _, err := os.Stat(filepath.Join(defaultConfigFolderPath, profileRootFolder)) + if err != nil { + if os.IsNotExist(err) { + return profiles, nil + } + return nil, fmt.Errorf("get profile root folder: %w", err) + } + profileFolders, err := os.ReadDir(filepath.Join(defaultConfigFolderPath, profileRootFolder)) if err != nil { return nil, fmt.Errorf("read profile folders: %w", err)