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") } }) }