From a3560e20c2b209eceeb2924d32c1a902d15d68a8 Mon Sep 17 00:00:00 2001 From: erikostien-pingidentity <69643860+erikostien-pingidentity@users.noreply.github.com> Date: Wed, 11 Sep 2024 16:27:53 -0600 Subject: [PATCH] PDI-1926: Combine profile command into config (#134) * PDI-1926: Combine profile command into config - Remove profile subcommand and move old profile commands into the config subcommand group. - Refactor profiles "Options" to their own package "configuration". This allows the env vars and cobra flag parameters to be decoupled from viper's configuration management, avoiding complicated and confusing interactions with the module. - As part of the configuration package, move all cobra param names, param value variables, env var names, default values, pflag Flags, and viper keys into the new configuration "Options" for easy reference and development. - Use profiles.GetOptionValue() to get configuration in a priority order: cobra flag params > env vars > viper config values > default values. - Add additional custom types that implement pflag.Value interface for configuration options - Remove need for all viper configuration to be set in config file for it to be valid. --- cmd/config/add_profile.go | 42 + cmd/config/add_profile_test.go | 68 ++ cmd/config/config.go | 49 +- cmd/config/config_test.go | 39 +- cmd/config/delete_profile.go | 39 + cmd/config/delete_profile_test.go | 49 ++ cmd/config/get.go | 19 +- cmd/config/get_test.go | 37 +- cmd/config/list_profiles.go | 31 + cmd/config/list_profiles_test.go | 37 + cmd/config/profile/add.go | 56 -- cmd/config/profile/add_test.go | 72 -- cmd/config/profile/delete.go | 33 - cmd/config/profile/delete_test.go | 65 -- cmd/config/profile/describe.go | 32 - cmd/config/profile/describe_test.go | 58 -- cmd/config/profile/list.go | 30 - cmd/config/profile/list_test.go | 30 - cmd/config/profile/profile.go | 23 - cmd/config/profile/profile_test.go | 30 - cmd/config/profile/set_active.go | 32 - cmd/config/profile/set_active_test.go | 64 -- cmd/config/set.go | 7 +- cmd/config/set_active_profile.go | 39 + cmd/config/set_active_profile_test.go | 57 ++ cmd/config/set_test.go | 34 +- cmd/config/unset.go | 7 +- cmd/config/unset_test.go | 31 +- cmd/config/view_profile.go | 37 + cmd/config/view_profile_test.go | 41 + cmd/platform/export.go | 236 ++---- cmd/platform/export_test.go | 154 ++-- cmd/root.go | 169 ++-- .../commands/config/add_profile_internal.go | 116 +++ .../config/add_profile_internal_test.go | 133 +++ internal/commands/config/config_internal.go | 111 +++ .../commands/config/config_internal_test.go | 132 +++ .../config/delete_profile_internal.go | 52 ++ .../config/delete_profile_internal_test.go | 92 +++ internal/commands/config/get_internal.go | 80 +- internal/commands/config/get_internal_test.go | 95 +-- .../commands/config/list_profiles_internal.go | 25 + .../config/list_profiles_internal_test.go | 14 + .../commands/config/profile/add_internal.go | 45 - .../config/profile/add_internal_test.go | 191 ----- .../config/profile/delete_internal.go | 22 - .../config/profile/delete_internal_test.go | 53 -- .../config/profile/describe_internal.go | 53 -- .../config/profile/describe_internal_test.go | 76 -- .../commands/config/profile/list_internal.go | 27 - .../config/profile/list_internal_test.go | 19 - .../config/profile/set_active_internal.go | 25 - .../profile/set_active_internal_test.go | 51 -- .../config/set_active_profile_internal.go | 52 ++ .../set_active_profile_internal_test.go | 57 ++ internal/commands/config/set_internal.go | 196 +++-- internal/commands/config/set_internal_test.go | 144 ++-- internal/commands/config/unset_internal.go | 71 +- .../commands/config/unset_internal_test.go | 54 +- .../commands/config/view_profile_internal.go | 48 ++ .../config/view_profile_internal_test.go | 53 ++ .../feedback/feedback_internal_test.go | 23 - internal/commands/platform/export_internal.go | 221 ++--- .../commands/platform/export_internal_test.go | 773 +++--------------- internal/configuration/config/add_profile.go | 79 ++ internal/configuration/config/config.go | 91 +++ .../configuration/config/delete_profile.go | 33 + internal/configuration/config/get.go | 33 + internal/configuration/config/set.go | 33 + .../config/set_active_profile.go | 33 + internal/configuration/config/unset.go | 33 + internal/configuration/config/view_profile.go | 33 + internal/configuration/configuration.go | 96 +++ internal/configuration/configuration_test.go | 79 ++ internal/configuration/options/options.go | 136 +++ internal/configuration/platform/export.go | 523 ++++++++++++ internal/configuration/profiles/profiles.go | 22 + internal/configuration/root/root.go | 126 +++ .../pingfederate_connector_test.go | 60 +- .../pingfederate_open_id_connect_settings.go | 6 +- ...gfederate_open_id_connect_settings_test.go | 6 +- internal/customtypes/bool.go | 39 + internal/customtypes/bool_test.go | 84 ++ internal/customtypes/export_format.go | 14 +- internal/customtypes/export_format_test.go | 71 +- internal/customtypes/multi_service.go | 110 ++- internal/customtypes/multi_service_test.go | 113 +-- internal/customtypes/output_format.go | 15 +- internal/customtypes/output_format_test.go | 69 +- internal/customtypes/pingone_region.go | 13 +- internal/customtypes/pingone_region_test.go | 73 +- internal/customtypes/string.go | 30 + internal/customtypes/string_slice.go | 45 + internal/customtypes/string_slice_test.go | 51 ++ internal/customtypes/uuid.go | 40 + internal/customtypes/uuid_test.go | 52 ++ internal/input/input.go | 12 + internal/input/input_test.go | 14 + internal/output/output.go | 37 +- internal/profiles/main_viper.go | 179 ---- internal/profiles/main_viper_test.go | 244 ------ internal/profiles/profile_viper.go | 131 --- internal/profiles/profile_viper_test.go | 152 ---- internal/profiles/types.go | 250 ------ internal/profiles/types_test.go | 164 ---- internal/profiles/validate.go | 261 +++--- internal/profiles/validate_test.go | 96 +-- internal/profiles/viper.go | 359 ++++++++ internal/profiles/viper_test.go | 199 +++++ internal/testing/testutils/utils.go | 22 +- .../testing/testutils_cobra/cobra_utils.go | 4 + .../testing/testutils_viper/viper_utils.go | 113 ++- 112 files changed, 5031 insertions(+), 4298 deletions(-) create mode 100644 cmd/config/add_profile.go create mode 100644 cmd/config/add_profile_test.go create mode 100644 cmd/config/delete_profile.go create mode 100644 cmd/config/delete_profile_test.go create mode 100644 cmd/config/list_profiles.go create mode 100644 cmd/config/list_profiles_test.go delete mode 100644 cmd/config/profile/add.go delete mode 100644 cmd/config/profile/add_test.go delete mode 100644 cmd/config/profile/delete.go delete mode 100644 cmd/config/profile/delete_test.go delete mode 100644 cmd/config/profile/describe.go delete mode 100644 cmd/config/profile/describe_test.go delete mode 100644 cmd/config/profile/list.go delete mode 100644 cmd/config/profile/list_test.go delete mode 100644 cmd/config/profile/profile.go delete mode 100644 cmd/config/profile/profile_test.go delete mode 100644 cmd/config/profile/set_active.go delete mode 100644 cmd/config/profile/set_active_test.go create mode 100644 cmd/config/set_active_profile.go create mode 100644 cmd/config/set_active_profile_test.go create mode 100644 cmd/config/view_profile.go create mode 100644 cmd/config/view_profile_test.go create mode 100644 internal/commands/config/add_profile_internal.go create mode 100644 internal/commands/config/add_profile_internal_test.go create mode 100644 internal/commands/config/config_internal.go create mode 100644 internal/commands/config/config_internal_test.go create mode 100644 internal/commands/config/delete_profile_internal.go create mode 100644 internal/commands/config/delete_profile_internal_test.go create mode 100644 internal/commands/config/list_profiles_internal.go create mode 100644 internal/commands/config/list_profiles_internal_test.go delete mode 100644 internal/commands/config/profile/add_internal.go delete mode 100644 internal/commands/config/profile/add_internal_test.go delete mode 100644 internal/commands/config/profile/delete_internal.go delete mode 100644 internal/commands/config/profile/delete_internal_test.go delete mode 100644 internal/commands/config/profile/describe_internal.go delete mode 100644 internal/commands/config/profile/describe_internal_test.go delete mode 100644 internal/commands/config/profile/list_internal.go delete mode 100644 internal/commands/config/profile/list_internal_test.go delete mode 100644 internal/commands/config/profile/set_active_internal.go delete mode 100644 internal/commands/config/profile/set_active_internal_test.go create mode 100644 internal/commands/config/set_active_profile_internal.go create mode 100644 internal/commands/config/set_active_profile_internal_test.go create mode 100644 internal/commands/config/view_profile_internal.go create mode 100644 internal/commands/config/view_profile_internal_test.go delete mode 100644 internal/commands/feedback/feedback_internal_test.go create mode 100644 internal/configuration/config/add_profile.go create mode 100644 internal/configuration/config/config.go create mode 100644 internal/configuration/config/delete_profile.go create mode 100644 internal/configuration/config/get.go create mode 100644 internal/configuration/config/set.go create mode 100644 internal/configuration/config/set_active_profile.go create mode 100644 internal/configuration/config/unset.go create mode 100644 internal/configuration/config/view_profile.go create mode 100644 internal/configuration/configuration.go create mode 100644 internal/configuration/configuration_test.go create mode 100644 internal/configuration/options/options.go create mode 100644 internal/configuration/platform/export.go create mode 100644 internal/configuration/profiles/profiles.go create mode 100644 internal/configuration/root/root.go create mode 100644 internal/customtypes/bool.go create mode 100644 internal/customtypes/bool_test.go create mode 100644 internal/customtypes/string.go create mode 100644 internal/customtypes/string_slice.go create mode 100644 internal/customtypes/string_slice_test.go create mode 100644 internal/customtypes/uuid.go create mode 100644 internal/customtypes/uuid_test.go delete mode 100644 internal/profiles/main_viper.go delete mode 100644 internal/profiles/main_viper_test.go delete mode 100644 internal/profiles/profile_viper.go delete mode 100644 internal/profiles/profile_viper_test.go delete mode 100644 internal/profiles/types.go delete mode 100644 internal/profiles/types_test.go create mode 100644 internal/profiles/viper.go create mode 100644 internal/profiles/viper_test.go diff --git a/cmd/config/add_profile.go b/cmd/config/add_profile.go new file mode 100644 index 0000000..5469637 --- /dev/null +++ b/cmd/config/add_profile.go @@ -0,0 +1,42 @@ +package config + +import ( + "os" + + "github.com/pingidentity/pingctl/cmd/common" + config_internal "github.com/pingidentity/pingctl/internal/commands/config" + "github.com/pingidentity/pingctl/internal/configuration/options" + "github.com/pingidentity/pingctl/internal/logger" + "github.com/spf13/cobra" +) + +func NewConfigAddProfileCommand() *cobra.Command { + cmd := &cobra.Command{ + Args: common.ExactArgs(0), + DisableFlagsInUseLine: true, // We write our own flags in @Use attribute + Example: `pingctl config add-profile +pingctl config add-profile --name myprofile --description "My Profile desc" +pingctl config add-profile --set-active=true`, + Long: `Add a new configuration profile to pingctl.`, + RunE: configAddProfileRunE, + Short: "Add a new configuration profile to pingctl.", + Use: "add-profile [flags]", + } + + cmd.Flags().AddFlag(options.ConfigAddProfileNameOption.Flag) + cmd.Flags().AddFlag(options.ConfigAddProfileDescriptionOption.Flag) + cmd.Flags().AddFlag(options.ConfigAddProfileSetActiveOption.Flag) + + return cmd +} + +func configAddProfileRunE(cmd *cobra.Command, args []string) error { + l := logger.Get() + l.Debug().Msgf("Config add-profile Subcommand Called.") + + if err := config_internal.RunInternalConfigAddProfile(os.Stdin); err != nil { + return err + } + + return nil +} diff --git a/cmd/config/add_profile_test.go b/cmd/config/add_profile_test.go new file mode 100644 index 0000000..c5146cb --- /dev/null +++ b/cmd/config/add_profile_test.go @@ -0,0 +1,68 @@ +package config_test + +import ( + "testing" + + "github.com/pingidentity/pingctl/internal/testing/testutils" + "github.com/pingidentity/pingctl/internal/testing/testutils_cobra" +) + +// Test config add profile command executes without issue +func TestConfigAddProfileCmd_Execute(t *testing.T) { + err := testutils_cobra.ExecutePingctl(t, "config", "add-profile", + "--name", "test-profile", + "--description", "test description", + "--set-active=false") + testutils.CheckExpectedError(t, err, nil) +} + +// Test config add profile command fails when provided too many arguments +func TestConfigAddProfileCmd_TooManyArgs(t *testing.T) { + expectedErrorPattern := `^failed to execute 'pingctl config add-profile': command accepts 0 arg\(s\), received 1$` + err := testutils_cobra.ExecutePingctl(t, "config", "add-profile", "extra-arg") + testutils.CheckExpectedError(t, err, &expectedErrorPattern) +} + +// Test config add profile command fails when provided an invalid flag +func TestConfigAddProfileCmd_InvalidFlag(t *testing.T) { + expectedErrorPattern := `^unknown flag: --invalid-flag$` + err := testutils_cobra.ExecutePingctl(t, "config", "add-profile", "--invalid-flag") + testutils.CheckExpectedError(t, err, &expectedErrorPattern) +} + +// Test config add profile command fails when provided an invalid value for a flag +func TestConfigAddProfileCmd_InvalidFlagValue(t *testing.T) { + expectedErrorPattern := `^invalid argument ".*" for ".*" flag: strconv\.ParseBool: parsing ".*": invalid syntax$` + err := testutils_cobra.ExecutePingctl(t, "config", "add-profile", "--set-active", "invalid-value") + testutils.CheckExpectedError(t, err, &expectedErrorPattern) +} + +// Test config add profile command fails when provided a duplicate profile name +func TestConfigAddProfileCmd_DuplicateProfileName(t *testing.T) { + expectedErrorPattern := `^failed to add profile: invalid profile name: '.*'\. profile already exists$` + err := testutils_cobra.ExecutePingctl(t, "config", "add-profile", + "--name", "default", + "--description", "test description", + "--set-active=false") + testutils.CheckExpectedError(t, err, &expectedErrorPattern) +} + +// Test config add profile command fails when provided an invalid profile name +func TestConfigAddProfileCmd_InvalidProfileName(t *testing.T) { + expectedErrorPattern := `^failed to add profile: invalid profile name: '.*'\. name must contain only alphanumeric characters, underscores, and dashes$` + err := testutils_cobra.ExecutePingctl(t, "config", "add-profile", + "--name", "pname&*^*&^$&@!", + "--description", "test description", + "--set-active=false") + testutils.CheckExpectedError(t, err, &expectedErrorPattern) +} + +// Test config add profile command fails when provided an invalid set-active value +func TestConfigAddProfileCmd_InvalidSetActiveValue(t *testing.T) { + expectedErrorPattern := `^invalid argument ".*" for "-s, --set-active" flag: strconv\.ParseBool: parsing ".*": invalid syntax$` + err := testutils_cobra.ExecutePingctl(t, "config", "add-profile", + "--name", "test-profile", + "--description", "test description", + "--set-active", "invalid-value") + testutils.CheckExpectedError(t, err, &expectedErrorPattern) +} diff --git a/cmd/config/config.go b/cmd/config/config.go index 3a7efd8..4c91d19 100644 --- a/cmd/config/config.go +++ b/cmd/config/config.go @@ -1,21 +1,54 @@ package config import ( - "github.com/pingidentity/pingctl/cmd/config/profile" + "os" + + "github.com/pingidentity/pingctl/cmd/common" + config_internal "github.com/pingidentity/pingctl/internal/commands/config" + "github.com/pingidentity/pingctl/internal/configuration/options" + "github.com/pingidentity/pingctl/internal/logger" "github.com/spf13/cobra" ) func NewConfigCommand() *cobra.Command { cmd := &cobra.Command{ - Long: `Command to get, set, and unset pingctl configuration settings.`, - Short: "Command to get, set, and unset pingctl configuration settings.", - Use: "config", + Args: common.ExactArgs(0), + DisableFlagsInUseLine: true, // We write our own flags in @Use attribute + Example: `pingctl config +pingctl config --profile myprofile +pingctl config --name myprofile --description "My Profile"`, + Long: `Update an existing configuration profile's name and description. See subcommands for more profile configuration management options.`, + RunE: configRunE, + Short: "Update an existing configuration profile's name and description. See subcommands for more profile configuration management options.", + Use: "config [flags]", } - cmd.AddCommand(NewConfigGetCommand()) - cmd.AddCommand(NewConfigSetCommand()) - cmd.AddCommand(NewConfigUnsetCommand()) - cmd.AddCommand(profile.NewConfigProfileCommand()) + // Add subcommands + cmd.AddCommand( + NewConfigAddProfileCommand(), + NewConfigDeleteProfileCommand(), + NewConfigViewProfileCommand(), + NewConfigListProfilesCommand(), + NewConfigSetActiveProfileCommand(), + NewConfigGetCommand(), + NewConfigSetCommand(), + NewConfigUnsetCommand(), + ) + + cmd.Flags().AddFlag(options.ConfigProfileOption.Flag) + cmd.Flags().AddFlag(options.ConfigNameOption.Flag) + cmd.Flags().AddFlag(options.ConfigDescriptionOption.Flag) return cmd } + +func configRunE(cmd *cobra.Command, args []string) error { + l := logger.Get() + l.Debug().Msgf("Config Subcommand Called.") + + if err := config_internal.RunInternalConfig(os.Stdin); err != nil { + return err + } + + return nil +} diff --git a/cmd/config/config_test.go b/cmd/config/config_test.go index ad96759..ba554b5 100644 --- a/cmd/config/config_test.go +++ b/cmd/config/config_test.go @@ -9,7 +9,11 @@ import ( // Test Config Command Executes without issue func TestConfigCmd_Execute(t *testing.T) { - err := testutils_cobra.ExecutePingctl(t, "config") + err := testutils_cobra.ExecutePingctl(t, "config", + "--profile", "production", + "--name", "myProfile", + "--description", "hello") + testutils.CheckExpectedError(t, err, nil) } @@ -28,3 +32,36 @@ func TestConfigCmd_HelpFlag(t *testing.T) { err = testutils_cobra.ExecutePingctl(t, "config", "-h") testutils.CheckExpectedError(t, err, nil) } + +// Test Config Command fails when provided a profile name that does not exist +func TestConfigCmd_ProfileDoesNotExist(t *testing.T) { + expectedErrorPattern := `^failed to update profile '.*' name to: .*\. invalid profile name: '.*' profile does not exist$` + err := testutils_cobra.ExecutePingctl(t, "config", + "--profile", "nonexistent", + "--name", "myProfile", + "--description", "hello") + + testutils.CheckExpectedError(t, err, &expectedErrorPattern) +} + +// Test Config Command fails when attempting to update the active profile +func TestConfigCmd_UpdateActiveProfile(t *testing.T) { + expectedErrorPattern := `^failed to update profile '.*' name to: .*\. '.*' is the active profile and cannot be deleted$` + err := testutils_cobra.ExecutePingctl(t, "config", + "--profile", "default", + "--name", "myProfile", + "--description", "hello") + + testutils.CheckExpectedError(t, err, &expectedErrorPattern) +} + +// Test Config Command fails when provided an invalid profile name +func TestConfigCmd_InvalidProfileName(t *testing.T) { + expectedErrorPattern := `^failed to update profile '.*' name to: .*\. invalid profile name: '.*'\. name must contain only alphanumeric characters, underscores, and dashes$` + err := testutils_cobra.ExecutePingctl(t, "config", + "--profile", "production", + "--name", "pname&*^*&^$&@!", + "--description", "hello") + + testutils.CheckExpectedError(t, err, &expectedErrorPattern) +} diff --git a/cmd/config/delete_profile.go b/cmd/config/delete_profile.go new file mode 100644 index 0000000..781ac22 --- /dev/null +++ b/cmd/config/delete_profile.go @@ -0,0 +1,39 @@ +package config + +import ( + "os" + + "github.com/pingidentity/pingctl/cmd/common" + config_internal "github.com/pingidentity/pingctl/internal/commands/config" + "github.com/pingidentity/pingctl/internal/configuration/options" + "github.com/pingidentity/pingctl/internal/logger" + "github.com/spf13/cobra" +) + +func NewConfigDeleteProfileCommand() *cobra.Command { + cmd := &cobra.Command{ + Args: common.ExactArgs(0), + DisableFlagsInUseLine: true, // We write our own flags in @Use attribute + Example: `pingctl config delete-profile +pingctl config delete-profile --profile myprofile`, + Long: `Delete a configuration profile from pingctl.`, + RunE: configDeleteProfileRunE, + Short: "Delete a configuration profile from pingctl.", + Use: "delete-profile [flags]", + } + + cmd.Flags().AddFlag(options.ConfigDeleteProfileOption.Flag) + + return cmd +} + +func configDeleteProfileRunE(cmd *cobra.Command, args []string) error { + l := logger.Get() + l.Debug().Msgf("Config delete-profile Subcommand Called.") + + if err := config_internal.RunInternalConfigDeleteProfile(os.Stdin); err != nil { + return err + } + + return nil +} diff --git a/cmd/config/delete_profile_test.go b/cmd/config/delete_profile_test.go new file mode 100644 index 0000000..20dfae3 --- /dev/null +++ b/cmd/config/delete_profile_test.go @@ -0,0 +1,49 @@ +package config_test + +import ( + "testing" + + "github.com/pingidentity/pingctl/internal/testing/testutils" + "github.com/pingidentity/pingctl/internal/testing/testutils_cobra" +) + +// Test Config delete-profile Command Executes without issue +func TestConfigDeleteProfileCmd_Execute(t *testing.T) { + err := testutils_cobra.ExecutePingctl(t, "config", "delete-profile", "--profile", "production") + testutils.CheckExpectedError(t, err, nil) +} + +// Test Config delete-profile Command fails when provided too many arguments +func TestConfigDeleteProfileCmd_TooManyArgs(t *testing.T) { + expectedErrorPattern := `^failed to execute 'pingctl config delete-profile': command accepts 0 arg\(s\), received 1$` + err := testutils_cobra.ExecutePingctl(t, "config", "delete-profile", "extra-arg") + testutils.CheckExpectedError(t, err, &expectedErrorPattern) +} + +// Test Config delete-profile Command fails when provided an invalid flag +func TestConfigDeleteProfileCmd_InvalidFlag(t *testing.T) { + expectedErrorPattern := `^unknown flag: --invalid$` + err := testutils_cobra.ExecutePingctl(t, "config", "delete-profile", "--invalid") + testutils.CheckExpectedError(t, err, &expectedErrorPattern) +} + +// Test Config delete-profile Command fails when provided an non-existent profile name +func TestConfigDeleteProfileCmd_NonExistentProfileName(t *testing.T) { + expectedErrorPattern := `^failed to delete profile: invalid profile name: '.*' profile does not exist$` + err := testutils_cobra.ExecutePingctl(t, "config", "delete-profile", "--profile", "nonexistent") + testutils.CheckExpectedError(t, err, &expectedErrorPattern) +} + +// Test Config delete-profile Command fails when provided the active profile +func TestConfigDeleteProfileCmd_ActiveProfile(t *testing.T) { + expectedErrorPattern := `^failed to delete profile: '.*' is the active profile and cannot be deleted$` + err := testutils_cobra.ExecutePingctl(t, "config", "delete-profile", "--profile", "default") + testutils.CheckExpectedError(t, err, &expectedErrorPattern) +} + +// Test Config delete-profile Command fails when provided an invalid profile name +func TestConfigDeleteProfileCmd_InvalidProfileName(t *testing.T) { + expectedErrorPattern := `^failed to delete profile: invalid profile name: '.*'\. name must contain only alphanumeric characters, underscores, and dashes$` + err := testutils_cobra.ExecutePingctl(t, "config", "delete-profile", "--profile", "pname&*^*&^$&@!") + testutils.CheckExpectedError(t, err, &expectedErrorPattern) +} diff --git a/cmd/config/get.go b/cmd/config/get.go index 320e6cf..2d23603 100644 --- a/cmd/config/get.go +++ b/cmd/config/get.go @@ -3,24 +3,26 @@ package config import ( "github.com/pingidentity/pingctl/cmd/common" config_internal "github.com/pingidentity/pingctl/internal/commands/config" + "github.com/pingidentity/pingctl/internal/configuration/options" "github.com/pingidentity/pingctl/internal/logger" "github.com/spf13/cobra" ) func NewConfigGetCommand() *cobra.Command { cmd := &cobra.Command{ - Args: common.RangeArgs(0, 1), + Args: common.ExactArgs(1), DisableFlagsInUseLine: true, // We write our own flags in @Use attribute - Example: `pingctl config get -pingctl config get pingone -pingctl config get pingctl.color + Example: `pingctl config get pingone +pingctl config get --profile myProfile pingctl.color pingctl config get pingone.export.environmentID`, Long: `Get pingctl configuration settings.`, RunE: configGetRunE, Short: "Get pingctl configuration settings.", - Use: "get [flags] [key]", + Use: "get [flags] key", } + cmd.Flags().AddFlag(options.ConfigGetProfileOption.Flag) + return cmd } @@ -28,12 +30,7 @@ func configGetRunE(cmd *cobra.Command, args []string) error { l := logger.Get() l.Debug().Msgf("Config Get Subcommand Called.") - key := "" - if len(args) > 0 { - key = args[0] - } - - if err := config_internal.RunInternalConfigGet(key); err != nil { + if err := config_internal.RunInternalConfigGet(args[0]); err != nil { return err } diff --git a/cmd/config/get_test.go b/cmd/config/get_test.go index 70a5ab4..d1519e8 100644 --- a/cmd/config/get_test.go +++ b/cmd/config/get_test.go @@ -3,39 +3,62 @@ package config_test import ( "testing" - "github.com/pingidentity/pingctl/internal/profiles" + "github.com/pingidentity/pingctl/internal/configuration/options" "github.com/pingidentity/pingctl/internal/testing/testutils" "github.com/pingidentity/pingctl/internal/testing/testutils_cobra" ) // Test Config Get Command Executes without issue func TestConfigGetCmd_Execute(t *testing.T) { - err := testutils_cobra.ExecutePingctl(t, "config", "get") + err := testutils_cobra.ExecutePingctl(t, "config", "get", "export") testutils.CheckExpectedError(t, err, nil) } // Test Config Get Command fails when provided too many arguments func TestConfigGetCmd_TooManyArgs(t *testing.T) { - expectedErrorPattern := `^failed to execute 'pingctl config get': command accepts 0 to 1 arg\(s\), received 2$` - err := testutils_cobra.ExecutePingctl(t, "config", "get", profiles.ColorOption.ViperKey, profiles.OutputOption.ViperKey) + expectedErrorPattern := `^failed to execute 'pingctl config get': command accepts 1 arg\(s\), received 2$` + err := testutils_cobra.ExecutePingctl(t, "config", "get", options.RootColorOption.ViperKey, options.RootOutputFormatOption.ViperKey) testutils.CheckExpectedError(t, err, &expectedErrorPattern) } // Test Config Get Command Executes when provided a full key func TestConfigGetCmd_FullKey(t *testing.T) { - err := testutils_cobra.ExecutePingctl(t, "config", "get", profiles.PingOneWorkerClientIDOption.ViperKey) + err := testutils_cobra.ExecutePingctl(t, "config", "get", options.PlatformExportPingoneWorkerClientIDOption.ViperKey) testutils.CheckExpectedError(t, err, nil) } // Test Config Get Command Executes when provided a partial key func TestConfigGetCmd_PartialKey(t *testing.T) { - err := testutils_cobra.ExecutePingctl(t, "config", "get", "pingone") + err := testutils_cobra.ExecutePingctl(t, "config", "get", "export.pingone") testutils.CheckExpectedError(t, err, nil) } // Test Config Get Command fails when provided an invalid key func TestConfigGetCmd_InvalidKey(t *testing.T) { - expectedErrorPattern := `^unable to get configuration: value 'pingctl\.invalid' is not recognized as a valid configuration key\. Valid keys: [A-Za-z\.\s,]+$` + expectedErrorPattern := `^failed to get configuration: key '.*' is not recognized as a valid configuration key\. Valid keys: [A-Za-z\.\s,]+$` err := testutils_cobra.ExecutePingctl(t, "config", "get", "pingctl.invalid") testutils.CheckExpectedError(t, err, &expectedErrorPattern) } + +// Test Config Get Command fails when provided an invalid flag +func TestConfigGetCmd_InvalidFlag(t *testing.T) { + expectedErrorPattern := `^unknown flag: --invalid$` + err := testutils_cobra.ExecutePingctl(t, "config", "get", "--invalid") + testutils.CheckExpectedError(t, err, &expectedErrorPattern) +} + +// Test Config Get Command --help, -h flag +func TestConfigGetCmd_HelpFlag(t *testing.T) { + err := testutils_cobra.ExecutePingctl(t, "config", "get", "--help") + testutils.CheckExpectedError(t, err, nil) + + err = testutils_cobra.ExecutePingctl(t, "config", "get", "-h") + testutils.CheckExpectedError(t, err, nil) +} + +// Test Config Get Command fails when provided no key +func TestConfigGetCmd_NoKey(t *testing.T) { + expectedErrorPattern := `^failed to execute '.*': command accepts 1 arg\(s\), received 0$` + err := testutils_cobra.ExecutePingctl(t, "config", "get") + testutils.CheckExpectedError(t, err, &expectedErrorPattern) +} diff --git a/cmd/config/list_profiles.go b/cmd/config/list_profiles.go new file mode 100644 index 0000000..996b4ae --- /dev/null +++ b/cmd/config/list_profiles.go @@ -0,0 +1,31 @@ +package config + +import ( + "github.com/pingidentity/pingctl/cmd/common" + config_internal "github.com/pingidentity/pingctl/internal/commands/config" + "github.com/pingidentity/pingctl/internal/logger" + "github.com/spf13/cobra" +) + +func NewConfigListProfilesCommand() *cobra.Command { + cmd := &cobra.Command{ + Args: common.ExactArgs(0), + DisableFlagsInUseLine: true, // We write our own flags in @Use attribute + Example: `pingctl config list-profiles`, + Long: `List all configuration profiles from pingctl.`, + RunE: configListProfilesRunE, + Short: "List all configuration profiles from pingctl.", + Use: "list-profiles", + } + + return cmd +} + +func configListProfilesRunE(cmd *cobra.Command, args []string) error { + l := logger.Get() + l.Debug().Msgf("Config list-profiles Subcommand Called.") + + config_internal.RunInternalConfigListProfiles() + + return nil +} diff --git a/cmd/config/list_profiles_test.go b/cmd/config/list_profiles_test.go new file mode 100644 index 0000000..c2cb422 --- /dev/null +++ b/cmd/config/list_profiles_test.go @@ -0,0 +1,37 @@ +package config_test + +import ( + "testing" + + "github.com/pingidentity/pingctl/internal/testing/testutils" + "github.com/pingidentity/pingctl/internal/testing/testutils_cobra" +) + +// Test Config list-profiles Command Executes without issue +func TestConfigListProfilesCmd_Execute(t *testing.T) { + err := testutils_cobra.ExecutePingctl(t, "config", "list-profiles") + testutils.CheckExpectedError(t, err, nil) +} + +// Test Config list-profiles Command fails when provided too many arguments +func TestConfigListProfilesCmd_TooManyArgs(t *testing.T) { + expectedErrorPattern := `^failed to execute 'pingctl config list-profiles': command accepts 0 arg\(s\), received 1$` + err := testutils_cobra.ExecutePingctl(t, "config", "list-profiles", "extra-arg") + testutils.CheckExpectedError(t, err, &expectedErrorPattern) +} + +// Test Config list-profiles Command fails when provided an invalid flag +func TestConfigListProfilesCmd_InvalidFlag(t *testing.T) { + expectedErrorPattern := `^unknown flag: --invalid$` + err := testutils_cobra.ExecutePingctl(t, "config", "list-profiles", "--invalid") + testutils.CheckExpectedError(t, err, &expectedErrorPattern) +} + +// Test Config list-profiles Command --help, -h flag +func TestConfigListProfilesCmd_HelpFlag(t *testing.T) { + err := testutils_cobra.ExecutePingctl(t, "config", "list-profiles", "--help") + testutils.CheckExpectedError(t, err, nil) + + err = testutils_cobra.ExecutePingctl(t, "config", "list-profiles", "-h") + testutils.CheckExpectedError(t, err, nil) +} diff --git a/cmd/config/profile/add.go b/cmd/config/profile/add.go deleted file mode 100644 index 9c0328a..0000000 --- a/cmd/config/profile/add.go +++ /dev/null @@ -1,56 +0,0 @@ -package profile - -import ( - "os" - - "github.com/pingidentity/pingctl/cmd/common" - profile_internal "github.com/pingidentity/pingctl/internal/commands/config/profile" - "github.com/pingidentity/pingctl/internal/logger" - "github.com/pingidentity/pingctl/internal/profiles" - "github.com/spf13/cobra" - "github.com/spf13/pflag" -) - -var ( - profileName string - description string - setActive bool - - setActiveFlag *pflag.Flag -) - -func NewConfigProfileAddCommand() *cobra.Command { - cmd := &cobra.Command{ - Args: common.ExactArgs(0), - DisableFlagsInUseLine: true, // We write our own flags in @Use attribute - Example: `pingctl config profile add -pingctl config profile add --name my-profile -pingctl config profile add --name my-profile --set-active -pingctl config profile add --name my-profile --description "My new profile"`, - Long: `Command to add a new configuration profile to pingctl.`, - RunE: configProfileAddRunE, - Short: "Command to add a new configuration profile to pingctl.", - Use: "add [flags]", - } - - // Add flags that are not tracked in the viper configuration file - cmd.Flags().StringVarP(&profileName, "name", "n", "", "Set the name of the new profile.") - cmd.Flags().StringVarP(&description, profiles.ProfileDescriptionOption.CobraParamName, "d", "", "Set the description of the new profile.") - cmd.Flags().BoolVarP(&setActive, "set-active", "s", false, "Set the new profile as the active profile for pingctl.") - - // create flag variable to determine if the boolean flag is default or changed value - // If default, we will need to prompt the user to decide if they want to set the profile as active - setActiveFlag = cmd.Flags().Lookup("set-active") - return cmd -} - -func configProfileAddRunE(cmd *cobra.Command, args []string) error { - l := logger.Get() - l.Debug().Msgf("Config Profile Add Subcommand Called.") - - if err := profile_internal.RunInternalConfigProfileAdd(profileName, description, setActive, setActiveFlag.Changed, os.Stdin); err != nil { - return err - } - - return nil -} diff --git a/cmd/config/profile/add_test.go b/cmd/config/profile/add_test.go deleted file mode 100644 index d0e7012..0000000 --- a/cmd/config/profile/add_test.go +++ /dev/null @@ -1,72 +0,0 @@ -package profile_test - -import ( - "testing" - - "github.com/pingidentity/pingctl/internal/testing/testutils" - "github.com/pingidentity/pingctl/internal/testing/testutils_cobra" -) - -// Test Config Profile Add Command Executes without issue -func TestConfigProfileAddCmd_Execute(t *testing.T) { - err := testutils_cobra.ExecutePingctl(t, "config", "profile", "add", "--name", "new-test-profile-name", "--set-active", "--description", "test-description") - testutils.CheckExpectedError(t, err, nil) -} - -// Test Config Profile Add Command fails when provided too many arguments -func TestConfigProfileAddCmd_TooManyArgs(t *testing.T) { - expectedErrorPattern := `^failed to execute 'pingctl config profile add': command accepts 0 arg\(s\), received 1$` - err := testutils_cobra.ExecutePingctl(t, "config", "profile", "add", "extra-arg") - testutils.CheckExpectedError(t, err, &expectedErrorPattern) -} - -// Test Config Profile Add Command fails when provided an invalid profile name -func TestConfigProfileAddCmd_InvalidProfileName(t *testing.T) { - expectedErrorPattern := `^invalid profile name: '.*'. name must contain only alphanumeric characters, underscores, and dashes$` - err := testutils_cobra.ExecutePingctl(t, "config", "profile", "add", "--name", "invalid$++$#@#$", "--set-active", "--description", "test-description") - testutils.CheckExpectedError(t, err, &expectedErrorPattern) -} - -// Test Config Profile Add Command fails when provided an existing profile name -func TestConfigProfileAddCmd_ExistingProfileName(t *testing.T) { - expectedErrorPattern := `^invalid profile name: '.*' profile already exists$` - err := testutils_cobra.ExecutePingctl(t, "config", "profile", "add", "--name", "default", "--set-active", "--description", "test-description") - testutils.CheckExpectedError(t, err, &expectedErrorPattern) -} - -// Test Config Profile Add Command fails when name flag is not provided a value -func TestConfigProfileAddCmd_NoProfileName(t *testing.T) { - expectedErrorPattern := `^flag needs an argument: --name$` - err := testutils_cobra.ExecutePingctl(t, "config", "profile", "add", "--set-active", "--description", "test-description", "--name") - testutils.CheckExpectedError(t, err, &expectedErrorPattern) -} - -// Test Config Profile Add Command fails when description flag is not provided a value -func TestConfigProfileAddCmd_NoProfileDescription(t *testing.T) { - expectedErrorPattern := `^flag needs an argument: --description$` - err := testutils_cobra.ExecutePingctl(t, "config", "profile", "add", "--name", "new-test-profile-name", "--set-active", "--description") - testutils.CheckExpectedError(t, err, &expectedErrorPattern) -} - -// Test Config Profile Add Command fails when set-active flag is provided an invalid value -func TestConfigProfileAddCmd_InvalidSetActiveValue(t *testing.T) { - expectedErrorPattern := `^invalid argument "invalid" for "-s, --set-active" flag: strconv.ParseBool: parsing "invalid": invalid syntax$` - err := testutils_cobra.ExecutePingctl(t, "config", "profile", "add", "--name", "new-test-profile-name", "--set-active=invalid", "--description", "test-description") - testutils.CheckExpectedError(t, err, &expectedErrorPattern) -} - -// Test Config Profile Add Command fails when provided an invalid flag -func TestConfigProfileAddCmd_InvalidFlag(t *testing.T) { - expectedErrorPattern := `^unknown flag: --invalid$` - err := testutils_cobra.ExecutePingctl(t, "config", "profile", "add", "--invalid") - testutils.CheckExpectedError(t, err, &expectedErrorPattern) -} - -// Test Config Profile Add Command executes successfully when provided help flag -func TestConfigProfileAddCmd_HelpFlag(t *testing.T) { - err := testutils_cobra.ExecutePingctl(t, "config", "profile", "add", "--help") - testutils.CheckExpectedError(t, err, nil) - - err = testutils_cobra.ExecutePingctl(t, "config", "profile", "add", "-h") - testutils.CheckExpectedError(t, err, nil) -} diff --git a/cmd/config/profile/delete.go b/cmd/config/profile/delete.go deleted file mode 100644 index d654c60..0000000 --- a/cmd/config/profile/delete.go +++ /dev/null @@ -1,33 +0,0 @@ -package profile - -import ( - "github.com/pingidentity/pingctl/cmd/common" - profile_internal "github.com/pingidentity/pingctl/internal/commands/config/profile" - "github.com/pingidentity/pingctl/internal/logger" - "github.com/spf13/cobra" -) - -func NewConfigProfileDeleteCommand() *cobra.Command { - cmd := &cobra.Command{ - Args: common.ExactArgs(1), - DisableFlagsInUseLine: true, // We write our own flags in @Use attribute - Example: `pingctl config profile delete my-profile`, - Long: `Command to delete a configuration profile.`, - RunE: configProfileDeleteRunE, - Short: "Command to delete a configuration profile.", - Use: "delete [flags] profile", - } - - return cmd -} - -func configProfileDeleteRunE(cmd *cobra.Command, args []string) error { - l := logger.Get() - l.Debug().Msgf("Config Profile Delete Subcommand Called.") - - if err := profile_internal.RunInternalConfigProfileDelete(args[0]); err != nil { - return err - } - - return nil -} diff --git a/cmd/config/profile/delete_test.go b/cmd/config/profile/delete_test.go deleted file mode 100644 index 6a9b063..0000000 --- a/cmd/config/profile/delete_test.go +++ /dev/null @@ -1,65 +0,0 @@ -package profile_test - -import ( - "testing" - - "github.com/pingidentity/pingctl/internal/testing/testutils" - "github.com/pingidentity/pingctl/internal/testing/testutils_cobra" -) - -// Test Config Profile Delete Command Executes without issue -func TestConfigProfileDeleteCmd_Execute(t *testing.T) { - err := testutils_cobra.ExecutePingctl(t, "config", "profile", "delete", "production") - testutils.CheckExpectedError(t, err, nil) -} - -// Test Config Profile Delete Command fails when provided too few arguments -func TestConfigProfileDeleteCmd_TooFewArgs(t *testing.T) { - expectedErrorPattern := `^failed to execute 'pingctl config profile delete': command accepts 1 arg\(s\), received 0$` - err := testutils_cobra.ExecutePingctl(t, "config", "profile", "delete") - testutils.CheckExpectedError(t, err, &expectedErrorPattern) -} - -// Test Config Profile Delete Command fails when provided too many arguments -func TestConfigProfileDeleteCmd_TooManyArgs(t *testing.T) { - expectedErrorPattern := `^failed to execute 'pingctl config profile delete': command accepts 1 arg\(s\), received 2$` - err := testutils_cobra.ExecutePingctl(t, "config", "profile", "delete", "production", "extra-arg") - testutils.CheckExpectedError(t, err, &expectedErrorPattern) -} - -// Test Config Profile Delete Command fails when provided an non-existent profile name -func TestConfigProfileDeleteCmd_NonExistentProfileName(t *testing.T) { - expectedErrorPattern := `^failed to delete profile: invalid profile name: '.*' profile does not exist$` - err := testutils_cobra.ExecutePingctl(t, "config", "profile", "delete", "invalid-profile") - testutils.CheckExpectedError(t, err, &expectedErrorPattern) -} - -// Test Config Profile Delete Command fails when provided the active profile name -func TestConfigProfileDeleteCmd_ActiveProfileName(t *testing.T) { - expectedErrorPattern := `^failed to delete profile: '.*' is the active profile and cannot be deleted$` - err := testutils_cobra.ExecutePingctl(t, "config", "profile", "delete", "default") - testutils.CheckExpectedError(t, err, &expectedErrorPattern) -} - -// Test Config Profile Delete Command fails when provided an invalid flag -func TestConfigProfileDeleteCmd_InvalidFlag(t *testing.T) { - expectedErrorPattern := `^unknown flag: --invalid$` - err := testutils_cobra.ExecutePingctl(t, "config", "profile", "delete", "--invalid") - testutils.CheckExpectedError(t, err, &expectedErrorPattern) -} - -// Test Config Profile Delete Command fails when provided an invalid profile name -func TestConfigProfileDeleteCmd_InvalidProfileName(t *testing.T) { - expectedErrorPattern := `^failed to delete profile: invalid profile name: '.*'. name must contain only alphanumeric characters, underscores, and dashes$` - err := testutils_cobra.ExecutePingctl(t, "config", "profile", "delete", "invalid$++$#@#$") - testutils.CheckExpectedError(t, err, &expectedErrorPattern) -} - -// Test Config Profile Delete Command executes successfully when provided the help flag -func TestConfigProfileDeleteCmd_HelpFlag(t *testing.T) { - err := testutils_cobra.ExecutePingctl(t, "config", "profile", "delete", "--help") - testutils.CheckExpectedError(t, err, nil) - - err = testutils_cobra.ExecutePingctl(t, "config", "profile", "delete", "-h") - testutils.CheckExpectedError(t, err, nil) -} diff --git a/cmd/config/profile/describe.go b/cmd/config/profile/describe.go deleted file mode 100644 index b40eebb..0000000 --- a/cmd/config/profile/describe.go +++ /dev/null @@ -1,32 +0,0 @@ -package profile - -import ( - "github.com/pingidentity/pingctl/cmd/common" - profile_internal "github.com/pingidentity/pingctl/internal/commands/config/profile" - "github.com/pingidentity/pingctl/internal/logger" - "github.com/spf13/cobra" -) - -func NewConfigProfileDescribeCommand() *cobra.Command { - cmd := &cobra.Command{ - Args: common.ExactArgs(1), - Example: `pingctl config profile describe my-profile`, - Long: `Command to describe a configuration profile.`, - RunE: configProfileDescribeRunE, - Short: "Command to describe a configuration profile.", - Use: "describe [flags] profile", - } - - return cmd -} - -func configProfileDescribeRunE(cmd *cobra.Command, args []string) error { - l := logger.Get() - l.Debug().Msgf("Config Profile Describe Subcommand Called.") - - if err := profile_internal.RunInternalConfigProfileDescribe(args[0]); err != nil { - return err - } - - return nil -} diff --git a/cmd/config/profile/describe_test.go b/cmd/config/profile/describe_test.go deleted file mode 100644 index 5ff4bc8..0000000 --- a/cmd/config/profile/describe_test.go +++ /dev/null @@ -1,58 +0,0 @@ -package profile_test - -import ( - "testing" - - "github.com/pingidentity/pingctl/internal/testing/testutils" - "github.com/pingidentity/pingctl/internal/testing/testutils_cobra" -) - -// Test Config Profile Describe Command Executes without issue -func TestConfigProfileDescribeCmd_Execute(t *testing.T) { - err := testutils_cobra.ExecutePingctl(t, "config", "profile", "describe", "production") - testutils.CheckExpectedError(t, err, nil) -} - -// Test Config Profile Describe Command fails when provided too few arguments -func TestConfigProfileDescribeCmd_TooFewArgs(t *testing.T) { - expectedErrorPattern := `^failed to execute 'pingctl config profile describe': command accepts 1 arg\(s\), received 0$` - err := testutils_cobra.ExecutePingctl(t, "config", "profile", "describe") - testutils.CheckExpectedError(t, err, &expectedErrorPattern) -} - -// Test Config Profile Describe Command fails when provided too many arguments -func TestConfigProfileDescribeCmd_TooManyArgs(t *testing.T) { - expectedErrorPattern := `^failed to execute 'pingctl config profile describe': command accepts 1 arg\(s\), received 2$` - err := testutils_cobra.ExecutePingctl(t, "config", "profile", "describe", "production", "extra-arg") - testutils.CheckExpectedError(t, err, &expectedErrorPattern) -} - -// Test Config Profile Describe Command fails when provided an non-existent profile name -func TestConfigProfileDescribeCmd_NonExistentProfileName(t *testing.T) { - expectedErrorPattern := `^failed to describe profile: invalid profile name: '.*' profile does not exist$` - err := testutils_cobra.ExecutePingctl(t, "config", "profile", "describe", "invalid-profile") - testutils.CheckExpectedError(t, err, &expectedErrorPattern) -} - -// Test Config Profile Describe Command fails when provided an invalid flag -func TestConfigProfileDescribeCmd_InvalidFlag(t *testing.T) { - expectedErrorPattern := `^unknown flag: --invalid$` - err := testutils_cobra.ExecutePingctl(t, "config", "profile", "describe", "--invalid") - testutils.CheckExpectedError(t, err, &expectedErrorPattern) -} - -// Test Config Profile Describe Command fails when provided an invalid profile name -func TestConfigProfileDescribeCmd_InvalidProfileName(t *testing.T) { - expectedErrorPattern := `^failed to describe profile: invalid profile name: '.*'. name must contain only alphanumeric characters, underscores, and dashes$` - err := testutils_cobra.ExecutePingctl(t, "config", "profile", "describe", "invalid$++$#@#$") - testutils.CheckExpectedError(t, err, &expectedErrorPattern) -} - -// Test Config Profile Describe Command executes successfully when provided the help flag -func TestConfigProfileDescribeCmd_HelpFlag(t *testing.T) { - err := testutils_cobra.ExecutePingctl(t, "config", "profile", "describe", "--help") - testutils.CheckExpectedError(t, err, nil) - - err = testutils_cobra.ExecutePingctl(t, "config", "profile", "describe", "-h") - testutils.CheckExpectedError(t, err, nil) -} diff --git a/cmd/config/profile/list.go b/cmd/config/profile/list.go deleted file mode 100644 index adb750a..0000000 --- a/cmd/config/profile/list.go +++ /dev/null @@ -1,30 +0,0 @@ -package profile - -import ( - "github.com/pingidentity/pingctl/cmd/common" - profile_internal "github.com/pingidentity/pingctl/internal/commands/config/profile" - "github.com/pingidentity/pingctl/internal/logger" - "github.com/spf13/cobra" -) - -func NewConfigProfileListCommand() *cobra.Command { - cmd := &cobra.Command{ - Args: common.ExactArgs(0), - DisableFlagsInUseLine: true, // We write our own flags in @Use attribute - Long: `Command to list all configuration profiles.`, - RunE: configProfileListRunE, - Short: "Command to list all configuration profiles.", - Use: "list [flags]", - } - - return cmd -} - -func configProfileListRunE(cmd *cobra.Command, args []string) error { - l := logger.Get() - l.Debug().Msgf("Config Profile List Subcommand Called.") - - profile_internal.RunInternalConfigProfileList() - - return nil -} diff --git a/cmd/config/profile/list_test.go b/cmd/config/profile/list_test.go deleted file mode 100644 index bca20ba..0000000 --- a/cmd/config/profile/list_test.go +++ /dev/null @@ -1,30 +0,0 @@ -package profile_test - -import ( - "testing" - - "github.com/pingidentity/pingctl/internal/testing/testutils" - "github.com/pingidentity/pingctl/internal/testing/testutils_cobra" -) - -// Test Config Profile List Command Executes without issue -func TestConfigProfileListCmd_Execute(t *testing.T) { - err := testutils_cobra.ExecutePingctl(t, "config", "profile", "list") - testutils.CheckExpectedError(t, err, nil) -} - -// Test Config Profile List Command fails when provided too many arguments -func TestConfigProfileListCmd_TooManyArgs(t *testing.T) { - expectedErrorPattern := `^failed to execute '.*': command accepts \d arg\(s\), received 1$` - err := testutils_cobra.ExecutePingctl(t, "config", "profile", "list", "extra-arg") - testutils.CheckExpectedError(t, err, &expectedErrorPattern) -} - -// Test Config Profile List Command executes successfully when provided the help flag -func TestConfigProfileListCmd_HelpFlag(t *testing.T) { - err := testutils_cobra.ExecutePingctl(t, "config", "profile", "list", "--help") - testutils.CheckExpectedError(t, err, nil) - - err = testutils_cobra.ExecutePingctl(t, "config", "profile", "list", "-h") - testutils.CheckExpectedError(t, err, nil) -} diff --git a/cmd/config/profile/profile.go b/cmd/config/profile/profile.go deleted file mode 100644 index 0d7244e..0000000 --- a/cmd/config/profile/profile.go +++ /dev/null @@ -1,23 +0,0 @@ -package profile - -import ( - "github.com/spf13/cobra" -) - -func NewConfigProfileCommand() *cobra.Command { - cmd := &cobra.Command{ - Long: `Command to add, list, describe, delete, and set active configuration profiles.`, - Short: "Command to add, list, describe, delete, and set active configuration profiles.", - Use: "profile", - } - - cmd.AddCommand( - NewConfigProfileAddCommand(), - NewConfigProfileDeleteCommand(), - NewConfigProfileDescribeCommand(), - NewConfigProfileListCommand(), - NewConfigProfileSetActiveCommand(), - ) - - return cmd -} diff --git a/cmd/config/profile/profile_test.go b/cmd/config/profile/profile_test.go deleted file mode 100644 index da6307b..0000000 --- a/cmd/config/profile/profile_test.go +++ /dev/null @@ -1,30 +0,0 @@ -package profile_test - -import ( - "testing" - - "github.com/pingidentity/pingctl/internal/testing/testutils" - "github.com/pingidentity/pingctl/internal/testing/testutils_cobra" -) - -// Test Config Command Executes without issue -func TestConfigProfileCmd_Execute(t *testing.T) { - err := testutils_cobra.ExecutePingctl(t, "config", "profile") - testutils.CheckExpectedError(t, err, nil) -} - -// Test Config Command fails when provided invalid flag -func TestConfigProfileCmd_InvalidFlag(t *testing.T) { - expectedErrorPattern := `^unknown flag: --invalid$` - err := testutils_cobra.ExecutePingctl(t, "config", "profile", "--invalid") - testutils.CheckExpectedError(t, err, &expectedErrorPattern) -} - -// Test Config Command --help, -h flag -func TestConfigProfileCmd_HelpFlag(t *testing.T) { - err := testutils_cobra.ExecutePingctl(t, "config", "profile", "--help") - testutils.CheckExpectedError(t, err, nil) - - err = testutils_cobra.ExecutePingctl(t, "config", "profile", "-h") - testutils.CheckExpectedError(t, err, nil) -} diff --git a/cmd/config/profile/set_active.go b/cmd/config/profile/set_active.go deleted file mode 100644 index da5f844..0000000 --- a/cmd/config/profile/set_active.go +++ /dev/null @@ -1,32 +0,0 @@ -package profile - -import ( - "github.com/pingidentity/pingctl/cmd/common" - profile_internal "github.com/pingidentity/pingctl/internal/commands/config/profile" - "github.com/pingidentity/pingctl/internal/logger" - "github.com/spf13/cobra" -) - -func NewConfigProfileSetActiveCommand() *cobra.Command { - cmd := &cobra.Command{ - Args: common.ExactArgs(1), - Example: `pingctl config profile set-active my-profile`, - Long: `Command to set the active configuration profile.`, - RunE: configProfileSetActiveRunE, - Short: "Command to set the active configuration profile.", - Use: "set-active [flags] profile", - } - - return cmd -} - -func configProfileSetActiveRunE(cmd *cobra.Command, args []string) error { - l := logger.Get() - l.Debug().Msgf("Config Profile set-active Subcommand Called.") - - if err := profile_internal.RunInternalConfigProfileSetActive(args[0]); err != nil { - return err - } - - return nil -} diff --git a/cmd/config/profile/set_active_test.go b/cmd/config/profile/set_active_test.go deleted file mode 100644 index eeb7624..0000000 --- a/cmd/config/profile/set_active_test.go +++ /dev/null @@ -1,64 +0,0 @@ -package profile_test - -import ( - "testing" - - "github.com/pingidentity/pingctl/internal/testing/testutils" - "github.com/pingidentity/pingctl/internal/testing/testutils_cobra" -) - -// Test Config Profile Set-Active Command Executes without issue -func TestConfigProfileSetActiveCmd_Execute(t *testing.T) { - err := testutils_cobra.ExecutePingctl(t, "config", "profile", "set-active", "production") - testutils.CheckExpectedError(t, err, nil) -} - -// Test Config Profile Set-Active Command fails when provided too few arguments -func TestConfigProfileSetActiveCmd_TooFewArgs(t *testing.T) { - expectedErrorPattern := `^failed to execute 'pingctl config profile set-active': command accepts 1 arg\(s\), received 0$` - err := testutils_cobra.ExecutePingctl(t, "config", "profile", "set-active") - testutils.CheckExpectedError(t, err, &expectedErrorPattern) -} - -// Test Config Profile Set-Active Command fails when provided too many arguments -func TestConfigProfileSetActiveCmd_TooManyArgs(t *testing.T) { - expectedErrorPattern := `^failed to execute 'pingctl config profile set-active': command accepts 1 arg\(s\), received 2$` - err := testutils_cobra.ExecutePingctl(t, "config", "profile", "set-active", "production", "extra-arg") - testutils.CheckExpectedError(t, err, &expectedErrorPattern) -} - -// Test Config Profile Set-Active Command fails when provided an non-existent profile name -func TestConfigProfileSetActiveCmd_NonExistentProfileName(t *testing.T) { - expectedErrorPattern := `^failed to set active profile: invalid profile name: '.*' profile does not exist$` - err := testutils_cobra.ExecutePingctl(t, "config", "profile", "set-active", "invalid-profile") - testutils.CheckExpectedError(t, err, &expectedErrorPattern) -} - -// Test Config Profile Set-Active Command executes successfully when provided the already active profile name -func TestConfigProfileSetActiveCmd_ActiveProfileName(t *testing.T) { - err := testutils_cobra.ExecutePingctl(t, "config", "profile", "set-active", "default") - testutils.CheckExpectedError(t, err, nil) -} - -// Test Config Profile Set-Active Command fails when provided an invalid flag -func TestConfigProfileSetActiveCmd_InvalidFlag(t *testing.T) { - expectedErrorPattern := `^unknown flag: --invalid$` - err := testutils_cobra.ExecutePingctl(t, "config", "profile", "set-active", "--invalid") - testutils.CheckExpectedError(t, err, &expectedErrorPattern) -} - -// Test Config Profile Set-Active Command fails when provided an invalid profile name -func TestConfigProfileSetActiveCmd_InvalidProfileName(t *testing.T) { - expectedErrorPattern := `^failed to set active profile: invalid profile name: '.*'. name must contain only alphanumeric characters, underscores, and dashes$` - err := testutils_cobra.ExecutePingctl(t, "config", "profile", "set-active", "invalid$++$#@#$") - testutils.CheckExpectedError(t, err, &expectedErrorPattern) -} - -// Test Config Profile Set-Active Command executes successfully when provided the help flag -func TestConfigProfileSetActiveCmd_HelpFlag(t *testing.T) { - err := testutils_cobra.ExecutePingctl(t, "config", "profile", "set-active", "--help") - testutils.CheckExpectedError(t, err, nil) - - err = testutils_cobra.ExecutePingctl(t, "config", "profile", "set-active", "-h") - testutils.CheckExpectedError(t, err, nil) -} diff --git a/cmd/config/set.go b/cmd/config/set.go index e02c828..a999ccc 100644 --- a/cmd/config/set.go +++ b/cmd/config/set.go @@ -3,6 +3,7 @@ package config import ( "github.com/pingidentity/pingctl/cmd/common" config_internal "github.com/pingidentity/pingctl/internal/commands/config" + "github.com/pingidentity/pingctl/internal/configuration/options" "github.com/pingidentity/pingctl/internal/logger" "github.com/spf13/cobra" ) @@ -12,18 +13,20 @@ func NewConfigSetCommand() *cobra.Command { Args: common.ExactArgs(1), DisableFlagsInUseLine: true, // We write our own flags in @Use attribute Example: `pingctl config set pingctl.color=true -pingctl config set pingone.region=AsiaPacific`, +pingctl config set --profile myProfile pingone.region=AsiaPacific`, Long: `Set pingctl configuration settings.`, RunE: configSetRunE, Short: "Set pingctl configuration settings.", Use: "set [flags] key=value", } + cmd.Flags().AddFlag(options.ConfigSetProfileOption.Flag) + return cmd } func configSetRunE(cmd *cobra.Command, args []string) error { l := logger.Get() - l.Debug().Msgf("Config Get Subcommand Called.") + l.Debug().Msgf("Config set Subcommand Called.") if err := config_internal.RunInternalConfigSet(args[0]); err != nil { return err diff --git a/cmd/config/set_active_profile.go b/cmd/config/set_active_profile.go new file mode 100644 index 0000000..b36b49a --- /dev/null +++ b/cmd/config/set_active_profile.go @@ -0,0 +1,39 @@ +package config + +import ( + "os" + + "github.com/pingidentity/pingctl/cmd/common" + config_internal "github.com/pingidentity/pingctl/internal/commands/config" + "github.com/pingidentity/pingctl/internal/configuration/options" + "github.com/pingidentity/pingctl/internal/logger" + "github.com/spf13/cobra" +) + +func NewConfigSetActiveProfileCommand() *cobra.Command { + cmd := &cobra.Command{ + Args: common.ExactArgs(0), + DisableFlagsInUseLine: true, // We write our own flags in @Use attribute + Example: `pingctl config set-active-profile +pingctl config set-active-profile --profile myprofile`, + Long: `Set a configuration profile as the in-use profile for pingctl.`, + RunE: configSetActiveProfileRunE, + Short: "Set a configuration profile as the in-use profile for pingctl.", + Use: "set-active-profile [flags]", + } + + cmd.Flags().AddFlag(options.ConfigSetActiveProfileOption.Flag) + + return cmd +} + +func configSetActiveProfileRunE(cmd *cobra.Command, args []string) error { + l := logger.Get() + l.Debug().Msgf("Config set-active-profile Subcommand Called.") + + if err := config_internal.RunInternalConfigSetActiveProfile(os.Stdin); err != nil { + return err + } + + return nil +} diff --git a/cmd/config/set_active_profile_test.go b/cmd/config/set_active_profile_test.go new file mode 100644 index 0000000..9b1047c --- /dev/null +++ b/cmd/config/set_active_profile_test.go @@ -0,0 +1,57 @@ +package config_test + +import ( + "testing" + + "github.com/pingidentity/pingctl/internal/testing/testutils" + "github.com/pingidentity/pingctl/internal/testing/testutils_cobra" +) + +// Test Config set-active-profile Command Executes without issue +func TestConfigSetActiveProfileCmd_Execute(t *testing.T) { + err := testutils_cobra.ExecutePingctl(t, "config", "set-active-profile", "--profile", "production") + testutils.CheckExpectedError(t, err, nil) +} + +// Test Config set-active-profile Command fails when provided too many arguments +func TestConfigSetActiveProfileCmd_TooManyArgs(t *testing.T) { + expectedErrorPattern := `^failed to execute 'pingctl config set-active-profile': command accepts 0 arg\(s\), received 1$` + err := testutils_cobra.ExecutePingctl(t, "config", "set-active-profile", "extra-arg") + testutils.CheckExpectedError(t, err, &expectedErrorPattern) +} + +// Test Config set-active-profile Command fails when provided an invalid flag +func TestConfigSetActiveProfileCmd_InvalidFlag(t *testing.T) { + expectedErrorPattern := `^unknown flag: --invalid$` + err := testutils_cobra.ExecutePingctl(t, "config", "set-active-profile", "--invalid") + testutils.CheckExpectedError(t, err, &expectedErrorPattern) +} + +// Test Config set-active-profile Command fails when provided an non-existent profile name +func TestConfigSetActiveProfileCmd_NonExistentProfileName(t *testing.T) { + expectedErrorPattern := `^failed to set active profile: invalid profile name: '.*' profile does not exist$` + err := testutils_cobra.ExecutePingctl(t, "config", "set-active-profile", "--profile", "nonexistent") + testutils.CheckExpectedError(t, err, &expectedErrorPattern) +} + +// Test Config set-active-profile Command succeeds when provided the active profile +func TestConfigSetActiveProfileCmd_ActiveProfile(t *testing.T) { + err := testutils_cobra.ExecutePingctl(t, "config", "set-active-profile", "--profile", "default") + testutils.CheckExpectedError(t, err, nil) +} + +// Test Config set-active-profile Command fails when provided an invalid profile name +func TestConfigSetActiveProfileCmd_InvalidProfileName(t *testing.T) { + expectedErrorPattern := `^failed to set active profile: invalid profile name: '.*'\. name must contain only alphanumeric characters, underscores, and dashes$` + err := testutils_cobra.ExecutePingctl(t, "config", "set-active-profile", "--profile", "pname&*^*&^$&@!") + testutils.CheckExpectedError(t, err, &expectedErrorPattern) +} + +// Test Config set-active-profile Command --help, -h flag +func TestConfigSetActiveProfileCmd_HelpFlag(t *testing.T) { + err := testutils_cobra.ExecutePingctl(t, "config", "set-active-profile", "--help") + testutils.CheckExpectedError(t, err, nil) + + err = testutils_cobra.ExecutePingctl(t, "config", "set-active-profile", "-h") + testutils.CheckExpectedError(t, err, nil) +} diff --git a/cmd/config/set_test.go b/cmd/config/set_test.go index 9f5a1ff..629947f 100644 --- a/cmd/config/set_test.go +++ b/cmd/config/set_test.go @@ -4,6 +4,7 @@ import ( "fmt" "testing" + "github.com/pingidentity/pingctl/internal/configuration/options" "github.com/pingidentity/pingctl/internal/profiles" "github.com/pingidentity/pingctl/internal/testing/testutils" "github.com/pingidentity/pingctl/internal/testing/testutils_cobra" @@ -11,7 +12,7 @@ import ( // Test Config Set Command Executes without issue func TestConfigSetCmd_Execute(t *testing.T) { - err := testutils_cobra.ExecutePingctl(t, "config", "set", fmt.Sprintf("%s=false", profiles.ColorOption.ViperKey)) + err := testutils_cobra.ExecutePingctl(t, "config", "set", fmt.Sprintf("%s=false", options.RootColorOption.ViperKey)) testutils.CheckExpectedError(t, err, nil) } @@ -25,7 +26,7 @@ func TestConfigSetCmd_TooFewArgs(t *testing.T) { // Test Config Set Command Fails when provided too many arguments func TestConfigSetCmd_TooManyArgs(t *testing.T) { expectedErrorPattern := `^failed to execute 'pingctl config set': command accepts 1 arg\(s\), received 2$` - err := testutils_cobra.ExecutePingctl(t, "config", "set", fmt.Sprintf("%s=false", profiles.ColorOption.ViperKey), fmt.Sprintf("%s=true", profiles.ColorOption.ViperKey)) + err := testutils_cobra.ExecutePingctl(t, "config", "set", fmt.Sprintf("%s=false", options.RootColorOption.ViperKey), fmt.Sprintf("%s=true", options.RootColorOption.ViperKey)) testutils.CheckExpectedError(t, err, &expectedErrorPattern) } @@ -38,28 +39,47 @@ func TestConfigSetCmd_InvalidKey(t *testing.T) { // Test Config Set Command Fails when an invalid value type is provided func TestConfigSetCmd_InvalidValueType(t *testing.T) { - expectedErrorPattern := `^failed to set configuration: value for key 'pingctl\.color' must be a boolean\. Use 'true' or 'false'$` - err := testutils_cobra.ExecutePingctl(t, "config", "set", fmt.Sprintf("%s=invalid", profiles.ColorOption.ViperKey)) + expectedErrorPattern := `^failed to set configuration: value for key '.*' must be a boolean\. Allowed .*: strconv\.ParseBool: parsing ".*": invalid syntax$` + err := testutils_cobra.ExecutePingctl(t, "config", "set", fmt.Sprintf("%s=invalid", options.RootColorOption.ViperKey)) testutils.CheckExpectedError(t, err, &expectedErrorPattern) } // Test Config Set Command Fails when no value is provided func TestConfigSetCmd_NoValueProvided(t *testing.T) { expectedErrorPattern := `^failed to set configuration: value for key 'pingctl\.color' is empty\. Use 'pingctl config unset pingctl\.color' to unset the key$` - err := testutils_cobra.ExecutePingctl(t, "config", "set", fmt.Sprintf("%s=", profiles.ColorOption.ViperKey)) + err := testutils_cobra.ExecutePingctl(t, "config", "set", fmt.Sprintf("%s=", options.RootColorOption.ViperKey)) testutils.CheckExpectedError(t, err, &expectedErrorPattern) } // Test Config Set Command for key 'pingone.worker.clientId' updates viper configuration func TestConfigSetCmd_CheckViperConfig(t *testing.T) { - viperKey := profiles.PingOneWorkerClientIDOption.ViperKey + viperKey := options.PlatformExportPingoneWorkerClientIDOption.ViperKey viperNewUUID := "12345678-1234-1234-1234-123456789012" err := testutils_cobra.ExecutePingctl(t, "config", "set", fmt.Sprintf("%s=%s", viperKey, viperNewUUID)) testutils.CheckExpectedError(t, err, nil) - viperNewValue := profiles.GetProfileViper().GetString(viperKey) + mainViper := profiles.GetMainConfig().ViperInstance() + profileViperKey := profiles.GetMainConfig().ActiveProfile().Name() + "." + viperKey + + viperNewValue := mainViper.GetString(profileViperKey) if viperNewValue != viperNewUUID { t.Errorf("Expected viper configuration value to be updated") } } + +// Test Config Set Command --help, -h flag +func TestConfigSetCmd_HelpFlag(t *testing.T) { + err := testutils_cobra.ExecutePingctl(t, "config", "set", "--help") + testutils.CheckExpectedError(t, err, nil) + + err = testutils_cobra.ExecutePingctl(t, "config", "set", "-h") + testutils.CheckExpectedError(t, err, nil) +} + +// Test Config Set Command Fails when provided an invalid flag +func TestConfigSetCmd_InvalidFlag(t *testing.T) { + expectedErrorPattern := `^unknown flag: --invalid$` + err := testutils_cobra.ExecutePingctl(t, "config", "set", "--invalid") + testutils.CheckExpectedError(t, err, &expectedErrorPattern) +} diff --git a/cmd/config/unset.go b/cmd/config/unset.go index 4c6ac40..d4bc593 100644 --- a/cmd/config/unset.go +++ b/cmd/config/unset.go @@ -3,6 +3,7 @@ package config import ( "github.com/pingidentity/pingctl/cmd/common" config_internal "github.com/pingidentity/pingctl/internal/commands/config" + "github.com/pingidentity/pingctl/internal/configuration/options" "github.com/pingidentity/pingctl/internal/logger" "github.com/spf13/cobra" ) @@ -12,18 +13,20 @@ func NewConfigUnsetCommand() *cobra.Command { Args: common.ExactArgs(1), DisableFlagsInUseLine: true, // We write our own flags in @Use attribute Example: `pingctl config unset pingctl.color -pingctl config unset pingone.region`, +pingctl config unset --profile myProfile pingone.region`, Long: `Unset pingctl configuration settings.`, RunE: configUnsetRunE, Short: "Unset pingctl configuration settings.", Use: "unset [flags] key", } + cmd.Flags().AddFlag(options.ConfigUnsetProfileOption.Flag) + return cmd } func configUnsetRunE(cmd *cobra.Command, args []string) error { l := logger.Get() - l.Debug().Msgf("Config Get Subcommand Called.") + l.Debug().Msgf("Config unset Subcommand Called.") if err := config_internal.RunInternalConfigUnset(args[0]); err != nil { return err diff --git a/cmd/config/unset_test.go b/cmd/config/unset_test.go index 8c4f0f4..a7aaa6b 100644 --- a/cmd/config/unset_test.go +++ b/cmd/config/unset_test.go @@ -4,6 +4,7 @@ import ( "os" "testing" + "github.com/pingidentity/pingctl/internal/configuration/options" "github.com/pingidentity/pingctl/internal/profiles" "github.com/pingidentity/pingctl/internal/testing/testutils" "github.com/pingidentity/pingctl/internal/testing/testutils_cobra" @@ -11,7 +12,7 @@ import ( // Test Config Unset Command Executes without issue func TestConfigUnsetCmd_Execute(t *testing.T) { - err := testutils_cobra.ExecutePingctl(t, "config", "unset", profiles.ColorOption.ViperKey) + err := testutils_cobra.ExecutePingctl(t, "config", "unset", options.RootColorOption.ViperKey) testutils.CheckExpectedError(t, err, nil) } @@ -25,26 +26,28 @@ func TestConfigUnsetCmd_TooFewArgs(t *testing.T) { // Test Config Set Command Fails when provided too many arguments func TestConfigUnsetCmd_TooManyArgs(t *testing.T) { expectedErrorPattern := `^failed to execute 'pingctl config unset': command accepts 1 arg\(s\), received 2$` - err := testutils_cobra.ExecutePingctl(t, "config", "unset", profiles.ColorOption.ViperKey, profiles.OutputOption.ViperKey) + err := testutils_cobra.ExecutePingctl(t, "config", "unset", options.RootColorOption.ViperKey, options.RootOutputFormatOption.ViperKey) testutils.CheckExpectedError(t, err, &expectedErrorPattern) } // Test Config Unset Command Fails when an invalid key is provided func TestConfigUnsetCmd_InvalidKey(t *testing.T) { - expectedErrorPattern := `^unable to unset configuration: key 'pingctl\.invalid' is not recognized as a valid configuration key\. Valid keys: [A-Za-z\.\s,]+$` + expectedErrorPattern := `^failed to unset configuration: key '.*' is not recognized as a valid configuration key\. Valid keys: [A-Za-z\.\s,]+$` err := testutils_cobra.ExecutePingctl(t, "config", "unset", "pingctl.invalid") testutils.CheckExpectedError(t, err, &expectedErrorPattern) } // Test Config Unset Command for key 'pingone.worker.clientId' updates viper configuration func TestConfigUnsetCmd_CheckViperConfig(t *testing.T) { - viperKey := profiles.PingOneWorkerClientIDOption.ViperKey - viperOldValue := os.Getenv(profiles.PingOneWorkerClientIDOption.EnvVar) + viperKey := options.PlatformExportPingoneWorkerClientIDOption.ViperKey + viperOldValue := os.Getenv(options.PlatformExportPingoneWorkerClientIDOption.EnvVar) err := testutils_cobra.ExecutePingctl(t, "config", "unset", viperKey) testutils.CheckExpectedError(t, err, nil) - viperNewValue := profiles.GetProfileViper().GetString(viperKey) + mainViper := profiles.GetMainConfig().ViperInstance() + profileViperKey := profiles.GetMainConfig().ActiveProfile().Name() + "." + viperKey + viperNewValue := mainViper.GetString(profileViperKey) if viperOldValue == viperNewValue { t.Errorf("Expected viper configuration value to be updated. Old: %s, New: %s", viperOldValue, viperNewValue) } @@ -53,3 +56,19 @@ func TestConfigUnsetCmd_CheckViperConfig(t *testing.T) { t.Errorf("Expected viper configuration value to be empty. Got: %s", viperNewValue) } } + +// Test Config Unset Command Fails when provided an invalid flag +func TestConfigUnsetCmd_InvalidFlag(t *testing.T) { + expectedErrorPattern := `^unknown flag: --invalid$` + err := testutils_cobra.ExecutePingctl(t, "config", "unset", "--invalid") + testutils.CheckExpectedError(t, err, &expectedErrorPattern) +} + +// Test Config Unset Command --help, -h flag +func TestConfigUnsetCmd_HelpFlag(t *testing.T) { + err := testutils_cobra.ExecutePingctl(t, "config", "unset", "--help") + testutils.CheckExpectedError(t, err, nil) + + err = testutils_cobra.ExecutePingctl(t, "config", "unset", "-h") + testutils.CheckExpectedError(t, err, nil) +} diff --git a/cmd/config/view_profile.go b/cmd/config/view_profile.go new file mode 100644 index 0000000..b0a4c98 --- /dev/null +++ b/cmd/config/view_profile.go @@ -0,0 +1,37 @@ +package config + +import ( + "github.com/pingidentity/pingctl/cmd/common" + config_internal "github.com/pingidentity/pingctl/internal/commands/config" + "github.com/pingidentity/pingctl/internal/configuration/options" + "github.com/pingidentity/pingctl/internal/logger" + "github.com/spf13/cobra" +) + +func NewConfigViewProfileCommand() *cobra.Command { + cmd := &cobra.Command{ + Args: common.ExactArgs(0), + DisableFlagsInUseLine: true, // We write our own flags in @Use attribute + Example: `pingctl config view-profile +pingctl config view-profile --profile myprofile`, + Long: `View a configuration profile from pingctl.`, + RunE: configViewProfileRunE, + Short: "View a configuration profile from pingctl.", + Use: "view-profile [flags]", + } + + cmd.Flags().AddFlag(options.ConfigViewProfileOption.Flag) + + return cmd +} + +func configViewProfileRunE(cmd *cobra.Command, args []string) error { + l := logger.Get() + l.Debug().Msgf("Config view-profile Subcommand Called.") + + if err := config_internal.RunInternalConfigViewProfile(); err != nil { + return err + } + + return nil +} diff --git a/cmd/config/view_profile_test.go b/cmd/config/view_profile_test.go new file mode 100644 index 0000000..082e407 --- /dev/null +++ b/cmd/config/view_profile_test.go @@ -0,0 +1,41 @@ +package config_test + +import ( + "testing" + + "github.com/pingidentity/pingctl/internal/testing/testutils" + "github.com/pingidentity/pingctl/internal/testing/testutils_cobra" +) + +// Test Config Set Command Executes without issue +func TestConfigViewProfileCmd_Execute(t *testing.T) { + err := testutils_cobra.ExecutePingctl(t, "config", "view-profile") + testutils.CheckExpectedError(t, err, nil) +} + +// Test Config Set Command Executes with --profile flag +func TestConfigViewProfileCmd_Execute_WithProfileFlag(t *testing.T) { + err := testutils_cobra.ExecutePingctl(t, "config", "view-profile", "--profile", "production") + testutils.CheckExpectedError(t, err, nil) +} + +// Test Config Set Command fails with invalid flag +func TestConfigViewProfileCmd_Execute_InvalidFlag(t *testing.T) { + expectedErrorPattern := `^unknown flag: --invalid$` + err := testutils_cobra.ExecutePingctl(t, "config", "view-profile", "--invalid") + testutils.CheckExpectedError(t, err, &expectedErrorPattern) +} + +// Test Config Set Command fails with non-existent profile +func TestConfigViewProfileCmd_Execute_NonExistentProfile(t *testing.T) { + expectedErrorPattern := `^failed to view profile: invalid profile name: '.*' profile does not exist$` + err := testutils_cobra.ExecutePingctl(t, "config", "view-profile", "--profile", "non-existent") + testutils.CheckExpectedError(t, err, &expectedErrorPattern) +} + +// Test Config Set Command fails with invalid profile +func TestConfigViewProfileCmd_Execute_InvalidProfile(t *testing.T) { + expectedErrorPattern := `^failed to view profile: invalid profile name: '.*'\. name must contain only alphanumeric characters, underscores, and dashes$` + err := testutils_cobra.ExecutePingctl(t, "config", "view-profile", "--profile", "(*&*(#))") + testutils.CheckExpectedError(t, err, &expectedErrorPattern) +} diff --git a/cmd/platform/export.go b/cmd/platform/export.go index 4efd225..7340a6a 100644 --- a/cmd/platform/export.go +++ b/cmd/platform/export.go @@ -1,27 +1,13 @@ package platform import ( - "fmt" - "strings" - "github.com/pingidentity/pingctl/cmd/common" platform_internal "github.com/pingidentity/pingctl/internal/commands/platform" - "github.com/pingidentity/pingctl/internal/connector" - "github.com/pingidentity/pingctl/internal/customtypes" + "github.com/pingidentity/pingctl/internal/configuration/options" "github.com/pingidentity/pingctl/internal/logger" - "github.com/pingidentity/pingctl/internal/profiles" "github.com/spf13/cobra" ) -var ( - multiService customtypes.MultiService = *customtypes.NewMultiService() - - exportFormat customtypes.ExportFormat = connector.ENUMEXPORTFORMAT_HCL - pingoneRegion customtypes.PingOneRegion - outputDir string - overwriteExport bool -) - func NewExportCommand() *cobra.Command { cmd := &cobra.Command{ Args: common.ExactArgs(0), @@ -57,195 +43,89 @@ func exportRunE(cmd *cobra.Command, args []string) error { l.Debug().Msgf("Platform Export Subcommand Called.") - pfBasicAuthFlagsUsed := false - pfAccessTokenAuthFlagsUsed := false - - //Check if basic auth flags are used - if cmd.Flags().Lookup(profiles.PingFederateUsernameOption.CobraParamName).Changed { - pfBasicAuthFlagsUsed = true - } - - //Check if access token auth flags are used - if cmd.Flags().Lookup(profiles.PingFederateAccessTokenOption.CobraParamName).Changed { - pfAccessTokenAuthFlagsUsed = true - } - - return platform_internal.RunInternalExport(cmd.Context(), cmd.Root().Version, outputDir, string(exportFormat), overwriteExport, &multiService, pfBasicAuthFlagsUsed, pfAccessTokenAuthFlagsUsed) + return platform_internal.RunInternalExport(cmd.Context(), cmd.Root().Version) } func initGeneralExportFlags(cmd *cobra.Command) { - // Add flags that are not tracked in the viper configuration file - cmd.Flags().VarP(&exportFormat, "export-format", "e", fmt.Sprintf("Specifies export format\nAllowed: %q", connector.ENUMEXPORTFORMAT_HCL)) - cmd.Flags().VarP(&multiService, "service", "s", fmt.Sprintf("Specifies service(s) to export. Allowed services: %s", multiService.String())) - cmd.Flags().StringVarP(&outputDir, "output-directory", "d", "", "Specifies output directory for export (Default: Present working directory)") - cmd.Flags().BoolVarP(&overwriteExport, "overwrite", "o", false, "Overwrite existing generated exports if set.") + cmd.Flags().AddFlag(options.PlatformExportExportFormatOption.Flag) + cmd.Flags().AddFlag(options.PlatformExportServiceOption.Flag) + cmd.Flags().AddFlag(options.PlatformExportOutputDirectoryOption.Flag) + cmd.Flags().AddFlag(options.PlatformExportOverwriteOption.Flag) } func initPingOneExportFlags(cmd *cobra.Command) { - cmd.Flags().String(profiles.PingOneWorkerEnvironmentIDOption.CobraParamName, "", fmt.Sprintf("The ID of the PingOne environment that contains the worker client used to authenticate. Also configurable via environment variable %s", profiles.PingOneWorkerEnvironmentIDOption.EnvVar)) - profiles.AddFlagBinding(profiles.Binding{ - Option: profiles.PingOneWorkerEnvironmentIDOption, - Flag: cmd.Flags().Lookup(profiles.PingOneWorkerEnvironmentIDOption.CobraParamName), - }) - profiles.AddEnvVarBinding(profiles.PingOneWorkerEnvironmentIDOption) - - cmd.Flags().String(profiles.PingOneExportEnvironmentIDOption.CobraParamName, "", fmt.Sprintf("The ID of the PingOne environment to export. Also configurable via environment variable %s (Default: The PingOne worker environment ID)", profiles.PingOneExportEnvironmentIDOption.EnvVar)) - profiles.AddFlagBinding(profiles.Binding{ - Option: profiles.PingOneExportEnvironmentIDOption, - Flag: cmd.Flags().Lookup(profiles.PingOneExportEnvironmentIDOption.CobraParamName), - }) - profiles.AddEnvVarBinding(profiles.PingOneExportEnvironmentIDOption) - - cmd.Flags().String(profiles.PingOneWorkerClientIDOption.CobraParamName, "", fmt.Sprintf("The ID of the PingOne worker client used to authenticate. Also configurable via environment variable %s", profiles.PingOneWorkerClientIDOption.EnvVar)) - profiles.AddFlagBinding(profiles.Binding{ - Option: profiles.PingOneWorkerClientIDOption, - Flag: cmd.Flags().Lookup(profiles.PingOneWorkerClientIDOption.CobraParamName), - }) - profiles.AddEnvVarBinding(profiles.PingOneWorkerClientIDOption) - - cmd.Flags().String(profiles.PingOneWorkerClientSecretOption.CobraParamName, "", fmt.Sprintf("The PingOne worker client secret used to authenticate. Also configurable via environment variable %s", profiles.PingOneWorkerClientSecretOption.EnvVar)) - profiles.AddFlagBinding(profiles.Binding{ - Option: profiles.PingOneWorkerClientSecretOption, - Flag: cmd.Flags().Lookup(profiles.PingOneWorkerClientSecretOption.CobraParamName), - }) - profiles.AddEnvVarBinding(profiles.PingOneWorkerClientSecretOption) - - cmd.Flags().Var(&pingoneRegion, profiles.PingOneRegionOption.CobraParamName, fmt.Sprintf("The region of the PingOne service(s). Allowed: %s. Also configurable via environment variable %s", strings.Join(customtypes.PingOneRegionValidValues(), ", "), profiles.PingOneRegionOption.EnvVar)) - profiles.AddFlagBinding(profiles.Binding{ - Option: profiles.PingOneRegionOption, - Flag: cmd.Flags().Lookup(profiles.PingOneRegionOption.CobraParamName), - }) - profiles.AddEnvVarBinding(profiles.PingOneRegionOption) - - cmd.MarkFlagsRequiredTogether(profiles.PingOneWorkerEnvironmentIDOption.CobraParamName, profiles.PingOneWorkerClientIDOption.CobraParamName, profiles.PingOneWorkerClientSecretOption.CobraParamName, profiles.PingOneRegionOption.CobraParamName) + cmd.Flags().AddFlag(options.PlatformExportPingoneWorkerEnvironmentIDOption.Flag) + cmd.Flags().AddFlag(options.PlatformExportPingoneExportEnvironmentIDOption.Flag) + cmd.Flags().AddFlag(options.PlatformExportPingoneWorkerClientIDOption.Flag) + cmd.Flags().AddFlag(options.PlatformExportPingoneWorkerClientSecretOption.Flag) + cmd.Flags().AddFlag(options.PlatformExportPingoneRegionOption.Flag) + + cmd.MarkFlagsRequiredTogether( + options.PlatformExportPingoneWorkerEnvironmentIDOption.CobraParamName, + options.PlatformExportPingoneWorkerClientIDOption.CobraParamName, + options.PlatformExportPingoneWorkerClientSecretOption.CobraParamName, + options.PlatformExportPingoneRegionOption.CobraParamName) } func initPingFederateGeneralFlags(cmd *cobra.Command) { - // HTTPS host flag - cmd.Flags().String(profiles.PingFederateHttpsHostOption.CobraParamName, "", fmt.Sprintf("The PingFederate HTTPS host used to communicate with PingFederate's API. Also configurable via environment variable %s", profiles.PingFederateHttpsHostOption.EnvVar)) - profiles.AddFlagBinding(profiles.Binding{ - Option: profiles.PingFederateHttpsHostOption, - Flag: cmd.Flags().Lookup(profiles.PingFederateHttpsHostOption.CobraParamName), - }) - profiles.AddEnvVarBinding(profiles.PingFederateHttpsHostOption) - - // Admin API path flag - cmd.Flags().String(profiles.PingFederateAdminApiPathOption.CobraParamName, "/pf-admin-api/v1", fmt.Sprintf("The PingFederate API URL path used to communicate with PingFederate's API. Also configurable via environment variable %s", profiles.PingFederateAdminApiPathOption.EnvVar)) - profiles.AddFlagBinding(profiles.Binding{ - Option: profiles.PingFederateAdminApiPathOption, - Flag: cmd.Flags().Lookup(profiles.PingFederateAdminApiPathOption.CobraParamName), - }) - profiles.AddEnvVarBinding(profiles.PingFederateAdminApiPathOption) - - // Require both HTTPS host and admin API path flags to be used together - cmd.MarkFlagsRequiredTogether(profiles.PingFederateHttpsHostOption.CobraParamName, profiles.PingFederateAdminApiPathOption.CobraParamName) - - // X-Bypass-External-Validation header flag - cmd.Flags().Bool(profiles.PingFederateXBypassExternalValidationHeaderOption.CobraParamName, false, fmt.Sprintf("Header value in request for PingFederate. PingFederate's connection tests will be bypassed when set to true. Also configurable via environment variable %s", profiles.PingFederateXBypassExternalValidationHeaderOption.EnvVar)) - profiles.AddFlagBinding(profiles.Binding{ - Option: profiles.PingFederateXBypassExternalValidationHeaderOption, - Flag: cmd.Flags().Lookup(profiles.PingFederateXBypassExternalValidationHeaderOption.CobraParamName), - }) - profiles.AddEnvVarBinding(profiles.PingFederateXBypassExternalValidationHeaderOption) - - // CA certificate pem files flag - cmd.Flags().StringSlice(profiles.PingFederateCACertificatePemFilesOption.CobraParamName, []string{}, fmt.Sprintf("Paths to files containing PEM-encoded certificates to be trusted as root CAs when connecting to the PingFederate server over HTTPS. Accepts comma-separated string to delimit multiple PEM files if necessary. Also configurable via environment variable %s", profiles.PingFederateCACertificatePemFilesOption.EnvVar)) - profiles.AddFlagBinding(profiles.Binding{ - Option: profiles.PingFederateCACertificatePemFilesOption, - Flag: cmd.Flags().Lookup(profiles.PingFederateCACertificatePemFilesOption.CobraParamName), - }) - profiles.AddEnvVarBinding(profiles.PingFederateCACertificatePemFilesOption) - - // Insecure Trust All TLS flag - cmd.Flags().Bool(profiles.PingFederateInsecureTrustAllTLSOption.CobraParamName, false, fmt.Sprintf("Set to true to trust any certificate when connecting to the PingFederate server. This is insecure and should not be enabled outside of testing. Also configurable via environment variable %s", profiles.PingFederateInsecureTrustAllTLSOption.EnvVar)) - profiles.AddFlagBinding(profiles.Binding{ - Option: profiles.PingFederateInsecureTrustAllTLSOption, - Flag: cmd.Flags().Lookup(profiles.PingFederateInsecureTrustAllTLSOption.CobraParamName), - }) - profiles.AddEnvVarBinding(profiles.PingFederateInsecureTrustAllTLSOption) + cmd.Flags().AddFlag(options.PlatformExportPingfederateHTTPSHostOption.Flag) + cmd.Flags().AddFlag(options.PlatformExportPingfederateAdminAPIPathOption.Flag) + + cmd.MarkFlagsRequiredTogether( + options.PlatformExportPingfederateHTTPSHostOption.CobraParamName, + options.PlatformExportPingfederateAdminAPIPathOption.CobraParamName) + + cmd.Flags().AddFlag(options.PlatformExportPingfederateXBypassExternalValidationHeaderOption.Flag) + cmd.Flags().AddFlag(options.PlatformExportPingfederateCACertificatePemFilesOption.Flag) + cmd.Flags().AddFlag(options.PlatformExportPingfederateInsecureTrustAllTLSOption.Flag) } func initPingFederateBasicAuthFlags(cmd *cobra.Command) { - cmd.Flags().String(profiles.PingFederateUsernameOption.CobraParamName, "", fmt.Sprintf("The PingFederate username used to authenticate. Also configurable via environment variable %s", profiles.PingFederateUsernameOption.EnvVar)) - profiles.AddFlagBinding(profiles.Binding{ - Option: profiles.PingFederateUsernameOption, - Flag: cmd.Flags().Lookup(profiles.PingFederateUsernameOption.CobraParamName), - }) - profiles.AddEnvVarBinding(profiles.PingFederateUsernameOption) - - cmd.Flags().String(profiles.PingFederatePasswordOption.CobraParamName, "", fmt.Sprintf("The PingFederate password used to authenticate. Also configurable via environment variable %s", profiles.PingFederatePasswordOption.EnvVar)) - profiles.AddFlagBinding(profiles.Binding{ - Option: profiles.PingFederatePasswordOption, - Flag: cmd.Flags().Lookup(profiles.PingFederatePasswordOption.CobraParamName), - }) - profiles.AddEnvVarBinding(profiles.PingFederatePasswordOption) - - // When either the username or password flag is used, both must be used - cmd.MarkFlagsRequiredTogether(profiles.PingFederateUsernameOption.CobraParamName, profiles.PingFederatePasswordOption.CobraParamName) + cmd.Flags().AddFlag(options.PlatformExportPingfederateUsernameOption.Flag) + cmd.Flags().AddFlag(options.PlatformExportPingfederatePasswordOption.Flag) + + cmd.MarkFlagsRequiredTogether( + options.PlatformExportPingfederateUsernameOption.CobraParamName, + options.PlatformExportPingfederatePasswordOption.CobraParamName) } func initPingFederateAccessTokenFlags(cmd *cobra.Command) { - cmd.Flags().String(profiles.PingFederateAccessTokenOption.CobraParamName, "", fmt.Sprintf("The PingFederate access token used to authenticate. Also configurable via environment variable %s", profiles.PingFederateAccessTokenOption.EnvVar)) - profiles.AddFlagBinding(profiles.Binding{ - Option: profiles.PingFederateAccessTokenOption, - Flag: cmd.Flags().Lookup(profiles.PingFederateAccessTokenOption.CobraParamName), - }) - profiles.AddEnvVarBinding(profiles.PingFederateAccessTokenOption) + cmd.Flags().AddFlag(options.PlatformExportPingfederateAccessTokenOption.Flag) } func initPingFederateClientCredentialsFlags(cmd *cobra.Command) { - cmd.Flags().String(profiles.PingFederateClientIDOption.CobraParamName, "", fmt.Sprintf("The PingFederate OAuth client ID used to authenticate. Also configurable via environment variable %s", profiles.PingFederateClientIDOption.EnvVar)) - profiles.AddFlagBinding(profiles.Binding{ - Option: profiles.PingFederateClientIDOption, - Flag: cmd.Flags().Lookup(profiles.PingFederateClientIDOption.CobraParamName), - }) - profiles.AddEnvVarBinding(profiles.PingFederateClientIDOption) - - cmd.Flags().String(profiles.PingFederateClientSecretOption.CobraParamName, "", fmt.Sprintf("The PingFederate OAuth client secret used to authenticate. Also configurable via environment variable %s", profiles.PingFederateClientSecretOption.EnvVar)) - profiles.AddFlagBinding(profiles.Binding{ - Option: profiles.PingFederateClientSecretOption, - Flag: cmd.Flags().Lookup(profiles.PingFederateClientSecretOption.CobraParamName), - }) - profiles.AddEnvVarBinding(profiles.PingFederateClientSecretOption) - - cmd.Flags().String(profiles.PingFederateTokenURLOption.CobraParamName, "", fmt.Sprintf("The PingFederate OAuth token URL used to authenticate. Also configurable via environment variable %s", profiles.PingFederateTokenURLOption.EnvVar)) - profiles.AddFlagBinding(profiles.Binding{ - Option: profiles.PingFederateTokenURLOption, - Flag: cmd.Flags().Lookup(profiles.PingFederateTokenURLOption.CobraParamName), - }) - profiles.AddEnvVarBinding(profiles.PingFederateTokenURLOption) - - // When any of the above flags are used, all must be used - cmd.MarkFlagsRequiredTogether(profiles.PingFederateClientIDOption.CobraParamName, profiles.PingFederateClientSecretOption.CobraParamName, profiles.PingFederateTokenURLOption.CobraParamName) - - cmd.Flags().StringSlice(profiles.PingFederateScopesOption.CobraParamName, []string{}, fmt.Sprintf("The PingFederate OAuth scopes used to authenticate. Accepts comma-separated string to delimit multiple scopes if necessary. Also configurable via environment variable %s", profiles.PingFederateScopesOption.EnvVar)) - profiles.AddFlagBinding(profiles.Binding{ - Option: profiles.PingFederateScopesOption, - Flag: cmd.Flags().Lookup(profiles.PingFederateScopesOption.CobraParamName), - }) - profiles.AddEnvVarBinding(profiles.PingFederateScopesOption) + cmd.Flags().AddFlag(options.PlatformExportPingfederateClientIDOption.Flag) + cmd.Flags().AddFlag(options.PlatformExportPingfederateClientSecretOption.Flag) + cmd.Flags().AddFlag(options.PlatformExportPingfederateTokenURLOption.Flag) + + cmd.MarkFlagsRequiredTogether( + options.PlatformExportPingfederateClientIDOption.CobraParamName, + options.PlatformExportPingfederateClientSecretOption.CobraParamName, + options.PlatformExportPingfederateTokenURLOption.CobraParamName) + + cmd.Flags().AddFlag(options.PlatformExportPingfederateScopesOption.Flag) } func markPingFederateFlagsExclusive(cmd *cobra.Command) { // The username flag cannot be used with the access token or client credentials authentication methods - cmd.MarkFlagsMutuallyExclusive(profiles.PingFederateUsernameOption.CobraParamName, profiles.PingFederateAccessTokenOption.CobraParamName) - cmd.MarkFlagsMutuallyExclusive(profiles.PingFederateUsernameOption.CobraParamName, profiles.PingFederateClientIDOption.CobraParamName) - cmd.MarkFlagsMutuallyExclusive(profiles.PingFederateUsernameOption.CobraParamName, profiles.PingFederateClientSecretOption.CobraParamName) - cmd.MarkFlagsMutuallyExclusive(profiles.PingFederateUsernameOption.CobraParamName, profiles.PingFederateTokenURLOption.CobraParamName) - cmd.MarkFlagsMutuallyExclusive(profiles.PingFederateUsernameOption.CobraParamName, profiles.PingFederateScopesOption.CobraParamName) + cmd.MarkFlagsMutuallyExclusive(options.PlatformExportPingfederateUsernameOption.CobraParamName, options.PlatformExportPingfederateAccessTokenOption.CobraParamName) + cmd.MarkFlagsMutuallyExclusive(options.PlatformExportPingfederateUsernameOption.CobraParamName, options.PlatformExportPingfederateClientIDOption.CobraParamName) + cmd.MarkFlagsMutuallyExclusive(options.PlatformExportPingfederateUsernameOption.CobraParamName, options.PlatformExportPingfederateClientSecretOption.CobraParamName) + cmd.MarkFlagsMutuallyExclusive(options.PlatformExportPingfederateUsernameOption.CobraParamName, options.PlatformExportPingfederateTokenURLOption.CobraParamName) + cmd.MarkFlagsMutuallyExclusive(options.PlatformExportPingfederateUsernameOption.CobraParamName, options.PlatformExportPingfederateScopesOption.CobraParamName) // The password flag cannot be used with the access token or client credentials authentication methods - cmd.MarkFlagsMutuallyExclusive(profiles.PingFederatePasswordOption.CobraParamName, profiles.PingFederateAccessTokenOption.CobraParamName) - cmd.MarkFlagsMutuallyExclusive(profiles.PingFederatePasswordOption.CobraParamName, profiles.PingFederateClientIDOption.CobraParamName) - cmd.MarkFlagsMutuallyExclusive(profiles.PingFederatePasswordOption.CobraParamName, profiles.PingFederateClientSecretOption.CobraParamName) - cmd.MarkFlagsMutuallyExclusive(profiles.PingFederatePasswordOption.CobraParamName, profiles.PingFederateTokenURLOption.CobraParamName) - cmd.MarkFlagsMutuallyExclusive(profiles.PingFederatePasswordOption.CobraParamName, profiles.PingFederateScopesOption.CobraParamName) + cmd.MarkFlagsMutuallyExclusive(options.PlatformExportPingfederatePasswordOption.CobraParamName, options.PlatformExportPingfederateAccessTokenOption.CobraParamName) + cmd.MarkFlagsMutuallyExclusive(options.PlatformExportPingfederatePasswordOption.CobraParamName, options.PlatformExportPingfederateClientIDOption.CobraParamName) + cmd.MarkFlagsMutuallyExclusive(options.PlatformExportPingfederatePasswordOption.CobraParamName, options.PlatformExportPingfederateClientSecretOption.CobraParamName) + cmd.MarkFlagsMutuallyExclusive(options.PlatformExportPingfederatePasswordOption.CobraParamName, options.PlatformExportPingfederateTokenURLOption.CobraParamName) + cmd.MarkFlagsMutuallyExclusive(options.PlatformExportPingfederatePasswordOption.CobraParamName, options.PlatformExportPingfederateScopesOption.CobraParamName) // The access token flag cannot be used with the client credentials authentication method - cmd.MarkFlagsMutuallyExclusive(profiles.PingFederateAccessTokenOption.CobraParamName, profiles.PingFederateClientIDOption.CobraParamName) - cmd.MarkFlagsMutuallyExclusive(profiles.PingFederateAccessTokenOption.CobraParamName, profiles.PingFederateClientSecretOption.CobraParamName) - cmd.MarkFlagsMutuallyExclusive(profiles.PingFederateAccessTokenOption.CobraParamName, profiles.PingFederateTokenURLOption.CobraParamName) - cmd.MarkFlagsMutuallyExclusive(profiles.PingFederateAccessTokenOption.CobraParamName, profiles.PingFederateScopesOption.CobraParamName) + cmd.MarkFlagsMutuallyExclusive(options.PlatformExportPingfederateAccessTokenOption.CobraParamName, options.PlatformExportPingfederateClientIDOption.CobraParamName) + cmd.MarkFlagsMutuallyExclusive(options.PlatformExportPingfederateAccessTokenOption.CobraParamName, options.PlatformExportPingfederateClientSecretOption.CobraParamName) + cmd.MarkFlagsMutuallyExclusive(options.PlatformExportPingfederateAccessTokenOption.CobraParamName, options.PlatformExportPingfederateTokenURLOption.CobraParamName) + cmd.MarkFlagsMutuallyExclusive(options.PlatformExportPingfederateAccessTokenOption.CobraParamName, options.PlatformExportPingfederateScopesOption.CobraParamName) // Client credential flag exclusivity is already defined above. } diff --git a/cmd/platform/export_test.go b/cmd/platform/export_test.go index 3613478..14a6345 100644 --- a/cmd/platform/export_test.go +++ b/cmd/platform/export_test.go @@ -4,21 +4,26 @@ import ( "os" "testing" - "github.com/pingidentity/pingctl/internal/profiles" + "github.com/pingidentity/pingctl/internal/configuration/options" "github.com/pingidentity/pingctl/internal/testing/testutils" "github.com/pingidentity/pingctl/internal/testing/testutils_cobra" + "github.com/pingidentity/pingctl/internal/testing/testutils_viper" ) // Test Platform Export Command Executes without issue func TestPlatformExportCmd_Execute(t *testing.T) { outputDir := t.TempDir() - err := testutils_cobra.ExecutePingctl(t, "platform", "export", "--output-directory", outputDir, "--overwrite") + err := testutils_cobra.ExecutePingctl(t, "platform", "export", + "--output-directory", outputDir, + "--overwrite", "true") testutils.CheckExpectedError(t, err, nil) } // Test Platform Export Command fails when provided too many arguments func TestPlatformExportCmd_TooManyArgs(t *testing.T) { + testutils_viper.InitVipers(t) + expectedErrorPattern := `^failed to execute 'pingctl platform export': command accepts 0 arg\(s\), received 1$` err := testutils_cobra.ExecutePingctl(t, "platform", "export", "extra-arg") testutils.CheckExpectedError(t, err, &expectedErrorPattern) @@ -44,14 +49,17 @@ func TestPlatformExportCmd_HelpFlag(t *testing.T) { func TestPlatformExportCmd_ServiceFlag(t *testing.T) { outputDir := t.TempDir() - err := testutils_cobra.ExecutePingctl(t, "platform", "export", "--output-directory", outputDir, "--overwrite", "--service", "pingone-protect") + err := testutils_cobra.ExecutePingctl(t, "platform", "export", + "--output-directory", outputDir, + "--overwrite", "true", + "--services", "pingone-protect") testutils.CheckExpectedError(t, err, nil) } // Test Platform Export Command --service flag with invalid service func TestPlatformExportCmd_ServiceFlagInvalidService(t *testing.T) { - expectedErrorPattern := `^invalid argument "invalid" for "-s, --service" flag: unrecognized service 'invalid'\. Must be one of: [a-z-\s,]+$` - err := testutils_cobra.ExecutePingctl(t, "platform", "export", "--service", "invalid") + expectedErrorPattern := `^invalid argument "invalid" for "-s, --services" flag: unrecognized service 'invalid'\. Must be one of: [a-z-\s,]+$` + err := testutils_cobra.ExecutePingctl(t, "platform", "export", "--services", "invalid") testutils.CheckExpectedError(t, err, &expectedErrorPattern) } @@ -59,7 +67,11 @@ func TestPlatformExportCmd_ServiceFlagInvalidService(t *testing.T) { func TestPlatformExportCmd_ExportFormatFlag(t *testing.T) { outputDir := t.TempDir() - err := testutils_cobra.ExecutePingctl(t, "platform", "export", "--output-directory", outputDir, "--export-format", "HCL", "--overwrite", "--service", "pingone-protect") + err := testutils_cobra.ExecutePingctl(t, "platform", "export", + "--output-directory", outputDir, + "--export-format", "HCL", + "--overwrite", "true", + "--services", "pingone-protect") testutils.CheckExpectedError(t, err, nil) } @@ -74,7 +86,10 @@ func TestPlatformExportCmd_ExportFormatFlagInvalidFormat(t *testing.T) { func TestPlatformExportCmd_OutputDirectoryFlag(t *testing.T) { outputDir := t.TempDir() - err := testutils_cobra.ExecutePingctl(t, "platform", "export", "--output-directory", outputDir, "--overwrite", "--service", "pingone-protect") + err := testutils_cobra.ExecutePingctl(t, "platform", "export", + "--output-directory", outputDir, + "--overwrite", "true", + "--services", "pingone-protect") testutils.CheckExpectedError(t, err, nil) } @@ -89,7 +104,10 @@ func TestPlatformExportCmd_OutputDirectoryFlagInvalidDirectory(t *testing.T) { func TestPlatformExportCmd_OverwriteFlag(t *testing.T) { outputDir := t.TempDir() - err := testutils_cobra.ExecutePingctl(t, "platform", "export", "--output-directory", outputDir, "--overwrite", "--service", "pingone-protect") + err := testutils_cobra.ExecutePingctl(t, "platform", "export", + "--output-directory", outputDir, + "--overwrite", "true", + "--services", "pingone-protect") testutils.CheckExpectedError(t, err, nil) } @@ -104,7 +122,10 @@ func TestPlatformExportCmd_OverwriteFlagFalseWithExistingDirectory(t *testing.T) } expectedErrorPattern := `^'platform export' output directory '[A-Za-z0-9_\-\/]+' is not empty\. Use --overwrite to overwrite existing export data$` - err = testutils_cobra.ExecutePingctl(t, "platform", "export", "--output-directory", outputDir, "--service", "pingone-protect", "--overwrite=false") + err = testutils_cobra.ExecutePingctl(t, "platform", "export", + "--output-directory", outputDir, + "--services", "pingone-protect", + "--overwrite", "false") testutils.CheckExpectedError(t, err, &expectedErrorPattern) } @@ -118,7 +139,10 @@ func TestPlatformExportCmd_OverwriteFlagTrueWithExistingDirectory(t *testing.T) t.Errorf("Error creating file in output directory: %v", err) } - err = testutils_cobra.ExecutePingctl(t, "platform", "export", "--output-directory", outputDir, "--service", "pingone-protect", "--overwrite") + err = testutils_cobra.ExecutePingctl(t, "platform", "export", + "--output-directory", outputDir, + "--services", "pingone-protect", + "--overwrite", "true") testutils.CheckExpectedError(t, err, nil) } @@ -132,12 +156,12 @@ func TestPlatformExportCmd_PingOneWorkerEnvironmentIdFlag(t *testing.T) { err := testutils_cobra.ExecutePingctl(t, "platform", "export", "--output-directory", outputDir, - "--overwrite", - "--service", "pingone-protect", - "--pingone-worker-environment-id", os.Getenv(profiles.PingOneWorkerEnvironmentIDOption.EnvVar), - "--pingone-worker-client-id", os.Getenv(profiles.PingOneWorkerClientIDOption.EnvVar), - "--pingone-worker-client-secret", os.Getenv(profiles.PingOneWorkerClientSecretOption.EnvVar), - "--pingone-region", os.Getenv(profiles.PingOneRegionOption.EnvVar)) + "--overwrite", "true", + "--services", "pingone-protect", + "--pingone-worker-environment-id", os.Getenv(options.PlatformExportPingoneWorkerEnvironmentIDOption.EnvVar), + "--pingone-worker-client-id", os.Getenv(options.PlatformExportPingoneWorkerClientIDOption.EnvVar), + "--pingone-worker-client-secret", os.Getenv(options.PlatformExportPingoneWorkerClientSecretOption.EnvVar), + "--pingone-region", os.Getenv(options.PlatformExportPingoneRegionOption.EnvVar)) testutils.CheckExpectedError(t, err, nil) } @@ -145,7 +169,7 @@ func TestPlatformExportCmd_PingOneWorkerEnvironmentIdFlag(t *testing.T) { func TestPlatformExportCmd_PingOneWorkerEnvironmentIdFlagRequiredTogether(t *testing.T) { expectedErrorPattern := `^if any flags in the group \[pingone-worker-environment-id pingone-worker-client-id pingone-worker-client-secret pingone-region] are set they must all be set; missing \[pingone-region pingone-worker-client-id pingone-worker-client-secret]$` err := testutils_cobra.ExecutePingctl(t, "platform", "export", - "--pingone-worker-environment-id", os.Getenv(profiles.PingOneWorkerEnvironmentIDOption.EnvVar)) + "--pingone-worker-environment-id", os.Getenv(options.PlatformExportPingoneWorkerEnvironmentIDOption.EnvVar)) testutils.CheckExpectedError(t, err, &expectedErrorPattern) } @@ -155,10 +179,10 @@ func TestPlatformExportCmd_PingFederateBasicAuthFlags(t *testing.T) { err := testutils_cobra.ExecutePingctl(t, "platform", "export", "--output-directory", outputDir, - "--overwrite", - "--service", "pingfederate", - "--pingfederate-username", os.Getenv(profiles.PingFederateUsernameOption.EnvVar), - "--pingfederate-password", os.Getenv(profiles.PingFederatePasswordOption.EnvVar)) + "--overwrite", "true", + "--services", "pingfederate", + "--pingfederate-username", os.Getenv(options.PlatformExportPingfederateUsernameOption.EnvVar), + "--pingfederate-password", os.Getenv(options.PlatformExportPingfederatePasswordOption.EnvVar)) testutils.CheckExpectedError(t, err, nil) } @@ -177,8 +201,8 @@ func TestPlatformExportCmd_PingFederateBasicAuthFlagsInvalid(t *testing.T) { expectedErrorPattern := `^failed to export 'pingfederate' service: failed to export resource .*\. err: .* Request for resource '.*' was not successful\.\s+Response Code: 401 Unauthorized\s+Response Body: {{"resultId":"invalid_credentials","message":"The credentials you provided were not recognized\."}}\s+Error: 401 Unauthorized$` err := testutils_cobra.ExecutePingctl(t, "platform", "export", "--output-directory", outputDir, - "--overwrite", - "--service", "pingfederate", + "--overwrite", "true", + "--services", "pingfederate", "--pingfederate-username", "Administrator", "--pingfederate-password", "invalid") testutils.CheckExpectedError(t, err, &expectedErrorPattern) @@ -190,12 +214,12 @@ func TestPlatformExportCmd_PingFederateClientCredentialsAuthFlags(t *testing.T) err := testutils_cobra.ExecutePingctl(t, "platform", "export", "--output-directory", outputDir, - "--overwrite", - "--service", "pingfederate", - "--pingfederate-client-id", os.Getenv(profiles.PingFederateClientIDOption.EnvVar), - "--pingfederate-client-secret", os.Getenv(profiles.PingFederateClientSecretOption.EnvVar), - "--pingfederate-scopes", os.Getenv(profiles.PingFederateScopesOption.EnvVar), - "--pingfederate-token-url", os.Getenv(profiles.PingFederateTokenURLOption.EnvVar)) + "--overwrite", "true", + "--services", "pingfederate", + "--pingfederate-client-id", os.Getenv(options.PlatformExportPingfederateClientIDOption.EnvVar), + "--pingfederate-client-secret", os.Getenv(options.PlatformExportPingfederateClientSecretOption.EnvVar), + "--pingfederate-scopes", os.Getenv(options.PlatformExportPingfederateScopesOption.EnvVar), + "--pingfederate-token-url", os.Getenv(options.PlatformExportPingfederateTokenURLOption.EnvVar)) testutils.CheckExpectedError(t, err, nil) } @@ -214,8 +238,8 @@ func TestPlatformExportCmd_PingFederateClientCredentialsAuthFlagsInvalid(t *test expectedErrorPattern := `^failed to export 'pingfederate' service: failed to export resource .*\. err: .* Request for resource '.*' was not successful\. Response is nil\. Error: oauth2: "invalid_client" "Invalid client or client credentials\."$` err := testutils_cobra.ExecutePingctl(t, "platform", "export", "--output-directory", outputDir, - "--overwrite", - "--service", "pingfederate", + "--overwrite", "true", + "--services", "pingfederate", "--pingfederate-client-id", "test", "--pingfederate-client-secret", "invalid", "--pingfederate-token-url", "https://localhost:9031/as/token.oauth2") @@ -229,10 +253,10 @@ func TestPlatformExportCmd_PingFederateClientCredentialsAuthFlagsInvalidTokenURL expectedErrorPattern := `(?s)^failed to export 'pingfederate' service: failed to export resource.*\. err:.*Request for resource '.*' was not successful\. Response is nil\. Error: oauth2: cannot fetch token: 404 Not Found\s+Response: \\s+.*$` err := testutils_cobra.ExecutePingctl(t, "platform", "export", "--output-directory", outputDir, - "--overwrite", - "--service", "pingfederate", - "--pingfederate-client-id", os.Getenv(profiles.PingFederateClientIDOption.EnvVar), - "--pingfederate-client-secret", os.Getenv(profiles.PingFederateClientSecretOption.EnvVar), + "--overwrite", "true", + "--services", "pingfederate", + "--pingfederate-client-id", os.Getenv(options.PlatformExportPingfederateClientIDOption.EnvVar), + "--pingfederate-client-secret", os.Getenv(options.PlatformExportPingfederateClientSecretOption.EnvVar), "--pingfederate-token-url", "https://localhost:9031/as/invalid") testutils.CheckExpectedError(t, err, &expectedErrorPattern) } @@ -241,11 +265,11 @@ func TestPlatformExportCmd_PingFederateClientCredentialsAuthFlagsInvalidTokenURL func TestPlatformExportCmd_PingFederateClientCredentialsAuthFlagsWithUsername(t *testing.T) { expectedErrorPattern := `^if any flags in the group \[.*\] are set none of the others can be; \[.*\] were all set$` err := testutils_cobra.ExecutePingctl(t, "platform", "export", - "--pingfederate-client-id", os.Getenv(profiles.PingFederateClientIDOption.EnvVar), - "--pingfederate-client-secret", os.Getenv(profiles.PingFederateClientSecretOption.EnvVar), - "--pingfederate-token-url", os.Getenv(profiles.PingFederateTokenURLOption.EnvVar), - "--pingfederate-username", os.Getenv(profiles.PingFederateUsernameOption.EnvVar), - "--pingfederate-password", os.Getenv(profiles.PingFederatePasswordOption.EnvVar)) + "--pingfederate-client-id", os.Getenv(options.PlatformExportPingfederateClientIDOption.EnvVar), + "--pingfederate-client-secret", os.Getenv(options.PlatformExportPingfederateClientSecretOption.EnvVar), + "--pingfederate-token-url", os.Getenv(options.PlatformExportPingfederateTokenURLOption.EnvVar), + "--pingfederate-username", os.Getenv(options.PlatformExportPingfederateUsernameOption.EnvVar), + "--pingfederate-password", os.Getenv(options.PlatformExportPingfederatePasswordOption.EnvVar)) testutils.CheckExpectedError(t, err, &expectedErrorPattern) } @@ -254,9 +278,9 @@ func TestPlatformExportCmd_PingFederateClientCredentialsAuthFlagsWithUsername(t func TestPlatformExportCmd_PingFederateClientCredentialsAuthFlagsWithAccessToken(t *testing.T) { expectedErrorPattern := `^if any flags in the group \[.*\] are set none of the others can be; \[.*\] were all set$` err := testutils_cobra.ExecutePingctl(t, "platform", "export", - "--pingfederate-client-id", os.Getenv(profiles.PingFederateClientIDOption.EnvVar), - "--pingfederate-client-secret", os.Getenv(profiles.PingFederateClientSecretOption.EnvVar), - "--pingfederate-token-url", os.Getenv(profiles.PingFederateTokenURLOption.EnvVar), + "--pingfederate-client-id", os.Getenv(options.PlatformExportPingfederateClientIDOption.EnvVar), + "--pingfederate-client-secret", os.Getenv(options.PlatformExportPingfederateClientSecretOption.EnvVar), + "--pingfederate-token-url", os.Getenv(options.PlatformExportPingfederateTokenURLOption.EnvVar), "--pingfederate-access-token", "token") testutils.CheckExpectedError(t, err, &expectedErrorPattern) @@ -269,11 +293,11 @@ func TestPlatformExportCmd_PingFederateClientCredentialsAuthFlagsWithInvalidBasi expectedErrorPattern := `^failed to export 'pingfederate' service: failed to export resource .*\. err: .* Request for resource '.*' was not successful\.\s+Response Code: 401 Unauthorized\s+Response Body: {{"resultId":"invalid_credentials","message":"The credentials you provided were not recognized\."}}\s+Error: 401 Unauthorized$` err := testutils_cobra.ExecutePingctl(t, "platform", "export", - "--pingfederate-username", os.Getenv(profiles.PingFederateUsernameOption.EnvVar), + "--pingfederate-username", os.Getenv(options.PlatformExportPingfederateUsernameOption.EnvVar), "--pingfederate-password", "invalid", "--output-directory", outputDir, - "--service", "pingfederate", - "--overwrite") + "--services", "pingfederate", + "--overwrite", "true") testutils.CheckExpectedError(t, err, &expectedErrorPattern) } @@ -283,11 +307,11 @@ func TestPlatformExportCmd_PingFederateXBypassHeaderFlag(t *testing.T) { err := testutils_cobra.ExecutePingctl(t, "platform", "export", "--output-directory", outputDir, - "--overwrite", - "--service", "pingfederate", + "--overwrite", "true", + "--services", "pingfederate", "--pingfederate-x-bypass-external-validation-header=true", - "--pingfederate-username", os.Getenv(profiles.PingFederateUsernameOption.EnvVar), - "--pingfederate-password", os.Getenv(profiles.PingFederatePasswordOption.EnvVar)) + "--pingfederate-username", os.Getenv(options.PlatformExportPingfederateUsernameOption.EnvVar), + "--pingfederate-password", os.Getenv(options.PlatformExportPingfederatePasswordOption.EnvVar)) testutils.CheckExpectedError(t, err, nil) } @@ -297,11 +321,11 @@ func TestPlatformExportCmd_PingFederateTrustAllTLSFlag(t *testing.T) { err := testutils_cobra.ExecutePingctl(t, "platform", "export", "--output-directory", outputDir, - "--overwrite", - "--service", "pingfederate", + "--overwrite", "true", + "--services", "pingfederate", "--pingfederate-insecure-trust-all-tls=true", - "--pingfederate-username", os.Getenv(profiles.PingFederateUsernameOption.EnvVar), - "--pingfederate-password", os.Getenv(profiles.PingFederatePasswordOption.EnvVar)) + "--pingfederate-username", os.Getenv(options.PlatformExportPingfederateUsernameOption.EnvVar), + "--pingfederate-password", os.Getenv(options.PlatformExportPingfederatePasswordOption.EnvVar)) testutils.CheckExpectedError(t, err, nil) } @@ -312,11 +336,11 @@ func TestPlatformExportCmd_PingFederateTrustAllTLSFlagFalse(t *testing.T) { expectedErrorPattern := `^failed to export 'pingfederate' service: failed to export resource .*. err: .* Request for resource '.*' was not successful. Response is nil. Error: Get "https.*": tls: failed to verify certificate: x509: certificate signed by unknown authority$` err := testutils_cobra.ExecutePingctl(t, "platform", "export", "--output-directory", outputDir, - "--overwrite", - "--service", "pingfederate", + "--overwrite", "true", + "--services", "pingfederate", "--pingfederate-insecure-trust-all-tls=false", - "--pingfederate-username", os.Getenv(profiles.PingFederateUsernameOption.EnvVar), - "--pingfederate-password", os.Getenv(profiles.PingFederatePasswordOption.EnvVar)) + "--pingfederate-username", os.Getenv(options.PlatformExportPingfederateUsernameOption.EnvVar), + "--pingfederate-password", os.Getenv(options.PlatformExportPingfederatePasswordOption.EnvVar)) testutils.CheckExpectedError(t, err, &expectedErrorPattern) } @@ -326,12 +350,12 @@ func TestPlatformExportCmd_PingFederateCaCertificatePemFiles(t *testing.T) { err := testutils_cobra.ExecutePingctl(t, "platform", "export", "--output-directory", outputDir, - "--overwrite", - "--service", "pingfederate", + "--overwrite", "true", + "--services", "pingfederate", "--pingfederate-insecure-trust-all-tls=false", "--pingfederate-ca-certificate-pem-files", "testdata/ssl-server-crt.pem", - "--pingfederate-username", os.Getenv(profiles.PingFederateUsernameOption.EnvVar), - "--pingfederate-password", os.Getenv(profiles.PingFederatePasswordOption.EnvVar)) + "--pingfederate-username", os.Getenv(options.PlatformExportPingfederateUsernameOption.EnvVar), + "--pingfederate-password", os.Getenv(options.PlatformExportPingfederatePasswordOption.EnvVar)) testutils.CheckExpectedError(t, err, nil) } @@ -339,9 +363,9 @@ func TestPlatformExportCmd_PingFederateCaCertificatePemFiles(t *testing.T) { func TestPlatformExportCmd_PingFederateCaCertificatePemFilesInvalid(t *testing.T) { expectedErrorPattern := `^failed to read CA certificate PEM file '.*': open .*: no such file or directory$` err := testutils_cobra.ExecutePingctl(t, "platform", "export", - "--service", "pingfederate", + "--services", "pingfederate", "--pingfederate-ca-certificate-pem-files", "invalid/crt.pem", - "--pingfederate-username", os.Getenv(profiles.PingFederateUsernameOption.EnvVar), - "--pingfederate-password", os.Getenv(profiles.PingFederatePasswordOption.EnvVar)) + "--pingfederate-username", os.Getenv(options.PlatformExportPingfederateUsernameOption.EnvVar), + "--pingfederate-password", os.Getenv(options.PlatformExportPingfederatePasswordOption.EnvVar)) testutils.CheckExpectedError(t, err, &expectedErrorPattern) } diff --git a/cmd/root.go b/cmd/root.go index 1f2b36d..b695a1f 100644 --- a/cmd/root.go +++ b/cmd/root.go @@ -4,12 +4,12 @@ import ( "fmt" "os" "path/filepath" - "strings" "github.com/pingidentity/pingctl/cmd/config" "github.com/pingidentity/pingctl/cmd/feedback" "github.com/pingidentity/pingctl/cmd/platform" - "github.com/pingidentity/pingctl/internal/customtypes" + "github.com/pingidentity/pingctl/internal/configuration" + "github.com/pingidentity/pingctl/internal/configuration/options" "github.com/pingidentity/pingctl/internal/logger" "github.com/pingidentity/pingctl/internal/output" "github.com/pingidentity/pingctl/internal/profiles" @@ -17,37 +17,13 @@ import ( "github.com/spf13/viper" ) -const ( - defaultProfileName string = "default" -) - -var ( - cfgFile string - defaultCfgFile string - profileName string - - // Custom pflag.Value types - outputFormat customtypes.OutputFormat = customtypes.OutputFormat(customtypes.ENUM_OUTPUT_FORMAT_TEXT) -) - func init() { l := logger.Get() - l.Debug().Msgf("Initializing Root command...") - - // Determine the default configuration file location - home, err := os.UserHomeDir() - if err != nil { - output.Print(output.Opts{ - Message: "Failed to determine user's home directory", - Result: output.ENUM_RESULT_FAILURE, - FatalMessage: err.Error(), - }) - } - - // Default the config in $home/.pingctl directory with name "config.yaml". - defaultCfgFile = fmt.Sprintf("%s/.pingctl/config.yaml", home) + l.Debug().Msgf("Initializing Pingctl options...") + configuration.InitAllOptions() + l.Debug().Msgf("Initializing Root command...") cobra.OnInitialize(initViperProfile) } @@ -68,25 +44,10 @@ func NewRootCommand() *cobra.Command { platform.NewPlatformCommand(), ) - // flags used within this file assigned to variables - cmd.PersistentFlags().StringVarP(&cfgFile, "config", "C", "", "Configuration file location (default \"$HOME/.pingctl/config.yaml\")") - cmd.PersistentFlags().StringVarP(&profileName, profiles.ProfileOption.CobraParamName, "P", "", "Profile to use from configuration file") - - // custom pflag.Value types use Var() method - cmd.PersistentFlags().VarP(&outputFormat, profiles.OutputOption.CobraParamName, "O", fmt.Sprintf("Specifies output format. Valid formats: %s", strings.Join(customtypes.OutputFormatValidValues(), ", "))) - profiles.AddPFlagBinding(profiles.Binding{ - Option: profiles.OutputOption, - Flag: cmd.PersistentFlags().Lookup(profiles.OutputOption.CobraParamName), - }) - profiles.AddEnvVarBinding(profiles.OutputOption) - - // flags where values are stored and accessed via viper - cmd.PersistentFlags().Bool(profiles.ColorOption.CobraParamName, true, "Use colorized output") - profiles.AddPFlagBinding(profiles.Binding{ - Option: profiles.ColorOption, - Flag: cmd.PersistentFlags().Lookup(profiles.ColorOption.CobraParamName), - }) - profiles.AddEnvVarBinding(profiles.ColorOption) + cmd.PersistentFlags().AddFlag(options.RootConfigOption.Flag) + cmd.PersistentFlags().AddFlag(options.RootActiveProfileOption.Flag) + cmd.PersistentFlags().AddFlag(options.RootOutputFormatOption.Flag) + cmd.PersistentFlags().AddFlag(options.RootColorOption.Flag) // Make sure cobra is outputting to stdout and stderr cmd.SetOut(os.Stdout) @@ -98,31 +59,38 @@ func NewRootCommand() *cobra.Command { func initViperProfile() { l := logger.Get() + cfgFile, err := profiles.GetOptionValue(options.RootConfigOption) + if err != nil { + output.Print(output.Opts{ + Message: "Failed to get configuration file location", + Result: output.ENUM_RESULT_FAILURE, + FatalMessage: err.Error(), + }) + } + + l.Debug().Msgf("Using configuration file location for initialization: %s", cfgFile) + // Handle the config file location - checkCfgFileLocation() + checkCfgFileLocation(cfgFile) + + l.Debug().Msgf("Validated configuration file location at: %s", cfgFile) //Configure the main viper instance - initMainViper() - - // Prefer parameter, then environment variable, then configuration file - // like with viper hierarchy. Finally default to @defaultProfileName if not found - // NOTE: this is needed because parameter and env var are not bound to - // the main viper instance - if profileName == "" { - profileName = os.Getenv(profiles.ProfileOption.EnvVar) - } - if profileName == "" { - mainViper := profiles.GetMainViper() - profileName = mainViper.GetString(profiles.ProfileOption.ViperKey) - } - if profileName == "" { - profileName = defaultProfileName + initMainViper(cfgFile) + + profileName, err := profiles.GetOptionValue(options.RootActiveProfileOption) + if err != nil { + output.Print(output.Opts{ + Message: "Failed to get active profile", + Result: output.ENUM_RESULT_FAILURE, + FatalMessage: err.Error(), + }) } l.Debug().Msgf("Using configuration profile: %s", profileName) // Configure the profile viper instance - if err := profiles.SetProfileViperWithProfile(profileName); err != nil { + if err := profiles.GetMainConfig().ChangeActiveProfile(profileName); err != nil { output.Print(output.Opts{ Message: "Failed to set profile viper", Result: output.ENUM_RESULT_FAILURE, @@ -130,36 +98,28 @@ func initViperProfile() { }) } - // All bindings have been set by NewXCommand() functions previous to OnInitialize() - if err := profiles.ApplyBindingsToProfileViper(); err != nil { + // Validate the configuration + if err := profiles.Validate(); err != nil { output.Print(output.Opts{ - Message: "Failed to apply bindings to profile viper", + Message: "Failed to validate pingctl configuration", Result: output.ENUM_RESULT_FAILURE, FatalMessage: err.Error(), }) } } -func checkCfgFileLocation() { - l := logger.Get() - - // If no configuration file location is specified, use the default configuration file location - if cfgFile == "" { - l.Debug().Msgf("No configuration file location specified. Using default configuration file location: %s", defaultCfgFile) - cfgFile = defaultCfgFile - } - +func checkCfgFileLocation(cfgFile string) { // Check existence of configuration file _, err := os.Stat(cfgFile) if os.IsNotExist(err) { // Only create a new configuration file if it is the default configuration file location - if cfgFile == defaultCfgFile { + if cfgFile == options.RootConfigOption.DefaultValue.String() { output.Print(output.Opts{ Message: fmt.Sprintf("Pingctl configuration file '%s' does not exist.", cfgFile), Result: output.ENUM_RESULT_NOACTION_WARN, }) - createDefaultConfigFile() + createConfigFile(options.RootConfigOption.DefaultValue.String()) } else { output.Print(output.Opts{ Message: fmt.Sprintf("Configuration file '%s' does not exist.", cfgFile), @@ -177,7 +137,7 @@ func checkCfgFileLocation() { } -func createDefaultConfigFile() { +func createConfigFile(cfgFile string) { l := logger.Get() l.Debug().Msgf("Creating new pingctl configuration file at: %s", cfgFile) @@ -192,11 +152,10 @@ func createDefaultConfigFile() { } tempViper := viper.New() - tempViper.Set(profiles.ProfileOption.ViperKey, defaultProfileName) + tempViper.Set(options.RootActiveProfileOption.ViperKey, options.RootActiveProfileOption.DefaultValue) + tempViper.Set(fmt.Sprintf("%s.%v", options.RootActiveProfileOption.DefaultValue.String(), options.ProfileDescriptionOption.ViperKey), "Default profile created by pingctl") - // SafeWriteConfigAs writes current configuration to a given filename if it does not exist. - // Use global viper instance as main viper instance is not yet configured. - err = tempViper.SafeWriteConfigAs(cfgFile) + err = tempViper.WriteConfigAs(cfgFile) if err != nil { output.Print(output.Opts{ Message: fmt.Sprintf("Failed to create configuration file at: %s", cfgFile), @@ -206,10 +165,23 @@ func createDefaultConfigFile() { } } -func initMainViper() { +func initMainViper(cfgFile string) { + l := logger.Get() + + loadMainViperConfig(cfgFile) + + // If there are no profiles in the configuration file, seed the default profile + if len(profiles.GetMainConfig().ProfileNames()) == 0 { + l.Debug().Msgf("No profiles found in configuration file. Creating default profile in configuration file '%s'", cfgFile) + createConfigFile(cfgFile) + loadMainViperConfig(cfgFile) + } +} + +func loadMainViperConfig(cfgFile string) { l := logger.Get() - mainViper := profiles.GetMainViper() + mainViper := profiles.GetMainConfig().ViperInstance() // Use config file from the flag. mainViper.SetConfigFile(cfgFile) @@ -223,29 +195,4 @@ func initMainViper() { } else { l.Info().Msgf("Using configuration file: %s", mainViper.ConfigFileUsed()) } - - // If there are no profiles in the configuration file, seed the default profile - if len(profiles.ConfigProfileNames()) == 0 { - output.Print(output.Opts{ - Message: fmt.Sprintf("No profiles found in configuration file: %s. Creating 'default' profile.", mainViper.ConfigFileUsed()), - Result: output.ENUM_RESULT_NOACTION_WARN, - }) - - if err := profiles.CreateNewProfile(defaultProfileName, "Default profile created by pingctl", true); err != nil { - output.Print(output.Opts{ - Message: "Failed to create default profile", - Result: output.ENUM_RESULT_FAILURE, - FatalMessage: err.Error(), - }) - } - } - - // Validate the configuration - if err := profiles.Validate(); err != nil { - output.Print(output.Opts{ - Message: "Failed to validate pingctl configuration", - Result: output.ENUM_RESULT_FAILURE, - FatalMessage: err.Error(), - }) - } } diff --git a/internal/commands/config/add_profile_internal.go b/internal/commands/config/add_profile_internal.go new file mode 100644 index 0000000..7710dbc --- /dev/null +++ b/internal/commands/config/add_profile_internal.go @@ -0,0 +1,116 @@ +package config_internal + +import ( + "fmt" + "io" + "strconv" + + "github.com/pingidentity/pingctl/internal/configuration/options" + "github.com/pingidentity/pingctl/internal/input" + "github.com/pingidentity/pingctl/internal/output" + "github.com/pingidentity/pingctl/internal/profiles" + "github.com/spf13/viper" +) + +func RunInternalConfigAddProfile(rc io.ReadCloser) (err error) { + newProfileName, newDescription, setActive, err := readConfigAddProfileOptions(rc) + if err != nil { + return fmt.Errorf("failed to add profile: %v", err) + } + + err = profiles.GetMainConfig().ValidateNewProfileName(newProfileName) + if err != nil { + return fmt.Errorf("failed to add profile: %v", err) + } + + output.Print(output.Opts{ + Message: fmt.Sprintf("Adding new profile '%s'...", newProfileName), + Result: output.ENUM_RESULT_NIL, + }) + + subViper := viper.New() + subViper.Set(options.ProfileDescriptionOption.ViperKey, newDescription) + + if err = profiles.GetMainConfig().SaveProfile(newProfileName, subViper); err != nil { + return fmt.Errorf("failed to add profile: %v", err) + } + + output.Print(output.Opts{ + Message: fmt.Sprintf("Profile created. Update additional profile attributes via 'pingctl config set' or directly within the config file at '%s'", profiles.GetMainConfig().ViperInstance().ConfigFileUsed()), + Result: output.ENUM_RESULT_SUCCESS, + }) + + if setActive { + if err = profiles.GetMainConfig().ChangeActiveProfile(newProfileName); err != nil { + return fmt.Errorf("failed to set active profile: %v", err) + } + + output.Print(output.Opts{ + Message: fmt.Sprintf("Profile '%s' set as active.", newProfileName), + Result: output.ENUM_RESULT_SUCCESS, + }) + } + + return nil +} + +func readConfigAddProfileOptions(rc io.ReadCloser) (newProfileName, newDescription string, setActive bool, err error) { + if newProfileName, err = readConfigAddProfileNameOption(rc); err != nil { + return newProfileName, newDescription, setActive, err + } + + if newDescription, err = readConfigAddProfileDescriptionOption(rc); err != nil { + return newProfileName, newDescription, setActive, err + } + + if setActive, err = readConfigAddProfileSetActiveOption(rc); err != nil { + return newProfileName, newDescription, setActive, err + } + + return newProfileName, newDescription, setActive, nil +} + +func readConfigAddProfileNameOption(rc io.ReadCloser) (newProfileName string, err error) { + if !options.ConfigAddProfileNameOption.Flag.Changed { + newProfileName, err = input.RunPrompt("New profile name: ", profiles.GetMainConfig().ValidateNewProfileName, rc) + if err != nil { + return newProfileName, err + } + + if newProfileName == "" { + return newProfileName, fmt.Errorf("unable to determine profile name") + } + } else { + newProfileName, err = profiles.GetOptionValue(options.ConfigAddProfileNameOption) + if err != nil { + return newProfileName, err + } + + if newProfileName == "" { + return newProfileName, fmt.Errorf("unable to determine profile name") + } + } + + return newProfileName, nil +} + +func readConfigAddProfileDescriptionOption(rc io.ReadCloser) (newDescription string, err error) { + if !options.ConfigAddProfileDescriptionOption.Flag.Changed { + return input.RunPrompt("New profile description: ", nil, rc) + } else { + return profiles.GetOptionValue(options.ConfigAddProfileDescriptionOption) + } +} + +func readConfigAddProfileSetActiveOption(rc io.ReadCloser) (setActive bool, err error) { + if !options.ConfigAddProfileSetActiveOption.Flag.Changed { + return input.RunPromptConfirm("Set new profile as active: ", rc) + } else { + boolStr, err := profiles.GetOptionValue(options.ConfigAddProfileSetActiveOption) + if err != nil { + return setActive, err + } + + return strconv.ParseBool(boolStr) + } +} diff --git a/internal/commands/config/add_profile_internal_test.go b/internal/commands/config/add_profile_internal_test.go new file mode 100644 index 0000000..9a24f0e --- /dev/null +++ b/internal/commands/config/add_profile_internal_test.go @@ -0,0 +1,133 @@ +package config_internal + +import ( + "os" + "testing" + + "github.com/pingidentity/pingctl/internal/configuration/options" + "github.com/pingidentity/pingctl/internal/customtypes" + "github.com/pingidentity/pingctl/internal/testing/testutils" + "github.com/pingidentity/pingctl/internal/testing/testutils_viper" +) + +// Test RunInternalConfigAddProfile function +func Test_RunInternalConfigAddProfile(t *testing.T) { + testutils_viper.InitVipers(t) + + var ( + profileName = customtypes.String("test-profile") + description = customtypes.String("test-description") + setActive = customtypes.Bool(false) + ) + + options.ConfigAddProfileNameOption.Flag.Changed = true + options.ConfigAddProfileNameOption.CobraParamValue = &profileName + + options.ConfigAddProfileDescriptionOption.Flag.Changed = true + options.ConfigAddProfileDescriptionOption.CobraParamValue = &description + + options.ConfigAddProfileSetActiveOption.Flag.Changed = true + options.ConfigAddProfileSetActiveOption.CobraParamValue = &setActive + + err := RunInternalConfigAddProfile(os.Stdin) + if err != nil { + t.Errorf("RunInternalConfigAddProfile returned error: %v", err) + } +} + +// Test RunInternalConfigAddProfile function fails when existing profile name is provided +func Test_RunInternalConfigAddProfile_ExistingProfileName(t *testing.T) { + testutils_viper.InitVipers(t) + + var ( + profileName = customtypes.String("default") + description = customtypes.String("test-description") + setActive = customtypes.Bool(false) + ) + + options.ConfigAddProfileNameOption.Flag.Changed = true + options.ConfigAddProfileNameOption.CobraParamValue = &profileName + + options.ConfigAddProfileDescriptionOption.Flag.Changed = true + options.ConfigAddProfileDescriptionOption.CobraParamValue = &description + + options.ConfigAddProfileSetActiveOption.Flag.Changed = true + options.ConfigAddProfileSetActiveOption.CobraParamValue = &setActive + + expectedErrorPattern := `^failed to add profile: invalid profile name: '.*'. profile already exists$` + err := RunInternalConfigAddProfile(os.Stdin) + testutils.CheckExpectedError(t, err, &expectedErrorPattern) +} + +// Test RunInternalConfigAddProfile function fails when profile name is not provided +func Test_RunInternalConfigAddProfile_NoProfileName(t *testing.T) { + testutils_viper.InitVipers(t) + + var ( + profileName = customtypes.String("") + description = customtypes.String("test-description") + setActive = customtypes.Bool(false) + ) + + options.ConfigAddProfileNameOption.Flag.Changed = true + options.ConfigAddProfileNameOption.CobraParamValue = &profileName + + options.ConfigAddProfileDescriptionOption.Flag.Changed = true + options.ConfigAddProfileDescriptionOption.CobraParamValue = &description + + options.ConfigAddProfileSetActiveOption.Flag.Changed = true + options.ConfigAddProfileSetActiveOption.CobraParamValue = &setActive + + expectedErrorPattern := `^failed to add profile: unable to determine profile name$` + err := RunInternalConfigAddProfile(os.Stdin) + testutils.CheckExpectedError(t, err, &expectedErrorPattern) +} + +// Test RunInternalConfigAddProfile function succeeds with set active flag set to true +func Test_RunInternalConfigAddProfile_SetActive(t *testing.T) { + testutils_viper.InitVipers(t) + + var ( + profileName = customtypes.String("test-profile") + description = customtypes.String("test-description") + setActive = customtypes.Bool(true) + ) + + options.ConfigAddProfileNameOption.Flag.Changed = true + options.ConfigAddProfileNameOption.CobraParamValue = &profileName + + options.ConfigAddProfileDescriptionOption.Flag.Changed = true + options.ConfigAddProfileDescriptionOption.CobraParamValue = &description + + options.ConfigAddProfileSetActiveOption.Flag.Changed = true + options.ConfigAddProfileSetActiveOption.CobraParamValue = &setActive + + err := RunInternalConfigAddProfile(os.Stdin) + if err != nil { + t.Errorf("RunInternalConfigAddProfile returned error: %v", err) + } +} + +// Test RunInternalConfigAddProfile function fails with invalid set active flag +func Test_RunInternalConfigAddProfile_InvalidSetActive(t *testing.T) { + testutils_viper.InitVipers(t) + + var ( + profileName = customtypes.String("test-profile") + description = customtypes.String("test-description") + setActive = customtypes.String("invalid") + ) + + options.ConfigAddProfileNameOption.Flag.Changed = true + options.ConfigAddProfileNameOption.CobraParamValue = &profileName + + options.ConfigAddProfileDescriptionOption.Flag.Changed = true + options.ConfigAddProfileDescriptionOption.CobraParamValue = &description + + options.ConfigAddProfileSetActiveOption.Flag.Changed = true + options.ConfigAddProfileSetActiveOption.CobraParamValue = &setActive + + expectedErrorPattern := `^failed to add profile: strconv.ParseBool: parsing ".*": invalid syntax$` + err := RunInternalConfigAddProfile(os.Stdin) + testutils.CheckExpectedError(t, err, &expectedErrorPattern) +} diff --git a/internal/commands/config/config_internal.go b/internal/commands/config/config_internal.go new file mode 100644 index 0000000..294017a --- /dev/null +++ b/internal/commands/config/config_internal.go @@ -0,0 +1,111 @@ +package config_internal + +import ( + "fmt" + "io" + + "github.com/pingidentity/pingctl/internal/configuration/options" + "github.com/pingidentity/pingctl/internal/input" + "github.com/pingidentity/pingctl/internal/output" + "github.com/pingidentity/pingctl/internal/profiles" +) + +func RunInternalConfig(rc io.ReadCloser) (err error) { + profileName, newProfileName, newDescription, err := readConfigOptions(rc) + if err != nil { + return fmt.Errorf("failed to update profile. %v", err) + } + + output.Print(output.Opts{ + Message: fmt.Sprintf("Updating profile '%s'...", profileName), + Result: output.ENUM_RESULT_NIL, + }) + + if err = profiles.GetMainConfig().ChangeProfileName(profileName, newProfileName); err != nil { + return fmt.Errorf("failed to update profile '%s' name to: %s. %v", profileName, newProfileName, err) + } + + if err = profiles.GetMainConfig().ChangeProfileDescription(newProfileName, newDescription); err != nil { + return fmt.Errorf("failed to update profile '%s' description to: %s. %v", newProfileName, newDescription, err) + } + + output.Print(output.Opts{ + Message: fmt.Sprintf("Profile updated. Update additional profile attributes via 'pingctl config set' or directly within the config file at '%s'", profiles.GetMainConfig().ViperInstance().ConfigFileUsed()), + Result: output.ENUM_RESULT_SUCCESS, + }) + + return nil +} + +func readConfigOptions(rc io.ReadCloser) (profileName, newName, description string, err error) { + if profileName, err = readConfigProfileNameOption(); err != nil { + return profileName, newName, description, err + } + + if newName, err = readConfigNameOption(rc); err != nil { + return profileName, newName, description, err + } + + if description, err = readConfigDescriptionOption(rc); err != nil { + return profileName, newName, description, err + } + + return profileName, newName, description, nil +} + +func readConfigProfileNameOption() (pName string, err error) { + if !options.ConfigProfileOption.Flag.Changed { + pName, err = profiles.GetOptionValue(options.RootActiveProfileOption) + } else { + pName, err = profiles.GetOptionValue(options.ConfigProfileOption) + } + + if err != nil { + return pName, err + } + + if pName == "" { + return pName, fmt.Errorf("unable to determine profile name to update") + } + + return pName, nil +} + +func readConfigNameOption(rc io.ReadCloser) (newName string, err error) { + if !options.ConfigNameOption.Flag.Changed { + newName, err = input.RunPrompt("New profile name: ", validateChangeProfileName, rc) + } else { + newName, err = profiles.GetOptionValue(options.ConfigNameOption) + } + + if err != nil { + return newName, err + } + + if newName == "" { + return newName, fmt.Errorf("unable to determine new profile name") + } + + return newName, nil +} + +func readConfigDescriptionOption(rc io.ReadCloser) (description string, err error) { + if !options.ConfigDescriptionOption.Flag.Changed { + return input.RunPrompt("New profile description: ", nil, rc) + } else { + return profiles.GetOptionValue(options.ConfigDescriptionOption) + } +} + +func validateChangeProfileName(newName string) (err error) { + oldName, err := readConfigProfileNameOption() + if err != nil { + return err + } + + if err = profiles.GetMainConfig().ValidateUpdateExistingProfileName(oldName, newName); err != nil { + return err + } + + return nil +} diff --git a/internal/commands/config/config_internal_test.go b/internal/commands/config/config_internal_test.go new file mode 100644 index 0000000..771c620 --- /dev/null +++ b/internal/commands/config/config_internal_test.go @@ -0,0 +1,132 @@ +package config_internal + +import ( + "os" + "testing" + + "github.com/pingidentity/pingctl/internal/configuration/options" + "github.com/pingidentity/pingctl/internal/customtypes" + "github.com/pingidentity/pingctl/internal/testing/testutils" + "github.com/pingidentity/pingctl/internal/testing/testutils_viper" +) + +// Test RunInternalConfig function +func Test_RunInternalConfig(t *testing.T) { + testutils_viper.InitVipers(t) + + var ( + oldProfile = customtypes.String("production") + profileName = customtypes.String("test-profile") + description = customtypes.String("test-description") + ) + + options.ConfigProfileOption.Flag.Changed = true + options.ConfigProfileOption.CobraParamValue = &oldProfile + + options.ConfigNameOption.Flag.Changed = true + options.ConfigNameOption.CobraParamValue = &profileName + + options.ConfigDescriptionOption.Flag.Changed = true + options.ConfigDescriptionOption.CobraParamValue = &description + + err := RunInternalConfig(os.Stdin) + if err != nil { + t.Errorf("RunInternalConfig returned error: %v", err) + } +} + +// Test RunInternalConfig function fails when existing profile name is provided +func Test_RunInternalConfig_ExistingProfileName(t *testing.T) { + testutils_viper.InitVipers(t) + + var ( + oldProfile = customtypes.String("production") + profileName = customtypes.String("default") + description = customtypes.String("test-description") + ) + + options.ConfigProfileOption.Flag.Changed = true + options.ConfigProfileOption.CobraParamValue = &oldProfile + + options.ConfigNameOption.Flag.Changed = true + options.ConfigNameOption.CobraParamValue = &profileName + + options.ConfigDescriptionOption.Flag.Changed = true + options.ConfigDescriptionOption.CobraParamValue = &description + + expectedErrorPattern := `^failed to update profile '.*' name to: .*\. invalid profile name: '.*'\. profile already exists$` + err := RunInternalConfig(os.Stdin) + testutils.CheckExpectedError(t, err, &expectedErrorPattern) +} + +// Test RunInternalConfig function fails when invalid profile name is provided +func Test_RunInternalConfig_InvalidProfileName(t *testing.T) { + testutils_viper.InitVipers(t) + + var ( + oldProfile = customtypes.String("production") + profileName = customtypes.String("test-profile!") + description = customtypes.String("test-description") + ) + + options.ConfigProfileOption.Flag.Changed = true + options.ConfigProfileOption.CobraParamValue = &oldProfile + + options.ConfigNameOption.Flag.Changed = true + options.ConfigNameOption.CobraParamValue = &profileName + + options.ConfigDescriptionOption.Flag.Changed = true + options.ConfigDescriptionOption.CobraParamValue = &description + + expectedErrorPattern := `^failed to update profile '.*' name to: .*\. invalid profile name: '.*'\. name must contain only alphanumeric characters, underscores, and dashes$` + err := RunInternalConfig(os.Stdin) + testutils.CheckExpectedError(t, err, &expectedErrorPattern) +} + +// Test RunInternalConfig function fails when profile name is not provided +func Test_RunInternalConfig_NoProfileName(t *testing.T) { + testutils_viper.InitVipers(t) + + var ( + oldProfile = customtypes.String("production") + profileName = customtypes.String("") + description = customtypes.String("test-description") + ) + + options.ConfigProfileOption.Flag.Changed = true + options.ConfigProfileOption.CobraParamValue = &oldProfile + + options.ConfigNameOption.Flag.Changed = true + options.ConfigNameOption.CobraParamValue = &profileName + + options.ConfigDescriptionOption.Flag.Changed = true + options.ConfigDescriptionOption.CobraParamValue = &description + + expectedErrorPattern := `^failed to update profile\. unable to determine new profile name$` + err := RunInternalConfig(os.Stdin) + testutils.CheckExpectedError(t, err, &expectedErrorPattern) +} + +// Test RunInternalConfig function fails when provided active profile name +func Test_RunInternalConfig_ActiveProfileName(t *testing.T) { + testutils_viper.InitVipers(t) + + var ( + oldProfile = customtypes.String("default") + profileName = customtypes.String("test-profile") + description = customtypes.String("test-description") + ) + + options.ConfigProfileOption.Flag.Changed = true + options.ConfigProfileOption.CobraParamValue = &oldProfile + + options.ConfigNameOption.Flag.Changed = true + options.ConfigNameOption.CobraParamValue = &profileName + + options.ConfigDescriptionOption.Flag.Changed = true + options.ConfigDescriptionOption.CobraParamValue = &description + + expectedErrorPattern := `^failed to update profile '.*' name to: .*\. '.*' is the active profile and cannot be deleted$` + err := RunInternalConfig(os.Stdin) + testutils.CheckExpectedError(t, err, &expectedErrorPattern) +} diff --git a/internal/commands/config/delete_profile_internal.go b/internal/commands/config/delete_profile_internal.go new file mode 100644 index 0000000..6d8d578 --- /dev/null +++ b/internal/commands/config/delete_profile_internal.go @@ -0,0 +1,52 @@ +package config_internal + +import ( + "fmt" + "io" + + "github.com/pingidentity/pingctl/internal/configuration/options" + "github.com/pingidentity/pingctl/internal/input" + "github.com/pingidentity/pingctl/internal/output" + "github.com/pingidentity/pingctl/internal/profiles" +) + +func RunInternalConfigDeleteProfile(rc io.ReadCloser) (err error) { + pName, err := readConfigDeleteProfileOptions(rc) + if err != nil { + return fmt.Errorf("failed to delete profile: %v", err) + } + + output.Print(output.Opts{ + Message: fmt.Sprintf("Deleting profile '%s'...", pName), + Result: output.ENUM_RESULT_NIL, + }) + + if err := profiles.GetMainConfig().DeleteProfile(pName); err != nil { + return fmt.Errorf("failed to delete profile: %v", err) + } + + output.Print(output.Opts{ + Message: fmt.Sprintf("Profile '%s' deleted.", pName), + Result: output.ENUM_RESULT_SUCCESS, + }) + + return nil +} + +func readConfigDeleteProfileOptions(rc io.ReadCloser) (pName string, err error) { + if !options.ConfigDeleteProfileOption.Flag.Changed { + pName, err = input.RunPromptSelect("Select profile to delete: ", profiles.GetMainConfig().ProfileNames(), rc) + } else { + pName, err = profiles.GetOptionValue(options.ConfigDeleteProfileOption) + } + + if err != nil { + return pName, err + } + + if pName == "" { + return pName, fmt.Errorf("unable to determine profile name to delete") + } + + return pName, nil +} diff --git a/internal/commands/config/delete_profile_internal_test.go b/internal/commands/config/delete_profile_internal_test.go new file mode 100644 index 0000000..47cb645 --- /dev/null +++ b/internal/commands/config/delete_profile_internal_test.go @@ -0,0 +1,92 @@ +package config_internal + +import ( + "os" + "testing" + + "github.com/pingidentity/pingctl/internal/configuration/options" + "github.com/pingidentity/pingctl/internal/customtypes" + "github.com/pingidentity/pingctl/internal/testing/testutils" + "github.com/pingidentity/pingctl/internal/testing/testutils_viper" +) + +// Test RunInternalConfigDeleteProfile function +func Test_RunInternalConfigDeleteProfile(t *testing.T) { + testutils_viper.InitVipers(t) + + var ( + profileName = customtypes.String("production") + ) + + options.ConfigDeleteProfileOption.Flag.Changed = true + options.ConfigDeleteProfileOption.CobraParamValue = &profileName + + err := RunInternalConfigDeleteProfile(os.Stdin) + if err != nil { + t.Errorf("RunInternalConfigDeleteProfile returned error: %v", err) + } +} + +// Test RunInternalConfigDeleteProfile function fails with active profile +func Test_RunInternalConfigDeleteProfile_ActiveProfile(t *testing.T) { + testutils_viper.InitVipers(t) + + var ( + profileName = customtypes.String("default") + ) + + options.ConfigDeleteProfileOption.Flag.Changed = true + options.ConfigDeleteProfileOption.CobraParamValue = &profileName + + expectedErrorPattern := `^failed to delete profile: '.*' is the active profile and cannot be deleted$` + err := RunInternalConfigDeleteProfile(os.Stdin) + testutils.CheckExpectedError(t, err, &expectedErrorPattern) +} + +// Test RunInternalConfigDeleteProfile function fails with invalid profile name +func Test_RunInternalConfigDeleteProfile_InvalidProfileName(t *testing.T) { + testutils_viper.InitVipers(t) + + var ( + profileName = customtypes.String("(*#&)") + ) + + options.ConfigDeleteProfileOption.Flag.Changed = true + options.ConfigDeleteProfileOption.CobraParamValue = &profileName + + expectedErrorPattern := `^failed to delete profile: invalid profile name: '.*'\. name must contain only alphanumeric characters, underscores, and dashes$` + err := RunInternalConfigDeleteProfile(os.Stdin) + testutils.CheckExpectedError(t, err, &expectedErrorPattern) +} + +// Test RunInternalConfigDeleteProfile function fails with empty profile name +func Test_RunInternalConfigDeleteProfile_EmptyProfileName(t *testing.T) { + testutils_viper.InitVipers(t) + + var ( + profileName = customtypes.String("") + ) + + options.ConfigDeleteProfileOption.Flag.Changed = true + options.ConfigDeleteProfileOption.CobraParamValue = &profileName + + expectedErrorPattern := `^failed to delete profile: unable to determine profile name to delete$` + err := RunInternalConfigDeleteProfile(os.Stdin) + testutils.CheckExpectedError(t, err, &expectedErrorPattern) +} + +// Test RunInternalConfigDeleteProfile function fails with non-existent profile name +func Test_RunInternalConfigDeleteProfile_NonExistentProfileName(t *testing.T) { + testutils_viper.InitVipers(t) + + var ( + profileName = customtypes.String("non-existent") + ) + + options.ConfigDeleteProfileOption.Flag.Changed = true + options.ConfigDeleteProfileOption.CobraParamValue = &profileName + + expectedErrorPattern := `^failed to delete profile: invalid profile name: '.*' profile does not exist$` + err := RunInternalConfigDeleteProfile(os.Stdin) + testutils.CheckExpectedError(t, err, &expectedErrorPattern) +} diff --git a/internal/commands/config/get_internal.go b/internal/commands/config/get_internal.go index bfbd130..73f544a 100644 --- a/internal/commands/config/get_internal.go +++ b/internal/commands/config/get_internal.go @@ -2,80 +2,50 @@ package config_internal import ( "fmt" - "slices" - "strings" + "github.com/pingidentity/pingctl/internal/configuration" + "github.com/pingidentity/pingctl/internal/configuration/options" "github.com/pingidentity/pingctl/internal/output" "github.com/pingidentity/pingctl/internal/profiles" - "gopkg.in/yaml.v3" ) -func RunInternalConfigGet(viperKey string) error { - // Write the profile configuration to file, - // even though no configuration change is happening here - // This handles the edge case where the config.yaml file was generated for - // the first time, but no configuration changes were made and parameters/env vars were used - if err := profiles.SaveProfileViperToFile(); err != nil { - return err +func RunInternalConfigGet(viperKey string) (err error) { + if err = configuration.ValidateParentViperKey(viperKey); err != nil { + return fmt.Errorf("failed to get configuration: %v", err) } - if viperKey == "" { - if err := PrintConfig(); err != nil { - return err - } - return nil - } - - // The only valid configuration keys are those defined in profiles/types.go, - // and their parent keys - validKeys := profiles.ExpandedProfileKeys() - if !slices.ContainsFunc(validKeys, func(v string) bool { - return strings.EqualFold(v, viperKey) - }) { - validKeyStr := strings.Join(validKeys, ", ") - return fmt.Errorf("unable to get configuration: value '%s' is not recognized as a valid configuration key. Valid keys: %s", viperKey, validKeyStr) - } - - // Check if the viper configuration key is set - if !profiles.GetProfileViper().IsSet(viperKey) { - output.Print(output.Opts{ - Result: output.ENUM_RESULT_NOACTION_WARN, - Message: fmt.Sprintf("Configuration key '%s' is not set", viperKey), - }) - return nil - } - - if err := printConfigFromKey(viperKey); err != nil { - return err + pName, err := readConfigGetOptions() + if err != nil { + return fmt.Errorf("failed to get configuration: %v", err) } - return nil -} - -func PrintConfig() error { - // Print the updated configuration - yaml, err := yaml.Marshal(profiles.GetProfileViper().AllSettings()) + yamlStr, err := profiles.GetMainConfig().ProfileViperValue(pName, viperKey) if err != nil { - return fmt.Errorf("failed to yaml marshal pingctl configuration: %s", err.Error()) + return fmt.Errorf("failed to get configuration: %v", err) } + output.Print(output.Opts{ + Message: yamlStr, Result: output.ENUM_RESULT_NIL, - Message: string(yaml), }) return nil } -func printConfigFromKey(viperKey string) error { - // Print the updated configuration - yaml, err := yaml.Marshal(profiles.GetProfileViper().Get(viperKey)) +func readConfigGetOptions() (pName string, err error) { + if !options.ConfigGetProfileOption.Flag.Changed { + pName, err = profiles.GetOptionValue(options.RootActiveProfileOption) + } else { + pName, err = profiles.GetOptionValue(options.ConfigGetProfileOption) + } + if err != nil { - return fmt.Errorf("failed to yaml marshal viper configuration: %s", err.Error()) + return pName, err } - output.Print(output.Opts{ - Result: output.ENUM_RESULT_NIL, - Message: string(yaml), - }) - return nil + if pName == "" { + return pName, fmt.Errorf("unable to determine profile to get configuration from") + } + + return pName, nil } diff --git a/internal/commands/config/get_internal_test.go b/internal/commands/config/get_internal_test.go index 76f0bfa..82e2e09 100644 --- a/internal/commands/config/get_internal_test.go +++ b/internal/commands/config/get_internal_test.go @@ -3,87 +3,60 @@ package config_internal import ( "testing" - "github.com/pingidentity/pingctl/internal/profiles" + "github.com/pingidentity/pingctl/internal/configuration/options" + "github.com/pingidentity/pingctl/internal/customtypes" "github.com/pingidentity/pingctl/internal/testing/testutils" "github.com/pingidentity/pingctl/internal/testing/testutils_viper" - "github.com/spf13/viper" ) // Test RunInternalConfigGet function -func Test_RunInternalConfigGet_NoArgs(t *testing.T) { +func Test_RunInternalConfigGet(t *testing.T) { testutils_viper.InitVipers(t) - err := RunInternalConfigGet("") - testutils.CheckExpectedError(t, err, nil) + err := RunInternalConfigGet("pingctl") + if err != nil { + t.Errorf("RunInternalConfigGet returned error: %v", err) + } } -// Test RunInternalConfigGet function with args that are set -func Test_RunInternalConfigGet_WithArgs(t *testing.T) { +// Test RunInternalConfigGet function fails with invalid key +func Test_RunInternalConfigGet_InvalidKey(t *testing.T) { testutils_viper.InitVipers(t) - err := RunInternalConfigGet(profiles.ColorOption.ViperKey) - testutils.CheckExpectedError(t, err, nil) + expectedErrorPattern := `^failed to get configuration: key '.*' is not recognized as a valid configuration key. Valid keys: .*$` + err := RunInternalConfigGet("invalid-key") + testutils.CheckExpectedError(t, err, &expectedErrorPattern) } -// Test RunInternalConfigGet function with args that are not set -func Test_RunInternalConfigGet_WithArgs_NotSet(t *testing.T) { +// Test RunInternalConfigGet function with different profile +func Test_RunInternalConfigGet_DifferentProfile(t *testing.T) { testutils_viper.InitVipers(t) - err := RunInternalConfigGet(profiles.PingOneWorkerClientIDOption.ViperKey) - testutils.CheckExpectedError(t, err, nil) -} + var ( + profileName = customtypes.String("production") + ) -// Test RunInternalConfigGet function with invalid key -func Test_RunInternalConfigGet_InvalidKey(t *testing.T) { - testutils_viper.InitVipers(t) + options.ConfigGetProfileOption.Flag.Changed = true + options.ConfigGetProfileOption.CobraParamValue = &profileName - expectedErrorPattern := `^unable to get configuration: value 'pingctl\.invalid' is not recognized as a valid configuration key\. Valid keys: [A-Za-z\.\s,]+$` - err := RunInternalConfigGet("pingctl.invalid") - testutils.CheckExpectedError(t, err, &expectedErrorPattern) + err := RunInternalConfigGet("pingctl") + if err != nil { + t.Errorf("RunInternalConfigGet returned error: %v", err) + } } -// Test PrintConfig() function -func Example_printConfig() { - // set viper configuration key-value for testing - profileViper := viper.New() - profileViper.Set(profiles.ColorOption.ViperKey, true) - profileViper.Set(profiles.OutputOption.ViperKey, "text") - profileViper.Set(profiles.PingOneRegionOption.ViperKey, "test-region") - profileViper.Set(profiles.PingOneWorkerClientIDOption.ViperKey, "test-client-id") - profileViper.Set(profiles.PingOneWorkerClientSecretOption.ViperKey, "test-client-secret") - profileViper.Set(profiles.PingOneWorkerEnvironmentIDOption.ViperKey, "test-environment-id") - profileViper.Set(profiles.PingOneExportEnvironmentIDOption.ViperKey, "test-export-environment-id") - profiles.SetProfileViperWithViper(profileViper, "testProfile") - - _ = PrintConfig() - - // Output: - // pingctl: - // color: true - // outputformat: text - // pingone: - // export: - // environmentid: test-export-environment-id - // region: test-region - // worker: - // clientid: test-client-id - // clientsecret: test-client-secret - // environmentid: test-environment-id -} +// Test RunInternalConfigGet function fails with invalid profile name +func Test_RunInternalConfigGet_InvalidProfileName(t *testing.T) { + testutils_viper.InitVipers(t) -// Test printConfigFromKey() function -func Example_printConfigFromKey() { - // set viper configuration key-value for testing - profileViper := viper.New() - profileViper.Set(profiles.PingOneRegionOption.ViperKey, "test-region") - profileViper.Set(profiles.OutputOption.ViperKey, "text") - profiles.SetProfileViperWithViper(profileViper, "testProfile") + var ( + profileName = customtypes.String("invalid") + ) - _ = printConfigFromKey(profiles.PingOneRegionOption.ViperKey) - _ = printConfigFromKey(profiles.OutputOption.ViperKey) + options.ConfigGetProfileOption.Flag.Changed = true + options.ConfigGetProfileOption.CobraParamValue = &profileName - // Output: - // test-region - // - // text + expectedErrorPattern := `^failed to get configuration: invalid profile name: '.*' profile does not exist$` + err := RunInternalConfigGet("pingctl") + testutils.CheckExpectedError(t, err, &expectedErrorPattern) } diff --git a/internal/commands/config/list_profiles_internal.go b/internal/commands/config/list_profiles_internal.go new file mode 100644 index 0000000..cb6c25e --- /dev/null +++ b/internal/commands/config/list_profiles_internal.go @@ -0,0 +1,25 @@ +package config_internal + +import ( + "github.com/pingidentity/pingctl/internal/output" + "github.com/pingidentity/pingctl/internal/profiles" +) + +func RunInternalConfigListProfiles() { + profileNames := profiles.GetMainConfig().ProfileNames() + activeProfile := profiles.GetMainConfig().ActiveProfile().Name() + + listStr := "Profiles:\n" + for _, profileName := range profileNames { + if profileName == activeProfile { + listStr += "- " + profileName + " (active)\n" + } else { + listStr += "- " + profileName + "\n" + } + } + + output.Print(output.Opts{ + Message: listStr, + Result: output.ENUM_RESULT_NIL, + }) +} diff --git a/internal/commands/config/list_profiles_internal_test.go b/internal/commands/config/list_profiles_internal_test.go new file mode 100644 index 0000000..6c85cc9 --- /dev/null +++ b/internal/commands/config/list_profiles_internal_test.go @@ -0,0 +1,14 @@ +package config_internal + +import ( + "testing" + + "github.com/pingidentity/pingctl/internal/testing/testutils_viper" +) + +// Test RunInternalConfigListProfiles function +func Test_RunInternalConfigListProfiles(t *testing.T) { + testutils_viper.InitVipers(t) + + RunInternalConfigListProfiles() +} diff --git a/internal/commands/config/profile/add_internal.go b/internal/commands/config/profile/add_internal.go deleted file mode 100644 index 7e65582..0000000 --- a/internal/commands/config/profile/add_internal.go +++ /dev/null @@ -1,45 +0,0 @@ -package profile_internal - -import ( - "fmt" - "io" - - "github.com/pingidentity/pingctl/internal/input" - "github.com/pingidentity/pingctl/internal/output" - "github.com/pingidentity/pingctl/internal/profiles" -) - -func RunInternalConfigProfileAdd(profileName, description string, setActive, setActiveChanged bool, r io.ReadCloser) (err error) { - if profileName == "" { - if profileName, err = input.RunPrompt("New Profile Name", profiles.ValidateNewProfileName, r); err != nil { - return err - } - } else { - if err = profiles.ValidateNewProfileName(profileName); err != nil { - return err - } - } - - if description == "" { - if description, err = input.RunPrompt("New Profile Description", nil, r); err != nil { - return err - } - } - - if !setActiveChanged { - if setActive, err = input.RunPromptConfirm("Set Profile as Active Profile", r); err != nil { - return err - } - } - - if err = profiles.CreateNewProfile(profileName, description, setActive); err != nil { - return err - } - - output.Print(output.Opts{ - Message: fmt.Sprintf("Profile '%s' added successfully", profileName), - Result: output.ENUM_RESULT_SUCCESS, - }) - - return nil -} diff --git a/internal/commands/config/profile/add_internal_test.go b/internal/commands/config/profile/add_internal_test.go deleted file mode 100644 index 85682b3..0000000 --- a/internal/commands/config/profile/add_internal_test.go +++ /dev/null @@ -1,191 +0,0 @@ -package profile_internal - -import ( - "fmt" - "regexp" - "testing" - - "github.com/manifoldco/promptui" - "github.com/pingidentity/pingctl/internal/profiles" - "github.com/pingidentity/pingctl/internal/testing/testutils" - "github.com/pingidentity/pingctl/internal/testing/testutils_viper" -) - -// Test RunInternalConfigProfileAdd function with no user input needed -func Test_RunInternalConfigProfileAdd_NoInput(t *testing.T) { - testutils_viper.InitVipers(t) - - newProfileName := "test-profile" - - err := RunInternalConfigProfileAdd(newProfileName, "test-description", false, true, nil) - testutils.CheckExpectedError(t, err, nil) - - // Check if the profile is created - if err := profiles.ValidateExistingProfileName(newProfileName); err != nil { - t.Fatalf("Expected profile '%s' to exist, got error: %v", newProfileName, err) - } -} - -// Test RunInternalConfigProfileAdd function with a valid name from user input -func Test_RunInternalConfigProfileAdd_ValidName(t *testing.T) { - testutils_viper.InitVipers(t) - - newProfileName := "test-profile" - - reader := testutils.WriteStringToPipe(fmt.Sprintf("%s\n", newProfileName), t) - defer reader.Close() - - err := RunInternalConfigProfileAdd("", "test-description", false, true, reader) - testutils.CheckExpectedError(t, err, nil) - - // Check if the profile is created - if err := profiles.ValidateExistingProfileName(newProfileName); err != nil { - t.Fatalf("Expected profile '%s' to exist, got error: %v", newProfileName, err) - } -} - -// Test RunInternalConfigProfileAdd function with an invalid name from user input -func Test_RunInternalConfigProfileAdd_InvalidName(t *testing.T) { - testutils_viper.InitVipers(t) - - reader := testutils.WriteStringToPipe("invalid$++$#@#$\n", t) - defer reader.Close() - - promptuiErrMsg := promptui.ErrEOF.Error() - expectedErrorPattern := fmt.Sprintf("^%s$", regexp.QuoteMeta(promptuiErrMsg)) - err := RunInternalConfigProfileAdd("", "test-description", false, true, reader) - testutils.CheckExpectedError(t, err, &expectedErrorPattern) -} - -// Test RunInternalConfigProfileAdd function with an empty name from user input -func Test_RunInternalConfigProfileAdd_EmptyName(t *testing.T) { - testutils_viper.InitVipers(t) - - reader := testutils.WriteStringToPipe("\n", t) - defer reader.Close() - - promptuiErrMsg := promptui.ErrEOF.Error() - expectedErrorPattern := fmt.Sprintf("^%s$", regexp.QuoteMeta(promptuiErrMsg)) - err := RunInternalConfigProfileAdd("", "test-description", false, true, reader) - testutils.CheckExpectedError(t, err, &expectedErrorPattern) -} - -// Test RunInternalConfigProfileAdd function with a valid description from user input -func Test_RunInternalConfigProfileAdd_ValidDescription(t *testing.T) { - testutils_viper.InitVipers(t) - - newProfileName := "test-profile" - - reader := testutils.WriteStringToPipe("test-description\n", t) - defer reader.Close() - - err := RunInternalConfigProfileAdd(newProfileName, "", false, true, reader) - testutils.CheckExpectedError(t, err, nil) - - // Check if the profile is created - if err := profiles.ValidateExistingProfileName(newProfileName); err != nil { - t.Fatalf("Expected profile '%s' to exist, got error: %v", newProfileName, err) - } -} - -// Test RunInternalConfigProfileAdd function with an empty description from user input -func Test_RunInternalConfigProfileAdd_EmptyDescription(t *testing.T) { - testutils_viper.InitVipers(t) - - newProfileName := "test-profile" - - reader := testutils.WriteStringToPipe("\n", t) - defer reader.Close() - - err := RunInternalConfigProfileAdd(newProfileName, "", false, true, reader) - testutils.CheckExpectedError(t, err, nil) - - // Check if the profile is created - if err := profiles.ValidateExistingProfileName(newProfileName); err != nil { - t.Fatalf("Expected profile '%s' to exist, got error: %v", newProfileName, err) - } -} - -// Test RunInternalConfigProfileAdd function with a 'y' user input to set active confirm prompt -func Test_RunInternalConfigProfileAdd_ActiveProfileYes(t *testing.T) { - testutils_viper.InitVipers(t) - - newProfileName := "test-profile" - - reader := testutils.WriteStringToPipe("y\n", t) - defer reader.Close() - - err := RunInternalConfigProfileAdd(newProfileName, "test-description", false, false, reader) - testutils.CheckExpectedError(t, err, nil) - - // Check if the profile is created - if err := profiles.ValidateExistingProfileName(newProfileName); err != nil { - t.Fatalf("Expected profile '%s' to exist, got error: %v", newProfileName, err) - } - - // Check if the profile is active - profileName := profiles.GetConfigActiveProfile() - if profileName != newProfileName { - t.Fatalf("Expected active profile to be '%s', got '%s'", newProfileName, profileName) - } -} - -// Test RunInternalConfigProfileAdd function with a 'n' user input to set active confirm prompt -func Test_RunInternalConfigProfileAdd_ActiveProfileNo(t *testing.T) { - testutils_viper.InitVipers(t) - - newProfileName := "test-profile" - - reader := testutils.WriteStringToPipe("n\n", t) - defer reader.Close() - - err := RunInternalConfigProfileAdd(newProfileName, "test-description", false, false, reader) - testutils.CheckExpectedError(t, err, nil) - - // Check if the profile is created - if err := profiles.ValidateExistingProfileName(newProfileName); err != nil { - t.Fatalf("Expected profile '%s' to exist, got error: %v", newProfileName, err) - } - - // Check if the profile is not active - profileName := profiles.GetConfigActiveProfile() - if profileName == newProfileName { - t.Fatalf("Expected active profile to not be '%s'", newProfileName) - } -} - -// Test RunInternalConfigProfileAdd function with an invalid set active confirm prompt user input -// promptui assumes non-'y' responses to be a 'n' response -func Test_RunInternalConfigProfileAdd_ActiveProfileInvalidInput(t *testing.T) { - testutils_viper.InitVipers(t) - - newProfileName := "test-profile" - - reader := testutils.WriteStringToPipe("invalid\n", t) - defer reader.Close() - - err := RunInternalConfigProfileAdd(newProfileName, "test-description", false, false, reader) - testutils.CheckExpectedError(t, err, nil) - - // Check if the profile is created - if err := profiles.ValidateExistingProfileName(newProfileName); err != nil { - t.Fatalf("Expected profile '%s' to exist, got error: %v", newProfileName, err) - } - - // Check if the profile is not active - profileName := profiles.GetConfigActiveProfile() - if profileName == newProfileName { - t.Fatalf("Expected active profile to not be '%s'", newProfileName) - } -} - -// Test RunInternalConfigProfileAdd function with an existing profile name -func Test_RunInternalConfigProfileAdd_ExistingProfile(t *testing.T) { - testutils_viper.InitVipers(t) - - newProfileName := "default" - - expectedErrorPattern := "^invalid profile name: '.*' profile already exists$" - err := RunInternalConfigProfileAdd(newProfileName, "test-description", false, true, nil) - testutils.CheckExpectedError(t, err, &expectedErrorPattern) -} diff --git a/internal/commands/config/profile/delete_internal.go b/internal/commands/config/profile/delete_internal.go deleted file mode 100644 index bee20e7..0000000 --- a/internal/commands/config/profile/delete_internal.go +++ /dev/null @@ -1,22 +0,0 @@ -package profile_internal - -import ( - "fmt" - - "github.com/pingidentity/pingctl/internal/output" - "github.com/pingidentity/pingctl/internal/profiles" -) - -func RunInternalConfigProfileDelete(pName string) (err error) { - err = profiles.DeleteConfigProfile(pName) - if err != nil { - return fmt.Errorf("failed to delete profile: %v", err) - } - - output.Print(output.Opts{ - Message: fmt.Sprintf("Profile '%s' deleted successfully", pName), - Result: output.ENUM_RESULT_SUCCESS, - }) - - return nil -} diff --git a/internal/commands/config/profile/delete_internal_test.go b/internal/commands/config/profile/delete_internal_test.go deleted file mode 100644 index f3d2a12..0000000 --- a/internal/commands/config/profile/delete_internal_test.go +++ /dev/null @@ -1,53 +0,0 @@ -package profile_internal - -import ( - "testing" - - "github.com/pingidentity/pingctl/internal/testing/testutils" - "github.com/pingidentity/pingctl/internal/testing/testutils_viper" -) - -// Test RunInternalConfigProfileDelete function with valid arg -func Test_RunInternalConfigProfileDelete_ValidArg(t *testing.T) { - testutils_viper.InitVipers(t) - - err := RunInternalConfigProfileDelete("production") - testutils.CheckExpectedError(t, err, nil) -} - -// Test RunInternalConfigProfileDelete function with invalid profile name -func Test_RunInternalConfigProfileDelete_InvalidProfileName(t *testing.T) { - testutils_viper.InitVipers(t) - - expectedErrorPattern := "^failed to delete profile: invalid profile name: '.*'. name must contain only alphanumeric characters, underscores, and dashes$" - err := RunInternalConfigProfileDelete("invalid&*^*&") - testutils.CheckExpectedError(t, err, &expectedErrorPattern) -} - -// Test RunInternalConfigProfileDelete function with non-existent profile -func Test_RunInternalConfigProfileDelete_NonExistentProfile(t *testing.T) { - testutils_viper.InitVipers(t) - - expectedErrorPattern := "^failed to delete profile: invalid profile name: '.*' profile does not exist$" - err := RunInternalConfigProfileDelete("invalid") - testutils.CheckExpectedError(t, err, &expectedErrorPattern) -} - -// Test RunInternalConfigProfileDelete function with active profile -func Test_RunInternalConfigProfileDelete_ActiveProfile(t *testing.T) { - testutils_viper.InitVipers(t) - - expectedErrorPattern := "^failed to delete profile: '.*' is the active profile and cannot be deleted$" - err := RunInternalConfigProfileDelete("default") - testutils.CheckExpectedError(t, err, &expectedErrorPattern) -} - -// Check output of RunInternalConfigProfileDelete function -func Example_runInternalConfigProfileDelete() { - testutils_viper.InitVipers(&testing.T{}) - - _ = RunInternalConfigProfileDelete("production") - - // Output: - // Profile 'production' deleted successfully - Success -} diff --git a/internal/commands/config/profile/describe_internal.go b/internal/commands/config/profile/describe_internal.go deleted file mode 100644 index 5b4ef3f..0000000 --- a/internal/commands/config/profile/describe_internal.go +++ /dev/null @@ -1,53 +0,0 @@ -package profile_internal - -import ( - "fmt" - - "github.com/pingidentity/pingctl/internal/output" - "github.com/pingidentity/pingctl/internal/profiles" -) - -func RunInternalConfigProfileDescribe(pName string) (err error) { - err = profiles.ValidateExistingProfileName(pName) - if err != nil { - return fmt.Errorf("failed to describe profile: %v", err) - } - - // Create temp sub viper for profile - mainViper := profiles.GetMainViper() - tempViper := mainViper.Sub(pName) - - descStr := fmt.Sprintf("Profile Name: %s\n", pName) - descStr += fmt.Sprintf("Description: %s\n\n", tempViper.GetString(profiles.ProfileDescriptionOption.ViperKey)) - - setStr := "" - unsetStr := "" - for _, opt := range profiles.ConfigOptions.Options { - if opt.ViperKey == profiles.ProfileDescriptionOption.ViperKey || opt.ViperKey == profiles.ProfileOption.ViperKey { - continue - } - - vValue := tempViper.Get(opt.ViperKey) - if vValue == nil || vValue == "" { - unsetStr += fmt.Sprintf(" - %s\n", opt.ViperKey) - } else { - setStr += fmt.Sprintf(" - %s: %v\n", opt.ViperKey, vValue) - } - - } - - if setStr != "" { - descStr += fmt.Sprintf("Set Options:\n%s\n", setStr) - } - - if unsetStr != "" { - descStr += fmt.Sprintf("Unset Options:\n%s\n", unsetStr) - } - - output.Print(output.Opts{ - Message: descStr, - Result: output.ENUM_RESULT_NIL, - }) - - return nil -} diff --git a/internal/commands/config/profile/describe_internal_test.go b/internal/commands/config/profile/describe_internal_test.go deleted file mode 100644 index 2a0e398..0000000 --- a/internal/commands/config/profile/describe_internal_test.go +++ /dev/null @@ -1,76 +0,0 @@ -package profile_internal - -import ( - "testing" - - "github.com/pingidentity/pingctl/internal/testing/testutils" - "github.com/pingidentity/pingctl/internal/testing/testutils_viper" -) - -// Test RunInternalConfigProfileDescribe function with valid arg -func Test_RunInternalConfigProfileDescribe_ValidArg(t *testing.T) { - testutils_viper.InitVipers(t) - - err := RunInternalConfigProfileDescribe("production") - testutils.CheckExpectedError(t, err, nil) -} - -// Test RunInternalConfigProfileDescribe function with invalid profile name -func Test_RunInternalConfigProfileDescribe_InvalidProfileName(t *testing.T) { - testutils_viper.InitVipers(t) - - expectedErrorPattern := "^failed to describe profile: invalid profile name: '.*'. name must contain only alphanumeric characters, underscores, and dashes$" - err := RunInternalConfigProfileDescribe("invalid&*^*&") - testutils.CheckExpectedError(t, err, &expectedErrorPattern) -} - -// Test RunInternalConfigProfileDescribe function with non-existent profile -func Test_RunInternalConfigProfileDescribe_NonExistentProfile(t *testing.T) { - testutils_viper.InitVipers(t) - - expectedErrorPattern := "^failed to describe profile: invalid profile name: '.*' profile does not exist$" - err := RunInternalConfigProfileDescribe("invalid") - testutils.CheckExpectedError(t, err, &expectedErrorPattern) -} - -// Test RunInternalConfigProfileDescribe function with active profile -func Test_RunInternalConfigProfileDescribe_ActiveProfile(t *testing.T) { - testutils_viper.InitVipers(t) - - err := RunInternalConfigProfileDescribe("default") - testutils.CheckExpectedError(t, err, nil) -} - -// Check output of RunInternalConfigProfileDescribe function -func Example_runInternalConfigProfileDescribe() { - testutils_viper.InitVipers(&testing.T{}) - - _ = RunInternalConfigProfileDescribe("production") - - // Output: - // Profile Name: production - // Description: test profile description - // - // Set Options: - // - pingctl.outputFormat: text - // - pingctl.color: true - // - pingfederate.clientCredentialsAuth.scopes: [] - // - pingfederate.xBypassExternalValidationHeader: false - // - pingfederate.caCertificatePemFiles: [] - // - pingfederate.insecureTrustAllTLS: false - // - // Unset Options: - // - pingone.export.environmentID - // - pingone.worker.environmentID - // - pingone.worker.clientID - // - pingone.worker.clientSecret - // - pingone.region - // - pingfederate.basicAuth.username - // - pingfederate.basicAuth.password - // - pingfederate.httpsHost - // - pingfederate.adminApiPath - // - pingfederate.clientCredentialsAuth.clientID - // - pingfederate.clientCredentialsAuth.clientSecret - // - pingfederate.clientCredentialsAuth.tokenURL - // - pingfederate.accessTokenAuth.accessToken -} diff --git a/internal/commands/config/profile/list_internal.go b/internal/commands/config/profile/list_internal.go deleted file mode 100644 index 66e37d6..0000000 --- a/internal/commands/config/profile/list_internal.go +++ /dev/null @@ -1,27 +0,0 @@ -package profile_internal - -import ( - "fmt" - - "github.com/pingidentity/pingctl/internal/output" - "github.com/pingidentity/pingctl/internal/profiles" -) - -func RunInternalConfigProfileList() { - activeProfileName := profiles.GetConfigActiveProfile() - profileNames := profiles.ConfigProfileNames() - listOutputString := "pingctl profiles:\n" - - for _, pName := range profileNames { - if pName == activeProfileName { - listOutputString += fmt.Sprintf(" * %s\n", pName) - } else { - listOutputString += fmt.Sprintf(" %s\n", pName) - } - } - - output.Print(output.Opts{ - Message: listOutputString, - Result: output.ENUM_RESULT_NIL, - }) -} diff --git a/internal/commands/config/profile/list_internal_test.go b/internal/commands/config/profile/list_internal_test.go deleted file mode 100644 index 6410785..0000000 --- a/internal/commands/config/profile/list_internal_test.go +++ /dev/null @@ -1,19 +0,0 @@ -package profile_internal - -import ( - "testing" - - "github.com/pingidentity/pingctl/internal/testing/testutils_viper" -) - -// Test RunInternalConfigProfileList function -func Example_runInternalConfigProfileList() { - testutils_viper.InitVipers(&testing.T{}) - - RunInternalConfigProfileList() - - // Output: - // pingctl profiles: - // * default - // production -} diff --git a/internal/commands/config/profile/set_active_internal.go b/internal/commands/config/profile/set_active_internal.go deleted file mode 100644 index dde791c..0000000 --- a/internal/commands/config/profile/set_active_internal.go +++ /dev/null @@ -1,25 +0,0 @@ -package profile_internal - -import ( - "fmt" - - "github.com/pingidentity/pingctl/internal/output" - "github.com/pingidentity/pingctl/internal/profiles" -) - -func RunInternalConfigProfileSetActive(profileName string) (err error) { - if err = profiles.ValidateExistingProfileName(profileName); err != nil { - return fmt.Errorf("failed to set active profile: %v", err) - } - - if err = profiles.SetConfigActiveProfile(profileName); err != nil { - return fmt.Errorf("failed to set active profile: %v", err) - } - - output.Print(output.Opts{ - Message: fmt.Sprintf("Active configuration profile set to '%s'", profileName), - Result: output.ENUM_RESULT_SUCCESS, - }) - - return nil -} diff --git a/internal/commands/config/profile/set_active_internal_test.go b/internal/commands/config/profile/set_active_internal_test.go deleted file mode 100644 index 973625a..0000000 --- a/internal/commands/config/profile/set_active_internal_test.go +++ /dev/null @@ -1,51 +0,0 @@ -package profile_internal - -import ( - "testing" - - "github.com/pingidentity/pingctl/internal/testing/testutils" - "github.com/pingidentity/pingctl/internal/testing/testutils_viper" -) - -// Test RunInternalConfigProfileSetActive function with valid args -func Test_RunInternalConfigProfileSetActive_ValidArgs(t *testing.T) { - testutils_viper.InitVipers(t) - - err := RunInternalConfigProfileSetActive("production") - testutils.CheckExpectedError(t, err, nil) -} - -// Test RunInternalConfigProfileSetActive function with invalid profile name -func Test_RunInternalConfigProfileSetActive_InvalidProfileName(t *testing.T) { - testutils_viper.InitVipers(t) - - expectedErrorPattern := "^failed to set active profile: invalid profile name: '.*'. name must contain only alphanumeric characters, underscores, and dashes$" - err := RunInternalConfigProfileSetActive("invalid&*^*&") - testutils.CheckExpectedError(t, err, &expectedErrorPattern) -} - -// Test RunInternalConfigProfileSetActive function with non-existent profile -func Test_RunInternalConfigProfileSetActive_NonExistentProfile(t *testing.T) { - testutils_viper.InitVipers(t) - - expectedErrorPattern := "^failed to set active profile: invalid profile name: '.*' profile does not exist$" - err := RunInternalConfigProfileSetActive("invalid") - testutils.CheckExpectedError(t, err, &expectedErrorPattern) -} - -// Test RunInternalConfigProfileSetActive function with active profile -func Test_RunInternalConfigProfileSetActive_ActiveProfile(t *testing.T) { - testutils_viper.InitVipers(t) - - err := RunInternalConfigProfileSetActive("default") - testutils.CheckExpectedError(t, err, nil) -} - -func Example_runInternalConfigProfileSetActive() { - testutils_viper.InitVipers(&testing.T{}) - - _ = RunInternalConfigProfileSetActive("production") - - // Output: - // Active configuration profile set to 'production' - Success -} diff --git a/internal/commands/config/set_active_profile_internal.go b/internal/commands/config/set_active_profile_internal.go new file mode 100644 index 0000000..cafbbea --- /dev/null +++ b/internal/commands/config/set_active_profile_internal.go @@ -0,0 +1,52 @@ +package config_internal + +import ( + "fmt" + "io" + + "github.com/pingidentity/pingctl/internal/configuration/options" + "github.com/pingidentity/pingctl/internal/input" + "github.com/pingidentity/pingctl/internal/output" + "github.com/pingidentity/pingctl/internal/profiles" +) + +func RunInternalConfigSetActiveProfile(rc io.ReadCloser) (err error) { + pName, err := readConfigSetActiveProfileOptions(rc) + if err != nil { + return fmt.Errorf("failed to set active profile: %v", err) + } + + output.Print(output.Opts{ + Message: fmt.Sprintf("Setting active profile to '%s'...", pName), + Result: output.ENUM_RESULT_NIL, + }) + + if err = profiles.GetMainConfig().ChangeActiveProfile(pName); err != nil { + return fmt.Errorf("failed to set active profile: %v", err) + } + + output.Print(output.Opts{ + Message: fmt.Sprintf("Active profile set to '%s'", pName), + Result: output.ENUM_RESULT_SUCCESS, + }) + + return nil +} + +func readConfigSetActiveProfileOptions(rc io.ReadCloser) (pName string, err error) { + if !options.ConfigSetActiveProfileOption.Flag.Changed { + pName, err = input.RunPromptSelect("Select profile to set as active: ", profiles.GetMainConfig().ProfileNames(), rc) + } else { + pName, err = profiles.GetOptionValue(options.ConfigSetActiveProfileOption) + } + + if err != nil { + return pName, err + } + + if pName == "" { + return pName, fmt.Errorf("unable to determine profile name to set as active") + } + + return pName, nil +} diff --git a/internal/commands/config/set_active_profile_internal_test.go b/internal/commands/config/set_active_profile_internal_test.go new file mode 100644 index 0000000..9889844 --- /dev/null +++ b/internal/commands/config/set_active_profile_internal_test.go @@ -0,0 +1,57 @@ +package config_internal + +import ( + "os" + "testing" + + "github.com/pingidentity/pingctl/internal/configuration/options" + "github.com/pingidentity/pingctl/internal/customtypes" + "github.com/pingidentity/pingctl/internal/testing/testutils" + "github.com/pingidentity/pingctl/internal/testing/testutils_viper" +) + +// Test RunInternalConfigSetActiveProfile function +func Test_RunInternalConfigSetActiveProfile(t *testing.T) { + testutils_viper.InitVipers(t) + + var ( + profileName = customtypes.String("production") + ) + options.ConfigSetActiveProfileOption.Flag.Changed = true + options.ConfigSetActiveProfileOption.CobraParamValue = &profileName + + err := RunInternalConfigSetActiveProfile(os.Stdin) + if err != nil { + t.Errorf("RunInternalConfigSetActiveProfile returned error: %v", err) + } +} + +// Test RunInternalConfigSetActiveProfile function fails with invalid profile name +func Test_RunInternalConfigSetActiveProfile_InvalidProfileName(t *testing.T) { + testutils_viper.InitVipers(t) + + var ( + profileName = customtypes.String("(*#&)") + ) + options.ConfigSetActiveProfileOption.Flag.Changed = true + options.ConfigSetActiveProfileOption.CobraParamValue = &profileName + + expectedErrorPattern := `^failed to set active profile: invalid profile name: '.*'\. name must contain only alphanumeric characters, underscores, and dashes$` + err := RunInternalConfigSetActiveProfile(os.Stdin) + testutils.CheckExpectedError(t, err, &expectedErrorPattern) +} + +// Test RunInternalConfigSetActiveProfile function fails with non-existent profile +func Test_RunInternalConfigSetActiveProfile_NonExistentProfile(t *testing.T) { + testutils_viper.InitVipers(t) + + var ( + profileName = customtypes.String("non-existent") + ) + options.ConfigSetActiveProfileOption.Flag.Changed = true + options.ConfigSetActiveProfileOption.CobraParamValue = &profileName + + expectedErrorPattern := `^failed to set active profile: invalid profile name: '.*' profile does not exist$` + err := RunInternalConfigSetActiveProfile(os.Stdin) + testutils.CheckExpectedError(t, err, &expectedErrorPattern) +} diff --git a/internal/commands/config/set_internal.go b/internal/commands/config/set_internal.go index 3a5cf69..d7cd028 100644 --- a/internal/commands/config/set_internal.go +++ b/internal/commands/config/set_internal.go @@ -2,138 +2,160 @@ package config_internal import ( "fmt" - "slices" - "strconv" "strings" - "github.com/hashicorp/go-uuid" + "github.com/pingidentity/pingctl/internal/configuration" + "github.com/pingidentity/pingctl/internal/configuration/options" "github.com/pingidentity/pingctl/internal/customtypes" + "github.com/pingidentity/pingctl/internal/output" "github.com/pingidentity/pingctl/internal/profiles" + "github.com/spf13/viper" ) -func RunInternalConfigSet(kvPair string) error { - // Parse the key=value pair from the command line arguments - viperKey, value, err := parseKeyValuePair(kvPair) +func RunInternalConfigSet(kvPair string) (err error) { + pName, vKey, vValue, err := readConfigSetOptions(kvPair) if err != nil { - return err + return fmt.Errorf("failed to set configuration: %v", err) } - // Check if the key is a valid viper configuration key - validKeys := profiles.ProfileKeys() - if !slices.ContainsFunc(validKeys, func(v string) bool { - return strings.EqualFold(v, viperKey) - }) { - slices.Sort(validKeys) - validKeysStr := strings.Join(validKeys, ", ") - return fmt.Errorf("failed to set configuration: key '%s' is not recognized as a valid configuration key. Valid keys: %s", viperKey, validKeysStr) + if err = configuration.ValidateViperKey(vKey); err != nil { + return fmt.Errorf("failed to set configuration: %v", err) } // Make sure value is not empty, and suggest unset command if it is - if value == "" { - return fmt.Errorf("failed to set configuration: value for key '%s' is empty. Use 'pingctl config unset %s' to unset the key", viperKey, viperKey) + if vValue == "" { + return fmt.Errorf("failed to set configuration: value for key '%s' is empty. Use 'pingctl config unset %s' to unset the key", vKey, vKey) } - valueType, ok := profiles.OptionTypeFromViperKey(viperKey) - if !ok { - return fmt.Errorf("failed to set configuration: value type for key %s unrecognized", viperKey) + if err = profiles.GetMainConfig().ValidateExistingProfileName(pName); err != nil { + return fmt.Errorf("failed to set configuration: %v", err) } - if err := setValue(viperKey, value, valueType); err != nil { - return err - } - - if err := profiles.SaveProfileViperToFile(); err != nil { - return err - } + subViper := profiles.GetMainConfig().ViperInstance().Sub(pName) - if err := PrintConfig(); err != nil { - return err + opt, err := configuration.OptionFromViperKey(vKey) + if err != nil { + return fmt.Errorf("failed to set configuration: %v", err) } - return nil -} - -func parseKeyValuePair(kvPair string) (string, string, error) { - parsedInput := strings.SplitN(kvPair, "=", 2) - if len(parsedInput) != 2 { - return "", "", fmt.Errorf("failed to set configuration: invalid assignment format '%s'. Expect 'key=value' format", kvPair) + if err = setValue(subViper, vKey, vValue, opt.Type); err != nil { + return fmt.Errorf("failed to set configuration: %v", err) } - return parsedInput[0], parsedInput[1], nil -} - -func setValue(viperKey, value string, valueType profiles.OptionType) error { - switch valueType { - case profiles.ENUM_BOOL: - return setBool(viperKey, value) - case profiles.ENUM_ID: - return setUUID(viperKey, value) - case profiles.ENUM_OUTPUT_FORMAT: - return setOutputFormat(viperKey, value) - case profiles.ENUM_PINGONE_REGION: - return setPingOneRegion(viperKey, value) - case profiles.ENUM_STRING: - profiles.GetProfileViper().Set(viperKey, string(value)) - return nil - case profiles.ENUM_STRING_SLICE: - return setStringSlice(viperKey, value) - default: - return fmt.Errorf("failed to set configuration: variable type for key '%s' is not recognized", viperKey) + if err = profiles.GetMainConfig().SaveProfile(pName, subViper); err != nil { + return fmt.Errorf("failed to set configuration: %v", err) } -} -func setBool(viperKey string, value string) error { - boolValue, err := strconv.ParseBool(value) + yamlStr, err := profiles.GetMainConfig().ProfileToString(pName) if err != nil { - return fmt.Errorf("failed to set configuration: value for key '%s' must be a boolean. Use 'true' or 'false'", viperKey) + return fmt.Errorf("failed to set configuration: %v", err) } - profiles.GetProfileViper().Set(viperKey, boolValue) + output.Print(output.Opts{ + Message: "Configuration set successfully", + Result: output.ENUM_RESULT_SUCCESS, + }) + + output.Print(output.Opts{ + Message: yamlStr, + Result: output.ENUM_RESULT_NIL, + }) return nil } -func setUUID(viperKey string, value string) error { - // Check string is in the form of a UUID - if _, err := uuid.ParseUUID(value); err != nil { - return fmt.Errorf("failed to set configuration: value for key '%s' must be a valid UUID", viperKey) +func readConfigSetOptions(kvPair string) (pName string, vKey string, vValue string, err error) { + if pName, err = readConfigSetProfileName(); err != nil { + return pName, vKey, vValue, err } - profiles.GetProfileViper().Set(viperKey, string(value)) + if vKey, vValue, err = parseKeyValuePair(kvPair); err != nil { + return pName, vKey, vValue, err + } - return nil + return pName, vKey, vValue, nil } -func setOutputFormat(viperKey string, value string) error { - outputFormat := customtypes.OutputFormat("") - if err := outputFormat.Set(value); err != nil { - return fmt.Errorf("failed to set configuration: %s", err.Error()) +func readConfigSetProfileName() (pName string, err error) { + if !options.ConfigSetProfileOption.Flag.Changed { + pName, err = profiles.GetOptionValue(options.RootActiveProfileOption) + } else { + pName, err = profiles.GetOptionValue(options.ConfigSetProfileOption) + } + + if err != nil { + return pName, err } - profiles.GetProfileViper().Set(viperKey, outputFormat) + if pName == "" { + return pName, fmt.Errorf("unable to determine profile to set configuration to") + } - return nil + return pName, nil } -func setPingOneRegion(viperKey string, value string) error { - region := customtypes.PingOneRegion("") - if err := region.Set(value); err != nil { - return fmt.Errorf("failed to set configuration: %s", err.Error()) +func parseKeyValuePair(kvPair string) (string, string, error) { + parsedInput := strings.SplitN(kvPair, "=", 2) + if len(parsedInput) < 2 { + return "", "", fmt.Errorf("invalid assignment format '%s'. Expect 'key=value' format", kvPair) } - profiles.GetProfileViper().Set(viperKey, region) - - return nil + return parsedInput[0], parsedInput[1], nil } -func setStringSlice(viperKey string, value string) error { - ss := []string{} - - value = strings.TrimSpace(value) - for _, v := range strings.Split(value, ",") { - ss = append(ss, strings.TrimSpace(v)) +func setValue(profileViper *viper.Viper, vKey, vValue string, valueType options.OptionType) (err error) { + switch valueType { + case options.ENUM_BOOL: + var bool customtypes.Bool + if err = bool.Set(vValue); err != nil { + return fmt.Errorf("value for key '%s' must be a boolean. Allowed [true, false]: %v", vKey, err) + } + profileViper.Set(vKey, bool) + case options.ENUM_EXPORT_FORMAT: + var exportFormat customtypes.ExportFormat + if err = exportFormat.Set(vValue); err != nil { + return fmt.Errorf("value for key '%s' must be a valid export format. Allowed [%s]: %v", vKey, strings.Join(customtypes.ExportFormatValidValues(), ", "), err) + } + profileViper.Set(vKey, exportFormat) + case options.ENUM_MULTI_SERVICE: + var multiService customtypes.MultiService + if err = multiService.Set(vValue); err != nil { + return fmt.Errorf("value for key '%s' must be a valid multi-service. Allowed [%s]: %v", vKey, strings.Join(customtypes.MultiServiceValidValues(), ", "), err) + } + profileViper.Set(vKey, multiService) + case options.ENUM_OUTPUT_FORMAT: + var outputFormat customtypes.OutputFormat + if err = outputFormat.Set(vValue); err != nil { + return fmt.Errorf("value for key '%s' must be a valid output format. Allowed [%s]: %v", vKey, strings.Join(customtypes.OutputFormatValidValues(), ", "), err) + } + profileViper.Set(vKey, outputFormat) + case options.ENUM_PINGONE_REGION: + var region customtypes.PingOneRegion + if err = region.Set(vValue); err != nil { + return fmt.Errorf("value for key '%s' must be a valid PingOne region. Allowed [%s]: %v", vKey, strings.Join(customtypes.PingOneRegionValidValues(), ", "), err) + } + profileViper.Set(vKey, region) + case options.ENUM_STRING: + var str customtypes.String + if err = str.Set(vValue); err != nil { + return fmt.Errorf("value for key '%s' must be a string: %v", vKey, err) + } + profileViper.Set(vKey, str) + case options.ENUM_STRING_SLICE: + var strSlice customtypes.StringSlice + if err = strSlice.Set(vValue); err != nil { + return fmt.Errorf("value for key '%s' must be a string slice: %v", vKey, err) + } + profileViper.Set(vKey, strSlice) + case options.ENUM_UUID: + var uuid customtypes.UUID + if err = uuid.Set(vValue); err != nil { + return fmt.Errorf("value for key '%s' must be a valid UUID: %v", vKey, err) + } + profileViper.Set(vKey, uuid) + default: + return fmt.Errorf("failed to set configuration: variable type for key '%s' is not recognized", vKey) } - profiles.GetProfileViper().Set(viperKey, ss) return nil } diff --git a/internal/commands/config/set_internal_test.go b/internal/commands/config/set_internal_test.go index c7db2f5..ac07ddf 100644 --- a/internal/commands/config/set_internal_test.go +++ b/internal/commands/config/set_internal_test.go @@ -1,147 +1,105 @@ package config_internal import ( - "fmt" "testing" - "github.com/hashicorp/go-uuid" - "github.com/pingidentity/pingctl/internal/profiles" + "github.com/pingidentity/pingctl/internal/configuration/options" + "github.com/pingidentity/pingctl/internal/customtypes" "github.com/pingidentity/pingctl/internal/testing/testutils" "github.com/pingidentity/pingctl/internal/testing/testutils_viper" ) -// Test RunInternalConfigSet function with args -func Test_RunInternalConfigSet_WithArgs(t *testing.T) { +// Test RunInternalConfigSet function +func Test_RunInternalConfigSet(t *testing.T) { testutils_viper.InitVipers(t) - uuid, err := uuid.GenerateUUID() + err := RunInternalConfigSet("pingctl.color=true") if err != nil { - t.Fatalf("failed to generate UUID: %v", err) + t.Errorf("RunInternalConfigSet returned error: %v", err) } - - err = RunInternalConfigSet(fmt.Sprintf("%s=%s", profiles.PingOneWorkerClientIDOption.ViperKey, uuid)) - testutils.CheckExpectedError(t, err, nil) } -// Test RunInternalConfigSet function with invalid key +// Test RunInternalConfigSet function fails with invalid key func Test_RunInternalConfigSet_InvalidKey(t *testing.T) { - expectedErrorPattern := `^failed to set configuration: key 'pingctl\.invalid' is not recognized as a valid configuration key\. Valid keys: [A-Za-z\.\s,]+$` - err := RunInternalConfigSet("pingctl.invalid=true") - testutils.CheckExpectedError(t, err, &expectedErrorPattern) -} + testutils_viper.InitVipers(t) -// Test RunInternalConfigSet function with empty value -func Test_RunInternalConfigSet_EmptyValue(t *testing.T) { - expectedErrorPattern := `^failed to set configuration: value for key 'pingone\.worker\.clientID' is empty\. Use 'pingctl config unset pingone\.worker\.clientID' to unset the key$` - err := RunInternalConfigSet(fmt.Sprintf("%s=", profiles.PingOneWorkerClientIDOption.ViperKey)) + expectedErrorPattern := `^failed to set configuration: key '.*' is not recognized as a valid configuration key. Valid keys: .*$` + err := RunInternalConfigSet("invalid-key=false") testutils.CheckExpectedError(t, err, &expectedErrorPattern) } -// Test RunInternalConfigSet function with invalid value +// Test RunInternalConfigSet function fails with invalid value func Test_RunInternalConfigSet_InvalidValue(t *testing.T) { - expectedErrorPattern := `^failed to set configuration: value for key 'pingone\.worker\.clientID' must be a valid UUID$` - err := RunInternalConfigSet(fmt.Sprintf("%s=invalid", profiles.PingOneWorkerClientIDOption.ViperKey)) - testutils.CheckExpectedError(t, err, &expectedErrorPattern) -} + testutils_viper.InitVipers(t) -// Test RunInternalConfigSet function with invalid value type -func Test_RunInternalConfigSet_InvalidValueType(t *testing.T) { - expectedErrorPattern := `^failed to set configuration: value for key 'pingctl\.color' must be a boolean. Use 'true' or 'false'$` - err := RunInternalConfigSet(fmt.Sprintf("%s=notboolean", profiles.ColorOption.ViperKey)) + expectedErrorPattern := `^failed to set configuration: value for key '.*' must be a boolean. Allowed .*: strconv.ParseBool: parsing ".*": invalid syntax$` + err := RunInternalConfigSet("pingctl.color=invalid") testutils.CheckExpectedError(t, err, &expectedErrorPattern) } -// Test setValue() function with valid value -func Test_setValue_ValidValue(t *testing.T) { +// Test RunInternalConfigSet function fails with non-existent profile name +func Test_RunInternalConfigSet_NonExistentProfileName(t *testing.T) { testutils_viper.InitVipers(t) - err := setValue(profiles.ColorOption.ViperKey, "false", profiles.ENUM_BOOL) - testutils.CheckExpectedError(t, err, nil) -} + var ( + profileName = customtypes.String("non-existent") + ) -// Test setValue() function with invalid value -func Test_setValue_InvalidValue(t *testing.T) { - expectedErrorPattern := `^failed to set configuration: value for key 'pingone\.worker\.clientID' must be a valid UUID$` - err := setValue(profiles.PingOneWorkerClientIDOption.ViperKey, "invalid", profiles.ENUM_ID) - testutils.CheckExpectedError(t, err, &expectedErrorPattern) -} + options.ConfigSetProfileOption.Flag.Changed = true + options.ConfigSetProfileOption.CobraParamValue = &profileName -// Test setValue() function with invalid value type -func Test_setValue_InvalidValueType(t *testing.T) { - expectedErrorPattern := `^failed to set configuration: variable type for key 'pingctl\.color' is not recognized$` - err := setValue(profiles.ColorOption.ViperKey, "false", "invalid") + expectedErrorPattern := `^failed to set configuration: invalid profile name: '.*' profile does not exist$` + err := RunInternalConfigSet("pingctl.color=true") testutils.CheckExpectedError(t, err, &expectedErrorPattern) } -// Test setBool() function with valid value -func Test_setBool_ValidValue(t *testing.T) { +// Test RunInternalConfigSet function with different profile +func Test_RunInternalConfigSet_DifferentProfile(t *testing.T) { testutils_viper.InitVipers(t) - err := setBool(profiles.ColorOption.ViperKey, "false") - testutils.CheckExpectedError(t, err, nil) -} - -// Test setBool() function with invalid value -func Test_setBool_InvalidValue(t *testing.T) { - expectedErrorPattern := `^failed to set configuration: value for key 'pingctl\.color' must be a boolean. Use 'true' or 'false'$` - err := setBool(profiles.ColorOption.ViperKey, "invalid") - testutils.CheckExpectedError(t, err, &expectedErrorPattern) -} + var ( + profileName = customtypes.String("production") + ) -// Test setUUID() function with valid value -func Test_setUUID_ValidValue(t *testing.T) { - testutils_viper.InitVipers(t) + options.ConfigSetProfileOption.Flag.Changed = true + options.ConfigSetProfileOption.CobraParamValue = &profileName - uuid, err := uuid.GenerateUUID() + err := RunInternalConfigSet("pingctl.color=true") if err != nil { - t.Fatalf("failed to generate UUID: %v", err) + t.Errorf("RunInternalConfigSet returned error: %v", err) } - - err = setUUID(profiles.PingOneWorkerClientIDOption.ViperKey, uuid) - testutils.CheckExpectedError(t, err, nil) -} - -// Test setUUID() function with invalid value -func Test_setUUID_InvalidValue(t *testing.T) { - expectedErrorPattern := `^failed to set configuration: value for key 'pingone\.worker\.clientID' must be a valid UUID$` - err := setUUID(profiles.PingOneWorkerClientIDOption.ViperKey, "invalid") - testutils.CheckExpectedError(t, err, &expectedErrorPattern) } -// Test setOutputFormat() function with valid value -func Test_setOutputFormat_ValidValue(t *testing.T) { +// Test RunInternalConfigSet function fails with invalid profile name +func Test_RunInternalConfigSet_InvalidProfileName(t *testing.T) { testutils_viper.InitVipers(t) - err := setOutputFormat(profiles.OutputOption.ViperKey, "json") - testutils.CheckExpectedError(t, err, nil) -} + var ( + profileName = customtypes.String("*&%*&") + ) + + options.ConfigSetProfileOption.Flag.Changed = true + options.ConfigSetProfileOption.CobraParamValue = &profileName -// Test setOutputFormat() function with invalid value -func Test_setOutputFormat_InvalidValue(t *testing.T) { - expectedErrorPattern := `^failed to set configuration: unrecognized Output Format: 'invalid'\. Must be one of: [a-z\s,]+$` - err := setOutputFormat(profiles.OutputOption.ViperKey, "invalid") + expectedErrorPattern := `^failed to set configuration: invalid profile name: '.*'\. name must contain only alphanumeric characters, underscores, and dashes$` + err := RunInternalConfigSet("pingctl.color=true") testutils.CheckExpectedError(t, err, &expectedErrorPattern) } -// Test setPingOneRegion() function with valid value -func Test_setPingOneRegion_ValidValue(t *testing.T) { +// Test RunInternalConfigSet function fails with no value provided +func Test_RunInternalConfigSet_NoValue(t *testing.T) { testutils_viper.InitVipers(t) - err := setPingOneRegion(profiles.PingOneRegionOption.ViperKey, "AsiaPacific") - testutils.CheckExpectedError(t, err, nil) -} - -// Test setPingOneRegion() function with invalid value -func Test_setPingOneRegion_InvalidValue(t *testing.T) { - expectedErrorPattern := `^failed to set configuration: unrecognized PingOne Region: 'invalid'\. Must be one of: [A-Za-z\s,]+$` - err := setPingOneRegion(profiles.PingOneRegionOption.ViperKey, "invalid") + expectedErrorPattern := `^failed to set configuration: value for key '.*' is empty. Use 'pingctl config unset .*' to unset the key$` + err := RunInternalConfigSet("pingctl.color=") testutils.CheckExpectedError(t, err, &expectedErrorPattern) } -// Test setStringSlice() function with valid value -func Test_setStringSlice_ValidValue(t *testing.T) { +// Test RunInternalConfigSet function fails with no keyValue provided +func Test_RunInternalConfigSet_NoKeyValue(t *testing.T) { testutils_viper.InitVipers(t) - err := setStringSlice(profiles.PingFederateScopesOption.ViperKey, "email,test,var") - testutils.CheckExpectedError(t, err, nil) + expectedErrorPattern := `^failed to set configuration: invalid assignment format ''\. Expect 'key=value' format$` + err := RunInternalConfigSet("") + testutils.CheckExpectedError(t, err, &expectedErrorPattern) } diff --git a/internal/commands/config/unset_internal.go b/internal/commands/config/unset_internal.go index d211bda..fde6b70 100644 --- a/internal/commands/config/unset_internal.go +++ b/internal/commands/config/unset_internal.go @@ -2,41 +2,72 @@ package config_internal import ( "fmt" - "slices" - "strings" + "github.com/pingidentity/pingctl/internal/configuration" + "github.com/pingidentity/pingctl/internal/configuration/options" + "github.com/pingidentity/pingctl/internal/output" "github.com/pingidentity/pingctl/internal/profiles" ) -func RunInternalConfigUnset(viperKey string) error { - // Check if the key is a valid viper configuration key - validKeys := profiles.ProfileKeys() - if !slices.ContainsFunc(validKeys, func(v string) bool { - return strings.EqualFold(v, viperKey) - }) { - slices.Sort(validKeys) - validKeysStr := strings.Join(validKeys, ", ") - return fmt.Errorf("unable to unset configuration: key '%s' is not recognized as a valid configuration key. Valid keys: %s", viperKey, validKeysStr) +func RunInternalConfigUnset(viperKey string) (err error) { + if err = configuration.ValidateViperKey(viperKey); err != nil { + return fmt.Errorf("failed to unset configuration: %v", err) + } + + pName, err := readConfigUnsetOptions() + if err != nil { + return fmt.Errorf("failed to unset configuration: %v", err) } - valueType, ok := profiles.OptionTypeFromViperKey(viperKey) - if !ok { - return fmt.Errorf("failed to unset configuration: value type for key %s unrecognized", viperKey) + if err = profiles.GetMainConfig().ValidateExistingProfileName(pName); err != nil { + return fmt.Errorf("failed to unset configuration: %v", err) } - defVal, err := profiles.GetDefaultValue(valueType) + subViper := profiles.GetMainConfig().ViperInstance().Sub(pName) + + opt, err := configuration.OptionFromViperKey(viperKey) if err != nil { return fmt.Errorf("failed to unset configuration: %v", err) } - profiles.GetProfileViper().Set(viperKey, defVal) - if err := profiles.SaveProfileViperToFile(); err != nil { - return err + subViper.Set(viperKey, opt.DefaultValue) + + if err = profiles.GetMainConfig().SaveProfile(pName, subViper); err != nil { + return fmt.Errorf("failed to unset configuration: %v", err) } - if err := PrintConfig(); err != nil { - return err + yamlStr, err := profiles.GetMainConfig().ProfileToString(pName) + if err != nil { + return fmt.Errorf("failed to unset configuration: %v", err) } + output.Print(output.Opts{ + Message: "Configuration unset successfully", + Result: output.ENUM_RESULT_SUCCESS, + }) + + output.Print(output.Opts{ + Message: yamlStr, + Result: output.ENUM_RESULT_NIL, + }) + return nil } + +func readConfigUnsetOptions() (pName string, err error) { + if !options.ConfigUnsetProfileOption.Flag.Changed { + pName, err = profiles.GetOptionValue(options.RootActiveProfileOption) + } else { + pName, err = profiles.GetOptionValue(options.ConfigUnsetProfileOption) + } + + if err != nil { + return pName, err + } + + if pName == "" { + return pName, fmt.Errorf("unable to determine profile to unset configuration from") + } + + return pName, nil +} diff --git a/internal/commands/config/unset_internal_test.go b/internal/commands/config/unset_internal_test.go index c7f6958..0d05fb0 100644 --- a/internal/commands/config/unset_internal_test.go +++ b/internal/commands/config/unset_internal_test.go @@ -3,22 +3,60 @@ package config_internal import ( "testing" - "github.com/pingidentity/pingctl/internal/profiles" + "github.com/pingidentity/pingctl/internal/configuration/options" + "github.com/pingidentity/pingctl/internal/customtypes" "github.com/pingidentity/pingctl/internal/testing/testutils" "github.com/pingidentity/pingctl/internal/testing/testutils_viper" ) -// Test RunInternalConfigUnset function with invalid key +// Test RunInternalConfigUnset function +func Test_RunInternalConfigUnset(t *testing.T) { + testutils_viper.InitVipers(t) + + err := RunInternalConfigUnset("pingctl.color") + if err != nil { + t.Errorf("RunInternalConfigUnset returned error: %v", err) + } +} + +// Test RunInternalConfigUnset function fails with invalid key func Test_RunInternalConfigUnset_InvalidKey(t *testing.T) { - expectedErrorPattern := `^unable to unset configuration: key 'pingctl\.invalid' is not recognized as a valid configuration key\. Valid keys: [A-Za-z\.\s,]+$` - err := RunInternalConfigUnset("pingctl.invalid") + testutils_viper.InitVipers(t) + + expectedErrorPattern := `^failed to unset configuration: key '.*' is not recognized as a valid configuration key. Valid keys: .*$` + err := RunInternalConfigUnset("invalid-key") testutils.CheckExpectedError(t, err, &expectedErrorPattern) } -// Test RunInternalConfigUnset function with valid key -func Test_RunInternalConfigUnset_ValidKey(t *testing.T) { +// Test RunInternalConfigUnset function with different profile +func Test_RunInternalConfigUnset_DifferentProfile(t *testing.T) { testutils_viper.InitVipers(t) - err := RunInternalConfigUnset(profiles.ColorOption.ViperKey) - testutils.CheckExpectedError(t, err, nil) + var ( + profileName = customtypes.String("production") + ) + + options.ConfigUnsetProfileOption.Flag.Changed = true + options.ConfigUnsetProfileOption.CobraParamValue = &profileName + + err := RunInternalConfigUnset("pingctl.color") + if err != nil { + t.Errorf("RunInternalConfigUnset returned error: %v", err) + } +} + +// Test RunInternalConfigUnset function fails with invalid profile name +func Test_RunInternalConfigUnset_InvalidProfileName(t *testing.T) { + testutils_viper.InitVipers(t) + + var ( + profileName = customtypes.String("invalid") + ) + + options.ConfigUnsetProfileOption.Flag.Changed = true + options.ConfigUnsetProfileOption.CobraParamValue = &profileName + + expectedErrorPattern := `^failed to unset configuration: invalid profile name: '.*' profile does not exist$` + err := RunInternalConfigUnset("pingctl.color") + testutils.CheckExpectedError(t, err, &expectedErrorPattern) } diff --git a/internal/commands/config/view_profile_internal.go b/internal/commands/config/view_profile_internal.go new file mode 100644 index 0000000..cf4c5fc --- /dev/null +++ b/internal/commands/config/view_profile_internal.go @@ -0,0 +1,48 @@ +package config_internal + +import ( + "fmt" + + "github.com/pingidentity/pingctl/internal/configuration/options" + "github.com/pingidentity/pingctl/internal/output" + "github.com/pingidentity/pingctl/internal/profiles" +) + +func RunInternalConfigViewProfile() (err error) { + pName, err := readConfigViewProfileOptions() + if err != nil { + return fmt.Errorf("failed to view profile: %v", err) + } + + profileStr, err := profiles.GetMainConfig().ProfileToString(pName) + if err != nil { + return fmt.Errorf("failed to view profile: %v", err) + } + + profileStr = fmt.Sprintf("Profile: %s\n\n%s", pName, profileStr) + + output.Print(output.Opts{ + Message: profileStr, + Result: output.ENUM_RESULT_NIL, + }) + + return nil +} + +func readConfigViewProfileOptions() (pName string, err error) { + if !options.ConfigViewProfileOption.Flag.Changed { + pName, err = profiles.GetOptionValue(options.RootActiveProfileOption) + } else { + pName, err = profiles.GetOptionValue(options.ConfigViewProfileOption) + } + + if err != nil { + return pName, err + } + + if pName == "" { + return pName, fmt.Errorf("unable to determine profile name to view") + } + + return pName, nil +} diff --git a/internal/commands/config/view_profile_internal_test.go b/internal/commands/config/view_profile_internal_test.go new file mode 100644 index 0000000..6deb5f4 --- /dev/null +++ b/internal/commands/config/view_profile_internal_test.go @@ -0,0 +1,53 @@ +package config_internal + +import ( + "testing" + + "github.com/pingidentity/pingctl/internal/configuration/options" + "github.com/pingidentity/pingctl/internal/customtypes" + "github.com/pingidentity/pingctl/internal/testing/testutils" + "github.com/pingidentity/pingctl/internal/testing/testutils_viper" +) + +// Test RunInternalConfigViewProfile function +func Test_RunInternalConfigViewProfile(t *testing.T) { + testutils_viper.InitVipers(t) + + err := RunInternalConfigViewProfile() + if err != nil { + t.Errorf("RunInternalConfigViewProfile returned error: %v", err) + } +} + +// Test RunInternalConfigViewProfile function fails with invalid profile name +func Test_RunInternalConfigViewProfile_InvalidProfileName(t *testing.T) { + testutils_viper.InitVipers(t) + + var ( + profileName = customtypes.String("invalid") + ) + + options.ConfigViewProfileOption.Flag.Changed = true + options.ConfigViewProfileOption.CobraParamValue = &profileName + + expectedErrorPattern := `^failed to view profile: invalid profile name: '.*' profile does not exist$` + err := RunInternalConfigViewProfile() + testutils.CheckExpectedError(t, err, &expectedErrorPattern) +} + +// Test RunInternalConfigViewProfile function with different profile +func Test_RunInternalConfigViewProfile_DifferentProfile(t *testing.T) { + testutils_viper.InitVipers(t) + + var ( + profileName = customtypes.String("production") + ) + + options.ConfigViewProfileOption.Flag.Changed = true + options.ConfigViewProfileOption.CobraParamValue = &profileName + + err := RunInternalConfigViewProfile() + if err != nil { + t.Errorf("RunInternalConfigViewProfile returned error: %v", err) + } +} diff --git a/internal/commands/feedback/feedback_internal_test.go b/internal/commands/feedback/feedback_internal_test.go deleted file mode 100644 index 310b822..0000000 --- a/internal/commands/feedback/feedback_internal_test.go +++ /dev/null @@ -1,23 +0,0 @@ -package feedback_internal_test - -import ( - feedback_internal "github.com/pingidentity/pingctl/internal/commands/feedback" -) - -func Example_printFeedbackMessage() { - feedback_internal.PrintFeedbackMessage() - - //Output: - //Thank you for participating in early adoption of the refreshed Ping Identity universal CLI! - // - //We appreciate your feedback and suggestions for improvement regarding your experiences with the CLI. - // - //Please visit the following URL in your browser to fill out a short, anonymous survey that will help guide our development efforts and improve the CLI for all users: - // - // https://forms.gle/xLz6ao4Ts86Zn2yt9 - // - //If you encounter any bugs while using the tool, please open an issue on the project's GitHub repository's issue tracker: - // - // https://github.com/pingidentity/pingctl/issues/new - // -} diff --git a/internal/commands/platform/export_internal.go b/internal/commands/platform/export_internal.go index b2d270d..a758794 100644 --- a/internal/commands/platform/export_internal.go +++ b/internal/commands/platform/export_internal.go @@ -12,6 +12,7 @@ import ( "strings" pingoneGoClient "github.com/patrickcping/pingone-go-sdk-v2/pingone" + "github.com/pingidentity/pingctl/internal/configuration/options" "github.com/pingidentity/pingctl/internal/connector" "github.com/pingidentity/pingctl/internal/connector/common" "github.com/pingidentity/pingctl/internal/connector/pingfederate" @@ -37,35 +38,56 @@ var ( pingoneContext context.Context ) -func RunInternalExport(ctx context.Context, commandVersion string, outputDir, exportFormat string, overwriteExport bool, multiService *customtypes.MultiService, basicAuthFlagsUsed, AccessTokenAuthFlagsUsed bool) (err error) { +func RunInternalExport(ctx context.Context, commandVersion string) (err error) { if ctx == nil { return fmt.Errorf("failed to run 'platform export' command. context is nil") } - if multiService.ContainsPingOneService() { + exportFormat, err := profiles.GetOptionValue(options.PlatformExportExportFormatOption) + if err != nil { + return err + } + multiService, err := profiles.GetOptionValue(options.PlatformExportServiceOption) + if err != nil { + return err + } + outputDir, err := profiles.GetOptionValue(options.PlatformExportOutputDirectoryOption) + if err != nil { + return err + } + overwriteExport, err := profiles.GetOptionValue(options.PlatformExportOverwriteOption) + if err != nil { + return err + } + + ms := customtypes.NewMultiService() + if err = ms.Set(multiService); err != nil { + return err + } + + if ms.ContainsPingOneService() { if err = initPingOneServices(ctx, commandVersion); err != nil { return err } } - if multiService.ContainsPingFederateService() { - if err = initPingFederateServices(ctx, commandVersion, basicAuthFlagsUsed, AccessTokenAuthFlagsUsed); err != nil { + if ms.ContainsPingFederateService() { + if err = initPingFederateServices(ctx, commandVersion); err != nil { return err } } - outputDir, err = fixEmptyOutputDirVar(outputDir) + overwriteExportBool, err := strconv.ParseBool(overwriteExport) if err != nil { return err } - - if err := createOrValidateOutputDir(outputDir, overwriteExport); err != nil { + if err := createOrValidateOutputDir(outputDir, overwriteExportBool); err != nil { return err } - exportableConnectors := getExportableConnectors(multiService) + exportableConnectors := getExportableConnectors(ms) - if err := exportConnectors(exportableConnectors, exportFormat, outputDir, overwriteExport); err != nil { + if err := exportConnectors(exportableConnectors, exportFormat, outputDir, overwriteExportBool); err != nil { return err } @@ -73,28 +95,62 @@ func RunInternalExport(ctx context.Context, commandVersion string, outputDir, ex Message: fmt.Sprintf("Export to directory '%s' complete.", outputDir), Result: output.ENUM_RESULT_SUCCESS, }) + return nil } -func initPingFederateServices(ctx context.Context, pingctlVersion string, basicAuthFlagsUsed, accessTokenAuthFlagsUsed bool) (err error) { +func initPingFederateServices(ctx context.Context, pingctlVersion string) (err error) { + if ctx == nil { + return fmt.Errorf("failed to initialize PingFederate services. context is nil") + } + // Get all the PingFederate configuration values - profileViper := profiles.GetProfileViper() - pfClientID := profileViper.GetString(profiles.PingFederateClientIDOption.ViperKey) - pfClientSecret := profileViper.GetString(profiles.PingFederateClientSecretOption.ViperKey) - pfTokenUrl := profileViper.GetString(profiles.PingFederateTokenURLOption.ViperKey) - pfScopes := profileViper.GetStringSlice(profiles.PingFederateScopesOption.ViperKey) - pfAccessToken := profileViper.GetString(profiles.PingFederateAccessTokenOption.ViperKey) - pfUsername := profileViper.GetString(profiles.PingFederateUsernameOption.ViperKey) - pfPassword := profileViper.GetString(profiles.PingFederatePasswordOption.ViperKey) - pfInsecureTrustAllTLS := profileViper.GetBool(profiles.PingFederateInsecureTrustAllTLSOption.ViperKey) - caCertPemFiles := profileViper.GetStringSlice(profiles.PingFederateCACertificatePemFilesOption.ViperKey) + pfClientID, err := profiles.GetOptionValue(options.PlatformExportPingfederateClientIDOption) + if err != nil { + return err + } + pfClientSecret, err := profiles.GetOptionValue(options.PlatformExportPingfederateClientSecretOption) + if err != nil { + return err + } + pfTokenUrl, err := profiles.GetOptionValue(options.PlatformExportPingfederateTokenURLOption) + if err != nil { + return err + } + pfScopes, err := profiles.GetOptionValue(options.PlatformExportPingfederateScopesOption) + if err != nil { + return err + } + pfAccessToken, err := profiles.GetOptionValue(options.PlatformExportPingfederateAccessTokenOption) + if err != nil { + return err + } + pfUsername, err := profiles.GetOptionValue(options.PlatformExportPingfederateUsernameOption) + if err != nil { + return err + } + pfPassword, err := profiles.GetOptionValue(options.PlatformExportPingfederatePasswordOption) + if err != nil { + return err + } + pfInsecureTrustAllTLS, err := profiles.GetOptionValue(options.PlatformExportPingfederateInsecureTrustAllTLSOption) + if err != nil { + return err + } + caCertPemFiles, err := profiles.GetOptionValue(options.PlatformExportPingfederateCACertificatePemFilesOption) + if err != nil { + return err + } caCertPool := x509.NewCertPool() - for _, caCertPemFile := range caCertPemFiles { + for _, caCertPemFile := range strings.Split(caCertPemFiles, ",") { + if caCertPemFile == "" { + continue + } caCertPemFile := filepath.Clean(caCertPemFile) caCert, err := os.ReadFile(caCertPemFile) if err != nil { - return fmt.Errorf("failed to read CA certificate PEM file '%s': %s", caCertPemFile, err.Error()) + return fmt.Errorf("failed to read CA certificate PEM file '%s': %v", caCertPemFile, err) } ok := caCertPool.AppendCertsFromPEM(caCert) @@ -103,9 +159,14 @@ func initPingFederateServices(ctx context.Context, pingctlVersion string, basicA } } + pfInsecureTrustAllTLSBool, err := strconv.ParseBool(pfInsecureTrustAllTLS) + if err != nil { + return err + } + tr := &http.Transport{ TLSClientConfig: &tls.Config{ - InsecureSkipVerify: pfInsecureTrustAllTLS, //#nosec G402 -- This is defined by the user (default false), and warned as inappropriate in production. + InsecureSkipVerify: pfInsecureTrustAllTLSBool, //#nosec G402 -- This is defined by the user (default false), and warned as inappropriate in production. RootCAs: caCertPool, }, } @@ -115,12 +176,12 @@ func initPingFederateServices(ctx context.Context, pingctlVersion string, basicA } switch { - case basicAuthFlagsUsed && pfUsername != "" && pfPassword != "": + case options.PlatformExportPingfederateUsernameOption.Flag.Changed && options.PlatformExportPingfederatePasswordOption.Flag.Changed: pingfederateContext = context.WithValue(ctx, pingfederateGoClient.ContextBasicAuth, pingfederateGoClient.BasicAuth{ UserName: pfUsername, Password: pfPassword, }) - case accessTokenAuthFlagsUsed && pfAccessToken != "": + case options.PlatformExportPingfederateAccessTokenOption.Flag.Changed: pingfederateContext = context.WithValue(ctx, pingfederateGoClient.ContextAccessToken, pfAccessToken) case pfClientID != "" && pfClientSecret != "" && pfTokenUrl != "": pingfederateContext = context.WithValue(ctx, pingfederateGoClient.ContextOAuth2, pingfederateGoClient.OAuthValues{ @@ -128,7 +189,7 @@ func initPingFederateServices(ctx context.Context, pingctlVersion string, basicA TokenUrl: pfTokenUrl, ClientId: pfClientID, ClientSecret: pfClientSecret, - Scopes: pfScopes, + Scopes: strings.Split(pfScopes, ","), }) case pfAccessToken != "": pingfederateContext = context.WithValue(ctx, pingfederateGoClient.ContextAccessToken, pfAccessToken) @@ -170,10 +231,18 @@ func initPingFederateApiClient(tr *http.Transport, pingctlVersion string) (err e return fmt.Errorf("failed to initialize pingfederate API client. http transport is nil") } - profileViper := profiles.GetProfileViper() - httpsHost := profileViper.GetString(profiles.PingFederateHttpsHostOption.ViperKey) - adminApiPath := profileViper.GetString(profiles.PingFederateAdminApiPathOption.ViperKey) - xBypassExternalValidationHeader := profileViper.GetBool(profiles.PingFederateXBypassExternalValidationHeaderOption.ViperKey) + httpsHost, err := profiles.GetOptionValue(options.PlatformExportPingfederateHTTPSHostOption) + if err != nil { + return err + } + adminApiPath, err := profiles.GetOptionValue(options.PlatformExportPingfederateAdminAPIPathOption) + if err != nil { + return err + } + xBypassExternalValidationHeader, err := profiles.GetOptionValue(options.PlatformExportPingfederateXBypassExternalValidationHeaderOption) + if err != nil { + return err + } // default adminApiPath to /pf-admin-api/v1 if not set if adminApiPath == "" { @@ -193,7 +262,7 @@ func initPingFederateApiClient(tr *http.Transport, pingctlVersion string) (err e pfClientConfig := pingfederateGoClient.NewConfiguration() pfClientConfig.UserAgentSuffix = &userAgent pfClientConfig.DefaultHeader["X-Xsrf-Header"] = "PingFederate" - pfClientConfig.DefaultHeader["X-BypassExternalValidation"] = strconv.FormatBool(xBypassExternalValidationHeader) + pfClientConfig.DefaultHeader["X-BypassExternalValidation"] = xBypassExternalValidationHeader pfClientConfig.Servers = pingfederateGoClient.ServerConfigurations{ { URL: httpsHost + adminApiPath, @@ -211,41 +280,29 @@ func initPingOneApiClient(ctx context.Context, pingctlVersion string) (err error l := logger.Get() l.Debug().Msgf("Initializing PingOne API client.") - profileViper := profiles.GetProfileViper() - - // Make sure the API client can be initialized with the required parameters - if !profileViper.IsSet(profiles.PingOneWorkerEnvironmentIDOption.ViperKey) || - !profileViper.IsSet(profiles.PingOneRegionOption.ViperKey) || - !profileViper.IsSet(profiles.PingOneWorkerClientIDOption.ViperKey) || - !profileViper.IsSet(profiles.PingOneWorkerClientSecretOption.ViperKey) { - return fmt.Errorf(`failed to initialize pingone API client. one of worker environment ID, worker client ID, worker client secret, and/or pingone region is not set. configure these properties via parameter flags, environment variables, or the tool's configuration file (default: $HOME/.pingctl/config.yaml)`) + if ctx == nil { + return fmt.Errorf("failed to initialize pingone API client. context is nil") } - pingoneApiClientId = profileViper.GetString(profiles.PingOneWorkerClientIDOption.ViperKey) - clientSecret := profileViper.GetString(profiles.PingOneWorkerClientSecretOption.ViperKey) - environmentID := profileViper.GetString(profiles.PingOneWorkerEnvironmentIDOption.ViperKey) - region := profileViper.Get(profiles.PingOneRegionOption.ViperKey) - - var regionStr string - switch regionVal := region.(type) { - case string: - regionStr = regionVal - case customtypes.PingOneRegion: - regionStr = string(regionVal) - default: - return fmt.Errorf("failed to initialize pingone API client. unrecognized pingone region variable type: %T", region) + pingoneApiClientId, err = profiles.GetOptionValue(options.PlatformExportPingoneWorkerClientIDOption) + if err != nil { + return err } - - switch regionStr { - case customtypes.ENUM_PINGONE_REGION_AP, customtypes.ENUM_PINGONE_REGION_CA, customtypes.ENUM_PINGONE_REGION_EU, customtypes.ENUM_PINGONE_REGION_NA: - l.Debug().Msgf("pingone region '%s' validated.", regionStr) - default: - return fmt.Errorf("failed to initialize pingone API client. unrecognized pingone region: '%s'. Must be one of: %s", regionStr, strings.Join(customtypes.PingOneRegionValidValues(), ", ")) + clientSecret, err := profiles.GetOptionValue(options.PlatformExportPingoneWorkerClientSecretOption) + if err != nil { + return err + } + environmentID, err := profiles.GetOptionValue(options.PlatformExportPingoneWorkerEnvironmentIDOption) + if err != nil { + return err + } + region, err := profiles.GetOptionValue(options.PlatformExportPingoneRegionOption) + if err != nil { + return err } - // Make sure the client credentials are not empty - if pingoneApiClientId == "" || clientSecret == "" || environmentID == "" { - return fmt.Errorf(`failed to initialize pingone API client. one of worker client ID, worker client secret, and/or worker environment ID is empty. configure these properties via parameter flags, environment variables, or the tool's configuration file (default: $HOME/.pingctl/config.yaml)`) + if pingoneApiClientId == "" || clientSecret == "" || environmentID == "" || region == "" { + return fmt.Errorf(`failed to initialize pingone API client. one of worker client ID, worker client secret, pingone region, and/or worker environment ID is empty. configure these properties via parameter flags, environment variables, or the tool's configuration file (default: $HOME/.pingctl/config.yaml)`) } userAgent := fmt.Sprintf("pingctl/%s", pingctlVersion) @@ -258,7 +315,7 @@ func initPingOneApiClient(ctx context.Context, pingctlVersion string) (err error ClientID: &pingoneApiClientId, ClientSecret: &clientSecret, EnvironmentID: &environmentID, - Region: regionStr, + Region: region, UserAgentSuffix: &userAgent, } @@ -285,26 +342,6 @@ pingone region - %s return nil } -func fixEmptyOutputDirVar(outputDir string) (newOutputDir string, err error) { - if outputDir == "" { - // Default the outputDir variable to the user's present working directory. - outputDir, err = os.Getwd() - if err != nil { - return "", fmt.Errorf("failed to determine user's present working directory: %s", err.Error()) - } - - // Append "export" to the output directory as export needs an empty directory to write to - outputDir = filepath.Join(outputDir, "export") - - output.Print(output.Opts{ - Message: fmt.Sprintf("Defaulting 'platform export' command output directory to '%s'", outputDir), - Result: output.ENUM_RESULT_NOACTION_WARN, - }) - } - - return outputDir, nil -} - func createOrValidateOutputDir(outputDir string, overwriteExport bool) (err error) { l := logger.Get() @@ -347,16 +384,18 @@ func createOrValidateOutputDir(outputDir string, overwriteExport bool) (err erro } func getPingOneExportEnvID() (err error) { - profileViper := profiles.GetProfileViper() + pingoneExportEnvID, err = profiles.GetOptionValue(options.PlatformExportPingoneExportEnvironmentIDOption) + if err != nil { + return err + } - // Find the env ID to export. Default to worker env id if not provided by user. - pingoneExportEnvID = profileViper.GetString(profiles.PingOneExportEnvironmentIDOption.ViperKey) if pingoneExportEnvID == "" { - pingoneExportEnvID = profileViper.GetString(profiles.PingOneWorkerEnvironmentIDOption.ViperKey) - - // if the exportEnvID is still empty, this is a problem. Return error. + pingoneExportEnvID, err = profiles.GetOptionValue(options.PlatformExportPingoneWorkerEnvironmentIDOption) + if err != nil { + return err + } if pingoneExportEnvID == "" { - return fmt.Errorf("failed to determine pingone export environment ID") + return fmt.Errorf("failed to determine pingone export environment ID.") } output.Print(output.Opts{ @@ -401,7 +440,7 @@ func getExportableConnectors(multiService *customtypes.MultiService) (exportable return &connectors } - for _, service := range *multiService.GetServices() { + for _, service := range multiService.GetServices() { switch service { case customtypes.ENUM_SERVICE_PINGONE_PLATFORM: connectors = append(connectors, platform.PlatformConnector(pingoneContext, pingoneApiClient, &pingoneApiClientId, pingoneExportEnvID)) @@ -433,7 +472,7 @@ func exportConnectors(exportableConnectors *[]connector.Exportable, exportFormat Result: output.ENUM_RESULT_NIL, }) - err := connector.Export(string(exportFormat), outputDir, overwriteExport) + err := connector.Export(exportFormat, outputDir, overwriteExport) if err != nil { return fmt.Errorf("failed to export '%s' service: %s", connector.ConnectorServiceName(), err.Error()) } diff --git a/internal/commands/platform/export_internal_test.go b/internal/commands/platform/export_internal_test.go index 018edb8..521bbfb 100644 --- a/internal/commands/platform/export_internal_test.go +++ b/internal/commands/platform/export_internal_test.go @@ -3,15 +3,13 @@ package platform_internal import ( "context" "crypto/tls" - "fmt" "net/http" "os" "regexp" "testing" - "github.com/hashicorp/go-uuid" + "github.com/pingidentity/pingctl/internal/configuration/options" "github.com/pingidentity/pingctl/internal/connector" - "github.com/pingidentity/pingctl/internal/connector/pingfederate" "github.com/pingidentity/pingctl/internal/customtypes" "github.com/pingidentity/pingctl/internal/profiles" "github.com/pingidentity/pingctl/internal/testing/testutils" @@ -23,14 +21,15 @@ import ( func TestRunInternalExport(t *testing.T) { testutils_viper.InitVipers(t) - // Create a directory in the temp directory - outputDir := t.TempDir() - - // Run the export command - err := RunInternalExport(context.Background(), "v1.2.3", outputDir, connector.ENUMEXPORTFORMAT_HCL, true, customtypes.NewMultiService(), false, false) + err := RunInternalExport(context.Background(), "v1.2.3") testutils.CheckExpectedError(t, err, nil) // Check if there are terraform files in the export directory + outputDir, err := profiles.GetOptionValue(options.PlatformExportOutputDirectoryOption) + if err != nil { + t.Fatalf("profiles.GetOptionValue() error = %v", err) + } + files, err := os.ReadDir(outputDir) if err != nil { t.Fatalf("os.ReadDir() error = %v", err) @@ -54,366 +53,53 @@ func TestRunInternalExport(t *testing.T) { } } -// Test RunInternalExport function fails on invalid output directory -func TestRunInternalExport_invalidOutputDir(t *testing.T) { - testutils_viper.InitVipers(t) - - expectedErrorPattern := `^failed to create 'platform export' output directory '/invalid': mkdir /invalid:.*$` - err := RunInternalExport(context.Background(), "v1.2.3", "/invalid", connector.ENUMEXPORTFORMAT_HCL, true, customtypes.NewMultiService(), false, false) +// Test RunInternalExport function fails with nil context +func TestRunInternalExportNilContext(t *testing.T) { + expectedErrorPattern := `^failed to run 'platform export' command\. context is nil$` + err := RunInternalExport(nil, "v1.2.3") //nolint:staticcheck // SA1012 this is a test testutils.CheckExpectedError(t, err, &expectedErrorPattern) } -// Test RunInternalExport function fails on invalid export format -func TestRunInternalExport_invalidExportFormat(t *testing.T) { - testutils_viper.InitVipers(t) - - // Create a directory in the temp directory - outputDir := t.TempDir() - - expectedErrorPattern := `^failed to export '.*' service: unrecognized export format "invalid". Must be one of: \[.*\]$` - err := RunInternalExport(context.Background(), "v1.2.3", outputDir, "invalid", true, customtypes.NewMultiService(), false, false) - testutils.CheckExpectedError(t, err, &expectedErrorPattern) -} - -// Test RunInternalExport function fails nil context -func TestRunInternalExport_nilContext(t *testing.T) { - testutils_viper.InitVipers(t) - - // Create a directory in the temp directory - outputDir := t.TempDir() - - expectedErrorPattern := `^failed to run 'platform export' command. context is nil$` - // nolint:staticcheck // ignore SA1012 this is a test - err := RunInternalExport(nil, "v1.2.3", outputDir, connector.ENUMEXPORTFORMAT_HCL, true, customtypes.NewMultiService(), false, false) //lint:ignore SA1012 this is a test - testutils.CheckExpectedError(t, err, &expectedErrorPattern) -} - -// Test RunInternalExport function fails when not overwriting a populated output directory -func TestRunInternalExport_notOverwriting(t *testing.T) { - testutils_viper.InitVipers(t) - - // Create a directory in the temp directory - outputDir := t.TempDir() - - // Create a file in the new directory - file := outputDir + "/file" - if _, err := os.Create(file); err != nil { - t.Fatalf("os.Create() error = %v", err) - } - - expectedErrorPattern := `'platform export' output directory '.*' is not empty. Use --overwrite to overwrite existing export data$` - err := RunInternalExport(context.Background(), "v1.2.3", outputDir, connector.ENUMEXPORTFORMAT_HCL, false, customtypes.NewMultiService(), false, false) - testutils.CheckExpectedError(t, err, &expectedErrorPattern) -} - -// Test RunInternalExport function succeeds on nil multiService -func TestRunInternalExport_nilMultiService(t *testing.T) { - testutils_viper.InitVipers(t) - - // Create a directory in the temp directory - outputDir := t.TempDir() - - err := RunInternalExport(context.Background(), "v1.2.3", outputDir, connector.ENUMEXPORTFORMAT_HCL, true, nil, false, false) - testutils.CheckExpectedError(t, err, nil) - - // Check if there are terraform files in the export directory - files, err := os.ReadDir(outputDir) - if err != nil { - t.Fatalf("os.ReadDir() error = %v", err) - } - - // Check the number of files in the directory - if len(files) != 0 { - t.Errorf("RunInternalExport() num files = %v, want 0", len(files)) - } -} - // Test initPingFederateServices function -func Test_initPingFederateServices(t *testing.T) { +func TestInitPingFederateServices(t *testing.T) { testutils_viper.InitVipers(t) - // Test the function - err := initPingFederateServices(context.Background(), "v1.2.3", false, false) + err := initPingFederateServices(context.Background(), "v1.2.3") testutils.CheckExpectedError(t, err, nil) - // Check the API client is not nil - if pingfederateApiClient == nil { - t.Errorf("initPingFederateServices() apiClient = %v, want non-nil", pingfederateApiClient) - } - - // Check the context is not nil + // make sure pf context is not nil if pingfederateContext == nil { - t.Errorf("initPingFederateServices() context = %v, want non-nil", pingfederateContext) + t.Errorf("initPingFederateServices() pingfederateContext = %v, want non-nil", pingfederateContext) } -} - -// Test initPingFederateServices function with basic auth used -func Test_initPingFederateServices_basicAuth(t *testing.T) { - testutils_viper.InitVipers(t) - // Test the function - err := initPingFederateServices(context.Background(), "v1.2.3", true, false) - testutils.CheckExpectedError(t, err, nil) - - // Check the API client is not nil - if pingfederateApiClient == nil { - t.Fatalf("initPingFederateServices() apiClient = %v, want non-nil", pingfederateApiClient) + // check pf context has auth values included + if pingfederateContext.Value(pingfederateGoClient.ContextOAuth2) == nil { + t.Errorf("initPingFederateServices() pingfederateContext.Value = %v, want non-nil", pingfederateContext.Value(pingfederateGoClient.ContextOAuth2)) } - - // Check the context is not nil - if pingfederateContext == nil { - t.Fatalf("initPingFederateServices() context = %v, want non-nil", pingfederateContext) - } - - // Check context has basic auth value - basicAuthInfo := pingfederateContext.Value(pingfederateGoClient.ContextBasicAuth) - if basicAuthInfo == nil { - t.Fatalf("initPingFederateServices() basicAuthInfo = %v, want non-nil", basicAuthInfo) - } - - // Check auth username and password are not empty - basicAuth, ok := basicAuthInfo.(pingfederateGoClient.BasicAuth) - if !ok { - t.Fatalf("initPingFederateServices() basicAuth = %v, want PF go-client BasicAuth struct.", basicAuth) - } - if basicAuth.UserName == "" || basicAuth.Password == "" { - t.Errorf("initPingFederateServices() basicAuth = %v, want non-empty username and password", basicAuth) - } - - if basicAuth.UserName != os.Getenv(profiles.PingFederateUsernameOption.EnvVar) { - t.Errorf("initPingFederateServices() basicAuth.UserName = %v, want %v", basicAuth.UserName, os.Getenv(profiles.PingFederateUsernameOption.EnvVar)) - } - - if basicAuth.Password != os.Getenv(profiles.PingFederatePasswordOption.EnvVar) { - t.Errorf("initPingFederateServices() basicAuth.Password = %v, want %v", basicAuth.Password, os.Getenv(profiles.PingFederatePasswordOption.EnvVar)) - } -} - -// Test initPingFederateServices function with client credentials used -func Test_initPingFederateServices_clientCredentials(t *testing.T) { - testutils_viper.InitVipers(t) - - // Test the function - err := initPingFederateServices(context.Background(), "v1.2.3", false, false) - testutils.CheckExpectedError(t, err, nil) - - // Check the API client is not nil - if pingfederateApiClient == nil { - t.Fatalf("initPingFederateServices() apiClient = %v, want non-nil", pingfederateApiClient) - } - - // Check the context is not nil - if pingfederateContext == nil { - t.Fatalf("initPingFederateServices() context = %v, want non-nil", pingfederateContext) - } - - // Check context has basic auth value - ccAuthInfo := pingfederateContext.Value(pingfederateGoClient.ContextOAuth2) - if ccAuthInfo == nil { - t.Fatalf("initPingFederateServices() ccAuthInfo = %v, want non-nil", ccAuthInfo) - } - - ccAuth, ok := ccAuthInfo.(pingfederateGoClient.OAuthValues) - if !ok { - t.Fatalf("initPingFederateServices() ccAuth = %v, want PF go-client OAuthValues struct.", ccAuth) - } - - // Check client ID and secret are not empty - if ccAuth.ClientId == "" || ccAuth.ClientSecret == "" { - t.Errorf("initPingFederateServices() ccAuth = %v, want non-empty client ID and secret", ccAuth) - } - - if ccAuth.ClientId != os.Getenv(profiles.PingFederateClientIDOption.EnvVar) { - t.Errorf("initPingFederateServices() ccAuth.ClientId = %v, want %v", ccAuth.ClientId, os.Getenv(profiles.PingFederateClientIDOption.EnvVar)) - } - - if ccAuth.ClientSecret != os.Getenv(profiles.PingFederateClientSecretOption.EnvVar) { - t.Errorf("initPingFederateServices() ccAuth.ClientSecret = %v, want %v", ccAuth.ClientSecret, os.Getenv(profiles.PingFederateClientSecretOption.EnvVar)) - } -} - -// Test initPingFederateServices function fails when no auth method is provided -func Test_initPingFederateServices_noAuth(t *testing.T) { - testutils_viper.InitVipersCustomFile(t, fmt.Sprintf(`activeProfile: default -default: - description: "default description" - pingctl: - color: true - outputFormat: text - pingone: - export: - environmentid: "" - region: "" - worker: - clientid: "" - clientsecret: "" - environmentid: "" - pingfederate: - accesstokenauth: - accesstoken: "" - adminapipath: "" - basicauth: - password: "" - username: "" - clientcredentialsauth: - clientid: "" - clientsecret: "" - scopes: "" - tokenurl: "" - httpshost: "%s"`, - os.Getenv(profiles.PingFederateHttpsHostOption.EnvVar))) - - expectedErrorPattern := `^failed to initialize PingFederate API client\. none of the following sets of authentication configuration values are set: OAuth2 client credentials \(client ID, client secret, token URL\), Access token, or Basic Authentication credentials \(username, password\)\. configure these properties via parameter flags, environment variables, or the tool\'s configuration file \(default: \$HOME/\.pingctl/config\.yaml\)$` - err := initPingFederateServices(context.Background(), "v1.2.3", false, false) - testutils.CheckExpectedError(t, err, &expectedErrorPattern) } -// Test initPingFederateServices function fails when no https host is provided -func Test_initPingFederateServices_noHttpsHost(t *testing.T) { - testutils_viper.InitVipersCustomFile(t, `activeProfile: default -default: - description: "default description" - pingctl: - color: true - outputFormat: text - pingone: - export: - environmentid: "" - region: "" - worker: - clientid: "" - clientsecret: "" - environmentid: "" - pingfederate: - accesstokenauth: - accesstoken: "" - adminapipath: "" - basicauth: - password: "" - username: "" - clientcredentialsauth: - clientid: "" - clientsecret: "" - scopes: "" - tokenurl: "" - httpshost: ""`) - - expectedErrorPattern := `^failed to initialize pingfederate API client\. the pingfederate https host configuration value is not set: configure this property via parameter flags, environment variables, or the tool\'s configuration file \(default: \$HOME/\.pingctl/config\.yaml\)$` - err := initPingFederateServices(context.Background(), "v1.2.3", false, false) +// Test initPingFederateServices function fails with nil context +func TestInitPingFederateServicesNilContext(t *testing.T) { + expectedErrorPattern := `^failed to initialize PingFederate services\. context is nil$` + err := initPingFederateServices(nil, "v1.2.3") //nolint:staticcheck // SA1012 this is a test testutils.CheckExpectedError(t, err, &expectedErrorPattern) } // Test initPingOneServices function -func Test_initPingOneServices(t *testing.T) { +func TestInitPingOneServices(t *testing.T) { testutils_viper.InitVipers(t) - // Test the function err := initPingOneServices(context.Background(), "v1.2.3") testutils.CheckExpectedError(t, err, nil) - // Check the API client is not nil - if pingoneApiClient == nil { - t.Errorf("initPingOneServices() apiClient = %v, want non-nil", pingoneApiClient) - } - - // Check the API client ID is not empty - if pingoneApiClientId == "" { - t.Errorf("initPingOneServices() apiClientId = '%s', want non-empty", pingoneApiClientId) - } - - // Check the API export environment ID is not empty - if pingoneExportEnvID == "" { - t.Errorf("initPingOneServices() exportEnvID = '%s', want non-empty", pingoneExportEnvID) - } - - // Check the context is not nil + // make sure po context is not nil if pingoneContext == nil { - t.Errorf("initPingOneServices() context = %v, want non-nil", pingoneContext) - } - - // Check the API client ID is a valid UUID - if _, err := uuid.ParseUUID(pingoneApiClientId); err != nil { - t.Errorf("initPingOneServices() api clientId = '%s', want valid UUID", pingoneApiClientId) + t.Errorf("initPingOneServices() pingoneContext = %v, want non-nil", pingoneContext) } - - // Check the API export environment ID is a valid UUID - if _, err := uuid.ParseUUID(pingoneExportEnvID); err != nil { - t.Errorf("initPingOneServices() exportEnvID = '%s', want valid UUID", pingoneExportEnvID) - } -} - -// Test initPingOneServices function fails with no region -func Test_initPingOneServices_noPingOneRegion(t *testing.T) { - testutils_viper.InitVipersCustomFile(t, `activeProfile: default -default: - description: "default description" - pingctl: - color: true - outputFormat: text - pingone: - export: - environmentid: "" - region: "" - worker: - clientid: "" - clientsecret: "" - environmentid: "" - pingfederate: - accesstokenauth: - accesstoken: "" - adminapipath: "" - basicauth: - password: "" - username: "" - clientcredentialsauth: - clientid: "" - clientsecret: "" - scopes: "" - tokenurl: "" - httpshost: ""`) - - expectedErrorPattern := `^failed to initialize pingone API client\. unrecognized pingone region: ''. Must be one of:.*$` - err := initPingOneServices(context.Background(), "v1.2.3") - testutils.CheckExpectedError(t, err, &expectedErrorPattern) -} - -// Test initPingOneServices function fails with no client credentials -func Test_initPingOneServices_noPingOneClientCredentials(t *testing.T) { - testutils_viper.InitVipersCustomFile(t, fmt.Sprintf(`activeProfile: default -default: - description: "default description" - pingctl: - color: true - outputFormat: text - pingone: - export: - environmentid: "" - region: "%s" - worker: - clientid: "" - clientsecret: "" - environmentid: "" - pingfederate: - accesstokenauth: - accesstoken: "" - adminapipath: "" - basicauth: - password: "" - username: "" - clientcredentialsauth: - clientid: "" - clientsecret: "" - scopes: "" - tokenurl: "" - httpshost: ""`, os.Getenv(profiles.PingOneRegionOption.EnvVar))) - - expectedErrorPattern := `^failed to initialize pingone API client\. one of worker client ID, worker client secret, and/or worker environment ID is empty\. configure these properties via parameter flags, environment variables, or the tool's configuration file \(default: \$HOME/\.pingctl/config\.yaml\)$` - err := initPingOneServices(context.Background(), "v1.2.3") - testutils.CheckExpectedError(t, err, &expectedErrorPattern) } // Test initPingFederateApiClient function -func Test_initPingFederateApiClient(t *testing.T) { +func TestInitPingFederateApiClient(t *testing.T) { testutils_viper.InitVipers(t) tr := &http.Transport{ @@ -422,436 +108,241 @@ func Test_initPingFederateApiClient(t *testing.T) { }, } - // Test the function err := initPingFederateApiClient(tr, "v1.2.3") testutils.CheckExpectedError(t, err, nil) - // Check the API client is not nil + // make sure pf client is not nil if pingfederateApiClient == nil { - t.Errorf("initPingFederateApiClient() apiClient = %v, want non-nil", pingfederateApiClient) - } - - // Check the API client has a valid http client - if pingfederateApiClient.GetConfig().HTTPClient == nil { - t.Errorf("initPingFederateApiClient() httpClient = %v, want non-nil", pingfederateApiClient.GetConfig().HTTPClient) - } - - // Check the API client has a valid http transport - if pingfederateApiClient.GetConfig().HTTPClient.Transport == nil { - t.Errorf("initPingFederateApiClient() httpTransport = %v, want non-nil", pingfederateApiClient.GetConfig().HTTPClient.Transport) - } - - // Check the API client has a valid default "X-Xsrf-Header" header - if pingfederateApiClient.GetConfig().DefaultHeader["X-Xsrf-Header"] != "PingFederate" { - t.Errorf("initPingFederateApiClient() defaultHeader = %v, want 'PingFederate'", pingfederateApiClient.GetConfig().DefaultHeader["X-Xsrf-Header"]) - } - - // Check the API client Servers - if len(pingfederateApiClient.GetConfig().Servers) == 0 { - t.Errorf("initPingFederateApiClient() servers = %v, want non-empty", pingfederateApiClient.GetConfig().Servers) - } - - if pingfederateApiClient.GetConfig().Servers[0].URL != os.Getenv(profiles.PingFederateHttpsHostOption.EnvVar)+os.Getenv(profiles.PingFederateAdminApiPathOption.EnvVar) { - t.Errorf("initPingFederateApiClient() server URL = %v, want %v", pingfederateApiClient.GetConfig().Servers[0].URL, os.Getenv(profiles.PingFederateHttpsHostOption.EnvVar)+os.Getenv(profiles.PingFederateAdminApiPathOption.EnvVar)) + t.Errorf("initPingFederateApiClient() pingfederateApiClient = %v, want non-nil", pingfederateApiClient) } } -// Test initPingFederateApiClient function fails on missing https host -func Test_initPingFederateApiClient_noHttpsHost(t *testing.T) { - testutils_viper.InitVipersCustomFile(t, `activeProfile: default -default: - description: "default description" - pingctl: - color: true - outputFormat: text - pingone: - export: - environmentid: "" - region: "" - worker: - clientid: "" - clientsecret: "" - environmentid: "" - pingfederate: - accesstokenauth: - accesstoken: "" - adminapipath: "" - basicauth: - password: "" - username: "" - clientcredentialsauth: - clientid: "" - clientsecret: "" - scopes: "" - tokenurl: "" - httpshost: ""`) - - expectedErrorPattern := `^failed to initialize pingfederate API client\. the pingfederate https host configuration value is not set: configure this property via parameter flags, environment variables, or the tool's configuration file \(default: \$HOME/\.pingctl/config\.yaml\)$` - err := initPingFederateApiClient(&http.Transport{}, "v1.2.3") - testutils.CheckExpectedError(t, err, &expectedErrorPattern) -} - -// Test initPingFederateApiClient function fails on nil transport -func Test_initPingFederateApiClient_nilTransport(t *testing.T) { +// Test initPingFederateApiClient function fails with nil transport +func TestInitPingFederateApiClientNilTransport(t *testing.T) { expectedErrorPattern := `^failed to initialize pingfederate API client\. http transport is nil$` err := initPingFederateApiClient(nil, "v1.2.3") testutils.CheckExpectedError(t, err, &expectedErrorPattern) } // Test initPingOneApiClient function -func Test_initPingOneApiClient(t *testing.T) { +func TestInitPingOneApiClient(t *testing.T) { testutils_viper.InitVipers(t) - // Test the function err := initPingOneApiClient(context.Background(), "v1.2.3") testutils.CheckExpectedError(t, err, nil) - // Check the API client is not nil + // make sure po client is not nil if pingoneApiClient == nil { - t.Errorf("initPingOneApiClient() apiClient = %v, want non-nil", pingoneApiClient) - } - - if pingoneApiClient.ManagementAPIClient.GetConfig().HTTPClient == nil { - t.Errorf("initPingOneApiClient() httpClient = %v, want non-nil", pingoneApiClient.ManagementAPIClient.GetConfig().HTTPClient) - } - - // Check the API client id is not empty - if pingoneApiClientId == "" { - t.Errorf("initPingOneApiClient() api client id = '%s', want non-empty", pingoneApiClientId) + t.Errorf("initPingOneApiClient() pingoneApiClient = %v, want non-nil", pingoneApiClient) } } -// Test fixEmptyOutputDirVar function -func Test_fixEmptyOutputDirVar(t *testing.T) { - // Test the function - outputDir, err := fixEmptyOutputDirVar("") - if err != nil { - t.Errorf("fixEmptyOutputDirVar() error = %v", err) - } - if outputDir == "" { - t.Errorf("fixEmptyOutputDirVar() outputDir = %v, want non-empty", outputDir) - } +// Test initPingOneApiClient function fails with nil context +func TestInitPingOneApiClientNilContext(t *testing.T) { + expectedErrorPattern := `^failed to initialize pingone API client\. context is nil$` + err := initPingOneApiClient(nil, "v1.2.3") //nolint:staticcheck // SA1012 this is a test + testutils.CheckExpectedError(t, err, &expectedErrorPattern) } -// Test fixEmptyOutputDirVar function with non-empty output directory -func Test_fixEmptyOutputDirVar_nonEmpty(t *testing.T) { - // Test the function - outputDir, err := fixEmptyOutputDirVar("test") - if err != nil { - t.Errorf("fixEmptyOutputDirVar() error = %v", err) - } - if outputDir != "test" { - t.Errorf("fixEmptyOutputDirVar() outputDir = %v, want 'test'", outputDir) - } -} +// Test createOrValidateOutputDir function with non-existent directory +func TestCreateOrValidateOutputDir(t *testing.T) { + testutils_viper.InitVipers(t) -// Test createOrValidateOutputDir function -// - Empty directory that exists is valid, and should not return an error -func Test_createOrValidateOutputDir_emptyDir(t *testing.T) { - // Create a directory in the temp directory - outputDir := t.TempDir() + outputDir := os.TempDir() + "/nonexistantdir" - // Test the function err := createOrValidateOutputDir(outputDir, false) testutils.CheckExpectedError(t, err, nil) - - err = createOrValidateOutputDir(outputDir, true) - testutils.CheckExpectedError(t, err, nil) } -// Test createOrValidateOutputDir function -// - Create an empty directory that does exist -// - Add a file to the directory -// - Validate that the function returns an error with overwrite set to false -func Test_createOrValidateOutputDir_existingDir(t *testing.T) { - // Create a directory in the temp directory - outputDir := t.TempDir() +// Test createOrValidateOutputDir function with existent directory +func TestCreateOrValidateOutputDirExistentDir(t *testing.T) { + testutils_viper.InitVipers(t) - // Create a file in the new directory - file := outputDir + "/file" - if _, err := os.Create(file); err != nil { - t.Fatalf("os.Create() error = %v", err) - } + outputDir := t.TempDir() - // Test the function - expectedErrorPattern := `^'platform export' output directory '.*' is not empty. Use --overwrite to overwrite existing export data$` err := createOrValidateOutputDir(outputDir, false) - testutils.CheckExpectedError(t, err, &expectedErrorPattern) - - err = createOrValidateOutputDir(outputDir, true) testutils.CheckExpectedError(t, err, nil) } -// Test createOrValidateOutputDir function -// - Provide function with a directory that does not exist -// - Validate that the function creates the directory -func Test_createOrValidateOutputDir_nonExistingDir(t *testing.T) { - // Create a directory in the temp directory - outputDir := t.TempDir() + "/new" +// Test createOrValidateOutputDir function with existent directory and overwrite flag +// when there is a file in the directory +func TestCreateOrValidateOutputDirExistentDirWithFile(t *testing.T) { + testutils_viper.InitVipers(t) - // Test the function - err := createOrValidateOutputDir(outputDir, false) - testutils.CheckExpectedError(t, err, nil) + outputDir := t.TempDir() - // Check if the directory was created - if _, err := os.Stat(outputDir); os.IsNotExist(err) { - t.Errorf("createOrValidateOutputDir() directory = %v, want exists", outputDir) + file, err := os.Create(outputDir + "/file") + if err != nil { + t.Fatalf("os.Create() error = %v", err) } + file.Close() err = createOrValidateOutputDir(outputDir, true) testutils.CheckExpectedError(t, err, nil) } -// Test getPingOneExportEnvID function -func Test_getPingOneExportEnvID(t *testing.T) { +// Test createOrValidateOutputDir function fails with existent directory and no overwrite flag +// when there is a file in the directory +func TestCreateOrValidateOutputDirExistentDirWithFileNoOverwrite(t *testing.T) { testutils_viper.InitVipers(t) - // Test the function - err := getPingOneExportEnvID() - testutils.CheckExpectedError(t, err, nil) + outputDir := t.TempDir() - // Check the export environment ID is not empty - if pingoneExportEnvID == "" { - t.Errorf("getPingOneExportEnvID() pingoneExportEnvID = '%s', want non-empty", pingoneExportEnvID) + file, err := os.Create(outputDir + "/file") + if err != nil { + t.Fatalf("os.Create() error = %v", err) } -} + file.Close() -// Test getPingOneExportEnvID function fails with no export environment ID -func Test_getPingOneExportEnvID_noExportEnvID(t *testing.T) { - testutils_viper.InitVipersCustomFile(t, `activeProfile: default -default: - description: "default description" - pingctl: - color: true - outputFormat: text - pingone: - export: - environmentid: "" - region: "" - worker: - clientid: "" - clientsecret: "" - environmentid: "" - pingfederate: - accesstokenauth: - accesstoken: "" - adminapipath: "" - basicauth: - password: "" - username: "" - clientcredentialsauth: - clientid: "" - clientsecret: "" - scopes: "" - tokenurl: "" - httpshost: ""`) - - expectedErrorPattern := `^failed to determine pingone export environment ID$` - err := getPingOneExportEnvID() + expectedErrorPattern := `^'platform export' output directory '.*' is not empty\. Use --overwrite to overwrite existing export data$` + err = createOrValidateOutputDir(outputDir, false) testutils.CheckExpectedError(t, err, &expectedErrorPattern) } -// Test validatePingOneExportEnvID function -func Test_validatePingOneExportEnvID(t *testing.T) { +// Test getPingOneExportEnvID function +func TestGetPingOneExportEnvID(t *testing.T) { testutils_viper.InitVipers(t) - // init the api client - err := initPingOneApiClient(context.Background(), "v1.2.3") - if err != nil { - t.Fatalf("initPingOneApiClient() error = %v", err) + if err := getPingOneExportEnvID(); err != nil { + t.Errorf("getPingOneExportEnvID() error = %v, want nil", err) } - // Test the function - err = validatePingOneExportEnvID(context.Background()) - testutils.CheckExpectedError(t, err, nil) + // Check pingoneExportEnvID is not empty + if pingoneExportEnvID == "" { + t.Errorf("getPingOneExportEnvID() pingoneExportEnvID = %v, want non-empty", pingoneExportEnvID) + } } -// Test validatePingOneExportEnvID function fails with invalid export environment ID -func Test_validatePingOneExportEnvID_invalidExportEnvID(t *testing.T) { +// Test validatePingOneExportEnvID function +func TestValidatePingOneExportEnvID(t *testing.T) { testutils_viper.InitVipers(t) - // init the api client - err := initPingOneApiClient(context.Background(), "v1.2.3") - if err != nil { - t.Fatalf("initPingOneApiClient() error = %v", err) + if err := initPingOneApiClient(context.Background(), "v1.2.3"); err != nil { + t.Errorf("initPingOneApiClient() error = %v, want nil", err) } - // Set the export environment ID to an invalid value - pingoneExportEnvID, err = uuid.GenerateUUID() - if err != nil { - t.Fatalf("uuid.GenerateUUID() error = %v", err) + if err := getPingOneExportEnvID(); err != nil { + t.Errorf("getPingOneExportEnvID() error = %v, want nil", err) } - expectedErrorPattern := `(?s)^.* Request for resource '.*' was not successful\..*Response Code: 404 Not Found.*Response Body: {{.*"id" : ".*",.*"code" : "NOT_FOUND",.*"message" : "Unable to find environment with ID: '.*'".*}}.*Error: 404 Not Found$` - err = validatePingOneExportEnvID(context.Background()) - testutils.CheckExpectedError(t, err, &expectedErrorPattern) - - // Reset the export environment ID - pingoneExportEnvID = "" + err := validatePingOneExportEnvID(context.Background()) + testutils.CheckExpectedError(t, err, nil) } // Test validatePingOneExportEnvID function fails with nil context -func Test_validatePingOneExportEnvID_nilContext(t *testing.T) { - testutils_viper.InitVipers(t) - - if err := initPingOneServices(context.Background(), "v1.2.3"); err != nil { - t.Fatalf("initPingOneServices() error = %v", err) - } - - pingoneContext = nil - - expectedErrorPattern := `^failed to validate pingone environment ID '.*'. context is nil$` - // nolint:staticcheck // ignore SA1012 this is a test - err := validatePingOneExportEnvID(nil) //lint:ignore SA1012 this is a test - testutils.CheckExpectedError(t, err, &expectedErrorPattern) -} - -// Test validatePingOneExportEnvID function fails with nil api client -func Test_validatePingOneExportEnvID_nilApiClient(t *testing.T) { - testutils_viper.InitVipers(t) - - if err := initPingOneServices(context.Background(), "v1.2.3"); err != nil { - t.Fatalf("initPingOneServices() error = %v", err) - } - - pingoneApiClient = nil - - expectedErrorPattern := `^failed to validate pingone environment ID '.*'. apiClient is nil$` - err := validatePingOneExportEnvID(context.Background()) +func TestValidatePingOneExportEnvIDNilContext(t *testing.T) { + expectedErrorPattern := `^failed to validate pingone environment ID '.*'\. context is nil$` + err := validatePingOneExportEnvID(nil) //nolint:staticcheck // SA1012 this is a test testutils.CheckExpectedError(t, err, &expectedErrorPattern) } // Test getExportableConnectors function -func Test_getExportableConnectors(t *testing.T) { - // Test the function - connectors := getExportableConnectors(customtypes.NewMultiService()) - if connectors == nil { - t.Errorf("getExportableConnectors() connectors = %v, want non-nil", connectors) - } +func TestGetExportableConnectors(t *testing.T) { + testutils_viper.InitVipers(t) - // Check the number of connectors - if len(*connectors) == 0 { - t.Errorf("getExportableConnectors() num connectors = %v, want non-zero", len(*connectors)) - } + ms := customtypes.NewMultiService() - // Check the connectors are not nil - for _, connector := range *connectors { - if connector == nil { - t.Errorf("getExportableConnectors() connector = %v, want non-nil", connector) - } - } + expectedConnectors := len(ms.GetServices()) - if len(customtypes.MultiServiceValidValues()) != len(*connectors) { - t.Errorf("getExportableConnectors() num connectors = %v, want %v", len(*connectors), len(customtypes.MultiServiceValidValues())) + exportableConnectors := getExportableConnectors(ms) + if len(*exportableConnectors) == 0 { + t.Errorf("getExportableConnectors() exportableConnectors = %v, want non-empty", exportableConnectors) } -} -// Test getExportableConnectors function with nil multiService -func Test_getExportableConnectors_nilMultiService(t *testing.T) { - // Test the function - connectors := getExportableConnectors(nil) - if len(*connectors) != 0 { - t.Errorf("getExportableConnectors() num connectors = %v, want 0", len(*connectors)) + if len(*exportableConnectors) != expectedConnectors { + t.Errorf("getExportableConnectors() exportableConnectors = %v, want %v", len(*exportableConnectors), expectedConnectors) } } -// Test getExportableConnectors function with one service -func Test_getExportableConnectors_oneService(t *testing.T) { - ms := customtypes.NewMultiService() - if err := ms.Set(customtypes.ENUM_SERVICE_PINGFEDERATE); err != nil { - t.Fatalf("ms.Set() error = %v", err) - } - - // Test the function - connectors := getExportableConnectors(ms) - if len(*connectors) != 1 { - t.Errorf("getExportableConnectors() num connectors = %v, want 1", len(*connectors)) - } +// Test getExportableConnectors function with nil MultiService +func TestGetExportableConnectorsNilMultiService(t *testing.T) { + exportableConnectors := getExportableConnectors(nil) - // Make sure the connector is not nil - if (*connectors)[0] == nil { - t.Errorf("getExportableConnectors() connector = %v, want non-nil", (*connectors)[0]) - } - - // Make sure the connector is the correct type - _, ok := (*connectors)[0].(*pingfederate.PingfederateConnector) - if !ok { - t.Errorf("getExportableConnectors() connector = %v, want PingfederateConnector", (*connectors)[0]) + expectedConnectors := 0 + if len(*exportableConnectors) != expectedConnectors { + t.Errorf("getExportableConnectors() exportableConnectors = %v, want %v", len(*exportableConnectors), expectedConnectors) } } // Test exportConnectors function -func Test_exportConnectors(t *testing.T) { +func TestExportConnectors(t *testing.T) { testutils_viper.InitVipers(t) - // init the pingfederate services - err := initPingFederateServices(context.Background(), "v1.2.3", false, false) + err := initPingOneServices(context.Background(), "v1.2.3") if err != nil { - t.Fatalf("initPingFederateServices() error = %v", err) + t.Fatalf("initPingOneServices() error = %v", err) } ms := customtypes.NewMultiService() - if err := ms.Set(customtypes.ENUM_SERVICE_PINGFEDERATE); err != nil { + err = ms.Set(customtypes.ENUM_SERVICE_PINGONE_PROTECT) + if err != nil { t.Fatalf("ms.Set() error = %v", err) } exportableConnectors := getExportableConnectors(ms) - // Test the function err = exportConnectors(exportableConnectors, connector.ENUMEXPORTFORMAT_HCL, t.TempDir(), true) testutils.CheckExpectedError(t, err, nil) } -// Test exportConnectors function fails with nil connectors -func Test_exportConnectors_nilConnectors(t *testing.T) { - // Test the function - expectedErrorPattern := `^failed to export services. exportable connectors list is nil$` +// Test exportConnectors function with nil exportable connectors +func TestExportConnectorsNilExportableConnectors(t *testing.T) { err := exportConnectors(nil, connector.ENUMEXPORTFORMAT_HCL, t.TempDir(), true) + + expectedErrorPattern := `^failed to export services\. exportable connectors list is nil$` testutils.CheckExpectedError(t, err, &expectedErrorPattern) } -// Test exportConnectors function fails with invalid export format -func Test_exportConnectors_invalidExportFormat(t *testing.T) { +// Test exportConnectors function with empty exportable connectors +func TestExportConnectorsEmptyExportableConnectors(t *testing.T) { + exportableConnectors := &[]connector.Exportable{} + + err := exportConnectors(exportableConnectors, connector.ENUMEXPORTFORMAT_HCL, t.TempDir(), true) + testutils.CheckExpectedError(t, err, nil) +} + +// Test exportConnectors function with invalid export format +func TestExportConnectorsInvalidExportFormat(t *testing.T) { testutils_viper.InitVipers(t) - // init the pingfederate services - err := initPingFederateServices(context.Background(), "v1.2.3", false, false) + err := initPingOneServices(context.Background(), "v1.2.3") if err != nil { - t.Fatalf("initPingFederateServices() error = %v", err) + t.Fatalf("initPingOneServices() error = %v", err) } ms := customtypes.NewMultiService() - if err := ms.Set(customtypes.ENUM_SERVICE_PINGFEDERATE); err != nil { + err = ms.Set(customtypes.ENUM_SERVICE_PINGONE_PROTECT) + if err != nil { t.Fatalf("ms.Set() error = %v", err) } exportableConnectors := getExportableConnectors(ms) - // Test the function - expectedErrorPattern := `^failed to export '.*' service: unrecognized export format "invalid". Must be one of: \[.*\]$` err = exportConnectors(exportableConnectors, "invalid", t.TempDir(), true) + + expectedErrorPattern := `^failed to export '.*' service: unrecognized export format ".*"\. Must be one of: .*$` testutils.CheckExpectedError(t, err, &expectedErrorPattern) } -// Test exportConnectors function fails with invalid output directory -func Test_exportConnectors_invalidOutputDir(t *testing.T) { +// Test exportConnectors function with invalid output directory +func TestExportConnectorsInvalidOutputDir(t *testing.T) { testutils_viper.InitVipers(t) - // init the pingfederate services - err := initPingFederateServices(context.Background(), "v1.2.3", false, false) + err := initPingOneServices(context.Background(), "v1.2.3") if err != nil { - t.Fatalf("initPingFederateServices() error = %v", err) + t.Fatalf("initPingOneServices() error = %v", err) } ms := customtypes.NewMultiService() - if err := ms.Set(customtypes.ENUM_SERVICE_PINGFEDERATE); err != nil { + err = ms.Set(customtypes.ENUM_SERVICE_PINGONE_PROTECT) + if err != nil { t.Fatalf("ms.Set() error = %v", err) } exportableConnectors := getExportableConnectors(ms) - // Test the function - expectedErrorPattern := `^failed to export '.*' service: failed to create export file ".*"\. err: open /invalid/.*\.tf: no such file or directory$` err = exportConnectors(exportableConnectors, connector.ENUMEXPORTFORMAT_HCL, "/invalid", true) + + expectedErrorPattern := `^failed to export '.*' service: failed to create export file ".*". err: open .*: no such file or directory$` testutils.CheckExpectedError(t, err, &expectedErrorPattern) } diff --git a/internal/configuration/config/add_profile.go b/internal/configuration/config/add_profile.go new file mode 100644 index 0000000..c40fa02 --- /dev/null +++ b/internal/configuration/config/add_profile.go @@ -0,0 +1,79 @@ +package configuration_config + +import ( + "github.com/pingidentity/pingctl/internal/configuration/options" + "github.com/pingidentity/pingctl/internal/customtypes" + "github.com/spf13/pflag" +) + +func InitConfigAddProfileOptions() { + initAddProfileDescriptionOption() + initAddProfileNameOption() + initAddProfileSetActiveOption() +} + +func initAddProfileDescriptionOption() { + cobraParamName := "description" + cobraValue := new(customtypes.String) + defaultValue := customtypes.String("") + + options.ConfigAddProfileDescriptionOption = options.Option{ + CobraParamName: cobraParamName, + CobraParamValue: cobraValue, + DefaultValue: &defaultValue, + EnvVar: "", // No environment variable + Flag: &pflag.Flag{ + Name: cobraParamName, + Shorthand: "d", + Usage: "The description of the new configuration profile.", + Value: cobraValue, + DefValue: "", + }, + Type: options.ENUM_STRING, + ViperKey: "", // No viper key + } +} + +func initAddProfileNameOption() { + cobraParamName := "name" + cobraValue := new(customtypes.String) + defaultValue := customtypes.String("") + + options.ConfigAddProfileNameOption = options.Option{ + CobraParamName: cobraParamName, + CobraParamValue: cobraValue, + DefaultValue: &defaultValue, + EnvVar: "", // No environment variable + Flag: &pflag.Flag{ + Name: cobraParamName, + Shorthand: "n", + Usage: "The name of the new configuration profile.", + Value: cobraValue, + DefValue: "", + }, + Type: options.ENUM_STRING, + ViperKey: "", // No viper key + } +} + +func initAddProfileSetActiveOption() { + cobraParamName := "set-active" + cobraValue := new(customtypes.Bool) + defaultValue := customtypes.Bool(false) + + options.ConfigAddProfileSetActiveOption = options.Option{ + CobraParamName: cobraParamName, + CobraParamValue: cobraValue, + DefaultValue: &defaultValue, + EnvVar: "", // No environment variable + Flag: &pflag.Flag{ + Name: cobraParamName, + Shorthand: "s", + Usage: "Set the new configuration profile as the active profile for pingctl.", + Value: cobraValue, + DefValue: "false", + }, + Type: options.ENUM_BOOL, + ViperKey: "", // No viper key + } +} diff --git a/internal/configuration/config/config.go b/internal/configuration/config/config.go new file mode 100644 index 0000000..ec2a8ce --- /dev/null +++ b/internal/configuration/config/config.go @@ -0,0 +1,91 @@ +package configuration_config + +import ( + "github.com/pingidentity/pingctl/internal/configuration/options" + "github.com/pingidentity/pingctl/internal/customtypes" + "github.com/spf13/pflag" +) + +func InitConfigOptions() { + initConfigProfileOption() + initConfigNameOption() + initConfigDescriptionOption() + + // initDeleteProfileOption() + + // initViewProfileOption() + + // initSetActiveProfileOption() + + // initGetProfileOption() + + // initSetProfileOption() + + // initUnsetProfileOption() +} + +func initConfigProfileOption() { + cobraParamName := "profile" + cobraValue := new(customtypes.String) + defaultValue := customtypes.String("") + + options.ConfigProfileOption = options.Option{ + CobraParamName: cobraParamName, + CobraParamValue: cobraValue, + DefaultValue: &defaultValue, + EnvVar: "", // No environment variable + Flag: &pflag.Flag{ + Name: cobraParamName, + Shorthand: "p", + Usage: "The name of the profile to update.", + Value: cobraValue, + DefValue: "The active profile", + }, + Type: options.ENUM_STRING, + ViperKey: "", // No viper key + } +} + +func initConfigNameOption() { + cobraParamName := "name" + cobraValue := new(customtypes.String) + defaultValue := customtypes.String("") + + options.ConfigNameOption = options.Option{ + CobraParamName: cobraParamName, + CobraParamValue: cobraValue, + DefaultValue: &defaultValue, + EnvVar: "", // No environment variable + Flag: &pflag.Flag{ + Name: cobraParamName, + Shorthand: "n", + Usage: "The new name for the profile.", + Value: cobraValue, + DefValue: "", + }, + Type: options.ENUM_STRING, + ViperKey: "", // No viper key + } +} + +func initConfigDescriptionOption() { + cobraParamName := "description" + cobraValue := new(customtypes.String) + defaultValue := customtypes.String("") + + options.ConfigDescriptionOption = options.Option{ + CobraParamName: cobraParamName, + CobraParamValue: cobraValue, + DefaultValue: &defaultValue, + EnvVar: "", // No environment variable + Flag: &pflag.Flag{ + Name: cobraParamName, + Shorthand: "d", + Usage: "The new description for the profile.", + Value: cobraValue, + DefValue: "", + }, + Type: options.ENUM_STRING, + ViperKey: "", // No viper key + } +} diff --git a/internal/configuration/config/delete_profile.go b/internal/configuration/config/delete_profile.go new file mode 100644 index 0000000..5adf4ec --- /dev/null +++ b/internal/configuration/config/delete_profile.go @@ -0,0 +1,33 @@ +package configuration_config + +import ( + "github.com/pingidentity/pingctl/internal/configuration/options" + "github.com/pingidentity/pingctl/internal/customtypes" + "github.com/spf13/pflag" +) + +func InitConfigDeleteProfileOptions() { + initDeleteProfileOption() +} + +func initDeleteProfileOption() { + cobraParamName := "profile" + cobraValue := new(customtypes.String) + defaultValue := customtypes.String("") + + options.ConfigDeleteProfileOption = options.Option{ + CobraParamName: cobraParamName, + CobraParamValue: cobraValue, + DefaultValue: &defaultValue, + EnvVar: "", // No environment variable + Flag: &pflag.Flag{ + Name: cobraParamName, + Shorthand: "p", + Usage: "The configuration profile to delete.", + Value: cobraValue, + DefValue: "", + }, + Type: options.ENUM_STRING, + ViperKey: "", // No viper key + } +} diff --git a/internal/configuration/config/get.go b/internal/configuration/config/get.go new file mode 100644 index 0000000..77c5621 --- /dev/null +++ b/internal/configuration/config/get.go @@ -0,0 +1,33 @@ +package configuration_config + +import ( + "github.com/pingidentity/pingctl/internal/configuration/options" + "github.com/pingidentity/pingctl/internal/customtypes" + "github.com/spf13/pflag" +) + +func InitConfigGetOptions() { + initGetProfileOption() +} + +func initGetProfileOption() { + cobraParamName := "profile" + cobraValue := new(customtypes.String) + defaultValue := customtypes.String("") + + options.ConfigGetProfileOption = options.Option{ + CobraParamName: cobraParamName, + CobraParamValue: cobraValue, + DefaultValue: &defaultValue, + EnvVar: "", // No environment variable + Flag: &pflag.Flag{ + Name: cobraParamName, + Shorthand: "p", + Usage: "The configuration profile used to get the configuration value from.", + Value: cobraValue, + DefValue: "The active profile", + }, + Type: options.ENUM_STRING, + ViperKey: "", // No viper key + } +} diff --git a/internal/configuration/config/set.go b/internal/configuration/config/set.go new file mode 100644 index 0000000..f0ecf06 --- /dev/null +++ b/internal/configuration/config/set.go @@ -0,0 +1,33 @@ +package configuration_config + +import ( + "github.com/pingidentity/pingctl/internal/configuration/options" + "github.com/pingidentity/pingctl/internal/customtypes" + "github.com/spf13/pflag" +) + +func InitConfigSetOptions() { + initSetProfileOption() +} + +func initSetProfileOption() { + cobraParamName := "profile" + cobraValue := new(customtypes.String) + defaultValue := customtypes.String("") + + options.ConfigSetProfileOption = options.Option{ + CobraParamName: cobraParamName, + CobraParamValue: cobraValue, + DefaultValue: &defaultValue, + EnvVar: "", // No environment variable + Flag: &pflag.Flag{ + Name: cobraParamName, + Shorthand: "p", + Usage: "The configuration profile used to set the configuration value.", + Value: cobraValue, + DefValue: "The active profile", + }, + Type: options.ENUM_STRING, + ViperKey: "", // No viper key + } +} diff --git a/internal/configuration/config/set_active_profile.go b/internal/configuration/config/set_active_profile.go new file mode 100644 index 0000000..1f5c237 --- /dev/null +++ b/internal/configuration/config/set_active_profile.go @@ -0,0 +1,33 @@ +package configuration_config + +import ( + "github.com/pingidentity/pingctl/internal/configuration/options" + "github.com/pingidentity/pingctl/internal/customtypes" + "github.com/spf13/pflag" +) + +func InitConfigSetActiveProfileOptions() { + initSetActiveProfileOption() +} + +func initSetActiveProfileOption() { + cobraParamName := "profile" + cobraValue := new(customtypes.String) + defaultValue := customtypes.String("") + + options.ConfigSetActiveProfileOption = options.Option{ + CobraParamName: cobraParamName, + CobraParamValue: cobraValue, + DefaultValue: &defaultValue, + EnvVar: "", // No environment variable + Flag: &pflag.Flag{ + Name: cobraParamName, + Shorthand: "p", + Usage: "The configuration profile to set as the active profile.", + Value: cobraValue, + DefValue: "", + }, + Type: options.ENUM_STRING, + ViperKey: "", // No viper key + } +} diff --git a/internal/configuration/config/unset.go b/internal/configuration/config/unset.go new file mode 100644 index 0000000..f0de794 --- /dev/null +++ b/internal/configuration/config/unset.go @@ -0,0 +1,33 @@ +package configuration_config + +import ( + "github.com/pingidentity/pingctl/internal/configuration/options" + "github.com/pingidentity/pingctl/internal/customtypes" + "github.com/spf13/pflag" +) + +func InitConfigUnsetOptions() { + initUnsetProfileOption() +} + +func initUnsetProfileOption() { + cobraParamName := "profile" + cobraValue := new(customtypes.String) + defaultValue := customtypes.String("") + + options.ConfigUnsetProfileOption = options.Option{ + CobraParamName: cobraParamName, + CobraParamValue: cobraValue, + DefaultValue: &defaultValue, + EnvVar: "", // No environment variable + Flag: &pflag.Flag{ + Name: cobraParamName, + Shorthand: "p", + Usage: "The configuration profile used to unset the configuration value.", + Value: cobraValue, + DefValue: "The active profile", + }, + Type: options.ENUM_STRING, + ViperKey: "", // No viper key + } +} diff --git a/internal/configuration/config/view_profile.go b/internal/configuration/config/view_profile.go new file mode 100644 index 0000000..99e91dc --- /dev/null +++ b/internal/configuration/config/view_profile.go @@ -0,0 +1,33 @@ +package configuration_config + +import ( + "github.com/pingidentity/pingctl/internal/configuration/options" + "github.com/pingidentity/pingctl/internal/customtypes" + "github.com/spf13/pflag" +) + +func InitConfigViewProfileOptions() { + initViewProfileOption() +} + +func initViewProfileOption() { + cobraParamName := "profile" + cobraValue := new(customtypes.String) + defaultValue := customtypes.String("") + + options.ConfigViewProfileOption = options.Option{ + CobraParamName: cobraParamName, + CobraParamValue: cobraValue, + DefaultValue: &defaultValue, + EnvVar: "", // No environment variable + Flag: &pflag.Flag{ + Name: cobraParamName, + Shorthand: "p", + Usage: "The configuration profile name to view.", + Value: cobraValue, + DefValue: "The active profile", + }, + Type: options.ENUM_STRING, + ViperKey: "", // No viper key + } +} diff --git a/internal/configuration/configuration.go b/internal/configuration/configuration.go new file mode 100644 index 0000000..edf5ddc --- /dev/null +++ b/internal/configuration/configuration.go @@ -0,0 +1,96 @@ +package configuration + +import ( + "fmt" + "slices" + "strings" + + configuration_config "github.com/pingidentity/pingctl/internal/configuration/config" + "github.com/pingidentity/pingctl/internal/configuration/options" + configuration_platform "github.com/pingidentity/pingctl/internal/configuration/platform" + configuration_profiles "github.com/pingidentity/pingctl/internal/configuration/profiles" + configuration_root "github.com/pingidentity/pingctl/internal/configuration/root" +) + +func ViperKeys() (keys []string) { + for _, opt := range options.Options() { + if opt.ViperKey != "" { + keys = append(keys, opt.ViperKey) + } + } + + slices.Sort(keys) + return keys +} + +func ValidateViperKey(viperKey string) error { + validKeys := ViperKeys() + for _, vKey := range validKeys { + if vKey == viperKey { + return nil + } + } + + validKeysStr := strings.Join(validKeys, ", ") + return fmt.Errorf("key '%s' is not recognized as a valid configuration key. Valid keys: %s", viperKey, validKeysStr) +} + +// Return a list of all viper keys from Options +// Including all substrings of parent keys. +// For example, the option key export.environmentID adds the keys +// 'export' and 'export.environmentID' to the list. +func ExpandedViperKeys() (keys []string) { + leafKeys := ViperKeys() + for _, key := range leafKeys { + keySplit := strings.Split(key, ".") + for i := 0; i < len(keySplit); i++ { + curKey := strings.Join(keySplit[:i+1], ".") + if !slices.ContainsFunc(keys, func(v string) bool { + return strings.EqualFold(v, curKey) + }) { + keys = append(keys, curKey) + } + } + } + + slices.Sort(keys) + return keys +} + +func ValidateParentViperKey(viperKey string) error { + validKeys := ExpandedViperKeys() + for _, vKey := range validKeys { + if vKey == viperKey { + return nil + } + } + + validKeysStr := strings.Join(validKeys, ", ") + return fmt.Errorf("key '%s' is not recognized as a valid configuration key. Valid keys: %s", viperKey, validKeysStr) +} + +func OptionFromViperKey(viperKey string) (opt options.Option, err error) { + for _, opt := range options.Options() { + if strings.EqualFold(opt.ViperKey, viperKey) { + return opt, nil + } + } + return opt, fmt.Errorf("failed to get option: no option found for viper key: %s", viperKey) +} + +func InitAllOptions() { + configuration_config.InitConfigOptions() + configuration_config.InitConfigAddProfileOptions() + configuration_config.InitConfigDeleteProfileOptions() + configuration_config.InitConfigViewProfileOptions() + configuration_config.InitConfigSetActiveProfileOptions() + configuration_config.InitConfigSetOptions() + configuration_config.InitConfigGetOptions() + configuration_config.InitConfigUnsetOptions() + + configuration_platform.InitPlatformExportOptions() + + configuration_profiles.InitProfilesOptions() + + configuration_root.InitRootOptions() +} diff --git a/internal/configuration/configuration_test.go b/internal/configuration/configuration_test.go new file mode 100644 index 0000000..537d4f7 --- /dev/null +++ b/internal/configuration/configuration_test.go @@ -0,0 +1,79 @@ +package configuration_test + +import ( + "testing" + + "github.com/pingidentity/pingctl/internal/configuration" + "github.com/pingidentity/pingctl/internal/testing/testutils" + "github.com/pingidentity/pingctl/internal/testing/testutils_viper" +) + +// Test ValidateViperKey function +func Test_ValidateViperKey(t *testing.T) { + testutils_viper.InitVipers(t) + + err := configuration.ValidateViperKey("pingctl.color") + if err != nil { + t.Errorf("ValidateViperKey returned error: %v", err) + } +} + +// Test ValidateViperKey function fails with invalid key +func Test_ValidateViperKey_InvalidKey(t *testing.T) { + testutils_viper.InitVipers(t) + + expectedErrorPattern := `^key '.*' is not recognized as a valid configuration key. Valid keys: .*$` + err := configuration.ValidateViperKey("invalid-key") + testutils.CheckExpectedError(t, err, &expectedErrorPattern) +} + +// Test ValidateViperKey function fails with empty key +func Test_ValidateViperKey_EmptyKey(t *testing.T) { + testutils_viper.InitVipers(t) + + expectedErrorPattern := `^key '' is not recognized as a valid configuration key. Valid keys: .*$` + err := configuration.ValidateViperKey("") + testutils.CheckExpectedError(t, err, &expectedErrorPattern) +} + +// Test ValidateParentViperKey function +func Test_ValidateParentViperKey(t *testing.T) { + testutils_viper.InitVipers(t) + + err := configuration.ValidateParentViperKey("pingctl") + if err != nil { + t.Errorf("ValidateParentViperKey returned error: %v", err) + } +} + +// Test ValidateParentViperKey function fails with invalid key +func Test_ValidateParentViperKey_InvalidKey(t *testing.T) { + testutils_viper.InitVipers(t) + + expectedErrorPattern := `^key '.*' is not recognized as a valid configuration key. Valid keys: .*$` + err := configuration.ValidateParentViperKey("invalid-key") + testutils.CheckExpectedError(t, err, &expectedErrorPattern) +} + +// Test ValidateParentViperKey function fails with empty key +func Test_ValidateParentViperKey_EmptyKey(t *testing.T) { + testutils_viper.InitVipers(t) + + expectedErrorPattern := `^key '' is not recognized as a valid configuration key. Valid keys: .*$` + err := configuration.ValidateParentViperKey("") + testutils.CheckExpectedError(t, err, &expectedErrorPattern) +} + +// Test OptionFromViperKey function +func Test_OptionFromViperKey(t *testing.T) { + testutils_viper.InitVipers(t) + + opt, err := configuration.OptionFromViperKey("pingctl.color") + if err != nil { + t.Errorf("OptionFromViperKey returned error: %v", err) + } + + if opt.ViperKey != "pingctl.color" { + t.Errorf("OptionFromViperKey returned invalid option: %v", opt) + } +} diff --git a/internal/configuration/options/options.go b/internal/configuration/options/options.go new file mode 100644 index 0000000..9e98652 --- /dev/null +++ b/internal/configuration/options/options.go @@ -0,0 +1,136 @@ +package options + +import "github.com/spf13/pflag" + +type OptionType string + +// OptionType enums +const ( + ENUM_BOOL OptionType = "ENUM_BOOL" + ENUM_EXPORT_FORMAT OptionType = "ENUM_EXPORT_FORMAT" + ENUM_UUID OptionType = "ENUM_UUID" + ENUM_MULTI_SERVICE OptionType = "ENUM_MULTI_SERVICE" + ENUM_OUTPUT_FORMAT OptionType = "ENUM_OUTPUT_FORMAT" + ENUM_PINGONE_REGION OptionType = "ENUM_PINGONE_REGION" + ENUM_STRING OptionType = "ENUM_STRING" + ENUM_STRING_SLICE OptionType = "ENUM_STRING_SLICE" +) + +type Option struct { + CobraParamName string + CobraParamValue pflag.Value + DefaultValue pflag.Value + EnvVar string + Flag *pflag.Flag + Type OptionType + ViperKey string +} + +func Options() []Option { + return []Option{ + PlatformExportExportFormatOption, + PlatformExportServiceOption, + PlatformExportOutputDirectoryOption, + PlatformExportOverwriteOption, + PlatformExportPingoneWorkerEnvironmentIDOption, + PlatformExportPingoneExportEnvironmentIDOption, + PlatformExportPingoneWorkerClientIDOption, + PlatformExportPingoneWorkerClientSecretOption, + PlatformExportPingoneRegionOption, + PlatformExportPingfederateHTTPSHostOption, + PlatformExportPingfederateAdminAPIPathOption, + PlatformExportPingfederateXBypassExternalValidationHeaderOption, + PlatformExportPingfederateCACertificatePemFilesOption, + PlatformExportPingfederateInsecureTrustAllTLSOption, + PlatformExportPingfederateUsernameOption, + PlatformExportPingfederatePasswordOption, + PlatformExportPingfederateAccessTokenOption, + PlatformExportPingfederateClientIDOption, + PlatformExportPingfederateClientSecretOption, + PlatformExportPingfederateTokenURLOption, + PlatformExportPingfederateScopesOption, + + RootActiveProfileOption, + RootColorOption, + RootConfigOption, + RootOutputFormatOption, + + ProfileDescriptionOption, + + ConfigProfileOption, + ConfigNameOption, + ConfigDescriptionOption, + ConfigAddProfileDescriptionOption, + ConfigAddProfileNameOption, + ConfigAddProfileSetActiveOption, + ConfigDeleteProfileOption, + ConfigViewProfileOption, + ConfigSetActiveProfileOption, + ConfigGetProfileOption, + ConfigSetProfileOption, + ConfigUnsetProfileOption, + } +} + +// 'pingctl config' command options +var ( + ConfigProfileOption Option + ConfigNameOption Option + ConfigDescriptionOption Option + + ConfigAddProfileDescriptionOption Option + ConfigAddProfileNameOption Option + ConfigAddProfileSetActiveOption Option + + ConfigDeleteProfileOption Option + + ConfigViewProfileOption Option + + ConfigSetActiveProfileOption Option + + ConfigGetProfileOption Option + + ConfigSetProfileOption Option + + ConfigUnsetProfileOption Option +) + +// 'pingctl platform export' command options +var ( + PlatformExportExportFormatOption Option + PlatformExportServiceOption Option + PlatformExportOutputDirectoryOption Option + PlatformExportOverwriteOption Option + + PlatformExportPingoneWorkerEnvironmentIDOption Option + PlatformExportPingoneExportEnvironmentIDOption Option + PlatformExportPingoneWorkerClientIDOption Option + PlatformExportPingoneWorkerClientSecretOption Option + PlatformExportPingoneRegionOption Option + + PlatformExportPingfederateHTTPSHostOption Option + PlatformExportPingfederateAdminAPIPathOption Option + PlatformExportPingfederateXBypassExternalValidationHeaderOption Option + PlatformExportPingfederateCACertificatePemFilesOption Option + PlatformExportPingfederateInsecureTrustAllTLSOption Option + PlatformExportPingfederateUsernameOption Option + PlatformExportPingfederatePasswordOption Option + PlatformExportPingfederateAccessTokenOption Option + PlatformExportPingfederateClientIDOption Option + PlatformExportPingfederateClientSecretOption Option + PlatformExportPingfederateTokenURLOption Option + PlatformExportPingfederateScopesOption Option +) + +// Generic viper profile options +var ( + ProfileDescriptionOption Option +) + +// Options +var ( + RootActiveProfileOption Option + RootColorOption Option + RootConfigOption Option + RootOutputFormatOption Option +) diff --git a/internal/configuration/platform/export.go b/internal/configuration/platform/export.go new file mode 100644 index 0000000..41c3e73 --- /dev/null +++ b/internal/configuration/platform/export.go @@ -0,0 +1,523 @@ +package configuration_platform + +import ( + "fmt" + "os" + "strings" + + "github.com/pingidentity/pingctl/internal/configuration/options" + "github.com/pingidentity/pingctl/internal/connector" + "github.com/pingidentity/pingctl/internal/customtypes" + "github.com/pingidentity/pingctl/internal/logger" + "github.com/spf13/pflag" +) + +func InitPlatformExportOptions() { + initExportFormatOption() + initServicesOption() + initOutputDirectoryOption() + initOverwriteOption() + + initPingOneWorkerEnvironmentIDOption() + initPingOneExportEnvironmentIDOption() + initPingOneWorkerClientIDOption() + initPingOneWorkerClientSecretOption() + initPingOneRegionOption() + + initPingFederateHTTPSHostOption() + initPingFederateAdminAPIPathOption() + initPingFederateXBypassExternalValidationHeaderOption() + initPingFederateCACertificatePemFilesOption() + initPingFederateInsecureTrustAllTLSOption() + initPingFederateUsernameOption() + initPingFederatePasswordOption() + initPingFederateAccessTokenOption() + initPingFederateClientIDOption() + initPingFederateClientSecretOption() + initPingFederateTokenURLOption() + initPingFederateScopesOption() +} + +func initExportFormatOption() { + cobraParamName := "export-format" + cobraValue := new(customtypes.ExportFormat) + defaultValue := customtypes.ExportFormat(connector.ENUMEXPORTFORMAT_HCL) + envVar := "PINGCTL_EXPORT_FORMAT" + + options.PlatformExportExportFormatOption = options.Option{ + CobraParamName: cobraParamName, + CobraParamValue: cobraValue, + DefaultValue: &defaultValue, + EnvVar: envVar, + Flag: &pflag.Flag{ + Name: cobraParamName, + Shorthand: "e", + Usage: fmt.Sprintf("Specifies export format\nAllowed: [%s]. Also configurable via environment variable %s", strings.Join(customtypes.ExportFormatValidValues(), ", "), envVar), + Value: cobraValue, + DefValue: connector.ENUMEXPORTFORMAT_HCL, + }, + Type: options.ENUM_STRING, + ViperKey: "export.exportFormat", + } +} + +func initServicesOption() { + cobraParamName := "services" + cobraValue := new(customtypes.MultiService) + defaultValue := customtypes.NewMultiService() + envVar := "PINGCTL_EXPORT_SERVICES" + + options.PlatformExportServiceOption = options.Option{ + CobraParamName: cobraParamName, + CobraParamValue: cobraValue, + DefaultValue: defaultValue, + EnvVar: envVar, + Flag: &pflag.Flag{ + Name: cobraParamName, + Shorthand: "s", + Usage: fmt.Sprintf("Specifies service(s) to export. Accepts comma-separated string to delimit multiple services. Allowed: [%s]. Also configurable via environment variable %s", strings.Join(customtypes.MultiServiceValidValues(), ", "), envVar), + Value: cobraValue, + DefValue: strings.Join(customtypes.MultiServiceValidValues(), ", "), + }, + Type: options.ENUM_MULTI_SERVICE, + ViperKey: "export.services", + } +} + +func initOutputDirectoryOption() { + cobraParamName := "output-directory" + cobraValue := new(customtypes.String) + defaultValue := getDefaultExportDir() + envVar := "PINGCTL_EXPORT_OUTPUT_DIRECTORY" + + options.PlatformExportOutputDirectoryOption = options.Option{ + CobraParamName: cobraParamName, + CobraParamValue: cobraValue, + DefaultValue: defaultValue, + EnvVar: envVar, + Flag: &pflag.Flag{ + Name: cobraParamName, + Shorthand: "d", + Usage: fmt.Sprintf("Specifies output directory for export. Also configurable via environment variable %s", envVar), + Value: cobraValue, + DefValue: "$(pwd)/export", + }, + Type: options.ENUM_STRING, + ViperKey: "export.outputDirectory", + } +} + +func initOverwriteOption() { + cobraParamName := "overwrite" + cobraValue := new(customtypes.Bool) + defaultValue := customtypes.Bool(false) + + options.PlatformExportOverwriteOption = options.Option{ + CobraParamName: cobraParamName, + CobraParamValue: cobraValue, + DefaultValue: &defaultValue, + EnvVar: "PINGCTL_EXPORT_OVERWRITE", + Flag: &pflag.Flag{ + Name: cobraParamName, + Shorthand: "o", + Usage: "Overwrite existing generated exports in output directory.", + Value: cobraValue, + DefValue: "false", + }, + Type: options.ENUM_BOOL, + ViperKey: "export.overwrite", + } +} + +func initPingOneWorkerEnvironmentIDOption() { + cobraParamName := "pingone-worker-environment-id" + cobraValue := new(customtypes.UUID) + defaultValue := customtypes.UUID("") + envVar := "PINGCTL_PINGONE_WORKER_ENVIRONMENT_ID" + + options.PlatformExportPingoneWorkerEnvironmentIDOption = options.Option{ + CobraParamName: cobraParamName, + CobraParamValue: cobraValue, + DefaultValue: &defaultValue, + EnvVar: envVar, + Flag: &pflag.Flag{ + Name: cobraParamName, + Usage: fmt.Sprintf("The ID of the PingOne environment that contains the worker client used to authenticate. Also configurable via environment variable %s", envVar), + Value: cobraValue, + DefValue: "", + }, + Type: options.ENUM_UUID, + ViperKey: "export.pingone.worker.environmentID", + } +} + +func initPingOneExportEnvironmentIDOption() { + cobraParamName := "pingone-export-environment-id" + cobraValue := new(customtypes.UUID) + defaultValue := customtypes.UUID("") + envVar := "PING_CTL_PINGONE_EXPORT_ENVIRONMENT_ID" + + options.PlatformExportPingoneExportEnvironmentIDOption = options.Option{ + CobraParamName: cobraParamName, + CobraParamValue: cobraValue, + DefaultValue: &defaultValue, + EnvVar: envVar, + Flag: &pflag.Flag{ + Name: cobraParamName, + Usage: fmt.Sprintf("The ID of the PingOne environment to export. Also configurable via environment variable %s", envVar), + Value: cobraValue, + DefValue: "", + }, + Type: options.ENUM_UUID, + ViperKey: "export.pingone.export.environmentID", + } +} + +func initPingOneWorkerClientIDOption() { + cobraParamName := "pingone-worker-client-id" + cobraValue := new(customtypes.UUID) + defaultValue := customtypes.UUID("") + envVar := "PINGCTL_PINGONE_WORKER_CLIENT_ID" + + options.PlatformExportPingoneWorkerClientIDOption = options.Option{ + CobraParamName: cobraParamName, + CobraParamValue: cobraValue, + DefaultValue: &defaultValue, + EnvVar: envVar, + Flag: &pflag.Flag{ + Name: cobraParamName, + Usage: fmt.Sprintf("The ID of the PingOne worker client used to authenticate. Also configurable via environment variable %s", envVar), + Value: cobraValue, + DefValue: "", + }, + Type: options.ENUM_UUID, + ViperKey: "export.pingone.worker.clientID", + } +} + +func initPingOneWorkerClientSecretOption() { + cobraParamName := "pingone-worker-client-secret" + cobraValue := new(customtypes.String) + defaultValue := customtypes.String("") + envVar := "PINGCTL_PINGONE_WORKER_CLIENT_SECRET" + + options.PlatformExportPingoneWorkerClientSecretOption = options.Option{ + CobraParamName: cobraParamName, + CobraParamValue: cobraValue, + DefaultValue: &defaultValue, + EnvVar: envVar, + Flag: &pflag.Flag{ + Name: cobraParamName, + Usage: fmt.Sprintf("The PingOne worker client secret used to authenticate. Also configurable via environment variable %s", envVar), + Value: cobraValue, + DefValue: "", + }, + Type: options.ENUM_STRING, + ViperKey: "export.pingone.worker.clientSecret", + } +} + +func initPingOneRegionOption() { + cobraParamName := "pingone-region" + cobraValue := new(customtypes.PingOneRegion) + defaultValue := customtypes.PingOneRegion("") + envVar := "PINGCTL_PINGONE_REGION" + + options.PlatformExportPingoneRegionOption = options.Option{ + CobraParamName: cobraParamName, + CobraParamValue: cobraValue, + DefaultValue: &defaultValue, + EnvVar: envVar, + Flag: &pflag.Flag{ + Name: cobraParamName, + Usage: fmt.Sprintf("The region of the PingOne service(s). Allowed: %s. Also configurable via environment variable %s", strings.Join(customtypes.PingOneRegionValidValues(), ", "), envVar), + Value: cobraValue, + DefValue: "", + }, + Type: options.ENUM_STRING, + ViperKey: "export.pingone.region", + } +} + +func initPingFederateHTTPSHostOption() { + cobraParamName := "pingfederate-https-host" + cobraValue := new(customtypes.String) + defaultValue := customtypes.String("") + envVar := "PINGCTL_PINGFEDERATE_HTTPS_HOST" + + options.PlatformExportPingfederateHTTPSHostOption = options.Option{ + CobraParamName: cobraParamName, + CobraParamValue: cobraValue, + DefaultValue: &defaultValue, + EnvVar: envVar, + Flag: &pflag.Flag{ + Name: cobraParamName, + Usage: fmt.Sprintf("The PingFederate HTTPS host used to communicate with PingFederate's API. Also configurable via environment variable %s", envVar), + Value: cobraValue, + DefValue: "", + }, + Type: options.ENUM_STRING, + ViperKey: "export.pingfederate.httpsHost", + } +} + +func initPingFederateAdminAPIPathOption() { + cobraParamName := "pingfederate-admin-api-path" + cobraValue := new(customtypes.String) + defaultValue := customtypes.String("/pf-admin-api/v1") + envVar := "PINGCTL_PINGFEDERATE_ADMIN_API_PATH" + + options.PlatformExportPingfederateAdminAPIPathOption = options.Option{ + CobraParamName: cobraParamName, + CobraParamValue: cobraValue, + DefaultValue: &defaultValue, + EnvVar: envVar, + Flag: &pflag.Flag{ + Name: cobraParamName, + Usage: fmt.Sprintf("The PingFederate API URL path used to communicate with PingFederate's API. Also configurable via environment variable %s", envVar), + Value: cobraValue, + DefValue: "/pf-admin-api/v1", + }, + Type: options.ENUM_STRING, + ViperKey: "export.pingfederate.adminAPIPath", + } +} + +func initPingFederateXBypassExternalValidationHeaderOption() { + cobraParamName := "pingfederate-x-bypass-external-validation-header" + cobraValue := new(customtypes.Bool) + defaultValue := customtypes.Bool(false) + envVar := "PINGCTL_PINGFEDERATE_X_BYPASS_EXTERNAL_VALIDATION_HEADER" + + options.PlatformExportPingfederateXBypassExternalValidationHeaderOption = options.Option{ + CobraParamName: cobraParamName, + CobraParamValue: cobraValue, + DefaultValue: &defaultValue, + EnvVar: envVar, + Flag: &pflag.Flag{ + Name: cobraParamName, + Usage: fmt.Sprintf("Header value in request for PingFederate. PingFederate's connection tests will be bypassed when set to true. Also configurable via environment variable %s", envVar), + Value: cobraValue, + DefValue: "false", + }, + Type: options.ENUM_BOOL, + ViperKey: "export.pingfederate.xBypassExternalValidationHeader", + } +} + +func initPingFederateCACertificatePemFilesOption() { + cobraParamName := "pingfederate-ca-certificate-pem-files" + cobraValue := new(customtypes.StringSlice) + defaultValue := customtypes.StringSlice{} + envVar := "PINGCTL_PINGFEDERATE_CA_CERTIFICATE_PEM_FILES" + + options.PlatformExportPingfederateCACertificatePemFilesOption = options.Option{ + CobraParamName: cobraParamName, + CobraParamValue: cobraValue, + DefaultValue: &defaultValue, + EnvVar: envVar, + Flag: &pflag.Flag{ + Name: cobraParamName, + Usage: fmt.Sprintf("Paths to files containing PEM-encoded certificates to be trusted as root CAs when connecting to the PingFederate server over HTTPS. Accepts comma-separated string to delimit multiple PEM files. Also configurable via environment variable %s", envVar), + Value: cobraValue, + DefValue: "[]", + }, + Type: options.ENUM_STRING_SLICE, + ViperKey: "export.pingfederate.caCertificatePemFiles", + } +} + +func initPingFederateInsecureTrustAllTLSOption() { + cobraParamName := "pingfederate-insecure-trust-all-tls" + cobraValue := new(customtypes.Bool) + defaultValue := customtypes.Bool(false) + envVar := "PINGCTL_PINGFEDERATE_INSECURE_TRUST_ALL_TLS" + + options.PlatformExportPingfederateInsecureTrustAllTLSOption = options.Option{ + CobraParamName: cobraParamName, + CobraParamValue: cobraValue, + DefaultValue: &defaultValue, + EnvVar: envVar, + Flag: &pflag.Flag{ + Name: cobraParamName, + Usage: fmt.Sprintf("Set to true to trust any certificate when connecting to the PingFederate server. This is insecure and should not be enabled outside of testing. Also configurable via environment variable %s", envVar), + Value: cobraValue, + DefValue: "false", + }, + Type: options.ENUM_BOOL, + ViperKey: "export.pingfederate.insecureTrustAllTLS", + } +} + +func initPingFederateUsernameOption() { + cobraParamName := "pingfederate-username" + cobraValue := new(customtypes.String) + defaultValue := customtypes.String("") + envVar := "PINGCTL_PINGFEDERATE_USERNAME" + + options.PlatformExportPingfederateUsernameOption = options.Option{ + CobraParamName: cobraParamName, + CobraParamValue: cobraValue, + DefaultValue: &defaultValue, + EnvVar: envVar, + Flag: &pflag.Flag{ + Name: cobraParamName, + Usage: fmt.Sprintf("The PingFederate username used to authenticate. Also configurable via environment variable %s", envVar), + Value: cobraValue, + DefValue: "", + }, + Type: options.ENUM_STRING, + ViperKey: "export.pingfederate.basicAuth.username", + } +} + +func initPingFederatePasswordOption() { + cobraParamName := "pingfederate-password" + cobraValue := new(customtypes.String) + defaultValue := customtypes.String("") + envVar := "PINGCTL_PINGFEDERATE_PASSWORD" + + options.PlatformExportPingfederatePasswordOption = options.Option{ + CobraParamName: cobraParamName, + CobraParamValue: cobraValue, + DefaultValue: &defaultValue, + EnvVar: envVar, + Flag: &pflag.Flag{ + Name: cobraParamName, + Usage: fmt.Sprintf("The PingFederate password used to authenticate. Also configurable via environment variable %s", envVar), + Value: cobraValue, + DefValue: "", + }, + Type: options.ENUM_STRING, + ViperKey: "export.pingfederate.basicAuth.password", + } +} + +func initPingFederateAccessTokenOption() { + cobraParamName := "pingfederate-access-token" + cobraValue := new(customtypes.String) + defaultValue := customtypes.String("") + envVar := "PINGCTL_PINGFEDERATE_ACCESS_TOKEN" + + options.PlatformExportPingfederateAccessTokenOption = options.Option{ + CobraParamName: cobraParamName, + CobraParamValue: cobraValue, + DefaultValue: &defaultValue, + EnvVar: envVar, + Flag: &pflag.Flag{ + Name: cobraParamName, + Usage: fmt.Sprintf("The PingFederate access token used to authenticate. Also configurable via environment variable %s", envVar), + Value: cobraValue, + DefValue: "", + }, + Type: options.ENUM_STRING, + ViperKey: "export.pingfederate.accessTokenAuth.accessToken", + } +} + +func initPingFederateClientIDOption() { + cobraParamName := "pingfederate-client-id" + cobraValue := new(customtypes.String) + defaultValue := customtypes.String("") + envVar := "PINGCTL_PINGFEDERATE_CLIENT_ID" + + options.PlatformExportPingfederateClientIDOption = options.Option{ + CobraParamName: cobraParamName, + CobraParamValue: cobraValue, + DefaultValue: &defaultValue, + EnvVar: envVar, + Flag: &pflag.Flag{ + Name: cobraParamName, + Usage: fmt.Sprintf("The PingFederate OAuth client ID used to authenticate. Also configurable via environment variable %s", envVar), + Value: cobraValue, + DefValue: "", + }, + Type: options.ENUM_STRING, + ViperKey: "export.pingfederate.clientCredentialsAuth.clientID", + } +} + +func initPingFederateClientSecretOption() { + cobraParamName := "pingfederate-client-secret" + cobraValue := new(customtypes.String) + defaultValue := customtypes.String("") + envVar := "PINGCTL_PINGFEDERATE_CLIENT_SECRET" + + options.PlatformExportPingfederateClientSecretOption = options.Option{ + CobraParamName: cobraParamName, + CobraParamValue: cobraValue, + DefaultValue: &defaultValue, + EnvVar: envVar, + Flag: &pflag.Flag{ + Name: cobraParamName, + Usage: fmt.Sprintf("The PingFederate OAuth client secret used to authenticate. Also configurable via environment variable %s", envVar), + Value: cobraValue, + DefValue: "", + }, + Type: options.ENUM_STRING, + ViperKey: "export.pingfederate.clientCredentialsAuth.clientSecret", + } +} + +func initPingFederateTokenURLOption() { + cobraParamName := "pingfederate-token-url" + cobraValue := new(customtypes.String) + defaultValue := customtypes.String("") + envVar := "PINGCTL_PINGFEDERATE_TOKEN_URL" + + options.PlatformExportPingfederateTokenURLOption = options.Option{ + CobraParamName: cobraParamName, + CobraParamValue: cobraValue, + DefaultValue: &defaultValue, + EnvVar: envVar, + Flag: &pflag.Flag{ + Name: cobraParamName, + Usage: fmt.Sprintf("The PingFederate OAuth token URL used to authenticate. Also configurable via environment variable %s", envVar), + Value: cobraValue, + DefValue: "", + }, + Type: options.ENUM_STRING, + ViperKey: "export.pingfederate.clientCredentialsAuth.tokenURL", + } +} + +func initPingFederateScopesOption() { + cobraParamName := "pingfederate-scopes" + cobraValue := new(customtypes.StringSlice) + defaultValue := customtypes.StringSlice{} + envVar := "PINGCTL_PINGFEDERATE_SCOPES" + + options.PlatformExportPingfederateScopesOption = options.Option{ + CobraParamName: cobraParamName, + CobraParamValue: cobraValue, + DefaultValue: &defaultValue, + EnvVar: envVar, + Flag: &pflag.Flag{ + Name: cobraParamName, + Usage: fmt.Sprintf("The PingFederate OAuth scopes used to authenticate. Accepts comma-separated string to delimit multiple scopes. Also configurable via environment variable %s", envVar), + Value: cobraValue, + DefValue: "[]", + }, + Type: options.ENUM_STRING_SLICE, + ViperKey: "export.pingfederate.clientCredentialsAuth.scopes", + } +} + +func getDefaultExportDir() (defaultExportDir *customtypes.String) { + l := logger.Get() + pwd, err := os.Getwd() + if err != nil { + l.Err(err).Msg("Failed to determine current working directory") + return nil + } + + defaultExportDir = new(customtypes.String) + + err = defaultExportDir.Set(fmt.Sprintf("%s/export", pwd)) + if err != nil { + l.Err(err).Msg("Failed to set default export directory") + return nil + } + + return defaultExportDir +} diff --git a/internal/configuration/profiles/profiles.go b/internal/configuration/profiles/profiles.go new file mode 100644 index 0000000..a6404cd --- /dev/null +++ b/internal/configuration/profiles/profiles.go @@ -0,0 +1,22 @@ +package configuration_profiles + +import ( + "github.com/pingidentity/pingctl/internal/configuration/options" + "github.com/pingidentity/pingctl/internal/customtypes" +) + +func InitProfilesOptions() { + initDescriptionOption() +} + +func initDescriptionOption() { + options.ProfileDescriptionOption = options.Option{ + CobraParamName: "", // No cobra param name + CobraParamValue: nil, // No cobra param value + DefaultValue: new(customtypes.String), + EnvVar: "", // No environment variable + Flag: nil, // No flag + Type: options.ENUM_STRING, + ViperKey: "description", + } +} diff --git a/internal/configuration/root/root.go b/internal/configuration/root/root.go new file mode 100644 index 0000000..3164a05 --- /dev/null +++ b/internal/configuration/root/root.go @@ -0,0 +1,126 @@ +package configuration_root + +import ( + "fmt" + "os" + "strings" + + "github.com/pingidentity/pingctl/internal/configuration/options" + "github.com/pingidentity/pingctl/internal/customtypes" + "github.com/pingidentity/pingctl/internal/logger" + "github.com/spf13/pflag" +) + +func InitRootOptions() { + initActiveProfileOption() + initColorOption() + initConfigOption() + initOutputFormatOption() +} + +func initActiveProfileOption() { + cobraParamName := "active-profile" + cobraValue := new(customtypes.String) + defaultValue := customtypes.String("default") + + options.RootActiveProfileOption = options.Option{ + CobraParamName: cobraParamName, + CobraParamValue: cobraValue, + DefaultValue: &defaultValue, + EnvVar: "PINGCTL_ACTIVE_PROFILE", + Flag: &pflag.Flag{ + Name: cobraParamName, + Shorthand: "P", + Usage: "Profile to use from configuration file", + Value: cobraValue, + DefValue: "default", + }, + Type: options.ENUM_STRING, + ViperKey: "activeProfile", + } +} + +func initColorOption() { + cobraParamName := "color" + cobraValue := new(customtypes.Bool) + defaultValue := customtypes.Bool(true) + + options.RootColorOption = options.Option{ + CobraParamName: cobraParamName, + CobraParamValue: cobraValue, + DefaultValue: &defaultValue, + EnvVar: "PINGCTL_COLOR", + Flag: &pflag.Flag{ + Name: cobraParamName, + Usage: "Use colorized output", + Value: cobraValue, + DefValue: "true", + }, + Type: options.ENUM_BOOL, + ViperKey: "pingctl.color", + } +} + +func initConfigOption() { + cobraParamName := "config" + cobraValue := new(customtypes.String) + defaultValue := getDefaultConfigFilepath() + + options.RootConfigOption = options.Option{ + CobraParamName: cobraParamName, + CobraParamValue: cobraValue, + DefaultValue: defaultValue, + EnvVar: "PINGCTL_CONFIG", + Flag: &pflag.Flag{ + Name: cobraParamName, + Shorthand: "C", + Usage: "Configuration file location", + Value: cobraValue, + DefValue: "\"$HOME/.pingctl/config.yaml\"", + }, + Type: options.ENUM_STRING, + ViperKey: "", // No viper key + } +} + +func initOutputFormatOption() { + cobraParamName := "output-format" + cobraValue := new(customtypes.OutputFormat) + defaultValue := customtypes.OutputFormat(customtypes.ENUM_OUTPUT_FORMAT_TEXT) + + options.RootOutputFormatOption = options.Option{ + CobraParamName: cobraParamName, + CobraParamValue: cobraValue, + DefaultValue: &defaultValue, + EnvVar: "PINGCTL_OUTPUT_FORMAT", + Flag: &pflag.Flag{ + Name: cobraParamName, + Shorthand: "O", + Usage: fmt.Sprintf("Specifies pingctl's console output format. Allowed: %s", strings.Join(customtypes.OutputFormatValidValues(), ", ")), + Value: cobraValue, + DefValue: customtypes.ENUM_OUTPUT_FORMAT_TEXT, + }, + Type: options.ENUM_OUTPUT_FORMAT, + ViperKey: "pingctl.outputFormat", + } +} + +func getDefaultConfigFilepath() (defaultConfigFilepath *customtypes.String) { + l := logger.Get() + + defaultConfigFilepath = new(customtypes.String) + + homeDir, err := os.UserHomeDir() + if err != nil { + l.Err(err).Msg("Failed to determine user's home directory") + return nil + } + + err = defaultConfigFilepath.Set(fmt.Sprintf("%s/.pingctl/config.yaml", homeDir)) + if err != nil { + l.Err(err).Msg("Failed to set default config file path") + return nil + } + + return defaultConfigFilepath +} diff --git a/internal/connector/pingfederate/pingfederate_connector_test.go b/internal/connector/pingfederate/pingfederate_connector_test.go index fa7c66d..f72ae72 100644 --- a/internal/connector/pingfederate/pingfederate_connector_test.go +++ b/internal/connector/pingfederate/pingfederate_connector_test.go @@ -32,12 +32,9 @@ func TestPingFederateTerraformPlan(t *testing.T) { ignoredErrors: nil, }, { - name: "PingFederateAuthenticationPolicies", - resource: resources.AuthenticationPolicies(PingFederateClientInfo), - ignoredErrors: []string{ - "Error: Plugin did not respond", - "Error: Request cancelled", - }, + name: "PingFederateAuthenticationPolicies", + resource: resources.AuthenticationPolicies(PingFederateClientInfo), + ignoredErrors: nil, }, { name: "PingFederateAuthenticationPoliciesFragment", @@ -57,12 +54,9 @@ func TestPingFederateTerraformPlan(t *testing.T) { ignoredErrors: nil, }, { - name: "PingFederateAuthenticationSelector", - resource: resources.AuthenticationSelector(PingFederateClientInfo), - ignoredErrors: []string{ - "Error: Plugin did not respond", - "Error: Request cancelled", - }, + name: "PingFederateAuthenticationSelector", + resource: resources.AuthenticationSelector(PingFederateClientInfo), + ignoredErrors: nil, }, { name: "PingFederateCertificateCA", @@ -79,12 +73,9 @@ func TestPingFederateTerraformPlan(t *testing.T) { }, }, { - name: "PingFederateExtendedProperties", - resource: resources.ExtendedProperties(PingFederateClientInfo), - ignoredErrors: []string{ - "Error: Plugin did not respond", - "Error: Request cancelled", - }, + name: "PingFederateExtendedProperties", + resource: resources.ExtendedProperties(PingFederateClientInfo), + ignoredErrors: nil, }, { name: "PingFederateIDPAdapter", @@ -105,12 +96,9 @@ func TestPingFederateTerraformPlan(t *testing.T) { ignoredErrors: nil, }, { - name: "PingFederateIncomingProxySettings", - resource: resources.IncomingProxySettings(PingFederateClientInfo), - ignoredErrors: []string{ - "Error: Plugin did not respond", - "Error: Request cancelled", - }, + name: "PingFederateIncomingProxySettings", + resource: resources.IncomingProxySettings(PingFederateClientInfo), + ignoredErrors: nil, }, { name: "PingFederateKerberosRealm", @@ -125,12 +113,9 @@ func TestPingFederateTerraformPlan(t *testing.T) { ignoredErrors: nil, }, { - name: "PingFederateNotificationPublishersSettings", - resource: resources.NotificationPublishersSettings(PingFederateClientInfo), - ignoredErrors: []string{ - "Error: Plugin did not respond", - "Error: Request cancelled", - }, + name: "PingFederateNotificationPublishersSettings", + resource: resources.NotificationPublishersSettings(PingFederateClientInfo), + ignoredErrors: nil, }, { name: "PingFederateOAuthAccessTokenManager", @@ -141,8 +126,8 @@ func TestPingFederateTerraformPlan(t *testing.T) { name: "PingFederateOAuthAccessTokenMapping", resource: resources.OAuthAccessTokenMapping(PingFederateClientInfo), ignoredErrors: []string{ - "Error: Plugin did not respond", - "Error: Request cancelled", + "Error: Invalid attribute combination", + "Error: Invalid Attribute Value Length", }, }, { @@ -168,8 +153,7 @@ func TestPingFederateTerraformPlan(t *testing.T) { name: "PingFederateOpenIDConnectSettings", resource: resources.OpenIDConnectSettings(PingFederateClientInfo), ignoredErrors: []string{ - "Error: Plugin did not respond", - "Error: Request cancelled", + "Error: Missing Configuration for Required Attribute", }, }, { @@ -187,9 +171,11 @@ func TestPingFederateTerraformPlan(t *testing.T) { ignoredErrors: nil, }, { - name: "PingFederateServerSettings", - resource: resources.ServerSettings(PingFederateClientInfo), - ignoredErrors: nil, + name: "PingFederateServerSettings", + resource: resources.ServerSettings(PingFederateClientInfo), + ignoredErrors: []string{ + "Error: Invalid Attribute Value Length", + }, }, { name: "PingFederateServerSettingsSystemKeys", diff --git a/internal/connector/pingfederate/resources/pingfederate_open_id_connect_settings.go b/internal/connector/pingfederate/resources/pingfederate_open_id_connect_settings.go index ff5212b..f76ee8b 100644 --- a/internal/connector/pingfederate/resources/pingfederate_open_id_connect_settings.go +++ b/internal/connector/pingfederate/resources/pingfederate_open_id_connect_settings.go @@ -29,8 +29,8 @@ func (r *PingFederateOpenIDConnectSettingsResource) ExportAll() (*[]connector.Im l.Debug().Msgf("Generating Import Blocks for all %s resources...", r.ResourceType()) - openIDConnectSettingsId := "open_id_connect_settings_singleton_id" - openIDConnectSettingsName := "Open ID Connect Settings" + openIDConnectSettingsId := "openid_connect_settings_singleton_id" + openIDConnectSettingsName := "OpenID Connect Settings" commentData := map[string]string{ "Resource Type": r.ResourceType(), @@ -48,5 +48,5 @@ func (r *PingFederateOpenIDConnectSettingsResource) ExportAll() (*[]connector.Im } func (r *PingFederateOpenIDConnectSettingsResource) ResourceType() string { - return "pingfederate_open_id_connect_settings" + return "pingfederate_openid_connect_settings" } diff --git a/internal/connector/pingfederate/resources/pingfederate_open_id_connect_settings_test.go b/internal/connector/pingfederate/resources/pingfederate_open_id_connect_settings_test.go index 1285807..0e4e866 100644 --- a/internal/connector/pingfederate/resources/pingfederate_open_id_connect_settings_test.go +++ b/internal/connector/pingfederate/resources/pingfederate_open_id_connect_settings_test.go @@ -16,9 +16,9 @@ func TestPingFederateOpenIDConnectSettingsExport(t *testing.T) { // Defined the expected ImportBlocks for the resource expectedImportBlocks := []connector.ImportBlock{ { - ResourceType: "pingfederate_open_id_connect_settings", - ResourceName: "Open ID Connect Settings", - ResourceID: "open_id_connect_settings_singleton_id", + ResourceType: "pingfederate_openid_connect_settings", + ResourceName: "OpenID Connect Settings", + ResourceID: "openid_connect_settings_singleton_id", }, } diff --git a/internal/customtypes/bool.go b/internal/customtypes/bool.go new file mode 100644 index 0000000..42112a9 --- /dev/null +++ b/internal/customtypes/bool.go @@ -0,0 +1,39 @@ +package customtypes + +import ( + "fmt" + "strconv" + + "github.com/spf13/pflag" +) + +type Bool bool + +// Verify that the custom type satisfies the pflag.Value interface +var _ pflag.Value = (*Bool)(nil) + +func (b *Bool) Set(val string) error { + if b == nil { + return fmt.Errorf("failed to set Bool value: %s. Bool is nil", val) + } + + parsedBool, err := strconv.ParseBool(val) + if err != nil { + return err + } + *b = Bool(parsedBool) + + return nil +} + +func (b Bool) Type() string { + return "bool" +} + +func (b Bool) String() string { + return strconv.FormatBool(bool(b)) +} + +func (b Bool) Bool() bool { + return bool(b) +} diff --git a/internal/customtypes/bool_test.go b/internal/customtypes/bool_test.go new file mode 100644 index 0000000..b421418 --- /dev/null +++ b/internal/customtypes/bool_test.go @@ -0,0 +1,84 @@ +package customtypes_test + +import ( + "testing" + + "github.com/pingidentity/pingctl/internal/customtypes" + "github.com/pingidentity/pingctl/internal/testing/testutils" +) + +// Test Bool Set function +func Test_Bool_Set(t *testing.T) { + b := new(customtypes.Bool) + val := "true" + + err := b.Set(val) + if err != nil { + t.Errorf("Set returned error: %v", err) + } + + val = "false" + + err = b.Set(val) + if err != nil { + t.Errorf("Set returned error: %v", err) + } +} + +// Test Set function fails with invalid value +func Test_Bool_Set_InvalidValue(t *testing.T) { + b := new(customtypes.Bool) + val := "invalid" + + expectedErrorPattern := `^strconv.ParseBool: parsing ".*": invalid syntax$` + err := b.Set(val) + testutils.CheckExpectedError(t, err, &expectedErrorPattern) +} + +// Test Set function fails with nil +func Test_Bool_Set_Nil(t *testing.T) { + var b *customtypes.Bool + val := "true" + + expectedErrorPattern := `^failed to set Bool value: .* Bool is nil$` + err := b.Set(val) + testutils.CheckExpectedError(t, err, &expectedErrorPattern) +} + +// Test String function +func Test_Bool_String(t *testing.T) { + b := customtypes.Bool(true) + + expected := "true" + actual := b.String() + if actual != expected { + t.Errorf("String returned: %s, expected: %s", actual, expected) + } + + b = customtypes.Bool(false) + + expected = "false" + actual = b.String() + if actual != expected { + t.Errorf("String returned: %s, expected: %s", actual, expected) + } +} + +// Test Bool function +func Test_Bool_Bool(t *testing.T) { + b := customtypes.Bool(true) + + expected := true + actual := b.Bool() + if actual != expected { + t.Errorf("Bool returned: %t, expected: %t", actual, expected) + } + + b = customtypes.Bool(false) + + expected = false + actual = b.Bool() + if actual != expected { + t.Errorf("Bool returned: %t, expected: %t", actual, expected) + } +} diff --git a/internal/customtypes/export_format.go b/internal/customtypes/export_format.go index d43acf7..a744d8c 100644 --- a/internal/customtypes/export_format.go +++ b/internal/customtypes/export_format.go @@ -16,22 +16,26 @@ var _ pflag.Value = (*ExportFormat)(nil) // Implement pflag.Value interface for custom type in cobra export-format parameter -func (s *ExportFormat) Set(format string) error { +func (ef *ExportFormat) Set(format string) error { + if ef == nil { + return fmt.Errorf("failed to set Export Format value: %s. Export Format is nil", format) + } + switch format { case connector.ENUMEXPORTFORMAT_HCL: - *s = ExportFormat(format) + *ef = ExportFormat(format) default: return fmt.Errorf("unrecognized export format '%s'. Must be one of: %s", format, strings.Join(ExportFormatValidValues(), ", ")) } return nil } -func (s *ExportFormat) Type() string { +func (ef ExportFormat) Type() string { return "string" } -func (s *ExportFormat) String() string { - return string(*s) +func (ef ExportFormat) String() string { + return string(ef) } func ExportFormatValidValues() []string { diff --git a/internal/customtypes/export_format_test.go b/internal/customtypes/export_format_test.go index 960f3d6..ac7d496 100644 --- a/internal/customtypes/export_format_test.go +++ b/internal/customtypes/export_format_test.go @@ -3,55 +3,52 @@ package customtypes_test import ( "testing" + "github.com/pingidentity/pingctl/internal/connector" "github.com/pingidentity/pingctl/internal/customtypes" "github.com/pingidentity/pingctl/internal/testing/testutils" ) -// Test the custom type ExportFormat Set method with a valid value -func TestExportFormat_SetValid(t *testing.T) { - exportFormat := customtypes.ExportFormat("HCL") - err := exportFormat.Set("HCL") - testutils.CheckExpectedError(t, err, nil) +// Test ExportFormat Set function +func Test_ExportFormat_Set(t *testing.T) { + // Create a new ExportFormat + exportFormat := new(customtypes.ExportFormat) + + err := exportFormat.Set(connector.ENUMEXPORTFORMAT_HCL) + if err != nil { + t.Errorf("Set returned error: %v", err) + } } -// Test the custom type ExportFormat Set method with an invalid value -func TestExportFormat_SetInvalid(t *testing.T) { - expectedErrorPattern := `unrecognized export format 'INVALID'. Must be one of: [A-Z]+` - exportFormat := customtypes.ExportFormat("HCL") - err := exportFormat.Set("INVALID") +// Test Set function fails with invalid value +func Test_ExportFormat_Set_InvalidValue(t *testing.T) { + // Create a new ExportFormat + exportFormat := new(customtypes.ExportFormat) + + invalidValue := "invalid" + + expectedErrorPattern := `^unrecognized export format '.*'. Must be one of: .*$` + err := exportFormat.Set(invalidValue) testutils.CheckExpectedError(t, err, &expectedErrorPattern) } -// Test the custom type ExportFormat Type method -func TestExportFormat_Type(t *testing.T) { - exportFormat := customtypes.ExportFormat("HCL") - typeValue := exportFormat.Type() - if typeValue != "string" { - t.Errorf("Expected 'string' but got '%s'", typeValue) - } -} +// Test Set function fails with nil +func Test_ExportFormat_Set_Nil(t *testing.T) { + var exportFormat *customtypes.ExportFormat -// Test the custom type ExportFormat String method -func TestExportFormat_String(t *testing.T) { - exportFormat := customtypes.ExportFormat("HCL") - stringValue := exportFormat.String() - if stringValue != "HCL" { - t.Errorf("Expected 'HCL' but got '%s'", stringValue) - } -} + val := connector.ENUMEXPORTFORMAT_HCL -// Test the custom type ExportFormat ExportFormatValidValues method -func TestExportFormat_ExportFormatValidValues(t *testing.T) { - expectedValues := []string{"HCL"} - validValues := customtypes.ExportFormatValidValues() + expectedErrorPattern := `^failed to set Export Format value: .* Export Format is nil$` + err := exportFormat.Set(val) + testutils.CheckExpectedError(t, err, &expectedErrorPattern) +} - if len(validValues) != len(expectedValues) { - t.Errorf("Expected %d valid values but got %d", len(expectedValues), len(validValues)) - } +// Test String function +func Test_ExportFormat_String(t *testing.T) { + exportFormat := customtypes.ExportFormat(connector.ENUMEXPORTFORMAT_HCL) - for i, expectedValue := range expectedValues { - if validValues[i] != expectedValue { - t.Errorf("Expected '%s' but got '%s'", expectedValue, validValues[i]) - } + expected := connector.ENUMEXPORTFORMAT_HCL + actual := exportFormat.String() + if actual != expected { + t.Errorf("String returned: %s, expected: %s", actual, expected) } } diff --git a/internal/customtypes/multi_service.go b/internal/customtypes/multi_service.go index e7cec69..2cb905c 100644 --- a/internal/customtypes/multi_service.go +++ b/internal/customtypes/multi_service.go @@ -16,10 +16,7 @@ const ( ENUM_SERVICE_PINGFEDERATE string = "pingfederate" ) -type MultiService struct { - services *map[string]bool - isDefaultServices bool -} +type MultiService map[string]bool // Verify that the custom type satisfies the pflag.Value interface var _ pflag.Value = (*MultiService)(nil) @@ -27,26 +24,25 @@ var _ pflag.Value = (*MultiService)(nil) // Implement pflag.Value interface for custom type in cobra MultiService parameter func NewMultiService() *MultiService { - return &MultiService{ - services: &map[string]bool{ - ENUM_SERVICE_PINGFEDERATE: true, - ENUM_SERVICE_PINGONE_PLATFORM: true, - ENUM_SERVICE_PINGONE_SSO: true, - ENUM_SERVICE_PINGONE_MFA: true, - ENUM_SERVICE_PINGONE_PROTECT: true, - }, - isDefaultServices: true, + ms := map[string]bool{ + ENUM_SERVICE_PINGFEDERATE: true, + ENUM_SERVICE_PINGONE_PLATFORM: true, + ENUM_SERVICE_PINGONE_SSO: true, + ENUM_SERVICE_PINGONE_MFA: true, + ENUM_SERVICE_PINGONE_PROTECT: true, } + + return (*MultiService)(&ms) } -func (s *MultiService) GetServices() *[]string { +func (ms MultiService) GetServices() []string { enabledExportServices := []string{} - if s == nil { - return &enabledExportServices + if ms == nil { + return enabledExportServices } - for k, v := range *s.services { + for k, v := range ms { if v { enabledExportServices = append(enabledExportServices, k) } @@ -54,74 +50,72 @@ func (s *MultiService) GetServices() *[]string { slices.Sort(enabledExportServices) - return &enabledExportServices + return enabledExportServices } -func (s *MultiService) Set(service string) error { - if s == nil { - return fmt.Errorf("MultiService is nil") - } - - // If the user is defining services to export, remove default services from map - if s.isDefaultServices { - s.services = &map[string]bool{} - s.isDefaultServices = false +func (ms *MultiService) Set(services string) error { + if ms == nil { + return fmt.Errorf("failed to set MultiService value: %s. MultiService is nil", services) } - switch service { - case ENUM_SERVICE_PINGFEDERATE: - (*s.services)[ENUM_SERVICE_PINGFEDERATE] = true - case ENUM_SERVICE_PINGONE_PLATFORM: - (*s.services)[ENUM_SERVICE_PINGONE_PLATFORM] = true - case ENUM_SERVICE_PINGONE_SSO: - (*s.services)[ENUM_SERVICE_PINGONE_SSO] = true - case ENUM_SERVICE_PINGONE_MFA: - (*s.services)[ENUM_SERVICE_PINGONE_MFA] = true - case ENUM_SERVICE_PINGONE_PROTECT: - (*s.services)[ENUM_SERVICE_PINGONE_PROTECT] = true - default: - return fmt.Errorf("unrecognized service '%s'. Must be one of: %s", service, strings.Join(MultiServiceValidValues(), ", ")) + *ms = map[string]bool{} + + serviceList := strings.Split(services, ",") + + for _, service := range serviceList { + switch service { + case ENUM_SERVICE_PINGFEDERATE: + (*ms)[ENUM_SERVICE_PINGFEDERATE] = true + case ENUM_SERVICE_PINGONE_PLATFORM: + (*ms)[ENUM_SERVICE_PINGONE_PLATFORM] = true + case ENUM_SERVICE_PINGONE_SSO: + (*ms)[ENUM_SERVICE_PINGONE_SSO] = true + case ENUM_SERVICE_PINGONE_MFA: + (*ms)[ENUM_SERVICE_PINGONE_MFA] = true + case ENUM_SERVICE_PINGONE_PROTECT: + (*ms)[ENUM_SERVICE_PINGONE_PROTECT] = true + default: + return fmt.Errorf("unrecognized service '%s'. Must be one of: %s", service, strings.Join(MultiServiceValidValues(), ", ")) + } } return nil } -func (s *MultiService) ContainsPingOneService() bool { - if s == nil { +func (ms MultiService) ContainsPingOneService() bool { + if ms == nil { return false } - return (*s.services)[ENUM_SERVICE_PINGONE_PLATFORM] || - (*s.services)[ENUM_SERVICE_PINGONE_SSO] || - (*s.services)[ENUM_SERVICE_PINGONE_MFA] || - (*s.services)[ENUM_SERVICE_PINGONE_PROTECT] + return ms[ENUM_SERVICE_PINGONE_PLATFORM] || + ms[ENUM_SERVICE_PINGONE_SSO] || + ms[ENUM_SERVICE_PINGONE_MFA] || + ms[ENUM_SERVICE_PINGONE_PROTECT] } -func (s *MultiService) ContainsPingFederateService() bool { - if s == nil { +func (ms MultiService) ContainsPingFederateService() bool { + if ms == nil { return false } - return (*s.services)[ENUM_SERVICE_PINGFEDERATE] + return ms[ENUM_SERVICE_PINGFEDERATE] } -func (s *MultiService) Type() string { +func (ms MultiService) Type() string { return "string" } -func (s *MultiService) String() string { - if s == nil { - return "[]" +func (ms MultiService) String() string { + if ms == nil { + return "" } - enabledExportServices := *s.GetServices() + enabledExportServices := ms.GetServices() if len(enabledExportServices) == 0 { - return "[]" + return "" } - slices.Sort(enabledExportServices) - - return strings.Join(enabledExportServices, ", ") + return strings.Join(enabledExportServices, ",") } func MultiServiceValidValues() []string { diff --git a/internal/customtypes/multi_service_test.go b/internal/customtypes/multi_service_test.go index dfac7c0..1079b71 100644 --- a/internal/customtypes/multi_service_test.go +++ b/internal/customtypes/multi_service_test.go @@ -1,89 +1,108 @@ package customtypes_test import ( - "slices" - "strings" "testing" "github.com/pingidentity/pingctl/internal/customtypes" "github.com/pingidentity/pingctl/internal/testing/testutils" ) -// Test custom type MultiService NewMultiService() method -func TestMultiService_NewMultiService(t *testing.T) { +// Test MultiService NewMultiService function +func Test_MultiService_NewMultiService(t *testing.T) { multiService := customtypes.NewMultiService() + if multiService == nil { - t.Error("Expected non-nil MultiService object") + t.Fatalf("NewMultiService returned nil") + } + + if len(multiService.GetServices()) == 0 { + t.Fatalf("NewMultiService returned empty Services") } } -// Test custom type MultiService GetServices() method -func TestMultiService_GetServices(t *testing.T) { +// Test MultiService Set function +func Test_MultiService_Set(t *testing.T) { multiService := customtypes.NewMultiService() - expectedService := customtypes.ENUM_SERVICE_PINGONE_PLATFORM - err := multiService.Set(expectedService) - testutils.CheckExpectedError(t, err, nil) - services := multiService.GetServices() - if services == nil || *services == nil { - t.Fatal("Expected non-nil services slice") + service := customtypes.ENUM_SERVICE_PINGONE_MFA + err := multiService.Set(service) + if err != nil { + t.Errorf("Set returned error: %v", err) } - if len(*services) != 1 { - t.Errorf("Expected 1 service but got %d", len(*services)) + services := multiService.GetServices() + if len(services) != 1 { + t.Errorf("GetServices returned: %v, expected: %v", services, service) } - if (*services)[0] != expectedService { - t.Errorf("Expected service '%s' but got '%s'", expectedService, (*services)[0]) + if services[0] != service { + t.Errorf("GetServices returned: %v, expected: %v", services, service) } } -// Test custom type MultiService Set() method with a valid value -func TestMultiService_SetValid(t *testing.T) { +// Test MultiService Set function with invalid value +func Test_MultiService_Set_InvalidValue(t *testing.T) { multiService := customtypes.NewMultiService() - err := multiService.Set(customtypes.ENUM_SERVICE_PINGONE_PLATFORM) - testutils.CheckExpectedError(t, err, nil) + + invalidValue := "invalid" + expectedErrorPattern := `^unrecognized service '.*'. Must be one of: .*$` + err := multiService.Set(invalidValue) + testutils.CheckExpectedError(t, err, &expectedErrorPattern) } -// Test custom type MultiService Set() method with an invalid value -func TestMultiService_SetInvalid(t *testing.T) { - expectedErrorPattern := `unrecognized service 'INVALID'. Must be one of: [a-z\-]+` - multiService := customtypes.NewMultiService() - err := multiService.Set("INVALID") +// Test MultiService Set function with nil +func Test_MultiService_Set_Nil(t *testing.T) { + var multiService *customtypes.MultiService + + service := customtypes.ENUM_SERVICE_PINGONE_MFA + expectedErrorPattern := `^failed to set MultiService value: .* MultiService is nil$` + err := multiService.Set(service) testutils.CheckExpectedError(t, err, &expectedErrorPattern) } -// Test custom type MultiService Type() method -func TestMultiService_Type(t *testing.T) { +// Test MultiService ContainsPingOneService function +func Test_MultiService_ContainsPingOneService(t *testing.T) { multiService := customtypes.NewMultiService() - typeValue := multiService.Type() - if typeValue != "string" { - t.Errorf("Expected 'string' but got '%s'", typeValue) + + service := customtypes.ENUM_SERVICE_PINGONE_MFA + err := multiService.Set(service) + if err != nil { + t.Errorf("Set returned error: %v", err) + } + + if !multiService.ContainsPingOneService() { + t.Errorf("ContainsPingOneService returned false, expected true") } } -// Test custom type MultiService String() method -func TestMultiService_String(t *testing.T) { - expectedServicesStr := strings.Join(*customtypes.NewMultiService().GetServices(), ", ") +// Test MultiService ContainsPingFederateService function +func Test_MultiService_ContainsPingFederateService(t *testing.T) { multiService := customtypes.NewMultiService() - stringValue := multiService.String() - if stringValue != expectedServicesStr { - t.Errorf("Expected '%s' but got '%s'", expectedServicesStr, stringValue) + + service := customtypes.ENUM_SERVICE_PINGFEDERATE + err := multiService.Set(service) + if err != nil { + t.Errorf("Set returned error: %v", err) + } + + if !multiService.ContainsPingFederateService() { + t.Errorf("ContainsPingFederateService returned false, expected true") } } -// Test custom type MultiService MultiServiceValidValues() method -func TestMultiService_MultiServiceValidValues(t *testing.T) { - expectedValues := *customtypes.NewMultiService().GetServices() - validValues := customtypes.MultiServiceValidValues() +// Test MultiService String function +func Test_MultiService_String(t *testing.T) { + multiService := customtypes.NewMultiService() - if len(validValues) != len(expectedValues) { - t.Errorf("Expected %d valid values but got %d", len(expectedValues), len(validValues)) + service := customtypes.ENUM_SERVICE_PINGONE_MFA + err := multiService.Set(service) + if err != nil { + t.Errorf("Set returned error: %v", err) } - for _, expectedValue := range expectedValues { - if !slices.Contains(validValues, expectedValue) { - t.Errorf("Expected value '%s' is not in %v", expectedValue, validValues) - } + expected := service + actual := multiService.String() + if actual != expected { + t.Errorf("String returned: %s, expected: %s", actual, expected) } } diff --git a/internal/customtypes/output_format.go b/internal/customtypes/output_format.go index 5762e62..2af81a0 100644 --- a/internal/customtypes/output_format.go +++ b/internal/customtypes/output_format.go @@ -20,23 +20,26 @@ var _ pflag.Value = (*OutputFormat)(nil) // Implement pflag.Value interface for custom type in cobra pingctl-output parameter -func (s *OutputFormat) Set(outputFormat string) error { - switch outputFormat { +func (o *OutputFormat) Set(outputFormat string) error { + if o == nil { + return fmt.Errorf("failed to set Output Format value: %s. Output Format is nil", outputFormat) + } + switch outputFormat { case ENUM_OUTPUT_FORMAT_TEXT, ENUM_OUTPUT_FORMAT_JSON: - *s = OutputFormat(outputFormat) + *o = OutputFormat(outputFormat) default: return fmt.Errorf("unrecognized Output Format: '%s'. Must be one of: %s", outputFormat, strings.Join(OutputFormatValidValues(), ", ")) } return nil } -func (s *OutputFormat) Type() string { +func (o OutputFormat) Type() string { return "string" } -func (s *OutputFormat) String() string { - return string(*s) +func (o OutputFormat) String() string { + return string(o) } func OutputFormatValidValues() []string { diff --git a/internal/customtypes/output_format_test.go b/internal/customtypes/output_format_test.go index 61d7f34..693e8ac 100644 --- a/internal/customtypes/output_format_test.go +++ b/internal/customtypes/output_format_test.go @@ -1,58 +1,51 @@ package customtypes_test import ( - "slices" "testing" "github.com/pingidentity/pingctl/internal/customtypes" "github.com/pingidentity/pingctl/internal/testing/testutils" ) -// Test the custom type OutputFormat Set method with a valid value -func TestOutputFormat_SetValid(t *testing.T) { - outputFormat := customtypes.OutputFormat("text") - err := outputFormat.Set("json") - testutils.CheckExpectedError(t, err, nil) +// Test OutputFormat Set function +func Test_OutputFormat_Set(t *testing.T) { + outputFormat := new(customtypes.OutputFormat) + + err := outputFormat.Set(customtypes.ENUM_OUTPUT_FORMAT_JSON) + if err != nil { + t.Errorf("Set returned error: %v", err) + } } -// Test the custom type OutputFormat Set method with an invalid value -func TestOutputFormat_SetInvalid(t *testing.T) { - expectedErrorPattern := `unrecognized Output Format: 'INVALID'. Must be one of: [a-z\s,]+` - outputFormat := customtypes.OutputFormat("text") - err := outputFormat.Set("INVALID") +// Test Set function fails with invalid value +func Test_OutputFormat_Set_InvalidValue(t *testing.T) { + outputFormat := new(customtypes.OutputFormat) + + invalidValue := "invalid" + + expectedErrorPattern := `^unrecognized Output Format: '.*'\. Must be one of: .*$` + err := outputFormat.Set(invalidValue) testutils.CheckExpectedError(t, err, &expectedErrorPattern) } -// Test the custom type OutputFormat Type method -func TestOutputFormat_Type(t *testing.T) { - outputFormat := customtypes.OutputFormat("text") - typeValue := outputFormat.Type() - if typeValue != "string" { - t.Errorf("Expected 'string' but got '%s'", typeValue) - } -} +// Test Set function fails with nil +func Test_OutputFormat_Set_Nil(t *testing.T) { + var outputFormat *customtypes.OutputFormat -// Test the custom type OutputFormat String method -func TestOutputFormat_String(t *testing.T) { - outputFormat := customtypes.OutputFormat("text") - stringValue := outputFormat.String() - if stringValue != "text" { - t.Errorf("Expected 'text' but got '%s'", stringValue) - } -} + val := customtypes.ENUM_OUTPUT_FORMAT_JSON -// Test the custom type OutputFormat OutputFormatValidValues method -func TestOutputFormat_OutputFormatValidValues(t *testing.T) { - expectedValues := []string{customtypes.ENUM_OUTPUT_FORMAT_TEXT, customtypes.ENUM_OUTPUT_FORMAT_JSON} - validValues := customtypes.OutputFormatValidValues() + expectedErrorPattern := `^failed to set Output Format value: .* Output Format is nil$` + err := outputFormat.Set(val) + testutils.CheckExpectedError(t, err, &expectedErrorPattern) +} - if len(validValues) != len(expectedValues) { - t.Errorf("Expected %d valid values but got %d", len(expectedValues), len(validValues)) - } +// Test String function +func Test_OutputFormat_String(t *testing.T) { + outputFormat := customtypes.OutputFormat(customtypes.ENUM_OUTPUT_FORMAT_JSON) - for _, expectedValue := range expectedValues { - if !slices.Contains(validValues, expectedValue) { - t.Errorf("Expected value '%s' is not in %v", expectedValue, validValues) - } + expected := customtypes.ENUM_OUTPUT_FORMAT_JSON + actual := outputFormat.String() + if actual != expected { + t.Errorf("String returned: %s, expected: %s", actual, expected) } } diff --git a/internal/customtypes/pingone_region.go b/internal/customtypes/pingone_region.go index 4527bb1..c63d4a9 100644 --- a/internal/customtypes/pingone_region.go +++ b/internal/customtypes/pingone_region.go @@ -22,22 +22,25 @@ var _ pflag.Value = (*PingOneRegion)(nil) // Implement pflag.Value interface for custom type in cobra pingone-region parameter -func (s *PingOneRegion) Set(region string) error { +func (p *PingOneRegion) Set(region string) error { + if p == nil { + return fmt.Errorf("failed to set PingOne Region value: %s. PingOne Region is nil", region) + } switch region { case ENUM_PINGONE_REGION_AP, ENUM_PINGONE_REGION_CA, ENUM_PINGONE_REGION_EU, ENUM_PINGONE_REGION_NA: - *s = PingOneRegion(region) + *p = PingOneRegion(region) default: return fmt.Errorf("unrecognized PingOne Region: '%s'. Must be one of: %s", region, strings.Join(PingOneRegionValidValues(), ", ")) } return nil } -func (s *PingOneRegion) Type() string { +func (p PingOneRegion) Type() string { return "string" } -func (s *PingOneRegion) String() string { - return string(*s) +func (p PingOneRegion) String() string { + return string(p) } func PingOneRegionValidValues() []string { diff --git a/internal/customtypes/pingone_region_test.go b/internal/customtypes/pingone_region_test.go index fa742c5..bf0e77e 100644 --- a/internal/customtypes/pingone_region_test.go +++ b/internal/customtypes/pingone_region_test.go @@ -1,62 +1,51 @@ package customtypes_test import ( - "slices" "testing" "github.com/pingidentity/pingctl/internal/customtypes" "github.com/pingidentity/pingctl/internal/testing/testutils" ) -// Test the custom type PingOneRegion Set method with a valid value -func TestPingOneRegion_SetValid(t *testing.T) { - pingOneRegion := customtypes.PingOneRegion("AsiaPacific") - err := pingOneRegion.Set("Europe") - testutils.CheckExpectedError(t, err, nil) +// Test PingOneRegion Set function +func Test_PingOneRegion_Set(t *testing.T) { + pingoneRegion := new(customtypes.PingOneRegion) + + err := pingoneRegion.Set(customtypes.ENUM_PINGONE_REGION_CA) + if err != nil { + t.Errorf("Set returned error: %v", err) + } } -// Test the custom type PingOneRegion Set method with an invalid value -func TestPingOneRegion_SetInvalid(t *testing.T) { - expectedErrorPattern := `^unrecognized PingOne Region: 'INVALID'. Must be one of: [A-Za-z\s,]+$` - pingOneRegion := customtypes.PingOneRegion("AsiaPacific") - err := pingOneRegion.Set("INVALID") +// Test Set function fails with invalid value +func Test_PingOneRegion_Set_InvalidValue(t *testing.T) { + pingoneRegion := new(customtypes.PingOneRegion) + + invalidValue := "invalid" + + expectedErrorPattern := `^unrecognized PingOne Region: '.*'\. Must be one of: .*$` + err := pingoneRegion.Set(invalidValue) testutils.CheckExpectedError(t, err, &expectedErrorPattern) } -// Test the custom type PingOneRegion Type method -func TestPingOneRegion_Type(t *testing.T) { - pingOneRegion := customtypes.PingOneRegion("AsiaPacific") - typeValue := pingOneRegion.Type() - if typeValue != "string" { - t.Errorf("Expected 'string' but got '%s'", typeValue) - } -} +// Test Set function fails with nil +func Test_PingOneRegion_Set_Nil(t *testing.T) { + var pingoneRegion *customtypes.PingOneRegion -// Test the custom type PingOneRegion String method -func TestPingOneRegion_String(t *testing.T) { - pingOneRegion := customtypes.PingOneRegion("AsiaPacific") - stringValue := pingOneRegion.String() - if stringValue != "AsiaPacific" { - t.Errorf("Expected 'AsiaPacific' but got '%s'", stringValue) - } + val := customtypes.ENUM_PINGONE_REGION_CA + + expectedErrorPattern := `^failed to set PingOne Region value: .* PingOne Region is nil$` + err := pingoneRegion.Set(val) + testutils.CheckExpectedError(t, err, &expectedErrorPattern) } -// Test the custom type PingOneRegion PingOneRegionValidValues method -func TestPingOneRegion_PingOneRegionValidValues(t *testing.T) { - expectedValues := []string{ - customtypes.ENUM_PINGONE_REGION_AP, - customtypes.ENUM_PINGONE_REGION_CA, - customtypes.ENUM_PINGONE_REGION_EU, - customtypes.ENUM_PINGONE_REGION_NA} - validValues := customtypes.PingOneRegionValidValues() - - if len(validValues) != len(expectedValues) { - t.Errorf("Expected %d valid values but got %d", len(expectedValues), len(validValues)) - } +// Test String function +func Test_PingOneRegion_String(t *testing.T) { + pingoneRegion := customtypes.PingOneRegion(customtypes.ENUM_PINGONE_REGION_CA) - for _, expectedValue := range expectedValues { - if !slices.Contains(validValues, expectedValue) { - t.Errorf("Expected value '%s' is not in %v", expectedValue, validValues) - } + expected := customtypes.ENUM_PINGONE_REGION_CA + actual := pingoneRegion.String() + if actual != expected { + t.Errorf("String returned: %s, expected: %s", actual, expected) } } diff --git a/internal/customtypes/string.go b/internal/customtypes/string.go new file mode 100644 index 0000000..6906795 --- /dev/null +++ b/internal/customtypes/string.go @@ -0,0 +1,30 @@ +package customtypes + +import ( + "fmt" + + "github.com/spf13/pflag" +) + +type String string + +// Verify that the custom type satisfies the pflag.Value interface +var _ pflag.Value = (*String)(nil) + +func (s *String) Set(val string) error { + if s == nil { + return fmt.Errorf("failed to set String value: %s. String is nil", val) + } + + *s = String(val) + + return nil +} + +func (s String) Type() string { + return "string" +} + +func (s String) String() string { + return string(s) +} diff --git a/internal/customtypes/string_slice.go b/internal/customtypes/string_slice.go new file mode 100644 index 0000000..1c6cec3 --- /dev/null +++ b/internal/customtypes/string_slice.go @@ -0,0 +1,45 @@ +package customtypes + +import ( + "fmt" + "strings" + + "github.com/spf13/pflag" +) + +type StringSlice []string + +// Verify that the custom type satisfies the pflag.Value interface +var _ pflag.Value = (*StringSlice)(nil) + +func (ss *StringSlice) Set(val string) error { + if ss == nil { + return fmt.Errorf("failed to set StringSlice value: %s. StringSlice is nil", val) + } + + valSs := strings.Split(val, ",") + + *ss = StringSlice(valSs) + + return nil +} + +func (ss StringSlice) Type() string { + return "[]string" +} + +func (ss StringSlice) String() string { + if ss == nil { + return "" + } + + return strings.Join(ss, ",") +} + +func (ss StringSlice) StringSlice() []string { + if ss == nil { + return []string{} + } + + return []string(ss) +} diff --git a/internal/customtypes/string_slice_test.go b/internal/customtypes/string_slice_test.go new file mode 100644 index 0000000..e0d22ef --- /dev/null +++ b/internal/customtypes/string_slice_test.go @@ -0,0 +1,51 @@ +package customtypes_test + +import ( + "testing" + + "github.com/pingidentity/pingctl/internal/customtypes" + "github.com/pingidentity/pingctl/internal/testing/testutils" +) + +// Test StringSlice Set function +func Test_StringSlice_Set(t *testing.T) { + ss := new(customtypes.StringSlice) + + val := "value1,value2" + err := ss.Set(val) + if err != nil { + t.Errorf("Set returned error: %v", err) + } +} + +// Test Set function fails with nil +func Test_StringSlice_Set_Nil(t *testing.T) { + var ss *customtypes.StringSlice + + val := "value1,value2" + expectedErrorPattern := `^failed to set StringSlice value: .* StringSlice is nil$` + err := ss.Set(val) + testutils.CheckExpectedError(t, err, &expectedErrorPattern) +} + +// Test String function +func Test_StringSlice_String(t *testing.T) { + ss := customtypes.StringSlice([]string{"value1", "value2"}) + + expected := "value1,value2" + actual := ss.String() + if actual != expected { + t.Errorf("String returned: %s, expected: %s", actual, expected) + } +} + +// Test StringSlice String function with empty slice +func Test_StringSlice_String_Empty(t *testing.T) { + ss := customtypes.StringSlice([]string{}) + + expected := "" + actual := ss.String() + if actual != expected { + t.Errorf("String returned: %s, expected: %s", actual, expected) + } +} diff --git a/internal/customtypes/uuid.go b/internal/customtypes/uuid.go new file mode 100644 index 0000000..55e280f --- /dev/null +++ b/internal/customtypes/uuid.go @@ -0,0 +1,40 @@ +package customtypes + +import ( + "fmt" + + "github.com/hashicorp/go-uuid" + "github.com/spf13/pflag" +) + +type UUID string + +// Verify that the custom type satisfies the pflag.Value interface +var _ pflag.Value = (*UUID)(nil) + +func (u *UUID) Set(val string) error { + if u == nil { + return fmt.Errorf("failed to set UUID value: %s. UUID is nil", val) + } + + _, err := uuid.ParseUUID(val) + if err != nil { + return err + } + + *u = UUID(val) + + return nil +} + +func (u *UUID) Type() string { + return "string" +} + +func (u *UUID) String() string { + if u == nil { + return "" + } + + return string(*u) +} diff --git a/internal/customtypes/uuid_test.go b/internal/customtypes/uuid_test.go new file mode 100644 index 0000000..e6b2a6f --- /dev/null +++ b/internal/customtypes/uuid_test.go @@ -0,0 +1,52 @@ +package customtypes_test + +import ( + "testing" + + "github.com/pingidentity/pingctl/internal/customtypes" + "github.com/pingidentity/pingctl/internal/testing/testutils" +) + +// Test UUID Set function +func Test_UUID_Set(t *testing.T) { + uuid := new(customtypes.UUID) + + val := "123e4567-e89b-12d3-a456-426614174000" + err := uuid.Set(val) + if err != nil { + t.Errorf("Set returned error: %v", err) + } +} + +// Test Set function fails with invalid value +func Test_UUID_Set_InvalidValue(t *testing.T) { + uuid := new(customtypes.UUID) + + invalidValue := "invalid" + + expectedErrorPattern := `^uuid string is wrong length$` + err := uuid.Set(invalidValue) + testutils.CheckExpectedError(t, err, &expectedErrorPattern) +} + +// Test Set function fails with nil +func Test_UUID_Set_Nil(t *testing.T) { + var uuid *customtypes.UUID + + val := "123e4567-e89b-12d3-a456-426614174000" + + expectedErrorPattern := `^failed to set UUID value: .* UUID is nil$` + err := uuid.Set(val) + testutils.CheckExpectedError(t, err, &expectedErrorPattern) +} + +// Test String function +func Test_UUID_String(t *testing.T) { + uuid := customtypes.UUID("123e4567-e89b-12d3-a456-426614174000") + + expected := "123e4567-e89b-12d3-a456-426614174000" + actual := uuid.String() + if actual != expected { + t.Errorf("String returned: %s, expected: %s", actual, expected) + } +} diff --git a/internal/input/input.go b/internal/input/input.go index 548ed74..e932d46 100644 --- a/internal/input/input.go +++ b/internal/input/input.go @@ -36,3 +36,15 @@ func RunPromptConfirm(message string, rc io.ReadCloser) (bool, error) { return true, nil } + +func RunPromptSelect(message string, items []string, rc io.ReadCloser) (selection string, err error) { + p := promptui.Select{ + Label: message, + Items: items, + Size: len(items), + Stdin: rc, + } + + _, selection, err = p.Run() + return selection, err +} diff --git a/internal/input/input_test.go b/internal/input/input_test.go index c190a97..63337bf 100644 --- a/internal/input/input_test.go +++ b/internal/input/input_test.go @@ -103,3 +103,17 @@ func TestRunPromptConfirmJunkInput(t *testing.T) { t.Errorf("Expected false, but got true") } } + +// Test RunPromptSelect function +func TestRunPromptSelect(t *testing.T) { + testInput := "test-input" + reader := testutils.WriteStringToPipe(fmt.Sprintf("%s\n", testInput), t) + parsedInput, err := RunPromptSelect("test", []string{testInput}, reader) + if err != nil { + t.Errorf("Error running RunPromptSelect: %v", err) + } + + if parsedInput != testInput { + t.Errorf("Expected '%s', but got '%s'", testInput, parsedInput) + } +} diff --git a/internal/output/output.go b/internal/output/output.go index 8c583eb..c6b24db 100644 --- a/internal/output/output.go +++ b/internal/output/output.go @@ -3,8 +3,10 @@ package output import ( "encoding/json" "fmt" + "strconv" "github.com/fatih/color" + "github.com/pingidentity/pingctl/internal/configuration/options" "github.com/pingidentity/pingctl/internal/customtypes" "github.com/pingidentity/pingctl/internal/logger" "github.com/pingidentity/pingctl/internal/profiles" @@ -38,33 +40,24 @@ const ( ) func Print(output Opts) { - profileViper := profiles.GetProfileViper() - var colorizeOutput bool - var outputFormat interface{} - if profileViper != nil { - colorizeOutput = profiles.GetProfileViper().GetBool(profiles.ColorOption.ViperKey) - outputFormat = profiles.GetProfileViper().Get(profiles.OutputOption.ViperKey) + colorizeOutput, err := profiles.GetOptionValue(options.RootColorOption) + if err != nil { + color.NoColor = false } else { - colorizeOutput = true - outputFormat = customtypes.ENUM_OUTPUT_FORMAT_TEXT - } - - if !colorizeOutput { - color.NoColor = true + colorizeOutputBool, err := strconv.ParseBool(colorizeOutput) + if err != nil { + color.NoColor = false + } else { + color.NoColor = !colorizeOutputBool + } } - // Get the output format from viper configuration - // If output format is loaded from file, it is of type string - // if output is loaded from parameter or "config set" it is of type common.OutputFormat - var outputFormatString string - switch format := outputFormat.(type) { - case customtypes.OutputFormat: - outputFormatString = format.String() - case string: - outputFormatString = format + outputFormat, err := profiles.GetOptionValue(options.RootOutputFormatOption) + if err != nil { + outputFormat = customtypes.ENUM_OUTPUT_FORMAT_TEXT } - switch outputFormatString { + switch outputFormat { case customtypes.ENUM_OUTPUT_FORMAT_TEXT: printText(output) case customtypes.ENUM_OUTPUT_FORMAT_JSON: diff --git a/internal/profiles/main_viper.go b/internal/profiles/main_viper.go deleted file mode 100644 index 1164080..0000000 --- a/internal/profiles/main_viper.go +++ /dev/null @@ -1,179 +0,0 @@ -package profiles - -/* The main viper instance should ONLY interact with the configuration file -on disk. No viper overrides, environment variable bindings, or pflag -bindings should be used with this viper instance. This keeps the config -file as the ONLY source of truth for the main viper instance, and prevents -profile drift, as well as active profile drift and other niche bugs. As a -result, much of the logic in this file avoids the use of mainViper.Set(), and -goes out of the way to modify the config file.*/ - -import ( - "bytes" - "encoding/json" - "fmt" - "regexp" - "slices" - "strings" - - "github.com/spf13/viper" -) - -var ( - mainViper *viper.Viper = viper.New() -) - -func GetMainViper() *viper.Viper { - return mainViper -} - -func GetConfigActiveProfile() string { - return mainViper.GetString(ProfileOption.ViperKey) -} - -func SetConfigActiveProfile(pName string) (err error) { - tempViper := viper.New() - tempViper.SetConfigFile(mainViper.ConfigFileUsed()) - if err = tempViper.ReadInConfig(); err != nil { - return err - } - - tempViper.Set(ProfileOption.ViperKey, pName) - if err = tempViper.WriteConfig(); err != nil { - return err - } - - if err = mainViper.ReadInConfig(); err != nil { - return err - } - - return nil -} - -// Get all profile names from config.yaml configuration file -func ConfigProfileNames() (profileKeys []string) { - keySet := make(map[string]struct{}) - allKeys := mainViper.AllKeys() - - for _, key := range allKeys { - //remove "activeProfile" from profileKeys - if strings.EqualFold(key, ProfileOption.ViperKey) { - continue - } - - rootKey := strings.Split(key, ".")[0] - if _, ok := keySet[rootKey]; !ok { - keySet[rootKey] = struct{}{} - profileKeys = append(profileKeys, rootKey) - } - } - - slices.Sort(profileKeys) - return profileKeys -} - -// The profile name must contain only alphanumeric characters, underscores, and dashes -// The profile name cannot be empty -// The profile name must be unique -func ValidateNewProfileName(pName string) (err error) { - if pName == "" { - return fmt.Errorf("invalid profile name: profile name cannot be empty") - } - - if err := ValidateProfileNameFormat(pName); err != nil { - return err - } - - pNames := ConfigProfileNames() - if slices.ContainsFunc(pNames, func(n string) bool { - return strings.EqualFold(n, pName) - }) { - return fmt.Errorf("invalid profile name: '%s' profile already exists", pName) - } - - return nil -} - -func ValidateExistingProfileName(pName string) (err error) { - if pName == "" { - return fmt.Errorf("invalid profile name: profile name cannot be empty") - } - - if err := ValidateProfileNameFormat(pName); err != nil { - return err - } - - pNames := ConfigProfileNames() - if !slices.ContainsFunc(pNames, func(n string) bool { - return strings.EqualFold(n, pName) - }) { - return fmt.Errorf("invalid profile name: '%s' profile does not exist", pName) - } - - return nil -} - -func ValidateProfileNameFormat(pName string) error { - re := regexp.MustCompile(`^[a-zA-Z0-9\_\-]+$`) - if !re.MatchString(pName) { - return fmt.Errorf("invalid profile name: '%s'. name must contain only alphanumeric characters, underscores, and dashes", pName) - } - - return nil -} - -// Viper gives no built-in delete or unset method for keys -// Using this "workaround" described here: https://github.com/spf13/viper/issues/632 -func DeleteConfigProfile(pName string) (err error) { - err = ValidateExistingProfileName(pName) - if err != nil { - return err - } - - if pName == GetConfigActiveProfile() { - return fmt.Errorf("'%s' is the active profile and cannot be deleted", pName) - } - - configMap := mainViper.AllSettings() - delete(configMap, pName) - - encodedConfig, err := json.MarshalIndent(configMap, "", " ") - if err != nil { - return err - } - - err = mainViper.ReadConfig(bytes.NewReader(encodedConfig)) - if err != nil { - return err - } - - err = mainViper.WriteConfig() - if err != nil { - return err - } - - return nil -} - -func SaveProfileViperToFile() (err error) { - tempViper := viper.New() - tempViper.SetConfigFile(mainViper.ConfigFileUsed()) - if err = tempViper.ReadInConfig(); err != nil { - return err - } - - profileKeys := profileViper.AllKeys() - for _, key := range profileKeys { - tempViper.Set(fmt.Sprintf("%s.%s", profileName, key), profileViper.Get(key)) - } - - if err := tempViper.WriteConfig(); err != nil { - return fmt.Errorf("failed to write pingctl configuration to file '%s': %v", mainViper.ConfigFileUsed(), err) - } - - if err := mainViper.ReadInConfig(); err != nil { - return fmt.Errorf("failed to read pingctl configuration from file '%s': %v", mainViper.ConfigFileUsed(), err) - } - - return nil -} diff --git a/internal/profiles/main_viper_test.go b/internal/profiles/main_viper_test.go deleted file mode 100644 index 16c8401..0000000 --- a/internal/profiles/main_viper_test.go +++ /dev/null @@ -1,244 +0,0 @@ -package profiles_test - -import ( - "testing" - - "github.com/pingidentity/pingctl/internal/profiles" - "github.com/pingidentity/pingctl/internal/testing/testutils_viper" -) - -// Test GetMainViper function -func TestGetMainViper(t *testing.T) { - testutils_viper.InitVipers(t) - - v := profiles.GetMainViper() - if v == nil { - t.Errorf("GetMainViper returned nil") - } -} - -// Test GetConfigActiveProfile function -func TestGetConfigActiveProfile(t *testing.T) { - testutils_viper.InitVipers(t) - - profile := profiles.GetConfigActiveProfile() - if profile == "" { - t.Errorf("GetConfigActiveProfile returned empty string") - } - - if profile != "default" { - t.Errorf("GetConfigActiveProfile returned %s, expected 'default'", profile) - } -} - -// Test SetConfigActiveProfile function -func TestSetConfigActiveProfile(t *testing.T) { - testutils_viper.InitVipers(t) - - pName := "test" - - err := profiles.SetConfigActiveProfile(pName) - if err != nil { - t.Errorf("SetConfigActiveProfile returned error: %v", err) - } - - profile := profiles.GetConfigActiveProfile() - if profile != pName { - t.Errorf("GetConfigActiveProfile returned '%s', expected '%s'", profile, pName) - } -} - -// Test ConfigProfileNames function -func TestConfigProfileNames(t *testing.T) { - testutils_viper.InitVipers(t) - - profileKeys := profiles.ConfigProfileNames() - if len(profileKeys) == 0 { - t.Errorf("ConfigProfileNames returned empty slice") - } - - if len(profileKeys) != 2 { - t.Errorf("ConfigProfileNames returned %d profiles, expected 2", len(profileKeys)) - } - - if profileKeys[0] != "default" { - t.Errorf("ConfigProfileNames returned %s, expected 'default'", profileKeys[0]) - } - - if profileKeys[1] != "production" { - t.Errorf("ConfigProfileNames returned %s, expected 'production'", profileKeys[1]) - } -} - -// Test ValidateNewProfileName function -func TestValidateNewProfileName(t *testing.T) { - testutils_viper.InitVipers(t) - - err := profiles.ValidateNewProfileName("") - if err == nil { - t.Errorf("ValidateNewProfileName returned nil, expected error") - } - - err = profiles.ValidateNewProfileName("default") - if err == nil { - t.Errorf("ValidateNewProfileName returned nil, expected error") - } - - err = profiles.ValidateNewProfileName("production") - if err == nil { - t.Errorf("ValidateNewProfileName returned nil, expected error") - } - - err = profiles.ValidateNewProfileName("test") - if err != nil { - t.Errorf("ValidateNewProfileName returned error: %v", err) - } - - err = profiles.ValidateNewProfileName("invalid(*^&^%&%&^$)") - if err == nil { - t.Errorf("ValidateNewProfileName returned nil, expected error") - } -} - -// Test ValidateExistingProfileName function -func TestValidateExistingProfileName(t *testing.T) { - testutils_viper.InitVipers(t) - - err := profiles.ValidateExistingProfileName("") - if err == nil { - t.Errorf("ValidateExistingProfileName returned nil, expected error") - } - - err = profiles.ValidateExistingProfileName("default") - if err != nil { - t.Errorf("ValidateExistingProfileName returned error: %v", err) - } - - err = profiles.ValidateExistingProfileName("production") - if err != nil { - t.Errorf("ValidateExistingProfileName returned error: %v", err) - } - - err = profiles.ValidateExistingProfileName("test") - if err == nil { - t.Errorf("ValidateExistingProfileName returned nil, expected error") - } - - err = profiles.ValidateExistingProfileName("invalid(*^&^%&%&^$)") - if err == nil { - t.Errorf("ValidateExistingProfileName returned nil, expected error") - } -} - -// Test ValidateProfileNameFormat function -func TestValidateProfileNameFormat(t *testing.T) { - err := profiles.ValidateProfileNameFormat("") - if err == nil { - t.Errorf("ValidateProfileNameFormat returned nil, expected error") - } - - err = profiles.ValidateProfileNameFormat("default") - if err != nil { - t.Errorf("ValidateProfileNameFormat returned error: %v", err) - } - - err = profiles.ValidateProfileNameFormat("production") - if err != nil { - t.Errorf("ValidateProfileNameFormat returned error: %v", err) - } - - err = profiles.ValidateProfileNameFormat("test") - if err != nil { - t.Errorf("ValidateProfileNameFormat returned error: %v", err) - } - - err = profiles.ValidateProfileNameFormat("invalid(*^&^%&%&^$)") - if err == nil { - t.Errorf("ValidateProfileNameFormat returned nil, expected error") - } -} - -// Test DeleteConfigProfile function -func TestDeleteConfigProfile(t *testing.T) { - testutils_viper.InitVipers(t) - - err := profiles.DeleteConfigProfile("") - if err == nil { - t.Errorf("DeleteConfigProfile returned nil, expected error") - } - - err = profiles.DeleteConfigProfile("default") - if err == nil { - t.Errorf("DeleteConfigProfile returned nil, expected error") - } - - err = profiles.DeleteConfigProfile("production") - if err != nil { - t.Errorf("DeleteConfigProfile returned error: %v", err) - } - - err = profiles.DeleteConfigProfile("test") - if err == nil { - t.Errorf("DeleteConfigProfile returned nil, expected error") - } - - err = profiles.DeleteConfigProfile("invalid(*^&^%&%&^$)") - if err == nil { - t.Errorf("DeleteConfigProfile returned nil, expected error") - } - - profileKeys := profiles.ConfigProfileNames() - if len(profileKeys) != 1 { - t.Errorf("ConfigProfileNames returned %d profiles, expected 1", len(profileKeys)) - } - - if profileKeys[0] != "default" { - t.Errorf("ConfigProfileNames returned %s, expected 'default'", profileKeys[0]) - } -} - -// Test SaveProfileViperToFile function -func TestSaveProfileViperToFile(t *testing.T) { - testutils_viper.InitVipers(t) - - // Create a new profile - err := profiles.CreateNewProfile("test", "test", true) - if err != nil { - t.Errorf("CreateNewProfile returned error: %v", err) - } - - // Use the new profile - err = profiles.SetConfigActiveProfile("test") - if err != nil { - t.Errorf("SetConfigActiveProfile returned error: %v", err) - } - - err = profiles.SetProfileViperWithProfile("test") - if err != nil { - t.Errorf("SetProfileViperWithProfile returned error: %v", err) - } - - // Save the new profile to file - err = profiles.SaveProfileViperToFile() - if err != nil { - t.Errorf("SaveProfileViperToFile returned error: %v", err) - } - - // Check if the new profile was saved to file - profileKeys := profiles.ConfigProfileNames() - if len(profileKeys) != 3 { - t.Errorf("ConfigProfileNames returned %d profiles, expected 3", len(profileKeys)) - } - - if profileKeys[0] != "default" { - t.Errorf("ConfigProfileNames returned %s, expected 'default'", profileKeys[0]) - } - - if profileKeys[1] != "production" { - t.Errorf("ConfigProfileNames returned %s, expected 'production'", profileKeys[1]) - } - - if profileKeys[2] != "test" { - t.Errorf("ConfigProfileNames returned %s, expected 'test'", profileKeys[2]) - } -} diff --git a/internal/profiles/profile_viper.go b/internal/profiles/profile_viper.go deleted file mode 100644 index 1b54e7a..0000000 --- a/internal/profiles/profile_viper.go +++ /dev/null @@ -1,131 +0,0 @@ -package profiles - -import ( - "fmt" - - "github.com/spf13/pflag" - "github.com/spf13/viper" -) - -type Binding struct { - Option Option - Flag *pflag.Flag -} - -var ( - profileViper *viper.Viper - profileName string - flagBindings []Binding - pFlagBindings []Binding - envVarBindings []Option -) - -func SetProfileViperWithProfile(pName string) (err error) { - if err := ValidateExistingProfileName(pName); err != nil { - return err - } - - subViper := mainViper.Sub(pName) - if subViper == nil { - return fmt.Errorf("profile '%s' not found in configuration file: %s", pName, mainViper.ConfigFileUsed()) - } - - profileViper = subViper - profileName = pName - return nil -} - -func SetProfileViperWithViper(v *viper.Viper, pName string) { - profileViper = v - profileName = pName -} - -func GetProfileViper() *viper.Viper { - return profileViper -} - -func AddFlagBinding(binding Binding) { - flagBindings = append(flagBindings, binding) -} - -func AddPFlagBinding(binding Binding) { - pFlagBindings = append(pFlagBindings, binding) -} - -func AddEnvVarBinding(opt Option) { - envVarBindings = append(envVarBindings, opt) -} - -func ApplyBindingsToProfileViper() (err error) { - for _, binding := range flagBindings { - err = profileViper.BindPFlag(binding.Option.ViperKey, binding.Flag) - if err != nil { - return err - } - } - - for _, binding := range pFlagBindings { - err = profileViper.BindPFlag(binding.Option.ViperKey, binding.Flag) - if err != nil { - return err - } - } - - for _, opt := range envVarBindings { - err = profileViper.BindEnv(opt.ViperKey, opt.EnvVar) - if err != nil { - return err - } - } - return nil -} - -func CreateNewProfile(pName, desc string, setActive bool) (err error) { - err = ValidateNewProfileName(pName) - if err != nil { - return err - } - - oldProfileName := profileName - newProfileViper := viper.New() - - for _, opt := range ConfigOptions.Options { - if opt.ViperKey == ProfileOption.ViperKey { - continue - } - - // Set all options to their default state - defVal, err := GetDefaultValue(opt.Type) - if err != nil { - return fmt.Errorf("failed to create new profile: %v", err) - } - newProfileViper.Set(opt.ViperKey, defVal) - } - - // set the new profile description - newProfileViper.Set(ProfileDescriptionOption.ViperKey, desc) - - // set the new viper as the profile viper - SetProfileViperWithViper(newProfileViper, pName) - - // save the new profile to the configuration file - if err := SaveProfileViperToFile(); err != nil { - return fmt.Errorf("failed to create new profile: %v", err) - } - - // set the profile viper back to the old profile if it existed - if oldProfileName != "" { - if err = SetProfileViperWithProfile(oldProfileName); err != nil { - return fmt.Errorf("failed to create new profile: %v", err) - } - } - - // set the new profile as the active profile if applicable - if setActive { - if err = SetConfigActiveProfile(pName); err != nil { - return fmt.Errorf("failed to create new profile: %v", err) - } - } - - return nil -} diff --git a/internal/profiles/profile_viper_test.go b/internal/profiles/profile_viper_test.go deleted file mode 100644 index cd2023d..0000000 --- a/internal/profiles/profile_viper_test.go +++ /dev/null @@ -1,152 +0,0 @@ -package profiles_test - -import ( - "testing" - - "github.com/pingidentity/pingctl/internal/profiles" - "github.com/pingidentity/pingctl/internal/testing/testutils_viper" - "github.com/spf13/viper" -) - -// Test SetProfileViperWithProfile function -func TestSetProfileViperWithProfile(t *testing.T) { - testutils_viper.InitVipers(t) - - // Test SetProfileViperWithProfile function - err := profiles.SetProfileViperWithProfile("default") - if err != nil { - t.Errorf("SetProfileViperWithProfile returned error: %v", err) - } - - profileViper := profiles.GetProfileViper() - if profileViper == nil { - t.Errorf("GetProfileViper returned nil") - } - - val := profileViper.Get(profiles.ProfileDescriptionOption.ViperKey) - if val == nil { - t.Errorf("Get returned nil") - } - if val != "default description" { - t.Errorf("Get returned %s, expected 'default description'", val) - } - - err = profiles.SetProfileViperWithProfile("production") - if err != nil { - t.Errorf("SetProfileViperWithProfile returned error: %v", err) - } - - err = profiles.SetProfileViperWithProfile("test") - if err == nil { - t.Errorf("SetProfileViperWithProfile returned nil, expected error") - } - - err = profiles.SetProfileViperWithProfile("invalid(*^&^%&%&^$)") - if err == nil { - t.Errorf("SetProfileViperWithProfile returned nil, expected error") - } -} - -// Test SetProfileViperWithViper function -func TestSetProfileViperWithViper(t *testing.T) { - testutils_viper.InitVipers(t) - - testDesc := "test viper description" - - // Create new viper - testViper := viper.New() - testViper.Set(profiles.ProfileDescriptionOption.ViperKey, testDesc) - - // Test SetProfileViperWithViper function - profiles.SetProfileViperWithViper(testViper, "test") - profileViper := profiles.GetProfileViper() - - val := profileViper.Get(profiles.ProfileDescriptionOption.ViperKey) - if val == nil { - t.Errorf("Get returned nil") - } - if val != testDesc { - t.Errorf("Get returned %s, expected %s", val, testDesc) - } -} - -// Test GetProfileViper function -func TestGetProfileViper(t *testing.T) { - testutils_viper.InitVipers(t) - - // Test GetProfileViper function - profileViper := profiles.GetProfileViper() - if profileViper == nil { - t.Errorf("GetProfileViper returned nil") - } - - val := profileViper.Get(profiles.ProfileDescriptionOption.ViperKey) - if val == nil { - t.Errorf("Get returned nil") - } - if val != "default description" { - t.Errorf("Get returned %s, expected 'default description'", val) - } -} - -// Test CreateNewProfile function -func TestCreateNewProfile(t *testing.T) { - testutils_viper.InitVipers(t) - - // Test CreateNewProfile function - err := profiles.CreateNewProfile("test", "test description", false) - if err != nil { - t.Errorf("CreateNewProfile returned error: %v", err) - } - - profileViper := profiles.GetProfileViper() - if profileViper == nil { - t.Errorf("GetProfileViper returned nil") - } - - val := profileViper.Get(profiles.ProfileDescriptionOption.ViperKey) - if val == nil { - t.Errorf("Get returned nil") - } - if val != "default description" { - t.Errorf("Get returned %s, expected 'default description'", val) - } - - // Test CreateNewProfile function with setActive true - err = profiles.CreateNewProfile("test2", "test description 2", true) - if err != nil { - t.Errorf("CreateNewProfile returned error: %v", err) - } - - // Use the new profile - err = profiles.SetProfileViperWithProfile("test2") - if err != nil { - t.Errorf("SetProfileViperWithProfile returned error: %v", err) - } - - profileViper = profiles.GetProfileViper() - if profileViper == nil { - t.Errorf("GetProfileViper returned nil") - } - - val = profileViper.Get(profiles.ProfileDescriptionOption.ViperKey) - if val == nil { - t.Errorf("Get returned nil") - } - if val != "test description 2" { - t.Errorf("Get returned %s, expected 'test description 2'", val) - } - - // CHeck profile names - profileKeys := profiles.ConfigProfileNames() - if len(profileKeys) != 4 { - t.Errorf("ConfigProfileNames returned %d profiles, expected 4", len(profileKeys)) - } - - //Check active profile - activeProfile := profiles.GetConfigActiveProfile() - if activeProfile != "test2" { - t.Errorf("GetConfigActiveProfile returned %s, expected 'test2'", activeProfile) - } - -} diff --git a/internal/profiles/types.go b/internal/profiles/types.go deleted file mode 100644 index 0e0ee56..0000000 --- a/internal/profiles/types.go +++ /dev/null @@ -1,250 +0,0 @@ -package profiles - -import ( - "fmt" - "slices" - "strings" - - "github.com/pingidentity/pingctl/internal/customtypes" -) - -type ConfigOpts struct { - Options []Option -} - -type Option struct { - CobraParamName string - ViperKey string - EnvVar string - Type OptionType -} - -type OptionType string - -// Variable type enums -const ( - ENUM_BOOL OptionType = "ENUM_BOOL" - ENUM_ID OptionType = "ENUM_ID" - ENUM_OUTPUT_FORMAT OptionType = "ENUM_OUTPUT_FORMAT" - ENUM_PINGONE_REGION OptionType = "ENUM_PINGONE_REGION" - ENUM_STRING OptionType = "ENUM_STRING" - ENUM_STRING_SLICE OptionType = "ENUM_STRING_SLICE" -) - -var ( - OutputOption = Option{ - CobraParamName: "output-format", - ViperKey: "pingctl.outputFormat", - EnvVar: "PINGCTL_OUTPUT_FORMAT", - Type: ENUM_OUTPUT_FORMAT, - } - ColorOption = Option{ - CobraParamName: "color", - ViperKey: "pingctl.color", - EnvVar: "PINGCTL_COLOR", - Type: ENUM_BOOL, - } - ProfileOption = Option{ - CobraParamName: "active-profile", - ViperKey: "activeProfile", - EnvVar: "PINGCTL_ACTIVE_PROFILE", - Type: ENUM_STRING, - } - ProfileDescriptionOption = Option{ - CobraParamName: "description", - ViperKey: "description", - Type: ENUM_STRING, - } - PingOneExportEnvironmentIDOption = Option{ - CobraParamName: "pingone-export-environment-id", - ViperKey: "pingone.export.environmentID", - EnvVar: "PINGCTL_PINGONE_EXPORT_ENVIRONMENT_ID", - Type: ENUM_ID, - } - PingOneWorkerEnvironmentIDOption = Option{ - CobraParamName: "pingone-worker-environment-id", - ViperKey: "pingone.worker.environmentID", - EnvVar: "PINGCTL_PINGONE_WORKER_ENVIRONMENT_ID", - Type: ENUM_ID, - } - PingOneWorkerClientIDOption = Option{ - CobraParamName: "pingone-worker-client-id", - ViperKey: "pingone.worker.clientID", - EnvVar: "PINGCTL_PINGONE_WORKER_CLIENT_ID", - Type: ENUM_ID, - } - PingOneWorkerClientSecretOption = Option{ - CobraParamName: "pingone-worker-client-secret", - ViperKey: "pingone.worker.clientSecret", - EnvVar: "PINGCTL_PINGONE_WORKER_CLIENT_SECRET", - Type: ENUM_STRING, - } - PingOneRegionOption = Option{ - CobraParamName: "pingone-region", - ViperKey: "pingone.region", - EnvVar: "PINGCTL_PINGONE_REGION", - Type: ENUM_PINGONE_REGION, - } - PingFederateUsernameOption = Option{ - CobraParamName: "pingfederate-username", - ViperKey: "pingfederate.basicAuth.username", - EnvVar: "PINGCTL_PINGFEDERATE_USERNAME", - Type: ENUM_STRING, - } - PingFederatePasswordOption = Option{ - CobraParamName: "pingfederate-password", - ViperKey: "pingfederate.basicAuth.password", - EnvVar: "PINGCTL_PINGFEDERATE_PASSWORD", - Type: ENUM_STRING, - } - PingFederateHttpsHostOption = Option{ - CobraParamName: "pingfederate-https-host", - ViperKey: "pingfederate.httpsHost", - EnvVar: "PINGCTL_PINGFEDERATE_HTTPS_HOST", - Type: ENUM_STRING, - } - PingFederateAdminApiPathOption = Option{ - CobraParamName: "pingfederate-admin-api-path", - ViperKey: "pingfederate.adminApiPath", - EnvVar: "PINGCTL_PINGFEDERATE_ADMIN_API_PATH", - Type: ENUM_STRING, - } - PingFederateClientIDOption = Option{ - CobraParamName: "pingfederate-client-id", - ViperKey: "pingfederate.clientCredentialsAuth.clientID", - EnvVar: "PINGCTL_PINGFEDERATE_CLIENT_ID", - Type: ENUM_STRING, - } - PingFederateClientSecretOption = Option{ - CobraParamName: "pingfederate-client-secret", - ViperKey: "pingfederate.clientCredentialsAuth.clientSecret", - EnvVar: "PINGCTL_PINGFEDERATE_CLIENT_SECRET", - Type: ENUM_STRING, - } - PingFederateTokenURLOption = Option{ - CobraParamName: "pingfederate-token-url", - ViperKey: "pingfederate.clientCredentialsAuth.tokenURL", - EnvVar: "PINGCTL_PINGFEDERATE_TOKEN_URL", - Type: ENUM_STRING, - } - PingFederateScopesOption = Option{ - CobraParamName: "pingfederate-scopes", - ViperKey: "pingfederate.clientCredentialsAuth.scopes", - EnvVar: "PINGCTL_PINGFEDERATE_SCOPES", - Type: ENUM_STRING_SLICE, - } - PingFederateAccessTokenOption = Option{ - CobraParamName: "pingfederate-access-token", - ViperKey: "pingfederate.accessTokenAuth.accessToken", - EnvVar: "PINGCTL_PINGFEDERATE_ACCESS_TOKEN", - Type: ENUM_STRING, - } - PingFederateXBypassExternalValidationHeaderOption = Option{ - CobraParamName: "pingfederate-x-bypass-external-validation-header", - ViperKey: "pingfederate.xBypassExternalValidationHeader", - EnvVar: "PINGCTL_PINGFEDERATE_X_BYPASS_EXTERNAL_VALIDATION_HEADER", - Type: ENUM_BOOL, - } - PingFederateCACertificatePemFilesOption = Option{ - CobraParamName: "pingfederate-ca-certificate-pem-files", - ViperKey: "pingfederate.caCertificatePemFiles", - EnvVar: "PINGCTL_PINGFEDERATE_CA_CERTIFICATE_PEM_FILES", - Type: ENUM_STRING_SLICE, - } - PingFederateInsecureTrustAllTLSOption = Option{ - CobraParamName: "pingfederate-insecure-trust-all-tls", - ViperKey: "pingfederate.insecureTrustAllTLS", - EnvVar: "PINGCTL_PINGFEDERATE_INSECURE_TRUST_ALL_TLS", - Type: ENUM_BOOL, - } - - ConfigOptions = ConfigOpts{ - Options: []Option{ - OutputOption, - ColorOption, - ProfileOption, - PingOneExportEnvironmentIDOption, - PingOneWorkerEnvironmentIDOption, - PingOneWorkerClientIDOption, - PingOneWorkerClientSecretOption, - PingOneRegionOption, - ProfileDescriptionOption, - PingFederateUsernameOption, - PingFederatePasswordOption, - PingFederateHttpsHostOption, - PingFederateAdminApiPathOption, - PingFederateClientIDOption, - PingFederateClientSecretOption, - PingFederateTokenURLOption, - PingFederateScopesOption, - PingFederateAccessTokenOption, - PingFederateXBypassExternalValidationHeaderOption, - PingFederateCACertificatePemFilesOption, - PingFederateInsecureTrustAllTLSOption, - }, - } -) - -// Return a list of all viper keys from Options defined in @ConfigOptions -func ProfileKeys() (keys []string) { - for _, option := range ConfigOptions.Options { - if option.ViperKey == ProfileOption.ViperKey { - continue - } - - keys = append(keys, option.ViperKey) - } - - slices.Sort(keys) - return keys -} - -// Return a list of all viper keys from Options defined in @ConfigOptions -// Including all substrings of parent keys. -// For example, the option key export.environmentID adds the keys -// 'export' and 'export.environmentID' to the list. -func ExpandedProfileKeys() (keys []string) { - leafKeys := ProfileKeys() - for _, key := range leafKeys { - keySplit := strings.Split(key, ".") - for i := 0; i < len(keySplit); i++ { - curKey := strings.Join(keySplit[:i+1], ".") - if !slices.ContainsFunc(keys, func(v string) bool { - return strings.EqualFold(v, curKey) - }) { - keys = append(keys, curKey) - } - } - } - - slices.Sort(keys) - return keys -} - -func OptionTypeFromViperKey(key string) (optType OptionType, ok bool) { - for _, opt := range ConfigOptions.Options { - if strings.EqualFold(opt.ViperKey, key) { - return opt.Type, true - } - } - return "", false -} - -func GetDefaultValue(optType OptionType) (val any, err error) { - switch optType { - case ENUM_BOOL: - return false, nil - case ENUM_ID: - return "", nil - case ENUM_OUTPUT_FORMAT: - return customtypes.OutputFormat("text"), nil - case ENUM_PINGONE_REGION: - return customtypes.PingOneRegion(""), nil - case ENUM_STRING: - return "", nil - case ENUM_STRING_SLICE: - return []string{}, nil - default: - return nil, fmt.Errorf("failed to get default value: invalid option type %s", optType) - } -} diff --git a/internal/profiles/types_test.go b/internal/profiles/types_test.go deleted file mode 100644 index 98a52f2..0000000 --- a/internal/profiles/types_test.go +++ /dev/null @@ -1,164 +0,0 @@ -package profiles_test - -import ( - "slices" - "testing" - - "github.com/pingidentity/pingctl/internal/customtypes" - "github.com/pingidentity/pingctl/internal/profiles" - "github.com/pingidentity/pingctl/internal/testing/testutils_viper" -) - -// Test ProfileKeys function -func TestProfileKeys(t *testing.T) { - testutils_viper.InitVipers(t) - - expectedNumKeys := len(profiles.ConfigOptions.Options) - 1 - - // Test ProfileKeys function - keys := profiles.ProfileKeys() - if len(keys) != expectedNumKeys { - t.Errorf("Expected %d keys, but got %d", expectedNumKeys, len(keys)) - } - - // Make sure profile option is not in the list - for _, key := range keys { - if key == profiles.ProfileOption.ViperKey { - t.Errorf("Profile option key should not be in the list") - } - } - - // Check random profile key is in the list - if !slices.Contains(keys, profiles.PingOneWorkerClientIDOption.ViperKey) { - t.Errorf("Expected key %s in the list", profiles.PingOneWorkerClientIDOption.ViperKey) - } - - // Make sure the list is sorted - if !slices.IsSorted(keys) { - t.Errorf("Keys are not sorted") - } -} - -// Test ExpandedProfileKeys function -func TestExpandedProfileKeys(t *testing.T) { - testutils_viper.InitVipers(t) - - // Test ExpandedProfileKeys function - keys := profiles.ExpandedProfileKeys() - - // Make sure keys is not empty - if len(keys) == 0 { - t.Errorf("Keys should not be empty") - } - - // Make sure profile option is not in the list - for _, key := range keys { - if key == profiles.ProfileOption.ViperKey { - t.Errorf("Profile option key should not be in the list") - } - } - - // Check random profile key is in the list - if !slices.Contains(keys, profiles.PingOneWorkerClientIDOption.ViperKey) { - t.Errorf("Expected key %s in the list", profiles.PingOneWorkerClientIDOption.ViperKey) - } - - // Check random parent profile key is in the list - if !slices.Contains(keys, "pingctl") { - t.Errorf("Expected key %s in the list", "pingctl") - } - - // Make sure the list is sorted - if !slices.IsSorted(keys) { - t.Errorf("Keys are not sorted") - } -} - -// Test OptionTypeFromViperKey function -func TestOptionTypeFromViperKey(t *testing.T) { - // Test OptionTypeFromViperKey function - optType, ok := profiles.OptionTypeFromViperKey(profiles.PingOneWorkerClientIDOption.ViperKey) - if !ok { - t.Errorf("Expected key %s to be found", profiles.PingOneWorkerClientIDOption.ViperKey) - } - - // Check the type of the option - if optType != profiles.PingOneWorkerClientIDOption.Type { - t.Errorf("Expected type %s, but got %s", profiles.PingOneWorkerClientIDOption.Type, optType) - } - - // Check random key is not found - _, ok = profiles.OptionTypeFromViperKey("random") - if ok { - t.Errorf("Expected key %s to not be found", "random") - } -} - -// Test GetDefaultValue function -func TestGetDefaultValue(t *testing.T) { - // Test GetDefaultValue function with bool type - val, err := profiles.GetDefaultValue(profiles.ENUM_BOOL) - if err != nil { - t.Errorf("Expected error %v, but got %v", nil, err) - } - b, ok := val.(bool) - if !ok || b != false { - t.Errorf("Expected value %v, but got %v", false, val) - } - - // Test GetDefaultValue function with string type - val, err = profiles.GetDefaultValue(profiles.ENUM_STRING) - if err != nil { - t.Errorf("Expected error %v, but got %v", nil, err) - } - s, ok := val.(string) - if !ok || s != "" { - t.Errorf("Expected value %v, but got %v", "", val) - } - - // Test GetDefaultValue function with string slice type - val, err = profiles.GetDefaultValue(profiles.ENUM_STRING_SLICE) - if err != nil { - t.Errorf("Expected error %v, but got %v", nil, err) - } - ss, ok := val.([]string) - if !ok || !slices.Equal(ss, []string{}) { - t.Errorf("Expected value %v, but got %v", []string{}, val) - } - - // Test GetDefaultValue function with UUID type - val, err = profiles.GetDefaultValue(profiles.ENUM_ID) - if err != nil { - t.Errorf("Expected error %v, but got %v", nil, err) - } - s, ok = val.(string) - if !ok || s != "" { - t.Errorf("Expected value %v, but got %v", "", val) - } - - // Test GetDefaultValue function with output format type - val, err = profiles.GetDefaultValue(profiles.ENUM_OUTPUT_FORMAT) - if err != nil { - t.Errorf("Expected error %v, but got %v", nil, err) - } - o, ok := val.(customtypes.OutputFormat) - if !ok || o != customtypes.OutputFormat("text") { - t.Errorf("Expected value %v, but got %v", "text", val) - } - - // Test GetDefaultValue function with pingone region type - val, err = profiles.GetDefaultValue(profiles.ENUM_PINGONE_REGION) - if err != nil { - t.Errorf("Expected error %v, but got %v", nil, err) - } - p, ok := val.(customtypes.PingOneRegion) - if !ok || p != customtypes.PingOneRegion("") { - t.Errorf("Expected value %v, but got %v", "", val) - } - - // Test GetDefaultValue function with random type - _, err = profiles.GetDefaultValue("random") - if err == nil { - t.Errorf("Expected error, but got %v", nil) - } -} diff --git a/internal/profiles/validate.go b/internal/profiles/validate.go index 3de53ba..1dbf854 100644 --- a/internal/profiles/validate.go +++ b/internal/profiles/validate.go @@ -5,14 +5,15 @@ import ( "slices" "strings" - "github.com/hashicorp/go-uuid" + "github.com/pingidentity/pingctl/internal/configuration" + "github.com/pingidentity/pingctl/internal/configuration/options" "github.com/pingidentity/pingctl/internal/customtypes" "github.com/spf13/viper" ) func Validate() error { // Get a slice of all profile names configured in the config.yaml file - profileNames := ConfigProfileNames() + profileNames := GetMainConfig().ProfileNames() // Validate profile names if err := validateProfileNames(profileNames); err != nil { @@ -20,21 +21,21 @@ func Validate() error { } // Make sure selected active profile is in the configuration file - activeProfile := GetConfigActiveProfile() + activeProfile := GetMainConfig().ActiveProfile().Name() if !slices.Contains(profileNames, activeProfile) { - return fmt.Errorf("failed to validate pingctl configuration: active profile '%s' not found in configuration file %s", activeProfile, mainViper.ConfigFileUsed()) + return fmt.Errorf("failed to validate pingctl configuration: active profile '%s' not found in configuration file %s", activeProfile, GetMainConfig().ViperInstance().ConfigFileUsed()) } // for each profile key, set the profile based on mainViper.Sub() and validate the profile - for _, name := range profileNames { - v := mainViper.Sub(name) + for _, pName := range profileNames { + subViper := GetMainConfig().ViperInstance().Sub(pName) - if err := validateProfileKeys(name, v); err != nil { - return err + if err := validateProfileKeys(pName, subViper); err != nil { + return fmt.Errorf("failed to validate pingctl configuration: %v", err) } - if err := validateProfileValues(v); err != nil { - return err + if err := validateProfileValues(pName, subViper); err != nil { + return fmt.Errorf("failed to validate pingctl configuration: %v", err) } } @@ -43,7 +44,7 @@ func Validate() error { func validateProfileNames(profileNames []string) error { for _, profileName := range profileNames { - if err := ValidateProfileNameFormat(profileName); err != nil { + if err := GetMainConfig().ValidateProfileNameFormat(profileName); err != nil { return err } } @@ -51,7 +52,7 @@ func validateProfileNames(profileNames []string) error { } func validateProfileKeys(profileName string, profileViper *viper.Viper) error { - validProfileKeys := ProfileKeys() + validProfileKeys := configuration.ViperKeys() // Get all keys viper has loaded from config file. // If a key found in the config file is not in the viperKeys list, @@ -68,159 +69,123 @@ func validateProfileKeys(profileName string, profileViper *viper.Viper) error { if len(invalidKeys) > 0 { invalidKeysStr := strings.Join(invalidKeys, ", ") validKeysStr := strings.Join(validProfileKeys, ", ") - return fmt.Errorf("failed to validate pingctl configuration: invalid configuration key(s) found in profile %s: %s\nMust use one of: %s", profileName, invalidKeysStr, validKeysStr) + return fmt.Errorf("invalid configuration key(s) found in profile %s: %s\nMust use one of: %s", profileName, invalidKeysStr, validKeysStr) } return nil } -func validateProfileValues(profileViper *viper.Viper) error { - // Go through all valid profile keys, - // and set their values by value type set in the ConfigOptions map - // This will validate the viper configuration hierarchy. - // NOTE: IF there are invalid values in the config file, but they are overwritten by - // an env var or flag, the invalid values will not be caught here. - for _, opt := range ConfigOptions.Options { - viperKey := opt.ViperKey - if viperKey == ProfileOption.ViperKey { - continue +func validateProfileValues(pName string, profileViper *viper.Viper) (err error) { + for _, key := range profileViper.AllKeys() { + opt, err := configuration.OptionFromViperKey(key) + if err != nil { + return err } + + vValue := profileViper.Get(key) + switch opt.Type { - case ENUM_BOOL: - if err := validateBool(profileViper, viperKey); err != nil { - return err - } - case ENUM_ID: - if err := validateUUID(profileViper, viperKey); err != nil { - return err + case options.ENUM_BOOL: + switch typedValue := vValue.(type) { + case *customtypes.Bool: + continue + case string: + b := customtypes.Bool(false) + if err = b.Set(typedValue); err != nil { + return fmt.Errorf("profile '%s': variable type '%T' for key '%s' is not a boolean value: %v", pName, typedValue, key, err) + } + case bool: + continue + default: + return fmt.Errorf("profile '%s': variable type %T for key '%s' is not a boolean value", pName, typedValue, key) } - case ENUM_OUTPUT_FORMAT: - if err := validateOutputFormat(profileViper, viperKey); err != nil { - return err + case options.ENUM_UUID: + switch typedValue := vValue.(type) { + case *customtypes.UUID: + continue + case string: + u := customtypes.UUID("") + if err = u.Set(typedValue); err != nil { + return fmt.Errorf("profile '%s': variable type '%T' for key '%s' is not a UUID value: %v", pName, typedValue, key, err) + } + default: + return fmt.Errorf("profile '%s': variable type %T for key '%s' is not a UUID value", pName, typedValue, key) } - case ENUM_PINGONE_REGION: - if err := validatePingOneRegion(profileViper, viperKey); err != nil { - return err + case options.ENUM_OUTPUT_FORMAT: + switch typedValue := vValue.(type) { + case *customtypes.OutputFormat: + continue + case string: + o := customtypes.OutputFormat("") + if err = o.Set(typedValue); err != nil { + return fmt.Errorf("profile '%s': variable type '%T' for key '%s' is not an output format value: %v", pName, typedValue, key, err) + } + default: + return fmt.Errorf("profile '%s': variable type %T for key '%s' is not an output format value", pName, typedValue, key) } - case ENUM_STRING: - if err := validateString(profileViper, viperKey); err != nil { - return err + case options.ENUM_PINGONE_REGION: + switch typedValue := vValue.(type) { + case *customtypes.PingOneRegion: + continue + case string: + p := customtypes.PingOneRegion("") + if err = p.Set(typedValue); err != nil { + return fmt.Errorf("profile '%s': variable type '%T' for key '%s' is not a PingOne region value: %v", pName, typedValue, key, err) + } + default: + return fmt.Errorf("profile '%s': variable type %T for key '%s' is not a PingOne region value", pName, typedValue, key) } - case ENUM_STRING_SLICE: - if err := validateStringSlice(profileViper, viperKey); err != nil { - return err + case options.ENUM_STRING: + switch typedValue := vValue.(type) { + case *customtypes.String: + continue + case string: + s := customtypes.String("") + if err = s.Set(typedValue); err != nil { + return fmt.Errorf("profile '%s': variable type '%T' for key '%s' is not a string value: %v", pName, typedValue, key, err) + } + default: + return fmt.Errorf("profile '%s': variable type %T for key '%s' is not a string value", pName, typedValue, key) } - default: - return fmt.Errorf("failed to validate pingctl configuration: variable type for key '%s' is not recognized", viperKey) - } - } - return nil -} - -func validateBool(profileViper *viper.Viper, viperKey string) error { - value := profileViper.Get(viperKey) - switch valueBool := value.(type) { - case bool: - profileViper.Set(viperKey, valueBool) - default: - return fmt.Errorf("failed to validate pingctl configuration: variable type for key '%s' is not a boolean value", viperKey) - } - return nil -} - -func validateUUID(profileViper *viper.Viper, viperKey string) error { - value := profileViper.Get(viperKey) - switch valueUUID := value.(type) { - case string: - // Check string is in the form of a UUID or empty - if valueUUID == "" { - profileViper.Set(viperKey, valueUUID) - return nil - } - - if _, err := uuid.ParseUUID(valueUUID); err != nil { - return fmt.Errorf("failed to validate pingctl configuration: value for key '%s' must be a valid UUID", viperKey) - } - profileViper.Set(viperKey, valueUUID) - default: - return fmt.Errorf("failed to validate pingctl configuration: value for key '%s' is not a valid UUID", viperKey) - } - return nil -} - -func validateOutputFormat(profileViper *viper.Viper, viperKey string) error { - value := profileViper.Get(viperKey) - switch valueOutputFormat := value.(type) { - case customtypes.OutputFormat: - profileViper.Set(viperKey, valueOutputFormat) - case string: - outputFormat := customtypes.OutputFormat("") - if valueOutputFormat != "" { // Allow empty string for output format validation - if err := outputFormat.Set(valueOutputFormat); err != nil { - return fmt.Errorf("failed to validate pingctl configuration: %s", err.Error()) + case options.ENUM_STRING_SLICE: + switch typedValue := vValue.(type) { + case *customtypes.StringSlice: + continue + case string: + ss := customtypes.StringSlice([]string{}) + if err = ss.Set(typedValue); err != nil { + return fmt.Errorf("profile '%s': variable type '%T' for key '%s' is not a string slice value: %v", pName, typedValue, key, err) + } + default: + return fmt.Errorf("profile '%s': variable type %T for key '%s' is not a string slice value", pName, typedValue, key) } - } - profileViper.Set(viperKey, outputFormat) - default: - return fmt.Errorf("failed to validate pingctl configuration: value for key '%s' is not a valid output format. Must use one of: %s", viperKey, strings.Join(customtypes.OutputFormatValidValues(), ", ")) - } - return nil -} - -func validatePingOneRegion(profileViper *viper.Viper, viperKey string) error { - value := profileViper.Get(viperKey) - switch valuePingoneRegion := value.(type) { - case customtypes.PingOneRegion: - profileViper.Set(viperKey, valuePingoneRegion) - case string: - region := customtypes.PingOneRegion("") - if valuePingoneRegion != "" { // Allow empty string for pingone region validation - if err := region.Set(valuePingoneRegion); err != nil { - return fmt.Errorf("failed to validate pingctl configuration: %s", err.Error()) + case options.ENUM_MULTI_SERVICE: + switch typedValue := vValue.(type) { + case *customtypes.MultiService: + continue + case string: + ms := customtypes.NewMultiService() + if err = ms.Set(typedValue); err != nil { + return fmt.Errorf("profile '%s': variable type '%T' for key '%s' is not a multi-service value: %v", pName, typedValue, key, err) + } + default: + return fmt.Errorf("profile '%s': variable type %T for key '%s' is not a multi-service value", pName, typedValue, key) } - } - profileViper.Set(viperKey, region) - default: - return fmt.Errorf("failed to validate pingctl configuration: value for key '%s' is not a valid PingOne region. Must use one of: %s", viperKey, strings.Join(customtypes.PingOneRegionValidValues(), ", ")) - } - return nil -} - -func validateString(profileViper *viper.Viper, viperKey string) error { - value := profileViper.Get(viperKey) - switch valueString := value.(type) { - case string: - profileViper.Set(viperKey, valueString) - default: - return fmt.Errorf("failed to validate pingctl configuration: variable type for key '%s' is not string", viperKey) - } - return nil -} - -func validateStringSlice(profileViper *viper.Viper, viperKey string) error { - value := profileViper.Get(viperKey) - switch valueStringSlice := value.(type) { - case string: - ss := []string{} - s := strings.TrimSpace(valueStringSlice) - for _, v := range strings.Split(s, ",") { - ss = append(ss, strings.TrimSpace(v)) - } - profileViper.Set(viperKey, ss) - case []string: - profileViper.Set(viperKey, valueStringSlice) - case []interface{}: - ss := []string{} - for _, v := range valueStringSlice { - switch valueString := v.(type) { + case options.ENUM_EXPORT_FORMAT: + switch typedValue := vValue.(type) { + case *customtypes.ExportFormat: + continue case string: - ss = append(ss, valueString) + ef := customtypes.ExportFormat("") + if err = ef.Set(typedValue); err != nil { + return fmt.Errorf("profile '%s': variable type '%T' for key '%s' is not an export format value: %v", pName, typedValue, key, err) + } default: - return fmt.Errorf("failed to validate pingctl configuration: variable type for key '%s' is not a string slice", viperKey) + return fmt.Errorf("profile '%s': variable type %T for key '%s' is not an export format value", pName, typedValue, key) } + default: + return fmt.Errorf("profile '%s': variable type '%s' for key '%s' is not recognized", pName, opt.Type, key) } - profileViper.Set(viperKey, ss) - default: - return fmt.Errorf("failed to validate pingctl configuration: variable type for key '%s' is not a string slice", viperKey) } + return nil } diff --git a/internal/profiles/validate_test.go b/internal/profiles/validate_test.go index a373742..33bcb9e 100644 --- a/internal/profiles/validate_test.go +++ b/internal/profiles/validate_test.go @@ -22,17 +22,9 @@ func TestValidateInvalidProfile(t *testing.T) { fileContents := `activeProfile: default default: description: "default description" - pingctl: - color: true - outputFormat: text pingone: export: - environmentid: "invalid" - region: "" - worker: - clientid: "" - clientsecret: "" - environmentid: ""` + environmentid: "invalid"` testutils_viper.InitVipersCustomFile(t, fileContents) @@ -47,42 +39,8 @@ func TestValidateInvalidRegion(t *testing.T) { fileContents := `activeProfile: default default: description: "default description" - pingctl: - color: true - outputFormat: text - pingone: - export: - environmentid: "" - region: "invalid" - worker: - clientid: "" - clientsecret: "" - environmentid: ""` - - testutils_viper.InitVipersCustomFile(t, fileContents) - - err := profiles.Validate() - if err == nil { - t.Errorf("Validate returned nil, expected error") - } -} - -// Test Validate function with invalid active profile -func TestValidateInvalidActiveProfile(t *testing.T) { - fileContents := `activeProfile: invalid -default: - description: "default description" - pingctl: - color: true - outputFormat: text pingone: - export: - environmentid: "" - region: "" - worker: - clientid: "" - clientsecret: "" - environmentid: ""` + region: "invalid"` testutils_viper.InitVipersCustomFile(t, fileContents) @@ -94,20 +52,11 @@ default: // Test Validate function with invalid bool func TestValidateInvalidBool(t *testing.T) { - fileContents := `activeProfile: invalid + fileContents := `activeProfile: default default: description: "default description" pingctl: - color: invalid - outputFormat: text - pingone: - export: - environmentid: "" - region: "" - worker: - clientid: "" - clientsecret: "" - environmentid: ""` + color: invalid` testutils_viper.InitVipersCustomFile(t, fileContents) @@ -119,20 +68,11 @@ default: // Test Validate function with invalid output format func TestValidateInvalidOutputFormat(t *testing.T) { - fileContents := `activeProfile: invalid + fileContents := `activeProfile: default default: description: "default description" pingctl: - color: true - outputFormat: invalid - pingone: - export: - environmentid: "" - region: "" - worker: - clientid: "" - clientsecret: "" - environmentid: ""` + outputFormat: invalid` testutils_viper.InitVipersCustomFile(t, fileContents) @@ -147,30 +87,8 @@ func TestValidateInvalidProfileName(t *testing.T) { fileContents := `activeProfile: default default: description: "default description" - pingctl: - color: true - outputFormat: text - pingone: - export: - environmentid: "" - region: "" - worker: - clientid: "" - clientsecret: "" - environmentid: "" invalid(&*^&*^&*^**$): - description: "default description" - pingctl: - color: true - outputFormat: text - pingone: - export: - environmentid: "" - region: "" - worker: - clientid: "" - clientsecret: "" - environmentid: ""` + description: "default description"` testutils_viper.InitVipersCustomFile(t, fileContents) diff --git a/internal/profiles/viper.go b/internal/profiles/viper.go new file mode 100644 index 0000000..2dbb81f --- /dev/null +++ b/internal/profiles/viper.go @@ -0,0 +1,359 @@ +package profiles + +/* The main viper instance should ONLY interact with the configuration file +on disk. No viper overrides, environment variable bindings, or pflag +bindings should be used with this viper instance. This keeps the config +file as the ONLY source of truth for the main viper instance, and prevents +profile drift, as well as active profile drift and other niche bugs. As a +result, much of the logic in this file avoids the use of mainViper.Set(), and +goes out of the way to modify the config file.*/ + +import ( + "bytes" + "encoding/json" + "fmt" + "os" + "regexp" + "slices" + "strings" + + "github.com/pingidentity/pingctl/internal/configuration/options" + "github.com/spf13/viper" + "gopkg.in/yaml.v3" +) + +type ActiveProfile struct { + name string + viperInstance *viper.Viper +} + +type MainConfig struct { + viperInstance *viper.Viper + activeProfile *ActiveProfile +} + +var ( + mainViper *MainConfig = NewMainConfig() +) + +// Returns a new MainViper instance +func NewMainConfig() (newMainViper *MainConfig) { + newMainViper = &MainConfig{ + viperInstance: viper.New(), + activeProfile: nil, + } + + return newMainViper +} + +// Returns the MainViper struct +func GetMainConfig() *MainConfig { + return mainViper +} + +func (m MainConfig) ViperInstance() *viper.Viper { + return m.viperInstance +} + +func (m MainConfig) ActiveProfile() *ActiveProfile { + return m.activeProfile +} + +func (m *MainConfig) ChangeActiveProfile(pName string) (err error) { + if err = m.ValidateExistingProfileName(pName); err != nil { + return err + } + + tempViper := viper.New() + tempViper.SetConfigFile(m.ViperInstance().ConfigFileUsed()) + if err := tempViper.ReadInConfig(); err != nil { + return err + } + + tempViper.Set(options.RootActiveProfileOption.ViperKey, pName) + + if err = tempViper.WriteConfig(); err != nil { + return err + } + + if err = m.ViperInstance().ReadInConfig(); err != nil { + return err + } + + m.activeProfile = &ActiveProfile{ + name: pName, + viperInstance: m.ViperInstance().Sub(pName), + } + + return nil +} + +func (m MainConfig) ChangeProfileName(oldPName, newPName string) (err error) { + if oldPName == newPName { + return nil + } + + err = m.ValidateExistingProfileName(oldPName) + if err != nil { + return err + } + + err = m.ValidateNewProfileName(newPName) + if err != nil { + return err + } + + subViper := m.ViperInstance().Sub(oldPName) + + if err = m.DeleteProfile(oldPName); err != nil { + return err + } + + if err = m.SaveProfile(newPName, subViper); err != nil { + return err + } + + return nil +} + +func (m MainConfig) ChangeProfileDescription(pName, description string) (err error) { + if err = m.ValidateExistingProfileName(pName); err != nil { + return err + } + + subViper := m.ViperInstance().Sub(pName) + subViper.Set(options.ProfileDescriptionOption.ViperKey, description) + + if err = m.SaveProfile(pName, subViper); err != nil { + return err + } + + return nil +} + +// Viper gives no built-in delete or unset method for keys +// Using this "workaround" described here: https://github.com/spf13/viper/issues/632 +func (m MainConfig) DeleteProfile(pName string) (err error) { + if err = m.ValidateExistingProfileName(pName); err != nil { + return err + } + + if pName == m.ActiveProfile().Name() { + return fmt.Errorf("'%s' is the active profile and cannot be deleted", pName) + } + + mainViperConfigMap := m.ViperInstance().AllSettings() + delete(mainViperConfigMap, pName) + + encodedConfig, err := json.MarshalIndent(mainViperConfigMap, "", " ") + if err != nil { + return err + } + + err = m.ViperInstance().ReadConfig(bytes.NewReader(encodedConfig)) + if err != nil { + return err + } + + err = m.ViperInstance().WriteConfig() + if err != nil { + return err + } + + return nil +} + +// Get all profile names from config.yaml configuration file +func (m MainConfig) ProfileNames() (profileNames []string) { + keySet := make(map[string]struct{}) + mainViperKeys := m.ViperInstance().AllKeys() + for _, key := range mainViperKeys { + //Do not add Active profile viper key to profileNames + if strings.EqualFold(key, options.RootActiveProfileOption.ViperKey) { + continue + } + + pName := strings.Split(key, ".")[0] + if _, ok := keySet[pName]; !ok { + keySet[pName] = struct{}{} + profileNames = append(profileNames, pName) + } + } + + return profileNames +} + +func (m MainConfig) SaveProfile(pName string, subViper *viper.Viper) (err error) { + mainViperConfigMap := m.ViperInstance().AllSettings() + subViperConfigMap := subViper.AllSettings() + + mainViperConfigMap[pName] = subViperConfigMap + + encodedConfig, err := json.MarshalIndent(mainViperConfigMap, "", " ") + if err != nil { + return err + } + + err = m.ViperInstance().ReadConfig(bytes.NewReader(encodedConfig)) + if err != nil { + return err + } + + err = m.ViperInstance().WriteConfig() + if err != nil { + return err + } + + return nil +} + +// The profile name format must be valid +// The profile name must exist +func (m MainConfig) ValidateExistingProfileName(pName string) (err error) { + if err := m.ValidateProfileNameFormat(pName); err != nil { + return err + } + + pNames := m.ProfileNames() + if !slices.ContainsFunc(pNames, func(n string) bool { + return strings.EqualFold(n, pName) + }) { + return fmt.Errorf("invalid profile name: '%s' profile does not exist", pName) + } + + return nil +} + +// The profile name format must be valid +// The new profile name must be unique +func (m MainConfig) ValidateNewProfileName(pName string) (err error) { + if err = m.ValidateProfileNameFormat(pName); err != nil { + return err + } + + pNames := m.ProfileNames() + if slices.ContainsFunc(pNames, func(n string) bool { + return strings.EqualFold(n, pName) + }) { + return fmt.Errorf("invalid profile name: '%s'. profile already exists", pName) + } + + return nil +} + +// The profile name must contain only alphanumeric characters, underscores, and dashes +// The profile name cannot be empty +func (m MainConfig) ValidateProfileNameFormat(pName string) (err error) { + if pName == "" { + return fmt.Errorf("invalid profile name: profile name cannot be empty") + } + + re := regexp.MustCompile(`^[a-zA-Z0-9\_\-]+$`) + if !re.MatchString(pName) { + return fmt.Errorf("invalid profile name: '%s'. name must contain only alphanumeric characters, underscores, and dashes", pName) + } + + return nil +} + +// If the new profile name is the same as the existing profile name, that is valid +// Otherwise treat newPName as a new profile name and validate it +func (m MainConfig) ValidateUpdateExistingProfileName(ePName, newPName string) (err error) { + if ePName == newPName { + return nil + } + + if err = m.ValidateNewProfileName(newPName); err != nil { + return err + } + + return nil +} + +func (m MainConfig) ProfileToString(pName string) (yamlStr string, err error) { + if err = m.ValidateExistingProfileName(pName); err != nil { + return "", err + } + + subViper := m.ViperInstance().Sub(pName) + + yaml, err := yaml.Marshal(subViper.AllSettings()) + if err != nil { + return "", fmt.Errorf("failed to yaml marshal active profile: %v", err) + } + + return string(yaml), nil +} + +func (m MainConfig) ProfileViperValue(pName, viperKey string) (yamlStr string, err error) { + if err = m.ValidateExistingProfileName(pName); err != nil { + return "", err + } + + subViper := m.ViperInstance().Sub(pName) + + if !subViper.IsSet(viperKey) { + return "", fmt.Errorf("configuration key '%s' is not set in profile '%s'", viperKey, pName) + } + + yaml, err := yaml.Marshal(subViper.Get(viperKey)) + if err != nil { + return "", fmt.Errorf("failed to yaml marshal configuration value from key '%s': %v", viperKey, err) + } + + return string(yaml), nil +} + +func (a ActiveProfile) ViperInstance() *viper.Viper { + return a.viperInstance +} + +func (a ActiveProfile) Name() string { + return a.name +} + +func GetOptionValue(opt options.Option) (pFlagValue string, err error) { + if opt.CobraParamValue != nil && opt.Flag.Changed { + pFlagValue = opt.CobraParamValue.String() + return pFlagValue, nil + } + + pFlagValue = os.Getenv(opt.EnvVar) + if pFlagValue != "" { + return pFlagValue, nil + } + + mainConfig := GetMainConfig() + if opt.ViperKey != "" && mainConfig != nil { + var vValue any + + if opt.ViperKey == options.RootActiveProfileOption.ViperKey { + mainViperInstance := mainConfig.ViperInstance() + if mainViperInstance != nil { + vValue = mainViperInstance.Get(opt.ViperKey) + } + } else { + activeProfile := mainConfig.ActiveProfile() + if activeProfile != nil { + profileViperInstance := activeProfile.ViperInstance() + if profileViperInstance != nil { + vValue = profileViperInstance.Get(opt.ViperKey) + } + } + } + + if vValue != nil { + pFlagValue = fmt.Sprintf("%v", vValue) + if pFlagValue != "" { + return pFlagValue, nil + } + } + } + + if opt.DefaultValue != nil { + pFlagValue = opt.DefaultValue.String() + return pFlagValue, nil + } + + return pFlagValue, fmt.Errorf("failed to initialize %s option: %s. no value found", opt.Type, opt.CobraParamName) +} diff --git a/internal/profiles/viper_test.go b/internal/profiles/viper_test.go new file mode 100644 index 0000000..dbc071b --- /dev/null +++ b/internal/profiles/viper_test.go @@ -0,0 +1,199 @@ +package profiles_test + +import ( + "slices" + "testing" + + "github.com/pingidentity/pingctl/internal/configuration/options" + "github.com/pingidentity/pingctl/internal/profiles" + "github.com/pingidentity/pingctl/internal/testing/testutils" + "github.com/pingidentity/pingctl/internal/testing/testutils_viper" + "github.com/spf13/viper" +) + +// Test ChangeActiveProfile function +func Test_ChangeActiveProfile(t *testing.T) { + testutils_viper.InitVipers(t) + + mainConfig := profiles.GetMainConfig() + + err := mainConfig.ChangeActiveProfile("production") + if err != nil { + t.Errorf("ChangeActiveProfile returned error: %v", err) + } + + activeProfile, err := profiles.GetOptionValue(options.RootActiveProfileOption) + if err != nil { + t.Errorf("GetOptionValue returned error: %v", err) + } + + if activeProfile != "production" { + t.Errorf("activeProfile is %s, expected %s", activeProfile, "production") + } +} + +// Test ChangeActiveProfile function with invalid profile +func Test_ChangeActiveProfile_InvalidProfile(t *testing.T) { + testutils_viper.InitVipers(t) + + mainConfig := profiles.GetMainConfig() + + expectedErrorPattern := `^invalid profile name: '.*' profile does not exist$` + err := mainConfig.ChangeActiveProfile("invalid") + testutils.CheckExpectedError(t, err, &expectedErrorPattern) +} + +// Test ChangeProfileName function +func Test_ChangeProfileName(t *testing.T) { + testutils_viper.InitVipers(t) + + mainConfig := profiles.GetMainConfig() + + err := mainConfig.ChangeProfileName("production", "new") + if err != nil { + t.Errorf("ChangeProfileName returned error: %v", err) + } + + profiles := mainConfig.ProfileNames() + if len(profiles) != 2 { + t.Errorf("profiles length is %d, expected %d", len(profiles), 2) + } + + if slices.Contains(profiles, "production") { + t.Errorf("profiles contains production, expected it to be removed") + } + + if !slices.Contains(profiles, "new") { + t.Errorf("profiles does not contain new, expected it to be added") + } +} + +// Test ChangeProfileName function with same profile name +func Test_ChangeProfileName_SameProfileName(t *testing.T) { + testutils_viper.InitVipers(t) + + mainConfig := profiles.GetMainConfig() + + err := mainConfig.ChangeProfileName("production", "production") + if err != nil { + t.Errorf("ChangeProfileName returned error: %v", err) + } +} + +// Test ChangeProfileName function with invalid old profile name +func Test_ChangeProfileName_InvalidOldProfileName(t *testing.T) { + testutils_viper.InitVipers(t) + + mainConfig := profiles.GetMainConfig() + + expectedErrorPattern := `^invalid profile name: '.*' profile does not exist$` + err := mainConfig.ChangeProfileName("invalid", "new") + testutils.CheckExpectedError(t, err, &expectedErrorPattern) +} + +// Test ChangeProfileName function with invalid new profile name +func Test_ChangeProfileName_InvalidNewProfileName(t *testing.T) { + testutils_viper.InitVipers(t) + + mainConfig := profiles.GetMainConfig() + + expectedErrorPattern := `^invalid profile name: '.*'. profile already exists$` + err := mainConfig.ChangeProfileName("production", "default") + testutils.CheckExpectedError(t, err, &expectedErrorPattern) +} + +// Test DeleteProfile function +func Test_DeleteProfile(t *testing.T) { + testutils_viper.InitVipers(t) + + mainConfig := profiles.GetMainConfig() + + err := mainConfig.DeleteProfile("production") + if err != nil { + t.Errorf("DeleteProfile returned error: %v", err) + } + + profiles := mainConfig.ProfileNames() + if len(profiles) != 1 { + t.Errorf("profiles length is %d, expected %d", len(profiles), 0) + } + + if slices.Contains(profiles, "production") { + t.Errorf("profiles contains production, expected it to be removed") + } +} + +// Test DeleteProfile function with invalid profile name +func Test_DeleteProfile_InvalidProfileName(t *testing.T) { + testutils_viper.InitVipers(t) + + mainConfig := profiles.GetMainConfig() + + expectedErrorPattern := `^invalid profile name: '.*' profile does not exist$` + err := mainConfig.DeleteProfile("invalid") + testutils.CheckExpectedError(t, err, &expectedErrorPattern) +} + +// Test DeleteProfile function with active profile +func Test_DeleteProfile_ActiveProfile(t *testing.T) { + testutils_viper.InitVipers(t) + + mainConfig := profiles.GetMainConfig() + + expectedErrorPattern := `^'.*' is the active profile and cannot be deleted$` + err := mainConfig.DeleteProfile("default") + testutils.CheckExpectedError(t, err, &expectedErrorPattern) +} + +// Test SaveProfile function +func Test_SaveProfile(t *testing.T) { + testutils_viper.InitVipers(t) + + mainConfig := profiles.GetMainConfig() + + subViper := viper.New() + subViper.Set("description", "new description") + + err := mainConfig.SaveProfile("new", subViper) + if err != nil { + t.Errorf("SaveProfile returned error: %v", err) + } + + profiles := mainConfig.ProfileNames() + if len(profiles) != 3 { + t.Errorf("profiles length is %d, expected %d", len(profiles), 3) + } + + if !slices.Contains(profiles, "new") { + t.Errorf("profiles does not contain new, expected it to be added") + } +} + +// Test SaveProfile function with existing profile name +func Test_SaveProfile_ExistingProfileName(t *testing.T) { + testutils_viper.InitVipers(t) + + mainConfig := profiles.GetMainConfig() + + subViper := viper.New() + subViper.Set("description", "new description") + + err := mainConfig.SaveProfile("production", subViper) + testutils.CheckExpectedError(t, err, nil) + + profiles := mainConfig.ProfileNames() + if len(profiles) != 2 { + t.Errorf("profiles length is %d, expected %d", len(profiles), 2) + } + + if !slices.Contains(profiles, "production") { + t.Errorf("profiles does not contain production, expected it to be added") + } + + actual := mainConfig.ViperInstance().Get("production.description") + expected := "new description" + + if actual != expected { + t.Errorf("description is %s, expected %s", actual, expected) + } +} diff --git a/internal/testing/testutils/utils.go b/internal/testing/testutils/utils.go index 0141666..6bd77b1 100644 --- a/internal/testing/testutils/utils.go +++ b/internal/testing/testutils/utils.go @@ -10,8 +10,9 @@ import ( "testing" "github.com/patrickcping/pingone-go-sdk-v2/pingone" + "github.com/pingidentity/pingctl/internal/configuration" + "github.com/pingidentity/pingctl/internal/configuration/options" "github.com/pingidentity/pingctl/internal/connector" - "github.com/pingidentity/pingctl/internal/profiles" pingfederateGoClient "github.com/pingidentity/pingfederate-go-client/v1210/configurationapi" ) @@ -24,7 +25,7 @@ var ( func GetEnvironmentID() string { envIdOnce.Do(func() { - environmentId = os.Getenv(profiles.PingOneWorkerEnvironmentIDOption.EnvVar) + environmentId = os.Getenv(options.PlatformExportPingoneWorkerEnvironmentIDOption.EnvVar) }) return environmentId @@ -35,12 +36,13 @@ func GetPingOneClientInfo(t *testing.T) *connector.PingOneClientInfo { t.Helper() apiClientOnce.Do(func() { + configuration.InitAllOptions() // Grab environment vars for initializing the API client. // These are set in GitHub Actions. - clientID := os.Getenv(profiles.PingOneWorkerClientIDOption.EnvVar) - clientSecret := os.Getenv(profiles.PingOneWorkerClientSecretOption.EnvVar) + clientID := os.Getenv(options.PlatformExportPingoneWorkerClientIDOption.EnvVar) + clientSecret := os.Getenv(options.PlatformExportPingoneWorkerClientSecretOption.EnvVar) environmentId := GetEnvironmentID() - region := os.Getenv(profiles.PingOneRegionOption.EnvVar) + region := os.Getenv(options.PlatformExportPingoneRegionOption.EnvVar) if clientID == "" || clientSecret == "" || environmentId == "" || region == "" { t.Fatalf("Unable to retrieve env var value for one or more of clientID, clientSecret, environmentID, region.") @@ -76,10 +78,12 @@ func GetPingOneClientInfo(t *testing.T) *connector.PingOneClientInfo { func GetPingFederateClientInfo(t *testing.T) *connector.PingFederateClientInfo { t.Helper() - httpsHost := os.Getenv(profiles.PingFederateHttpsHostOption.EnvVar) - adminApiPath := os.Getenv(profiles.PingFederateAdminApiPathOption.EnvVar) - pfUsername := os.Getenv(profiles.PingFederateUsernameOption.EnvVar) - pfPassword := os.Getenv(profiles.PingFederatePasswordOption.EnvVar) + configuration.InitAllOptions() + + httpsHost := os.Getenv(options.PlatformExportPingfederateHTTPSHostOption.EnvVar) + adminApiPath := os.Getenv(options.PlatformExportPingfederateAdminAPIPathOption.EnvVar) + pfUsername := os.Getenv(options.PlatformExportPingfederateUsernameOption.EnvVar) + pfPassword := os.Getenv(options.PlatformExportPingfederatePasswordOption.EnvVar) if httpsHost == "" || adminApiPath == "" || pfUsername == "" || pfPassword == "" { t.Fatalf("Unable to retrieve env var value for one or more of httpsHost, adminApiPath, pfUsername, pfPassword.") diff --git a/internal/testing/testutils_cobra/cobra_utils.go b/internal/testing/testutils_cobra/cobra_utils.go index 169a0d3..025bfd4 100644 --- a/internal/testing/testutils_cobra/cobra_utils.go +++ b/internal/testing/testutils_cobra/cobra_utils.go @@ -5,6 +5,7 @@ import ( "testing" "github.com/pingidentity/pingctl/cmd" + "github.com/pingidentity/pingctl/internal/configuration" testutils_viper "github.com/pingidentity/pingctl/internal/testing/testutils_viper" ) @@ -13,6 +14,9 @@ import ( func ExecutePingctl(t *testing.T, args ...string) (err error) { t.Helper() + // Reset options for testing individual executions of pingctl + configuration.InitAllOptions() + root := cmd.NewRootCommand() // Add config location to the root command diff --git a/internal/testing/testutils_viper/viper_utils.go b/internal/testing/testutils_viper/viper_utils.go index 65fe6bf..90c7618 100644 --- a/internal/testing/testutils_viper/viper_utils.go +++ b/internal/testing/testutils_viper/viper_utils.go @@ -3,11 +3,18 @@ package testutils_viper import ( "fmt" "os" + "strings" "testing" + "github.com/pingidentity/pingctl/internal/configuration" + "github.com/pingidentity/pingctl/internal/configuration/options" "github.com/pingidentity/pingctl/internal/profiles" ) +const ( + outputDirectoryReplacement = "[REPLACE_WITH_OUTPUT_DIRECTORY]" +) + var ( configFileContents string defaultConfigFileContents string = fmt.Sprintf(`activeProfile: default @@ -16,79 +23,51 @@ default: pingctl: color: true outputFormat: text - pingone: - export: - environmentid: "" - region: %s - worker: - clientid: %s - clientsecret: %s - environmentid: %s - pingfederate: - accesstokenauth: - accesstoken: "" - adminapipath: "%s" - basicauth: - password: "%s" - username: "%s" - caCertificatePemFiles: "%s" - clientcredentialsauth: - clientid: "%s" - clientsecret: "%s" - scopes: "%s" - tokenurl: "%s" - httpshost: "%s" - insecureTrustAllTLS: true - xBypassExternalValidationHeader: true + export: + outputDirectory: %s + pingone: + region: %s + worker: + clientid: %s + clientsecret: %s + environmentid: %s + pingfederate: + adminapipath: "%s" + clientcredentialsauth: + clientid: "%s" + clientsecret: "%s" + scopes: "%s" + tokenurl: "%s" + httpshost: "%s" + insecureTrustAllTLS: true + xBypassExternalValidationHeader: true production: description: "test profile description" pingctl: color: true outputFormat: text - pingone: - export: - environmentid: "" - region: "" - worker: - clientid: "" - clientsecret: "" - environmentid: "" - pingfederate: - accesstokenauth: - accesstoken: "" - adminapipath: "" - basicauth: - password: "" - username: "" - caCertificatePemFiles: [] - clientcredentialsauth: - clientid: "" - clientsecret: "" - scopes: [] - tokenurl: "" - httpshost: "" - insecureTrustAllTLS: false - xBypassExternalValidationHeader: false`, - os.Getenv(profiles.PingOneRegionOption.EnvVar), - os.Getenv(profiles.PingOneWorkerClientIDOption.EnvVar), - os.Getenv(profiles.PingOneWorkerClientSecretOption.EnvVar), - os.Getenv(profiles.PingOneWorkerEnvironmentIDOption.EnvVar), - os.Getenv(profiles.PingFederateAdminApiPathOption.EnvVar), - os.Getenv(profiles.PingFederatePasswordOption.EnvVar), - os.Getenv(profiles.PingFederateUsernameOption.EnvVar), - os.Getenv(profiles.PingFederateCACertificatePemFilesOption.EnvVar), - os.Getenv(profiles.PingFederateClientIDOption.EnvVar), - os.Getenv(profiles.PingFederateClientSecretOption.EnvVar), - os.Getenv(profiles.PingFederateScopesOption.EnvVar), - os.Getenv(profiles.PingFederateTokenURLOption.EnvVar), - os.Getenv(profiles.PingFederateHttpsHostOption.EnvVar)) + export: + pingfederate: + insecureTrustAllTLS: false + xBypassExternalValidationHeader: false`, + outputDirectoryReplacement, + os.Getenv(options.PlatformExportPingoneRegionOption.EnvVar), + os.Getenv(options.PlatformExportPingoneWorkerClientIDOption.EnvVar), + os.Getenv(options.PlatformExportPingoneWorkerClientSecretOption.EnvVar), + os.Getenv(options.PlatformExportPingoneWorkerEnvironmentIDOption.EnvVar), + os.Getenv(options.PlatformExportPingfederateAdminAPIPathOption.EnvVar), + os.Getenv(options.PlatformExportPingfederateClientIDOption.EnvVar), + os.Getenv(options.PlatformExportPingfederateClientSecretOption.EnvVar), + os.Getenv(options.PlatformExportPingfederateScopesOption.EnvVar), + os.Getenv(options.PlatformExportPingfederateTokenURLOption.EnvVar), + os.Getenv(options.PlatformExportPingfederateHTTPSHostOption.EnvVar)) ) func CreateConfigFile(t *testing.T) string { t.Helper() if configFileContents == "" { - configFileContents = defaultConfigFileContents + configFileContents = strings.Replace(defaultConfigFileContents, outputDirectoryReplacement, t.TempDir(), 1) } configFilepath := t.TempDir() + "/config.yaml" @@ -105,13 +84,15 @@ func configureMainViper(t *testing.T) { // Create and write to a temporary config file configFilepath := CreateConfigFile(t) // Give main viper instance a file location to write to - mainViper := profiles.GetMainViper() + mainViper := profiles.GetMainConfig().ViperInstance() mainViper.SetConfigFile(configFilepath) if err := mainViper.ReadInConfig(); err != nil { t.Fatal(err) } - if err := profiles.SetProfileViperWithProfile("default"); err != nil { + activePName := profiles.GetMainConfig().ViperInstance().GetString(options.RootActiveProfileOption.ViperKey) + + if err := profiles.GetMainConfig().ChangeActiveProfile(activePName); err != nil { t.Fatal(err) } } @@ -119,7 +100,9 @@ func configureMainViper(t *testing.T) { func InitVipers(t *testing.T) { t.Helper() - configFileContents = defaultConfigFileContents + configuration.InitAllOptions() + + configFileContents = strings.Replace(defaultConfigFileContents, outputDirectoryReplacement, t.TempDir(), 1) configureMainViper(t) }