From 573d6b236e40dbc0c865cab32c9c643e1970cf56 Mon Sep 17 00:00:00 2001 From: Daniel Wedul <72031080+dwedul-figure@users.noreply.github.com> Date: Mon, 18 Oct 2021 09:32:24 -0600 Subject: [PATCH] feat: Add cosmovisor run command (#10316) * [10285]: Move the Run function into a new cmd/run.go and have main call the renamed/updated RunCosmovisorCommand function. * [10285]: Shorten the run warning to a single line and log it at the start and end of a run. * [10285]: Make the version.go funcs public. Allow for --version. Limit help request to just the first argument. * [10285]: Create a LogConfigOrError that logs the config or config errors in a standard way. * [10285]: Output config info/errors with the help command. * [10285]: Update the Run command to use the new config/error logging. * [10285]: Only provide the first arg to the cmd tester funcs, since that's all that really matters. * [10285]: Tweak a function comment. * [10285]: For the version command, run the binary with version too. * [10285]: Trim whitespace from the first argument before checking if it's any commands. * [10285]: Update and add some unit tests. * [10285]: In GetConfigFromEnv, make sure the cfg isn't null before trying to use it. * [10285]: Add some unit tests for LogConfigOrError. * [10285]: Allow for the poll interval to be defined as a duration. * [10285]: Add changelog line. * [10285]: Update the README to reflect the addition of the run action. * [10285]: slight tweak to changlog. * [10285]: Add a couple lines to the help text about getting help from the configured binary. * [10285]: Remove a println from a unit test. * [10285]: Put the help command first in the README. Co-authored-by: Robert Zaremba --- cosmovisor/CHANGELOG.md | 8 ++ cosmovisor/README.md | 38 +++---- cosmovisor/args.go | 39 ++++++-- cosmovisor/args_test.go | 98 +++++++++++++++++++ cosmovisor/cmd/cosmovisor/cmd/help.go | 34 ++++--- cosmovisor/cmd/cosmovisor/cmd/help_test.go | 92 +++++++---------- cosmovisor/cmd/cosmovisor/cmd/root.go | 45 +++++++-- cosmovisor/cmd/cosmovisor/cmd/run.go | 40 ++++++++ cosmovisor/cmd/cosmovisor/cmd/run_test.go | 76 ++++++++++++++ cosmovisor/cmd/cosmovisor/cmd/version.go | 14 ++- cosmovisor/cmd/cosmovisor/cmd/version_test.go | 97 +++++++++++++----- cosmovisor/cmd/cosmovisor/main.go | 40 +------- 12 files changed, 442 insertions(+), 179 deletions(-) create mode 100644 cosmovisor/cmd/cosmovisor/cmd/run.go create mode 100644 cosmovisor/cmd/cosmovisor/cmd/run_test.go diff --git a/cosmovisor/CHANGELOG.md b/cosmovisor/CHANGELOG.md index a5b298b1b9..91d0e9ffda 100644 --- a/cosmovisor/CHANGELOG.md +++ b/cosmovisor/CHANGELOG.md @@ -36,6 +36,14 @@ Ref: https://keepachangelog.com/en/1.0.0/ ## [Unreleased] +### Features + ++ [\#10285](https://github.com/cosmos/cosmos-sdk/pull/10316) Added `run` action. + +### Deprecated + ++ [\#10285](https://github.com/cosmos/cosmos-sdk/pull/10316) Running `cosmovisor` without the `run` argument. + ## v1.0.0 2021-09-30 ### Features diff --git a/cosmovisor/README.md b/cosmovisor/README.md index afb7f0fe2e..b6e3b3e8ef 100644 --- a/cosmovisor/README.md +++ b/cosmovisor/README.md @@ -4,9 +4,9 @@ #### Design -Cosmovisor is designed to be used as a wrapper for an `Cosmos SDK` app: -* it will pass all arguments to the associated app (configured by `DAEMON_NAME` env variable). - Running `cosmovisor arg1 arg2 ....` will run `app arg1 arg2 ...`; +Cosmovisor is designed to be used as a wrapper for a `Cosmos SDK` app: +* it will pass arguments to the associated app (configured by `DAEMON_NAME` env variable). + Running `cosmovisor run arg1 arg2 ....` will run `app arg1 arg2 ...`; * it will manage an app by restarting and upgrading if needed; * it is configured using environment variables, not positional arguments. @@ -34,7 +34,14 @@ go install github.com/cosmos/cosmos-sdk/cosmovisor/cmd/cosmovisor@latest ### Command Line Arguments And Environment Variables -All arguments passed to `cosmovisor` will be passed to the application binary (as a subprocess). `cosmovisor` will return `/dev/stdout` and `/dev/stderr` of the subprocess as its own. For this reason, `cosmovisor` cannot accept any command-line arguments other than those available to the application binary, nor will it print anything to output other than what is printed by the application binary. +The first argument passed to `cosmovisor` is the action for `cosmovisor` to take. Options are: +* `help`, `--help`, or `-h` - Output `cosmovisor` help information and check your `cosmovisor` configuration. +* `run` - Run the configured binary using the rest of the provided arguments. +* `version`, or `--version` - Output the `cosmovisor` version and also run the binary with the `version` argument. + +All arguments passed to `cosmovisor run` will be passed to the application binary (as a subprocess). `cosmovisor` will return `/dev/stdout` and `/dev/stderr` of the subprocess as its own. For this reason, `cosmovisor run` cannot accept any command-line arguments other than those available to the application binary. + +*Note: Use of `cosmovisor` without one of the action arguments is deprecated. For backwards compatability, if the first argument is not an action argument, `run` is assumed. However, this fallback might be removed in future versions, so it is recommended that you always provide `run`. `cosmovisor` reads its configuration from environment variables: @@ -42,11 +49,10 @@ All arguments passed to `cosmovisor` will be passed to the application binary (a * `DAEMON_NAME` is the name of the binary itself (e.g. `gaiad`, `regend`, `simd`, etc.). * `DAEMON_ALLOW_DOWNLOAD_BINARIES` (*optional*), if set to `true`, will enable auto-downloading of new binaries (for security reasons, this is intended for full nodes rather than validators). By default, `cosmovisor` will not auto-download new binaries. * `DAEMON_RESTART_AFTER_UPGRADE` (*optional*, default = `true`), if `true`, restarts the subprocess with the same command-line arguments and flags (but with the new binary) after a successful upgrade. Otherwise (`false`), `cosmovisor` stops running after an upgrade and requires the system administrator to manually restart it. Note restart is only after the upgrade and does not auto-restart the subprocess after an error occurs. -* `DAEMON_POLL_INTERVAL` is the interval length in milliseconds for polling the upgrade plan file. Default: 300. -* `UNSAFE_SKIP_BACKUP` (defaults to `false`), if set to `false`, backs up the data before trying the upgrade. Otherwise (`true`), upgrades directly without performing a backup. The default value of false is useful and recommended in case of failures and when a backup needed to rollback. We recommend using the default backup option `UNSAFE_SKIP_BACKUP=false`. +* `DAEMON_POLL_INTERVAL` is the interval length for polling the upgrade plan file. The value can either be a number (in milliseconds) or a duration (e.g. `1s`). Default: 300 milliseconds. +* `UNSAFE_SKIP_BACKUP` (defaults to `false`), if set to `true`, upgrades directly without performing a backup. Otherwise (`false`, default) backs up the data before trying the upgrade. The default value of false is useful and recommended in case of failures and when a backup needed to rollback. We recommend using the default backup option `UNSAFE_SKIP_BACKUP=false`. * `DAEMON_PREUPGRADE_MAX_RETRIES` (defaults to `0`). The maximum number of times to call `pre-upgrade` in the application after exit status of `31`. After the maximum number of retries, cosmovisor fails the upgrade. - ### Folder Layout `$DAEMON_HOME/cosmovisor` is expected to belong completely to `cosmovisor` and the subprocesses that are controlled by it. The folder content is organized as follows: @@ -91,22 +97,6 @@ In order to support downloadable binaries, a tarball for each upgrade binary wil The `DAEMON` specific code and operations (e.g. tendermint config, the application db, syncing blocks, etc.) all work as expected. The application binaries' directives such as command-line flags and environment variables also work as expected. -### Commands - -Because Cosmovisor is meant to be used as a wrapper for a Cosmos SDK application, it does not require many commands. - -To determine the version of Cosmovisor, run the following command: -``` -cosmovisor version -``` -The output of the `cosmovisor version` command shows the version of the Cosmos SDK application and the version of Cosmovisor: - -``` -Cosmovisor Version: v0.1.0-85-g65baacac0 -0.43.0-beta1-319-ge3aec1840 -``` - - ### Detecting Upgrades `cosmovisor` is polling the `$DAEMON_HOME/data/upgrade-info.json` file for new upgrade instructions. The file is created by the x/upgrade module in `BeginBlocker` when an upgrade is detected and the blockchain reaches the upgrade height. @@ -278,7 +268,7 @@ cp ./build/simd $DAEMON_HOME/cosmovisor/upgrades/test1/bin Start `cosmosvisor`: ``` -cosmovisor start +cosmovisor run start ``` Open a new terminal window and submit an upgrade proposal along with a deposit and a vote (these commands must be run within 20 seconds of each other): diff --git a/cosmovisor/args.go b/cosmovisor/args.go index c0c75996c3..0fbd2bda9a 100644 --- a/cosmovisor/args.go +++ b/cosmovisor/args.go @@ -11,6 +11,8 @@ import ( "strings" "time" + "github.com/rs/zerolog" + cverrors "github.com/cosmos/cosmos-sdk/cosmovisor/errors" ) @@ -143,13 +145,18 @@ func GetConfigFromEnv() (*Config, error) { interval := os.Getenv(EnvInterval) if interval != "" { - switch i, e := strconv.ParseUint(interval, 10, 32); { - case e != nil: - errs = append(errs, fmt.Errorf("invalid %s: %w", EnvInterval, err)) - case i == 0: - errs = append(errs, fmt.Errorf("invalid %s: cannot be 0", EnvInterval)) - default: - cfg.PollInterval = time.Millisecond * time.Duration(i) + var intervalUInt uint64 + intervalUInt, err = strconv.ParseUint(interval, 10, 32) + if err == nil { + cfg.PollInterval = time.Millisecond * time.Duration(intervalUInt) + } else { + cfg.PollInterval, err = time.ParseDuration(interval) + } + switch { + case err != nil: + errs = append(errs, fmt.Errorf("invalid %s: could not parse \"%s\" into either a duration or uint (milliseconds)", EnvInterval, interval)) + case cfg.PollInterval <= 0: + errs = append(errs, fmt.Errorf("invalid %s: must be greater than 0", EnvInterval)) } } else { cfg.PollInterval = 300 * time.Millisecond @@ -168,6 +175,24 @@ func GetConfigFromEnv() (*Config, error) { return cfg, nil } +// LogConfigOrError logs either the config details or the error. +func LogConfigOrError(logger zerolog.Logger, cfg *Config, cerr error) { + switch { + case cerr != nil: + switch err := cerr.(type) { + case *cverrors.MultiError: + logger.Error().Msg("multiple configuration errors found:") + for i, e := range err.GetErrors() { + logger.Error().Err(e).Msg(fmt.Sprintf(" %d:", i+1)) + } + default: + logger.Error().Err(cerr).Msg("configuration error:") + } + case cfg != nil: + logger.Info().Msg("Configuration is valid:\n" + cfg.DetailString()) + } +} + // validate returns an error if this config is invalid. // it enforces Home/cosmovisor is a valid directory and exists, // and that Name is set diff --git a/cosmovisor/args_test.go b/cosmovisor/args_test.go index 564c7477db..86c0d0df1c 100644 --- a/cosmovisor/args_test.go +++ b/cosmovisor/args_test.go @@ -1,12 +1,15 @@ package cosmovisor import ( + "bytes" "fmt" + "io" "os" "path/filepath" "testing" "time" + "github.com/rs/zerolog" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "github.com/stretchr/testify/suite" @@ -451,6 +454,18 @@ func (s *argsTestSuite) TestGetConfigFromEnv() { expectedCfg: newConfig(absPath, "testname", false, false, false, 987, 1), expectedErrCount: 0, }, + { + name: "poll interval 1s", + envVals: cosmovisorEnv{absPath, "testname", "false", "false", "false", "1s", "1"}, + expectedCfg: newConfig(absPath, "testname", false, false, false, 1000, 1), + expectedErrCount: 0, + }, + { + name: "poll interval -3m", + envVals: cosmovisorEnv{absPath, "testname", "false", "false", "false", "-3m", "1"}, + expectedCfg: nil, + expectedErrCount: 1, + }, // EnvHome, EnvName, EnvDownloadBin, EnvRestartUpgrade, EnvSkipBackup, EnvInterval, EnvPreupgradeMaxRetries { name: "prepupgrade max retries bad", @@ -497,3 +512,86 @@ func (s *argsTestSuite) TestGetConfigFromEnv() { }) } } + +func (s *argsTestSuite) TestLogConfigOrError() { + cfg := &Config{ + Home: "/no/place/like/it", + Name: "cosmotestvisor", + AllowDownloadBinaries: true, + RestartAfterUpgrade: true, + PollInterval: 999, + UnsafeSkipBackup: false, + PreupgradeMaxRetries: 20, + } + errNormal := fmt.Errorf("this is a single error") + errs := []error{ + fmt.Errorf("multi-error error 1"), + fmt.Errorf("multi-error error 2"), + fmt.Errorf("multi-error error 3"), + } + errMulti := errors.FlattenErrors(errs...) + + makeTestLogger := func(testName string, out io.Writer) zerolog.Logger { + output := zerolog.ConsoleWriter{Out: out, TimeFormat: time.Kitchen, NoColor: true} + return zerolog.New(output).With().Str("test", testName).Timestamp().Logger() + } + + tests := []struct { + name string + cfg *Config + err error + contains []string + notcontains []string + }{ + { + name: "normal error", + cfg: nil, + err: errNormal, + contains: []string{"configuration error", errNormal.Error()}, // TODO: Fix this. + notcontains: nil, + }, + { + name: "multi error", + cfg: nil, + err: errMulti, + contains: []string{"multiple configuration errors found", errs[0].Error(), errs[1].Error(), errs[2].Error()}, + notcontains: nil, + }, + { + name: "config", + cfg: cfg, + err: nil, + contains: []string{"Configuration is valid", cfg.DetailString()}, + notcontains: nil, + }, + { + name: "error and config - no config details", + cfg: cfg, + err: errNormal, + contains: []string{"error"}, + notcontains: []string{"Configuration is valid", EnvName, cfg.Home}, // Just some spot checks. + }, + { + name: "nil nil - no output", + cfg: nil, + err: nil, + contains: nil, + notcontains: []string{" "}, + }, + } + + for _, tc := range tests { + s.T().Run(tc.name, func(t *testing.T) { + var b bytes.Buffer + logger := makeTestLogger(tc.name, &b) + LogConfigOrError(logger, tc.cfg, tc.err) + output := b.String() + for _, expected := range tc.contains { + assert.Contains(t, output, expected) + } + for _, unexpected := range tc.notcontains { + assert.NotContains(t, output, unexpected) + } + }) + } +} diff --git a/cosmovisor/cmd/cosmovisor/cmd/help.go b/cosmovisor/cmd/cosmovisor/cmd/help.go index 460c21d7e2..abb757dd91 100644 --- a/cosmovisor/cmd/cosmovisor/cmd/help.go +++ b/cosmovisor/cmd/cosmovisor/cmd/help.go @@ -3,33 +3,34 @@ package cmd import ( "fmt" "os" - "strings" + "time" + + "github.com/rs/zerolog" "github.com/cosmos/cosmos-sdk/cosmovisor" ) +// HelpArgs are the strings that indicate a cosmovisor help command. +var HelpArgs = []string{"help", "--help", "-h"} + // ShouldGiveHelp checks the env and provided args to see if help is needed or being requested. // Help is needed if either cosmovisor.EnvName and/or cosmovisor.EnvHome env vars aren't set. -// Help is requested if any args are "help", "--help", or "-h". -func ShouldGiveHelp(args []string) bool { - if len(os.Getenv(cosmovisor.EnvName)) == 0 || len(os.Getenv(cosmovisor.EnvHome)) == 0 { - return true - } - if len(args) == 0 { - return false - } - for _, arg := range args { - if strings.EqualFold(arg, "help") || strings.EqualFold(arg, "--help") || strings.EqualFold(arg, "-h") { - return true - } - } - return false +// Help is requested if the first arg is "help", "--help", or "-h". +func ShouldGiveHelp(arg string) bool { + return isOneOf(arg, HelpArgs) || len(os.Getenv(cosmovisor.EnvName)) == 0 || len(os.Getenv(cosmovisor.EnvHome)) == 0 } // DoHelp outputs help text func DoHelp() { // Not using the logger for this output because the header and footer look weird for help text. fmt.Println(GetHelpText()) + // Check the config and output details or any errors. + // Not using the cosmovisor.Logger in order to ignore any level it might have set, + // and also to not have any of the extra parameters in the output. + output := zerolog.ConsoleWriter{Out: os.Stdout, TimeFormat: time.Kitchen} + logger := zerolog.New(output).With().Timestamp().Logger() + cfg, err := cosmovisor.GetConfigFromEnv() + cosmovisor.LogConfigOrError(logger, cfg, err) } // GetHelpText creates the help text multi-line string. @@ -45,5 +46,8 @@ and restart the App. Configuration of Cosmovisor is done through environment variables, which are documented in: https://github.com/cosmos/cosmos-sdk/tree/master/cosmovisor/README.md + +To get help for the configured binary: + cosmovisor run help `, cosmovisor.EnvName, cosmovisor.EnvHome) } diff --git a/cosmovisor/cmd/cosmovisor/cmd/help_test.go b/cosmovisor/cmd/cosmovisor/cmd/help_test.go index 91a152e4d9..6366957019 100644 --- a/cosmovisor/cmd/cosmovisor/cmd/help_test.go +++ b/cosmovisor/cmd/cosmovisor/cmd/help_test.go @@ -170,13 +170,13 @@ func (s *HelpTestSuite) TestShouldGiveHelpEnvVars() { s.T().Run(tc.name, func(t *testing.T) { prepEnv(t, cosmovisor.EnvHome, tc.envHome) prepEnv(t, cosmovisor.EnvName, tc.envName) - actual := ShouldGiveHelp(nil) + actual := ShouldGiveHelp("not-a-help-arg") assert.Equal(t, tc.expected, actual) }) } } -func (s HelpTestSuite) TestShouldGiveHelpArgs() { +func (s HelpTestSuite) TestShouldGiveHelpArg() { initialEnv := s.clearEnv() defer s.setEnv(nil, initialEnv) @@ -184,79 +184,59 @@ func (s HelpTestSuite) TestShouldGiveHelpArgs() { tests := []struct { name string - args []string + arg string expected bool }{ { - name: "nil args", - args: nil, + name: "empty string", + arg: "", expected: false, }, { - name: "empty args", - args: []string{}, + name: "random", + arg: "random", expected: false, }, { - name: "one arg random", - args: []string{"random"}, + name: "help", + arg: "help", + expected: true, + }, + { + name: "-h", + arg: "-h", + expected: true, + }, + { + name: "--help", + arg: "--help", + expected: true, + }, + { + name: "help weird casing", + arg: "hELP", + expected: true, + }, + { + name: "version", + arg: "version", expected: false, }, { - name: "five args random", - args: []string{"random1", "--random2", "-r", "random4", "-random5"}, + name: "--version", + arg: "--version", expected: false, }, { - name: "one arg help", - args: []string{"help"}, - expected: true, - }, - { - name: " two args help first", - args: []string{"help", "arg2"}, - expected: true, - }, - { - name: "two args help second", - args: []string{"arg1", "help"}, - expected: true, - }, - { - name: "one arg -h", - args: []string{"-h"}, - expected: true, - }, - { - name: "two args -h first", - args: []string{"-h", "arg2"}, - expected: true, - }, - { - name: "two args -h second", - args: []string{"arg1", "-h"}, - expected: true, - }, - { - name: "one arg --help", - args: []string{"--help"}, - expected: true, - }, - { - name: "two args --help first", - args: []string{"--help", "arg2"}, - expected: true, - }, - { - name: "two args --help second", - args: []string{"arg1", "--help"}, - expected: true, + name: "run", + arg: "run", + expected: false, }, } for _, tc := range tests { - s.T().Run(tc.name, func(t *testing.T) { - actual := ShouldGiveHelp(tc.args) + s.T().Run(fmt.Sprintf("%s - %t", tc.name, tc.expected), func(t *testing.T) { + actual := ShouldGiveHelp(tc.arg) assert.Equal(t, tc.expected, actual) }) } diff --git a/cosmovisor/cmd/cosmovisor/cmd/root.go b/cosmovisor/cmd/cosmovisor/cmd/root.go index 3bea433f20..a4d3f2b2f1 100644 --- a/cosmovisor/cmd/cosmovisor/cmd/root.go +++ b/cosmovisor/cmd/cosmovisor/cmd/root.go @@ -1,12 +1,41 @@ package cmd -// RunCosmovisorCommands executes cosmosvisor commands e.g `cosmovisor version` -// Returned boolean is whether or not execution should continue. -func RunCosmovisorCommands(args []string) { - switch { - case ShouldGiveHelp(args): - DoHelp() - case isVersionCommand(args): - printVersion() +import ( + "strings" + + "github.com/cosmos/cosmos-sdk/cosmovisor" +) + +// RunCosmovisorCommand executes the desired cosmovisor command. +func RunCosmovisorCommand(args []string) error { + arg0 := "" + if len(args) > 0 { + arg0 = strings.TrimSpace(args[0]) } + switch { + case ShouldGiveHelp(arg0): + DoHelp() + return nil + case IsVersionCommand(arg0): + PrintVersion() + return Run([]string{"version"}) + case IsRunCommand(arg0): + return Run(args[1:]) + } + warnRun := func() { + cosmovisor.Logger.Warn().Msg("Use of cosmovisor without the 'run' command is deprecated. Use: cosmovisor run [args]") + } + warnRun() + defer warnRun() + return Run(args) +} + +// isOneOf returns true if the given arg equals one of the provided options (ignoring case). +func isOneOf(arg string, options []string) bool { + for _, opt := range options { + if strings.EqualFold(arg, opt) { + return true + } + } + return false } diff --git a/cosmovisor/cmd/cosmovisor/cmd/run.go b/cosmovisor/cmd/cosmovisor/cmd/run.go new file mode 100644 index 0000000000..428aa53917 --- /dev/null +++ b/cosmovisor/cmd/cosmovisor/cmd/run.go @@ -0,0 +1,40 @@ +package cmd + +import ( + "os" + + "github.com/cosmos/cosmos-sdk/cosmovisor" +) + +// RunArgs are the strings that indicate a cosmovisor run command. +var RunArgs = []string{"run"} + +// IsRunCommand checks if the given args indicate that a run is desired. +func IsRunCommand(arg string) bool { + return isOneOf(arg, RunArgs) +} + +// Run runs the configured program with the given args and monitors it for upgrades. +func Run(args []string) error { + cfg, cerr := cosmovisor.GetConfigFromEnv() + cosmovisor.LogConfigOrError(cosmovisor.Logger, cfg, cerr) + if cerr != nil { + return cerr + } + launcher, err := cosmovisor.NewLauncher(cfg) + if err != nil { + return err + } + + doUpgrade, err := launcher.Run(args, os.Stdout, os.Stderr) + // if RestartAfterUpgrade, we launch after a successful upgrade (only condition LaunchProcess returns nil) + for cfg.RestartAfterUpgrade && err == nil && doUpgrade { + cosmovisor.Logger.Info().Str("app", cfg.Name).Msg("upgrade detected, relaunching") + doUpgrade, err = launcher.Run(args, os.Stdout, os.Stderr) + } + if doUpgrade && err == nil { + cosmovisor.Logger.Info().Msg("upgrade detected, DAEMON_RESTART_AFTER_UPGRADE is off. Verify new upgrade and start cosmovisor again.") + } + + return err +} diff --git a/cosmovisor/cmd/cosmovisor/cmd/run_test.go b/cosmovisor/cmd/cosmovisor/cmd/run_test.go new file mode 100644 index 0000000000..b54b38fd8f --- /dev/null +++ b/cosmovisor/cmd/cosmovisor/cmd/run_test.go @@ -0,0 +1,76 @@ +package cmd + +import ( + "fmt" + "testing" + + "github.com/stretchr/testify/require" +) + +func TestIsRunCommand(t *testing.T) { + cases := []struct { + name string + arg string + expected bool + }{ + { + name: "empty string", + arg: "", + expected: false, + }, + { + name: "random", + arg: "random", + expected: false, + }, + { + name: "run", + arg: "run", + expected: true, + }, + { + name: "run weird casing", + arg: "RUn", + expected: true, + }, + { + name: "--run", + arg: "--run", + expected: false, + }, + { + name: "help", + arg: "help", + expected: false, + }, + { + name: "-h", + arg: "-h", + expected: false, + }, + { + name: "--help", + arg: "--help", + expected: false, + }, + { + name: "version", + arg: "version", + expected: false, + }, + { + name: "--version", + arg: "--version", + expected: false, + }, + } + + for _, tc := range cases { + t.Run(fmt.Sprintf("%s - %t", tc.name, tc.expected), func(t *testing.T) { + actual := IsRunCommand(tc.arg) + require.Equal(t, tc.expected, actual) + }) + } +} + +// TODO: Write tests for func Run(args []string) error diff --git a/cosmovisor/cmd/cosmovisor/cmd/version.go b/cosmovisor/cmd/cosmovisor/cmd/version.go index ad60a006f1..11c6ee75e5 100644 --- a/cosmovisor/cmd/cosmovisor/cmd/version.go +++ b/cosmovisor/cmd/cosmovisor/cmd/version.go @@ -2,14 +2,20 @@ package cmd import ( "fmt" - "strings" ) // Version represents Cosmovisor version value. Set during build var Version string -func isVersionCommand(args []string) bool { - return len(args) == 1 && strings.EqualFold(args[0], "version") +// VersionArgs is the strings that indicate a cosmovisor version command. +var VersionArgs = []string{"version", "--version"} + +// IsVersionCommand checks if the given args indicate that the version is being requested. +func IsVersionCommand(arg string) bool { + return isOneOf(arg, VersionArgs) } -func printVersion() { fmt.Println("Cosmovisor Version: ", Version) } +// PrintVersion prints the cosmovisor version. +func PrintVersion() { + fmt.Println("Cosmovisor Version: ", Version) +} diff --git a/cosmovisor/cmd/cosmovisor/cmd/version_test.go b/cosmovisor/cmd/cosmovisor/cmd/version_test.go index 2804b3a3ba..6c842494e8 100644 --- a/cosmovisor/cmd/cosmovisor/cmd/version_test.go +++ b/cosmovisor/cmd/cosmovisor/cmd/version_test.go @@ -1,6 +1,7 @@ package cmd import ( + "fmt" "testing" "github.com/stretchr/testify/require" @@ -8,33 +9,77 @@ import ( func TestIsVersionCommand(t *testing.T) { cases := []struct { - name string - args []string - expectRes bool - }{{ - name: "valid args - lowercase", - args: []string{"version"}, - expectRes: true, - }, { - name: "typo", - args: []string{"vrsion"}, - expectRes: false, - }, { - name: "non version command", - args: []string{"start"}, - expectRes: false, - }, { - name: "incorrect format", - args: []string{"start", "version"}, - expectRes: false, - }} + name string + arg string + expected bool + }{ + { + name: "empty string", + arg: "", + expected: false, + }, + { + name: "random", + arg: "random", + expected: false, + }, + { + name: "version", + arg: "version", + expected: true, + }, + { + name: "--version", + arg: "--version", + expected: true, + }, + { + name: "version weird casing", + arg: "veRSiOn", + expected: true, + }, + { + // -v should be reserved for verbose, and should not be used for --version. + name: "-v", + arg: "-v", + expected: false, + }, + { + name: "typo", + arg: "vrsion", + expected: false, + }, + { + name: "non version command", + arg: "start", + expected: false, + }, + { + name: "help", + arg: "help", + expected: false, + }, + { + name: "-h", + arg: "-h", + expected: false, + }, + { + name: "--help", + arg: "--help", + expected: false, + }, + { + name: "run", + arg: "run", + expected: false, + }, + } - for i := range cases { - tc := cases[i] - t.Run(tc.name, func(t *testing.T) { - require := require.New(t) - res := isVersionCommand(tc.args) - require.Equal(tc.expectRes, res) + for _, tc := range cases { + t.Run(fmt.Sprintf("%s - %t", tc.name, tc.expected), func(t *testing.T) { + actual := IsVersionCommand(tc.arg) + require.Equal(t, tc.expected, actual) }) } } diff --git a/cosmovisor/cmd/cosmovisor/main.go b/cosmovisor/cmd/cosmovisor/main.go index 07de6d0123..4d82805be2 100644 --- a/cosmovisor/cmd/cosmovisor/main.go +++ b/cosmovisor/cmd/cosmovisor/main.go @@ -1,54 +1,16 @@ package main import ( - "fmt" "os" "github.com/cosmos/cosmos-sdk/cosmovisor" "github.com/cosmos/cosmos-sdk/cosmovisor/cmd/cosmovisor/cmd" - "github.com/cosmos/cosmos-sdk/cosmovisor/errors" ) func main() { cosmovisor.SetupLogging() - if err := Run(os.Args[1:]); err != nil { + if err := cmd.RunCosmovisorCommand(os.Args[1:]); err != nil { cosmovisor.Logger.Error().Err(err).Msg("") os.Exit(1) } } - -// Run is the main loop, but returns an error -func Run(args []string) error { - cmd.RunCosmovisorCommands(args) - - cfg, cerr := cosmovisor.GetConfigFromEnv() - if cerr != nil { - switch err := cerr.(type) { - case *errors.MultiError: - cosmovisor.Logger.Error().Msg("multiple configuration errors found:") - for i, e := range err.GetErrors() { - cosmovisor.Logger.Error().Err(e).Msg(fmt.Sprintf(" %d:", i+1)) - } - default: - cosmovisor.Logger.Error().Err(err).Msg("configuration error:") - } - return cerr - } - cosmovisor.Logger.Info().Msg("Configuration is valid:\n" + cfg.DetailString()) - launcher, err := cosmovisor.NewLauncher(cfg) - if err != nil { - return err - } - - doUpgrade, err := launcher.Run(args, os.Stdout, os.Stderr) - // if RestartAfterUpgrade, we launch after a successful upgrade (only condition LaunchProcess returns nil) - for cfg.RestartAfterUpgrade && err == nil && doUpgrade { - cosmovisor.Logger.Info().Str("app", cfg.Name).Msg("upgrade detected, relaunching") - doUpgrade, err = launcher.Run(args, os.Stdout, os.Stderr) - } - if doUpgrade && err == nil { - cosmovisor.Logger.Info().Msg("upgrade detected, DAEMON_RESTART_AFTER_UPGRADE is off. Verify new upgrade and start cosmovisor again.") - } - - return err -}