diff --git a/cmd/config/list_keys_test.go b/cmd/config/list_keys_test.go index 15b0bb7e..aecd0635 100644 --- a/cmd/config/list_keys_test.go +++ b/cmd/config/list_keys_test.go @@ -50,6 +50,7 @@ func Example_listKeysValue() { // Valid Keys: // - activeProfile // - description + // - detailedExitCode // - export.format // - export.outputDirectory // - export.overwrite diff --git a/cmd/root.go b/cmd/root.go index 12c26d57..f919116b 100644 --- a/cmd/root.go +++ b/cmd/root.go @@ -55,6 +55,9 @@ func NewRootCommand(version string, commit string) *cobra.Command { // --config, -C cmd.PersistentFlags().AddFlag(options.RootConfigOption.Flag) + // --detailed-exitcode, -D + cmd.PersistentFlags().AddFlag(options.RootDetailedExitCodeOption.Flag) + // --profile, -P cmd.PersistentFlags().AddFlag(options.RootProfileOption.Flag) // auto-completion @@ -131,7 +134,7 @@ func checkCfgFileLocation(cfgFile string) { if os.IsNotExist(err) { // Only create a new configuration file if it is the default configuration file location if cfgFile == options.RootConfigOption.DefaultValue.String() { - output.Warn(fmt.Sprintf("Ping CLI configuration file '%s' does not exist.", cfgFile), nil) + output.Message(fmt.Sprintf("Ping CLI configuration file '%s' does not exist.", cfgFile), nil) createConfigFile(options.RootConfigOption.DefaultValue.String()) } else { diff --git a/cmd/root_test.go b/cmd/root_test.go index 51b4a05b..f53a9d9e 100644 --- a/cmd/root_test.go +++ b/cmd/root_test.go @@ -5,9 +5,12 @@ package cmd_test import ( "testing" + "github.com/pingidentity/pingcli/internal/configuration/options" "github.com/pingidentity/pingcli/internal/customtypes" + "github.com/pingidentity/pingcli/internal/output" "github.com/pingidentity/pingcli/internal/testing/testutils" "github.com/pingidentity/pingcli/internal/testing/testutils_cobra" + "github.com/pingidentity/pingcli/internal/testing/testutils_viper" ) // Test Root Command Executes without issue @@ -51,7 +54,7 @@ func TestRootCmd_VersionFlag(t *testing.T) { // Test Root Command Executes when provided the --output-format flag func TestRootCmd_OutputFormatFlag(t *testing.T) { for _, outputFormat := range customtypes.OutputFormatValidValues() { - err := testutils_cobra.ExecutePingcli(t, "--output-format", outputFormat) + err := testutils_cobra.ExecutePingcli(t, "--"+options.RootOutputFormatOption.CobraParamName, outputFormat) testutils.CheckExpectedError(t, err, nil) } } @@ -59,23 +62,23 @@ func TestRootCmd_OutputFormatFlag(t *testing.T) { // Test Root Command fails when provided an invalid value for the --output-format flag func TestRootCmd_InvalidOutputFlag(t *testing.T) { expectedErrorPattern := `^invalid argument "invalid" for "-O, --output-format" flag: unrecognized Output Format: 'invalid'\. Must be one of: [a-z\s,]+$` - err := testutils_cobra.ExecutePingcli(t, "--output-format", "invalid") + err := testutils_cobra.ExecutePingcli(t, "--"+options.RootOutputFormatOption.CobraParamName, "invalid") testutils.CheckExpectedError(t, err, &expectedErrorPattern) } // Test Root Command fails when provided no value for the --output-format flag func TestRootCmd_NoValueOutputFlag(t *testing.T) { expectedErrorPattern := `^flag needs an argument: --output-format$` - err := testutils_cobra.ExecutePingcli(t, "--output-format") + err := testutils_cobra.ExecutePingcli(t, "--"+options.RootOutputFormatOption.CobraParamName) testutils.CheckExpectedError(t, err, &expectedErrorPattern) } // Test Root Command Executes output does not change with output-format=text vs output-format=json func TestRootCmd_OutputFlagTextVsJSON(t *testing.T) { - textOutput, err := testutils_cobra.ExecutePingcliCaptureCobraOutput(t, "--output-format", "text") + textOutput, err := testutils_cobra.ExecutePingcliCaptureCobraOutput(t, "--"+options.RootOutputFormatOption.CobraParamName, "text") testutils.CheckExpectedError(t, err, nil) - jsonOutput, err := testutils_cobra.ExecutePingcliCaptureCobraOutput(t, "--output-format", "json") + jsonOutput, err := testutils_cobra.ExecutePingcliCaptureCobraOutput(t, "--"+options.RootOutputFormatOption.CobraParamName, "json") testutils.CheckExpectedError(t, err, nil) if textOutput != jsonOutput { @@ -85,42 +88,64 @@ func TestRootCmd_OutputFlagTextVsJSON(t *testing.T) { // Test Root Command Executes when provided the --no-color flag func TestRootCmd_ColorFlag(t *testing.T) { - err := testutils_cobra.ExecutePingcli(t, "--no-color") + err := testutils_cobra.ExecutePingcli(t, "--"+options.RootColorOption.CobraParamName) testutils.CheckExpectedError(t, err, nil) - err = testutils_cobra.ExecutePingcli(t, "--no-color=false") + err = testutils_cobra.ExecutePingcli(t, "--"+options.RootColorOption.CobraParamName+"=false") testutils.CheckExpectedError(t, err, nil) } // Test Root Command fails when provided an invalid value for the --no-color flag func TestRootCmd_InvalidColorFlag(t *testing.T) { expectedErrorPattern := `^invalid argument "invalid" for ".*" flag: strconv\.ParseBool: parsing "invalid": invalid syntax$` - err := testutils_cobra.ExecutePingcli(t, "--no-color=invalid") + err := testutils_cobra.ExecutePingcli(t, "--"+options.RootColorOption.CobraParamName+"=invalid") testutils.CheckExpectedError(t, err, &expectedErrorPattern) } // Test Root Command Executes when provided the --config flag func TestRootCmd_ConfigFlag(t *testing.T) { - err := testutils_cobra.ExecutePingcli(t, "--config", "config.yaml") + err := testutils_cobra.ExecutePingcli(t, "--"+options.RootConfigOption.CobraParamName, "config.yaml") testutils.CheckExpectedError(t, err, nil) } // Test Root Command fails when provided no value for the --config flag func TestRootCmd_NoValueConfigFlag(t *testing.T) { expectedErrorPattern := `^flag needs an argument: --config$` - err := testutils_cobra.ExecutePingcli(t, "--config") + err := testutils_cobra.ExecutePingcli(t, "--"+options.RootConfigOption.CobraParamName) testutils.CheckExpectedError(t, err, &expectedErrorPattern) } // Test Root Command Executes when provided the --profile flag func TestRootCmd_ProfileFlag(t *testing.T) { - err := testutils_cobra.ExecutePingcli(t, "--profile", "default") + err := testutils_cobra.ExecutePingcli(t, "--"+options.RootProfileOption.CobraParamName, "default") testutils.CheckExpectedError(t, err, nil) } // Test Root Command fails when provided no value for the --profile flag func TestRootCmd_NoValueProfileFlag(t *testing.T) { expectedErrorPattern := `^flag needs an argument: --profile$` - err := testutils_cobra.ExecutePingcli(t, "--profile") + err := testutils_cobra.ExecutePingcli(t, "--"+options.RootProfileOption.CobraParamName) testutils.CheckExpectedError(t, err, &expectedErrorPattern) } + +// // Test Root Command Detailed Exit Code Flag +func TestRootCmd_DetailedExitCodeFlag(t *testing.T) { + err := testutils_cobra.ExecutePingcli(t, "--"+options.RootDetailedExitCodeOption.CobraParamName) + testutils.CheckExpectedError(t, err, nil) + + err = testutils_cobra.ExecutePingcli(t, "-"+options.RootDetailedExitCodeOption.Flag.Shorthand) + testutils.CheckExpectedError(t, err, nil) +} + +// // Test Root Command Detailed Exit Code Flag with output Warn +func TestRootCmd_DetailedExitCodeWarnLoggedFunc(t *testing.T) { + testutils_viper.InitVipers(t) + t.Setenv(options.RootDetailedExitCodeOption.EnvVar, "true") + output.Warn("test warning", nil) + + warnLogged, err := output.DetailedExitCodeWarnLogged() + testutils.CheckExpectedError(t, err, nil) + if !warnLogged { + t.Errorf("Expected DetailedExitCodeWarnLogged to return true") + } +} diff --git a/internal/commands/platform/export_internal.go b/internal/commands/platform/export_internal.go index 647d8f14..03fabc05 100644 --- a/internal/commands/platform/export_internal.go +++ b/internal/commands/platform/export_internal.go @@ -398,7 +398,7 @@ func createOrValidateOutputDir(outputDir string, overwriteExport bool) (resolved l.Debug().Msgf("Validating export output directory '%s'", outputDir) _, err = os.Stat(outputDir) if err != nil { - output.Warn(fmt.Sprintf("Output directory does not exist. Creating the directory at filepath '%s'", outputDir), nil) + output.Message(fmt.Sprintf("Output directory does not exist. Creating the directory at filepath '%s'", outputDir), nil) err = os.MkdirAll(outputDir, os.ModePerm) if err != nil { @@ -440,7 +440,7 @@ func getPingOneExportEnvID() (err error) { return fmt.Errorf("failed to determine pingone export environment ID") } - output.Warn("No target PingOne export environment ID specified. Defaulting export environment ID to the Worker App environment ID.", nil) + output.Message("No target PingOne export environment ID specified. Defaulting export environment ID to the Worker App environment ID.", nil) } return nil diff --git a/internal/commands/request/request_internal.go b/internal/commands/request/request_internal.go index 1f707e0b..c39a2ff8 100644 --- a/internal/commands/request/request_internal.go +++ b/internal/commands/request/request_internal.go @@ -180,7 +180,7 @@ func pingoneAccessToken() (accessToken string, err error) { } } - output.Warn("PingOne access token does not exist or is expired, requesting a new token...", nil) + output.Message("PingOne access token does not exist or is expired, requesting a new token...", nil) // If no valid access token is available, login and get a new one return pingoneAuth() diff --git a/internal/configuration/options/options.go b/internal/configuration/options/options.go index 73181203..f81edb32 100644 --- a/internal/configuration/options/options.go +++ b/internal/configuration/options/options.go @@ -73,6 +73,7 @@ func Options() []Option { RootProfileOption, RootColorOption, RootConfigOption, + RootDetailedExitCodeOption, RootOutputFormatOption, ProfileDescriptionOption, @@ -157,11 +158,12 @@ var ( // Root Command Options var ( - RootActiveProfileOption Option - RootProfileOption Option - RootColorOption Option - RootConfigOption Option - RootOutputFormatOption Option + RootActiveProfileOption Option + RootDetailedExitCodeOption Option + RootProfileOption Option + RootColorOption Option + RootConfigOption Option + RootOutputFormatOption Option ) // 'pingcli request' command options diff --git a/internal/configuration/root/root.go b/internal/configuration/root/root.go index 9f042549..6ba4632e 100644 --- a/internal/configuration/root/root.go +++ b/internal/configuration/root/root.go @@ -18,6 +18,7 @@ func InitRootOptions() { initProfileOption() initColorOption() initConfigOption() + initDetailedExitCodeOption() initOutputFormatOption() initUnmaskSecretValuesOption() } @@ -104,6 +105,32 @@ func initConfigOption() { } } +func initDetailedExitCodeOption() { + cobraParamName := "detailed-exitcode" + cobraValue := new(customtypes.Bool) + defaultValue := customtypes.Bool(false) + + options.RootDetailedExitCodeOption = options.Option{ + CobraParamName: cobraParamName, + CobraParamValue: cobraValue, + DefaultValue: &defaultValue, + EnvVar: "PINGCLI_DETAILED_EXITCODE", + Flag: &pflag.Flag{ + Name: cobraParamName, + Shorthand: "D", + Usage: "Enable detailed exit code output. (default false)" + + "\n0 - pingcli command succeeded with no errors or warnings." + + "\n1 - pingcli command failed with errors." + + "\n2 - pingcli command succeeded with warnings.", + Value: cobraValue, + NoOptDefVal: "true", // Make this flag a boolean flag + }, + Sensitive: false, + Type: options.ENUM_BOOL, + ViperKey: "detailedExitCode", + } +} + func initOutputFormatOption() { cobraParamName := "output-format" cobraValue := new(customtypes.OutputFormat) diff --git a/internal/connector/pingone/platform/resources/pingone_custom_domain_test.go b/internal/connector/pingone/platform/resources/pingone_custom_domain_test.go index 368cff86..3cef8e43 100644 --- a/internal/connector/pingone/platform/resources/pingone_custom_domain_test.go +++ b/internal/connector/pingone/platform/resources/pingone_custom_domain_test.go @@ -20,8 +20,8 @@ func TestCustomDomainExport(t *testing.T) { expectedImportBlocks := []connector.ImportBlock{ { ResourceType: "pingone_custom_domain", - ResourceName: "pioneerpalaceband.com", - ResourceID: fmt.Sprintf("%s/5eb2548d-fdb2-45f6-85bc-7adfd856cbd9", clientInfo.PingOneExportEnvironmentID), + ResourceName: "mycustomdomain.com", + ResourceID: fmt.Sprintf("%s/2f478666-ce6d-48b8-aef8-043ad3093109", clientInfo.PingOneExportEnvironmentID), }, } diff --git a/internal/connector/pingone/platform/resources/pingone_trusted_email_domain_test.go b/internal/connector/pingone/platform/resources/pingone_trusted_email_domain_test.go index 49f1659b..7585d479 100644 --- a/internal/connector/pingone/platform/resources/pingone_trusted_email_domain_test.go +++ b/internal/connector/pingone/platform/resources/pingone_trusted_email_domain_test.go @@ -33,11 +33,6 @@ func TestTrustedEmailDomainExport(t *testing.T) { ResourceName: "demo.bxretail.org", ResourceID: fmt.Sprintf("%s/49f94864-f9c7-4778-ae37-839c2c546d1c", clientInfo.PingOneExportEnvironmentID), }, - { - ResourceType: "pingone_trusted_email_domain", - ResourceName: "pioneerpalaceband.com", - ResourceID: fmt.Sprintf("%s/63d645d1-046a-4d53-a267-513cfc1d4213", clientInfo.PingOneExportEnvironmentID), - }, } testutils.ValidateImportBlocks(t, resource, &expectedImportBlocks) diff --git a/internal/customtypes/export_services.go b/internal/customtypes/export_services.go index c3cfe2f8..09d5623d 100644 --- a/internal/customtypes/export_services.go +++ b/internal/customtypes/export_services.go @@ -68,6 +68,10 @@ func (es *ExportServices) SetServicesByServiceGroup(serviceGroup *ExportServiceG return fmt.Errorf("failed to set ExportServices value: %s. ExportServices is nil", serviceGroup) } + if serviceGroup.String() == "" { + return nil + } + switch { case strings.EqualFold(ENUM_EXPORT_SERVICE_GROUP_PINGONE, serviceGroup.String()): return es.Set(strings.Join(ExportServicesPingOneValidValues(), ",")) diff --git a/internal/output/output.go b/internal/output/output.go index ee236d5d..cbf3be01 100644 --- a/internal/output/output.go +++ b/internal/output/output.go @@ -16,12 +16,13 @@ import ( ) var ( - boldRed = color.New(color.FgRed).Add(color.Bold).SprintfFunc() - cyan = color.New(color.FgCyan).SprintfFunc() - green = color.New(color.FgGreen).SprintfFunc() - red = color.New(color.FgRed).SprintfFunc() - white = color.New(color.FgWhite).SprintfFunc() - yellow = color.New(color.FgYellow).SprintfFunc() + boldRed = color.New(color.FgRed).Add(color.Bold).SprintfFunc() + cyan = color.New(color.FgCyan).SprintfFunc() + green = color.New(color.FgGreen).SprintfFunc() + red = color.New(color.FgRed).SprintfFunc() + white = color.New(color.FgWhite).SprintfFunc() + yellow = color.New(color.FgYellow).SprintfFunc() + detailedExitCodeWarnLogged = false ) // Set the faith color option based on user configuration @@ -56,10 +57,23 @@ func Success(message string, fields map[string]interface{}) { // This function outputs yellow text to inform the user of a warning func Warn(message string, fields map[string]interface{}) { l := logger.Get() + detailedExitCodeWarnLogged = true print(fmt.Sprintf("WARNING: %s", message), fields, yellow, l.Warn) } +func DetailedExitCodeWarnLogged() (bool, error) { + detailedExitCodeEnabled, err := profiles.GetOptionValue(options.RootDetailedExitCodeOption) + if err != nil { + return false, err + } + + if detailedExitCodeEnabled == "true" { + return detailedExitCodeWarnLogged, nil + } + return false, nil +} + // This functions is used to inform the user their configuration // or input to pingcli has caused an error. func UserError(message string, fields map[string]interface{}) { diff --git a/main.go b/main.go index 0cb7a89b..3e5d2b68 100644 --- a/main.go +++ b/main.go @@ -38,4 +38,15 @@ func main() { output.UserError(fmt.Sprintf("Failed to execute pingcli: %v", err), nil) os.Exit(1) } + + detailedExitCodeWarnLogged, err := output.DetailedExitCodeWarnLogged() + if err != nil { + output.UserError(fmt.Sprintf("Failed to execute pingcli: %v", err), nil) + os.Exit(1) + } + if detailedExitCodeWarnLogged { + os.Exit(2) + } else { + os.Exit(0) + } }