diff --git a/cmd/config/config.go b/cmd/config/config.go index 89815d3e..d63b1c3a 100644 --- a/cmd/config/config.go +++ b/cmd/config/config.go @@ -23,12 +23,13 @@ func NewConfigCommand() *cobra.Command { cmd.AddCommand( NewConfigAddProfileCommand(), NewConfigDeleteProfileCommand(), - NewConfigViewProfileCommand(), + NewConfigGetCommand(), + NewConfigListKeysCommand(), NewConfigListProfilesCommand(), NewConfigSetActiveProfileCommand(), - NewConfigGetCommand(), NewConfigSetCommand(), NewConfigUnsetCommand(), + NewConfigViewProfileCommand(), ) return cmd diff --git a/cmd/config/get_test.go b/cmd/config/get_test.go index 0d4da6dd..62f43ca1 100644 --- a/cmd/config/get_test.go +++ b/cmd/config/get_test.go @@ -35,7 +35,7 @@ func TestConfigGetCmd_PartialKey(t *testing.T) { // Test Config Get Command fails when provided an invalid key func TestConfigGetCmd_InvalidKey(t *testing.T) { - expectedErrorPattern := `(?s)^failed to get configuration: key '.*' is not recognized as a valid configuration key\. Valid keys: .*$` + expectedErrorPattern := `^failed to get configuration: key '.*' is not recognized as a valid configuration key.\s*Use 'pingcli config list-keys' to view all available keys` err := testutils_cobra.ExecutePingcli(t, "config", "get", "pingcli.invalid") testutils.CheckExpectedError(t, err, &expectedErrorPattern) } diff --git a/cmd/config/list_keys.go b/cmd/config/list_keys.go new file mode 100644 index 00000000..5777c44c --- /dev/null +++ b/cmd/config/list_keys.go @@ -0,0 +1,44 @@ +package config + +import ( + "github.com/pingidentity/pingcli/cmd/common" + config_internal "github.com/pingidentity/pingcli/internal/commands/config" + "github.com/pingidentity/pingcli/internal/configuration/options" + "github.com/pingidentity/pingcli/internal/logger" + "github.com/spf13/cobra" +) + +const ( + listKeysCommandExamples = ` List all configuration keys stored in the CLI configuration file. + pingcli config list-keys + pingcli config list-keys --yaml` +) + +func NewConfigListKeysCommand() *cobra.Command { + cmd := &cobra.Command{ + Args: common.ExactArgs(0), + DisableFlagsInUseLine: true, // We write our own flags in @Use attribute + Example: listKeysCommandExamples, + Long: `View the complete list of available configuration options. These attributes can be saved via the set and unset config subcommands or stored in a profile within the config file. +For details on the configuration options visit: https://github.com/pingidentity/pingcli/blob/main/docs/tool-configuration/configuration-key.md`, + RunE: configListKeysRunE, + Short: "List all configuration keys.", + Use: "list-keys [flags]", + } + + cmd.Flags().AddFlag(options.ConfigListKeysYamlOption.Flag) + + return cmd +} + +func configListKeysRunE(cmd *cobra.Command, args []string) error { + l := logger.Get() + l.Debug().Msgf("Config list-keys Subcommand Called.") + + err := config_internal.RunInternalConfigListKeys() + if err != nil { + return err + } + + return nil +} diff --git a/cmd/config/list_keys_test.go b/cmd/config/list_keys_test.go new file mode 100644 index 00000000..be4bf0a8 --- /dev/null +++ b/cmd/config/list_keys_test.go @@ -0,0 +1,80 @@ +package config_test + +import ( + "testing" + + "github.com/pingidentity/pingcli/internal/configuration/options" + "github.com/pingidentity/pingcli/internal/testing/testutils" + "github.com/pingidentity/pingcli/internal/testing/testutils_cobra" +) + +// Test Config List Keys Command Executes without issue +func TestConfigListKeysCmd_Execute(t *testing.T) { + err := testutils_cobra.ExecutePingcli(t, "config", "list-keys") + testutils.CheckExpectedError(t, err, nil) +} + +// Test Config List Keys YAML Command --yaml, -y flag +func TestConfigListKeysCmd_YAMLFlag(t *testing.T) { + err := testutils_cobra.ExecutePingcli(t, "config", "list-keys", "--yaml") + testutils.CheckExpectedError(t, err, nil) + + err = testutils_cobra.ExecutePingcli(t, "config", "list-keys", "-y") + testutils.CheckExpectedError(t, err, nil) +} + +// Test Config List Keys Command --help, -h flag +func TestConfigListKeysCmd_HelpFlag(t *testing.T) { + err := testutils_cobra.ExecutePingcli(t, "config", "list-keys", "--help") + testutils.CheckExpectedError(t, err, nil) + + err = testutils_cobra.ExecutePingcli(t, "config", "list-keys", "-h") + testutils.CheckExpectedError(t, err, nil) +} + +// Test Config List Keys Command fails when provided too many arguments +func TestConfigListKeysCmd_TooManyArgs(t *testing.T) { + expectedErrorPattern := `^failed to execute 'pingcli config list-keys': command accepts 0 arg\(s\), received 1$` + err := testutils_cobra.ExecutePingcli(t, "config", "list-keys", options.RootColorOption.ViperKey) + testutils.CheckExpectedError(t, err, &expectedErrorPattern) +} + +// https://pkg.go.dev/testing#hdr-Examples +func Example_listKeysValue() { + t := testing.T{} + _ = testutils_cobra.ExecutePingcli(&t, "config", "list-keys") + + // Output: + // Valid Keys: + // - activeProfile + // - description + // - export.format + // - export.outputDirectory + // - export.overwrite + // - export.pingone.environmentID + // - export.services + // - noColor + // - outputFormat + // - request.accessToken + // - request.accessTokenExpiry + // - request.fail + // - request.service + // - service.pingfederate.adminAPIPath + // - service.pingfederate.authentication.accessTokenAuth.accessToken + // - service.pingfederate.authentication.basicAuth.password + // - service.pingfederate.authentication.basicAuth.username + // - service.pingfederate.authentication.clientCredentialsAuth.clientID + // - service.pingfederate.authentication.clientCredentialsAuth.clientSecret + // - service.pingfederate.authentication.clientCredentialsAuth.scopes + // - service.pingfederate.authentication.clientCredentialsAuth.tokenURL + // - service.pingfederate.authentication.type + // - service.pingfederate.caCertificatePemFiles + // - service.pingfederate.httpsHost + // - service.pingfederate.insecureTrustAllTLS + // - service.pingfederate.xBypassExternalValidationHeader + // - service.pingone.authentication.type + // - service.pingone.authentication.worker.clientID + // - service.pingone.authentication.worker.clientSecret + // - service.pingone.authentication.worker.environmentID + // - service.pingone.regionCode +} diff --git a/cmd/config/set_test.go b/cmd/config/set_test.go index 3c787b38..b60d98b0 100644 --- a/cmd/config/set_test.go +++ b/cmd/config/set_test.go @@ -32,7 +32,7 @@ func TestConfigSetCmd_TooManyArgs(t *testing.T) { // Test Config Set Command Fails when an invalid key is provided func TestConfigSetCmd_InvalidKey(t *testing.T) { - expectedErrorPattern := `^failed to set configuration: key 'pingcli\.invalid' is not recognized as a valid configuration key\. Valid keys: [A-Za-z\.\s,]+$` + expectedErrorPattern := `^failed to set configuration: key 'pingcli\.invalid' is not recognized as a valid configuration key\.\s*Use 'pingcli config list-keys' to view all available keys` err := testutils_cobra.ExecutePingcli(t, "config", "set", "pingcli.invalid=true") testutils.CheckExpectedError(t, err, &expectedErrorPattern) } diff --git a/cmd/config/unset_test.go b/cmd/config/unset_test.go index eefd3b5d..a6a9f088 100644 --- a/cmd/config/unset_test.go +++ b/cmd/config/unset_test.go @@ -32,7 +32,7 @@ func TestConfigUnsetCmd_TooManyArgs(t *testing.T) { // Test Config Unset Command Fails when an invalid key is provided func TestConfigUnsetCmd_InvalidKey(t *testing.T) { - expectedErrorPattern := `^failed to unset configuration: key '.*' 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\.\s*Use 'pingcli config list-keys' to view all available keys` err := testutils_cobra.ExecutePingcli(t, "config", "unset", "pingcli.invalid") testutils.CheckExpectedError(t, err, &expectedErrorPattern) } diff --git a/internal/commands/config/get_internal_test.go b/internal/commands/config/get_internal_test.go index ddc5ef02..69cfc0eb 100644 --- a/internal/commands/config/get_internal_test.go +++ b/internal/commands/config/get_internal_test.go @@ -23,7 +23,7 @@ func Test_RunInternalConfigGet(t *testing.T) { func Test_RunInternalConfigGet_InvalidKey(t *testing.T) { testutils_viper.InitVipers(t) - expectedErrorPattern := `(?s)^failed to get configuration: key '.*' is not recognized as a valid configuration key. Valid keys: .*$` + expectedErrorPattern := `(?s)^failed to get configuration: key '.*' is not recognized as a valid configuration key\.\s*Use 'pingcli config list-keys' to view all available keys` err := RunInternalConfigGet("invalid-key") testutils.CheckExpectedError(t, err, &expectedErrorPattern) } diff --git a/internal/commands/config/list_keys_internal.go b/internal/commands/config/list_keys_internal.go new file mode 100644 index 00000000..5c04167f --- /dev/null +++ b/internal/commands/config/list_keys_internal.go @@ -0,0 +1,93 @@ +package config_internal + +import ( + "fmt" + "strings" + + "github.com/pingidentity/pingcli/internal/configuration" + "github.com/pingidentity/pingcli/internal/configuration/options" + "github.com/pingidentity/pingcli/internal/output" + "github.com/pingidentity/pingcli/internal/profiles" + "gopkg.in/yaml.v3" +) + +func returnKeysYamlString() (string, error) { + var err error + viperKeys := configuration.ViperKeys() + + if len(viperKeys) == 0 { + return "", fmt.Errorf("unable to retrieve valid keys") + } + + // Split the input string into individual keys + keyMap := make(map[string]interface{}) + + // Iterate over each viper key + for _, viperKey := range viperKeys { + // Skip the "activeProfile" key + if viperKey == "activeProfile" { + continue + } + + // Create a nested map for each yaml key + currentMap := keyMap + yamlKeys := strings.Split(viperKey, ".") + for i, k := range yamlKeys { + // If it's the last yaml key, set an empty map + if i == len(yamlKeys)-1 { + currentMap[k] = "" + } else { + // Otherwise, create or navigate to the next level + if _, exists := currentMap[k]; !exists { + currentMap[k] = make(map[string]interface{}) + } + currentMap = currentMap[k].(map[string]interface{}) + } + } + } + + // Marshal the result into YAML + yamlData, err := yaml.Marshal(keyMap) + if err != nil { + return "", fmt.Errorf("error marshaling keys to YAML format") + } + + return string(yamlData), nil +} + +func returnKeysString() (string, error) { + // var err error + validKeys := configuration.ViperKeys() + + if len(validKeys) == 0 { + return "", fmt.Errorf("unable to retrieve valid keys") + } else { + validKeysJoined := strings.Join(validKeys, "\n- ") + return "Valid Keys:\n- " + validKeysJoined, nil + } +} + +func RunInternalConfigListKeys() (err error) { + var outputMessageString string + yamlFlagStr, err := profiles.GetOptionValue(options.ConfigListKeysYamlOption) + if err != nil { + return err + } + if yamlFlagStr == "true" { + // Output the YAML data as a string + outputMessageString, err = returnKeysYamlString() + if err != nil { + return err + } + } else { + // Output data list string + outputMessageString, err = returnKeysString() + if err != nil { + return err + } + } + + output.Message(outputMessageString, nil) + + return nil +} diff --git a/internal/commands/config/list_keys_internal_test.go b/internal/commands/config/list_keys_internal_test.go new file mode 100644 index 00000000..1b4119a5 --- /dev/null +++ b/internal/commands/config/list_keys_internal_test.go @@ -0,0 +1,16 @@ +package config_internal + +import ( + "testing" + + "github.com/pingidentity/pingcli/internal/testing/testutils" + "github.com/pingidentity/pingcli/internal/testing/testutils_viper" +) + +// Test RunInternalConfigListKeys function +func Test_RunInternalConfigListKeys(t *testing.T) { + testutils_viper.InitVipers(t) + + err := RunInternalConfigListKeys() + testutils.CheckExpectedError(t, err, nil) +} diff --git a/internal/commands/config/set_internal_test.go b/internal/commands/config/set_internal_test.go index 3d6da9d6..e8743d73 100644 --- a/internal/commands/config/set_internal_test.go +++ b/internal/commands/config/set_internal_test.go @@ -23,7 +23,7 @@ func Test_RunInternalConfigSet(t *testing.T) { func Test_RunInternalConfigSet_InvalidKey(t *testing.T) { testutils_viper.InitVipers(t) - expectedErrorPattern := `^failed to set configuration: key '.*' is not recognized as a valid configuration key. Valid keys: .*$` + expectedErrorPattern := `^failed to set configuration: key '.*' is not recognized as a valid configuration key.\s*Use 'pingcli config list-keys' to view all available keys` err := RunInternalConfigSet("invalid-key=false") testutils.CheckExpectedError(t, err, &expectedErrorPattern) } diff --git a/internal/commands/config/unset_internal_test.go b/internal/commands/config/unset_internal_test.go index 2dab8f94..7c73a38d 100644 --- a/internal/commands/config/unset_internal_test.go +++ b/internal/commands/config/unset_internal_test.go @@ -23,7 +23,7 @@ func Test_RunInternalConfigUnset(t *testing.T) { func Test_RunInternalConfigUnset_InvalidKey(t *testing.T) { testutils_viper.InitVipers(t) - expectedErrorPattern := `^failed to unset configuration: key '.*' is not recognized as a valid configuration key. Valid keys: .*$` + expectedErrorPattern := `^failed to unset configuration: key '.*' is not recognized as a valid configuration key.\s*Use 'pingcli config list-keys' to view all available keys` err := RunInternalConfigUnset("invalid-key") testutils.CheckExpectedError(t, err, &expectedErrorPattern) } diff --git a/internal/configuration/config/list_keys_yaml.go b/internal/configuration/config/list_keys_yaml.go new file mode 100644 index 00000000..bc2f540c --- /dev/null +++ b/internal/configuration/config/list_keys_yaml.go @@ -0,0 +1,35 @@ +package configuration_config + +import ( + "github.com/pingidentity/pingcli/internal/configuration/options" + "github.com/pingidentity/pingcli/internal/customtypes" + "github.com/spf13/pflag" +) + +func InitConfigListKeyOptions() { + initConfigListKeysYAMLOption() +} + +func initConfigListKeysYAMLOption() { + cobraParamName := "yaml" + cobraValue := new(customtypes.Bool) + defaultValue := customtypes.Bool(false) + + options.ConfigListKeysYamlOption = options.Option{ + CobraParamName: cobraParamName, + CobraParamValue: cobraValue, + DefaultValue: &defaultValue, + EnvVar: "", // No environment variable + Flag: &pflag.Flag{ + Name: cobraParamName, + Shorthand: "y", + Usage: "Output configuration keys in YAML format. " + + "(default false)", + Value: cobraValue, + NoOptDefVal: "true", // Make this flag a boolean flag + }, + Sensitive: false, + Type: options.ENUM_BOOL, + ViperKey: "", // No viper key + } +} diff --git a/internal/configuration/configuration.go b/internal/configuration/configuration.go index fd9ad30f..26390eb1 100644 --- a/internal/configuration/configuration.go +++ b/internal/configuration/configuration.go @@ -33,8 +33,7 @@ func ValidateViperKey(viperKey string) error { } } - validKeysStr := strings.Join(validKeys, ", ") - return fmt.Errorf("key '%s' is not recognized as a valid configuration key. Valid keys: %s", viperKey, validKeysStr) + return fmt.Errorf("key '%s' is not recognized as a valid configuration key.\nUse 'pingcli config list-keys' to view all available keys", viperKey) } // Return a list of all viper keys from Options @@ -67,8 +66,7 @@ func ValidateParentViperKey(viperKey string) error { } } - validKeysStr := "\n- " + strings.Join(validKeys, "\n- ") - return fmt.Errorf("key '%s' is not recognized as a valid configuration key. Valid keys: %s", viperKey, validKeysStr) + return fmt.Errorf("key '%s' is not recognized as a valid configuration key.\nUse 'pingcli config list-keys' to view all available keys", viperKey) } func OptionFromViperKey(viperKey string) (opt options.Option, err error) { @@ -83,6 +81,7 @@ func OptionFromViperKey(viperKey string) (opt options.Option, err error) { func InitAllOptions() { configuration_config.InitConfigAddProfileOptions() configuration_config.InitConfigDeleteProfileOptions() + configuration_config.InitConfigListKeyOptions() configuration_platform.InitPlatformExportOptions() diff --git a/internal/configuration/configuration_test.go b/internal/configuration/configuration_test.go index 08862b67..d2d3c52f 100644 --- a/internal/configuration/configuration_test.go +++ b/internal/configuration/configuration_test.go @@ -22,7 +22,7 @@ func Test_ValidateViperKey(t *testing.T) { func Test_ValidateViperKey_InvalidKey(t *testing.T) { testutils_viper.InitVipers(t) - expectedErrorPattern := `^key '.*' is not recognized as a valid configuration key. Valid keys: .*$` + expectedErrorPattern := `^key '.*' is not recognized as a valid configuration key.\s*Use 'pingcli config list-keys' to view all available keys` err := configuration.ValidateViperKey("invalid-key") testutils.CheckExpectedError(t, err, &expectedErrorPattern) } @@ -31,7 +31,7 @@ func Test_ValidateViperKey_InvalidKey(t *testing.T) { func Test_ValidateViperKey_EmptyKey(t *testing.T) { testutils_viper.InitVipers(t) - expectedErrorPattern := `^key '' is not recognized as a valid configuration key. Valid keys: .*$` + expectedErrorPattern := `^key '' is not recognized as a valid configuration key.\s*Use 'pingcli config list-keys' to view all available keys` err := configuration.ValidateViperKey("") testutils.CheckExpectedError(t, err, &expectedErrorPattern) } @@ -50,7 +50,7 @@ func Test_ValidateParentViperKey(t *testing.T) { func Test_ValidateParentViperKey_InvalidKey(t *testing.T) { testutils_viper.InitVipers(t) - expectedErrorPattern := `(?s)^key '.*' is not recognized as a valid configuration key. Valid keys: .*$` + expectedErrorPattern := `^key '.*' is not recognized as a valid configuration key.\s*Use 'pingcli config list-keys' to view all available keys` err := configuration.ValidateParentViperKey("invalid-key") testutils.CheckExpectedError(t, err, &expectedErrorPattern) } @@ -59,7 +59,7 @@ func Test_ValidateParentViperKey_InvalidKey(t *testing.T) { func Test_ValidateParentViperKey_EmptyKey(t *testing.T) { testutils_viper.InitVipers(t) - expectedErrorPattern := `(?s)^key '' is not recognized as a valid configuration key. Valid keys: .*$` + expectedErrorPattern := `^key '' is not recognized as a valid configuration key.\s*Use 'pingcli config list-keys' to view all available keys` err := configuration.ValidateParentViperKey("") testutils.CheckExpectedError(t, err, &expectedErrorPattern) } diff --git a/internal/configuration/options/options.go b/internal/configuration/options/options.go index 2afeaa85..fa57bb59 100644 --- a/internal/configuration/options/options.go +++ b/internal/configuration/options/options.go @@ -77,6 +77,7 @@ func Options() []Option { ConfigAddProfileNameOption, ConfigAddProfileSetActiveOption, ConfigDeleteAutoAcceptOption, + ConfigListKeysYamlOption, RequestDataOption, RequestHTTPMethodOption, @@ -126,6 +127,8 @@ var ( ConfigAddProfileNameOption Option ConfigAddProfileSetActiveOption Option + ConfigListKeysYamlOption Option + ConfigDeleteAutoAcceptOption Option )