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 <robert@zaremba.ch>
This commit is contained in:
parent
99e2e0f060
commit
573d6b236e
|
@ -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
|
||||
|
|
|
@ -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):
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
})
|
||||
}
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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
|
||||
}
|
|
@ -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
|
|
@ -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)
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue