From 9ab9f13819a23e64cb02daa7cc2f743c3135f4d6 Mon Sep 17 00:00:00 2001 From: Jiri Hnidek Date: Thu, 2 May 2024 20:00:28 +0200 Subject: [PATCH 1/3] feat: Add CLI option --format to disconnect command * It is possible to print output of disconnect command in machine-readable format. The --format requires argument and it could be only "json" ATM. It follows practice of status command. * Refactored code a little to share some code with status command. --- main.go | 233 ++++++++++++++++++++++++++++++++++++++++++++------------ 1 file changed, 183 insertions(+), 50 deletions(-) diff --git a/main.go b/main.go index cf77b48..83b94c8 100644 --- a/main.go +++ b/main.go @@ -401,35 +401,171 @@ func connectAction(ctx *cli.Context) error { return nil } -// disconnectAction tries to stop rhscd service, disconnect from Red Hat Insights and finally -// it unregister system from Red Hat Subscription Management +// setupFormatOption ensures the user has supplied a correct `--format` flag +// and set values in uiSettings, when JSON format is used. +func setupFormatOption(ctx *cli.Context) error { + // This is run after the `app.Before()` has been run, + // the uiSettings is already set up for us to modify. + format := ctx.String("format") + switch format { + case "": + return nil + case "json": + uiSettings.isMachineReadable = true + uiSettings.isRich = false + return nil + default: + err := fmt.Errorf( + "unsupported format: %s (supported formats: %s)", + format, + `"json"`, + ) + return cli.Exit(err, 1) + } +} + +// DisconnectResult is structure holding information about result of +// disconnect command. The result could be printed in machine-readable format. +type DisconnectResult struct { + Hostname string `json:"hostname"` + HostnameError string `json:"hostname_error,omitempty"` + UID int `json:"uid"` + UIDError string `json:"uid_error,omitempty"` + RHSMDisconnected bool `json:"rhsm_disconnected"` + RHSMDisconnectedError string `json:"rhsm_disconnect_error,omitempty"` + InsightsDisconnected bool `json:"insights_disconnected"` + InsightsDisconnectedError string `json:"insights_disconnected_error,omitempty"` + YggdrasilStopped bool `json:"yggdrasil_stopped"` + YggdrasilStoppedError string `json:"yggdrasil_stopped_error,omitempty"` + exitCode int + format string +} + +// Error implement error interface for structure DisconnectResult +func (disconnectResult DisconnectResult) Error() string { + var result string + switch disconnectResult.format { + case "json": + data, err := json.MarshalIndent(disconnectResult, "", " ") + if err != nil { + return err.Error() + } + result = string(data) + default: + result = "error: unsupported document format: " + disconnectResult.format + } + return result +} + +// printJSONDisconnectResult tries to print the result of disconnect as JSON to stdout. +// When marshaling of systemStatus fails, then error is returned +func printJSONDisconnectResult(disconnectResult *DisconnectResult) error { + data, err := json.MarshalIndent(disconnectResult, "", " ") + if err != nil { + return err + } + fmt.Println(string(data)) + return nil +} + +// beforeDisconnectAction ensures the used has supplied a correct `--format` flag +func beforeDisconnectAction(ctx *cli.Context) error { + return setupFormatOption(ctx) +} + +// interactivePrintf is method for printing human-readable output. It suppresses output, when +// machine-readable format is used. +func interactivePrintf(format string, a ...interface{}) { + if !uiSettings.isMachineReadable { + fmt.Printf(format, a...) + } +} + +// disconnectAction tries to stop (yggdrasil) rhcd service, disconnect from Red Hat Insights, +// and finally it unregisters system from Red Hat Subscription Management func disconnectAction(ctx *cli.Context) error { + var disconnectResult DisconnectResult + var machineReadablePrintFunc func(disconnectResult *DisconnectResult) error + + disconnectResult.format = ctx.String("format") + format := ctx.String("format") + switch format { + case "json": + machineReadablePrintFunc = printJSONDisconnectResult + default: + break + } + + // When printing of status is requested, then print machine-readable file format + // at the end of this function + if uiSettings.isMachineReadable { + defer func(disconnectResult *DisconnectResult) { + // When exit code is zero, then print machine-readable output + // When exit code has non-zero value, then disconnectResult is returned as a error + if disconnectResult.exitCode == 0 && machineReadablePrintFunc != nil { + err := machineReadablePrintFunc(disconnectResult) + // When it was not possible to print result of disconnect to machine-readable format, then + // change returned error to CLI exit error to be able to set exit code to + // a non-zero value + if err != nil { + panic(fmt.Errorf("unable to print status as %s document: %s", format, err.Error())) + } + } + }(&disconnectResult) + } + + disconnectResult.exitCode = 0 + uid := os.Getuid() if uid != 0 { - return cli.Exit(fmt.Errorf("error: non-root user cannot disconnect system"), 1) + errMsg := "non-root user cannot disconnect system" + exitCode := 1 + if uiSettings.isMachineReadable { + disconnectResult.UID = uid + disconnectResult.UIDError = errMsg + disconnectResult.exitCode = exitCode + return cli.Exit(disconnectResult, exitCode) + } else { + return cli.Exit(fmt.Errorf("error: %s", errMsg), exitCode) + } } - var start time.Time - durations := make(map[string]time.Duration) - errorMessages := make(map[string]LogMessage) hostname, err := os.Hostname() + if uiSettings.isMachineReadable { + disconnectResult.Hostname = hostname + } if err != nil { - return cli.Exit(err, 1) + exitCode := 1 + if uiSettings.isMachineReadable { + disconnectResult.HostnameError = err.Error() + disconnectResult.exitCode = exitCode + return cli.Exit(disconnectResult, exitCode) + } else { + return cli.Exit(err, exitCode) + } } - fmt.Printf("Disconnecting %v from %v.\nThis might take a few seconds.\n\n", hostname, Provider) + + interactivePrintf("Disconnecting %v from %v.\nThis might take a few seconds.\n\n", hostname, Provider) + + var start time.Time + durations := make(map[string]time.Duration) + errorMessages := make(map[string]LogMessage) /* 1. Deactivate yggdrasil (rhcd) service */ start = time.Now() progressMessage := fmt.Sprintf(" Deactivating the %v service", ServiceName) err = showProgress(progressMessage, deactivateService) if err != nil { + errMsg := fmt.Sprintf("Cannot deactivate %s service: %v", ServiceName, err) errorMessages[ServiceName] = LogMessage{ - level: log.LevelError, - message: fmt.Errorf("cannot deactivate %s service: %w", - ServiceName, err)} - fmt.Printf(uiSettings.iconError+" Cannot deactivate the %v service\n", ServiceName) + level: log.LevelError, + message: fmt.Errorf(errMsg)} + disconnectResult.YggdrasilStopped = false + disconnectResult.YggdrasilStoppedError = errMsg + interactivePrintf(uiSettings.iconError + " " + errMsg + "\n") } else { - fmt.Printf(uiSettings.iconOK+" Deactivated the %v service\n", ServiceName) + disconnectResult.YggdrasilStopped = true + interactivePrintf(uiSettings.iconOK+" Deactivated the %v service\n", ServiceName) } durations[ServiceName] = time.Since(start) @@ -437,13 +573,16 @@ func disconnectAction(ctx *cli.Context) error { start = time.Now() err = showProgress(" Disconnecting from Red Hat Insights...", unregisterInsights) if err != nil { + errMsg := fmt.Sprintf("Cannot disconnect from Red Hat Insights: %v", err) errorMessages["insights"] = LogMessage{ - level: log.LevelError, - message: fmt.Errorf("cannot disconnect from Red Hat Insights: %w", - err)} - fmt.Printf(uiSettings.iconError + " Cannot disconnect from Red Hat Insights\n") + level: log.LevelError, + message: fmt.Errorf(errMsg)} + disconnectResult.InsightsDisconnected = false + disconnectResult.InsightsDisconnectedError = errMsg + interactivePrintf(uiSettings.iconError + " " + errMsg + "\n") } else { - fmt.Print(uiSettings.iconOK + " Disconnected from Red Hat Insights\n") + disconnectResult.InsightsDisconnected = true + interactivePrintf(uiSettings.iconOK + " Disconnected from Red Hat Insights\n") } durations["insights"] = time.Since(start) @@ -452,25 +591,28 @@ func disconnectAction(ctx *cli.Context) error { " Disconnecting from Red Hat Subscription Management...", unregister, ) if err != nil { + errMsg := fmt.Sprintf("Cannot disconnect from Red Hat Subscription Management: %v", err) errorMessages["rhsm"] = LogMessage{ - level: log.LevelError, - message: fmt.Errorf("cannot disconnect from Red Hat Subscription Management: %w", - err)} - fmt.Printf( - uiSettings.iconError + " Cannot disconnect from Red Hat Subscription Management\n", - ) + level: log.LevelError, + message: fmt.Errorf(errMsg)} + + disconnectResult.RHSMDisconnected = false + disconnectResult.RHSMDisconnectedError = errMsg + interactivePrintf(uiSettings.iconError + " " + errMsg + "\n") } else { - fmt.Printf(uiSettings.iconOK + " Disconnected from Red Hat Subscription Management\n") + disconnectResult.RHSMDisconnected = true + interactivePrintf(uiSettings.iconOK + " Disconnected from Red Hat Subscription Management\n") } durations["rhsm"] = time.Since(start) - fmt.Printf("\nManage your connected systems: https://red.ht/connector\n") - - showTimeDuration(durations) + if !uiSettings.isMachineReadable { + fmt.Printf("\nManage your connected systems: https://red.ht/connector\n") + showTimeDuration(durations) - err = showErrorMessages("disconnect", errorMessages) - if err != nil { - return err + err = showErrorMessages("disconnect", errorMessages) + if err != nil { + return err + } } return nil @@ -519,24 +661,7 @@ func printJSONStatus(systemStatus *SystemStatus) error { // beforeStatusAction ensures the user has supplied a correct `--format` flag. func beforeStatusAction(ctx *cli.Context) error { - // This is run after the `app.Before()` has been run, - // the uiSettings is already set up for us to modify. - format := ctx.String("format") - switch format { - case "": - return nil - case "json": - uiSettings.isMachineReadable = true - uiSettings.isRich = false - return nil - default: - err := fmt.Errorf( - "unsupported format: %s (supported formats: %s)", - format, - `"json"`, - ) - return cli.Exit(err, 1) - } + return setupFormatOption(ctx) } // statusAction tries to print status of system. It means that it gives @@ -774,10 +899,18 @@ func main() { Action: connectAction, }, { - Name: "disconnect", + Name: "disconnect", + Flags: []cli.Flag{ + &cli.StringFlag{ + Name: "format", + Usage: "prints output of disconnection in machine-readable format (supported formats: \"json\")", + Aliases: []string{"f"}, + }, + }, Usage: "Disconnects the system from " + Provider, UsageText: fmt.Sprintf("%v disconnect", app.Name), Description: fmt.Sprintf("The disconnect command disconnects the system from Red Hat Subscription Management, Red Hat Insights and %v and deactivates the %v service. %v will no longer be able to interact with the system.", Provider, ServiceName, Provider), + Before: beforeDisconnectAction, Action: disconnectAction, }, { From 1490c11d3e63ae6d0cf6ee0427601dd566e47e52 Mon Sep 17 00:00:00 2001 From: Jiri Hnidek Date: Wed, 21 Aug 2024 15:20:32 +0200 Subject: [PATCH 2/3] refactor: Refactoring of disconnect using --format json * When --format json is used, then use only Error() interface for printing disconnect result --- main.go | 46 ++-------------------------------------------- 1 file changed, 2 insertions(+), 44 deletions(-) diff --git a/main.go b/main.go index 83b94c8..aa0b92c 100644 --- a/main.go +++ b/main.go @@ -437,11 +437,11 @@ type DisconnectResult struct { InsightsDisconnectedError string `json:"insights_disconnected_error,omitempty"` YggdrasilStopped bool `json:"yggdrasil_stopped"` YggdrasilStoppedError string `json:"yggdrasil_stopped_error,omitempty"` - exitCode int format string } // Error implement error interface for structure DisconnectResult +// It is used for printing DisconnectResult by cli.Exit() func (disconnectResult DisconnectResult) Error() string { var result string switch disconnectResult.format { @@ -457,17 +457,6 @@ func (disconnectResult DisconnectResult) Error() string { return result } -// printJSONDisconnectResult tries to print the result of disconnect as JSON to stdout. -// When marshaling of systemStatus fails, then error is returned -func printJSONDisconnectResult(disconnectResult *DisconnectResult) error { - data, err := json.MarshalIndent(disconnectResult, "", " ") - if err != nil { - return err - } - fmt.Println(string(data)) - return nil -} - // beforeDisconnectAction ensures the used has supplied a correct `--format` flag func beforeDisconnectAction(ctx *cli.Context) error { return setupFormatOption(ctx) @@ -485,36 +474,7 @@ func interactivePrintf(format string, a ...interface{}) { // and finally it unregisters system from Red Hat Subscription Management func disconnectAction(ctx *cli.Context) error { var disconnectResult DisconnectResult - var machineReadablePrintFunc func(disconnectResult *DisconnectResult) error - disconnectResult.format = ctx.String("format") - format := ctx.String("format") - switch format { - case "json": - machineReadablePrintFunc = printJSONDisconnectResult - default: - break - } - - // When printing of status is requested, then print machine-readable file format - // at the end of this function - if uiSettings.isMachineReadable { - defer func(disconnectResult *DisconnectResult) { - // When exit code is zero, then print machine-readable output - // When exit code has non-zero value, then disconnectResult is returned as a error - if disconnectResult.exitCode == 0 && machineReadablePrintFunc != nil { - err := machineReadablePrintFunc(disconnectResult) - // When it was not possible to print result of disconnect to machine-readable format, then - // change returned error to CLI exit error to be able to set exit code to - // a non-zero value - if err != nil { - panic(fmt.Errorf("unable to print status as %s document: %s", format, err.Error())) - } - } - }(&disconnectResult) - } - - disconnectResult.exitCode = 0 uid := os.Getuid() if uid != 0 { @@ -523,7 +483,6 @@ func disconnectAction(ctx *cli.Context) error { if uiSettings.isMachineReadable { disconnectResult.UID = uid disconnectResult.UIDError = errMsg - disconnectResult.exitCode = exitCode return cli.Exit(disconnectResult, exitCode) } else { return cli.Exit(fmt.Errorf("error: %s", errMsg), exitCode) @@ -538,7 +497,6 @@ func disconnectAction(ctx *cli.Context) error { exitCode := 1 if uiSettings.isMachineReadable { disconnectResult.HostnameError = err.Error() - disconnectResult.exitCode = exitCode return cli.Exit(disconnectResult, exitCode) } else { return cli.Exit(err, exitCode) @@ -615,7 +573,7 @@ func disconnectAction(ctx *cli.Context) error { } } - return nil + return cli.Exit(disconnectResult, 0) } // canonicalFactAction tries to gather canonical facts about system, From 1b53bd3e2a16f61b674938fe2fb94bb92d6549b6 Mon Sep 17 00:00:00 2001 From: Jiri Hnidek Date: Mon, 2 Sep 2024 10:58:45 +0200 Subject: [PATCH 3/3] fix: Do not use non-constant format string with Printf/Errorf * Fix some code style issues and make code linter more happy --- main.go | 39 +++++++++++++++++++++------------------ status.go | 4 ++-- 2 files changed, 23 insertions(+), 20 deletions(-) diff --git a/main.go b/main.go index aa0b92c..8e66dec 100644 --- a/main.go +++ b/main.go @@ -298,10 +298,11 @@ func connectAction(ctx *cli.Context) error { message: fmt.Errorf("cannot connect to Red Hat Subscription Management: %w", err)} fmt.Printf( - uiSettings.iconError + " Cannot connect to Red Hat Subscription Management\n", + "%v Cannot connect to Red Hat Subscription Management\n", + uiSettings.iconError, ) } else { - fmt.Printf(uiSettings.iconOK + " " + returnedMsg + "\n") + fmt.Printf("%v %v\n", uiSettings.iconOK, returnedMsg) } durations["rhsm"] = time.Since(start) @@ -309,7 +310,8 @@ func connectAction(ctx *cli.Context) error { if errors, exist := errorMessages["rhsm"]; exist { if errors.level == log.LevelError { fmt.Printf( - uiSettings.iconError + " Skipping connection to Red Hat Insights\n", + "%v Skipping connection to Red Hat Insights\n", + uiSettings.iconError, ) } } else { @@ -320,9 +322,9 @@ func connectAction(ctx *cli.Context) error { level: log.LevelError, message: fmt.Errorf("cannot connect to Red Hat Insights: %w", err)} - fmt.Printf(uiSettings.iconError + " Cannot connect to Red Hat Insights\n") + fmt.Printf("%v Cannot connect to Red Hat Insights\n", uiSettings.iconError) } else { - fmt.Printf(uiSettings.iconOK + " Connected to Red Hat Insights\n") + fmt.Printf("%v Connected to Red Hat Insights\n", uiSettings.iconOK) } durations["insights"] = time.Since(start) } @@ -331,7 +333,8 @@ func connectAction(ctx *cli.Context) error { if errors, exist := errorMessages["rhsm"]; exist { if errors.level == log.LevelError { fmt.Printf( - uiSettings.iconError+" Skipping activation of %v service\n", + "%v Skipping activation of %v service\n", + uiSettings.iconError, ServiceName, ) } @@ -344,9 +347,9 @@ func connectAction(ctx *cli.Context) error { level: log.LevelError, message: fmt.Errorf("cannot activate %s service: %w", ServiceName, err)} - fmt.Printf(uiSettings.iconError+" Cannot activate the %v service\n", ServiceName) + fmt.Printf("%v Cannot activate the %v service\n", uiSettings.iconError, ServiceName) } else { - fmt.Printf(uiSettings.iconOK+" Activated the %v service\n", ServiceName) + fmt.Printf("%v Activated the %v service\n", uiSettings.iconOK, ServiceName) } durations[ServiceName] = time.Since(start) } @@ -379,7 +382,7 @@ func connectAction(ctx *cli.Context) error { message: fmt.Errorf("cannot get the user profile: %w", err)} } else { - fmt.Printf(uiSettings.iconInfo + " Enabled console.redhat.com services: ") + fmt.Printf("%v Enabled console.redhat.com services: ", uiSettings.iconInfo) showConfProfile(&profile) fmt.Printf("\n") } @@ -517,13 +520,13 @@ func disconnectAction(ctx *cli.Context) error { errMsg := fmt.Sprintf("Cannot deactivate %s service: %v", ServiceName, err) errorMessages[ServiceName] = LogMessage{ level: log.LevelError, - message: fmt.Errorf(errMsg)} + message: fmt.Errorf("%v", errMsg)} disconnectResult.YggdrasilStopped = false disconnectResult.YggdrasilStoppedError = errMsg - interactivePrintf(uiSettings.iconError + " " + errMsg + "\n") + interactivePrintf("%v %v\n", uiSettings.iconError, errMsg) } else { disconnectResult.YggdrasilStopped = true - interactivePrintf(uiSettings.iconOK+" Deactivated the %v service\n", ServiceName) + interactivePrintf("%v Deactivated the %v service\n", uiSettings.iconOK, ServiceName) } durations[ServiceName] = time.Since(start) @@ -534,13 +537,13 @@ func disconnectAction(ctx *cli.Context) error { errMsg := fmt.Sprintf("Cannot disconnect from Red Hat Insights: %v", err) errorMessages["insights"] = LogMessage{ level: log.LevelError, - message: fmt.Errorf(errMsg)} + message: fmt.Errorf("%v", errMsg)} disconnectResult.InsightsDisconnected = false disconnectResult.InsightsDisconnectedError = errMsg - interactivePrintf(uiSettings.iconError + " " + errMsg + "\n") + interactivePrintf("%v %v\n", uiSettings.iconError, errMsg) } else { disconnectResult.InsightsDisconnected = true - interactivePrintf(uiSettings.iconOK + " Disconnected from Red Hat Insights\n") + interactivePrintf("%v Disconnected from Red Hat Insights\n", uiSettings.iconOK) } durations["insights"] = time.Since(start) @@ -552,14 +555,14 @@ func disconnectAction(ctx *cli.Context) error { errMsg := fmt.Sprintf("Cannot disconnect from Red Hat Subscription Management: %v", err) errorMessages["rhsm"] = LogMessage{ level: log.LevelError, - message: fmt.Errorf(errMsg)} + message: fmt.Errorf("%v", errMsg)} disconnectResult.RHSMDisconnected = false disconnectResult.RHSMDisconnectedError = errMsg - interactivePrintf(uiSettings.iconError + " " + errMsg + "\n") + interactivePrintf("%v %v\n", uiSettings.iconError, errMsg) } else { disconnectResult.RHSMDisconnected = true - interactivePrintf(uiSettings.iconOK + " Disconnected from Red Hat Subscription Management\n") + interactivePrintf("%v Disconnected from Red Hat Subscription Management\n", uiSettings.iconOK) } durations["rhsm"] = time.Since(start) diff --git a/status.go b/status.go index 4be7d0b..baf44bd 100644 --- a/status.go +++ b/status.go @@ -22,13 +22,13 @@ func rhsmStatus(systemStatus *SystemStatus) error { if uiSettings.isMachineReadable { systemStatus.RHSMConnected = false } else { - fmt.Printf(uiSettings.iconInfo + " Not connected to Red Hat Subscription Management\n") + fmt.Printf("%v Not connected to Red Hat Subscription Management\n", uiSettings.iconInfo) } } else { if uiSettings.isMachineReadable { systemStatus.RHSMConnected = true } else { - fmt.Printf(uiSettings.iconOK + " Connected to Red Hat Subscription Management\n") + fmt.Printf("%v Connected to Red Hat Subscription Management\n", uiSettings.iconOK) } } return nil