From 41cad0c972d76963597856bcc39e4a827321bb88 Mon Sep 17 00:00:00 2001 From: Adam Ross Date: Tue, 6 Feb 2018 15:26:06 -0800 Subject: [PATCH 1/9] Add explicit privilege prompt to improve sudo UX (#138) * Explicitly prompt for privilege escallation * Remove password prompt part of privilege message * Expand sudo detection. * Tidy up timing issues. * Consolidate messaging and avoid newline in verbose. * Cleanup ToString, sudo contains, cover more exec methods. * Lint does not catch all of fmt. * Remove unnecessary password prompt from networking cleanup. * Remove color reset and cat /dev/null to clear route text. --- commands/stop.go | 8 +++----- util/logger.go | 39 +++++++++++++++++++++++++++++++-------- util/shell_exec.go | 32 +++++++++++++++++++++++++++++--- util/slices.go | 30 ++++++++++++++++++++++++++++++ 4 files changed, 93 insertions(+), 16 deletions(-) create mode 100644 util/slices.go diff --git a/commands/stop.go b/commands/stop.go index 39807d7..7927ee9 100644 --- a/commands/stop.go +++ b/commands/stop.go @@ -3,7 +3,6 @@ package commands import ( "fmt" - "github.com/fatih/color" "github.com/phase2/rig/util" "github.com/urfave/cli" ) @@ -58,15 +57,14 @@ func (cmd *Stop) StopOutrigger() error { } cmd.out.Info("Stopped machine '%s'", cmd.machine.Name) - cmd.out.Spin("Cleaning up local networking (may require your admin password)") + cmd.out.Spin("Cleaning up local networking...") if util.IsWindows() { util.Command("runas", "/noprofile", "/user:Administrator", "route", "DELETE", "172.17.0.0").Run() util.Command("runas", "/noprofile", "/user:Administrator", "route", "DELETE", "172.17.42.1").Run() } else { - util.Command("sudo", "route", "-n", "delete", "-net", "172.17.0.0").Run() - util.Command("sudo", "route", "-n", "delete", "-net", "172.17.42.1").Run() + util.Command("sudo", "-s", "--", "'", "cat", "/dev/null", ";", "route", "-n", "delete", "-net", "172.17.0.0", "'").Run() + util.Command("sudo", "-s", "--", "'", "cat", "/dev/null", ";", "route", "-n", "delete", "-net", "172.17.42.1", "'").Run() } - color.Unset() cmd.out.Info("Networking cleanup completed") return cmd.Success(fmt.Sprintf("Machine '%s' stopped", cmd.machine.Name)) diff --git a/util/logger.go b/util/logger.go index 0095225..0fa30fb 100644 --- a/util/logger.go +++ b/util/logger.go @@ -1,11 +1,11 @@ package util import ( + "fmt" "io/ioutil" "log" "os" - "fmt" "github.com/fatih/color" spun "github.com/slok/gospinner" ) @@ -24,10 +24,11 @@ type logChannels struct { // RigLogger is the global logger object type RigLogger struct { - Channel logChannels - Progress *RigSpinner - IsVerbose bool - Spinning bool + Channel logChannels + Progress *RigSpinner + IsVerbose bool + Spinning bool + Privileged bool } // RigSpinner object wrapper to facilitate our spinner service @@ -51,9 +52,10 @@ func LoggerInit(verbose bool) { Error: log.New(os.Stderr, color.RedString("[ERROR] "), 0), Verbose: log.New(verboseWriter, "[VERBOSE] ", 0), }, - IsVerbose: verbose, - Progress: &RigSpinner{s}, - Spinning: false, + IsVerbose: verbose, + Progress: &RigSpinner{s}, + Spinning: false, + Privileged: false, } } @@ -125,3 +127,24 @@ func (log *RigLogger) Verbose(format string, a ...interface{}) { func (log *RigLogger) Note(format string, a ...interface{}) { log.Channel.Info.Println(fmt.Sprintf(format, a...)) } + +// PrivilegeEscallationPrompt interrupts a running spinner to ensure clear +// prompting to the user for sudo password entry. It is up to the caller to know +// that privilege is needed. This prompt is only displayed on the first privilege +// escallation of a given rig process. +func (log *RigLogger) PrivilegeEscallationPrompt() { + defer func() { log.Privileged = true }() + + if log.Privileged { + return + } + + // This newline ensures the last status before escallation is preserved + // on-screen. It creates extraneous space in verbose mode. + if !log.IsVerbose { + fmt.Println() + } + message := "Administrative privileges needed..." + log.Spin(message) + log.Warning(message) +} diff --git a/util/shell_exec.go b/util/shell_exec.go index 45bf785..0798ffb 100644 --- a/util/shell_exec.go +++ b/util/shell_exec.go @@ -92,36 +92,53 @@ func (x Executor) Execute(forceOutput bool) error { // CombinedOutput runs a command via exec.CombinedOutput() without modification or output of the underlying command. func (x Executor) CombinedOutput() ([]byte, error) { x.Log("Executing") + if out := Logger(); out != nil && x.IsPrivileged() { + out.PrivilegeEscallationPrompt() + defer out.Spin("Resuming operation...") + } return x.cmd.CombinedOutput() } // Run runs a command via exec.Run() without modification or output of the underlying command. func (x Executor) Run() error { x.Log("Executing") + if out := Logger(); out != nil && x.IsPrivileged() { + out.PrivilegeEscallationPrompt() + defer out.Spin("Resuming operation...") + } return x.cmd.Run() } // Output runs a command via exec.Output() without modification or output of the underlying command. func (x Executor) Output() ([]byte, error) { x.Log("Executing") + if out := Logger(); out != nil && x.IsPrivileged() { + out.PrivilegeEscallationPrompt() + defer out.Spin("Resuming operation...") + } return x.cmd.Output() } // Start runs a command via exec.Start() without modification or output of the underlying command. func (x Executor) Start() error { x.Log("Executing") + if out := Logger(); out != nil && x.IsPrivileged() { + out.PrivilegeEscallationPrompt() + defer out.Spin("Resuming operation...") + } return x.cmd.Start() } // Log verbosely logs the command. func (x Executor) Log(tag string) { color.Set(color.FgMagenta) - Logger().Verbose("%s: %s", tag, x.ToString()) + Logger().Verbose("%s: %s", tag, x) color.Unset() } -// ToString converts a Command to a human-readable string with key context details. -func (x Executor) ToString() string { +// String converts a Command to a human-readable string with key context details. +// It is automatically applied in contexts such as fmt functions. +func (x Executor) String() string { context := "" if x.cmd.Dir != "" { context = fmt.Sprintf("(WD: %s", x.cmd.Dir) @@ -137,3 +154,12 @@ func (x Executor) ToString() string { return fmt.Sprintf("%s %s %s", x.cmd.Path, strings.Join(x.cmd.Args[1:], " "), context) } + +// IsPrivileged evaluates the command to determine if administrative privilege +// is required. +// @todo identify administrative escallation on Windows. +// E.g., "runas", "/noprofile", "/user:Administrator +func (x Executor) IsPrivileged() bool { + _, privileged := IndexOfSubstring(x.cmd.Args, "sudo") + return privileged +} diff --git a/util/slices.go b/util/slices.go new file mode 100644 index 0000000..30aa68d --- /dev/null +++ b/util/slices.go @@ -0,0 +1,30 @@ +package util + +import ( + "strings" +) + +// IndexOfString is a general utility function that can find the index of a value +// present in a string slice. The second value is true if the item is found. +func IndexOfString(slice []string, search string) (int, bool) { + for index, elem := range slice { + if elem == search { + return index, true + } + } + + return 0, false +} + +// IndexOfSubstring is a variation on IndexOfString which checks to see if a +// given slice value matches our search string, or if that search string is +// a substring of the element. The second value is true if the item is found. +func IndexOfSubstring(slice []string, search string) (int, bool) { + for index, elem := range slice { + if strings.Contains(elem, search) { + return index, true + } + } + + return 0, false +} From 35baaf2a9b7ea140d34c07bfe1413c2c8a89b1b6 Mon Sep 17 00:00:00 2001 From: Frank Febbraro Date: Thu, 8 Feb 2018 06:40:13 -0800 Subject: [PATCH 2/9] trying a different approach to requesting for admin privs (#144) --- commands/stop.go | 5 +++-- util/shell_exec.go | 7 +++++++ 2 files changed, 10 insertions(+), 2 deletions(-) diff --git a/commands/stop.go b/commands/stop.go index 7927ee9..31fde50 100644 --- a/commands/stop.go +++ b/commands/stop.go @@ -62,8 +62,9 @@ func (cmd *Stop) StopOutrigger() error { util.Command("runas", "/noprofile", "/user:Administrator", "route", "DELETE", "172.17.0.0").Run() util.Command("runas", "/noprofile", "/user:Administrator", "route", "DELETE", "172.17.42.1").Run() } else { - util.Command("sudo", "-s", "--", "'", "cat", "/dev/null", ";", "route", "-n", "delete", "-net", "172.17.0.0", "'").Run() - util.Command("sudo", "-s", "--", "'", "cat", "/dev/null", ";", "route", "-n", "delete", "-net", "172.17.42.1", "'").Run() + util.EscalatePrivilege() + util.Command("sudo", "route", "-n", "delete", "-net", "172.17.0.0").Run() + util.Command("sudo", "route", "-n", "delete", "-net", "172.17.42.1").Run() } cmd.out.Info("Networking cleanup completed") diff --git a/util/shell_exec.go b/util/shell_exec.go index 0798ffb..2318ad5 100644 --- a/util/shell_exec.go +++ b/util/shell_exec.go @@ -39,6 +39,13 @@ func Convert(cmd *exec.Cmd) Executor { return Executor{cmd} } +// EscalatePrivilege attempts to gain administrative privilege +// @todo identify administrative escallation on Windows. +// E.g., "runas", "/noprofile", "/user:Administrator +func EscalatePrivilege() error { + return Command("sudo", "-v").Run() +} + // PassthruCommand is similar to ForceStreamCommand in that it will issue all output // regardless of verbose mode. Further, this version of the command captures the // exit status of any executed command. This function is intended to simulate From 1d8682f7e9438f594323501840f538cc804b6fcc Mon Sep 17 00:00:00 2001 From: Adam Ross Date: Tue, 27 Feb 2018 09:12:46 -0800 Subject: [PATCH 3/9] Shouty errors - Highly verbose guidance when errors happen (#146) * Add SetVerbose method to logger. * Add expanded help output with --power-user suppression flag. * Fix whitespace in help.go --- cmd/main.go | 5 ++ commands/command.go | 5 +- util/help.go | 112 ++++++++++++++++++++++++++++++++++++++++++++ util/logger.go | 28 ++++++++--- 4 files changed, 143 insertions(+), 7 deletions(-) create mode 100644 util/help.go diff --git a/cmd/main.go b/cmd/main.go index cd61de4..4a5538b 100644 --- a/cmd/main.go +++ b/cmd/main.go @@ -36,6 +36,11 @@ func main() { Usage: "Disable all desktop notifications", EnvVar: "RIG_NOTIFY_QUIET", }, + cli.BoolFlag{ + Name: "power-user", + Usage: "Switch power-user mode on for quieter help output.", + EnvVar: "RIG_POWER_USER_MODE", + }, } app.Before = func(c *cli.Context) error { diff --git a/commands/command.go b/commands/command.go index f89e161..b2cdf1b 100644 --- a/commands/command.go +++ b/commands/command.go @@ -58,7 +58,10 @@ func (cmd *BaseCommand) Failure(message string, errorName string, exitCode int) cmd.out.NoSpin() // Handle error messaging. util.NotifyError(cmd.context, message) - + // Print expanded troubleshooting guidance. + if !cmd.context.GlobalBool("power-user") { + util.PrintDebugHelp(message, errorName, exitCode) + } return cli.NewExitError(fmt.Sprintf("ERROR: %s [%s] (%d)", message, errorName, exitCode), exitCode) } diff --git a/util/help.go b/util/help.go new file mode 100644 index 0000000..c500898 --- /dev/null +++ b/util/help.go @@ -0,0 +1,112 @@ +package util + +import ( + "bytes" + "fmt" + "os" + "runtime" + "strings" + + "github.com/fatih/color" +) + +// PrintDebugHelp provides expanded troubleshooting help content for an error. +// It is primarily called by command.go:Failure(). +// @todo consider switching this to a template. +func PrintDebugHelp(message, errorName string, exitCode int) { + header := color.New(color.FgYellow).Add(color.Underline).PrintlnFunc() + red := color.New(color.FgRed).PrintlnFunc() + code := color.New(color.BgHiBlack).PrintfFunc() + + header(StringPad(fmt.Sprintf("Error [%s]", errorName), " ", 80)) + fmt.Println() + red(color.RedString(message)) + + var codeMessage string + switch exitCode { + case 12: + codeMessage = "environmental" + case 13: + codeMessage = "external/upstream command" + default: + codeMessage = "general" + } + fmt.Println() + fmt.Printf("This is a %s error.\n", codeMessage) + fmt.Println() + + header(StringPad("Debugging Help", " ", 80)) + fmt.Println() + if !Logger().IsVerbose { + fmt.Println("Run again in verbose mode:") + fmt.Println() + line := fmt.Sprintf("%s --verbose %s", os.Args[0], strings.Join(os.Args[1:], " ")) + code("\t %s", StringPad("", " ", len(line)+1)) + fmt.Println() + code("\t %s", StringPad(line, " ", len(line)+1)) + fmt.Println() + code("\t %s", StringPad("", " ", len(line)+1)) + fmt.Println() + fmt.Println() + } + fmt.Println("Ask the doctor for a general health check:") + fmt.Println() + line := "rig doctor" + code("\t %s", StringPad("", " ", len(line)+1)) + fmt.Println() + code("\t %s", StringPad(line, " ", len(line)+1)) + fmt.Println() + code("\t %s", StringPad("", " ", len(line)+1)) + fmt.Println() + fmt.Println() + + header(StringPad("Get Support", " ", 80)) + fmt.Println() + fmt.Printf("To search for related issues or documentation use the error ID '%s'.\n", errorName) + fmt.Println() + fmt.Println("\tDocs:\t\thttp://docs.outrigger.sh") + fmt.Println("\tIssues:\t\thttps://github.com/phase2/rig/issues") + fmt.Println("\tChat:\t\thttp://slack.outrigger.sh/") + fmt.Println() + + header(StringPad("Your Environment Information", " ", 80)) + fmt.Println() + // Verbose output is distracting in this help output. + Logger().SetVerbose(false) + fmt.Println("\tOperating System:\t\t", runtime.GOOS) + fmt.Println("\tdocker version:\t\t\t", GetCurrentDockerVersion()) + fmt.Println("\tdocker client API version:\t", GetDockerClientAPIVersion()) + if version, err := GetDockerServerAPIVersion(); err == nil { + fmt.Println("\tdocker server API version:\t", version) + } else { + fmt.Println("\tdocker server API version:\t", err.Error()) + } + fmt.Println(color.CyanString("\nPlease include the 'Error' and 'Your Environment Information' sections in bug reports.")) + fmt.Println() + fmt.Println("To disable the extended troubleshooting output, run with --power-user or RIG_POWER_USER_MODE=1") + fmt.Println() +} + +// StringPad takes your string and returns it with the pad value repeatedly +// appended until it is the intended length. Note that if the delta between +// the initial string length and the intended size is not evenly divisible by +// the pad length, your final string could be slightly short -- partial padding +// is not applied. For guaranteed results, use a pad string of length 1. +func StringPad(s string, pad string, size int) string { + length := len(s) + if length < size { + var buffer bytes.Buffer + padLength := len(pad) + delta := size - length + iterations := delta / padLength + + buffer.WriteString(s) + for i := 0; i <= iterations; i += padLength { + buffer.WriteString(pad) + } + + return buffer.String() + } + + return s +} diff --git a/util/logger.go b/util/logger.go index 0fa30fb..72026cc 100644 --- a/util/logger.go +++ b/util/logger.go @@ -39,18 +39,13 @@ type RigSpinner struct { // LoggerInit initializes the global logger func LoggerInit(verbose bool) { - var verboseWriter = ioutil.Discard - if verbose { - verboseWriter = os.Stdout - } - s, _ := spun.NewSpinner(spun.Dots) logger = &RigLogger{ Channel: logChannels{ Info: log.New(os.Stdout, color.BlueString("[INFO] "), 0), Warning: log.New(os.Stdout, color.YellowString("[WARN] "), 0), Error: log.New(os.Stderr, color.RedString("[ERROR] "), 0), - Verbose: log.New(verboseWriter, "[VERBOSE] ", 0), + Verbose: deriveVerboseLogChannel(verbose), }, IsVerbose: verbose, Progress: &RigSpinner{s}, @@ -68,6 +63,27 @@ func Logger() *RigLogger { return logger } +// deriveVerboseLogChannel determines if and how verbose logs are used by +// creating the log channel they are routed through. This must be attached to +// a RigLogger as the value for Channel.Verbose. It is extracted into a function +// to support SetVerbose(). +func deriveVerboseLogChannel(verbose bool) *log.Logger { + verboseWriter := ioutil.Discard + if verbose { + verboseWriter = os.Stdout + } + return log.New(verboseWriter, "[VERBOSE] ", 0) +} + +// SetVerbose allows toggling verbose mode mid-execution of the program. +func (log *RigLogger) SetVerbose(verbose bool) { + if log.IsVerbose == verbose { + return + } + + log.Channel.Verbose = deriveVerboseLogChannel(verbose) +} + // Spin restarts the spinner for a new task. func (log *RigLogger) Spin(message string) { if !log.IsVerbose { From 4c1ef6b35271905665b7d6a8c49051dd1d1011e0 Mon Sep 17 00:00:00 2001 From: Frank Febbraro Date: Wed, 28 Feb 2018 08:55:52 -0800 Subject: [PATCH 4/9] Fixed prune with a confirmation, a force, and removed the spinner (#145) * Fixed prune with a confirmation, a force, and removed the spinner * added warning message and spinner * removed spinner for better output flow * Upgraded to golang 1.10, use dep official release, and added linter flag for consistent run execution --- Dockerfile | 7 ++++--- Gopkg.lock | 16 ++++++++-------- commands/prune.go | 16 +++++++++++----- docker-compose.yml | 2 +- 4 files changed, 24 insertions(+), 17 deletions(-) diff --git a/Dockerfile b/Dockerfile index 31a9ee6..b624d9d 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,7 +1,8 @@ -FROM golang:1.9-alpine +FROM golang:1.10-alpine RUN apk add --no-cache \ ca-certificates \ + curl \ git \ gcc \ libffi-dev \ @@ -12,8 +13,8 @@ RUN apk add --no-cache \ ruby-dev \ tar -RUN go get -u github.com/golang/dep/... \ - && go get -u github.com/alecthomas/gometalinter \ +RUN curl https://raw.githubusercontent.com/golang/dep/master/install.sh | sh +RUN go get -u github.com/alecthomas/gometalinter \ && go get -u github.com/goreleaser/goreleaser RUN gometalinter --install --update diff --git a/Gopkg.lock b/Gopkg.lock index 7045d6c..427548f 100644 --- a/Gopkg.lock +++ b/Gopkg.lock @@ -11,19 +11,19 @@ branch = "master" name = "github.com/deckarep/gosx-notifier" packages = ["."] - revision = "61a88900fb062ed8c63828adf4d7b58463b61f78" + revision = "e127226297fb751aa3b582db5e92361fcbfc5a6c" [[projects]] name = "github.com/fatih/color" packages = ["."] - revision = "570b54cabe6b8eb0bc2dfce68d964677d63b5260" - version = "v1.5.0" + revision = "507f6050b8568533fb3f5504de8e5205fa62a114" + version = "v1.6.0" [[projects]] branch = "master" name = "github.com/hashicorp/go-version" packages = ["."] - revision = "fc61389e27c71d120f87031ca8c88a3428f372dd" + revision = "4fe82ae3040f80a03d04d2cccb5606a626b8e1ee" [[projects]] branch = "master" @@ -35,7 +35,7 @@ branch = "master" name = "github.com/martinlindhe/notify" packages = ["."] - revision = "369b9400397a805575ad39e97cc4e7d493ea4335" + revision = "5013fd17b38d0637ca75445b55c892ab70f3e17d" [[projects]] name = "github.com/mattn/go-colorable" @@ -71,7 +71,7 @@ branch = "master" name = "golang.org/x/sys" packages = ["unix"] - revision = "92ac112afc6efd90284acda2b046fc0e351228f6" + revision = "f6cff0780e542efa0c8e864dc8fa522808f6a598" [[projects]] branch = "v1" @@ -80,10 +80,10 @@ revision = "b700e246b8b6d3e13554091e540e1019e26389f1" [[projects]] - branch = "v2" name = "gopkg.in/yaml.v2" packages = ["."] - revision = "eb3733d160e74a9c7e442f435eb3bea458e1d19f" + revision = "7f97868eec74b32b0982dd158a51a446d1da7eb5" + version = "v2.1.1" [solve-meta] analyzer-name = "dep" diff --git a/commands/prune.go b/commands/prune.go index 9cc5b3e..0624da6 100644 --- a/commands/prune.go +++ b/commands/prune.go @@ -26,11 +26,17 @@ func (cmd *Prune) Commands() []cli.Command { // Run executes the `rig prune` command func (cmd *Prune) Run(c *cli.Context) error { - cmd.out.Spin("Cleaning up unused Docker resources...") - /* #nosec */ - if exitCode := util.PassthruCommand(exec.Command("docker", "system", "prune", "--all", "--volumes")); exitCode != 0 { - return cmd.Failure("Failure pruning Docker resources.", "COMMAND-ERROR", 13) + + if util.AskYesNo("Are you sure you want to remove all unused containers, networks, images, caches, and volumes?") { + cmd.out.Info("Cleaning up unused Docker resources. This may take a while...") + /* #nosec */ + if exitCode := util.PassthruCommand(exec.Command("docker", "system", "prune", "--all", "--volumes", "--force")); exitCode != 0 { + return cmd.Failure("Failure pruning Docker resources.", "COMMAND-ERROR", 13) + } + cmd.out.Info("Unused Docker images, containers, volumes, and networks cleaned up.") + } else { + cmd.out.Warn("Cleanup aborted.") } - cmd.out.Info("Unused Docker images, containers, volumes, and networks cleaned up.") + return cmd.Success("") } diff --git a/docker-compose.yml b/docker-compose.yml index 2e7eb9a..bc72d6e 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -13,7 +13,7 @@ services: # Lint the codebase lint: extends: base - entrypoint: [ "gometalinter", "--vendor", "--config=gometalinter.json"] + entrypoint: [ "gometalinter", "--vendor", "--config=gometalinter.json", "--deadline=60s"] command: "./..." # Build rig, dropping off a working binary in build/darwin/rig. From 6e7566fda71f4c531c6d1cc004aa5e936e9631c6 Mon Sep 17 00:00:00 2001 From: Adam Ross Date: Wed, 28 Feb 2018 09:33:17 -0800 Subject: [PATCH 5/9] Add spinner support to rig project sync[:stop] (#141) * Retrofit spinner logging to file sync. * Add some more direction of what to do in the case of timeouts during sync start * Adjusting error message on sync failure to work better with spinner --- commands/project_sync.go | 49 ++++++++++++++++++++-------------------- util/logger.go | 8 +++++++ 2 files changed, 33 insertions(+), 24 deletions(-) diff --git a/commands/project_sync.go b/commands/project_sync.go index 8d01cbf..a85c1e0 100644 --- a/commands/project_sync.go +++ b/commands/project_sync.go @@ -49,8 +49,8 @@ func (cmd *ProjectSync) Commands() []cli.Command { Flags: []cli.Flag{ cli.IntFlag{ Name: "initial-sync-timeout", - Value: 60, - Usage: "Maximum amount of time in seconds to allow for detecting each of start of the Unison container and start of initial sync. (not needed on linux)", + Value: 120, + Usage: "Maximum amount of time in seconds to allow for detecting each of start of the Unison container and start of initial sync. If you encounter failures detecting initial sync increasing this value may help. Search for sync on http://docs.outrigger.sh/faq/troubleshooting/ (not needed on linux)", EnvVar: "RIG_PROJECT_SYNC_TIMEOUT", }, // Arbitrary sleep length but anything less than 3 wasn't catching @@ -119,20 +119,22 @@ func (cmd *ProjectSync) RunStart(ctx *cli.Context) error { // StartUnisonSync will create and launch the volumes and containers on systems that need/support Unison func (cmd *ProjectSync) StartUnisonSync(ctx *cli.Context, volumeName string, config *ProjectConfig, workingDir string) error { + cmd.out.Spin("Starting Outrigger Filesync (unison)...") + // Ensure the processes can handle a large number of watches if err := cmd.machine.SetSysctl("fs.inotify.max_user_watches", maxWatches); err != nil { cmd.Failure(fmt.Sprintf("Failure configuring file watches on Docker Machine: %v", err), "INOTIFY-WATCH-FAILURE", 12) } - cmd.out.Channel.Info.Printf("Starting sync volume: %s", volumeName) + cmd.out.Info("Starting sync volume: %s", volumeName) if err := util.Command("docker", "volume", "create", volumeName).Run(); err != nil { return cmd.Failure(fmt.Sprintf("Failed to create sync volume: %s", volumeName), "VOLUME-CREATE-FAILED", 13) } - - cmd.out.Info("Starting Unison container") + cmd.out.Info("Sync volume '%s' created", volumeName) + cmd.out.SpinWithVerbose(fmt.Sprintf("Starting sync container: %s (same name)", volumeName)) unisonMinorVersion := cmd.GetUnisonMinorVersion() - cmd.out.Channel.Verbose.Printf("Local Unison version for compatibilty: %s", unisonMinorVersion) + cmd.out.Verbose("Local Unison version for compatibilty: %s", unisonMinorVersion) util.Command("docker", "container", "stop", volumeName).Run() containerArgs := []string{ "container", "run", "--detach", "--rm", @@ -151,8 +153,8 @@ func (cmd *ProjectSync) StartUnisonSync(ctx *cli.Context, volumeName string, con if err != nil { return cmd.Failure(err.Error(), "SYNC-INIT-FAILED", 13) } - - cmd.out.Info("Initializing sync") + cmd.out.Info("Sync container '%s' started", volumeName) + cmd.out.SpinWithVerbose("Initializing file sync...") // Determine the location of the local Unison log file. var logFile = fmt.Sprintf("%s.log", volumeName) @@ -179,7 +181,7 @@ func (cmd *ProjectSync) StartUnisonSync(ctx *cli.Context, volumeName string, con unisonArgs = append(unisonArgs, "-ignore", ignore) } } - cmd.out.Verbose("Unison Args: %s", strings.Join(unisonArgs[:], " ")) + /* #nosec */ command := exec.Command("unison", unisonArgs...) command.Dir = workingDir @@ -197,7 +199,7 @@ func (cmd *ProjectSync) StartUnisonSync(ctx *cli.Context, volumeName string, con // SetupBindVolume will create minimal Docker Volumes for systems that have native container/volume support func (cmd *ProjectSync) SetupBindVolume(volumeName string, workingDir string) error { - cmd.out.Info("Starting local bind volume: %s", volumeName) + cmd.out.SpinWithVerbose("Starting local bind volume: %s", volumeName) util.Command("docker", "volume", "rm", volumeName).Run() volumeArgs := []string{ @@ -220,6 +222,7 @@ func (cmd *ProjectSync) RunStop(ctx *cli.Context) error { if runtime.GOOS == "linux" { return cmd.Success("No Unison container to stop, using local bind volume") } + cmd.out.Spin(fmt.Sprintf("Stopping Unison container")) cmd.Config = NewProjectConfig() if cmd.Config.NotEmpty() { @@ -238,7 +241,7 @@ func (cmd *ProjectSync) RunStop(ctx *cli.Context) error { return cmd.Failure(err.Error(), "SYNC-CONTAINER-FAILURE", 13) } - return cmd.Success(fmt.Sprintf("Unison container %s stopped", volumeName)) + return cmd.Success(fmt.Sprintf("Unison container '%s' stopped", volumeName)) } // GetVolumeName will find the volume name through a variety of fall backs @@ -283,7 +286,7 @@ func (cmd *ProjectSync) LoadComposeFile() (*ComposeFile, error) { // when compiled without -cgo this executable will not use the native mac dns resolution // which is how we have configured dnsdock to provide names for containers. func (cmd *ProjectSync) WaitForUnisonContainer(containerName string, timeoutSeconds int) (string, error) { - cmd.out.Info("Waiting for container to start") + cmd.out.SpinWithVerbose("Sync container '%s' started , waiting for unison server process...", containerName) var timeoutLoopSleep = time.Duration(100) * time.Millisecond // * 10 here because we loop once every 100 ms and we want to get to seconds @@ -303,7 +306,7 @@ func (cmd *ProjectSync) WaitForUnisonContainer(containerName string, timeoutSeco return ip, nil } - cmd.out.Info("Failure: %v", err) + cmd.out.SpinWithVerbose("Failure: %v", err) time.Sleep(timeoutLoopSleep) } @@ -313,12 +316,12 @@ func (cmd *ProjectSync) WaitForUnisonContainer(containerName string, timeoutSeco // WaitForSyncInit will wait for the local unison process to finish initializing // when the log file exists and has stopped growing in size func (cmd *ProjectSync) WaitForSyncInit(logFile string, workingDir string, timeoutSeconds int, syncWaitSeconds int) error { - cmd.out.Info("Waiting for initial sync detection") + cmd.out.SpinWithVerbose("Waiting for initial sync detection...") // The use of os.Stat below is not subject to our working directory configuration, // so to ensure we can stat the log file we convert it to an absolute path. if logFilePath, err := util.AbsJoin(workingDir, logFile); err != nil { - cmd.out.Info(err.Error()) + cmd.out.Error(err.Error()) } else { // Create a temp file to cause a sync action var tempFile = ".rig-check-sync-start" @@ -333,32 +336,29 @@ func (cmd *ProjectSync) WaitForSyncInit(logFile string, workingDir string, timeo var timeoutLoops = timeoutSeconds * 10 var statSleep = time.Duration(syncWaitSeconds) * time.Second for i := 1; i <= timeoutLoops; i++ { - if i%10 == 0 { - os.Stdout.WriteString(".") - } - statInfo, err := os.Stat(logFilePath) if err == nil { - os.Stdout.WriteString(" initial sync detected\n") + cmd.out.Info("Initial sync detected") - cmd.out.Info("Waiting for initial sync to finish") + cmd.out.SpinWithVerbose("Waiting for initial sync to finish") // Initialize at -2 to force at least one loop var lastSize = int64(-2) for lastSize != statInfo.Size() { - os.Stdout.WriteString(".") time.Sleep(statSleep) lastSize = statInfo.Size() if statInfo, err = os.Stat(logFilePath); err != nil { - cmd.out.Info(err.Error()) + cmd.out.Error(err.Error()) lastSize = -1 } } - os.Stdout.WriteString(" done\n") + // Remove the temp file, waiting until after sync so spurious // failure message doesn't show in log if err := util.RemoveFile(tempFile, workingDir); err != nil { cmd.out.Warning("Could not remove the temporary file: %s: %s", tempFile, err.Error()) } + + cmd.out.Info("File sync completed") return nil } @@ -373,6 +373,7 @@ func (cmd *ProjectSync) WaitForSyncInit(logFile string, workingDir string, timeo } } + cmd.out.Error("Initial sync detection failed, this could indicate a need to increase the initial-sync-timeout. See rig project sync --help") return fmt.Errorf("Failed to detect start of initial sync") } diff --git a/util/logger.go b/util/logger.go index 72026cc..0dccf56 100644 --- a/util/logger.go +++ b/util/logger.go @@ -92,6 +92,14 @@ func (log *RigLogger) Spin(message string) { } } +// SpinWithVerbose operates the spinner but also writes to the verbose log. +// This is used in cases where the spinner's initial context is needed for +// detailed verbose logging purposes. +func (log *RigLogger) SpinWithVerbose(message string, a ...interface{}) { + log.Spin(fmt.Sprintf(message, a...)) + log.Verbose(message, a...) +} + // NoSpin stops the Progress spinner. func (log *RigLogger) NoSpin() { log.Progress.Spins.Stop() From 806e0ee28d182ef8d469d2bcd832e0b4ec1ef4f4 Mon Sep 17 00:00:00 2001 From: Frank Febbraro Date: Fri, 2 Mar 2018 09:51:38 -0800 Subject: [PATCH 6/9] Upgraded travis config to go 1.10 (#151) * upgraded to go 1.10 * modernised the dep installation * Go 1.10 needs to be a string --- .travis.yml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.travis.yml b/.travis.yml index b9683e8..4119c6a 100644 --- a/.travis.yml +++ b/.travis.yml @@ -4,7 +4,7 @@ os: linux language: go go: - - 1.9 + - "1.10" - tip matrix: @@ -13,14 +13,14 @@ matrix: - go: tip install: - - "go get -u github.com/golang/dep/..." + - "curl https://raw.githubusercontent.com/golang/dep/master/install.sh | sh" - "go get -u github.com/alecthomas/gometalinter" - "gometalinter --install --update" - "dep ensure" script: - "scripts/test-go-fmt.sh" - - "gometalinter --vendor --config=gometalinter.json ./..." + - "gometalinter --vendor --deadline=60s --config=gometalinter.json ./..." - "go run cmd/main.go" notifications: From b8f97f8271a3471e66185aad7d4068ed4c2d6f78 Mon Sep 17 00:00:00 2001 From: Adam Ross Date: Fri, 2 Mar 2018 09:53:25 -0800 Subject: [PATCH 7/9] Spinner eats messages (#149) * Clean up spinner finalization. * Stop eating regular log messages alongside spinner usage. --- commands/command.go | 15 ++++++++------- commands/start.go | 4 +--- util/logger.go | 3 +++ 3 files changed, 12 insertions(+), 10 deletions(-) diff --git a/commands/command.go b/commands/command.go index b2cdf1b..70f5f0b 100644 --- a/commands/command.go +++ b/commands/command.go @@ -39,23 +39,24 @@ func (cmd *BaseCommand) Before(c *cli.Context) error { // Success encapsulates the functionality for reporting command success func (cmd *BaseCommand) Success(message string) error { - // Handle success messaging. + // Handle success messaging. If the spinner is running or not, this will + // output accordingly and issue a notification. if message != "" { cmd.out.Info(message) util.NotifySuccess(cmd.context, message) } - // If there is an active spinner wrap it up. This is not placed before the logging above so commands can rely on - // cmd.Success to set the last spinner status in lieu of an extraneous log entry. - cmd.out.NoSpin() - return nil } // Failure encapsulates the functionality for reporting command failure func (cmd *BaseCommand) Failure(message string, errorName string, exitCode int) error { - // Make sure any running spinner halts. - cmd.out.NoSpin() + // If the spinner is running, output something to get closure and shut it down. + if cmd.out.Spinning { + cmd.out.Error(message) + fmt.Println() + } + // Handle error messaging. util.NotifyError(cmd.context, message) // Print expanded troubleshooting guidance. diff --git a/commands/start.go b/commands/start.go index 7c4bcd9..a4150f8 100644 --- a/commands/start.go +++ b/commands/start.go @@ -89,11 +89,10 @@ func (cmd *Start) Run(c *cli.Context) error { cmd.out.Error("Docker could not be started") return cmd.Failure(err.Error(), "MACHINE-START-FAILED", 12) } - cmd.out.Info("Docker Machine (%s) Created", cmd.machine.Name) cmd.out.Verbose("Configuring the local Docker environment") cmd.machine.SetEnv() - cmd.out.Info("Docker Machine is ready") + cmd.out.Info("Docker Machine (%s) Created", cmd.machine.Name) dns := DNS{cmd.BaseCommand} dns.StartDNS(cmd.machine, c.String("nameservers")) @@ -148,7 +147,6 @@ func (cmd *Start) Run(c *cli.Context) error { cmd.out.Info("Run 'eval \"$(rig config)\"' to execute docker or docker-compose commands in your terminal.") return cmd.Success("Outrigger is ready to use") - } // StartMinimal will start "minimal" Outrigger operations, which refers to environments where diff --git a/util/logger.go b/util/logger.go index 0dccf56..91fcb71 100644 --- a/util/logger.go +++ b/util/logger.go @@ -113,6 +113,7 @@ func (log *RigLogger) Info(format string, a ...interface{}) { } else { log.Progress.Spins.SetMessage(fmt.Sprintf(format, a...)) log.Progress.Spins.Succeed() + log.Spinning = false } } @@ -123,6 +124,7 @@ func (log *RigLogger) Warning(format string, a ...interface{}) { } else { log.Progress.Spins.SetMessage(fmt.Sprintf(format, a...)) log.Progress.Spins.Warn() + log.Spinning = false } } @@ -138,6 +140,7 @@ func (log *RigLogger) Error(format string, a ...interface{}) { } else { log.Progress.Spins.SetMessage(fmt.Sprintf(format, a...)) log.Progress.Spins.Fail() + log.Spinning = false } } From 572c4159bee9654ad291e9c088c73755c5555610 Mon Sep 17 00:00:00 2001 From: Adam Ross Date: Fri, 2 Mar 2018 09:54:42 -0800 Subject: [PATCH 8/9] Developer Testing Commands (#150) * Clean up spinner finalization. * Add dev:win and dev:fail commands --- CONTRIBUTING.md | 6 ++++++ cmd/main.go | 1 + commands/command.go | 6 +++++- commands/dev.go | 50 +++++++++++++++++++++++++++++++++++++++++++++ util/help.go | 2 ++ 5 files changed, 64 insertions(+), 1 deletion(-) create mode 100644 commands/dev.go diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 447d48e..139a771 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -49,6 +49,12 @@ Here are a few conventions: return cmd.Failure(message) ``` +## Developer Testing Commands + +You can use `rig dev:win` or `rig dev:fail` as no-op commands to observe the +effects of a success or failure without external dependencies on the local +environment or side effects from "real" commands doing their job. + ## Development Environment Setup ### Developing with Docker diff --git a/cmd/main.go b/cmd/main.go index 4a5538b..853aa29 100644 --- a/cmd/main.go +++ b/cmd/main.go @@ -65,6 +65,7 @@ func main() { app.Commands = append(app.Commands, (&commands.Remove{}).Commands()...) app.Commands = append(app.Commands, (&commands.Project{}).Commands()...) app.Commands = append(app.Commands, (&commands.Doctor{}).Commands()...) + app.Commands = append(app.Commands, (&commands.Dev{}).Commands()...) app.Run(os.Args) } diff --git a/commands/command.go b/commands/command.go index 70f5f0b..f243d51 100644 --- a/commands/command.go +++ b/commands/command.go @@ -44,6 +44,11 @@ func (cmd *BaseCommand) Success(message string) error { if message != "" { cmd.out.Info(message) util.NotifySuccess(cmd.context, message) + } else { + // If there is an active spinner wrap it up. This is not placed before the + // logging above so commands can rely on cmd.Success to set the last spinner + // status in lieu of an extraneous log entry. + cmd.out.NoSpin() } return nil @@ -54,7 +59,6 @@ func (cmd *BaseCommand) Failure(message string, errorName string, exitCode int) // If the spinner is running, output something to get closure and shut it down. if cmd.out.Spinning { cmd.out.Error(message) - fmt.Println() } // Handle error messaging. diff --git a/commands/dev.go b/commands/dev.go new file mode 100644 index 0000000..9c13ce2 --- /dev/null +++ b/commands/dev.go @@ -0,0 +1,50 @@ +package commands + +import ( + "time" + + "github.com/urfave/cli" +) + +// Dev is the command for setting docker config to talk to a Docker Machine +type Dev struct { + BaseCommand +} + +// Commands returns the operations supported by this command +func (cmd *Dev) Commands() []cli.Command { + return []cli.Command{ + { + Name: "dev:win", + Usage: "A no-op command that will always succeed.", + Before: cmd.Before, + Action: cmd.RunSucceed, + Hidden: true, + }, + { + Name: "dev:fail", + Usage: "A no-op command that will always fail.", + Before: cmd.Before, + Action: cmd.RunFail, + Hidden: true, + }, + } +} + +// RunSucceed executes the `rig dev:succeed` command +func (cmd *Dev) RunSucceed(c *cli.Context) error { + cmd.out.Spin("Think positive...") + time.Sleep(3 * time.Second) + cmd.out.Info("We've got it.") + return cmd.Success("Positively successful!") +} + +// RunFail executes the `rig dev:fail` command +func (cmd *Dev) RunFail(c *cli.Context) error { + cmd.out.Spin("Abandon all hope...") + time.Sleep(3 * time.Second) + cmd.out.Warning("Hope slipping...") + cmd.out.Spin("Is the sky painted black?") + time.Sleep(3 * time.Second) + return cmd.Failure("Hope abandoned :(", "ABANDON-HOPE", 418) +} diff --git a/util/help.go b/util/help.go index c500898..add5b48 100644 --- a/util/help.go +++ b/util/help.go @@ -28,6 +28,8 @@ func PrintDebugHelp(message, errorName string, exitCode int) { codeMessage = "environmental" case 13: codeMessage = "external/upstream command" + case 418: + codeMessage = "rig developer test command" default: codeMessage = "general" } From 9bca3aad9341c1cf848a44d1f7d7bd4168ee82c4 Mon Sep 17 00:00:00 2001 From: Adam Ross Date: Fri, 2 Mar 2018 13:15:35 -0800 Subject: [PATCH 9/9] Sync spinner tweak and sync log reminder. (#152) --- commands/project_sync.go | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/commands/project_sync.go b/commands/project_sync.go index a85c1e0..b603255 100644 --- a/commands/project_sync.go +++ b/commands/project_sync.go @@ -126,7 +126,7 @@ func (cmd *ProjectSync) StartUnisonSync(ctx *cli.Context, volumeName string, con cmd.Failure(fmt.Sprintf("Failure configuring file watches on Docker Machine: %v", err), "INOTIFY-WATCH-FAILURE", 12) } - cmd.out.Info("Starting sync volume: %s", volumeName) + cmd.out.SpinWithVerbose("Starting sync volume: %s", volumeName) if err := util.Command("docker", "volume", "create", volumeName).Run(); err != nil { return cmd.Failure(fmt.Sprintf("Failed to create sync volume: %s", volumeName), "VOLUME-CREATE-FAILED", 13) } @@ -194,6 +194,8 @@ func (cmd *ProjectSync) StartUnisonSync(ctx *cli.Context, volumeName string, con return cmd.Failure(err.Error(), "UNISON-SYNC-FAILED", 13) } + cmd.out.Info("Watch unison process activities in the sync log: %s", logFile) + return cmd.Success("Unison sync started successfully") }